Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/npu.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,18 @@ roles from `layer_types` and projection shapes from the model's own dimensions,
`--check` re-generates and diffs against the file on disk, so a stale plan is a gate failure rather than a
wrong number; `--print` shows it without writing.

`container.tile_order` is the fourth decision written down, and it is the one that cannot be probed at all:
it names the q4_1 tile order the payload is in (`engine-1`, what this repo's converter writes). A container
from another converter can be a **different order**, and a wrong order decodes as noise *with no error
anywhere* — every structural check passes (same tensor names, dtypes, byte counts, chunk kinds), the container
loads, and the model simply answers badly (measured: one FLM converter checkout's containers answer the same
token five times where ours answer " Paris."). Absent means `engine-1`, so every container written before the
field existed keeps loading; an id this decoder does not implement is a refusal naming it, because the
alternative is to guess a tile order at the one point where a guess is silent.

The forward reads the plan with `npu/plan/plan.h` and **refuses by name** anything it cannot honour — a
`kernels.tag` that disagrees with the container, a sealed ELF named outside the pb16i family, a decode dir
`container.tile_order` other than `engine-1`, a `kernels.tag` that disagrees with the container, a sealed ELF
named outside the pb16i family, a decode dir
that is absolute or contains `..`, a layer count that disagrees with `layers[]`, or a kernel the plan names
that is not in the directory (`forward init failed: plan <path>: this model's plan needs the QKV kernel
"full_i8_QKV_K1024_N4096.elf" in <dir> (missing)`). A model directory **without** a plan keeps the probes,
Expand Down
19 changes: 19 additions & 0 deletions npu/plan/plan.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ struct Plan {
// "causal" is the default and the only mode that can generate; "noncausal_full_attention" is a
// prompt-encoding mode (every full-attention layer sees the whole prompt, which is what a d3
// readout pack is trained with), so the forward refuses to generate with it by name.
// The q4_1 tile ORDER the container's payload is written in (engine#1110). A container from another
// converter can be a different order, and a wrong order decodes as *noise with no error at all* --
// measured: one FLM converter checkout's containers answer a repeated single token (five times the same
// character) where ours answers " Paris.", and every structural check passes either way (same tensor
// names, dtypes, byte counts and chunk kinds). "engine-1" is what tools/npu_plan.py writes and
// the only order this decoder implements; absent means the same thing, so every container written
// before this field existed keeps loading, and present-but-unknown is a refusal naming the id --
// the only point at which a mis-ordered container can be caught before it answers confidently and
// wrongly.
std::string tile_order = "engine-1";
std::string attention_mode = "causal";
bool noncausal() const { return attention_mode == "noncausal_full_attention"; }
// Where the decode family is looked for, in order: which given path it hangs off, and the
Expand Down Expand Up @@ -171,6 +181,15 @@ struct Plan {
if (p.layout != "rm" && p.layout != "b8" && p.layout != "pb16i" && p.layout != "t2s")
throw PlanError("plan: " + path + " container.layout \"" + p.layout +
"\" is not rm, b8, pb16i or t2s");
// Optional, defaulted on purpose: a container written before this field existed is in our
// order, so this is not a format bump. An id we do not implement is refused rather than
// decoded at a guess (engine#1110).
p.tile_order = c.value("tile_order", "engine-1");
if (p.tile_order != "engine-1")
throw PlanError("plan: " + path + " container.tile_order \"" + p.tile_order +
"\" is not \"engine-1\", the only q4_1 tile order this engine decodes: "
"re-convert the model with this repo's tools/npu_plan.py rather than serving tiles "
"whose order we would have to guess (engine#1110)");
}
if (j.contains("fusion")) {
const auto& fz = obj(j, "fusion", path);
Expand Down
4 changes: 4 additions & 0 deletions tests/npu_plan_generate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ def touch(path):
check(plan["layers"][0]["qkv"]["N"] == (16 + 2 * 8) * 128, "attn QKV N is (nh + 2*nkv) * hd")
check(plan["layers"][1]["attn_gate"]["N"] == 8 * 128, "GDN gate N is the value width")
check(plan["decode"]["dirs"][-1] == {"root": "kernels", "path": "."}, "the kernel dir is the last decode dir")
# engine#1110: the generator declares the payload's q4_1 tile order, so a container from another
# converter (a different order that would decode as noise with no error) is refused at load.
check(plan["container"]["tile_order"] == "engine-1",
"the plan declares container.tile_order = engine-1 (engine#1110)")

# 2. --check agrees with what is on disk, and catches a kernel set that changed under it.
r = run(d, "--check")
Expand Down
26 changes: 26 additions & 0 deletions tests/npu_plan_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,31 @@ void test_valid() {
"a _t2s2 tag on a pb16i container is refused by name");
}

void test_tile_order() {
// engine#1110: a container declares the q4_1 tile order its payload is in. Absent means our order
// (every plan written before the field existed keeps loading); an id this decoder does not implement
// is refused by name, because a wrong order decodes as noise with no error anywhere else.
{
auto p = base_plan();
const Plan got = Plan::load(write_plan("tileorder_absent", p));
check(got.tile_order == "engine-1", "a plan without container.tile_order means engine-1");
}
{
auto p = base_plan();
p["container"]["tile_order"] = "engine-1";
check(Plan::load(write_plan("tileorder_ours", p)).tile_order == "engine-1",
"container.tile_order engine-1 loads");
}
{
auto p = base_plan();
p["container"]["tile_order"] = "flm-2026-10";
const std::string why = refusal(write_plan("tileorder_unknown", p));
check(has(why, "container.tile_order \"flm-2026-10\"") && has(why, "engine-1"),
"an unknown tile order is refused by name: " + why);
check(has(why, "engine#1110"), "the refusal points at the reason it exists");
}
}

void test_refusals() {
// The whole point: nothing is defaulted, every refusal names the field.
{
Expand Down Expand Up @@ -269,6 +294,7 @@ void test_refusals() {

int main() {
test_valid();
test_tile_order();
test_refusals();
std::printf("%s\n", failures ? "FAILED" : "npu_plan: ok");
return failures ? 1 : 0;
Expand Down
33 changes: 33 additions & 0 deletions tools/npu_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ def generate(model_dir: str, kernels: str) -> dict:
"container": {
"layout": layout,
"chunk_kind": "pb16i" if layout in ("pb16i", "t2s") else "mode0",
# The tile order of the payload this plan describes (engine#1110). Declared so a container
# from another converter -- a different order that would decode as noise with no error -- is
# refused by name at load instead of being served. Absent means this same value.
"tile_order": "engine-1",
"pb16i": layout in ("pb16i", "t2s"),
},
"fusion": {"gate_up": "fused" if fused_gu else "split"},
Expand Down Expand Up @@ -262,6 +266,28 @@ def moe_batched(cfg: dict, hidden: int, model_dir: str, kernels: str) -> dict:
return {"enabled": False, "K": 0, "N": 0}


def _only_defaulted_tile_order(on_disk: str, generated: str) -> bool:
"""True when the ONLY difference is a container.tile_order the on-disk plan does not carry.

engine#1110 added `container.tile_order`; absent means "engine-1", which is what the generator writes,
so an older plan is not stale -- it is silent about a field whose default is the generated value.
"""
try:
a = json.loads(on_disk)
b = json.loads(generated)
except ValueError:
return False
ca, cb = a.get("container"), b.get("container")
if not isinstance(ca, dict) or not isinstance(cb, dict):
return False
if "tile_order" in ca or cb.get("tile_order") != "engine-1":
return False
ca = dict(ca)
ca["tile_order"] = "engine-1"
a["container"] = ca
return a == b


def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("model_dir", help="the model directory (config.json + model.q4nx)")
Expand Down Expand Up @@ -296,6 +322,13 @@ def main(argv=None) -> int:
except OSError as exc:
print(f"{out}: {exc.strerror} (run without --check to write it)", file=sys.stderr)
return 1
if on_disk != text and _only_defaulted_tile_order(on_disk, text):
# A plan written before container.tile_order existed means exactly what the generated one says
# ("engine-1"), so a plan gate must not go red over a field whose absence has the same meaning.
# The loader makes the same assumption (npu/plan/plan.h), which is why this is not a fudge.
print(f'{out}: matches the model dir (written before container.tile_order, which defaults to '
f'"engine-1" -- the value it generates)')
return 0
if on_disk != text:
print(f"{out}: differs from what the model dir generates -- regenerate it", file=sys.stderr)
for line in __import__("difflib").unified_diff(
Expand Down
Loading