Skip to content

serve: the built-in template pins cover the two chat models the stick itself ships - #1158

Merged
bong-water-water-bong merged 3 commits into
mainfrom
i-tmplsec-stick-pins
Nov 5, 2026
Merged

bong-water-water-bong merged 3 commits into
mainfrom
i-tmplsec-stick-pins

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Second commit 1796a81b: short (Qwen3-0.6B-Q8_0.gguf, 57f1fd00…) was the one laya-first
routed row with no expectation — three of the four were pinned, the smallest was not. It is in now, so all
four router rows agree
with models/pins.json, and the table stands at 12 rows: every model this engine
can serve by name today.

Second commit 1796a81b: short (Qwen3-0.6B-Q8_0.gguf, 57f1fd00…) was the one laya-first
routed row with no expectation — three of the four were pinned, the smallest was not. It is in now, so all
four router rows agree
with models/pins.json, and the table stands at 12 rows: every model this engine
can serve by name today.

Root cause. config/template-pins.json pins 7 names, and not one of them is a row the appliance
ships
. The c26 stick's staged marker is Qwen3-4B-Instruct-2507-1bit
(unsloth/Qwen3-4B-Instruct-2507-GGUF:Qwen3-4B-Instruct-2507-Q4_K_M.gguf) and Qwen3-0.6B-NPU-1bit
(the FastFlowLM/Qwen3-0.6B-NPU2 container) — read from the shipping payload's
bin/resources/server_models.json (~/wt/payload-c26c, 2026.47.0~8.f44ff953). An unpinned model is
logged, not checked (docs/serve.md), so on the appliance's own default chat row
--template-pin-strict refuses nothing: the owner's open decision "flip the appliance's serve rows to
strict" would have bought that model nothing at all.

Measured, not guessed. Both digests come from the bytes we ship, and the container one matches an
independent instrument:

new pin (2 names each) digest where it comes from
qwen3-4b-instruct-2507-1bit, qwen3-4b-instruct-2507-q4_k_m.gguf c979e0e71a3e21b8f208e6ab120d5cb29327885f29d2a8b18fda67a723798e18 the staged Qwen3-4B-Instruct-2507-Q4_K_M.gguf on ~/stage-c26 (identical file to the c25 tree, 16/16 files compared), equal to models/pins.json's template_sha256 for the same path
qwen3-0.6b-npu-1bit, qwen3-0.6b-npu2 a136047ba140347b359d17b6ff8fa1bf8cb29d23f0c07bef7e0bf479b8680793 the container's tokenizer_config.json chat_template string — the digest #1095's serve line printed on a real container; not the JSON file's own digest (51015f78…), which would be two numbers for one template

Both names per row are real paths: the catalog id is what Lemonade passes as --alias, the file/dir name
is what a hand -m load uses — serve matches id first, then the file name, case-insensitively.

The test makes the claim checkable, and bites. tests/template_pin.sh gains one case: a fixture named
qwen3-4b-instruct-2507-q4_k_m.gguf whose template is not the pinned one must warn with that digest.
Verified both ways — against the current main build (~/wt/onebp-zyphra/build-main-merged/1bit, built
11:56 today from the merged #1089/#1095) every existing case passes and the new case fails (row
absent), so the assertion detects the gap it was written for; with this change,
cmake -S . -B /tmp/tp-cfg generates generated/template_pins_default.h containing the new names, which
is exactly what the binary compiles in. CI runs the suite (add_test(NAME template_pin …) + ctest), so
the green case is proven there rather than on my word.

Doc: one paragraph in docs/serve.md naming what the table covers and why a row is added from a measured
digest. No engine code changed; nothing about the appliance's launch line changes here (that is the
packaging decision, §9).

…easured templates

`config/template-pins.json` pins 7 names, and none of them is a row the appliance itself serves: the
stick's staged marker is `Qwen3-4B-Instruct-2507-1bit` (unsloth/Qwen3-4B-Instruct-2507-GGUF,
`Qwen3-4B-Instruct-2507-Q4_K_M.gguf`) and `Qwen3-0.6B-NPU-1bit` (the FastFlowLM/Qwen3-0.6B-NPU2
container) — the last row of ci.yml's appliance table and the one a stick image boots with. So on the
appliance's own default chat row, `--template-pin-strict` checked nothing: an unpinned model is logged,
not refused (docs/serve.md), and the owner's open decision "flip the serve rows to strict" would have
bought the appliance's model nothing.

