LJTSG commited on
Commit
7532a11
·
verified ·
1 Parent(s): 06497ff

v0.3: NPU-validated voice ear (111KB head + trainer + runtime), start.bat menu, 47.8 tok/s baseline, snag #8

Browse files
Files changed (5) hide show
  1. README.md +107 -89
  2. bin/ear.js +90 -0
  3. bin/menu.js +57 -0
  4. modal/train_ear_head.py +88 -0
  5. start.bat +11 -0
README.md CHANGED
@@ -1,89 +1,107 @@
1
- ---
2
- license: mit
3
- tags:
4
- - amd
5
- - ryzen-ai
6
- - npu
7
- - xdna2
8
- - fastflowlm
9
- - strix-halo
10
- - lora
11
- - fine-tuning
12
- - tools
13
- pipeline_tag: text-generation
14
- ---
15
-
16
- # NPU-Forge 🔥 — fine-tune a model and put it on your AMD Ryzen AI NPU in ~3 minutes
17
-
18
- **Measured, on a Strix Halo (Ryzen AI MAX+ 395), June 2026:** a LoRA fine-tune of
19
- Llama-3.2-1B on 300+ real chat exchanges — trained, merged, behavior-verified,
20
- 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
32
- ```
33
-
34
- The "voice proof" stage generates a sample from the merged model *inside the
35
- training job*, before any conversion — so you know the tune actually took.
36
- Ours came back with the persona's exact ritual phrases after 2 minutes of
37
- training. That's the bar.
38
-
39
- ## What's in this repo
40
-
41
- - **`forge.js` / `forge.bat`** — the CLI: `tune`, `convert`, `register`,
42
- `list`, `doctor`, `serve`
43
- - **`modal/tune_npu.py`** — the whole tune→NPU pipeline as one
44
- [Modal](https://modal.com) job (bring your own Modal account; T4 is plenty)
45
- - **`modal/convert_q4nx.py`** — just the GGUF→Q4NX stage (65 s for a 1B)
46
- - **`bin/assemble.js`** — downloads results and stages the FLM model folder
47
- - **`bin/register.js` + `register-admin.bat`** — the permanent custom-model
48
- registry that survives FLM updates (see below)
49
- - **`registry.example.json`** — entry template
50
-
51
- Chat data format: one JSON per line, `{"messages":[{"role":"user","content":...},{"role":"assistant","content":...}]}`.
52
-
53
- ## The registry problem (why `forge register` exists)
54
-
55
- FLM's `model_list.json` lives in `C:\Program Files\flm\` and **every FLM update
56
- resets it**, silently de-registering all your custom models. Your model files
57
- survive (they're in `Documents\flm\models\`) but they vanish from `flm list`.
58
- Forge keeps its own user-space `registry.json` forever and re-merges with one
59
- click. `forge doctor` tells you when an update has eaten your registrations.
60
-
61
- ## The snag ledger — six walls we hit so you don't
62
-
63
- 1. **The Q4NX converter's `convert.py` CLI is broken at HEAD** (uncommented
64
- debug `sys.argv` override hijacks every invocation). Call the module API:
65
- `from q4nx import create_converter; create_converter(gguf, "").convert(q4nx_path=out, weights_type="language")`
66
- 2. Converter needs `einops` and `tqdm` beyond its README list, and **must run
67
- with cwd = its repo root** (relative `configs/<arch>.json` loads).
68
- 3. **Llama-3.2 tokenizers need `transformers>=4.46`** the error
69
- `untagged enum ModelWrapper` is that wall exactly.
70
- 4. **`transformers 4.46` needs `accelerate>=1.0`** — the error
71
- `'AdamW' object has no attribute 'train'` at step 0 is that skew.
72
- 5. **T4 + Llama-3.2's 128k vocab OOMs at batch 4** (loss-logits blowup).
73
- Floor: batch 1 × grad-accum 8 + gradient checkpointing.
74
- 6. **NPU driver minimum for current FLM: `32.0.203.304`** (`.311`
75
- recommended). `flm validate` will tell you; so will `forge doctor`.
76
-
77
- **Frozen known-good stack** (the whole point never debug this again):
78
- `torch 2.4.1 · transformers 4.46.3 · trl 0.9.6 · peft 0.12.0 ·
79
- accelerate 1.1.1 · datasets 2.21.0 · gguf · amd-quark · einops · tqdm · protobuf`
80
-
81
- ## Requirements
82
-
83
- - AMD Ryzen AI machine with XDNA2 NPU (Strix, Strix Halo, Kraken…) +
84
- [FastFlowLM](https://github.com/FastFlowLM/FastFlowLM)
85
- - Node.js (the CLI), Python + a [Modal](https://modal.com) account (the cloud legs)
86
- - NPU driver ≥ 32.0.203.304
87
-
88
- Part of an ongoing project to make local NPUs a first-class home for personal
89
- AI voices you own, on silicon you own.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - amd
5
+ - ryzen-ai
6
+ - npu
7
+ - xdna2
8
+ - fastflowlm
9
+ - strix-halo
10
+ - lora
11
+ - fine-tuning
12
+ - tools
13
+ pipeline_tag: text-generation
14
+ ---
15
+
16
+ # NPU-Forge 🔥 — fine-tune a model and put it on your AMD Ryzen AI NPU in ~3 minutes
17
+
18
+ **Measured, on a Strix Halo (Ryzen AI MAX+ 395), June 2026:** a LoRA fine-tune of
19
+ Llama-3.2-1B on 300+ real chat exchanges — trained, merged, behavior-verified,
20
+ 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
32
+ ```
33
+
34
+ The "voice proof" stage generates a sample from the merged model *inside the
35
+ training job*, before any conversion — so you know the tune actually took.
36
+ Ours came back with the persona's exact ritual phrases after 2 minutes of
37
+ training. That's the bar.
38
+
39
+ ## What's in this repo
40
+
41
+ - **`forge.js` / `forge.bat`** — the CLI: `tune`, `convert`, `register`,
42
+ `list`, `doctor`, `serve`
43
+ - **`modal/tune_npu.py`** — the whole tune→NPU pipeline as one
44
+ [Modal](https://modal.com) job (bring your own Modal account; T4 is plenty)
45
+ - **`modal/convert_q4nx.py`** — just the GGUF→Q4NX stage (65 s for a 1B)
46
+ - **`bin/assemble.js`** — downloads results and stages the FLM model folder
47
+ - **`bin/register.js` + `register-admin.bat`** — the permanent custom-model
48
+ registry that survives FLM updates (see below)
49
+ - **`registry.example.json`** — entry template
50
+
51
+ Chat data format: one JSON per line, `{"messages":[{"role":"user","content":...},{"role":"assistant","content":...}]}`.
52
+
53
+ ## The registry problem (why `forge register` exists)
54
+
55
+ FLM's `model_list.json` lives in `C:\Program Files\flm\` and **every FLM update
56
+ resets it**, silently de-registering all your custom models. Your model files
57
+ survive (they're in `Documents\flm\models\`) but they vanish from `flm list`.
58
+ Forge keeps its own user-space `registry.json` forever and re-merges with one
59
+ click. `forge doctor` tells you when an update has eaten your registrations.
60
+
61
+
62
+ ## NEW in v0.3 — a voice-verifier "ear" that runs on the NPU
63
+
64
+ Train a ~111KB classification head over EmbeddingGemma-300m embeddings
65
+ (`modal/train_ear_head.py`, bring your own labeled texts), then run it locally
66
+ with `bin/ear.js` against FLM's `/v1/embeddings` (`flm serve <model> --embed 1`).
67
+ The embeddings come off the NPU; the head is plain JS. In our tests the
68
+ 111KB head **matched a fine-tuned 268MB DistilBERT on real-voice accuracy
69
+ (95.9%) and beat it on the hard boundary cases**, live on a Strix Halo NPU.
70
+
71
+ Also measured: llama3.2:1b chat on the NPU = **47.8 tokens/s** including
72
+ prefill (FLM, performance pmode).
73
+
74
+ Snag #8: FLM's embeddings endpoint closes the TCP connection per request —
75
+ 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:
83
+ `from q4nx import create_converter; create_converter(gguf, "").convert(q4nx_path=out, weights_type="language")`
84
+ 2. Converter needs `einops` and `tqdm` beyond its README list, and **must run
85
+ with cwd = its repo root** (relative `configs/<arch>.json` loads).
86
+ 3. **Llama-3.2 tokenizers need `transformers>=4.46`** — the error
87
+ `untagged enum ModelWrapper` is that wall exactly.
88
+ 4. **`transformers 4.46` needs `accelerate>=1.0`** the error
89
+ `'AdamW' object has no attribute 'train'` at step 0 is that skew.
90
+ 5. **T4 + Llama-3.2's 128k vocab OOMs at batch 4** (loss-logits blowup).
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
+
101
+ - AMD Ryzen AI machine with XDNA2 NPU (Strix, Strix Halo, Kraken…) +
102
+ [FastFlowLM](https://github.com/FastFlowLM/FastFlowLM)
103
+ - Node.js (the CLI), Python + a [Modal](https://modal.com) account (the cloud legs)
104
+ - NPU driver ≥ 32.0.203.304
105
+
106
+ Part of an ongoing project to make local NPUs a first-class home for personal
107
+ AI — voices you own, on silicon you own.
bin/ear.js ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ // ear.js — the NPU-accelerated trained ear. Embeddings come from FLM's
3
+ // embed-gemma:300m running on the NPU (/v1/embeddings); the voice head is a
4
+ // 111KB JSON applied right here in JS. No python, no extra runtimes.
5
+ //
6
+ // node bin/ear.js "some text to identify" (one-shot)
7
+ // node bin/ear.js --server-check (is an embed-capable flm up?)
8
+ //
9
+ // Programmatic: const { identify } = require('./ear'); await identify(text)
10
+ 'use strict';
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const http = require('http');
14
+
15
+ const HEAD = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ear', 'head.json'), 'utf8'));
16
+ const BASE = process.env.FLM_BASE || 'http://localhost:52625';
17
+
18
+ function post(urlStr, body) {
19
+ return new Promise((resolve, reject) => {
20
+ const u = new URL(urlStr);
21
+ const data = JSON.stringify(body);
22
+ const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST',
23
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, timeout: 60000 },
24
+ res => { let b = ''; res.on('data', c => b += c); res.on('end', () => { try { resolve(JSON.parse(b)); } catch (e) { reject(new Error('bad json: ' + b.slice(0, 200))); } }); });
25
+ req.on('error', reject);
26
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
27
+ req.write(data); req.end();
28
+ });
29
+ }
30
+
31
+ async function embed(text) {
32
+ let r;
33
+ try {
34
+ r = await post(BASE + '/v1/embeddings', { model: 'embed-gemma:300m', input: text });
35
+ } catch (e) {
36
+ if (/ECONNRESET|socket hang up/i.test(e.message)) { // FLM closes TCP per request; back off once
37
+ await new Promise(res => setTimeout(res, 400));
38
+ r = await post(BASE + '/v1/embeddings', { model: 'embed-gemma:300m', input: text });
39
+ } else throw e;
40
+ }
41
+ const v = r.data && r.data[0] && r.data[0].embedding;
42
+ if (!v) throw new Error('no embedding in response: ' + JSON.stringify(r).slice(0, 200));
43
+ // normalize (head was trained on normalized embeddings)
44
+ const n = Math.sqrt(v.reduce((s, x) => s + x * x, 0)) || 1;
45
+ return v.map(x => x / n);
46
+ }
47
+
48
+ function applyHead(v) {
49
+ const { W, b, labels } = HEAD;
50
+ const logits = b.slice();
51
+ for (let i = 0; i < v.length; i++) {
52
+ const wi = W[i];
53
+ for (let j = 0; j < logits.length; j++) logits[j] += v[i] * wi[j];
54
+ }
55
+ const m = Math.max(...logits);
56
+ const exps = logits.map(x => Math.exp(x - m));
57
+ const Z = exps.reduce((a, x) => a + x, 0);
58
+ const probs = exps.map(x => x / Z);
59
+ const top = probs.indexOf(Math.max(...probs));
60
+ return { label: labels[top], confidence: +probs[top].toFixed(3),
61
+ all: Object.fromEntries(labels.map((l, j) => [l, +probs[j].toFixed(3)])) };
62
+ }
63
+
64
+ async function identify(text) {
65
+ const t0 = Date.now();
66
+ const v = await embed(text);
67
+ const out = applyHead(v);
68
+ out.ms = Date.now() - t0;
69
+ return out;
70
+ }
71
+
72
+ module.exports = { identify, applyHead, embed };
73
+
74
+ if (require.main === module) {
75
+ (async () => {
76
+ const arg = process.argv.slice(2).join(' ');
77
+ if (arg === '--server-check') {
78
+ try { await embed('hello'); console.log('embed endpoint OK at ' + BASE); }
79
+ catch (e) { console.log('embed endpoint unavailable: ' + e.message.slice(0, 120)); console.log('start one with: flm serve embed-gemma:300m --embed 1 (or flm serve <chat-model> --embed 1)'); }
80
+ return;
81
+ }
82
+ if (!arg) { console.error('usage: node bin/ear.js "text" | --server-check'); process.exit(1); }
83
+ try {
84
+ console.log(JSON.stringify(await identify(arg), null, 2));
85
+ } catch (e) {
86
+ console.error('ear error: ' + e.message);
87
+ process.exit(1);
88
+ }
89
+ })();
90
+ }
bin/menu.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ // menu.js — npu-forge front door (start.bat runs this).
3
+ 'use strict';
4
+ const readline = require('readline');
5
+ const path = require('path');
6
+ const { spawnSync, spawn } = require('child_process');
7
+
8
+ const FORGE = path.join(__dirname, '..');
9
+ const ENV = { ...process.env, PYTHONIOENCODING: 'utf-8' };
10
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
11
+ const pending = [], waiting = [];
12
+ let closed = false;
13
+ rl.on('line', l => { const w = waiting.shift(); if (w) w(l); else pending.push(l); });
14
+ rl.on('close', () => { closed = true; while (waiting.length) waiting.shift()(null); });
15
+ const ask = q => { process.stdout.write(q); return new Promise(r => { const b = pending.shift(); if (b !== undefined) { console.log(b); r(b); } else if (closed) r(null); else waiting.push(r); }); };
16
+
17
+ function forge(args) {
18
+ rl.pause();
19
+ spawnSync('node', [path.join(FORGE, 'forge.js'), ...args], { stdio: 'inherit', env: ENV, shell: true });
20
+ rl.resume();
21
+ }
22
+
23
+ (async () => {
24
+ while (true) {
25
+ console.log('\n==============================================');
26
+ console.log(' NPU-FORGE — your models, on your NPU');
27
+ console.log('==============================================');
28
+ console.log(' 1. doctor — NPU / driver / registry health');
29
+ console.log(' 2. list — models + your forge customs');
30
+ console.log(' 3. register — activate customs (UAC, one click)');
31
+ console.log(' 4. serve <tag> — start an NPU server');
32
+ console.log(' t. tune — how to fine-tune your own (guide)');
33
+ console.log(' q. quit');
34
+ const pick = ((await ask('\nPick: ')) || '').trim().toLowerCase();
35
+ if (pick === null || pick === 'q' || pick === '') break;
36
+ if (pick === '1') forge(['doctor']);
37
+ else if (pick === '2') forge(['list']);
38
+ else if (pick === '3') {
39
+ spawn('cmd', ['/c', 'start', '', path.join(FORGE, 'register-admin.bat')], { detached: true });
40
+ console.log('(UAC window launched — click Yes; results land in register-cmd-log.txt)');
41
+ }
42
+ else if (pick.startsWith('4')) {
43
+ const tag = pick.split(/\s+/)[1] || (await ask('model tag (e.g. llama3.2:1b): ')) || '';
44
+ if (tag.trim()) { console.log('(Ctrl+C stops the server and returns here)'); forge(['serve', tag.trim()]); }
45
+ }
46
+ else if (pick === 't') {
47
+ console.log('\nFine-tune your own voice onto the NPU (needs a free modal.com account):');
48
+ console.log(' 1. Make chats.jsonl — one line per exchange:');
49
+ console.log(' {"messages":[{"role":"user","content":"..."},{"role":"assistant","content":"..."}]}');
50
+ console.log(' 2. node forge.js tune chats.jsonl --name myvoice');
51
+ console.log(' 3. node forge.js register (one UAC click)');
52
+ console.log(' 4. flm run myvoice-forge:1b — your voice, on your NPU.');
53
+ console.log(' (~3 minutes and ~$0.10 of cloud time for a 1B; see README.md)');
54
+ }
55
+ }
56
+ rl.close();
57
+ })();
modal/train_ear_head.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """npu-forge-ear: train a tiny voice-verifier HEAD on EmbeddingGemma-300m
2
+ embeddings. At runtime the embeddings come from FLM's NPU (embed-gemma:300m,
3
+ unquantized) and the head is ~20KB of JSON applied in plain JS — a fully
4
+ NPU-accelerated 'trained ear' with zero extra runtimes.
5
+
6
+ modal run train_ear_head.py
7
+ """
8
+ import modal
9
+
10
+ app = modal.App("npu-forge-ear")
11
+ image = (
12
+ modal.Image.debian_slim(python_version="3.11")
13
+ .pip_install("torch==2.5.1", "transformers==4.56.2", "sentence-transformers==5.1.0", "numpy", "huggingface_hub")
14
+ .add_local_dir("C:/Users/Forgemind/Desktop/voice-harness/scratch/verifier-data", remote_path="/data")
15
+ )
16
+ vol = modal.Volume.from_name("npu-forge-out", create_if_missing=True)
17
+
18
+
19
+ @app.function(image=image, gpu="T4", timeout=2400, volumes={"/out": vol}, secrets=[modal.Secret.from_name("huggingface-token")])
20
+ def train():
21
+ import json, numpy as np, torch
22
+ from sentence_transformers import SentenceTransformer
23
+
24
+ def load(p):
25
+ return [json.loads(l) for l in open(p, encoding="utf-8") if l.strip()]
26
+
27
+ labels = json.load(open("/data/labels.json"))
28
+ lab2id = {l: i for i, l in enumerate(labels)}
29
+ train_rows, eval_rows, probe_rows = load("/data/train.jsonl"), load("/data/eval.jsonl"), load("/data/probe.jsonl")
30
+
31
+ candidates = ["google/embeddinggemma-300m", "unsloth/embeddinggemma-300m", "onnx-community/embeddinggemma-300m-ONNX"]
32
+ model = None
33
+ for cid in candidates:
34
+ try:
35
+ model = SentenceTransformer(cid, device="cuda")
36
+ print("[embed model] " + cid)
37
+ break
38
+ except Exception as e:
39
+ print(f"[skip] {cid}: {str(e)[:120]}")
40
+ if model is None:
41
+ raise RuntimeError("no embeddinggemma variant loadable")
42
+
43
+ def embed(rows):
44
+ return np.asarray(model.encode([r["text"] for r in rows], batch_size=64, show_progress_bar=False, normalize_embeddings=True))
45
+
46
+ Xtr, Xev, Xpr = embed(train_rows), embed(eval_rows), embed(probe_rows)
47
+ ytr = np.array([lab2id[r["label"]] for r in train_rows])
48
+
49
+ # multinomial logistic head, full-batch
50
+ d, k = Xtr.shape[1], len(labels)
51
+ W = torch.zeros(d, k, requires_grad=True)
52
+ b = torch.zeros(k, requires_grad=True)
53
+ Xt = torch.tensor(Xtr, dtype=torch.float32)
54
+ yt = torch.tensor(ytr)
55
+ opt = torch.optim.Adam([W, b], lr=0.05)
56
+ for i in range(400):
57
+ loss = torch.nn.functional.cross_entropy(Xt @ W + b, yt) + 1e-4 * W.pow(2).sum()
58
+ opt.zero_grad(); loss.backward(); opt.step()
59
+ print(f"[head] final loss {loss.item():.4f}, dim={d}")
60
+
61
+ def acc(X, rows):
62
+ pred = (torch.tensor(X, dtype=torch.float32) @ W + b).argmax(-1).numpy()
63
+ per = {}
64
+ for r, p in zip(rows, pred):
65
+ s = per.setdefault(r["label"], [0, 0]); s[1] += 1
66
+ if labels[p] == r["label"]: s[0] += 1
67
+ overall = sum(v[0] for v in per.values()) / max(1, len(rows))
68
+ return overall, {l: f"{v[0]}/{v[1]}" for l, v in per.items()}
69
+
70
+ ev_overall, ev_per = acc(Xev, eval_rows)
71
+ pr_overall, pr_per = acc(Xpr, probe_rows)
72
+
73
+ head = {"labels": labels, "dim": d, "normalize": True,
74
+ "W": W.detach().numpy().round(5).tolist(), "b": b.detach().numpy().round(5).tolist()}
75
+ import os
76
+ os.makedirs("/out/ear-head", exist_ok=True)
77
+ with open("/out/ear-head/head.json", "w") as f:
78
+ json.dump(head, f)
79
+ vol.commit()
80
+ return {"eval_overall": round(ev_overall, 4), "eval_per_class": ev_per,
81
+ "probe_recognized": pr_per, "probe_overall": round(pr_overall, 4),
82
+ "head_kb": round(len(json.dumps(head)) / 1024)}
83
+
84
+
85
+ @app.local_entrypoint()
86
+ def main():
87
+ import json
88
+ print(json.dumps(train.remote(), indent=2))
start.bat ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ title NPU-Forge
3
+ cd /d "%~dp0"
4
+ where node >nul 2>nul
5
+ if errorlevel 1 (
6
+ echo [!] Node.js is required. Free download: https://nodejs.org - pick LTS, one click.
7
+ pause
8
+ exit /b 1
9
+ )
10
+ node "%~dp0bin\menu.js"
11
+ pause