Repository navigation
npu-pack: report which text artifacts a pack carries (info, import, container config) - #1030
Conversation
…ontainer config)
`1bit npu-pack import` copied a pack's tokenizer/chat files when it found them
and said nothing when it found none, so a weights-only pack produced a model
directory that only failed two commands later, at `1bit serve`
("... is not an NPU model directory (model.q4nx, config.json, tokenizer.json)").
That message reads like an engine bug, not an incomplete pack.
It is not hypothetical: the Qwen3-1.7B and Qwen3-4B pack dirs on this box held
5 files each (weights + config.json + cache/) because they were fetched weight
by weight, and the silence at import time is what let a session conclude "the
Qwen3 packs ship no tokenizer files" -- a false statement about AMD's repos,
which publish all eight text artifacts for both (npu-re FACTS §7.26). A pack's
text set also varies: Llama-3.2-1B ships a 17.2 MB tokenizer.json and no
vocab.json/merges.txt at all, so the importer reports the split rather than
assuming one.
- scan_text_artifacts(): one implementation of "which of the eight files does
this pack have", used by the copy loop, by `info` and by `import`.
- `info` carries text_artifacts {present, missing, servable}; info writes
nothing, so the gap is visible before an import.
- `import` prints "text: N of 8 tokenizer/chat files copied[, absent: ...]" or a
WARNING naming tokenizer.json and the files to drop beside model.q4nx.
- the container's config.json records q4nx_source.text_artifacts ("copied from
the pack" / "none in the pack"), so it still states this months later.
- servable is exactly "tokenizer.json is present", which is what docs/npu.md
requires of a model directory.
Measured, same tree/compiler, only these two files differ (binary before vs
after):
before: `info` on a 5-file pack -> 0 mentions of any text artifact;
`import --allow-incomplete` -> no text line, container = model.q4nx +
config.json only, and `1bit serve` then says "not an NPU model
directory (model.q4nx, config.json, tokenizer.json)".
after: `info` -> text_artifacts {present: [], missing: [8], servable: false};
`import` -> "text: none -- this pack ships no tokenizer.json" plus the
WARNING, and config.json records "none in the pack".
On a complete pack (Qwen2.5-0.5B, 71/71 files): "text: 8 of 8
tokenizer/chat files copied", the container carries all eight and
config.json records "copied from the pack"; adding tokenizer.json to
the weights-only dir changes `1bit serve`'s refusal from "model.q4nx,
config.json, tokenizer.json" to the real remaining one.
Tests: npu_pack_import_test's new test_text_artifacts (0 present/8 missing ->
2 present/6 missing, and the same fact through manifest()); `ctest -R npu_pack`
green, and the real-pack leg green on Qwen2.5-0.5B. Device-free throughout.
closes #1029
PR Reviewer Guide 🔍(Review updated until commit 970d18a)Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at e7594ce1
Built (-DONEBIT_NPU=ON, rc=0) and ran all three commands on real packs.
A pack with the text (Qwen2.5-0.5B-Instruct_rai_1.8.0_npu_4K):
$ 1bit npu-pack info <pack> -> "text_artifacts": {"present":[8 files],"missing":[],"servable":true}
$ 1bit npu-pack import <pack> -o /tmp/imp-text --allow-incomplete
text: 8 of 8 tokenizer/chat files copied
container config.json: "q4nx_source": {..., "text_artifacts": "copied from the pack"}
8 text files on disk
A weights-only pack (the engine#1024 case; every non-text artifact symlinked, so the import does the real work):
$ 1bit npu-pack info /tmp/pack-notext
"text_artifacts": {"missing":[all 8],"present":[],"servable":false}
$ 1bit npu-pack import /tmp/pack-notext -o /tmp/imp-notext --allow-incomplete
text: none -- this pack ships no tokenizer.json
WARNING: /tmp/pack-notext carries no tokenizer.json, so /tmp/imp-notext has weights and config.json
... model's own text artifacts there (tokenizer.json, tokenizer_config.json, ...
container: "text_artifacts": "none in the pack" and only config.json + model.q4nx were written
npu-pack_import_test golden legs still pass, and the refactor is clean: kTextFiles[] is now the single list and scan_text_artifacts() the single scanner, so the copier and the report cannot drift.
One real defect in the reporting, and it is the case the test does not cover. A pack that carries some text but no tokenizer.json is reported as carrying none:
$ 1bit npu-pack import /tmp/pack-partial ... (vocab.json + merges.txt, no tokenizer.json)
text: none -- this pack ships no tokenizer.json
$ ls /tmp/imp-partial -> merges.txt vocab.json <- both were copied
$ container: "text_artifacts": "copied from the pack"
$ 1bit npu-pack info /tmp/pack-partial
"present": ["vocab.json","merges.txt"], "missing":[6], "servable": false <- correct
So info and the container are right, and the console line says "none" while it copied two files. It matters because that line is one of the three outputs this PR exists to add, and the tests only ever write tokenizer.json and vocab.json together (test_text_artifacts), so the branch is never hit. Fix is two lines: print ta.present.size() of 8 and "not servable" when !ta.tokenizer and !ta.present.empty(), and keep "none" for the truly empty case; add the third case to the test.
Not a gate, because the field that decides whether a container runs (servable) is correct in every arm, and the wording wrong-way is "carries none" on a pack that cannot be served anyway.
One thing to know when reading import on this base: it still refuses the 0.5B pack without --allow-incomplete, because the final-norm refusal is the pre-#844 state of main — that is #844's fix, not this PR's, and it is the only refusal left on all four packs.
|
Persistent review updated to latest commit 970d18a |
Root cause
1bit npu-pack importcopied a pack's tokenizer/chat files when it found them and said nothing when it foundnone. A weights-only pack therefore produced a model directory with
model.q4nx+config.jsonand notokenizer.json, and the command that failed was1bit serve, two commands later:That reads like an engine bug, not an incomplete pack, and nothing in the import's output pointed at the pack.
This is not hypothetical. The Qwen3-1.7B and Qwen3-4B pack dirs on this box held 5 files each (weights +
config.json+cache/) because they were fetched weight by weight, while AMD's repos publish 71 files each —including all eight text artifacts (
tokenizer.json11,422,654 B,tokenizer_config.json,vocab.json,merges.txt,chat_template.jinja,special_tokens_map.json,added_tokens.json,generation_config.json).The silence at import time is what let a previous session record "the Qwen3 packs ship no tokenizer files" as a
fact about the vendor's packs (it is not;
amd-oss/npu-reFACTS §7.26, companion commit on the hub).The text set also varies per pack, so a fixed expectation is wrong in both directions: Llama-3.2-1B ships a
17.2 MB
tokenizer.jsonand novocab.json/merges.txt. The importer reports the split instead.Change
scan_text_artifacts()— one implementation of "which of the eight files does this pack have", used by thecopy loop, by
infoand byimport(one list, no drift between the three).npu-pack infogainstext_artifacts: {present, missing, servable}.infowrites nothing, so the gap isvisible before an import.
npu-pack importprintstext: N of 8 tokenizer/chat files copied[, absent from the pack: …], or aWARNING:namingtokenizer.jsonand the files to drop besidemodel.q4nx.config.jsonrecordsq4nx_source.text_artifacts(copied from the pack/none in the pack),so the container still states this months later.
servableis exactly "tokenizer.jsonis present", which is whatdocs/npu.mdrequires of a model directory.tools/gguf_to_q4nx.pywarning the pack path never had.Measured (before/after, same tree and compiler; the binaries differ only by these two files)
infoon a 5-file packtext_artifacts: {present: [], missing: [8], servable: false}import --allow-incompleteon itmodel.q4nx+config.jsontext: none -- this pack ships no tokenizer.json+ the WARNING;config.jsonrecordsnone in the packimporton a complete pack (Qwen2.5-0.5B, 71/71 files)text: 8 of 8 tokenizer/chat files copied; the container carries all eight;config.jsonrecordscopied from the pack1bit serveon the weights-only dir, before vs after addingtokenizer.jsonnot an NPU model directory (model.q4nx, config.json, tokenizer.json)not an NPU model directory (the fast lane needs npu/)— the tokenizer gap was that message; what is left is this host-only build's own missing pieceTests
test_text_artifactsintests/npu_pack_import_test.cpp: a pack with none reports 8 missing / notservable, and the same fact through
manifest(); with two files present it reports those two, six missing,servable.
ctest -R npu_packgreen; the real-pack leg green onQwen2.5-0.5B-Instruct_rai_1.8.0_npu_4K(
all npu-pack checks passed (including the pack leg))./dev/accel/accel0,:18097untouched.closes #1029