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
7 changes: 6 additions & 1 deletion npu/pack_import.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3363,6 +3363,11 @@ ContainerGeometry container_geometry(const json& meta, const json& config) {
return g;
}

int64_t head_stored_rows(int64_t vocab, bool from_fconst_tail) {
if (!from_fconst_tail || vocab <= 0) return vocab;
return (vocab + kTileRows - 1) / kTileRows * kTileRows;
}

std::string qk_norm_refusal(bool declared, bool found) {
if (declared && !found)
return "this pack declares use_qk_norm and its per-head q/k gains are not where the block layout says "
Expand Down Expand Up @@ -5350,7 +5355,7 @@ Report import_pack(const std::string& pack_dir, const std::string& out_dir, cons
// (Hunyuan-0.5B: 120,818 rows = 1,887.8 tiles, so the tail holds 1,888 and the last one is partly
// padding). The rows past V are dropped before the tensor is written, so the container's head has
// exactly V rows.
const int64_t rows = (V + kTileRows - 1) / kTileRows;
const int64_t rows = head_stored_rows(V, true) / kTileRows;
// The envelope is the same `64 + 2*hidden` the layer blocks open with (eps, padding, the norm), not a
// constant: 1,856 is only its qwen2.5-0.5B value. Qwen3-1.7B's tail is 4,160 + 2,374 x 73,984 to the
// byte, which the constant would have missed by 2,304.
Expand Down
6 changes: 6 additions & 0 deletions npu/pack_import.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ struct QkGainOffsets {
QkGainOffsets qk_gain_offsets(int64_t gap_start, int64_t head_dim, int64_t gap_bytes);
std::string qk_norm_refusal(bool declared, bool found);

// How many rows a head read from the .fconst tail occupies in the container. The pack stores the head in whole 64-row
// tiles, so a vocabulary that is not a multiple of 64 is carried with the pack's own padding rows (Hunyuan: 120,818 ->
// 120,832) while `vocab_size` and the plan's tensor shape stay the vocabulary. A head that is a graph initializer does
// not go through this (its own padding, to the 32-row tile quantum, is engine#1992's). This is the number a container's lm_head tile count follows from.
int64_t head_stored_rows(int64_t vocab, bool from_fconst_tail);

// Why a pack's two layout witnesses cannot be reconciled, or "" when they can (engine#1985).
//
// `vote_hdrs` is the number of in-block headers the epsilon-anchor vote recorded (0 when it could not win); `declared_qk`
Expand Down
25 changes: 22 additions & 3 deletions tests/npu_pack_import_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3007,10 +3007,13 @@ void test_pack(const std::string& pack, const std::string& golden_dir) {
"pack: 64 columns per gate|up unit and per down_proj group");

int norms = 0, attn_planned = 0, mlp_planned = 0, lm_tiles_planned = 0;
bool head_in_tail = false;
for (const auto& t : p.tensors) {
if (t.name.find("layernorm") != std::string::npos || t.name == "model.norm.weight") ++norms;
if (t.dtype == "q4_1" && t.refusal.empty() && t.name.compare(0, 9, "lm_head.w") == 0)
if (t.dtype == "q4_1" && t.refusal.empty() && t.name.compare(0, 9, "lm_head.w") == 0) {
lm_tiles_planned = int((t.shape[0] / 32) * ((t.shape[1] + 255) / 256));
head_in_tail = t.source == "fconst:tail";
}
if (t.dtype == "q4_1" && t.refusal.empty() && t.name.find("self_attn") != std::string::npos) ++attn_planned;
if (t.dtype == "q4_1" && t.refusal.empty() && t.name.find(".mlp.") != std::string::npos) ++mlp_planned;
}
Expand Down Expand Up @@ -3143,8 +3146,12 @@ void test_pack(const std::string& pack, const std::string& golden_dir) {
// the container must open with the engine's own loader
onebit::npu::Model m(out);
const auto& head = m.tensor("lm_head.weight");
check(head.shape.at(0) == int64_t((head_rows / 32) * ((p.tensors.at(1).shape.at(1) + 255) / 256)),
"pack: lm_head tile count matches the container");
// A head read from the .fconst tail keeps the pack's padding to whole 64-row tiles (Hunyuan: 120,818 rows are stored
// as 120,832), so the container's count follows the stored rows; the plan's shape stays the vocabulary.
const int64_t stored_rows = pi::head_stored_rows(head_rows, head_in_tail);
check(head.shape.at(0) == (stored_rows / 32) * ((p.tensors.at(1).shape.at(1) + 255) / 256),
"pack: lm_head tile count matches the container (" + std::to_string(head.shape.at(0)) + " tiles for " +
std::to_string(stored_rows) + " stored rows)");
check(m.tensor("model.layers.0.input_layernorm.weight").shape.at(0) == p.dims.hidden,
"pack: a layer norm is `hidden` bf16 values, whatever the hidden size");
check(m.tensor("model.norm.weight").shape.at(0) == int64_t(p.dims.hidden),
Expand Down Expand Up @@ -5616,6 +5623,17 @@ void test_q4_writer_code_range() {
// engine#1889: the arithmetic that claims the per-head QK gains out of the 640 B Hunyuan-7B carries between its last qkv
// tile and o_proj. A wrong offset there reads tile bytes as bf16 and mis-rotates every pack of the family, so both the
// right shape (2 * (64 + head_dim*2) = 640 at head_dim 128) and the neighbours that must NOT be taken for it are driven.
// The rows a tail head occupies in the container: the pack's own padding to whole 64-row tiles stays. The pack leg's
// tile-count check used the vocabulary, so it failed on every pack whose vocabulary is not a multiple of 64 although
// the import was right (Hunyuan-1.8B: 15,104 tiles written, 15,100 expected).
void test_head_stored_rows() {
check(pi::head_stored_rows(120818, true) == 120832, "head rows: Hunyuan's 120,818 are stored as 120,832");
check(pi::head_stored_rows(151936, true) == 151936, "head rows: a multiple of 64 is stored as it is");
check(pi::head_stored_rows(49152 + 32, true) == 49152 + 64, "head rows: a multiple of 32 that is not one of 64 is padded");
check(pi::head_stored_rows(120818, false) == 120818, "head rows: a graph head is not padded");
check(pi::head_stored_rows(0, true) == 0, "head rows: no vocabulary, no rows");
}

void test_qk_gain_offsets() {
const pi::QkGainOffsets o = pi::qk_gain_offsets(1000, 128, 640);
// engine#1934: block 0 is Q. Hunyuan-7B's own model.onnx carries both gains as FP16 initializers, and q_norm is the
Expand Down Expand Up @@ -5678,6 +5696,7 @@ int main(int argc, char** argv) {
test_encode_tile_code_range();
test_q4_writer_code_range();
test_qk_norm_refusal();
test_head_stored_rows();
test_qk_gain_offsets();
test_tail_head_placement();
const std::string golden_dir = argc > 1 ? argv[1] : "";
Expand Down
Loading