Bug: Qwen3/Gemma3 candle skip attention masks for equal-length batches
System Info
- TEI version: source checkout at
huggingface/text-embeddings-inference@main when filed, package version 1.9.3
- Local validation OS: macOS arm64 (
Darwin 25.5.0)
- Repository Rust toolchain:
rustc 1.92.0, cargo 1.92.0, matching rust-toolchain.toml
- Backend: candle CPU/Metal path
- Affected model families: Qwen3 embedding models and Gemma3 embedding models
- Original real-model reproduction:
Qwen/Qwen3-Embedding-0.6B, CPU, fp32 and fp16; also reproduced without MKL
- Synthetic reproductions: tiny random-weight Qwen3 and Gemma3 models, no model download required
Information
The problem arises when using:
Tasks
The thing I am working on is:
Reproduction
On the candle backend, Qwen3 and Gemma3 can skip their attention masks for multi-sequence batches where every sequence has the same length.
For Qwen3, Qwen3Model::forward only applied the causal mask when a padding bias existed. Equal-length batches need no padding, so attention_bias stayed None and the batch ran without the causal mask.
For Gemma3, Gemma3Model::forward similarly passed None when no padding was required. Each Gemma3 attention layer only builds and adds its causal or sliding-window mask when it receives an attention bias tensor, so equal-length batches skipped those masks too.
I added no-download synthetic-weight tests for both models:
cargo test -p text-embeddings-backend-candle --test test_qwen3_equal_length_batch --test test_gemma3_equal_length_batch
With the fixes applied, both pass:
test test_gemma3_equal_length_batch_matches_single ... ok
test test_qwen3_equal_length_batch_matches_single ... ok
Negative evidence with only the old Qwen3 gate restored:
equal-length batch vs single: cos_a=0.613198, cos_b=0.617658
equal-length batch[0] diverged from single inference (cos=0.61319774); causal mask skipped for no-padding batch?
Negative evidence with only the old Gemma3 no-bias behavior restored:
Gemma3 equal-length batch vs single: cos_a=0.924376, cos_b=0.900559
equal-length Gemma3 batch[0] diverged from single inference (cos=0.92437583); attention mask skipped for no-padding batch?
Expected behavior
The same input should produce the same embedding whether it is embedded alone or inside an equal-length backend batch.
Qwen3 should always apply its causal mask for non-bidirectional models, including no-padding batches.
Gemma3 should always create or receive a base attention bias so every attention layer adds its causal or sliding-window mask, including no-padding batches.
Impact
The router coalesces concurrent requests into backend batches. Any sub-batch that happens to contain equal-length inputs can silently produce wrong embeddings. This is especially plausible for retrieval/RAG workloads with short, similarly shaped queries.
This is independent of dtype or BLAS backend. The synthetic tests keep the regression coverage small and deterministic enough for CI.
Root cause
Qwen3 currently gates causal masking on attention_bias.is_some() in the batched path. Equal-length batches do not build padding bias, so causal masking is skipped.
Gemma3 currently lets attention_bias remain None for equal-length batches. Gemma3Attention::forward only calls create_attention_mask inside the Some(attention_bias) branch, so both full causal and sliding-window masks are skipped.
Expected fix
Always provide a zero base attention bias for equal-length batches that need masking but no padding:
- Qwen3: synthesize a zero bias before applying
get_causal_attention_bias.
- Gemma3: synthesize a zero bias in
Gemma3Model::forward so every attention layer adds its causal or sliding-window mask.
PR: #883
Bug: Qwen3/Gemma3 candle skip attention masks for equal-length batches
System Info
huggingface/text-embeddings-inference@mainwhen filed, package version1.9.3Darwin 25.5.0)rustc 1.92.0,cargo 1.92.0, matchingrust-toolchain.tomlQwen/Qwen3-Embedding-0.6B, CPU, fp32 and fp16; also reproduced without MKLInformation
The problem arises when using:
Tasks
The thing I am working on is:
Reproduction
On the candle backend, Qwen3 and Gemma3 can skip their attention masks for multi-sequence batches where every sequence has the same length.
For Qwen3,
Qwen3Model::forwardonly applied the causal mask when a padding bias existed. Equal-length batches need no padding, soattention_biasstayedNoneand the batch ran without the causal mask.For Gemma3,
Gemma3Model::forwardsimilarly passedNonewhen no padding was required. Each Gemma3 attention layer only builds and adds its causal or sliding-window mask when it receives an attention bias tensor, so equal-length batches skipped those masks too.I added no-download synthetic-weight tests for both models:
cargo test -p text-embeddings-backend-candle --test test_qwen3_equal_length_batch --test test_gemma3_equal_length_batchWith the fixes applied, both pass:
Negative evidence with only the old Qwen3 gate restored:
Negative evidence with only the old Gemma3 no-bias behavior restored:
Expected behavior
The same input should produce the same embedding whether it is embedded alone or inside an equal-length backend batch.
Qwen3 should always apply its causal mask for non-bidirectional models, including no-padding batches.
Gemma3 should always create or receive a base attention bias so every attention layer adds its causal or sliding-window mask, including no-padding batches.
Impact
The router coalesces concurrent requests into backend batches. Any sub-batch that happens to contain equal-length inputs can silently produce wrong embeddings. This is especially plausible for retrieval/RAG workloads with short, similarly shaped queries.
This is independent of dtype or BLAS backend. The synthetic tests keep the regression coverage small and deterministic enough for CI.
Root cause
Qwen3 currently gates causal masking on
attention_bias.is_some()in the batched path. Equal-length batches do not build padding bias, so causal masking is skipped.Gemma3 currently lets
attention_biasremainNonefor equal-length batches.Gemma3Attention::forwardonly callscreate_attention_maskinside theSome(attention_bias)branch, so both full causal and sliding-window masks are skipped.Expected fix
Always provide a zero base attention bias for equal-length batches that need masking but no padding:
get_causal_attention_bias.Gemma3Model::forwardso every attention layer adds its causal or sliding-window mask.PR: #883