Skip to content

Hybrid linear-attention (Qwen3.5/Qwen3-Next) KV cache spec + prefix-caching guard#1

Open
Carlos-Marques wants to merge 11 commits into
release-0.5.0from
devin/1781039459-qwen3-5-hybrid-cache
Open

Hybrid linear-attention (Qwen3.5/Qwen3-Next) KV cache spec + prefix-caching guard#1
Carlos-Marques wants to merge 11 commits into
release-0.5.0from
devin/1781039459-qwen3-5-hybrid-cache

Conversation

@Carlos-Marques

Copy link
Copy Markdown

Summary

Companion to exa-labs/neuronx-distributed-inference#1 (NxDI Qwen3.5/Qwen3-Next port). Makes the plugin's cache accounting correct for hybrid linear-attention models, where ~3/4 of layers (Gated DeltaNet) keep a constant-size per-sequence state instead of a growing KV cache.

Two changes:

  1. NeuronxDistributedModelRunner.get_kv_cache_spec() no longer declares FullAttentionSpec for every layer. For models whose hf_config.layer_types contains "linear_attention", those layers now get a MambaSpec:
kv_cache_spec[f"layers.{i}.linear_attn"] = MambaSpec(
    block_size=self.block_size,
    shapes=_linear_attention_state_shapes(hf_config),  # (conv_state, recurrent_state)
    dtypes=(torch.float32, torch.float32),
    mamba_type="linear_attention",
)

so vLLM's KV-cache manager budgets the real (constant) HBM footprint instead of 4x-overcounting and refusing admissible requests. State shapes come from linear_num_{key,value}_heads / linear_{key,value}_head_dim / linear_conv_kernel_dim: conv state (2*Hk*Dk + Hv*Dv, kernel-1), recurrent state (Hv, Dk, Dv).

  1. NeuronPlatform.check_and_update_config() force-disables --enable-prefix-caching for hybrid models, since DeltaNet's recurrent state is a running summary at one position — KV blocks can't be reused at branch points and prefix caching would silently produce garbage. Disabling also routes into the existing block_size = max_model_len contiguous path.

Non-hybrid models are unaffected (layer_types absent → all layers FullAttentionSpec, guard no-ops).

Testing

  • New unit tests: hybrid get_kv_cache_spec (per-layer spec types + state shapes) in test/unit/worker/test_model_runner.py; prefix-caching guard on/off + non-hybrid passthrough in test/unit/test_platform.py.
  • Full suite: pytest test/unit/test_platform.py test/unit/worker/test_model_runner.py295 passed (vLLM 0.16.0, CPU).
  • End-to-end on Trainium still pending (no hardware available); the NxDI side is CPU logit-verified against HF in the companion PR.

Link to Devin session: https://app.devin.ai/sessions/853f8dae8e27497a9a8ed7a1a6d347b0
Requested by: @Carlos-Marques

…efix-caching guard

Co-Authored-By: carlos <carlosmarques.personal@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration Bot and others added 10 commits June 13, 2026 00:50
Co-Authored-By: carlos <carlosmarques.personal@gmail.com>
…ror)

Linear-attention (DeltaNet) layers have constant-size recurrent state
managed by NxDI, not vLLM's block manager. Reporting MambaSpec for them
triggered ValueError in unify_hybrid_kv_cache_specs when the Hybrid KV
cache Manager (HMA) is disabled (always on the Neuron path).

Fix: only report FullAttentionSpec for actual attention layers.
Co-Authored-By: carlos <carlosmarques.personal@gmail.com>
When the CTE bucket is smaller than the prompt (e.g., bucket=128 but
prompt=7500 tokens), split the prompt into max_context_length-sized
chunks and feed each through context_encoding_model directly.  DeltaNet
layers accumulate recurrent state correctly across passes.

Also overrides max_prompt_length validation to allow prompts up to
max_model_len through the scheduler when multi-pass CTE is active.

Co-Authored-By: carlos <carlosmarques.personal@gmail.com>
ModelWrapper.pad_inputs expects args[1] (attention_mask) to be a tensor
with shape (batch, seq_len).  Passing None would crash on .shape[1].
Create an all-ones mask for each chunk since all tokens are valid.

Co-Authored-By: carlos <carlosmarques.personal@gmail.com>
…missing

The V1 engine's step_with_batch_queue dispatches execute_model and
sample_tokens as sequential non-blocking tasks. If execute_model raises,
sample_tokens still runs and must return None so the engine can surface
the real error via exec_model_fut.result().

Previously we raised RuntimeError which masked the actual forward() error.
Also add diagnostic logging around execute_model to capture the real
exception details.

Co-Authored-By: carlos <carlosmarques.personal@gmail.com>
The compiled CTE model expects prev_hidden with shape [batch_size],
not [0].  torch.empty(0) produced shape [0] which wasn't in the
input_shape_map.  This caused the forward pass to fail with:

  ValueError: Input shape ... not found in input_shape_map

Change to torch.zeros(batch_size) to match the compiled shape.

Co-Authored-By: carlos <carlosmarques.personal@gmail.com>
… diagnostics

- prev_hidden: use int32 (matches _process_args default tracing dtype)
- position_ids: use chunk-relative [0, chunk_len) instead of absolute positions
  to avoid potential KV workspace buffer overflow on device
- Add print/flush diagnostic logging for crash visibility in EngineCore subprocess

Co-Authored-By: carlos <carlosmarques.personal@gmail.com>
context_encoding_model() returns the raw output tensor — sampled token
IDs when on_device_sampling is enabled. The previous code tried to
access .hidden_states on this tensor, which only exists when going
through NeuronBaseForCausalLM.forward() (wraps in CausalLMOutputWithPast).

Fixes: AttributeError: 'Tensor' object has no attribute 'hidden_states'
Co-Authored-By: carlos <carlosmarques.personal@gmail.com>
…ill step

On Neuron the compiled model processes a fixed batch dimension regardless
of how many positions hold real data (unused = zero-padded). Admitting only
1 request per prefill step wasted (batch_size - 1) positions on padding,
multiplying total prefill time by batch_size.

Default max_prompt_batch_size is now max_num_running_reqs (= max_num_seqs).
Configurable via NEURON_MAX_PROMPT_BATCH_SIZE env var for A/B testing.

For the Qwen3.5-4B benchmark (batch16, 60 requests × 7500 in / 500 out):
old scheduler did 60 sequential 2.1s prefills (15/16 padded) → 126s blocked.
New scheduler batches all 16 into one 2.1s prefill → 4 batches × 2.1s = 8.4s.
Expected throughput lift: ~57% (97.77 → ~154 tok/s).

Co-Authored-By: carlos <carlosmarques.personal@gmail.com>
…ng logs

Co-Authored-By: carlos <carlosmarques.personal@gmail.com>

Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant