Skip to content

serve, gpu-gates: /v1/embeddings over an EmbeddingGemma 2 model held beside the chat model (--embd-model), with its serve gate - #323

Merged
juliankang4 merged 7 commits into
midagedev:mainfrom
juliankang4:dgx-spark-emb2-serve
Oct 16, 2026
Merged

juliankang4 merged 7 commits into
midagedev:mainfrom
juliankang4:dgx-spark-emb2-serve

Conversation

@juliankang4

Copy link
Copy Markdown
Collaborator

Part of #59 (plan and PR order in #190), E6: the embeddings endpoints, served by an EmbeddingGemma 2 model that bloomery-serve holds beside its chat model.

Builds on #299, now merged. The seven commits are listed under "What the PR adds".

What and why

A server started with --embd-model FILE or --embd-hf <repo>[:<quant>] loads an embedding model beside its chat model in the same process and answers llama-server's embeddings routes from it: POST /v1/embeddings in the /v1 shape, and POST /embedding and /embeddings in llama-server's own array. EmbeddingGemma 2 is the architecture it serves. The encoder is #299's; this PR picks its card, takes its bytes out of the chat plan and serves it.

Release notes: Added: /v1/embeddings, /embedding and /embeddings over an EmbeddingGemma 2 model held beside the chat model (--embd-model FILE or --embd-hf <repo>[:<quant>]), with MRL dimensions and llama-server's embd_normalize.

The maintainer's comment on #190 asked for three things. Items 1 and 2 are in this PR:

  • /v1/embeddings answers with the embedding model's name in model (its file name), never the chat model's. A request whose model names the chat model or any other model is a 400 invalid_request_error in the /v1 error shape, and no pass runs.
  • /v1/models lists the embedding model as its own data entry after the chat model, with meta.n_embd and meta.n_ctx; models::listing takes a list of models.

My reply on #190 put items 2 and 3 in a follow-up. This PR already changes the listing to take more than one model, so item 2 is here too. Item 3, an embedding_model field in /props, stays in that follow-up PR.

What the PR adds

