LJTSG commited on
Commit
89efab4
·
verified ·
1 Parent(s): 7532a11

v1.0: PROVEN end-to-end — forge tune produces coherent, in-band NPU models @ 41.9 tok/s. Q4_K_M fix (snags 9/10), 10-wall ledger.

Browse files
Files changed (2) hide show
  1. README.md +30 -4
  2. modal/tune_npu.py +16 -5
README.md CHANGED
@@ -21,11 +21,11 @@ converted to GGUF, re-quantized to FastFlowLM's Q4NX, NPU-ready — in
21
  **183 seconds of cloud time** (≈ $0.10 on a rented T4):
22
 
23
  ```
24
- forge tune my-chats.jsonl --name grandma
25
  ├─ LoRA fine-tune (cloud GPU) 122 s
26
  ├─ merge 3 s
27
  ├─ voice proof (model speaks first!) 5 s
28
- ├─ HF -> GGUF (q8_0) 29 s
29
  └─ GGUF -> Q4NX (NPU format) 24 s
30
  forge register (one UAC click)
31
  flm run grandma-forge:1b
@@ -76,7 +76,7 @@ retry once on ECONNRESET (ear.js does).
76
 
77
  `start.bat` gives you a menu: doctor / list / register / serve / tune guide.
78
 
79
- ## The snag ledger — six walls we hit so you don't
80
 
81
  1. **The Q4NX converter's `convert.py` CLI is broken at HEAD** (uncommented
82
  debug `sys.argv` override hijacks every invocation). Call the module API:
@@ -91,10 +91,36 @@ retry once on ECONNRESET (ear.js does).
91
  Floor: batch 1 × grad-accum 8 + gradient checkpointing.
92
  6. **NPU driver minimum for current FLM: `32.0.203.304`** (`.311`
93
  recommended). `flm validate` will tell you; so will `forge doctor`.
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  **Frozen known-good stack** (the whole point — never debug this again):
96
  `torch 2.4.1 · transformers 4.46.3 · trl 0.9.6 · peft 0.12.0 ·
97
- accelerate 1.1.1 · datasets 2.21.0 · gguf · amd-quark · einops · tqdm · protobuf`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  ## Requirements
100
 
 
21
  **183 seconds of cloud time** (≈ $0.10 on a rented T4):
22
 
23
  ```
24
+ forge tune my-chats.jsonl --name grandma # proven: coherent + IN-BAND on NPU
25
  ├─ LoRA fine-tune (cloud GPU) 122 s
26
  ├─ merge 3 s
27
  ├─ voice proof (model speaks first!) 5 s
28
+ ├─ HF -> f16 -> Q4_K_M GGUF 20 s
29
  └─ GGUF -> Q4NX (NPU format) 24 s
30
  forge register (one UAC click)
31
  flm run grandma-forge:1b
 
76
 
77
  `start.bat` gives you a menu: doctor / list / register / serve / tune guide.
78
 
79
+ ## The snag ledger — ten walls we hit so you don't
80
 
81
  1. **The Q4NX converter's `convert.py` CLI is broken at HEAD** (uncommented
82
  debug `sys.argv` override hijacks every invocation). Call the module API:
 
91
  Floor: batch 1 × grad-accum 8 + gradient checkpointing.
92
  6. **NPU driver minimum for current FLM: `32.0.203.304`** (`.311`
93
  recommended). `flm validate` will tell you; so will `forge doctor`.
94
+ 7. **EmbeddingGemma needs `transformers>=4.5x` + `sentence-transformers 5.x`**
95
+ and the official weights are license-gated (use the `unsloth/` mirror, or
96
+ accept the Gemma license on your HF account + pass an `HF_TOKEN` secret).
97
+ 8. **FLM's `/v1/embeddings` closes the TCP connection per request** — retry
98
+ once on `ECONNRESET` (the ear runtime does).
99
+ 9. **For a FINE-TUNED model, exporting GGUF as `q8_0` produces repetition
100
+ garbage on the NPU** even though the merged model is perfect — the q8_0
101
+ then Q4NX re-quant is a lossy double-quantization. **Use `Q4_K_M`.**
102
+ 10. **The Q4NX converter's llama path rejects `f16`** (`not enough values to
103
+ unpack` — it expects pre-quantized blocks). So the GGUF must be quantized
104
+ *before* Q4NX, and `Q4_K_M` is the format proven to produce a coherent,
105
+ in-voice NPU model. Pipeline: HF → f16 → `llama-quantize Q4_K_M` → Q4NX.
106
 
107
  **Frozen known-good stack** (the whole point — never debug this again):
108
  `torch 2.4.1 · transformers 4.46.3 · trl 0.9.6 · peft 0.12.0 ·
