Skip to content

sink-once

Source: fixes/sink-once/

Count attention sinks once per sequence in paged decode, and apply them in FusedSDPA prefill on v0.26.0.

Affects models with attention sinks (for example gpt-oss)
Releases v0.26.0 (decode and prefill), v0.30.0 (decode)
Patches vllm_gaudi/extension/ops.py
Upstream vllm-gaudi#1920 (open, decode part)
Test test_sink_once.py (needs a Gaudi device)

Problem

An attention sink is one extra logit per head that joins the softmax denominator: out = sum_j exp(s_j) v_j / (sum_j exp(s_j) + exp(sink)).

Decode (flat_pa and pipelined_pa) computes the softmax per KV block and merges the blocks of a sequence afterwards. pipelined_pa adds exp(sink - block_max) to every block's partial sum, so after the merge a sequence that spans n blocks has n copies of the sink in its denominator instead of one. At block size 128, a 10K-token context gets about 80 copies.

Prefill: FusedSDPA on Gaudi software 1.24.1 accepts a sink argument and does not apply it, so a prompt is attended as if the model had no sinks.

Symptom

Heads with a sink attend more weakly as the context grows. Short prompts look fine; long-context retrieval, tool-call JSON validity and instruction following degrade with context length. Prefill and decode also disagree with each other.

Who is affected

Models whose attention layers have sinks (gpt-oss and others that declare sinks). Models without sinks never reach the edited code. The Gaudi 3 fused block-softmax kernels are a different path and are not changed.

The fix

Decode: each block gets sink - log(n), with n the number of blocks of the block's sequence, so the n per-block terms add up to exactly one exp(sink) after the merge. Sequences that fit in one block are unchanged. Padding blocks map to no sequence, get n = 1 and are dropped by the merge as before. This is the change in the upstream pull request.

Prefill (v0.26.0 only): FusedSDPA gets one extra key whose logit is the sink. The head dimension grows by one (padded to a multiple of 64), the query carries sink / scale there, the extra key carries 1, and its value is zero. Real keys are untouched, so the result is exact up to the kernel's own precision, on the same kernel. Calls that pass window_size keep the stock path.

On v0.30.0 the FusedSDPA call sits inside a query-tiling helper. The prefill part has not been validated on that layout, so the fix applies only the decode part there.

Verify

python3 fixes/sink-once/apply.py --check   # 0 = applied, 1 = not applied, 2 = source drifted
pytest fixes/sink-once                     # on a Gaudi machine

The test applies the fix to a temporary copy of ops.py and compares stock and patched attention against an fp32 reference with a sink, for decode over 1 to 40 blocks and (on v0.26.0) prefill.