ErrLookup › Background articles › Tensor shape mismatch errors ("must have shape", "expected shape ... got ..."): when tensor dimensions disagree with what an op or layer was told to expect
Tensor shape mismatch errors ("must have shape", "expected shape ... got ..."): when tensor dimensions disagree with what an op or layer was told to expect
Tensor shape mismatch errors fire when a tensor's dimensions don't match a contract another component depends on — a kernel's expected strides, a layer's assumed model width, or metadata describing a batch. You meet them as ValueErrors naming expected and received shapes, raised by validation guards in attention kernels, multimodal preprocessors, quantization checkpoint checkers, CRF layers, and functional model call sites across libraries like SGLang, Keras, HanLP, and ruflo. They almost always mean two producers in the pipeline sized the same data differently, not that the math itself failed.
Distilled from 203 documented records across 6 repositories.
Background
Shape-mismatch errors come from validation guards sitting at the boundary between a producer and a consumer of a tensor. Kernels and low-level ops (SGLang's Triton decode kernels, fused RoPE pooling, FlashAttention in ruflo) check incoming tensors against an exact layout — e.g. d_cache must be [slots, HV, L, V] because the kernel indexes with those strides — and refuse anything else rather than silently corrupt memory. Higher-level layers (Keras GroupedQueryAttention, TimeDistributed, Functional ops; HanLP's TorchCRF) validate during shape inference or at call time, comparing symbolic input shapes against traced shapes or against each other. A third variant checks tensor-against-metadata consistency: SGLang's multimodal paths compare vision-encoder token counts against preprocessor grid metadata, and its quantization checkers verify checkpoint scale tensors against packed weight shapes.
From the caller's side the error usually prints both sides of the disagreement — "expected shape {X}, got {Y}" or "got {a} keys, {b} values" — which is the primary diagnostic: the diff between the two shapes tells you which dimension drifted. The rank (number of axes) is the most common offender (a 2-D tensor where 3-D is required, or a traced 3-D input receiving 4-D data), followed by a specific dimension disagreeing (head counts, per-token row counts, feature widths).
The family varies by library in what the shapes are measured against. In SGLang the reference is often derived from another tensor or config (num_v_heads from the model config, token counts from preprocessor metadata, HV/K inferred from initial_state), so a mismatch usually means config, checkpoint, and runtime allocation drifted apart. In Keras the reference is the shape the graph was traced or built with, so mismatches surface after data pipelines add or drop a batch axis. In HanLP it is pairwise consistency between two tensors (emissions vs tags) that must travel together; in ruflo it is parallel arrays (keys/values, query/key dimensions) that must be built from the same source. Note also that some checks are stricter than sister APIs — ruflo requires d_v == d_k where PyTorch-style attention allows them to differ — so ported code can trip checks that never fired in the original framework.
Common causes
- Rank mismatch (wrong number of dimensions). Passing a 2-D flattened tensor where the kernel wants 3-D [tokens, heads, dim] (rope_pool_fused), a 4-D [B,C,H,W] latent where [B,S,D] is expected, or calling a Keras Functional with more/fewer axes than it was traced with (e.g. a single image vs a batch). TimeDistributed needs at least 3 dims to split batch/time/features.
- Component dims disagree (Q/K/V or emissions/tags built from different sources). query width != value width in GroupedQueryAttention; keys.length != values.length or d_q != d_k in FlashAttention; emissions and tags padded to different lengths in a CRF. Typically two producers in the pipeline derived their sizes independently.
- Cache or buffer allocated under stale assumptions. A KV/state cache or MXFP8 scale buffer sized for a different page_size, head count, or value_dim than the current call — often after a cache resize, a config change, or a version upgrade where the shape formula changed.
- Config, checkpoint, and runtime geometry drift. Head counts, key/value dims, or dt_bias sizes derived from the model config don't match what the checkpoint actually contains (q_proj output width, pre_quant_scale length, scale-block layout). The exported tensors were built for a different architecture or quantization recipe than the one loading them.
- Batching and slicing that breaks paired data. Re-batching or slicing multimodal inputs separates images from their grid_thw metadata, or slices varlen inputs without slicing the state-pool indices with the same mask, so one side of a contract describes a different batch than the other.
- Recomputed vs original dimensions diverge. bs or draft_token_num recomputed separately from the tensor it must describe (DFlash logits row-count checks), cu_seqlens built for a different sequence count than initial_state_indices, or hardcoded shape literals that no longer track the actual tensor.
- Porting code between frameworks with different contracts. ruflo's FlashAttention requires d_v == d_k, stricter than APIs allowing independent value dims; layout conventions ([batch, seq, heads, dim] vs [tokens, heads, dim]) and batch_first defaults differ between libraries, so working code trips checks after a port.
What usually fixes it
- Read the two shapes in the message as a diff: the differing axis (or rank) identifies which producer is wrong; log or assert both shapes at the call site to find which side diverged.
- Make one component the source of truth: derive secondary values (head counts, token counts, expected rows, scale shapes) from the tensor or config they must describe — e.g. bs and draft_token_num from candidates.shape — instead of tracking them in separate variables or hardcoded literals.
- Keep paired data atomic: build keys and values from the same source, pad emissions and tags in the same collate function, slice metadata and index tensors with the same mask as their inputs, and let the scheduler batch multimodal inputs rather than hand-slicing.
- Insert projections or reshapes at boundaries: Dense(model_dim) before cross-attention between different-width models, .view/.reshape to the kernel's required layout before kernel calls, expand_dims for missing time/batch axes.
- After config changes, version upgrades, or cache resizes, reallocate caches and scale buffers from the current formula rather than reusing or hardcoding old shapes; re-quantize checkpoints instead of editing architectures post-export.
- If the reference value genuinely doesn't apply (packing disabled, no prior states needed, deterministic samples unnecessary), pass None so the callee generates the correct thing itself.
Documented occurrences
- Kimi-K3 deferred feature length does not match image grids (sgl-project/sglang)
- `d_cache` must have shape [slots, HV, L, V]. (sgl-project/sglang)
- MXFP8 fused prologue requires interleaved K/V scale buffers with shape {sf_shape}, got {tuple(sfk.shape)} and {tuple(sfv.shape)}. (sgl-project/sglang)
- pairs must be a torch.Tensor of shape [N, 2] (sgl-project/sglang)
- `k_cache` must have shape [slots, H, L, K]. (sgl-project/sglang)
- next_token_logits row count mismatch. Expected {bs * draft_token_num}, got {next_token_logits.shape[0]}. (sgl-project/sglang)
- The number of initial states is expected to be equal to the number of input sequences, i.e., {len(cu_seqlens) - 1} rather than {initial_state_indices.shape[0]}. (sgl-project/sglang)
- Expected packed image latents [B, S0, D]. (sgl-project/sglang)
- `g_cache` must have shape [slots, HV, L, K]. (sgl-project/sglang)
- The last dimension of `query_shape` and `value_shape` must be equal, but are {query_shape[-1]}, {value_shape[-1]}. Received: query_shape={query_shape}, value_shape={value_shape} (keras-team/keras)
- MXFP8 fused decode prologue requires interleaved K/V scale buffers with shape {sf_shape}, got {tuple(sfk.shape)} and {tuple(sfv.shape)}. (sgl-project/sglang)
- packed seq_len {seq_len} not divisible by the combined sequence-parallel world size {sp_ws} (ulysses={ulysses_ws} x ring={ring_ws}) (sgl-project/sglang)
- `TimeDistributed` Layer should be passed an `input_shape` with at least 3 dimensions, received: {input_shape} (keras-team/keras)
- FlashAttention: Keys and values must have same count. Got ${keys.length} keys, ${values.length} values (ruvnet/ruflo)
- Invalid packed `mixed_qkv` last dim={qkv_dim} for HV={HV}, V={V}. (sgl-project/sglang)
- Comfy W4A4 layer {prefix!r} has input size {logical_input_size}, incompatible with quant_group_size=64 and convrot_groupsize={convrot_group_size} (sgl-project/sglang)
- Comfy NVFP4 layer {prefix!r} has an incompatible pre_quant_scale: {pre_scale_dtype}{pre_scale_shape} (sgl-project/sglang)
- FlashAttention: Key and value dimensions must match. Got K=${kDim}, V=${vDim} (ruvnet/ruflo)
- For `padding='same'`, `output_size` width ({W}) must be in the range ((gW-1)*pW, gW*pW], i.e. ({static_gW * pW - pW}, {static_gW * pW}]. Got: gW={static_gW}, pW={pW}. (keras-team/keras)
- rope_pool_fused expects q/k/v to be 3-D (sgl-project/sglang)
…and 183 more across the corpus — use search.
Honest provenance: generated on 2026-08-28 from AI-assisted analysis of the linked records. See how records are made.