One contract a commit, seven commits:

  1. serve: the prompt reader, the panic text, the base64 encoder and the models listing each have one owner. No answer changes. prompt_ids reads with the tokenizer it is given, so a second vocabulary can read a prompt as /completion does. api::panic_text replaces the worker's and the decision server's copies of the panic message reader. media::encode_base64 is the one base64 encoder; serve_client::base64 re-exports it and the media tests' copy is gone. models::listing takes a list of models.
  2. serve: the first engine failure is the one /health, the board and the exit name. A behavior change for the existing engine path too. Before, each failure path wrote the fatal record, killed the board with its own reason and sent its own end, so a second failure overwrote the first. Now Shared::record_fatal adopts only the first failure and Shared::end_on sends that one to the end channel. A worker test holds two failures to it.
  3. gpu-gates: the reserves a plan holds beside its tensors have one owner, beside. A move. image_plan's reserve_on, reserve_in, with_reserve, note and refusal move to crates/gpu-gates/src/beside.rs. Beside names each reserve a plan holds on its stage card, one field per user (image, and from commit 5 embedding). The reserve name, the verdict note and the refusal text stay word for word, so by the code no seat's plan or record moves.
  4. serve: the embeddings endpoints, over an embedding model beside the engine. crates/serve/src/embed.rs is the seam: an Embed takes one input's ids and returns its pooled vector, and names its tokenizer, name, context and the widths it can be cut to. crates/serve/src/embeddings.rs reads a request as llama-server's handle_embeddings_impl does at llama.cpp 6868a52c:
    • input before content, and the array split rule (an array with an id at its top level is one input, else each element is one).
    • Text is tokenized with the embedding model's vocabulary. The first text element of each input gets its special tokens: BOS first where the GGUF sets add_bos_token, EOS last where it sets add_eos_token (Tokenizer::add_eos, false by default, so no existing model changes).
    • embd_normalize as common_embd_normalize, encoding_format float or base64, and dimensions, which cuts the vector to one of the model's widths before it is normalized.
    • With no embedding model, every route is a 501 not_supported_error.
    • A request checks the server's end before it waits for the model and again before every pass, so once the engine has failed or a stop has begun it gets a 503 and runs no pass.
    • A model failure, a panic, a vector of another width or a non-finite value ends the server as an engine failure does, through commit 2's record_fatal and end_on.
  5. gpu-gates: the side seat, an embedding model a generative seat serves beside its own. crates/gpu-gates/src/side_seat.rs:
    • Flags::take reads --embd-model FILE or --embd-hf <repo>[:<quant>], llama-server's -m and -hf behind --embd-. A flag with no value, a flag given twice and both flags at once are refused by name.
    • Flags::read reads the file after the seat's levers and before its plan. Its architecture must be a row of ROWS (crates/gpu-gates/src/arch/embedding.rs); the one row is gemma-embedding2, whose bytes are gpu, gpu-gemma-embedding2, gpu-gates: the EmbeddingGemma 2 encoder from the common kernels, with its end-to-end gate #299's card_bytes at the model card's 8,192 tokens.
    • Side::seat picks the card by the image encoder's rule: the smallest card the chat plan leaves off, else the stage card. On the stage card, every plan, whole-fit probe and placement-choice probe of the seat holds the model's bytes as the embedding model reserve through Beside. The embedding_seat record (architecture, card, bytes, reserve, context) is written once, after the placement is chosen.
    • SideEngine opens the model after the chat model loads, on a thread of its own with its own card stream, and implements Embed over a channel to that thread. No pass runs on the chat engine's thread. A panic in the open or in a pass comes back as a named failure.
    • EmbeddingGemma 2's row (side_seat/gemma_embedding2.rs) opens gpu, gpu-gemma-embedding2, gpu-gates: the EmbeddingGemma 2 encoder from the common kernels, with its end-to-end gate #299's Encoder on the chosen card and returns Pass::pooled, so the output projection and the mean stay in the encoder. Its widths are the 128, 256 and 512 cuts below the file's embedding_length_out, plus that full width; a file whose output is narrower than 128 is refused at open.
    • The generative seats take the model. The decide and nemotron seats refuse it, and -m refuses a file of an embedding architecture, naming --embd-model.
    • The three serving binaries require gpu, gpu-gemma-embedding2, gpu-gates: the EmbeddingGemma 2 encoder from the common kernels, with its end-to-end gate #299's gemma-embedding2 feature; the build recipes, the release build, docs/BUILD.md and docs/contrib/dgx-spark.md build with it. README.md's Use table and docs/serving.md describe the routes, and gate-ds41-bind runs the side seat's unit tests.
  6. gpu-gates: EmbeddingGemma 2 beside a chat seat, its vectors, reserve and greedy tokens. The serve gate, just gate-gpu-gemma-embedding2-serve, described under "What I ran".
  7. gpu-gates: every gate's file digest is the crate's sha256. gpu, gpu-gemma-embedding2, gpu-gates: the EmbeddingGemma 2 encoder from the common kernels, with its end-to-end gate #299 moved sha256 to the crate root. Four gate binaries and one shared taps file still carried their own copies; they now call the crate's. No answer changes.