Four rows added, keyed on both names a row can be served under (the catalog id that becomes `--alias`
and the file/dir name), with digests measured rather than guessed:

  qwen3-4b-instruct-2507-1bit                    c979e0e71a3e21b8…  (the staged
  qwen3-4b-instruct-2507-q4_k_m.gguf             c979e0e71a3e21b8…   Qwen3-4B-Instruct-2507-Q4_K_M.gguf
                                                                        on the c26 staging tree; equal to the
                                                                        digest models/pins.json records for it)
  qwen3-0.6b-npu-1bit                            a136047ba140347b…  (the container's tokenizer_config.json
  qwen3-0.6b-npu2                                a136047ba140347b…   `chat_template` string — the digest
                                                                        #1095's serve line printed on a real
                                                                        container, not the JSON file's own)

`tests/template_pin.sh` gains the case that makes the claim checkable: a fixture named
`qwen3-4b-instruct-2507-q4_k_m.gguf` whose template is not the pinned one must warn with that digest.
Verified in both directions — against the current main build (wt/onebp-zyphra/build-main-merged/1bit,
built 11:56 today from the merged #1089/#1095) every existing case passes and **the new case fails**, so
it detects the missing row; `cmake -S . -B /tmp/tp-cfg` then generates
`generated/template_pins_default.h` containing the new names, which is what the binary compiles in.

CI runs the suite (`add_test(NAME template_pin ...)`, ci.yml `ctest`), so the pass is proven there.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#1158 ready for review at 7aa4c7d.

…rry an expectation

The router's four classes are the one place a model is pulled and then served by name
(docs/laya-first.md). Three of them were pinned by file name; `short` — Qwen3-0.6B-Q8_0.gguf,
digest 57f1fd00f0013a2b… in models/pins.json — was not, so the smallest model in the routing table was
the one a strict pin could not refuse. All four now agree with models/pins.json's measured digests.

Doc: the coverage sentence says so, instead of listing the stick plus demo/scoreboard.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Second commit 1796a81b closes the other half of the same gap, found by checking the claim rather than asserting it.

docs/laya-first.md makes the router's four classes the one place a model is pulled and then served by name. Three of them were pinned by file name; short was not — so of the four routing rows, the one a strict pin could not refuse was the smallest one. Added from models/pins.json's measured digest:

row file digest
short Qwen3-0.6B-Q8_0.gguf 57f1fd00f0013a2be96aa79b857391f27e23df5b5f847072b524c897e24d0361

All four routed rows now have an expectation, and each digest in config/template-pins.json equals the one my independent audit (models/pins.json, read over HF range requests) recorded for the same file:

short     Qwen3-0.6B-Q8_0.gguf                     pinned=True agree=True
prose     zaya1-8b-Q4_K_M.gguf                     pinned=True agree=True
code      Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf pinned=True agree=True
long_doc  Qwen3.8-27B-UD-Q4_K_XL.gguf              pinned=True agree=True

Where the table now stands: 12 rows — the stick's two chat models (2 names each), the four router rows, and the demo/scoreboard names that were already there. That is every model this engine can serve by name today, which is what §9's --template-pin-strict decision needs to be safe. Nothing else changed: no engine code, no launch line.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#1158 ready for review at 1796a81 (2 commits).

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY

All four new digests check out against the files they name, and the one that first looked wrong is the interesting one.

config/template-pins.json is at 12 rows at this head, and the four laya-first routed rows all carry an expectation now: short qwen3-0.6b-q8_0.gguf 57f1fd00…, prose zaya1-8b-q4_k_m.gguf b55ecfbb…, code qwen3-coder-30b-a3b-instruct-q4_k_m.gguf 87710339…, long_doc qwen3.8-27b-ud-q4_k_xl.gguf 12827f24….

Re-derived by fetching each source and hashing the template the same way the tool does (GGUF metadata / container chat_template string):

row source the pin names digest I compute
qwen3-4b-instruct-2507-1bit, qwen3-4b-instruct-2507-q4_k_m.gguf unsloth/Qwen3-4B-Instruct-2507-GGUF c979e0e71a3e21b8… = the pinned c979e0e71a3e
qwen3-0.6b-npu-1bit, qwen3-0.6b-npu2 FastFlowLM/Qwen3-0.6B-NPU2 a136047ba140347b… = the pinned a136047ba140
qwen3-0.6b-q8_0.gguf 1bit-MONSTER/Qwen3-0.6B-GGUF 57f1fd00f0013a2b… = the pinned 57f1fd00f001

That last row is worth a sentence because my first cross-check got a different digest for the same file name: unsloth/Qwen3-0.6B-GGUF:Qwen3-0.6B-Q8_0.gguf hashes to 5da44855ab7e0641…, while 1bit-MONSTER and Qwen's own repo both give 57f1fd00…. Same file name, different upload, different template — so the "source" field these rows carry is load-bearing, not decoration, and the row is right because it names the repo it took the file from.

The claim about what the stick ships also holds: ~/wt/payload-c26c/lemonade-onebit-rc-2026-11-05-c26c/bin/resources/server_models.json contains both Qwen3-4B-Instruct-2507 and Qwen3-0.6B-NPU.

Test: built 1bit at this head (ONEBIT_NPU=ON, the private npu_rt) and ran the PR's own gate, bash tests/template_pin.sh <1bit> -> all 12 ok, including ok the appliance's staged chat model is a built-in pin (exit 0) and the directory-route cases (names its tokenizer_config.json template, names its chat_template.jinja template, mismatch warns / strict refuses, "none" with a template warns). So the new row is exercised through the same path a booted appliance uses, not only by reading the JSON.

Merge note, not a blocker: this is the same coupling #1157 has - both PRs describe or rely on models/pins.json, which is #1148 (open, now READY at 5c2a3c3c). #1158 does not cite that file, so it stands alone; #1157 does, and still needs its citation fixed or #1148 landed first.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

CI green at 1796a81b: build 6m43s (CodeQL + all five Analyze pass, hw_gate skipped, pr_agent red only because coder-llm is off). The new case ran in CI, not just locally — ctest at that head:

Start  8: template_pin
8/88 Test  #8: template_pin .....................   Passed    0.16 sec

and it bites: against the 11:56 build of merged main, the same script reports FAIL the appliance's staged chat model is a built-in pin while every pre-existing case passes.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

End-to-end evidence for the claim, on the real staged files, with the already-merged binary — not just the fixture. Binary: ~/wt/onebp-zyphra/build-main-merged/1bit (built 11:56 today from merged main, so it has #1089/#1095). Pin table: this branch's config/template-pins.json via --template-pin (so the rows are exercised exactly as they will be once compiled in).

# model table result
1 stick GGUF Qwen3-4B-Instruct-2507-Q4_K_M.gguf, --alias Qwen3-4B-Instruct-2507-1bit new + --template-pin-strict template sha256=c979e0e7… — passes, rc 0
2 same, same alias built-in (pre-PR) + strict template sha256=c979e0e7…, no warning, rc 0 — nothing checked, which is the gap: the appliance's own model was unpinned
3 same, same alias new + strict, with that row's sha256 zeroed expects 0000…, the model carries c979e0e7… : refused, --template-pin-strict, rc 1 — the row bites
4 NPU container Qwen3-0.6B-NPU2, by catalog alias Qwen3-0.6B-NPU-1bit new + strict template sha256=a136047b… — passes, rc 5 (--print-backend on a directory, i.e. after the line)
5 same container staged as the appliance does it (/data/models/Qwen3-0.6B-NPU2, no --alias) new + strict matches the directory-name row qwen3-0.6b-npu2 → template sha256=a136047b…, passes
6 same, no alias new + strict, that dir-name row zeroed expects 0000…: refused, --template-pin-strict, rc 1

So both keys a row can be served under are exercised on real bytes (the catalog alias, and the file/dir name the stick actually uses), the digests are the ones this PR records, and strict mode neither false-refuses the shipped media nor tolerates a different template under the same name. The two negatives (2, 3, 6) are the part worth checking: they show the rows are load-bearing rather than inert data.

bong-water-water-bong pushed a commit that referenced this pull request Nov 5, 2026
…pen PR would add

The reviewer's blocker, and it is the same class as the text being fixed: the paragraph said the digests
for the four rows "are already measured — models/pins.json records the template of every file we serve and
publish", and `models/pins.json` does not exist in this tree (`ls` fails, `git cat-file -e
origin/main:models/pins.json` fails). It is engine #1148, open, so a reader on `main` was sent to a path
that is not there, and the "already measured" claim rested on an unmerged working tree — including the one
digest that exists nowhere else yet (`short`'s 57f1fd00f0…).

Now the paragraph says what a reader can check today:

  - three of the four rows are in `config/template-pins.json` **now** — zaya1-8b-q4_k_m.gguf
    b55ecfbbbc…, qwen3-coder-30b-a3b-instruct-q4_k_m.gguf 87710339d2…, qwen3.8-27b-ud-q4_k_xl.gguf
    12827f24b7… — because they are the same files the load-time check already pins (verified against the
    tree at this head);
  - the fourth, `short`'s Qwen3-0.6B-Q8_0.gguf 57f1fd00f0…, is **not in any table in this tree yet**; it is
    measured, it is added by #1158, and it is recorded with every other served/published file in #1148's
    models/pins.json — said as such rather than implied to be here.

The Q4NX sentence now links docs/npu-chat-template.md, which is in this tree (#1115). Nothing else in the
paragraph changed: what runs today, what it catches, and what step 3 adds all verified in the review.
bong-water-water-bong added a commit that referenced this pull request Nov 5, 2026
…one that does not (#1157)

* docs/laya-first: say which template check runs today, and that the per-row digest is step 3

The section ended with a present-tense claim the tree does not meet: that
`~/.local/share/1bit/laya-first.json` "ships with the digests measured on
2026-11-05" and that "the table's validator (§8 step 3) fails" a row that skips
one. Measured on main: `config/laya-first.json` — the file compiled in as
`laya_first_default.h` — has no `template_sha256` in any of its four rows,
nothing in `app/`, `src/`, `tests/`, `include/` or `os/` reads one, and no
installed `<laya home>/laya-first.json` exists on the c25/c26 media, the staging
trees or the box (§8 step 3 is where that field and the pull-time check are
scheduled). A design note that asserts a check which does not run is the exact
failure this row is supposed to prevent, so the paragraph now states what is
true:

  - the one template check on a routed pull today is the load-time one, against
    the engine's `config/template-pins.json` — 7 names, three of them these rows;
  - that check warns unless the row ships `--template-pin-strict` (§9), and it
    catches one thing: the same name carrying a different template;
  - the field, the per-row requirement and the pull-time refusal are §8 step 3,
    and the digests they will need are already measured — `models/pins.json`
    records 57f1fd00f0… (short), b55ecfbbbc… (prose), 87710339d2… (code),
    12827f24b7… (long_doc).

Doc only; no code, no table and no operand changed.

* docs/laya-first: cite the digests that are in this tree, not one an open PR would add

The reviewer's blocker, and it is the same class as the text being fixed: the paragraph said the digests
for the four rows "are already measured — models/pins.json records the template of every file we serve and
publish", and `models/pins.json` does not exist in this tree (`ls` fails, `git cat-file -e
origin/main:models/pins.json` fails). It is engine #1148, open, so a reader on `main` was sent to a path
that is not there, and the "already measured" claim rested on an unmerged working tree — including the one
digest that exists nowhere else yet (`short`'s 57f1fd00f0…).

Now the paragraph says what a reader can check today:

  - three of the four rows are in `config/template-pins.json` **now** — zaya1-8b-q4_k_m.gguf
    b55ecfbbbc…, qwen3-coder-30b-a3b-instruct-q4_k_m.gguf 87710339d2…, qwen3.8-27b-ud-q4_k_xl.gguf
    12827f24b7… — because they are the same files the load-time check already pins (verified against the
    tree at this head);
  - the fourth, `short`'s Qwen3-0.6B-Q8_0.gguf 57f1fd00f0…, is **not in any table in this tree yet**; it is
    measured, it is added by #1158, and it is recorded with every other served/published file in #1148's
    models/pins.json — said as such rather than implied to be here.

The Q4NX sentence now links docs/npu-chat-template.md, which is in this tree (#1115). Nothing else in the
paragraph changed: what runs today, what it catches, and what step 3 adds all verified in the review.

---------

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
@bong-water-water-bong
bong-water-water-bong merged commit 0f7c9a2 into main Nov 5, 2026
9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the i-tmplsec-stick-pins branch November 5, 2026 22:40
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

1095 - Partially compliant

Compliant requirements:

  • Template pinning now supports model directories with tokenizer_config.json or chat_template.jinja files
  • announce_template no longer returns early for non-GGUF models
  • Documentation updated to reflect directory-based models are covered
  • Test cases added for directory-based models with various template configurations

Non-compliant requirements:

  • None

Requires further human verification:

  • None

1089 - Partially compliant

Compliant requirements:

  • Template sha256 logging added for GGUF models
  • Template pinning implemented with config/template-pins.json
  • --template-pin-strict flag and ONEBIT_TEMPLATE_PIN_STRICT environment variable added
  • Documentation updated to cover template pinning functionality
  • Tests added for template pinning functionality

Non-compliant requirements:

  • None

Requires further human verification:

  • None
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Template Pin Coverage

The new template pins include entries for models that are shipped with the appliance, specifically 'qwen3-0.6b-npu-1bit', 'qwen3-0.6b-npu2', 'qwen3-0.6b-q8_0.gguf', 'qwen3-4b-instruct-2507-1bit', and 'qwen3-4b-instruct-2507-q4_k_m.gguf'. These entries ensure that all models that can be served by name today are covered by the built-in template pins, which is important for ensuring consistent behavior across different deployment scenarios.

"qwen3-0.6b-npu-1bit": {
 "sha256": "a136047ba140347b359d17b6ff8fa1bf8cb29d23f0c07bef7e0bf479b8680793",
 "source": "FastFlowLM/Qwen3-0.6B-NPU2"
},
"qwen3-0.6b-npu2": {
 "sha256": "a136047ba140347b359d17b6ff8fa1bf8cb29d23f0c07bef7e0bf479b8680793",
 "source": "FastFlowLM/Qwen3-0.6B-NPU2"
},
"qwen3-0.6b-q8_0.gguf": {
 "sha256": "57f1fd00f0013a2be96aa79b857391f27e23df5b5f847072b524c897e24d0361",
 "source": "1bit-MONSTER/Qwen3-0.6B-GGUF"
},
"qwen3-4b-instruct-2507-1bit": {
 "sha256": "c979e0e71a3e21b8f208e6ab120d5cb29327885f29d2a8b18fda67a723798e18",
 "source": "unsloth/Qwen3-4B-Instruct-2507-GGUF"
},
"qwen3-4b-instruct-2507-q4_k_m.gguf": {
 "sha256": "c979e0e71a3e21b8f208e6ab120d5cb29327885f29d2a8b18fda67a723798e18",
 "source": "unsloth/Qwen3-4B-Instruct-2507-GGUF"
},
Documentation Update

The documentation has been updated to clarify that the template pin table now includes rows for models shipped with the appliance. This ensures that users understand the scope of template pinning coverage and that all models that can be served by name are properly covered by the built-in pins.

The table carries the rows the appliance itself ships — the stick's `Qwen3-4B-Instruct-2507-1bit` GGUF and
its `Qwen3-0.6B-NPU-1bit` container, whose digest is the `chat_template` string from
`tokenizer_config.json` ([npu-chat-template.md](npu-chat-template.md)) — plus the demo and scoreboard rows and all four `laya-first` routing rows.
A model with no row is logged, not checked, which is why a row is added with its measured digest rather
than guessed (`models/pins.json` records that digest for every file we serve and publish).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant