You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
LLM decode with a growing KV cache (the standard generate() pattern: feed past_key_values, read present.* as gpu-buffer) requests one same-sized buffer per KV output per step and releases the previous set — 64 buffers/step for a 32-layer model. 64 ≫ cap, so 64 − cap buffers are truly created (alloc + zero-fill) and destroyed on every step.
Step at S=6144 = the 12→16 MiB boundary; 12 MiB is not a power of two, so generic size-class effects predict nothing there.
Qwen3.5-4B (16 growing KV tensors, fits under caps): no comparable staircase, and the predicted negative step at 16→25 MiB (cap 10→15, miss 6→1) measured as −6.0 ms.
Zero-churn control (pre-allocated KV bound in-place via fetches, same graph): no staircase, ~3x faster at long context.
Why existing options don't cover this
storageBufferCacheMode: 'disabled': destroys released buffers immediately → use-after-destroy validation error with GPU-resident KV I/O binding.
Simply raising the caps doesn't work: the needed cap is model-dependent (40 layers → 80 tensors), and decode walks through every bucket over time, so cap=64 puts worst-case retained memory near ~4 GiB (vs ~815 MiB today). Alternatives:
Adapt per-bucket caps to the observed acquire rate (with decay), or
Cross-bucket eviction under a total-bytes budget — the bucket a growing KV has moved out of is pure dead weight today, or
Describe the issue
BUCKET_DEFAULT_LIMIT_TABLE(onnxruntime/core/providers/webgpu/buffer_manager.cc) caps retained buffers per size bucket:LLM decode with a growing KV cache (the standard
generate()pattern: feedpast_key_values, readpresent.*asgpu-buffer) requests one same-sized buffer per KV output per step and releases the previous set — 64 buffers/step for a 32-layer model. 64 ≫ cap, so64 − capbuffers are truly created (alloc + zero-fill) and destroyed on every step.Result: a permanent per-token latency staircase, steps exactly on bucket boundaries. Phi-4-mini q4f16 (64 KV tensors, KV tensor =
2048·Sbytes), onnxruntime-web 1.29, Intel iGPU:Decode drops ~22 → ~4 TPS over 8k context.
Evidence pinning this to the bucket table:
Why existing options don't cover this
storageBufferCacheMode: 'disabled': destroys released buffers immediately → use-after-destroy validation error with GPU-resident KV I/O binding.'simple': caches by exact size, never evicts → dynamic shapes miss every step and the pool grows unboundedly → device lost. (Fine for the static-shape case in [Web] Expose WebGPU EP buffer cache mode options in JS #29016 — different workload.)'bucket'(default): works, but fixed caps produce the above.Proposed direction
Simply raising the caps doesn't work: the needed cap is model-dependent (40 layers → 80 tensors), and decode walks through every bucket over time, so cap=64 puts worst-case retained memory near ~4 GiB (vs ~815 MiB today). Alternatives:
To reproduce
In Transformers.js/ort-web:
Urgency
No response
ONNX Runtime Installation
Built from Source
ONNX Runtime Version or Commit ID
1.29.0(commit id: 7ad118c)
Execution Provider
'webgpu' (WebGPU)