Choices #190 left open, and our defaults. The maintainer can change any of these here:

  • The flags are --embd-model and --embd-hf only (section 7, question 2 of model: EmbeddingGemma and EmbeddingGemma 2 on a side seat #190). An embedding model's image projector waits for E7, so there is no --embd-mmproj yet.
  • An input's limit is the model card's 8,192 tokens, read from one place, hparams::CARD_CTX; the GGUF says 262,144 (question 3). A longer input is a 400 exceed_context_size_error, with no flag to raise it.
  • /embedding and /embeddings ignore model, as llama-server's native routes do; only /v1/embeddings checks it.
  • The model is called through Embed on its own thread, not through the hidden-row media call that item 4 of model: EmbeddingGemma and EmbeddingGemma 2 on a side seat #190's section 3 named. The pooling still belongs to the encoder, as model: EmbeddingGemma and EmbeddingGemma 2 on a side seat #190 planned.
  • The reserve is a field of Beside rather than a name argument to reserve_on, so each plan states both reserves in one value.

Code shape

  • No second copy. The base64 encoder, the panic text, the models listing, the fatal record and the file digest each have one owner. The plan reserve has one owner, beside, with two users. The mean over positions is gpu, gpu-gemma-embedding2, gpu-gates: the EmbeddingGemma 2 encoder from the common kernels, with its end-to-end gate #299's Pass::pooled; the serve crate has no pooling of its own.
  • Model code holds only architecture facts. The serve side has no branch per model. EmbeddingGemma 2's own facts (its bytes, its context, its widths) sit in its row and in arch/embedding.rs.
  • Dead code is gone. serve_client::base64's body, the media tests' encoder, image_plan's reserve functions, the two copies of the panic reader and the five file-digest copies in the gate sources are removed.
  • No silent failure. A vector of the wrong width or with a non-finite value ends the server by name. A p below -1, a norm f32 cannot scale by, a width the model does not cut to and an output narrower than 128 are named errors.
  • Comments state what is true now.
  • No relaxed gate. The serve gate's bands start from the encoder gate's pooled floor 1e-2, which carries a PIN(2026-10-15): line in crates/refset/src/arch/gemma_embedding2/mod.rs. tools/defaults-twins.tsv gains three rows for the gate's pinned levers (a twin for BLOOMERY_DRAFT=off, dated reasons for the card budget and BLOOMERY_RESIDENCY=off), and tools/recipes.py's exact ds41 selection set gains the new gate with a PIN line, because the gate builds bloomery-serve.
  • cargo fmt is clean (under "What I ran"), and the Mac lint step counts the same 59 warnings as main.

Duplication found or removed (path:line, before this PR's commits): crates/gpu-gates/src/serve_client.rs:260 (base64) and crates/serve/src/media.rs:708 (the tests' encode), now crates/serve/src/media.rs:629 (encode_base64); crates/serve/src/worker.rs:406 and crates/serve/src/decide.rs:615 (the panic message reader), now crates/serve/src/api.rs:517 (panic_text); the file digests in crates/gpu-gates/src/bin/gate_clef_hidden.rs, gate_qwen35_media.rs, gate_qwen3_serve.rs and shared/qwen3moe_taps.rs, now crates/gpu-gates/src/lib.rs:143 (sha256).

What I ran

The branch sits on main 38c41f4e, where #299 merged, as head b75dff1a03dda54c4d4e606b3e8691e12a54a1e7. Its seven commits are the ones the checks below ran on at eabeae22, except two deleted doc comment lines (the stale sha256 sentences above open_set in two gates) and the side seat's row naming its architecture through model::arch::GEMMA_EMBEDDING2 instead of a second spelling of it; before #299 merged they sat on main a688accb2c2b43fc4407984f4210db3978753c00, with #299 at 3e8da8dc1df636744c9e150417198787d47ca309 below it. The rebase kept this PR's code. Only the lines beside main's new Bolt and nemotron arms moved (the nemotron seat's bind call, the new nemotron serve recipe's feature set, the ds41 expected set in tools/recipes.py), and tools/gate-paths.tsv and tools/unsafe-ratchet.txt were regenerated. At eabeae22 I ran again the host checks, just contrib-check --tier t0, the Mac static tier, the PTX comparison and the gates just affected selects (last item below). The serve gate, the CUDA build, the FAIL-first edits and the record schemas below are from the earlier head e1624025ef3a77fb3ca0f3d2677731aa2d6f297b on base d66ef090ccc31aa426074f2eaa52ae7c3f17955e, the same code before the rebase. Each says so. Machine for everything but the static tier: a DGX Spark in the project's dev container (CUDA 13.3.1), nvidia-smi:

NVIDIA GB10, 595.84, 12.1

A check that is red here and on main a688accb alike says so.

Host checks, at eabeae22. cargo fmt --all -- --check ended rc 0. cargo build -p bloomery-serve --bin bloomery-serve && cargo test --release -p bloomery-serve -p bloomery-vision -p bloomery-refset ended rc 0, with 232 passed; 0 failed for the serve library tests.

Static tier, on a Mac, at eabeae22. ./tools/mac-static.sh upstream/main:

mac-static: fmt-check rc 0 in 11 s
mac-static: lint rc 1 in 39 s, ^warning: 59, above the ratchet 41
mac-static: combos rc 0 in 78 s, 50 shapes run, 0 red, 7 skipped (scoped)
mac-static: check-recipes rc 1 in 198 s
mac-static: red steps: lint (rc 1) check-recipes (rc 1)

Both red steps are red on main a688accb on the same Mac. The lint step counts 59 warnings there too, from the same warning source files, and check-recipes shows the same FAIL cold run: the base side runs in a temp archive, away from this tree's git and ptx-compare: self-test FAIL (33 cases, 1 failed). I did not look for the cause. Every other check-* step is rc 0.

just contrib-check --tier t0, at eabeae22 and on a688accb: rc 1 on both, with DONE tier=t0 total=84 red=1 and the one red step check-recipes. Its gpu-gate self-test fails on the same line on both: FAIL --v41-queue runs under BLOOMERY_BOX_READONLY=1 and syncs nothing: rc 1 (want 0). The other 83 steps are green. I did not run a higher tier.

CUDA build (head e1624025). cargo oxide build --arch sm_121 -- -p bloomery-gpu-gates --features gpu,deepseek41,glm5next,vision,mimo2,clef,gemma-embedding2 --release --bins finished in 3m 17s with Cargo build succeeded, so the serving binaries and the serve gate binary build with the new feature. The PTX comparison below builds the three serving binaries again at eabeae22.

The serve gate (head e1624025). BLOOMERY_BOX_LOCAL=1 just gate-gpu-gemma-embedding2-serve runs Qwen3-30B-A3B-Instruct-2507 Q4_K_M as the chat model at a 12 GiB card budget, which leaves some experts on the host, with one visible card so the embedding model sits on the stage card. It checks:

  • the plans: at the same budget the side seat's plan puts fewer experts on the card (plan_expert_cost); with the reserve added to the budget it gives the plain split back (plan_same_split); the embedding_seat record names gpu, gpu-gemma-embedding2, gpu-gates: the EmbeddingGemma 2 encoder from the common kernels, with its end-to-end gate #299's card_bytes at 8,192 tokens (reserve_record); the cards term of Plan::pool_need counts the reserve (pool_cards_count_reserve);
  • the vectors: every text case of the server reference set through /v1/embeddings, raw and normalized, and the shortest case's 128, 256 and 512 cuts, against mainline llama-server's vectors. The bands are derived from the encoder gate's pooled band and the CPU twin, not measured serving distances;
  • the chat model: 512 greedy tokens the same with and without the embedding model, and the same while an embedding request on the longest reference input runs, which must see at least two decode steps (concurrent_greedy_same);
  • the routes: the listing, the 501 with no embedding model, the 400s for the chat model's name, an unknown name, dimensions 129 and an 8,193-token input, and -m given the embedding file.

The gate ran 70 s and exited 0: 188 checks PASS and none FAIL, 171 of them the vector comparisons. The concurrent check saw 2 decode steps during one 1734-token embedding. I did not run the gate again at eabeae22.

FAIL-first (head e1624025). Three groups of edits that break what a check holds. In each, the edited tree failed and the restored file passed.

The serve crate's width, finite-value and lock tests against edits that remove those checks. Each edit failed the one test that guards it (rc 101) and the restored file passed (rc 0):

host-mutant short-no-width mutant=101 restored=0 clean=0
host-mutant nan-no-finite mutant=101 restored=0 clean=0
host-mutant no-locked-open mutant=101 restored=0 clean=0
host mutation checks ok=True count=3

Two route tests, one for model on the native routes and one for the model type on /v1/embeddings: ROUTE-MUTANT model-on-native mutant=101 restored=0 and ROUTE-MUTANT oai-model-type mutant=101 restored=0.

The serve gate's clauses against 13 edits to the plan, the reserve, the vector path and the guards. Twelve edits made the gate exit 1 on the clause that holds the property, and the restored file passed (exit 0). The thirteenth, vector-shift, stayed inside the gate's bands: 172 comparisons were accepted, so the gate does not claim a detected fault for it. The log ends GPU-MUTANTS aggregate ok=True count=13.

The gates just affected selects against base a688accb2c2b43fc4407984f4210db3978753c00: still running on the card when this was posted; the result goes into this description when it ends.

PTX (at eabeae22). This stack adds one kernel entry, head_norm_neox_append_256_whole in crates/gpu/src/rope_neox.rs, and it comes from #299. This PR adds no kernel. just ptx-compare refuses sm_121 (the scan's blk/SM column is sm_86's occupancy arithmetic), so I ran its steps by hand on the card: cargo oxide build --arch sm_121 of each binary at base and head, PTX_SCAN_ARCH=sm_121 tools/ptx-scan.sh <bin>, then python3 tools/recipes.py ptx-compare on each pair. The comparison reads each scan's stderr and stdout joined, in that order.

ptx-compare: bin=bloomery-serve base=a688accb2c2b head=eabeae22 method=decl1 arch=sm_121 jit-cc=12.1
  base entries  347
  head entries  348
  added         1: head_norm_neox_append_256_whole
  removed       none
  changed       none
ptx-compare: added-only (rc 0)

bloomery-serve-ds41 gives the same result (347 base entries, 348 head entries, the same one added) and bloomery-serve-qwen38 gives 264 and 265 with the same one added. All three are rc 0.

Record schemas (head e1624025). The --records-schema output of bloomery-serve (with a Qwen3.8-Flash-Next file), bloomery-serve-ds41 and bloomery-serve-qwen38 was compared with the checked-in snapshots, which gain the embedding_seat kind. All three are byte-equal and the check ends records-refresh: PASS. I did not run the full just records-refresh, which also rewrites snapshots this PR does not touch.

What I ran into

None of this blocks this pull request.

  • An input past the context. llama-server checks its physical batch (n_ubatch) first and answers an input past it with a 500; past the context it answers exceed_context_size_error. This server has no batch limit there, so an input past the model's context is a 400 exceed_context_size_error naming both lengths.
  • Object inputs. llama-server reads {"prompt_string": …} and a content array of parts. Here an object input is a 400 naming the types it takes.
  • embd_normalize. Mainline computes a p-norm with a negative p for any value below -1, and its scale can come to zero or infinity for a large p. Here a value below -1, and a norm f32 cannot scale by, are a 400.
  • Two side models on one card. The image encoder and the embedding model each take the smallest card the plan leaves off. With --mmproj and --embd-model on a machine with one such card, both land there and nothing counts the two together; the second open fails by name if the card is short. docs/serving.md says so.

Hardware (if the change is for a machine)

  • The static tier on a Mac (Apple silicon), rustc 1.100.0-nightly (e457a7b0d 2026-08-27), just 1.58.0.
  • The host checks, contrib-check, the PTX comparison, the build, the record schemas, the FAIL-first edits, the serve gate and the affected gates on a DGX Spark (NVIDIA GB10, 595.84, 12.1), in the project's dev container (CUDA 13.3.1).
  • The models: ggml-org/embeddinggemma-2-GGUF embeddinggemma-2-Q8_0.gguf (sha256 2188ac1d..., the file the reference sets were dumped from) and Qwen3-30B-A3B-Instruct-2507 Q4_K_M.

AI help

AI 코딩 에이전트가 코드를 작성한 후 설명을 썼고, 테스트를 마쳤습니다. diff 리뷰와 PR 초안 리뷰는 제가 직접 했습니다.

…models listing each have one owner

prompt_ids reads with the tokenizer it is given, so a second vocabulary can
read a prompt as /completion does; special puts that tokenizer's BOS first
where it adds one, the rule the callers spelled with add_bos before. The
panic message reader is api::panic_text, which the worker and the decision
server call in place of their two copies. media::encode_base64 is the one
base64 encoder: the media tests' copy and serve_client's are gone, and
serve_client::base64 re-exports it for the gates. models::listing takes a
list of Listed models, the servers each passing their one. No answer
changes.
… exit name

Shared::record_fatal adopts a failure only when none was adopted before:
it sets the reason /health and every later request give, closes the
waiting requests' and actions' channels on it, and hands it back for
Shared::end_on, which sends it to the end channel once the requests that
met it have their answers. A later failure changes none of the three and
sends nothing. The worker's failure paths and its caught panic go through
the pair. Before this a later failure overwrote the reason, killed the
board again with its own, and sent a second end, so /health, a waiting
request and the process exit could each name a different failure once a
second thread can fail the server.
…r, beside

image_plan's reserve_on, reserve_in, with_reserve, note and refusal move
to beside.rs, keyed by what the reserve is for: Beside names each reserve
a plan holds on its stage card beside its own tensors, the image
encoder's tower its one field in this commit. The moved functions give the
names, bytes and lines image_plan gave: "image encoder" as the reserve,
the verdict note and the plan refusal word for word, so by the code no
seat's plan or record moves. image_plan keeps the media load's
per-position table term, and vision_seat re-exports only that. gate-paths
gains a beside.rs row with the gates that reached the reserve, and
image_plan's row narrows to the gates that still reach the table term.
…ngine

POST /v1/embeddings, /embeddings and /embedding read a request as
llama-server's embeddings handler does: input or content, the array split
rule (an array with an id is one input, else each element is one), the
embedding vocabulary's specials on each input's text (BOS first, EOS last
where the vocabulary adds one: Tokenizer::add_eos, false by default),
encoding_format float or base64, embd_normalize as common_embd_normalize,
and the OpenAI-format list or llama-server's own array. dimensions cuts the pooled
vector to a width the model names. The model sits behind serve::embed's
Embed trait (Server::with_side): one input's ids in, its pooled vector out;
the tokenizing, the cut, the normalization and the answer are the
server's, and /v1/models lists the model second.

/v1/embeddings accepts only the side model's name when model is set,
and refuses a name of another model or a value of another type before
running a pass. /embedding and /embeddings ignore model, as llama-server's
native routes do. Optional fields treat null as unset.

Where llama-server differs, the difference is named: an object input
(prompt_string, a content array of parts) is a 400; an input past the
model's context is a 400 exceed_context_size_error naming both lengths
(llama-server's 500 is its physical batch check, which this server does
not have); a norm f32 cannot scale by (a large embd_normalize p) and a p
below -1 are 400s, where common_embd_normalize gives zeros, infinities or
a negative p-norm. With no model the routes answer a 501
not_supported_error.

A request reads the server's end before the model lock and again under
it, before every pass: once the engine has failed or a stop has begun it
is a 503 and runs no pass. A model failure, a panic, a vector of another
width or a non-finite value ends the server as an engine failure does,
through Shared::record_fatal and end_on, so the first failure is the one
the server names. panic_text is public, so a model run on a thread of its
own names its panic the same way.
… beside its own

side_seat reads --embd-model FILE or --embd-hf <repo>[:<quant>] before
the chat plan. Its gemma-embedding2 row uses the encoder's card_bytes
at the model card's 8192-token bound, below the file's context_length.
The unimplemented gemma-embedding row stays out of the table.

The image encoder's seat rule picks the smallest card outside the plan,
else the stage card. Every plan, whole-fit probe and placement-choice
probe takes the stage reserve through Beside, including Qwen3.8's offer
before its tier count is chosen. The embedding_seat record names its
architecture, card, bytes, reserve and context once after the placement
is chosen, without recording the candidates. Its record group is required
by the side-seat entry and registered by the serving seats.

After the chat model loads, SideEngine opens the embedding model on its
own thread and card stream. The gemma-embedding2 feature opens Encoder
on the selected card and wraps its pass, which already owns output
projection and mean pooling. The side row reads embedding_length_out:
its MRL widths are the 128, 256 and 512 cuts below that width, plus the
full width. An output below 128 is refused at open. The HTTP layer cuts
before normalizing. Its model id is the file name.
The three serving bins require that feature; their build recipes and
release build use it, without changing the feature sets of unrelated
binaries built beside them.

A panic in open or a pass becomes a named failure. Drop closes the
channel and joins the thread. Listen.side attaches the model to the
server. The generative seats take it; decide and nemotron refuse one.
-m refuses an embedding architecture and names --embd-model.

The README Use row and serving and build instructions describe the
attached encoder, including native model-field handling and the separate
opens of image and embedding models on an off-plan card. gate-ds41-bind includes the side-seat tests. The
embedding thread stays in spawn-allow beside the engine thread.
…and greedy tokens

gate_gemma_embedding2_serve runs Qwen3-30B-A3B-Instruct-2507 Q4_K_M at a 12 GiB card budget that leaves host experts. The embedding reserve costs card experts at that budget; adding its bytes restores the plain split. The plan record names the bytes, and the real plan rows count them in the unified pool cards term, also exercised as a formula on discrete cards.

Every text case of the server reference set runs through /v1/embeddings, raw and normalized; the shortest also checks the MRL cuts. Cosine and maximum-absolute bands are derived from the encoder gate pooled relative band, the CPU twin and the retained norm. They are predictions, not measured serving distances. All distances must be finite.

The gate requires the same greedy tokens with the side seat and at least two decode steps during one embedding request using the longest reference input. It also checks that request's vector against the server set. It registers the server-spawn record group and waits up to 600 seconds for startup, as the Qwen3 serve gate does. The remaining checks cover the separate model listing, model identity, missing seat, dimensions, context and embedding-as-chat refusals. The recipe builds the release feature set and leaves all logs under its own directory.
@github-actions github-actions Bot added dgx-spark The DGX Spark (aarch64, GB10) port documentation Improvements or additions to documentation gpu The card engine: crates/gpu*, crates/app host Host crates: qdot, threads, engram, engram-lab, runtime, sampler, levers, decision, vision, refset needs-gpu Changes code a card must check, and carries no gate run on a card serve The HTTP server: crates/serve tools Tools, recipes, CI and the toolchain: tools/, justfile, .github/, Cargo labels Oct 16, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: CLEAN: No reachable behavior defect or proven quality defect survives review.

Checked

Duplicates: 150 new or moved Rust item headers checked against existing owners. Dryer reported zero pairs in serve/refset. Manual searches covered gpu-gates; the reserve, digest, panic-message and base64 moves reuse common owners.

Complexity: New serve-gate run (72) combines setup, plan assertions, serving and refusal checks; those are its stage boundaries. concurrent (24) coordinates requests and observations; vectors (16) walks the comparison matrix; embeddings answer (20) validates and renders requests. Existing run growth adds optional side-model handling: drive 26→30, qwen38 127→131, ds41 51→54, glm 69→72, mimo2 35→38, qwen3 52→55. API routing adds two arms, 27→29.

Silent failure: 15 added Result/default sites followed. Missing architecture and invalid context reach named errors. Zero reserve means an off-plan seat. Disconnected reply channels have no remaining consumer. Missing decode metrics cannot make the concurrency clause pass.

Tests: Static mutation checks covered normalization arithmetic, special-token insertion, input splitting, context boundaries, model selection, dimensions, response shapes, base64 padding, reserve accounting, thread ownership, first-failure retention and record registration. Dropping the width/finite guards or the shutdown recheck makes their assertions fail. The PR reports FAIL-first runs for width, NaN, shutdown and model guards; its vector-shift mutant remains inside the admitted bands. Tests were not executed in this review.

Gates: Existing encoder bands retain their values and dated PIN when moved. Reserve tests move with their implementation. New serving bands derive from the existing encoder floor and CPU twin.

Adversarial paths: Empty and over-context inputs fail before inference. A request waiting behind a failed or stopping server gets 503 before its pass. Placement probes and loaders carry the same stage-card reserve. Excessive normalization powers produce a named error when their scale is invalid.

Performance: No decode kernel or steady-step dispatch work changes. The new embedding worker copies 4 × input-token-count bytes per pass; this is separate from chat token dispatch.

Escape catalog: No introduced C1–C4 defect found. The changed records-refresh pipeline enables pipefail.

Notes

Dryer did not search gpu-gates; it was searched manually. The PR supplies the NVIDIA GB10 card line, commit hashes, model names and serve-gate command, but summarizes the gate result without its command output. Final affected-gate results are also pending in the description. GPU evidence is recorded as incomplete.

Card check: needs-gpu. This pull request changes crates/gpu-gates/Cargo.toml, crates/gpu-gates/src/arch/embedding.rs, crates/gpu-gates/src/beside.rs … and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

@juliankang4

Copy link
Copy Markdown
Collaborator Author

The review is done and the contribution check passed, so I'm merging this.

@juliankang4
juliankang4 merged commit 8551182 into midagedev:main Oct 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dgx-spark The DGX Spark (aarch64, GB10) port documentation Improvements or additions to documentation gpu The card engine: crates/gpu*, crates/app gpu-verified host Host crates: qdot, threads, engram, engram-lab, runtime, sampler, levers, decision, vision, refset needs-gpu Changes code a card must check, and carries no gate run on a card serve The HTTP server: crates/serve tools Tools, recipes, CI and the toolchain: tools/, justfile, .github/, Cargo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants