diff --git a/npu/pack_import.cpp b/npu/pack_import.cpp index 0f8071d5f..c80dcdf32 100644 --- a/npu/pack_import.cpp +++ b/npu/pack_import.cpp @@ -407,10 +407,16 @@ OnnxModel parse_onnx(const std::string& path) { continue; } if (!w) { kept.push_back(mm); continue; } // names nothing this graph declares: not this genre's business + // Two spellings of the placeholder: 1.9 names it `.empty` beside `.packed`; the 1.8 hybrid line names the + // node's own `` as the `[0]` placeholder beside `.packed` (Llama-3.2-1B-Instruct_rai_1.8.0_hybrid: all 162 + // `.MatMulNBits.*` initializers dims [0] and no data, the weights in model.bin). Missing the second spelling kept + // the node as a real weight, so `info` reported every projection importable and `import` died on + // `...MatMulNBits.bias: no data` (engine#1503). std::string packed = w->name; if (packed.size() >= 6 && packed.compare(packed.size() - 6, 6, ".empty") == 0) packed.replace(packed.size() - 6, 6, ".packed"); - else { kept.push_back(mm); continue; } + else + packed += ".packed"; const OnnxTensor* pw = find_init(packed); // No sibling at all: not this genre's pack. Keep the node exactly as it was -- it still names its `*.empty` // initializer and the reader below decides what that means. Dropping it here would be a decision made by an @@ -3596,7 +3602,12 @@ Plan plan(const std::string& pack_dir) { pack_dir + ": " + onnx.weightless_example + ", and the pack carries no Token .fconst either (no dd_metastate_Llm_Token_*.fconst): these weights are " "genuinely absent from the pack's external data, so this pack cannot be imported -- not a tile-layout or " - "block-size problem"); + "block-size problem" + + // The 1.8 hybrid line keeps them in a `model.bin` beside the graph (1.3 GB on Llama-3.2-1B-Instruct), a + // container this importer does not read; saying where they are is what tells a reader which work is missing. + std::string(fs::exists(pack_dir + "/model.bin") + ? ". This pack's weights are in its model.bin, which this importer does not read (engine#1503)" + : "")); const bool no_fconst = tok_path.empty(); // The 1.9 external-data genre (phi-4: 201 `MatMulNBitsBf` ops whose bytes are `*.qweight.packed` siblings in // `eager-mm.onnx.data`) must NOT ride the graph-weights path below, which was written for the 1.3/1.6/1.7.1 line's diff --git a/tests/npu_pack_import_test.cpp b/tests/npu_pack_import_test.cpp index ae29dc890..dc879424c 100644 --- a/tests/npu_pack_import_test.cpp +++ b/tests/npu_pack_import_test.cpp @@ -1350,6 +1350,40 @@ void test_crafted_refusals(const std::string& root) { } check(!fs::exists(dir / "out3" / "model.qnx") && !fs::exists(dir / "out3" / "model.q4nx"), "crafted: nothing written for the empty .packed sibling"); + + // 4. engine#1503: the 1.8 hybrid spelling of the same thing. The node names `` ITSELF as the [0] placeholder (no + // `.empty` suffix) beside an equally empty `.packed`, and the weights are in a model.bin next to the graph + // (Llama-3.2-1B-Instruct_rai_1.8.0_hybrid: all 162 `.MatMulNBits.*` initializers are this shape). Before the fix the + // walker only knew the `.empty` spelling, kept the node as a real weight, `info` called every projection importable + // and `import` died much later on `...MatMulNBits.bias: no data`. + put_file((dir / "model.onnx").string(), + pb::model({pb::tensor("model.embed_tokens.weight", {8, 8}, "model.onnx.data", 0, 256), + pb::tensor("rai18_qweight", {0}, "", 0, 0), + pb::tensor("rai18_qweight.packed", {0}, "", 0, 0), + pb::tensor("rai18_scales", {0}, "", 0, 0), + pb::tensor("rai18_zeros", {0}, "", 0, 0)}, + {pb::matmulnbits("mm", {"x", "rai18_qweight", "rai18_scales", "rai18_zeros"}, 64, 512, 4, 128)})); + put_file((dir / "model.bin").string(), std::string(64, '\0')); + try { + (void)pi::plan(dir.string()); + check(false, "crafted 1.8: a [0] qweight with an empty .packed sibling was refused"); + } catch (const std::exception& e) { + const std::string what = e.what(); + std::printf(" crafted 1.8 placeholder refusal: %s\n", what.c_str()); + check(what.find("rai18_qweight (its rai18_qweight.packed sibling carries no data either)") != std::string::npos && + what.find("genuinely absent") != std::string::npos, + "crafted 1.8: plan() refuses by name, naming the placeholder and its sibling (" + what + ")"); + check(what.find("weights are in its model.bin") != std::string::npos, + "crafted 1.8: and says where the weights are when a model.bin is there (" + what + ")"); + } + fs::remove(dir / "model.bin"); + try { + (void)pi::plan(dir.string()); + check(false, "crafted 1.8 without model.bin: refused"); + } catch (const std::exception& e) { + check(std::string(e.what()).find("model.bin") == std::string::npos, + "crafted 1.8: no model.bin, so the refusal does not claim one (" + std::string(e.what()) + ")"); + } fs::remove_all(dir); }