Skip to content

laya: an encoder registry; serve --laya runs the encoder on the NPU through the private add-on (ONEBIT_LAYA_DEVICE=npu) - #227

Merged
bong-water-water-bong merged 5 commits into
mainfrom
laya-npu-encoder
Sep 29, 2026
Merged

bong-water-water-bong merged 5 commits into
mainfrom
laya-npu-encoder

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

This opens the Laya router to the NPU (geramyl: "HRX + LOOM + NPU"). The engine gets only a generic hook. The NPU code stays in the private add-on.

Engine side (public):

  • laya/encoder.h: a registry of encoders by name. An encoder gets a sequence's embedded rows [rows][1024] with n real tokens, runs the 28 layers in place, and may decline.
  • Scorer::set_encoder: score() hands every sequence to the set encoder. If the encoder declines any of them, the whole batch runs on the CPU as before.
  • serve --laya with ONEBIT_LAYA_DEVICE=npu uses the C++ scorer with the npu encoder instead of the GGUF daemon. It fails before loading the checkpoint, saying how to build, when no add-on registered one.
  • tests/laya_encoder_test.cpp (ctest laya_encoder) covers the registry. tests/laya_gguf_route.sh checks the missing-add-on message.
  • Docs: docs/laya.md ("The encoder on the NPU") and docs/npu.md.

Private side, for reference: npu-kernels addons/npu-laya registers npu. It takes the engine's NPU device lock without waiting, and declines while another process holds it, so those decisions run on the CPU. It keeps its hardware context together with the lock for 2 s after a call, then releases both.

Measured on strixhalo (serve --laya with fake backends and tests/route-policy-e2e.json, real checkpoint):

  • Classes route correctly: code → hrx, prose → zinc, short → vulkan.
  • About 105 ms per warm decision: the NPU encoder is ~50 ms, the CPU head ~41 ms.
  • About 500 ms per decision while a peer holds the NPU lock (CPU fallback).
  • The lock is free again after 2 s idle.
  • Routing accuracy on the 200 cases with the NPU encoder is 194/200 = 97.0%; the CPU scorer gets 95.5%.

Local ctest passes 13/13.

🤖 Generated with Claude Code

bong-water-water-bong and others added 2 commits September 29, 2026 10:17
…uns the encoder on a private add-on

laya/encoder.h lets an add-on register a place for the scorer's 28 encoder
layers; Scorer::set_encoder hands it each sequence's embedded rows and runs
the layers on the CPU when it declines. The engine registers none: the
private NPU add-on (-DONEBIT_NPU_PRIVATE) registers "npu", and serve
--laya uses it with ONEBIT_LAYA_DEVICE=npu instead of the GGUF daemon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the checkpoint; tested

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@context7

context7 Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Docs7 for 1bit-monster/engine

Result Status Action
Deployment ➖ Not used —
Content review ➖ Did not run. This site has no agent runs available this month. Wait for the monthly reset or check your Docs7 plan. —

Commit fcda1bd

…d-on)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Preparing review...

1 similar comment
@github-actions

Copy link
Copy Markdown

Preparing review...

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Preparing review...

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit fcda1bd)

Here are some key observations to aid the review process:

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

Incorrect NPU encoder check

The check for ONEBIT_LAYA_DEVICE=npu occurs before the laya_daemon is initialized, but the laya_daemon is only created when !gguf.empty() && !bin.empty(). If gguf is empty but bin is not, or vice versa, the check for laya_npu will be skipped, and the code will proceed to try to load the encoder without ensuring that the NPU add-on is available. This could lead to a runtime error or an incorrect fallback behavior.

const onebit::laya::EncoderFactory* f = laya_npu ? onebit::laya::find_encoder("npu") : nullptr;
if (laya_npu && !f)
    throw std::runtime_error("laya: ONEBIT_LAYA_DEVICE=npu needs a build with the NPU add-on "
                             "(-DONEBIT_NPU_PRIVATE=<npu-kernels checkout>, docs/laya.md)");
Potential performance issue in encoder loop

The encoder loop iterates over all sequences in the batch (for (int b = 0; b < N && encoded; ++b)), and if any sequence is declined by the encoder, the entire batch falls back to CPU execution. This could lead to performance degradation if a small number of sequences are declined, as the entire batch must be processed on the CPU instead of just the declined ones.

    for (int b = 0; b < N && encoded; ++b)
        encoded = encoder_(out.data() + (size_t)b * L * D, L, (int)seqs[b].size());
    if (encoded) {
        h.swap(out);
        encoded_ += N;
    } else {
        declined_ += N;
    }
}

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit fcda1bd

@bong-water-water-bong
bong-water-water-bong merged commit b75e426 into main Sep 29, 2026
11 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the laya-npu-encoder branch September 29, 2026 13:40
bong-water-water-bong added a commit that referenced this pull request Nov 7, 2026
…t not in the docs (#1347)

The knob shipped with fork #216 and the pin now carries it (engine main c752b66 pins llama.cpp 092eb9dbc, and
469c69f5d is an ancestor), but docs/hrx.md never mentioned it. Rule: docs travel with the behaviour they describe.

What it says: Flash-Next's 48 MoE blocks are a quant mix (UD-IQ4_XS keeps IQ3_XXS on 28 of them), IQ3_XXS blocks cost
~2.4x the IQ4_XS blocks on identical geometry, and the knob selects a second router-expert kernel that decodes the
IQ3_XXS grid from a per-workgroup table. Off by default; unset or 0 is the shipping path.

Measured, one hold, control arm on the same binary: pp4096 270.62 -> 318.52 (1.18x) and 140.84 -> 182.10 (1.29x) in a
second window, pp512 177.62 -> 221.38 (1.25x), PPL 8x512 1.7198 -> 1.7198 identical (and identical in three separate
windows). The absolute tok/s carry their own warning: the same arm has read 182-318 tok/s across windows for identical
flags, so only paired ratios are quoted.

Why it stays opt-in, written into the section rather than left implicit: those two windows ran on a fork commit that
predates #227, and a 3,858-token prompt on it intermittently died in the CPU fallback of SET_ROWS (engine #1251). A
default change needs the long-prompt cell and test-backend-ops -o MUL_MAT -b HRX0 on the current pin; neither has been
run by this lane.

Part of #572.

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
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