Repository navigation
model: EmbeddingGemma and EmbeddingGemma 2 on a side seat #190
Description
말씀해주신 대전제, 그러니까 하나에 붙인다는 그 방향에 맞춰서 준비한 계획입니다.
I'm starting work on embedding support for both EmbeddingGemma models, on DGX Spark. This issue holds the plan and tracks the PRs. #59 settled the shape for EmbeddingGemma 2: a side seat beside the chat model, with the plan taking its bytes out of the chat model's expert budget. The plan below follows that and adds EmbeddingGemma (v1, 300M) on the same path, since the two share most of the encoder.
Results from other hardware are welcome, and so is help on the parts this plan leaves out (audio and video, section 6).
1. The models
| EmbeddingGemma 2 | EmbeddingGemma (v1) | |
|---|---|---|
| Weights | google/embeddinggemma-2, Apache-2.0 | google/embeddinggemma-300m, Gemma license, gated |
| GGUF I start from | ggml-org/embeddinggemma-2-GGUF Q8_0 (309,855,456 B), mmproj BF16 (982,074,784 B) | ggml-org/embeddinggemma-300M-GGUF Q8_0 (333,590,944 B) |
general.architecture |
gemma-embedding2 |
gemma-embedding |
| Context | 262144 in the GGUF, 8,192 on the model card | 2048 |
| llama.cpp | ggml-org/llama.cpp#30054 (merged 2026-10-06), src/models/gemma-embedding2.cpp |
ggml-org/llama.cpp#15798 (merged 2025-09-04), src/models/gemma-embedding.cpp |
2. What is new about them
Read from the GGUF headers above and the two llama.cpp files.
- Non-causal attention. Both run bidirectional with a symmetric sliding window, and every sixth layer (5, 11, 17, 23) is global. v2's header says so (
attention.causal = false,sliding_window_pattern); v1's carries neither key because llama.cpp sets both in code, so the v1 reader takes them from the architecture name. The window is 512 in v1's header and 1024 in v2's, while v2's Hugging Faceconfig.jsonsays 512. Whether llama.cpp reads that number as the full width or one side is open for both models, and the refsets (E3, E8) settle it. - Head sizes. v1 uses head 256 on every layer, with 3 query heads and 1 KV head. v2 uses head 256 on its 20 sliding layers (4 query heads, 2 KV heads) and head 512 on its 4 global layers (1 KV head). No attached model has a grouped-query head of 512 with separate K and V planes; the widest common GQA prefill entry is head 256 (
flash_gqa). V4.1's attention walks 512-wide rows, but one latent row is both key and value there, so v2's global layers need their own entries, and so does v1's group of 3 (the head-256 entries take groups of 8, multiples of 4 and other even groups). - Feed-forward. GELU-gated (
LLM_FFN_GELU,LLM_FFN_PAR), width 1152 (v1) and 2048 (v2). - v2 per-layer input.
per_layer_model_proj(BF16, also in the Q8_0 file), and per layerinp_gate,proj,post_normandlayer_output_scale. A finaloutput.weightprojects 512 to 768. - v1 dense head. After pooling,
dense_2(768 to 3072) anddense_3(3072 to 768). - Pooling. Mean (
pooling_type = 1), then normalization. The model cards give MRL widths 768, 512, 256 and 128; the headers carry no width list. - Tokenizers. v1 is
tokenizer.ggml.model = llama(SentencePiece). v2 isgemma4.bloomery-tokenizertakesgpt2only today, so both are new. - v2 images. The mmproj is
clipwith agemma4vvision projector (16 layers, patch 16, image 224, width 768, projection to 512). The same file also carries agemma4aaudio encoder.
The closest attached model is Clef-Flash: its decide seat is the one seat that answers from hidden rows instead of tokens, it takes images through the vision seat, and its Qwen3.5-9B backbone runs head-256 GQA through the common prefill entries.
The common owners from docs/contrib/new-model.md:
| Owner | This model |
|---|---|
| Placement plan | Does not attach: the encoder is dense with no experts, so its bytes enter the chat plan as a named reserve (section 3) |
| Kernel table | Already attached for the Q8_0 and F32 dense sites. dense has no BF16 row, so per_layer_model_proj loads as Bf16AsF32 and calls the f32 gemv directly, as Qwen3.8's indexer projections do |
| Card expert leg, Residency, Expert tier | Do not attach: no experts |
| Prompt walk, Resident slots | Do not attach: one non-causal pass over the whole input, with no cache kept between requests |
| Serve seat | Attaches as a side seat beside the chat model's seat |
| Timing | Already attached, through the serve seat |
3. Proposed shape
bloomery-servekeeps one chat model as its seat and takes the embedding model as a side seat in the same process, given by--embd-model PATHor--embd-hf repo:quant, plus--embd-mmprojfor images. These spellings are a proposal; see section 7.- The side seat follows the image encoder's seat rule as it is: the smallest card outside the plan, which costs the chat plan nothing, or else a named reserve on the stage card, whose bytes the chat plan takes out of the expert budget.
image_plan::reserve_onfixes the name"image encoder"today; it would take the name as an argument, so both seats enter their own reserve through it andreserve_inreads each by name. v2's image tower sits inside the embedding reserve. - The embedding model runs on its own thread and stream. The decoder's engine thread does not change.
- The model side runs the encoder through the hidden-row form of the media call (
MediaBody::prefill_hidden_media, which the decide seat reaches throughHidden), and the encoder body refuses the token form by name. It also owns pooling and the learned projections around it (v2'soutput, v1's dense head) and returns one pooled vector per input. A newserve::embedmodule owns the rest:/v1/embeddings(OpenAI shape) and/embedding(llama-server shape), normalization ported from llama-server'scommon_embd_normalize, the MRL cut with renormalization (dimensions), and base64 output. - Errors: 501 when no side seat is loaded, 400 for a bad
dimensions, 400exceed_context_size_errorfor an input past the context, as bloomery's completions already answer, and-mrefuses an embedding file by name. llama-server checks its physical batch (-ub) first and answers 500 past it, so it gives that 400 only when-ubcovers the context. The side seat runs an input in one call with no batch limit of its own, anddocs/serving.mdsays so.
4. Proposed PR sequence
E1 to E4 do not depend on each other.
- E4: Kernels: the head-512 GQA prefill entries, the non-causal symmetric window in prefill as a const generic, and an f32 GELU-gated entry on
gpu-vision'sgelu_tanh_f32rule, lifted intobloomery_gpu.ptx-scanis the base plus exactly the new entries,gate-ptx-spillis red on exactly them before pinning, and the cc 12.1ptx-shapesrows are filled. Checked against: host f64 rules on synthetic inputs. (gpu, gpu-gates: the head-512 rope append and prefill flash of EmbeddingGemma 2's global layers, with their gate #233, gpu: gqa_prefill_flash_256_p2_lower, a first key a row #234, gpu: geglu_tanh, the GELU-gated unit over f32 rows #237)
The oracle is mainline because ik_llama.cpp has neither architecture; the GLM-5.3 image sets already use mainline the same way.
- A second reference: pinned sentence-transformers output from the original EmbeddingGemma 2 weights, with an independent ceiling, checked on DGX Spark and RTX 5090. Merged as #413 (
1f262bd4).
The serve gate runs beside Qwen3-30B-A3B-Instruct-2507 Q4_K_M. The plain run's BLOOMERY_CARD_BUDGET leaves some experts on the host, and the side seat's run adds the embedding reserve to it. The gate checks that:
/v1/embeddingsvectors match the llama-server vectors inside a band (cosine and largest absolute difference);- both plans name the same
card_expertsandhost_experts, and the chat model's greedy tokens are the same bit for bit, with and without the side seat and while embedding requests run at the same time; - at the plain run's budget, the side seat's plan puts fewer experts on the card;
- the plan record names the embedding reserve and its bytes, and the
cardsterm ofPlan::pool_needcounts it; - the refusals in section 3 hold.
Every clause is broken on purpose first to see it fail.
5. Owner and test machine
I'm taking this on myself. The test machine is DGX Spark (GB10, cc 12.1, 128 GB unified memory). Kernel gates, the e2e and serve gates and the mainline oracle all run there. Pure tests run on macOS and Linux.
6. Left out of this plan
- Audio and video were outside the first plan. The v2 mmproj carries the audio encoder, which needs its own preprocessing and tower. They landed as follow-ups:
- E11: Served audio: decoded f32 PCM through
/v1/embeddingswith the Gemma4a side tower. (serve: embed decoded f32 PCM audio with the Gemma4a side tower #405) - V2: Video clips sampled at 1 FPS from decoded frames, checked against llama.cpp. (vision, gpu-gates, tools: EmbeddingGemma 2 video clips sampled at 1 FPS from decoded frames, checked against llama.cpp #383)
- V3: Served video: sampled frames through the embedding endpoints, checked against llama-server. (serve, vision, gpu-gates, tools: EmbeddingGemma 2 sampled video frames through the embedding endpoints, checked against llama-server #397)
- E11: Served audio: decoded f32 PCM through
- Task prefixes. The model cards put the query and document prompts in the input text. The first PRs pass input text through as given.
-
prompt_nameselects the query or document prefix fromconfig_sentence_transformers.jsonon both embedding routes. Merged as #414 (a6fa3eec).
-
- Repeated inputs. The routes ran the side model again for an input they had already embedded (serve: the embeddings routes run the side model again for an input they have already embedded #406).
- A result cache keeps pooled vectors and image tower rows within one byte budget. Merged as #417 (
d8959941).
- A result cache keeps pooled vectors and image tower rows within one byte budget. Merged as #417 (
- Embedding logic outside the GPU crates.
- Pooling, request fields and response encoding live in the GPU-free
bloomery-embedcrate, checked against pinned llama.cpp fixtures. Merged as #412 (c990f476).
- Pooling, request fields and response encoding live in the GPU-free
- Media speed against llama.cpp on RTX 5090.
7. Open questions
- A new kernel entry's cc 8.6
ptx-shapesrow (the JIT column) needs a cc 8.6 card. The PRs fill the ptxas column and the cc 12.1 tables. Must the cc 8.6 JIT pin be filled before such a PR merges, or can it follow? docs/research/tierengine.mdnames no flags for a second model in one server. Is there a planned spelling the--embd-*flags should follow?- For v2's context (section 1; model: EmbeddingGemma 2 (gemma-embedding2, 0.74B dense) #59 asked this too), should the side seat enforce the model card's 8,192 tokens, or allow an explicit override up to the GGUF's 262144?
- Should the reader describe an encoder with an
EncoderSpecbesideModelSpec(an encoder has pooling and no chat surface), or with an encoder variant ofModelSpec? E1 follows the answer.
Done
-
E8: EmbeddingGemma (v1): the
gemma-embeddingreader arm, thellamaSentencePiece tokenizer, a head-256 prefill entry for a group of 3 (pack 1 or 3), the body variant with the dense head, and its gates, on top of E4 to E6. Checked against: its own refset from the same pinned mainline. (refsets refset, tools: EmbeddingGemma (v1)'s mainline oracle sets and their families #267, reader model, models: gemma-embedding's header reader on a Gemma encoder trunk shared with gemma-embedding2, and one per-layer KV head check for the Gemma, mimo2 and nemotron_h readers #269, tokenizer tokenizer: encode llama SentencePiece with the shared merge owner #345, kernel entry gpu: head-256 prefill attention for EmbeddingGemma 1 (flash_band first-key bound, pack-1 lower entry) #351, body and serving gpu-gemma-embedding2, gpu-gates: the shared encoder and side seat run EmbeddingGemma v1 with its dense head #390); merged as #390 (ce96affc); merged as #269 (19114825); merged as #267 (73c307c4); merged as #345 (3fdbbf43); merged as #351 (bdc70824)-
V1: Video as ordered decoded RGB frames through the image tower. (vision, refset, gpu-gates, tools: EmbeddingGemma 2 ordered decoded video frames through the Gemma4 vision tower #374); merged as #374 (
402f9abc) -
E10: The audio tower crate
gpu-gemma4a, checked against the pinned mainline tower. (gpu-gemma4a: the audio Conformer tower and its mainline gate #375); merged as #375 (34ce3d67) -
E9: Audio preprocessing:
gemma4amel and its tower token count, checked bit for bit against the pinned mainline preprocessor (vision: gemma4a prepares decoded PCM as chunked mel and counts its tower tokens #331, merged as98a513fc).; merged as #331 (98a513fc)
-
-
E7: Images:
gemma4vpreprocessing and a tower ongpu-vision'sgemm_bf16, which reads bf16 matrices, hence the BF16 mmproj; agelu_quickentry, the activation mainline runs for this tower andgpu-visiondoes not have yet; a tower gate and the serve gate's image clause. Checked against: mainline mtmd dumps and llama-server image vectors. (vision, gpu-vision, gpu-gates, serve: EmbeddingGemma 2 image input through the Gemma4 vision tower #324); merged as #324 (6d4113c1) -
E6: Side seat and endpoints,
READMEanddocs/serving.md, and a serve gate. Checked against: llama-server vectors. (serve, gpu-gates: /v1/embeddings over an EmbeddingGemma 2 model held beside the chat model (--embd-model), with its serve gate #323); merged as #323 (85511826) -
E5: Body: a
gpu-gemma-embedding2crate built from common kernels only, and an e2e gate. Checked against: E3 text set. (gpu, gpu-gemma-embedding2, gpu-gates: the EmbeddingGemma 2 encoder from the common kernels, with its end-to-end gate #299); merged as #299 (38c41f4e) -
E3: Refset: mainline llama.cpp pinned at
6868a52c(2026-10-14), which has graph: pad the KV dimension of the no-cache attention to 256 ggml-org/llama.cpp#30416 (pads the no-cache attention's KV dimension) and models: drop the t_embd expand of the embedding graphs ggml-org/llama.cpp#30461 (drops thet_embdexpand of the embedding graphs); per-layer dumps (l_out,result_norm,result_embd), pooled vectors and llama-server vectors, text and image sets. Checked against: mainline. (refset, tools: EmbeddingGemma 2's mainline oracle sets and their families #260); merged as #260 (a9ff8aca) -
E2: Tokenizer: the
gemma4model. Checked against: mainlinellama-tokenizeids on a fixed text set (English, Korean, code, emoji, special-token look-alikes, a 1500-word text). (tokenizer: the gemma4 model and pre-tokenizer, BPE over metaspace text as the reference runs it #241); merged as #241 (c7af352e) -
E1: Reader:
EncoderSpec(question 4), thegemma-embedding2arm, header refusals, a meta test and its recipe, thetools/hf-arch-check.pymirror. Checked against: the GGUF header. (model, models: gemma-embedding2's header reader, with an EncoderSpec on ModelSpec #238); merged as #238 (5e837723) -
serve: embed decoded f32 PCM audio with the Gemma4a side tower #405: serve: embed decoded f32 PCM audio with the Gemma4a side tower; merged as #405 (
b1eea041) -
serve, vision, gpu-gates, tools: EmbeddingGemma 2 sampled video frames through the embedding endpoints, checked against llama-server #397: serve, vision, gpu-gates, tools: EmbeddingGemma 2 sampled video frames through the embedding endpoints, checked against llama-server; merged as #397 (
fa450a75) -
gpu-gemma-embedding2, gpu-gates: the shared encoder and side seat run EmbeddingGemma v1 with its dense head #390: gpu-gemma-embedding2, gpu-gates: the shared encoder and side seat run EmbeddingGemma v1 with its dense head; merged as #390 (
ce96affc) -
vision, gpu-gates, tools: EmbeddingGemma 2 video clips sampled at 1 FPS from decoded frames, checked against llama.cpp #383: vision, gpu-gates, tools: EmbeddingGemma 2 video clips sampled at 1 FPS from decoded frames, checked against llama.cpp; merged as #383 (
8171614c) -
serve, gpu-gates: /props names the embedding model held beside the chat seat #366: serve, gpu-gates: /props names the embedding model held beside the chat seat; merged as #366 (
db5c89e7) -
gpu: head-256 prefill attention for EmbeddingGemma 1 (flash_band first-key bound, pack-1 lower entry) #351: gpu: head-256 prefill attention for EmbeddingGemma 1 (flash_band first-key bound, pack-1 lower entry); merged as #351 (
bdc70824) -
tokenizer: encode llama SentencePiece with the shared merge owner #345: tokenizer: encode llama SentencePiece with the shared merge owner; merged as #345 (
3fdbbf43) -
vision, refset, gpu-gates, tools: EmbeddingGemma 2 ordered decoded video frames through the Gemma4 vision tower #374: vision, refset, gpu-gates, tools: EmbeddingGemma 2 ordered decoded video frames through the Gemma4 vision tower; merged as #374 (
402f9abc)
AI help
AI 코딩 에이전트가 자료를 조사해 계획과 이슈 초안을 썼습니다. 계획과 이슈 초안 리뷰는 제가 직접 했습니다.
Activity
[-]model: EmbeddingGemma and EmbeddingGemma 2 (gemma-embedding, gemma-embedding2) on a side seat[/-][+]model: EmbeddingGemma and EmbeddingGemma 2 on a side seat[/+]midagedev commentedon Oct 15, 2026
계획 잘 봤습니다. #59에서 말한 방향(한 서버에 여러 모델, 임베딩은 채팅 모델 옆 side seat, 바이트는 expert 예산에서 차감)과 그대로 맞습니다. 이미지 인코더 reserve 규칙을 재사용하는 것, 채팅 토큰이 side seat 유무와 상관없이 비트 단위로 같은지 게이트로 보는 것 모두 좋습니다.
열린 질문에 대한 답입니다.
--embd-model,--embd-hf,--embd-mmproj로 가 주세요.ModelSpec의 변형보다EncoderSpec을 따로 두는 쪽이 좋겠습니다. 채팅 표면이 없고 pooling이 있어서 분리하는 게 깔끔합니다.순서는 v2(E1~E7)를 먼저 끝내고, v1(E8)은 수요를 보고 나중에 해도 될 것 같습니다. v1은 gated 라이선스에 컨텍스트 2048이고, 그룹 3 전용 head-256 entry가 추가로 필요해서 들이는 비용에 비해 얻는 게 적습니다. 그리고 저희는 지금 0.3.3 마무리 중이라, 이 작업은 0.3.3 이후 릴리즈에 들어가는 것으로 보겠습니다.
이 답글은 bloomery AI 어시스턴트가 작성했고, 메인테이너가 확인한 뒤 올렸습니다.
midagedev commentedon Oct 15, 2026
덧붙여, Clef(SystemOne) 자리를 붙였던 경험에 비춰 보면 보강할 만한 점이 네 가지 있습니다. Clef도 토큰 대신 hidden row로 답하는 모델이라 구조가 많이 닮았습니다.
bloomery-decision이라는 순수 crate로 빼서 Mac에서도 Python과 바이트 단위로 대조했습니다. 임베딩도 pooling, normalize, MRL 자르기와 재정규화, 요청 파싱, base64 출력을 순수 crate로 떼면 GPU 없이 llama-server와 맞춰 볼 수 있습니다./v1/models를 공통 모듈(serve::models)로 모았으니, 임베딩 자리가 그 목록에 두 번째 모델로 나오면 됩니다. 잘못된 파일을 이름 붙여 거부하는 방식과 이미지 501 응답도 Clef가 정한 그대로 쓰면 됩니다.config_sentence_transformers.json의 prompts를 읽어서prompt_name으로 고르게 하면 llama.cpp보다 나은 지점이 됩니다. 나중 PR이어도 괜찮습니다.(수정) toktape는 토큰 출력만 녹화해서 임베딩 응답은 담을 수 없습니다. 릴리즈 전 실제 서빙 확인은 llama-server 벡터와의 비교 결과(cosine, 최대 절대 차이)를 남기는 것으로 하고, toktape는 side seat를 붙였을 때 채팅 모델 녹화가 그대로인지 보는 용도로만 쓰면 되겠습니다.
이 답글은 bloomery AI 어시스턴트가 작성했고, 메인테이너가 확인한 뒤 올렸습니다.
juliankang4 commentedon Oct 15, 2026
Thanks, this settles the open questions. The stack already matches answers 3 and 4: the encoder seat has its own
EncoderSpec, separate fromModelSpec, and the default context is the card's 8,192 (CARD_CTX), with the GGUF's 262,144 reachable when it is set explicitly. For answer 1, the cc 8.6 rows will carry a note in each PR that they came fromptxasand wait for a reading on a 3090 or A6000.On the four suggestions from Clef: each one widens the plan the open PRs were built and reviewed against. I'd like to keep the current stack (E1 to E7) at its present scope, get it merged, and then take them as follow-up PRs on top:
serve::modelsas the second model in/v1/models, with the named-file refusal and the image 501 shared with it.config_sentence_transformers.json, chosen byprompt_name.Thanks for the toktape correction. The serve check in the image PR compares against llama-server vectors (cosine and max abs diff), and toktape is used only to show the chat model's recording is unchanged with the embedding seat loaded.
midagedev commentedon Oct 15, 2026
참고용으로 EmbeddingGemma 2 기준 녹화를 하나 올려둡니다.
EmbeddingGemma 2 검색 데모 테이프
bloomery 쪽 side seat가 올라오면 같은 입력으로 녹화해서 이 테이프와 비교하는 기준으로 쓰면 될 것 같습니다. 녹화는
toktape embed --api llama로 할 수 있습니다.이 댓글은 bloomery AI 어시스턴트가 작성했습니다.
juliankang4 commentedon Oct 15, 2026
Thanks, that replaces what I wrote above about toktape. The image PR's serve check will record the side seat with
toktape embedon the same 20 documents and 6 questions, sending the same input text as this tape, and compare the two: recall@1 and the ranks at MRL 768, 256 and 128. The llama-server vector comparison (cosine and max abs diff) stays beside it. Choosing the prompt byprompt_namein the engine stays with the follow-up for item 4.juliankang4 commentedon Oct 15, 2026
Here's the side seat recorded with
toktape embedand published: https://tape.midagedev.com/r/jkq8iipaxrx748vrze4wIt's the same built-in set as the reference llama-server tape (20 documents, 6 questions, 52 requests with and without the prompts), sent to the EmbeddingGemma 2 seat on an RTX 5090 through
POST /embedding./v1/embeddings/embeddingThe two runs used different cards and engines, so the latency rows are not a speed comparison.
toktape compareagainst the reference tape matches all 52 inputs, with the lowest cosine 0.998881 onq-starter. For MRL I cut the stored vectors of both tapes to 768, 256 and 128 dimensions and ranked the documents by cosine. The right document is first for all six questions at every width on both tapes. Some lower places differ.One thing about the tape's header: the MODEL line reads
Qwen3.6-35B-A3B-Q4_K_M.gguf. That's the chat model the server was started with; the seat runs beside it. The REQUEST line shows the model that answered,embeddinggemma-2-Q8_0.gguf. toktape takes MODEL from/propsmodel_path, and--modelonly sets the request field, so I couldn't label it from the client side. Couldtoktape embeduse--model(or the model named in the response) for that line? Then a side seat tape would name the embedding model at the top.midagedev commentedon Oct 15, 2026
MODEL 줄 확인했어요.
toktape embed가 MODEL을/props의model_path에서만 가져와서, 옆에 붙은 seat가 답해도 서버를 띄운 채팅 모델이 찍혔어요.toktape #6에서 고쳤어요. 이제 응답의
model을 먼저 쓰고, 없으면--model, 그것도 없으면/v1/models에 여러 개가 있을 때 그중 embedding 모델을 써요. toktape main으로 다시 빌드해서 지금처럼--model embeddinggemma-2-Q8_0.gguf를 주고 녹화하면 MODEL 줄에 임베딩 모델이 나와요.(bloomery AI 어시스턴트가 작성)
midagedev commentedon Oct 15, 2026
E6 엔드포인트 관련해서 계획에 세 가지만 더 넣으면 좋겠습니다. bloomery는 API를 OpenAI와 최대한 호환되게 맞추는 방향이라서요.
/v1/embeddings응답의model에는 채팅 모델이 아니라 임베딩 seat의 이름(alias나 파일명)을 넣었으면 합니다. 요청의model이 채팅 모델을 가리키면 조용히 임베딩하지 말고 OpenAI 형식의 에러(400/404)로 돌려주면 좋겠습니다./v1/models의data에 임베딩 seat를 별도 항목으로 추가해 주세요. 지금models::listing은 id를 하나만 받으니 seat 목록을 받도록 바꿔야 할 것 같습니다.meta에 차원과 컨텍스트 길이가 있으면 클라이언트가 두 모델을 구분하기 쉽습니다./props에embedding_model필드(alias, path)를 두면 llama-server 쪽 도구에서도 임베딩 모델을 찾을 수 있습니다.(bloomery AI 어시스턴트가 작성)
juliankang4 commentedon Oct 15, 2026
Thanks. To keep the endpoint PR on time for tonight's release, I'll take item 1 there:
/v1/embeddingsanswers with the embedding seat's name inmodel, and a request whosemodelnames the chat model gets a 400 or 404 in the/v1error shape instead of a silent embedding.Items 2 and 3 change the existing
/v1/modelslisting and/props, so they go in a follow-up PR after the endpoint lands:models::listingtakes the seats, the embedding seat gets its owndataentry with dimension and context length inmeta, and/propsgets anembedding_modelfield with alias and path.juliankang4 commentedon Oct 15, 2026
Thanks for the toktape fix. I recorded the side seat again with the fixed
toktape embedand published it: https://tape.midagedev.com/r/ktnmzyeqwmqj5gqct4cyThe MODEL line now reads
embeddinggemma-2-Q8_0.gguf, the same name as the REQUEST line. Same server build, same built-in set (20 documents, 6 questions, 52 requests) on the RTX 5090 throughPOST /embedding.toktape compareagainst the reference tape still matches all 52 inputs, with the lowest cosine 0.998881 onq-starter. The earlier link can be ignored.midagedev commentedon Oct 15, 2026
깜고님, side seat 녹화에 쓰신
POST /embedding서버 코드가 아직 저장소에는 올라오지 않은 것 같습니다. 엔드포인트도 이번 릴리즈에 같이 넣고 싶어서 태그를 잠시 미뤄 두었는데, PR은 언제쯤 열 수 있을지 대략 알려 주시면 그에 맞춰 기다리겠습니다.(bloomery AI 어시스턴트가 작성)
juliankang4 commentedon Oct 15, 2026
@midagedev 답변이 늦어져서 죄송합니다 ㅠㅠ 엔드포인트는 E6에 들어가고 지금 노드 여러대에서 마지막 테스트 중입니다. E5와 E6 곧 스택해서 PR 올릴 것 같아요!!! 그 이후에 톡테이프도 다시 찍겠습니다.
juliankang4 commentedon Oct 16, 2026
@midagedev 정말정말 죄송합니다. 테스트에서 뮤턴트가 계속 나와서 이제야 클린 상황이고 e5 푸시했습니다 죄송해요!!!!
juliankang4 commentedon Oct 16, 2026
New E6 side-seat recording for #323, build
b75dff1a03dda54c4d4e606b3e8691e12a54a1e7: https://tape.midagedev.com/r/md5dubhjmtxxvm4p6jsmThe MODEL line reads
embeddinggemma-2-Q8_0.gguf. EmbeddingGemma 2 Q8_0 sits beside Qwen3.6-35B-A3B Q4_K_M on an RTX 5090 (driver 595.99.02, 400 W cap). The same built-in set runs throughPOST /embedding: 20 documents, 6 questions, 52 requests. The input text and task prefixes match the reference tape byte for byte. Warm-up requests are excluded; the CPU governor was set to performance.toktape compareagainst the reference llama-server tape matches all 52 inputs. The lowest cosine is 0.998881 and the maximum absolute vector difference is 0.007111, both on the promptedq-starter. The reference tape records llama-server commit843e7dcon an RTX 3090. These are separate recordings on different cards and engines, not an interleaved speed comparison.At MRL widths 768, 256 and 128, all 6 questions rank the expected document first on both tapes (computed from the saved f32 vector prefixes, renormalized, with f64 cosine). The full document rankings do differ.
juliankang4 commentedon Oct 17, 2026
@midagedev 님, EmbeddingGemma와 EmbeddingGemma 2 지원 작업을 완료했습니다. #190 계획에 있던 PR은 오늘 머지된 여섯 개까지 모두 main에 들어갔습니다. 이 이슈에서 제가 드린 약속도 하나씩 다시 확인했고 모두 지켰습니다.
아래에 오늘 머지된 PR, 약속별 결과, RTX 5090에서 llama.cpp와 비교한 벤치, 결과 캐시 측정을 정리했습니다. 계획이 모두 끝났으니 이 이슈는 닫겠습니다. #59도 보시고 괜찮으시면 닫아 주셔도 될 것 같습니다.
1. 오늘 머지된 PR
prompt_name으로 query와 document 프리픽스 선택 (Clef 제안 4)a6fa3eec1f262bd4--embd-cache-bytes(#406)d8959941bloomery-embedcrate (Clef 제안 2)c990f4764d8afaffb8dcc2afE1부터 E11, V1부터 V3까지 나머지 PR은 그 전에 머지되었고 본문 체크리스트에 커밋과 함께 적어 두었습니다.
2. 이 이슈에서 드린 약속
ptxas값이라고 PR마다 표시tools/ref/ptx-shapes.tsv에도 같은 주석이 6곳 있습니다sliding_window=512는 한쪽으로 닿는 폭이고 GGUF의 1024는 전체 폭이라서 bloomery가 원래 2로 나누던 방식이 HF와 맞습니다. HF 대비 최소 cosine은 bloomery 0.9992535192, 저장해 둔 llama.cpp 벡터 0.9991290441로 DGX Spark와 RTX 5090에서 같았습니다/v1/models에 두 번째 모델로 나오고 파일 거부와 이미지 501을 같이 씀prompt_name으로 프리픽스 선택model에 임베딩 seat 이름, 채팅 모델 이름이면 조용히 임베딩하지 않고 에러model은 seat 이름이고 다른 모델 이름은 400invalid_request_error로 거절합니다/v1/models에 seat 항목과meta,/props에embedding_model/v1/models는 #323에서meta에n_ctx,n_embd가 들어갔고 #414에서prompt_names가 더해졌습니다./props의embedding_model(alias, path)은 #366입니다toktape embed녹화로 recall@1과 MRL 768, 256, 128 순위 비교, llama-server 벡터 비교 병행, E6 뒤 다시 녹화b75dff1a) 녹화는 recall@1 6/6, 52개 입력 모두 일치, 최소 cosine 0.998881, 최대 절대 차이 0.007111입니다3. RTX 5090에서 llama.cpp와 비교
A는 mainline llama.cpp
6868a52c(E3 기준과 같은 핀), B는 bloomery mainb8dcc2af입니다. 둘 다 EmbeddingGemma 2 Q8_0과 BF16 projector, context 8192를 썼습니다. bloomery는 Qwen3-30B-A3B 채팅 모델 옆 side seat로 띄웠습니다. 입력은 seed 20261017로 고정한 subset입니다. 텍스트는 nanoscifact 문서 150개와 질의 50개, 이미지는 flickr30k 200개, 비디오는 msrvtt 50개와 Bunny 발췌 3개, 오디오는 clotho 200개입니다.한 세션에서 A, A, A, B, B, A 순서로 돌렸고 약 7분 08초 걸렸습니다. 비율은 두 A/B 쌍의 전체 처리 시간 비율(llama.cpp 시간 / bloomery 시간) 평균이라 1보다 크면 bloomery가 빠릅니다.
Bunny 두 행은 입력이 발췌 3개뿐이라, #419에서 같은 llama.cpp 핀과 같은 코드(
6e78c890, main에서는b8dcc2af)로 잰 값을 넣었습니다.검색 정확도는 B 빼기 A의 최대 절대 차이가 nDCG@10
0.00117821, Recall@10.004, Recall@50.002, Recall@100.001입니다.4. 결과 캐시 (#417)
--embd-cache-bytes 67108864를 켠 쪽과 main8d4f29d9를 세 라운드 번갈아 쟀습니다. 요청은 워크로드마다 라운드당 32개입니다. 아래는 p50 ms입니다.모든 arm의 전체 폭 벡터는 비트 단위로 같았습니다.
긴 계획 동안 답변과 기준 녹화, toktape 수정까지 챙겨 주셔서 감사합니다.