Qwen3.5 Simulation Support Design Document¶
Status: Draft Authors: @yuyinkai Created: 2026-05-19 Updated: 2026-06-11
1. Overview¶
1.1 Introduction¶
Qwen3.5 is the latest multimodal large language model in the Qwen series, introducing GatedDeltaNet (Linear Attention) as an alternative to standard Self-Attention. It uses linear-complexity attention in some layers to reduce the computational cost of long-sequence inference. This RFC describes the design for full Qwen3.5 model support in the msmodeling simulation framework (including Qwen3.5-27B, Qwen3.5-397B-A17B, and other variants).
Compared to the already-supported Qwen3 series, Qwen3.5 introduces the following new features:
- GatedDeltaNet Linear Attention: Includes Chunk Gated Delta Rule (prefill) and Recurrent Gated Delta Rule (decode) paths
- CausalConv1d: Depthwise separable causal convolution for sequence state management
- Gated RMSNorm: Gated RMSNorm (
output * (1.0 + weight.float())) - W8A8 Static Quantization: End-to-end INT8 inference with DynamicQuant + QuantBatchMatmulV3
- Hybrid Attention Architecture: Some layers use standard FlashAttention, others use GatedDeltaNet
- Multimodal Vision Encoder: VL variants include a vision encoder with different shapes from text-only models
- MTP (Multi-Task Prediction): Multi-token prediction affecting cache_position metadata and lm_head TP sharding
Core value:
- 100% operator coverage: All core computational logic in Qwen3.5 source code is modeled
- Controllable simulation accuracy: Calibrated against real profiling data, with <10% error for simulation vs hardware kernel time
- Extensible architecture: Supports Qwen3.5-27B, Qwen3.5-397B-A17B, and future MoE/VL variants
- Decomposed operator architecture: 7 decomposed LA ops (
la_*) replace a single fused op, each with independently registered performance models
1.2 Motivation¶
The msmodeling framework already supports Qwen3 series models, but Qwen3.5 introduces several architectural changes that prevent the existing framework from running directly or cause significant accuracy degradation:
| Pain Point | Description |
|---|---|
| Model loading failure | Qwen3.5's model_type is "qwen3_5" / "qwen3_5_moe", not registered in the framework |
| GatedDeltaNet not modelable | The original single linear_attention fused op couldn't distinguish prefill/decode paths or account for intermediate memory |
| RMSNorm fusion failure | Qwen3.5 RMSNorm uses output * (1.0 + weight) topology, mismatched with standard output * weight |
| Vision module TP sharding error | Qwen3.5 VL vision encoder should not be sharded by TP, but old code unconditionally sharded it |
| Intermediate memory not accounted | Chunk Delta Rule generates ~442MB of intermediate memory, the single fused op only counted inputs/outputs |
| CausalConv1d modeling incomplete | Original simulation merged state update and conv body into one stage |
| MTP support missing | Multi-token prediction scenarios lacked lm_head TP sharding and cache_position metadata handling |
| Vision TP config missing | No separate vision_tp_size parameter, unable to flexibly control vision module parallelism |
1.3 Goals¶
- Full support for all Qwen3.5 model variants (text-only, MoE, VL) inference simulation
- 7 decomposed LA ops (
la_*) independently modeled, each with@register_op_propertiesfor performance properties - CausalConv1d split into state update (
linear_attn_causal_conv_update) and conv body (linear_attn_causal_conv) operators - W8A8_STATIC quantization full-chain modeling (DynamicQuant + QuantBatchMatmulV3)
- Vision modules correctly skip TP sharding or use an independent
vision_tp_group - Chunk Delta Rule intermediate memory and
extra_static_cost_countproperly accounted - MTP
mtp.lm_headuses independentlmhead_tp_groupfor TP sharding - Adding new model variants only requires HF config registration and Monkey Patch, no core estimator modifications
Non-Goals:
- No training/fine-tuning simulation for Qwen3.5
- No precise All-Reduce communication modeling (TP communication is treated as a separate item, not included in per-layer kernel time)
- No modeling of NPU-specific ops (Transpose/Slice/ScatterUpdate/ZerosLike/GatherV3)
- No multi-node distributed inference simulation
2. Qwen3.5 Model Architecture Analysis¶
2.1 Hybrid Attention Architecture¶
In Qwen3.5's DecoderLayer, some layers use standard Self-Attention (FlashAttention), others use GatedDeltaNet (Linear Attention). Which layers use which attention type is determined by the self_attn.layer_type field.
Qwen3_5DecoderLayer:
├── input_layernorm → RMSNorm (standard)
├── self_attn:
│ ├── [FlashAttention] q_proj/k_proj/v_proj → RoPE → FlashAttention → o_proj
│ └── [GatedDeltaNet] in_proj_qkv/in_proj_z/in_proj_b/in_proj_a
│ → linear_attn_causal_conv / linear_attn_causal_conv_update → split QKV
│ → linear_attn_fused_gdn_gating (beta/g)
│ → linear_attn_chunk_gated_delta_rule / linear_attn_recurrent_gated_delta_rule
│ → linear_attn_gated_rmsnorm → out_proj
├── residual_add
├── post_attention_layernorm → RMSNorm
├── mlp: gate_proj/up_proj → SiLU(gate) * up → down_proj (SwiGLU)
└── residual_add
2.2 GatedDeltaNet Decomposed Op Flow¶
The GatedDeltaNet forward is decomposed into 7 TensorCast LA ops, routed through Monkey Patch:
| # | TC Op | Source Op | Constraint |
|---|---|---|---|
| 1 | linear_attn_apply_padding_mask |
apply_mask_to_padding_states(hidden_states, attention_mask) |
memory-bound |
| 2 | linear_attn_causal_conv / linear_attn_causal_conv_update |
causal_conv1d_fn(mixed_qkv) — conv body or state update |
memory-bound |
| 3 | linear_attn_fused_gdn_gating |
beta sigmoid + g (exp + softplus) + l2norm Q/K | compute-bound |
| 4 | linear_attn_chunk_gated_delta_rule |
chunk_gated_delta_rule(query, key, value, beta, g, chunk_size) |
compute-bound |
| 5 | linear_attn_recurrent_gated_delta_rule |
recurrent_gated_delta_rule(query, key, value, beta, g) |
compute-bound |
| 6 | linear_attn_gated_rmsnorm |
Gated RMSNorm (rmsnorm × weight × SiLU × z) | compute-bound |
| 7 | (Linear Projection) | in_proj_qkv/z/b/a + out_proj |
compute-bound |
2.3 Chunk/Recurrent Path Selection¶
In qwen3_5_moe.py's _patched_linear_attn_forward, the path is selected as follows:
has_previous_state = _has_previous_state(cache_params, cache_position, layer_idx)
use_recurrent = has_previous_state and _is_recurrent_decode_batch(seq_len, cache_position)
if use_recurrent:
conv_op = linear_attn_causal_conv_update
core_attn_out = linear_attn_recurrent_gated_delta_rule(...)
else:
conv_op = linear_attn_causal_conv
core_attn_out = linear_attn_chunk_gated_delta_rule(...)
tensor_cast_has_previous_state: read fromcache_positionmetadata, set by input_generator_is_recurrent_decode_batch: checkstensor_cast_query_lens/tensor_cast_is_decode/tensor_cast_num_mtp_tokens- In MTP scenarios, the decode batch's query_len can be
1or1 + num_mtp_tokens, both use the recurrent path - When a decode batch has multiple tokens and uses the recurrent path,
flatten_decode_batchreshapes(B, S, ...)to(B*S, 1, ...)
2.4 CausalConv1d Decomposition¶
CausalConv1d is split into two independent operators:
| Op | Description | HW Measured |
|---|---|---|
linear_attn_causal_conv |
Conv body: depthwise separable causal convolution + SiLU activation (prefill path) | ~150.1us (CausalConv1d kernel) |
linear_attn_causal_conv_update |
State update: read current input, shift and update convolution state buffer (decode path) | ~85.8us (_causal_conv1d_update_kernel) |
2.5 Quantization Scheme¶
Qwen3.5 uses W8A8_STATIC static quantization. LA ops are decoupled from quantization — Linear layers like in_proj_qkv/z/b/a and out_proj are automatically quantized via the framework's QuantLinearBase, while core attention ops like linear_attn_chunk_gated_delta_rule remain in FP32/BF16.
2.6 Model Variant Matrix¶
| Variant | model_type | Attention | MoE | Vision | MTP |
|---|---|---|---|---|---|
| Qwen3.5-27B | qwen3_5 |
Hybrid | No | Yes (VL) | Yes |
| Qwen3.5-397B-A17B | qwen3_5_moe |
Hybrid | Yes | Yes (VL) | Yes |
3. Design¶
3.1 Overall Architecture¶
3.1.1 Modified File List¶
tensor_cast/
├── ops/
│ └── la.py ← NEW: 7 LA decomposed operator definitions
├── performance_model/
│ └── __init__.py ← NEW: register_op_properties for 7 LA ops
│ KEPT: old linear_attention op (qwen3_next compat)
├── transformers/
│ ├── transformations.py ← NEW: vision_tp_group + LA TP plan + MTP lm_head TP
│ ├── builtin_model/
│ │ ├── qwen3_5_moe.py ← NEW: Monkey Patch (shared) + moe registration
│ │ └── qwen3_5.py ← NEW: Non-MoE variant registration (reuses patch)
├── core/
│ └── input_generator.py ← NEW: cache_position metadata + preprocessor_config
├── adapter/
│ └── context.py ← NEW: vision_tp_size context parameter
cli/
└── inference/
├── model_adapter.py ← NEW: --vision-tp-size argument
└── text_generate.py ← NEW: --vision-tp-size argument
3.1.2 Core Design: Decomposed Ops + Per-Op Property¶
The single fused linear_attention op is split into 7 independent LA ops, each with its own @register_op_properties for performance modeling. Comparison with the old approach (12-stage serial estimator):
| Approach | Pros | Cons | Verdict |
|---|---|---|---|
| Decomposed ops + per-op property | Framework handles serial stage summation automatically; each op independently modeled; consistent with framework Roofline engine | Cannot do per-stage max(compute, memory) + sum across stages |
✅ Adopted |
| Single fused op + custom estimator | Can precisely control 12-stage independent max(compute, memory) + sum |
Requires maintaining independent estimator logic, deviates from default Roofline engine | ❌ Not adopted |
In practice, since each decomposed op is already fine-grained enough (e.g., linear_attn_causal_conv contains only conv body, linear_attn_fused_gdn_gating contains only gating computation), the framework's default max(compute, memory) can accurately estimate time. The extra_static_cost_count mechanism additionally compensates for kernel launch overhead in chunk delta rule.
3.1.3 Intermediate Memory Modeling¶
Each LA op's register_op_properties additionally accounts for intermediate memory. Taking linear_attn_chunk_gated_delta_rule as an example:
# _add_linear_attention_chunk_scratch_memory:
# - Q/K/V/β/g 5-tensor FP32 cast + contiguous + transpose scratch
# - F.pad padding to chunk_size=64 multiple + chunk reshape
# - Output FP32→BF16 cast + transpose scratch
# - extra_static_cost_count += 8 (kernel launch overhead)
# _add_linear_attention_state_memory:
# - last_recurrent_state create and read/write (batch_size * num_v_heads * head_k_dim * head_v_dim * fp32_bytes)
# - state_read_passes and state_write_passes parameter control
3.1.4 Cache Position Metadata¶
When generating inputs for Qwen3.5 models, input_generator attaches the following metadata attributes to the cache_position tensor, used by _has_previous_state and _is_recurrent_decode_batch in qwen3_5_moe.py:
| Attribute | Type | Description |
|---|---|---|
tensor_cast_query_lens |
Tuple[int, ...] |
Query length per batch |
tensor_cast_is_decode |
Tuple[bool, ...] |
Whether each batch is decode |
tensor_cast_has_previous_state |
bool |
Whether previous KV cache state exists |
tensor_cast_base_decode_query_len |
int |
Base decode query length in MTP scenario |
tensor_cast_num_mtp_tokens |
int |
Number of MTP tokens |
tensor_cast_effective_decode_steps |
int |
Effective decode step count |
3.1.5 Vision TP and MTP LMHead TP¶
- Vision TP: New
--vision-tp-sizeCLI argument andvision_tensor_parallel_sizeconfig option. Defaults to 1 (vision module unsharded). Controlled viavision_tp_group. - MTP LMHead TP: In MTP scenarios,
mtp.lm_headuseslmhead_tp_group(defaults totp_group) for TP sharding, separate from the mainlm_headTP config.
3.2 Model Loading Flow¶
3.2.1 Model Registration¶
tensor_cast/transformers/builtin_model/__init__.pyauto-discovers and imports all.pymodulesqwen3_5_moe.pyregistersqwen3_5_moemodel_type with MoE config and sharedpatch_method_for_qwen3_5qwen3_5.pyregistersqwen3_5model_type (non-MoE), reusingpatch_method_for_qwen3_5
3.2.2 Monkey Patch Highlights¶
| Patch Target | Description |
|---|---|
Qwen3_5GatedDeltaNet.forward / Qwen3_5MoeGatedDeltaNet.forward |
Replace forward, routing projection, conv, attention, norm to 7 decomposed LA ops |
Qwen3_5DecoderLayer.forward / Qwen3_5MoeDecoderLayer.forward |
Replace forward, selecting linear_attention or full_attention path based on layer_type |
Qwen3_5TextModel._update_linear_attn_mask / Qwen3_5MoeTextModel._update_linear_attn_mask |
Replace mask update logic; returns None in compile mode to keep graph stable |
Qwen3_5Model.get_placeholder_mask / Qwen3_5MoeModel.get_placeholder_mask |
Skip image_features token count check in VL scenarios |
_set_qwen3_5_linear_attn_tp_size(model) |
Executed at the end of patch, sets tensor_cast_tp_size on linear attention layers |
3.3 LA TP Plan¶
In transformations.py, LA-related TP sharding plans are registered for Qwen3.5 models:
| Linear Layer | TP Type | TP Group | head_num |
|---|---|---|---|
linear_attn.in_proj_qkv |
COLWISE_LINEAR | tp_group | 2 * num_key_heads + num_value_heads |
linear_attn.in_proj_z |
COLWISE_LINEAR | tp_group | num_value_heads |
linear_attn.in_proj_b |
COLWISE_LINEAR | tp_group | num_value_heads |
linear_attn.in_proj_a |
COLWISE_LINEAR | tp_group | num_value_heads |
linear_attn.out_proj |
ROWWISE_LINEAR | o_proj_tp_group | num_value_heads |
Requires num_key_heads and num_value_heads to be divisible by tp_size.
4. Test Design¶
4.1 Unit Tests¶
| Test File | Test | Verification |
|---|---|---|
| test_runtime.py | linear_attn_causal_conv_eager |
linear_attn_causal_conv shape + perf model (no state memory) |
| test_runtime.py | linear_attn_causal_conv_update_eager |
linear_attn_causal_conv_update shape + state memory |
| test_runtime.py | linear_attn_apply_padding_mask_eager |
linear_attn_apply_padding_mask shape |
| test_runtime.py | linear_attn_fused_gdn_gating_eager |
4 output shapes (query_out, key_out, beta, g) |
| test_runtime.py | la_chunk_rule_includes_scratch_memory_and_extra_static |
memory_readwrite_bytes > 0 + extra_static_cost_count > 0 |
| test_runtime.py | linear_attn_recurrent_gated_delta_rule_eager |
shape + state memory read/write |
| test_runtime.py | linear_attn_gated_rmsnorm_eager |
shape |
| test_runtime.py | extra_static_cost_count_combines |
PerformanceProperties.combine correctly accumulates |
| test_runtime.py | qwen3_5_linear_attention_uses_local_tp_heads |
TP=16: prefill=chunk path, decode=recurrent path, MTP decode=recurrent path |
| test_runtime.py | qwen3_5_linear_attention_w8a8_reuses_quant_linear |
W8A8_STATIC: projections use quantize/static_quant_linear |
| test_runtime.py | qwen3_5_linear_attention_rejects_invalid_tp_size |
TP=32 not dividing num_k_heads=16, raises ValueError |
| test_runtime.py | qwen3_5_vision_tp_defaults_to_unsharded |
vision_tp_group.world_size=1, attn unsharded |
| test_runtime.py | qwen3_5_vision_tp_can_be_enabled_explicitly |
vision_tp_size=2 shards attn |
| test_runtime.py | qwen3_5_mtp_lm_head_uses_lmhead_tp_plan |
mtp.lm_head is ColumnParallelLinear |
| test_runtime.py | qwen3_5_linear_attention_with_padding_mask |
Non-None attention_mask triggers linear_attn_apply_padding_mask |
| test_input_generator.py | qwen3_5_decode_mtp_cache_position_metadata |
MTP decode: cache_position metadata correct |
| test_input_generator.py | varlen_qwen3_5_cache_position_starts_at_context |
Varlen: cache_position starts at context |
| test_input_generator.py | qwen3_vl_1080p_resize_to_1088x1920 |
VL image resize shape correct |
| test_input_generator.py | read_preprocessor_config_invalid_json_returns_none |
Invalid JSON returns None |
| test_input_generator.py | read_preprocessor_config_missing_file_returns_none |
Missing file returns None |
| test_input_generator.py | resolve_local_preprocessor_config_non_dir_returns_none |
Non-directory path returns None |
| test_input_generator.py | load_preprocessor_pixel_limits_no_config_json_returns_none |
Dir without config returns None |
4.2 Integration Tests¶
| Test | Content | Verification |
|---|---|---|
| End-to-end inference | Qwen3.5-27B full inference simulation | Simulation runs successfully, no crash |
| Multi-query scenario | decode mode (seq_len=1, has_previous_state) | Correctly uses recurrent path |
| Long sequence prefill | seq_len=2171, prefill | Correctly uses chunk path |
| W8A8_STATIC quantization | Full quantization chain | Quantization ops correctly called |
| TP=2 inference | Dual-card tensor parallelism | Communication ops correctly modeled |
| MoE variant | Qwen3.5-397B-A17B | grouped_matmul_swiglu correctly called |
4.3 Real HW Verification¶
The following are real HW measurements on ATLAS_800_A3_560T_128G_DIE (dual-card):
| Scenario | Input Len | Output Len | HW Time | Command |
|---|---|---|---|---|
| Prefill long text | 3500 | 1000 | 447.1 ms | python -m cli.inference.text_generate Qwen/Qwen3.5-27B --device ATLAS_800_A3_560T_128G_DIE --num-devices 2 --num-queries 1 --query-length 3510 --context-length 0 --quantize-linear-action W8A8_STATIC --tp-size 2 --compile |
| Decode long context | 3500 | 1000 | 27.33 ms | python -m cli.inference.text_generate Qwen/Qwen3.5-27B --device ATLAS_800_A3_560T_128G_DIE --num-devices 2 --num-queries 1 --query-length 4 --context-length 4250 --quantize-linear-action W8A8_STATIC --tp-size 2 --compile --num-mtp-tokens 3 --decode |
| Prefill very long text | 16000 | 1000 | 2.099 s | python -m cli.inference.text_generate Qwen/Qwen3.5-27B --device ATLAS_800_A3_560T_128G_DIE --num-devices 2 --num-queries 1 --query-length 16824 --context-length 0 --quantize-linear-action W8A8_STATIC --tp-size 2 --compile |
| Decode very long context | 16000 | 1000 | 28.161 ms | python -m cli.inference.text_generate Qwen/Qwen3.5-27B --device ATLAS_800_A3_560T_128G_DIE --num-devices 2 --num-queries 3 --query-length 4 --context-length 16500 --quantize-linear-action W8A8_STATIC --tp-size 2 --compile --num-mtp-tokens 3 --decode |
| VL Prefill (1080p + 30 token) | 1080p | 30 | 1.712 s | python -m cli.inference.text_generate Qwen/Qwen3.5-27B --device ATLAS_800_A3_560T_128G_DIE --num-devices 2 --num-queries 4 --query-length 30 --context-length 0 --image-height 1080 --image-width 1920 --image-batch-size 1 --quantize-linear-action W8A8_STATIC --tp-size 2 --compile --num-mtp-tokens 3 |
| VL Decode (1080p + 30 token) | 1080p | 30 | 39.84 ms | python -m cli.inference.text_generate Qwen/Qwen3.5-27B --device ATLAS_800_A3_560T_128G_DIE --num-devices 2 --num-queries 8 --query-length 4 --context-length 16500 --quantize-linear-action W8A8_STATIC --tp-size 2 --compile --num-mtp-tokens 3 --decode |
| Prefill very long text (35B-A3B) | 16000 | 1000 | 504 ms | python -m cli.inference.text_generate Qwen/Qwen3.5-35B-A3B --device ATLAS_800_A3_560T_128G_DIE --num-devices 2 --num-queries 1 --query-length 8192 --context-length 0 --quantize-linear-action W4A8_STATIC --tp-size 2 --compile --num-mtp-tokens 3 |
| Decode very long context (W4A8) | 16000 | 1000 | 29.76 ms | python -m cli.inference.text_generate Qwen/Qwen3.5-35B-A3B --device ATLAS_800_A3_560T_128G_DIE --num-devices 2 --num-queries 8 --query-length 4 --context-length 16500 --quantize-linear-action W4A8_STATIC --tp-size 2 --compile --num-mtp-tokens 3 --decode |
5. Risks and Mitigations¶
| Risk | Impact | Mitigation |
|---|---|---|
| Model architecture changes | New Qwen3.5 versions may modify GatedDeltaNet implementation | Monkey Patch must be updated synchronously; detected quickly by test_model_load |
| Intermediate memory estimation error | Chunk Delta Rule scratch memory estimation may differ from reality | Compensate kernel launch overhead via extra_static_cost_count |
| CausalConv1d tiled kernel overhead | Roofline model cannot fully cover NPU tiled kernel overhead | Marked as P1 optimization; ~125us residual acceptable at current stage |
| All-Reduce communication | 4 All-Reduce per layer (TP=2, ~682us), not yet modeled in per-layer kernel time | Communication as separate item, doesn't affect per-layer algorithm accuracy |
| Qwen3.5 upstream dependency | Depends on transformers mainline Qwen3.5 implementation | Must lock transformers version; Monkey Patch must adapt to corresponding version |
| Vision encoder complexity | VL variants contain special structures like DeepStack | Reuse existing Qwen3-VL Patch experience |
6. Related Work¶
| Project/Tool | Approach | Reference & Differences |
|---|---|---|
| Qwen3 simulation (existing) | Standard FlashAttention + RoPE + SwiGLU modeling | Qwen3.5 adds GatedDeltaNet and CausalConv1d on this basis |
| Qwen3-VL simulation (existing) | Vision encoder + DeepStack + multimodal fusion | Reuse vision module Monkey Patch and TP sharding experience |
| Qwen3Next simulation (existing) | Uses old linear_attention fused op |
Old op retained for compatibility |
| Chunk Delta Rule reference implementation | modeling_qwen3_5.py official source code |
Line-by-line cross-reference to ensure FLOPs and memory estimation consistency |
Appendix¶
Appendix A: References¶
- Qwen3.5 Model Source Code —
modeling_qwen3_5.py - Qwen3.5-27B Simulation Operator Coverage Analysis Report (internal document)
- Measured LINEARATTENTION Kernel Records (internal document)
- Code Change Alignment Notes (internal document)
- Visual Comparison (internal document)
Appendix B: Glossary¶
| Term | Description |
|---|---|
| GatedDeltaNet | Qwen3.5's linear attention mechanism |
| Chunk Gated Delta Rule | Sequence split into chunk_size=64 blocks for intra-chunk attention + inter-chunk recurrence |
| Recurrent Gated Delta Rule | Token-by-token recurrent attention state update |
| CausalConv1d | Causal depthwise separable convolution |
| Gated RMSNorm | Qwen3.5-specific RMSNorm variant: output * (1.0 + weight) |
| W8A8_STATIC | Weight INT8 + Activation INT8 static quantization |
| MTP | Multi-Task Prediction |
| LA | Linear Attention |
Appendix C: Document History¶
| Version | Date | Changes | Author |
|---|---|---|---|
| v1.0 | 2026-05-19 | Initial version (performance regression test framework) | @yuyinkai |
| v2.0 | 2026-06-04 | Rewritten as Qwen3.5 simulation support design document | @yuyinkai |
| v3.0 | 2026-06-11 | Refactored to decomposed LA operator architecture (7 la_* ops), added vision TP / MTP / cache_position metadata / preprocessor config support |
@yuyinkai |