Repository navigation
serve, gpu-gates: /v1/embeddings over an EmbeddingGemma 2 model held beside the chat model (--embd-model), with its serve gate - #323
Conversation
…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.
There was a problem hiding this comment.
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)
|
The review is done and the contribution check passed, so I'm merging this. |
Part of #59 (plan and PR order in #190), E6: the embeddings endpoints, served by an EmbeddingGemma 2 model that
bloomery-serveholds 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 FILEor--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/embeddingsin the/v1shape, andPOST /embeddingand/embeddingsin 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,/embeddingand/embeddingsover an EmbeddingGemma 2 model held beside the chat model (--embd-model FILEor--embd-hf <repo>[:<quant>]), with MRLdimensionsand llama-server'sembd_normalize.The maintainer's comment on #190 asked for three things. Items 1 and 2 are in this PR:
/v1/embeddingsanswers with the embedding model's name inmodel(its file name), never the chat model's. A request whosemodelnames the chat model or any other model is a 400invalid_request_errorin the/v1error shape, and no pass runs./v1/modelslists the embedding model as its owndataentry after the chat model, withmeta.n_embdandmeta.n_ctx;models::listingtakes 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_modelfield in/props, stays in that follow-up PR.What the PR adds
One contract a commit, seven commits:
serve: the prompt reader, the panic text, the base64 encoder and the models listing each have one owner. No answer changes.prompt_idsreads with the tokenizer it is given, so a second vocabulary can read a prompt as/completiondoes.api::panic_textreplaces the worker's and the decision server's copies of the panic message reader.media::encode_base64is the one base64 encoder;serve_client::base64re-exports it and the media tests' copy is gone.models::listingtakes a list of models.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. NowShared::record_fataladopts only the first failure andShared::end_onsends that one to the end channel. A worker test holds two failures to it.gpu-gates: the reserves a plan holds beside its tensors have one owner, beside. A move.image_plan'sreserve_on,reserve_in,with_reserve,noteandrefusalmove tocrates/gpu-gates/src/beside.rs.Besidenames each reserve a plan holds on its stage card, one field per user (image, and from commit 5embedding). 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.serve: the embeddings endpoints, over an embedding model beside the engine.crates/serve/src/embed.rsis the seam: anEmbedtakes 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.rsreads a request as llama-server'shandle_embeddings_impldoes at llama.cpp6868a52c:inputbeforecontent, and the array split rule (an array with an id at its top level is one input, else each element is one).add_bos_token, EOS last where it setsadd_eos_token(Tokenizer::add_eos, false by default, so no existing model changes).embd_normalizeascommon_embd_normalize,encoding_formatfloatorbase64, anddimensions, which cuts the vector to one of the model's widths before it is normalized.not_supported_error.record_fatalandend_on.gpu-gates: the side seat, an embedding model a generative seat serves beside its own.crates/gpu-gates/src/side_seat.rs:Flags::takereads--embd-model FILEor--embd-hf <repo>[:<quant>], llama-server's-mand-hfbehind--embd-. A flag with no value, a flag given twice and both flags at once are refused by name.Flags::readreads the file after the seat's levers and before its plan. Its architecture must be a row ofROWS(crates/gpu-gates/src/arch/embedding.rs); the one row isgemma-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'scard_bytesat the model card's 8,192 tokens.Side::seatpicks 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 theembedding modelreserve throughBeside. Theembedding_seatrecord (architecture, card, bytes, reserve, context) is written once, after the placement is chosen.SideEngineopens the model after the chat model loads, on a thread of its own with its own card stream, and implementsEmbedover 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.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'sEncoderon the chosen card and returnsPass::pooled, so the output projection and the mean stay in the encoder. Its widths are the 128, 256 and 512 cuts below the file'sembedding_length_out, plus that full width; a file whose output is narrower than 128 is refused at open.-mrefuses a file of an embedding architecture, naming--embd-model.gemma-embedding2feature; the build recipes, the release build,docs/BUILD.mdanddocs/contrib/dgx-spark.mdbuild with it.README.md's Use table anddocs/serving.mddescribe the routes, andgate-ds41-bindruns the side seat's unit tests.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".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 movedsha256to 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:
--embd-modeland--embd-hfonly (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-mmprojyet.hparams::CARD_CTX; the GGUF says 262,144 (question 3). A longer input is a 400exceed_context_size_error, with no flag to raise it./embeddingand/embeddingsignoremodel, as llama-server's native routes do; only/v1/embeddingschecks it.Embedon 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.Besiderather than a name argument toreserve_on, so each plan states both reserves in one value.Code shape
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'sPass::pooled; the serve crate has no pooling of its own.arch/embedding.rs.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.pbelow-1, a norm f32 cannot scale by, a width the model does not cut to and an output narrower than 128 are named errors.1e-2, which carries aPIN(2026-10-15):line incrates/refset/src/arch/gemma_embedding2/mod.rs.tools/defaults-twins.tsvgains three rows for the gate's pinned levers (a twin forBLOOMERY_DRAFT=off, dated reasons for the card budget andBLOOMERY_RESIDENCY=off), andtools/recipes.py's exact ds41 selection set gains the new gate with aPINline, because the gate buildsbloomery-serve.cargo fmtis 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) andcrates/serve/src/media.rs:708(the tests'encode), nowcrates/serve/src/media.rs:629(encode_base64);crates/serve/src/worker.rs:406andcrates/serve/src/decide.rs:615(the panic message reader), nowcrates/serve/src/api.rs:517(panic_text); the file digests incrates/gpu-gates/src/bin/gate_clef_hidden.rs,gate_qwen35_media.rs,gate_qwen3_serve.rsandshared/qwen3moe_taps.rs, nowcrates/gpu-gates/src/lib.rs:143(sha256).What I ran
The branch sits on main
38c41f4e, where #299 merged, as headb75dff1a03dda54c4d4e606b3e8691e12a54a1e7. Its seven commits are the ones the checks below ran on ateabeae22, except two deleted doc comment lines (the stalesha256sentences aboveopen_setin two gates) and the side seat's row naming its architecture throughmodel::arch::GEMMA_EMBEDDING2instead of a second spelling of it; before #299 merged they sat on maina688accb2c2b43fc4407984f4210db3978753c00, with #299 at3e8da8dc1df636744c9e150417198787d47ca309below 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 intools/recipes.py), andtools/gate-paths.tsvandtools/unsafe-ratchet.txtwere regenerated. Ateabeae22I ran again the host checks,just contrib-check --tier t0, the Mac static tier, the PTX comparison and the gatesjust affectedselects (last item below). The serve gate, the CUDA build, the FAIL-first edits and the record schemas below are from the earlier heade1624025ef3a77fb3ca0f3d2677731aa2d6f297bon based66ef090ccc31aa426074f2eaa52ae7c3f17955e, 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:A check that is red here and on main
a688accbalike says so.Host checks, at
eabeae22.cargo fmt --all -- --checkended rc 0.cargo build -p bloomery-serve --bin bloomery-serve && cargo test --release -p bloomery-serve -p bloomery-vision -p bloomery-refsetended rc 0, with232 passed; 0 failedfor the serve library tests.Static tier, on a Mac, at
eabeae22../tools/mac-static.sh upstream/main:Both red steps are red on main
a688accbon the same Mac. The lint step counts 59 warnings there too, from the same warning source files, andcheck-recipesshows the sameFAIL cold run: the base side runs in a temp archive, away from this tree's gitandptx-compare: self-test FAIL (33 cases, 1 failed). I did not look for the cause. Every othercheck-*step is rc 0.just contrib-check --tier t0, ateabeae22and ona688accb: rc 1 on both, withDONE tier=t0 total=84 red=1and the one red stepcheck-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 --binsfinished in 3m 17s withCargo 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 ateabeae22.The serve gate (head
e1624025).BLOOMERY_BOX_LOCAL=1 just gate-gpu-gemma-embedding2-serveruns 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:plan_expert_cost); with the reserve added to the budget it gives the plain split back (plan_same_split); theembedding_seatrecord names gpu, gpu-gemma-embedding2, gpu-gates: the EmbeddingGemma 2 encoder from the common kernels, with its end-to-end gate #299'scard_bytesat 8,192 tokens (reserve_record); thecardsterm ofPlan::pool_needcounts the reserve (pool_cards_count_reserve);/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;concurrent_greedy_same);dimensions129 and an 8,193-token input, and-mgiven 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):
Two route tests, one for
modelon the native routes and one for themodeltype on/v1/embeddings:ROUTE-MUTANT model-on-native mutant=101 restored=0andROUTE-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 endsGPU-MUTANTS aggregate ok=True count=13.The gates
just affectedselects against basea688accb2c2b43fc4407984f4210db3978753c00: 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_wholeincrates/gpu/src/rope_neox.rs, and it comes from #299. This PR adds no kernel.just ptx-comparerefusessm_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_121of each binary at base and head,PTX_SCAN_ARCH=sm_121 tools/ptx-scan.sh <bin>, thenpython3 tools/recipes.py ptx-compareon each pair. The comparison reads each scan's stderr and stdout joined, in that order.bloomery-serve-ds41gives the same result (347 base entries, 348 head entries, the same one added) andbloomery-serve-qwen38gives 264 and 265 with the same one added. All three are rc 0.Record schemas (head
e1624025). The--records-schemaoutput ofbloomery-serve(with a Qwen3.8-Flash-Next file),bloomery-serve-ds41andbloomery-serve-qwen38was compared with the checked-in snapshots, which gain theembedding_seatkind. All three are byte-equal and the check endsrecords-refresh: PASS. I did not run the fulljust records-refresh, which also rewrites snapshots this PR does not touch.What I ran into
None of this blocks this pull request.
n_ubatch) first and answers an input past it with a 500; past the context it answersexceed_context_size_error. This server has no batch limit there, so an input past the model's context is a 400exceed_context_size_errornaming both lengths.{"prompt_string": …}and acontentarray 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.--mmprojand--embd-modelon 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.mdsays so.Hardware (if the change is for a machine)
rustc 1.100.0-nightly (e457a7b0d 2026-08-27),just 1.58.0.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).embeddinggemma-2-Q8_0.gguf(sha2562188ac1d..., the file the reference sets were dumped from) and Qwen3-30B-A3B-Instruct-2507Q4_K_M.AI help
AI 코딩 에이전트가 코드를 작성한 후 설명을 썼고, 테스트를 마쳤습니다. diff 리뷰와 PR 초안 리뷰는 제가 직접 했습니다.