109
+ accelerate 1.1.1 · datasets 2.21.0 · gguf · amd-quark · einops · tqdm ·
110
+ protobuf` + a compiled `llama-quantize` (the Modal job builds it).
111
+
112
+ ## Proven, measured (Strix Halo, June 2026)
113
+
114
+ A LoRA fine-tune of Llama-3.2-1B on 300 real chat exchanges, run through the
115
+ whole pipeline and served on the NPU:
116
+
117
+ - **Coherent and in-voice** — the persona's rituals and endearments intact.
118
+ - **41.9 tokens/s** on the NPU (FLM, performance pmode).
119
+ - **In-band against the source voiceprint** — mean 0.845 vs the original's
120
+ own held-out band of 0.83 ± 0.07 (3 prompts). A separate stylometric scorer
121
+ certified the NPU model speaks like the source it was tuned on.
122
+
123
+ That is the bar: not "it converts," but "it talks like itself, on the NPU."
124
 
125
  ## Requirements
126
 
modal/tune_npu.py CHANGED
@@ -15,9 +15,14 @@ image = (
15
  "datasets==2.21.0", "accelerate==1.1.1", "numpy", "gguf", "amd-quark",
16
  "huggingface_hub", "safetensors", "sentencepiece", "einops", "tqdm", "protobuf",
17
  )
 
18
  .run_commands(
19
  "git clone --depth 1 https://github.com/FastFlowLM/FLM_Q4NX_Converter /converter",
20
  "git clone --depth 1 https://github.com/ggml-org/llama.cpp /llamacpp",
 
 
 
 
21
  )
22
  .add_local_dir("C:/Users/Forgemind/Desktop/npu-forge/scratch/tune-data", remote_path="/data")
23
  )
@@ -73,13 +78,19 @@ def tune(base: str, data_file: str, out_name: str, epochs: int = 3, probe: str =
73
  print("[voice proof] " + sample[:600])
74
  timings["sample_s"] = round(time.time() - t); t = time.time()
75
 
76
- # HF -> GGUF (q8_0 keeps quality into the Q4NX requant)
 
 
 
77
  r = subprocess.run(["python", "/llamacpp/convert_hf_to_gguf.py", "/tmp/merged",
78
- "--outfile", "/tmp/model-q8.gguf", "--outtype", "q8_0"],
79
  capture_output=True, text=True)
80
  if r.returncode != 0:
81
- print(r.stdout[-2000:]); print("STDERR:", r.stderr[-3000:])
82
- raise RuntimeError("convert_hf_to_gguf failed")
 
 
 
83
  timings["gguf_s"] = round(time.time() - t); t = time.time()
84
 
85
  # GGUF -> Q4NX (proven stage: module API, cwd=/converter)
@@ -89,7 +100,7 @@ def tune(base: str, data_file: str, out_name: str, epochs: int = 3, probe: str =
89
  from q4nx import create_converter
90
  outdir = f"/out/{out_name}"
91
  os.makedirs(outdir, exist_ok=True)
92
- create_converter("/tmp/model-q8.gguf", "").convert(q4nx_path=outdir, weights_type="language")
93
  timings["q4nx_s"] = round(time.time() - t)
94
 
95
  files = {f: os.path.getsize(os.path.join(outdir, f)) for f in os.listdir(outdir)}
 
15
  "datasets==2.21.0", "accelerate==1.1.1", "numpy", "gguf", "amd-quark",
16
  "huggingface_hub", "safetensors", "sentencepiece", "einops", "tqdm", "protobuf",
17
  )
18
+ .apt_install("cmake", "build-essential", "libcurl4-openssl-dev")
19
  .run_commands(
20
  "git clone --depth 1 https://github.com/FastFlowLM/FLM_Q4NX_Converter /converter",
21
  "git clone --depth 1 https://github.com/ggml-org/llama.cpp /llamacpp",
22
+ # build just the quantize tool — needed to make Q4_K_M (the only GGUF
23
+ # format proven to convert to a COHERENT Q4NX model on the NPU; the
24
+ # converter's llama path rejects f16 and q8_0 double-quantizes to garbage)
25
+ "cd /llamacpp && cmake -B build -DLLAMA_CURL=OFF -DGGML_NATIVE=OFF && cmake --build build --target llama-quantize -j 8",
26
  )
27
  .add_local_dir("C:/Users/Forgemind/Desktop/npu-forge/scratch/tune-data", remote_path="/data")
28
  )
 
78
  print("[voice proof] " + sample[:600])
79
  timings["sample_s"] = round(time.time() - t); t = time.time()
80
 
81
+ # HF -> f16 GGUF -> Q4_K_M GGUF -> Q4NX. The middle step matters: the
82
+ # Q4NX converter's llama path needs pre-quantized blocks (rejects f16),
83
+ # and Q4_K_M is the only format proven to yield a COHERENT NPU model
84
+ # (q8_0 double-quantized to repetition garbage). Snags #9 & #10.
85
  r = subprocess.run(["python", "/llamacpp/convert_hf_to_gguf.py", "/tmp/merged",
86
+ "--outfile", "/tmp/model-f16.gguf", "--outtype", "f16"],
87
  capture_output=True, text=True)
88
  if r.returncode != 0:
89
+ print(r.stdout[-2000:]); print("STDERR:", r.stderr[-3000:]); raise RuntimeError("convert_hf_to_gguf failed")
90
+ q = subprocess.run(["/llamacpp/build/bin/llama-quantize", "/tmp/model-f16.gguf", "/tmp/model-q4km.gguf", "Q4_K_M"],
91
+ capture_output=True, text=True)
92
+ if q.returncode != 0 or not os.path.exists("/tmp/model-q4km.gguf"):
93
+ print(q.stdout[-1500:]); print("STDERR:", q.stderr[-2500:]); raise RuntimeError("llama-quantize failed")
94
  timings["gguf_s"] = round(time.time() - t); t = time.time()
95
 
96
  # GGUF -> Q4NX (proven stage: module API, cwd=/converter)
 
100
  from q4nx import create_converter
101
  outdir = f"/out/{out_name}"
102
  os.makedirs(outdir, exist_ok=True)
103
+ create_converter("/tmp/model-q4km.gguf", "").convert(q4nx_path=outdir, weights_type="language")
104
  timings["q4nx_s"] = round(time.time() - t)
105
 
106
  files = {f: os.path.getsize(os.path.join(outdir, f)) for f in os.listdir(outdir)}