Text Generation
LiteRT-LM
litertlm
gemma3
chat
npu
edge-tpu
tensor-npu
tensor-g3
tensor-g4
pixel
on-device
quantized
int4
Instructions to use litert-community/Gemma3-1B-IT-Tensor-NPU with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LiteRT-LM
How to use litert-community/Gemma3-1B-IT-Tensor-NPU with LiteRT-LM:
# LiteRT-LM runs on various platforms (Android, iOS, Windows, Linux, macOS, IoT, Web/WASM) # and supports many APIs (C++, Python, Kotlin, Swift, JavaScript, Flutter). # For platform-specific integration guides, please refer to the official developer website: # https://ai.google.dev/edge/litert-lm # To try LiteRT-LM, the easiest way is to use our CLI tool. # 1. Install the LiteRT-LM CLI tool: pip install -U litert-lm # 2. Download and run this model locally: # See: https://ai.google.dev/edge/litert-lm/cli # A single .litertlm file in the repo is picked automatically; otherwise the CLI asks which one to run # (or pass its name right after the repo id). litert-lm run \ --from-huggingface-repo=litert-community/Gemma3-1B-IT-Tensor-NPU \ --prompt="Write me a poem"
- Notebooks
- Google Colab
- Kaggle
Add G4 NPU AOT build (Gemma3-1B-IT int4-hadamard, Tensor-G4) — runs in Box/EdgeTPU-app/litert_lm
Browse files- .gitattributes +5 -0
- Gemma3-1B-IT_int4hadamard_aot_ekv4096_G4.litertlm +3 -0
- MANIFEST.md +11 -0
- RANK2_FC_SURGERY.md +47 -0
- README.md +161 -0
- recipe/fc_rank2_surgery.py +162 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
Gemma3-1B-IT_mix4blk8_jit_ekv1280_Google_Tensor_G4.litertlm filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
Gemma3-1B-IT_q4_block32_jit_ekv1280_Google_Tensor_G4.litertlm filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
Gemma3-1B-IT_int4_aot_ekv1280_Google_Tensor_G4.litertlm filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
Gemma3-1B-IT_int4hadamard_aot_ekv4096_G4.litertlm filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
Gemma3-1B-IT_attn8head8_jit_ekv4096_Google_Tensor_G4.litertlm filter=lfs diff=lfs merge=lfs -text
|
Gemma3-1B-IT_int4hadamard_aot_ekv4096_G4.litertlm
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1c2962c8b18ff600ab37a1b15dfcccd6d1e5d8c155ad1ea46fe5fd0ab50dd6ee
|
| 3 |
+
size 874631968
|
MANIFEST.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Manifest
|
| 2 |
+
|
| 3 |
+
| File | Size (bytes) | SHA-256 |
|
| 4 |
+
|---|---|---|
|
| 5 |
+
| `Gemma3-1B-IT_int4hadamard_aot_ekv4096_G4.litertlm` | 874,631,968 | `1c2962c8b18ff600ab37a1b15dfcccd6d1e5d8c155ad1ea46fe5fd0ab50dd6ee` |
|
| 6 |
+
|
| 7 |
+
Verify after download:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
sha256sum Gemma3-1B-IT_int4hadamard_aot_ekv4096_G4.litertlm
|
| 11 |
+
```
|
RANK2_FC_SURGERY.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gemma-3-1B on the Tensor G4 NPU — the rank-2 FC surgery
|
| 2 |
+
|
| 3 |
+
The compiler-bug workaround that makes Gemma-3/4 AoT-compile to the Tensor G4 edgetpu, plus
|
| 4 |
+
working-model notes.
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## The rank-2 FULLY_CONNECTED surgery — the key enabler (affects G4 AND G5)
|
| 9 |
+
|
| 10 |
+
The DarwiNN AoT compiler **`OP_FAILED`s (`file.cc:4780`) on rank-3 `FULLY_CONNECTED` ops.**
|
| 11 |
+
Gemma-3 (and Gemma-4) emit these ~360× across the model, so the AoT compile fails outright
|
| 12 |
+
without a fix.
|
| 13 |
+
|
| 14 |
+
**Fix:** wrap every rank-3 FC as `RESHAPE → (rank-2) FC → RESHAPE` — same math, rank the
|
| 15 |
+
compiler accepts. Script: `recipe/fc_rank2_surgery.py` (`surgery(in_tflite, out_tflite)`), run
|
| 16 |
+
after quantization, before `aot_compile`. ~360 FCs rewritten for gemma-3-1b. The G5 uses the same
|
| 17 |
+
compiler family, so the surgery carries over to G5 as well.
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## Profile
|
| 22 |
+
|
| 23 |
+
**AoT (int4 per-channel + Hadamard):** ~100% NPU, prefill-monster (~660 tok/s prefill),
|
| 24 |
+
power-efficient — best for long-input / RAG. Decode is dispatch-bound rather than
|
| 25 |
+
compute-bound, so it sits around 16 tok/s regardless of prefill length.
|
| 26 |
+
|
| 27 |
+
Behavior note: tone/"feel" differences vs a reference are **runtime config** (system prompt +
|
| 28 |
+
sampling), not the quant — a faithful int4 copy matches the reference once system prompt and
|
| 29 |
+
top-k / top-p / temperature match.
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## On quantization support tables
|
| 34 |
+
|
| 35 |
+
Don't trust a fixed "what quantization compiles" matrix, including any you find in older copies
|
| 36 |
+
of this repo. Per-quant support is **compiler-build-dependent**: a result from one toolchain
|
| 37 |
+
build does not generalize to another, and rows that read "unsupported" have repeatedly turned out
|
| 38 |
+
to be one build's behaviour rather than a hardware or format limit.
|
| 39 |
+
|
| 40 |
+
Retest against your own target compiler rather than trusting a table.
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## Runtime requirement (brief)
|
| 45 |
+
|
| 46 |
+
int4 AoT DGCs require a compatible GoogleTensor dispatch library to load on-device; a stock app's
|
| 47 |
+
dispatch may reject them as a firmware mismatch.
|
README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: gemma
|
| 3 |
+
base_model: google/gemma-3-1b-it
|
| 4 |
+
base_model_relation: quantized
|
| 5 |
+
pipeline_tag: text-generation
|
| 6 |
+
library_name: litert-lm
|
| 7 |
+
tags:
|
| 8 |
+
- litert-lm
|
| 9 |
+
- litertlm
|
| 10 |
+
- gemma3
|
| 11 |
+
- chat
|
| 12 |
+
- text-generation
|
| 13 |
+
- npu
|
| 14 |
+
- edge-tpu
|
| 15 |
+
- tensor-g4
|
| 16 |
+
- pixel
|
| 17 |
+
- on-device
|
| 18 |
+
- quantized
|
| 19 |
+
- int4
|
| 20 |
+
extra_gated_heading: Access Gemma3-1B-IT (Tensor G4 NPU) on Hugging Face
|
| 21 |
+
extra_gated_prompt: >-
|
| 22 |
+
These are quantized, repackaged derivatives of google/gemma-3-1b-it and are
|
| 23 |
+
governed by the Gemma license. To access them you must review and agree to
|
| 24 |
+
Google's Gemma Terms of Use. Please ensure you are logged in to Hugging Face
|
| 25 |
+
and click below.
|
| 26 |
+
extra_gated_button_content: Acknowledge license
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
# Gemma3-1B-IT — Tensor G4 NPU (AOT, int4 per-channel + Hadamard)
|
| 30 |
+
|
| 31 |
+
> [!NOTE]
|
| 32 |
+
> A quantized, repackaged variant of
|
| 33 |
+
> [`google/gemma-3-1b-it`](https://huggingface.co/google/gemma-3-1b-it) that runs on the
|
| 34 |
+
> **Google Tensor G4 Edge TPU ("rio") NPU** — the SoC in the Pixel 9 / 9 Pro / 9 Pro XL / 9 Pro Fold.
|
| 35 |
+
> Packaged as a `.litertlm` bundle for the
|
| 36 |
+
> [LiteRT-LM](https://github.com/google-ai-edge/LiteRT-LM) runtime.
|
| 37 |
+
|
| 38 |
+
This is, to our knowledge, the **first publicly available int4 AOT Tensor-G4 NPU build of Gemma 3 1B** —
|
| 39 |
+
and it is **~half the size of the only other Tensor-NPU Gemma 3 1B bundle** published
|
| 40 |
+
(`litert-community/Gemma3-1B-IT_q8_ekv1280_Google_Tensor_G5.litertlm` is **1,678 MB**; this build is
|
| 41 |
+
**875 MB = 0.52×**), because it is int4-per-channel rather than int8.
|
| 42 |
+
|
| 43 |
+
`litert-community/Gemma3-1B-IT` ships Tensor **G5** and **G6** builds. There is no G4 build there.
|
| 44 |
+
|
| 45 |
+
---
|
| 46 |
+
|
| 47 |
+
## The build
|
| 48 |
+
|
| 49 |
+
| | |
|
| 50 |
+
|---|---|
|
| 51 |
+
| File | `Gemma3-1B-IT_int4hadamard_aot_ekv4096_G4.litertlm` |
|
| 52 |
+
| Mode | **AOT** — precompiled DarwiNN DGC |
|
| 53 |
+
| Quantization | int4 **per-channel + Hadamard rotation** (all FC / BMM / embed) |
|
| 54 |
+
| NPU share | **~100%** |
|
| 55 |
+
| Embedded KV cache | 4096 |
|
| 56 |
+
| Size | 874,631,968 B (875 MB) |
|
| 57 |
+
|
| 58 |
+
26-layer Gemma 3 1B-IT, embedded KV cache, embedded tokenizer + chat template, `.litertlm` v1.
|
| 59 |
+
Built from Google's mobile-QAT checkpoint
|
| 60 |
+
[`google/gemma-3-1b-it-qat-int4-unquantized`](https://huggingface.co/google/gemma-3-1b-it-qat-int4-unquantized).
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## Performance (Pixel 9 Pro XL, Tensor G4)
|
| 65 |
+
|
| 66 |
+
Measured on this exact bundle, 2026-09-10:
|
| 67 |
+
|
| 68 |
+
| prefill | decode | prefill tok/s | decode tok/s |
|
| 69 |
+
|---|---|---|---|
|
| 70 |
+
| 128 | 64 | 608.83 | 15.93 |
|
| 71 |
+
| 512 | 128 | 659.22 | 16.17 |
|
| 72 |
+
| 1024 | 64 | 663.99 | 15.89 |
|
| 73 |
+
|
| 74 |
+
**Prefill scales; decode is pinned.** Prefill rises 608 → 659 → 664 tok/s as the batch grows
|
| 75 |
+
128 → 1024, while decode holds at 15.89–16.17 tok/s regardless (±0.9% across an 8× change in
|
| 76 |
+
prefill length). That is a clean confirmation that decode is **dispatch-bound, not compute-bound** —
|
| 77 |
+
more work per dispatch makes prefill faster, but decode always does exactly one token per dispatch,
|
| 78 |
+
so nothing moves it.
|
| 79 |
+
|
| 80 |
+
The rise from 608 to 664 is fixed per-dispatch overhead amortizing: at 128 tokens you pay it once
|
| 81 |
+
per 128, at 1024 once per 1024. Extrapolating, the per-dispatch prefill floor is roughly **~670 tok/s**
|
| 82 |
+
for this model.
|
| 83 |
+
|
| 84 |
+
**AOT profile = prefill-monster / decode-laggard.** The Edge TPU crushes batched matmuls (prefill) but
|
| 85 |
+
a single-token decode underutilizes it — so this build is the fastest option on the phone for
|
| 86 |
+
prefill-heavy work (RAG, long-context, summarization, classification), and a strong speculative-decode
|
| 87 |
+
(MTP) candidate. Speculative decode is the real next perf lever and is not wired into this build.
|
| 88 |
+
|
| 89 |
+
> Behaviour/"feel" differences vs a reference are **runtime config** (system prompt + sampling), not the
|
| 90 |
+
> quant — a faithful int4 copy matches the reference once system prompt and top-k / top-p / temperature match.
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
## Quantization
|
| 95 |
+
|
| 96 |
+
### The rank-2 FC surgery — the key enabler (affects G4 **and** G5)
|
| 97 |
+
|
| 98 |
+
The DarwiNN AOT compiler `OP_FAILED`s on **rank-3 `FULLY_CONNECTED`** ops, which Gemma-3 emits ~360× — so
|
| 99 |
+
the AOT compile fails outright without a fix. **`recipe/fc_rank2_surgery.py`** wraps every rank-3 FC as
|
| 100 |
+
`RESHAPE → (rank-2) FC → RESHAPE` (same math, a rank the compiler accepts). This is what lets a full int4
|
| 101 |
+
Gemma 3 actually compile to a single G4 DGC. We are not aware of an upstream fix.
|
| 102 |
+
|
| 103 |
+
Details in **[`RANK2_FC_SURGERY.md`](./RANK2_FC_SURGERY.md)**.
|
| 104 |
+
|
| 105 |
+
### Hadamard rotation
|
| 106 |
+
|
| 107 |
+
On top of int4 per-channel, this build applies a **Hadamard rotation** to the weights before quantization
|
| 108 |
+
(a `DECOMPOSED_HADAMARD_ROTATION`-style outlier-suppression transform). Spreading outliers across the
|
| 109 |
+
rotated basis tightens the per-channel int4 grid — most visibly on the 262k-vocab head, where it improves
|
| 110 |
+
logit precision and coherence over plain per-channel int4, at a small size cost.
|
| 111 |
+
|
| 112 |
+
### Per-quant AOT support is compiler-build-dependent
|
| 113 |
+
|
| 114 |
+
There is **no fixed per-bit-width support matrix** for the DarwiNN AOT compiler — what a given toolchain
|
| 115 |
+
build accepts varies by compiler version. The shipping `gemma4-E2B` G4-NPU bundle is a **2-subgraph** model
|
| 116 |
+
carrying **int2, int4, and int8** weight tensors (verified directly), so mixed int2/int4/int8 does
|
| 117 |
+
AOT-compile. The verified enabler for compiling a full Gemma to a G4 DGC is the **rank-2 FC surgery**,
|
| 118 |
+
not any particular bit width.
|
| 119 |
+
|
| 120 |
+
Granularity is a build choice, not a compiler wall:
|
| 121 |
+
|
| 122 |
+
- **int4 per-channel + Hadamard** (one scale per output channel, rotated) → this build.
|
| 123 |
+
- **int4 per-channel** (one scale per output channel) → also coherent.
|
| 124 |
+
- **int4 per-tensor** also AOT-compiles, but one scale over the 262k-vocab head → word salad.
|
| 125 |
+
|
| 126 |
+
---
|
| 127 |
+
|
| 128 |
+
## Running
|
| 129 |
+
|
| 130 |
+
This is a **precompiled DarwiNN DGC**. It requires a **compatible GoogleTensor dispatch library** to load
|
| 131 |
+
on-device; a stock app's dispatch may reject it as a firmware mismatch. Runtime/dispatch specifics are out
|
| 132 |
+
of scope for this repo.
|
| 133 |
+
|
| 134 |
+
| Surface | Support |
|
| 135 |
+
|---|---|
|
| 136 |
+
| LiteRT-LM CLI (`--backend=npu`) | ✅ with a compatible dispatch runtime |
|
| 137 |
+
| Google AI Edge Gallery (NPU) | needs a compatible dispatch runtime |
|
| 138 |
+
| Tensor G4 (Pixel 9 series) | ✅ |
|
| 139 |
+
| Tensor G5 (Pixel 10) | ❌ DGC is G4-targeted |
|
| 140 |
+
| Older Tensor (G2 / G3) | ❌ |
|
| 141 |
+
| llama.cpp / GGUF, Web, iOS, desktop, non-Pixel | ❌ `.litertlm` is Pixel-NPU-only |
|
| 142 |
+
|
| 143 |
+
---
|
| 144 |
+
|
| 145 |
+
## Files
|
| 146 |
+
|
| 147 |
+
| File | Purpose |
|
| 148 |
+
|---|---|
|
| 149 |
+
| `Gemma3-1B-IT_int4hadamard_aot_ekv4096_G4.litertlm` | the model — per-channel int4 + Hadamard, 4096 ctx, ~100% NPU |
|
| 150 |
+
| `RANK2_FC_SURGERY.md` | the rank-2 FC surgery writeup + why per-quant AOT support is compiler-build-dependent |
|
| 151 |
+
| `recipe/fc_rank2_surgery.py` | rank-3 → rank-2 FC surgery (the DarwiNN AOT enabler) |
|
| 152 |
+
| `MANIFEST.md` | checksum + size |
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## License & attribution
|
| 157 |
+
|
| 158 |
+
Base model **Gemma 3 1B-IT** © Google, under the [Gemma Terms of Use](https://ai.google.dev/gemma/terms).
|
| 159 |
+
This quantized/repackaged derivative inherits the Gemma license. Runtime: **LiteRT-LM** / **LiteRT**
|
| 160 |
+
(Apache-2.0). Quantization, rank-2 FC surgery, per-channel int4 + Hadamard AOT compile, and G4 NPU
|
| 161 |
+
packaging by **@xThr45hx**.
|
recipe/fc_rank2_surgery.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""fc_rank2_surgery.py — post-export tflite graph rewrite: wrap every rank-3
|
| 2 |
+
FULLY_CONNECTED so its activation is rank-2, satisfying darwinn G4
|
| 3 |
+
(MASTER_NPU_SPEC: FC rank-2 only; rank-3 [1,T,K] => OP_FAILED line 4780).
|
| 4 |
+
|
| 5 |
+
Per rank-3 FC (in:[1,T,K] -> out:[1,T,N]):
|
| 6 |
+
in[1,T,K] --RESHAPE--> in2[T*?,K] --FC--> out2[T*?,N] --RESHAPE--> out[1,T,N]
|
| 7 |
+
Reshape is numerically a no-op so quant params copy unchanged (reshape input/
|
| 8 |
+
output share scale+zp). Done with the flatbuffers object API (ModelT).
|
| 9 |
+
|
| 10 |
+
Local-only: needs ai_edge_litert schema + flatbuffers (no torch, no box).
|
| 11 |
+
Usage: python fc_rank2_surgery.py IN.tflite OUT.tflite
|
| 12 |
+
"""
|
| 13 |
+
import sys, copy
|
| 14 |
+
import numpy as np
|
| 15 |
+
from ai_edge_litert import schema_py_generated as schema
|
| 16 |
+
import flatbuffers
|
| 17 |
+
|
| 18 |
+
FC = schema.BuiltinOperator.FULLY_CONNECTED
|
| 19 |
+
RESHAPE = schema.BuiltinOperator.RESHAPE
|
| 20 |
+
INT32 = schema.TensorType.INT32
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def opcode_index_for(model, builtin):
|
| 24 |
+
"""Return index of an OperatorCode for `builtin`, adding one if missing."""
|
| 25 |
+
for i, oc in enumerate(model.operatorCodes):
|
| 26 |
+
bc = oc.builtinCode if oc.builtinCode else oc.deprecatedBuiltinCode
|
| 27 |
+
if bc == builtin:
|
| 28 |
+
return i
|
| 29 |
+
oc = schema.OperatorCodeT()
|
| 30 |
+
oc.builtinCode = builtin
|
| 31 |
+
oc.deprecatedBuiltinCode = min(builtin, 127)
|
| 32 |
+
oc.version = 1
|
| 33 |
+
model.operatorCodes.append(oc)
|
| 34 |
+
return len(model.operatorCodes) - 1
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def add_buffer(model, data_bytes=None):
|
| 38 |
+
b = schema.BufferT()
|
| 39 |
+
if data_bytes is not None:
|
| 40 |
+
b.data = list(data_bytes)
|
| 41 |
+
model.buffers.append(b)
|
| 42 |
+
return len(model.buffers) - 1
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def clone_quant(q):
|
| 46 |
+
if q is None:
|
| 47 |
+
return None
|
| 48 |
+
nq = schema.QuantizationParametersT()
|
| 49 |
+
nq.scale = list(q.scale) if q.scale is not None else None
|
| 50 |
+
nq.zeroPoint = list(q.zeroPoint) if q.zeroPoint is not None else None
|
| 51 |
+
nq.quantizedDimension = q.quantizedDimension
|
| 52 |
+
nq.min = list(q.min) if q.min is not None else None
|
| 53 |
+
nq.max = list(q.max) if q.max is not None else None
|
| 54 |
+
return nq
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def new_tensor(sg, name, shape, ttype, quant):
|
| 58 |
+
t = schema.TensorT()
|
| 59 |
+
t.name = name.encode() if isinstance(name, str) else name
|
| 60 |
+
t.shape = list(shape)
|
| 61 |
+
t.type = ttype
|
| 62 |
+
t.buffer = 0 # intermediate (no data)
|
| 63 |
+
t.quantization = quant
|
| 64 |
+
sg.tensors.append(t)
|
| 65 |
+
return len(sg.tensors) - 1
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def surgery(in_path, out_path):
|
| 69 |
+
buf = open(in_path, "rb").read()
|
| 70 |
+
root = schema.Model.GetRootAs(buf, 0) # for external-buffer offsets
|
| 71 |
+
model = schema.ModelT.InitFromPackedBuf(buf, 0)
|
| 72 |
+
# Pull EXTERNAL buffers (offset>1, data appended after the flatbuffer) inline
|
| 73 |
+
# so Pack preserves the ~1GB of weights. Model is <2GB so inline re-pack is legal.
|
| 74 |
+
pulled = 0
|
| 75 |
+
for i in range(root.BuffersLength()):
|
| 76 |
+
fb = root.Buffers(i)
|
| 77 |
+
off, size = fb.Offset(), fb.Size()
|
| 78 |
+
if off and off > 1 and size:
|
| 79 |
+
model.buffers[i].data = np.frombuffer(buf[off:off + size], dtype=np.uint8)
|
| 80 |
+
model.buffers[i].offset = 0
|
| 81 |
+
model.buffers[i].size = 0
|
| 82 |
+
pulled += 1
|
| 83 |
+
print(f" pulled {pulled} external buffers inline")
|
| 84 |
+
reshape_oc = opcode_index_for(model, RESHAPE)
|
| 85 |
+
|
| 86 |
+
total_wrapped = 0
|
| 87 |
+
total_shared = 0
|
| 88 |
+
for sgi, sg in enumerate(model.subgraphs):
|
| 89 |
+
new_ops = []
|
| 90 |
+
wrapped = 0
|
| 91 |
+
shared = 0
|
| 92 |
+
in_reshape_cache = {} # (orig_in0, flat, K) -> in2 tensor idx (share qkv/gate-up input reshapes)
|
| 93 |
+
for op in sg.operators:
|
| 94 |
+
oc = model.operatorCodes[op.opcodeIndex]
|
| 95 |
+
bc = oc.builtinCode if oc.builtinCode else oc.deprecatedBuiltinCode
|
| 96 |
+
if bc != FC:
|
| 97 |
+
new_ops.append(op)
|
| 98 |
+
continue
|
| 99 |
+
in_t = sg.tensors[op.inputs[0]]
|
| 100 |
+
shp = list(in_t.shape) if in_t.shape is not None else []
|
| 101 |
+
if len(shp) <= 2:
|
| 102 |
+
new_ops.append(op)
|
| 103 |
+
continue
|
| 104 |
+
K = int(shp[-1])
|
| 105 |
+
flat = int(np.prod(shp[:-1])) # T (and any leading dims) collapsed
|
| 106 |
+
out_idx = int(op.outputs[0])
|
| 107 |
+
out_t = sg.tensors[out_idx]
|
| 108 |
+
out_shape = list(out_t.shape)
|
| 109 |
+
N = out_shape[-1]
|
| 110 |
+
|
| 111 |
+
# --- pre-reshape: in[..,K] -> in2[flat, K] (SHARED across FCs with same input) ---
|
| 112 |
+
orig_in0 = int(op.inputs[0])
|
| 113 |
+
cache_key = (orig_in0, flat, K)
|
| 114 |
+
if cache_key in in_reshape_cache:
|
| 115 |
+
in2 = in_reshape_cache[cache_key] # reuse — e.g. q/k/v share one reshape
|
| 116 |
+
shared += 1
|
| 117 |
+
else:
|
| 118 |
+
in2 = new_tensor(sg, f"fc_r2_in_{sgi}_{wrapped}", [flat, K],
|
| 119 |
+
in_t.type, clone_quant(in_t.quantization))
|
| 120 |
+
shp_buf = add_buffer(model, np.array([flat, K], dtype=np.int32).tobytes())
|
| 121 |
+
shp_t = new_tensor(sg, f"fc_r2_in_shape_{sgi}_{wrapped}", [2], INT32, None)
|
| 122 |
+
sg.tensors[shp_t].buffer = shp_buf
|
| 123 |
+
r1 = schema.OperatorT(); r1.opcodeIndex = reshape_oc
|
| 124 |
+
r1.inputs = [orig_in0, shp_t]; r1.outputs = [in2]
|
| 125 |
+
ro = schema.ReshapeOptionsT(); ro.newShape = [flat, K]
|
| 126 |
+
r1.builtinOptionsType = schema.BuiltinOptions.ReshapeOptions
|
| 127 |
+
r1.builtinOptions = ro
|
| 128 |
+
new_ops.append(r1)
|
| 129 |
+
in_reshape_cache[cache_key] = in2
|
| 130 |
+
|
| 131 |
+
# --- FC now rank-2: in2[flat,K] -> fcout[flat,N] ---
|
| 132 |
+
fcout = new_tensor(sg, f"fc_r2_out_{sgi}_{wrapped}", [flat, N],
|
| 133 |
+
out_t.type, clone_quant(out_t.quantization))
|
| 134 |
+
inl = list(int(x) for x in op.inputs); inl[0] = in2; op.inputs = inl
|
| 135 |
+
outl = list(int(x) for x in op.outputs); outl[0] = fcout; op.outputs = outl
|
| 136 |
+
new_ops.append(op)
|
| 137 |
+
|
| 138 |
+
# --- post-reshape: fcout[flat,N] -> out[orig shape] ---
|
| 139 |
+
oshp_buf = add_buffer(model, np.array(out_shape, dtype=np.int32).tobytes())
|
| 140 |
+
oshp_t = new_tensor(sg, f"fc_r2_out_shape_{sgi}_{wrapped}", [len(out_shape)], INT32, None)
|
| 141 |
+
sg.tensors[oshp_t].buffer = oshp_buf
|
| 142 |
+
r2 = schema.OperatorT(); r2.opcodeIndex = reshape_oc
|
| 143 |
+
r2.inputs = [fcout, oshp_t]; r2.outputs = [out_idx]
|
| 144 |
+
ro2 = schema.ReshapeOptionsT(); ro2.newShape = list(out_shape)
|
| 145 |
+
r2.builtinOptionsType = schema.BuiltinOptions.ReshapeOptions
|
| 146 |
+
r2.builtinOptions = ro2
|
| 147 |
+
new_ops.append(r2)
|
| 148 |
+
wrapped += 1
|
| 149 |
+
sg.operators = new_ops
|
| 150 |
+
total_wrapped += wrapped
|
| 151 |
+
total_shared += shared
|
| 152 |
+
if wrapped:
|
| 153 |
+
print(f" subgraph {sgi}: wrapped {wrapped} rank-3 FC ({shared} input-reshapes shared)")
|
| 154 |
+
|
| 155 |
+
b = flatbuffers.Builder(0)
|
| 156 |
+
b.Finish(model.Pack(b), b"TFL3")
|
| 157 |
+
open(out_path, "wb").write(b.Output())
|
| 158 |
+
print(f"wrapped {total_wrapped} FC total -> {out_path}")
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
surgery(sys.argv[1], sys.argv[2])
|