{
  "site": "https://neurarch.com",
  "description": "Every structural check Neurarch runs on a model graph. The same checks run in the editor, in CI through the GitHub Action, and over the wire at POST /api/v1/check.",
  "count": 41,
  "verify": "https://www.neurarch.com/api/v1/check",
  "catalogue": "https://neurarch.com/rules.html",
  "checks": [
    {
      "id": "G01",
      "title": "Impact preview",
      "severity": "warn",
      "category": "blast radius",
      "engineId": "impact",
      "page": "https://neurarch.com/r/impact-preview",
      "provenance": null,
      "detail": {
        "trigger": "Any destructive action (delete_component, delete_components_matching, rename_components_matching, clear_canvas, replace_model) where downstream impact > 4 layers, or any of them are shape-changing.",
        "surfaces": "Number of downstream layers affected, how many would reshape, how many carry weights (rebuild required).",
        "override": "User confirms in the impact-preview card before the action lands."
      }
    },
    {
      "id": "G02",
      "title": "Param explosion",
      "severity": "block",
      "category": "resource",
      "engineId": "param-explosion",
      "page": "https://neurarch.com/r/param-explosion",
      "provenance": null,
      "detail": {
        "trigger": "Estimated total parameter count grows by ≥ threshold × baseline (default 5×, slider 2-20×). Blocks at 2× threshold, warns below.",
        "why": "A misread \"scale this 10×\" prompt should not silently turn a 60M model into a 6B model that won't fit anywhere.",
        "setting": "useProviderStore.paramExplosionThreshold(). Settings → \"Param explosion threshold\"."
      }
    },
    {
      "id": "G03",
      "title": "Cycle introduction",
      "severity": "block",
      "category": "structure",
      "engineId": "cycle",
      "page": "https://neurarch.com/r/cycle-introduction",
      "provenance": null,
      "detail": {
        "trigger": "An add_connection closes a cycle in the existing DAG. DFS check on the union of current edges and proposed edges.",
        "why": "NN forward graphs are acyclic. Recurrence lives inside the layer type (LSTM, GRU), not as a graph cycle. A connection that closes a cycle is almost always a mis-step.",
        "source": "DAG requirement matches PyTorch nn.Module.forward semantics. torch.autograd docs note that gradient computation requires a DAG."
      }
    },
    {
      "id": "G04",
      "title": "Orphan layers",
      "severity": "warn",
      "category": "structure",
      "engineId": "orphan",
      "page": "https://neurarch.com/r/orphan-layers",
      "provenance": null,
      "detail": {
        "trigger": "add_component without afterName, and no follow-up add_connection referencing the new component as either endpoint.",
        "why": "A new layer that's not wired is dead code. The agent occasionally proposes these when it forgets to chain edits; the warning flips the decision back to the user."
      }
    },
    {
      "id": "G05",
      "title": "Shape inference (new)",
      "severity": "block",
      "category": "shape",
      "engineId": "shape",
      "page": "https://neurarch.com/r/shape-inference-new",
      "provenance": "head-dim-divisibility",
      "detail": {
        "trigger": "Propagates tensor shapes through the sandboxed post-action graph and surfaces only the issues the action would introduce. Sub-checks: attention embedDim % numHeads, GQA numHeads % numKVHeads, elementwise merge parent equality, concat axis compatibility, explicit linear inFeatures vs upstream, computeOutputShape throw, and outputs with NaN / 0 / negative dims at the first layer to introduce them.",
        "source": "Per-layer transforms from componentRegistry.computeOutputShape. Head-dim convention from Vaswani et al. 2017 (Attention Is All You Need). GQA ratio from Ainslie et al. 2023 (GQA).",
        "why": "Text-layer review tools (Cursor, Copilot) can't catch embedDim=384, numHeads=5 until the GPU rejects the kernel. This gate fires sub-millisecond, pre-apply."
      }
    },
    {
      "id": "G06",
      "title": "Param range",
      "severity": "warn",
      "category": "param-range",
      "engineId": null,
      "page": "https://neurarch.com/r/param-range",
      "provenance": null,
      "detail": {
        "trigger": "A single parameter value falls outside its conventional range on add_component or update_params: e.g. dropout above 1, stride of 0, numHeads of 0. Uses the same convention-based ranges the inspector shows inline.",
        "why": "These are values the shape gate cannot see: the graph still propagates, the model still builds, and the run is quietly wrong. A dropout of 1.2 is not a shape error, it is a silently dead layer.",
        "source": "src/utils/paramConstraints.ts::validateParamValue, shared with the inspector so CI and the canvas never disagree."
      }
    },
    {
      "id": "R01",
      "title": "No Input node",
      "severity": "block",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/no-input-node",
      "provenance": null,
      "detail": {
        "trigger": "Model has ≥ 1 component but no component of type input.",
        "why": "Without an Input layer, tensor shapes can't be propagated and generated code is incomplete.",
        "fix": "Drag an Input layer from the I/O section of the palette."
      }
    },
    {
      "id": "R02",
      "title": "No Output node",
      "severity": "warn",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/no-output-node",
      "provenance": null,
      "detail": {
        "trigger": "Model has > 1 component but no component of type output.",
        "why": "Code generator can't tell where the forward pass terminates."
      }
    },
    {
      "id": "R03",
      "title": "Isolated components",
      "severity": "warn",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/isolated-components",
      "provenance": null,
      "detail": {
        "trigger": "Component has no inbound and no outbound connections.",
        "why": "Most often a stranded layer left over from a refactor. Code generator skips it but it inflates the visual graph."
      }
    },
    {
      "id": "R04",
      "title": "Dead-end layer",
      "severity": "warn",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/dead-end-layer",
      "provenance": null,
      "detail": {
        "trigger": "A non-output node has inbound connections but no outbound connections.",
        "why": "Compute happens but the result is discarded. Gradients flow into a black hole."
      }
    },
    {
      "id": "R05",
      "title": "BatchNorm / LayerNorm after activation",
      "severity": "warn",
      "category": "ordering",
      "engineId": null,
      "page": "https://neurarch.com/r/batchnorm-layernorm-after-activation",
      "provenance": null,
      "detail": {
        "trigger": "A normalization layer is connected directly downstream of an activation (relu, gelu, swish, etc.).",
        "why": "Normalization is meant to stabilize the pre-activation distribution. Applying it after activation breaks the assumption and shifts already-nonlinear features back toward zero mean.",
        "source": "Ioffe & Szegedy 2015, BatchNorm places BN between Linear/Conv and the activation."
      }
    },
    {
      "id": "R06",
      "title": "Dropout directly before BatchNorm",
      "severity": "info",
      "category": "ordering",
      "engineId": null,
      "page": "https://neurarch.com/r/dropout-directly-before-batchnorm",
      "provenance": null,
      "detail": {
        "trigger": "A dropout layer is connected directly upstream of a BatchNorm.",
        "why": "Dropout at train time changes the activation variance; BN's running statistics get distorted. A known train-vs-eval mismatch.",
        "source": "Li et al. 2018, \"Understanding the Disharmony Between Dropout and Batch Normalization\"."
      }
    },
    {
      "id": "R07",
      "title": "Softmax / Sigmoid directly before Output",
      "severity": "info",
      "category": "ordering",
      "engineId": null,
      "page": "https://neurarch.com/r/softmax-sigmoid-directly-before-output",
      "provenance": null,
      "detail": {
        "trigger": "An explicit Softmax or Sigmoid layer is wired directly into Output.",
        "why": "PyTorch's nn.CrossEntropyLoss applies LogSoftmax internally; an explicit Softmax before it double-applies and slows training. BCEWithLogitsLoss has the same issue with Sigmoid.",
        "source": "torch.nn.CrossEntropyLoss docs."
      }
    },
    {
      "id": "R08",
      "title": "Normalization at output",
      "severity": "warn",
      "category": "ordering",
      "engineId": null,
      "page": "https://neurarch.com/r/normalization-at-output",
      "provenance": null,
      "detail": {
        "trigger": "A normalization layer is wired directly into Output without a final linear / classification head.",
        "why": "Normalization zero-centers the logits, breaking calibration. The model's output scale is no longer meaningful."
      }
    },
    {
      "id": "R09",
      "title": "Deep network with no residual connections",
      "severity": "warn",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/deep-network-with-no-residual-connections",
      "provenance": null,
      "detail": {
        "trigger": "≥ 8 weight-carrying layers (conv2d / linear / etc.) and zero residual / skip / add nodes.",
        "why": "Gradient signal degrades through depth without skip connections.",
        "source": "He et al. 2015, ResNet."
      }
    },
    {
      "id": "R10",
      "title": "Attention without positional encoding",
      "severity": "warn",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/attention-without-positional-encoding",
      "provenance": null,
      "detail": {
        "trigger": "Model contains attention layers but no positional encoding (sinusoidal, learned, RoPE, ALiBi).",
        "why": "Self-attention is permutation-invariant. Without PE, the model is a bag-of-words and can't learn order.",
        "source": "Vaswani et al. 2017, Attention Is All You Need, Section 3.5."
      }
    },
    {
      "id": "R11",
      "title": "Sigmoid / Tanh in deep networks",
      "severity": "info",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/sigmoid-tanh-in-deep-networks",
      "provenance": null,
      "detail": {
        "trigger": "Sigmoid or Tanh activation appears in a network with ≥ 5 weight-carrying layers.",
        "why": "Saturating activations vanish gradients in deep stacks. ReLU / GELU / SiLU are the modern defaults.",
        "source": "Glorot & Bengio 2010 on the vanishing gradient problem."
      }
    },
    {
      "id": "R12",
      "title": "Deep network with no normalization",
      "severity": "info",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/deep-network-with-no-normalization",
      "provenance": null,
      "detail": {
        "trigger": "≥ 7 non-I/O layers and zero normalization layers (BN / LN / IN / GN / RMSNorm).",
        "why": "Without normalization, deep nets converge slowly and are sensitive to initialization scale."
      }
    },
    {
      "id": "R13",
      "title": "Very high dropout rate",
      "severity": "warn",
      "category": "performance",
      "engineId": null,
      "page": "https://neurarch.com/r/very-high-dropout-rate",
      "provenance": null,
      "detail": {
        "trigger": "Any dropout layer with p > 0.65.",
        "why": "Common dropout rates are 0.1 to 0.5. Anything above 0.65 usually indicates a typo or a confused regularization strategy."
      }
    },
    {
      "id": "R14",
      "title": "Very large activation tensor",
      "severity": "warn",
      "category": "performance",
      "engineId": null,
      "page": "https://neurarch.com/r/very-large-activation-tensor",
      "provenance": null,
      "detail": {
        "trigger": "Any layer's estimated activation tensor exceeds 50 M elements (~200 MB per sample at float32).",
        "why": "Activations live in GPU memory through the backward pass. One bloated layer forces tiny batch sizes or OOM."
      }
    },
    {
      "id": "R15",
      "title": "MoE without auxiliary loss",
      "severity": "info",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/moe-without-auxiliary-loss",
      "provenance": null,
      "detail": {
        "trigger": "Model contains an MoE layer but no auxiliary load-balance loss is referenced.",
        "why": "Without an aux loss, experts collapse into a few dominant ones. Most papers add a 0.01 weight router-balance term.",
        "source": "Shazeer et al. 2017, Sparsely-Gated MoE, Section 4."
      }
    },
    {
      "id": "R16",
      "title": "GQA numHeads not divisible by numKVHeads",
      "severity": "block",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/gqa-numheads-not-divisible-by-numkvheads",
      "provenance": "gqa-head-divisibility",
      "detail": {
        "trigger": "A groupedQueryAttention component has params where numHeads % numKVHeads != 0.",
        "why": "GQA groups query heads; the group size must divide the head count. Mis-set ratios crash on the first attention forward.",
        "source": "Ainslie et al. 2023, GQA."
      }
    },
    {
      "id": "R17",
      "title": "SwiGLU intermediateSize convention",
      "severity": "info",
      "category": "performance",
      "engineId": null,
      "page": "https://neurarch.com/r/swiglu-intermediatesize-convention",
      "provenance": null,
      "detail": {
        "trigger": "A swiGLU / gated FFN has intermediateSize that doesn't follow the common ~(2/3) × 4 × hidden convention used in LLaMA / Mistral.",
        "why": "Param-count parity with standard FFN; mis-sized SwiGLU silently changes the FLOP and param budget.",
        "source": "Shazeer 2020, GLU Variants."
      }
    },
    {
      "id": "R18",
      "title": "Conv feeds Linear with no flatten / pool",
      "severity": "block",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/conv-feeds-linear-with-no-flatten-pool",
      "provenance": null,
      "detail": {
        "trigger": "A convolution (conv1d/2d/3d or a depthwise/separable/transpose variant) connects directly into a linear layer.",
        "why": "Conv outputs a multi-dimensional feature map; Linear expects a flat [batch, features] tensor. The forward pass raises a shape error, or silently mis-multiplies the spatial dims. Insert a Flatten or a Global Average Pool between them.",
        "source": "Standard CNN classifier construction (e.g. LeNet / AlexNet head)."
      }
    },
    {
      "id": "R19",
      "title": "Back-to-back activations",
      "severity": "warn",
      "category": "ordering",
      "engineId": null,
      "page": "https://neurarch.com/r/back-to-back-activations",
      "provenance": null,
      "detail": {
        "trigger": "An activation node (relu/gelu/silu/sigmoid/tanh/softmax/…) connects directly into another activation node with no Linear/Conv/Norm between them.",
        "why": "Two activations in a row add compute but no expressivity (same-type is a no-op duplicate). A common slip is ReLU → Softmax, which clips logits to ≥ 0 and distorts the output distribution.",
        "source": "Composition of pointwise non-linearities adds no representational power without an intervening affine map (universal-approximation premise)."
      }
    },
    {
      "id": "R20",
      "title": "Linear → Linear with no activation",
      "severity": "info",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/linear-linear-with-no-activation",
      "provenance": null,
      "detail": {
        "trigger": "A linear layer connects directly into another linear layer with no activation between them.",
        "why": "Two stacked linear maps collapse into one (W₂·W₁), so the extra layer costs parameters but adds no representational power, usually a forgotten ReLU. Deliberate low-rank / factorized projections (down-proj → up-proj) are the safe exception.",
        "source": "Linear-map composition closure; the rank of the product cannot exceed the smaller intermediate dimension."
      }
    },
    {
      "id": "R21",
      "title": "Dropout immediately before Output",
      "severity": "warn",
      "category": "ordering",
      "engineId": null,
      "page": "https://neurarch.com/r/dropout-immediately-before-output",
      "provenance": null,
      "detail": {
        "trigger": "A dropout layer is the last node before the output node.",
        "why": "In training this randomly zeroes the final logits fed to the loss; at eval it is a no-op, so train and eval behaviour diverge. Dropout belongs before the final projection (Linear/Conv), not after it.",
        "source": "Srivastava et al. 2014 (Dropout): dropout regularizes hidden representations, not output logits."
      }
    },
    {
      "id": "R22",
      "title": "Conv stride larger than kernel",
      "severity": "warn",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/conv-stride-larger-than-kernel",
      "provenance": null,
      "detail": {
        "trigger": "A downsampling convolution (conv1d/2d/3d, depthwise, or separable) has stride > kernelSize.",
        "why": "The kernel jumps past its own footprint each step, so a band of the input is never read, a silent loss of information. Non-overlapping patches use stride == kernel (e.g. ViT 16/16); stride > kernel is almost always a typo.",
        "source": "Convolution arithmetic: Dumoulin & Visin 2016."
      }
    },
    {
      "id": "R23",
      "title": "Non-spatial tensor into Conv",
      "severity": "warn",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/non-spatial-tensor-into-conv",
      "provenance": null,
      "detail": {
        "trigger": "A flatten or linear layer connects directly into a (non-transpose) convolution.",
        "why": "Mirror of R18: a Conv needs a [channels, …spatial] feature map, but Flatten / Linear emit a flat [batch, features] vector. The forward pass raises a shape error unless the spatial dims are rebuilt with a Reshape / Unflatten first.",
        "source": "PyTorch nn.Conv*d input-shape contract."
      }
    },
    {
      "id": "R24",
      "title": "Back-to-back normalization",
      "severity": "info",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/back-to-back-normalization",
      "provenance": null,
      "detail": {
        "trigger": "A normalization layer connects directly into another normalization layer (e.g. LayerNorm → BatchNorm).",
        "why": "Normalizing an already-normalized tensor is redundant; the second layer mostly re-centres/re-scales what the first produced and just burns its own learnable parameters.",
        "source": "Idempotence of standardization: Ioffe & Szegedy 2015."
      }
    },
    {
      "id": "R25",
      "title": "Duplicate positional encoding",
      "severity": "info",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/duplicate-positional-encoding",
      "provenance": null,
      "detail": {
        "trigger": "More than one positional-encoding layer (positionalEncoding, learnedPositionalEmbedding, rope, alibi) in the model.",
        "why": "Position is normally injected once. Stacking absolute + rotary, or two of the same, double-counts position and tends to hurt more than help.",
        "source": "Positional-encoding design: Vaswani et al. 2017, RoPE (Su et al. 2021)."
      }
    },
    {
      "id": "R26",
      "title": "Pooling feeds Linear with no flatten",
      "severity": "warn",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/pooling-feeds-linear-with-no-flatten",
      "provenance": null,
      "detail": {
        "trigger": "A spatial pooling layer (maxpool, avgpool, adaptive pool) connects directly into a linear layer. Global pools are exempt: they already collapse spatial dims.",
        "why": "Sibling of R18: pooling still emits a [channels, …spatial] map, but Linear expects a flat [batch, features] tensor, so the forward pass raises a shape error. Insert a Flatten or a Global Average Pool first.",
        "source": "Standard CNN classifier head construction."
      }
    },
    {
      "id": "R27",
      "title": "Flatten before attention",
      "severity": "warn",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/flatten-before-attention",
      "provenance": null,
      "detail": {
        "trigger": "A flatten layer connects directly into an attention layer.",
        "why": "Flatten collapses the sequence dimension into one long vector, leaving a length-1 sequence, so attention has nothing to relate. Keep the [sequence, dim] layout and flatten only after the attention stack.",
        "source": "Attention operates over a sequence axis: Vaswani et al. 2017."
      }
    },
    {
      "id": "R28",
      "title": "ConvTranspose checkerboard risk",
      "severity": "info",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/convtranspose-checkerboard-risk",
      "provenance": null,
      "detail": {
        "trigger": "A ConvTranspose2d has kernelSize not divisible by stride (with stride > 1).",
        "why": "Uneven kernel overlap during upsampling deposits more weight on some output pixels than others, producing checkerboard artifacts. Make kernelSize a multiple of stride, or upsample with Upsample + Conv (resize-convolution).",
        "source": "Odena et al. 2016, Deconvolution and Checkerboard Artifacts."
      }
    },
    {
      "id": "R29",
      "title": "GroupNorm channels not divisible by numGroups",
      "severity": "block",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/groupnorm-channels-not-divisible-by-numgroups",
      "provenance": "compute-error",
      "detail": {
        "trigger": "A GroupNorm layer's channel count is not an exact multiple of numGroups.",
        "why": "GroupNorm splits channels into equal groups; a non-divisible count raises at construction. Parallel to the GQA / attention head-dim divisibility checks. Set numGroups to a divisor of the channel count.",
        "source": "Wu & He 2018, Group Normalization."
      }
    },
    {
      "id": "R30",
      "title": "Very large Linear layer",
      "severity": "warn",
      "category": "performance",
      "engineId": null,
      "page": "https://neurarch.com/r/very-large-linear-layer",
      "provenance": null,
      "detail": {
        "trigger": "A single Linear exceeds ~1B parameters (inFeatures × outFeatures).",
        "why": "A dense layer that large (~4 GB float32) almost always means a feature map was flattened without pooling first. Add a Global Average Pool / more downsampling, or factorize the layer. Embedding and vocab-projection heads are the expected exception.",
        "source": "Parameter-budget hygiene; a single matrix this size dominates model memory."
      }
    },
    {
      "id": "R31",
      "title": "Full multi-head attention at LLM scale",
      "severity": "info",
      "category": "performance",
      "engineId": null,
      "page": "https://neurarch.com/r/full-multi-head-attention-at-llm-scale",
      "provenance": null,
      "detail": {
        "trigger": "A transformer stack (≥6 attention layers) at LLM width (embedDim ≥ 2048) uses full multi-head attention everywhere, with no grouped-query or latent attention present.",
        "why": "At real model scale the KV cache, not the weights, dominates serving memory. Full per-head K/V caching is what production LLMs move away from: grouped-query attention (e.g. 8:1) cuts the cache ~8×, and multi-head latent attention (MLA) shrinks it ~10× or more, without changing the parameter count. Advice, not a bug: set numKVHeads below numHeads, or switch to mla.",
        "source": "Serving-cost roofline; the same KV-cache math the app and the public serving calculator run."
      }
    },
    {
      "id": "R32",
      "title": "Default init assumes ReLU, saturating activation follows",
      "severity": "info",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/default-init-assumes-relu-saturating-activation-follows",
      "provenance": null,
      "detail": {
        "trigger": "A Linear/Conv layer feeds a sigmoid or tanh activation directly.",
        "why": "PyTorch's default Kaiming (He) init is derived for ReLU-family activations. Feeding a saturating activation from a He-initialized layer starts training in the saturated tails and shrinks early gradients. Use Xavier init with the matching gain, or a ReLU-family activation.",
        "source": "Glorot & Bengio 2010 (Xavier) vs He et al. 2015 (Kaiming) derivation assumptions."
      }
    },
    {
      "id": "R33",
      "title": "Deep attention stack without depth-scaled init",
      "severity": "info",
      "category": "pattern",
      "engineId": null,
      "page": "https://neurarch.com/r/deep-attention-stack-without-depth-scaled-init",
      "provenance": null,
      "detail": {
        "trigger": "≥ 8 attention layers stacked in one model.",
        "why": "Residual-branch outputs add up with depth; unscaled init lets activation variance grow layer over layer. GPT-2/LLaMA-family models scale residual output projections by depth: N(0, 0.02 / &radic;(2L)).",
        "source": "GPT-2 (Radford et al. 2019) init convention; carried forward by LLaMA/Mistral."
      }
    },
    {
      "id": "R34",
      "title": "LM head width disagrees with embedding vocab",
      "severity": "info",
      "category": "structure",
      "engineId": null,
      "page": "https://neurarch.com/r/lm-head-width-disagrees-with-embedding-vocab",
      "provenance": null,
      "detail": {
        "trigger": "The model has an embedding with a numeric vocabSize and attention layers, but the final Linear feeding the Output projects to a different width.",
        "why": "A language model's head must project back to vocab size (and is usually weight-tied to the embedding); a mismatch means the model cannot emit token logits. A classifier head over N classes is the expected exception, which is why this is info, not error.",
        "source": "Weight-tying convention (Press & Wolf 2017); standard LM head contract."
      }
    },
    {
      "id": "R35",
      "title": "KV cache exceeds serving budget at reference context",
      "severity": "warn",
      "category": "performance",
      "engineId": null,
      "page": "https://neurarch.com/r/kv-cache-exceeds-serving-budget-at-reference-context",
      "provenance": null,
      "detail": {
        "trigger": "Total fp16 K/V cache across all attention layers (GQA-aware: counts numKVHeads, skips MLA) exceeds 4 GB for a single 8,192-token sequence.",
        "why": "That much cache for ONE sequence is gone before weights or activations load; on a 24 GB GPU it caps concurrency at a handful of requests. Raise the GQA ratio, switch to MLA, reduce depth/width, or accept a shorter serving context.",
        "source": "Same per-token KV math as R31 and the serving calculator, applied as an absolute budget instead of a pattern."
      }
    }
  ]
}
