diff --git a/docs/ContribOperators.md b/docs/ContribOperators.md index f84d7593141f2..459e4cf71a98b 100644 --- a/docs/ContribOperators.md +++ b/docs/ContribOperators.md @@ -1789,6 +1789,14 @@ This version of the operator has been available since version 1 of the 'com.micr Auxiliary K/V use BNSH layout and are read-only. When `auxiliary_kv_shared=1`, auxiliary_value may be omitted and auxiliary_key is used as both K and V. DeepSeek-style post-attention inverse/conjugate RoPE and output projection remain the exporter's responsibility. + + Main caches may instead use INT8 with positive finite float32 key_scale and value_scale vectors, one scale per + KV-head channel. The cache is symmetric signed int8 without a zero point, like the 8-bit KV cache of GroupQueryAttention + and PagedAttention; tensor(uint8) is reserved for packed 4-bit caches in those operators and is not accepted here. + Newly appended keys (after optional normalization and RoPE) and values are divided by their scales, + rounded to nearest with ties to even, and saturated to [-128, 127]. Attention reads the quantized representation, + including newly appended rows. Auxiliary caches remain floating point. Invalid scale values produce validation_status=10 + when validation is requested; CUDA strict validation also reports them as an error. #### Version @@ -1825,7 +1833,7 @@ This version of the operator has been available since version 1 of the 'com.micr
Add a zero-valued sink logit when no explicit head_sink is supplied.
-#### Inputs (11 - 17) +#### Inputs (11 - 20)
query : T
@@ -1834,10 +1842,10 @@ This version of the operator has been available since version 1 of the 'com.micr
Current main key [batch, sequence, kv_num_heads * head_size].
value (optional) : T
Current main value [batch, sequence, kv_num_heads * head_size].
-
past_key (optional) : T
-
Main key cache in BNSH layout.
-
past_value (optional) : T
-
Main value cache in BNSH layout.
+
past_key (optional) : TCache
+
Main key cache in BNSH layout, same type as query or INT8.
+
past_value (optional) : TCache
+
Main value cache in BNSH layout, same type as past_key.
auxiliary_key (optional) : T
Read-only auxiliary key sequence in BNSH layout.
auxiliary_value (optional) : T
@@ -1862,17 +1870,25 @@ This version of the operator has been available since version 1 of the 'com.micr
Optional K RMSNorm weight [head_size].
head_sink (optional) : T
Optional sink logit [num_heads]. Participates in the shared softmax denominator in both attention modes.
+
committed_length (optional) : M
+
Optional per-request committed token count ([1] or [batch]) for cursor validation. When supplied, validation_status is 9 if seqlens_k, sequence_length, or cache_capacity is inconsistent, and CUDA strict validation fails with a cursor-specific error. CUDA only.
+
key_scale (optional) : S
+
Positive finite per-channel INT8 key-cache scales [kv_num_heads * head_size].
+
value_scale (optional) : S
+
Positive finite per-channel INT8 value-cache scales [kv_num_heads * head_size].
-#### Outputs (1 - 3) +#### Outputs (1 - 4)
output : T
Attention output [batch, sequence, num_heads * head_size].
-
present_key (optional) : T
+
present_key (optional) : TCache
Updated main key cache in BNSH layout.
-
present_value (optional) : T
+
present_value (optional) : TCache
Updated main value cache in BNSH layout.
+
validation_status (optional) : M
+
Optional CUDA-only int32 status: 0 when valid, otherwise a validation code (1-8: selection, sequence-length or rotary-position errors; 9: inconsistent committed_length cursor; 10: invalid INT8 cache scale).
#### Type Constraints @@ -1880,6 +1896,10 @@ This version of the operator has been available since version 1 of the 'com.micr
T : tensor(float), tensor(float16), tensor(bfloat16)
Constrain all floating-point inputs and outputs to one element type.
+
TCache : tensor(float), tensor(float16), tensor(bfloat16), tensor(int8)
+
Cache type must match T unless INT8 per-channel scales are supplied.
+
S : tensor(float)
+
INT8 cache scales use float32.
M : tensor(int32)
Constrain selection and sequence metadata to int32.
@@ -5280,8 +5300,8 @@ This version of the operator has been available since version 1 of the 'com.micr sum_h ReLU(q_h . k), the token_budget / compress_ratio highest scoring blocks are kept, and their token indices are emitted (request-local logical positions, i.e. the same numbering as past_sequence_lengths + local offset) followed by the causally visible tokens of the trailing - incomplete block. QSA positions are the request-local logical cache positions derived from - past_sequence_lengths and cumulative_sequence_lengths; position_ids must be omitted. + incomplete block. There is no position_ids input: QSA positions are the request-local logical + cache positions derived from past_sequence_lengths and cumulative_sequence_lengths. policy_mode = "csa" ("compressed sparse attention" block indexer) Applies the same window-plan arithmetic as SparseAttentionIndexer (overlap/leftover/new window @@ -5375,8 +5395,8 @@ This version of the operator has been available since version 1 of the 'com.micr
Only for policy_mode 'csa': per-head score weights with shape (total_tokens, num_heads).
position_ids (optional) : I
Only for policy_mode 'csa': absolute position of every packed query, with shape (total_tokens).
-
past_key_state : T
-
Generic fixed-capacity state: policy_mode 'qsa' stores prepared complete-block keys; policy_mode 'csa' stores compressed keys. Shape is (batch_size, state_capacity, head_size) and never changes across calls.
+
past_key_state : T_KEY
+
Generic fixed-capacity state: policy_mode 'qsa' stores prepared complete-block keys; policy_mode 'csa' stores compressed keys. Shape is (batch_size, state_capacity, head_size) and never changes across calls. CUDA QSA also supports FLOAT key state with BFLOAT16 query/key tensors. WebGPU requires key state and activations to have the same type.
past_kv_buffer : T
Generic fixed-capacity pending-token buffer. Shape is (batch_size, 2 * compress_ratio - 1, head_size) for policy_mode 'qsa' (which only ever uses up to compress_ratio - 1 of these entries) and (batch_size, 2 * compress_ratio - 1, 2 * head_size) for policy_mode 'csa'.
past_gate_buffer (optional) : T
@@ -5404,7 +5424,7 @@ This version of the operator has been available since version 1 of the 'com.micr
Selected entries with shape (total_tokens, capacity). capacity is token_budget + compress_ratio - 1 for policy_mode 'qsa' (request-local token positions into the main key/value cache) and index_topk for policy_mode 'csa' (compressed entry indices into key_state). Unused entries are -1.
selected_counts : M
Exact number of used (non -1) entries of selected_indices for every query, with shape (total_tokens).
-
present_key_state : T
+
present_key_state : T_KEY
Updated generic key state, with the same fixed shape as past_key_state.
present_kv_buffer : T
Updated generic pending-token buffer, with the same fixed shape as past_kv_buffer.
@@ -5425,6 +5445,8 @@ This version of the operator has been available since version 1 of the 'com.micr
T : tensor(float), tensor(float16), tensor(bfloat16)
Constrain floating point tensors to float, float16 and bfloat16.
+
T_KEY : tensor(float), tensor(float16), tensor(bfloat16)
+
Constrain the generic key-state tensor to float, float16 and bfloat16.
I : tensor(int64)
Constrain position ids to 64-bit integer tensors.
M : tensor(int32)
diff --git a/docs/OperatorKernels.md b/docs/OperatorKernels.md index 2497a7e6fd31a..cd0be24517121 100644 --- a/docs/OperatorKernels.md +++ b/docs/OperatorKernels.md @@ -1112,7 +1112,7 @@ The **OpSet Version** column uses the following notation: |DecoderMaskedSelfAttention|*in* input:**T**
*in* weights:**T**
*in* bias:**T**
*in* mask_index:**M**
*in* past:**T**
*in* attention_bias:**T**
*in* past_sequence_length:**M**
*in* beam_width:**M**
*in* cache_indirection:**M**
*out* output:**T**
*out* present:**T**|1+|**T** = tensor(float), tensor(float16)| |DequantizeLinear|*in* x:**T1**
*in* x_scale:**T2**
*in* x_zero_point:**T1**
*out* y:**T2**|1+|**T1** = tensor(int8), tensor(uint8)
**T2** = tensor(float16)| |DequantizeWithOrder|*in* input:**Q**
*in* scale_input:**S**
*out* output:**F**|1+|**F** = tensor(float), tensor(float16)
**Q** = tensor(int8)
**S** = tensor(float)| -|DynamicSparseAttention|*in* query:**T**
*in* key:**T**
*in* value:**T**
*in* past_key:**T**
*in* past_value:**T**
*in* auxiliary_key:**T**
*in* auxiliary_value:**T**
*in* selected_indices:**M**
*in* selected_counts:**M**
*in* seqlens_k:**M**
*in* total_sequence_length:**M**
*in* cos_cache:**T**
*in* sin_cache:**T**
*in* position_ids:**tensor(int64)**
*in* q_norm_weight:**T**
*in* k_norm_weight:**T**
*in* head_sink:**T**
*out* output:**T**
*out* present_key:**T**
*out* present_value:**T**|1+|**M** = tensor(int32)
**T** = tensor(bfloat16), tensor(float), tensor(float16)| +|DynamicSparseAttention|*in* query:**T**
*in* key:**T**
*in* value:**T**
*in* past_key:**TCache**
*in* past_value:**TCache**
*in* auxiliary_key:**T**
*in* auxiliary_value:**T**
*in* selected_indices:**M**
*in* selected_counts:**M**
*in* seqlens_k:**M**
*in* total_sequence_length:**M**
*in* cos_cache:**T**
*in* sin_cache:**T**
*in* position_ids:**tensor(int64)**
*in* q_norm_weight:**T**
*in* k_norm_weight:**T**
*in* head_sink:**T**
*in* committed_length:**M**
*in* key_scale:**S**
*in* value_scale:**S**
*out* output:**T**
*out* present_key:**TCache**
*out* present_value:**TCache**
*out* validation_status:**M**|1+|**M** = tensor(int32)
**S** = tensor(float)
**T** = tensor(bfloat16), tensor(float), tensor(float16)
**TCache** = tensor(bfloat16), tensor(float), tensor(float16), tensor(int8)| |DynamicTimeWarping|*in* input:**F**
*out* output:**I**|1+|**F** = tensor(float)
**I** = tensor(int32)| |EmbedLayerNormalization|*in* input_ids:**T1**
*in* segment_ids:**T1**
*in* word_embedding:**T**
*in* position_embedding:**T**
*in* segment_embedding:**T**
*in* gamma:**T**
*in* beta:**T**
*in* mask:**T1**
*in* position_ids:**T1**
*out* output:**T**
*out* mask_index:**T1**
*out* embedding_sum:**T**|1+|**T** = tensor(float), tensor(float16)| |EngramGate|*in* key:**T**
*in* query:**T**
*in* value:**T**
*in* key_norm_scale:**T**
*in* query_norm_scale:**T**
*in* conv_norm_scale:**T**
*out* output:**T**
*out* gated_value_normed:**T**|1+|**T** = tensor(bfloat16), tensor(float), tensor(float16)| @@ -1151,7 +1151,7 @@ The **OpSet Version** column uses the following notation: |NhwcConv|*in* X:**T**
*in* W:**T**
*in* B:**T**
*out* Y:**T**|1+|**T** = tensor(float), tensor(float16)| |PackedAttention|*in* input:**T**
*in* weights:**T**
*in* bias:**T**
*in* token_offset:**M**
*in* cumulative_sequence_length:**M**
*in* attention_bias:**T**
*out* output:**T**|1+|**T** = tensor(float), tensor(float16)| |PackedMultiHeadAttention|*in* query:**T**
*in* key:**T**
*in* value:**T**
*in* bias:**T**
*in* token_offset:**M**
*in* cumulative_sequence_length:**M**
*in* attention_bias:**T**
*out* output:**T**|1+|**T** = tensor(float), tensor(float16)| -|PackedSparseAttentionIndexer|*in* query:**T**
*in* key:**T**
*in* query_norm_weight:**T**
*in* key_norm_weight:**T**
*in* cos_cache:**T**
*in* sin_cache:**T**
*in* cumulative_sequence_lengths:**M**
*in* past_sequence_lengths:**M**
*in* gate:**T**
*in* position_bias:**T**
*in* head_weights:**T**
*in* position_ids:**I**
*in* past_key_state:**T**
*in* past_kv_buffer:**T**
*in* past_gate_buffer:**T**
*in* past_state_lengths:**M**
*in* state_update_capture_count:**M**
*in* state_update_active:**M**
*in* indexshare_mode:**M**
*in* merged_indices:**M**
*in* merged_counts:**M**
*in* merged_status:**M**
*out* selected_indices:**M**
*out* selected_counts:**M**
*out* present_key_state:**T**
*out* present_kv_buffer:**T**
*out* present_gate_buffer:**T**
*out* present_state_lengths:**M**
*out* state_update:**T**
*out* indexshare_status:**M**
*out* state_status:**M**|1+|**I** = tensor(int64)
**M** = tensor(int32)
**T** = tensor(bfloat16), tensor(float), tensor(float16)| +|PackedSparseAttentionIndexer|*in* query:**T**
*in* key:**T**
*in* query_norm_weight:**T**
*in* key_norm_weight:**T**
*in* cos_cache:**T**
*in* sin_cache:**T**
*in* cumulative_sequence_lengths:**M**
*in* past_sequence_lengths:**M**
*in* gate:**T**
*in* position_bias:**T**
*in* head_weights:**T**
*in* position_ids:**I**
*in* past_key_state:**T_KEY**
*in* past_kv_buffer:**T**
*in* past_gate_buffer:**T**
*in* past_state_lengths:**M**
*in* state_update_capture_count:**M**
*in* state_update_active:**M**
*in* indexshare_mode:**M**
*in* merged_indices:**M**
*in* merged_counts:**M**
*in* merged_status:**M**
*out* selected_indices:**M**
*out* selected_counts:**M**
*out* present_key_state:**T_KEY**
*out* present_kv_buffer:**T**
*out* present_gate_buffer:**T**
*out* present_state_lengths:**M**
*out* state_update:**T**
*out* indexshare_status:**M**
*out* state_status:**M**|1+|**I** = tensor(int64)
**M** = tensor(int32)
**T** = tensor(bfloat16), tensor(float), tensor(float16)
**T_KEY** = tensor(bfloat16), tensor(float), tensor(float16)| |PackedSparseAttentionIndexerMerge|*in* base_indices:**T**
*in* base_counts:**T**
*in* base_row_indices:**T**
*in* range_starts:**T**
*in* range_ends:**T**
*in* additional_indices:**T**
*in* additional_counts:**T**
*out* selected_indices:**T**
*out* selected_counts:**T**
*out* status:**T**|1+|**T** = tensor(int32)| |PagedAttention|*in* query:**T**
*in* key:**T**
*in* value:**T**
*in* key_cache:**T_CACHE**
*in* value_cache:**T_CACHE**
*in* cumulative_sequence_length:**S**
*in* past_seqlens:**S**
*in* block_table:**S**
*in* cos_cache:**T**
*in* sin_cache:**T**
*in* slot_mapping:**S**
*in* head_sink:**T**
*in* q_norm_weight:**T**
*in* k_norm_weight:**T**
*in* k_scale:**T_KV_SCALE**
*in* v_scale:**T_KV_SCALE**
*in* attention_metadata:**S**
*out* output:**T**
*out* key_cache_out:**T_CACHE**
*out* value_cache_out:**T_CACHE**|1+|**S** = tensor(int32)
**T** = tensor(bfloat16), tensor(float16)
**T_CACHE** = tensor(bfloat16), tensor(float16), tensor(float8e4m3fn), tensor(int8), tensor(uint8)
**T_KV_SCALE** = tensor(float)| |QAttention|*in* input:**T1**
*in* weight:**T2**
*in* bias:**T3**
*in* input_scale:**T3**
*in* weight_scale:**T3**
*in* mask_index:**T4**
*in* input_zero_point:**T1**
*in* weight_zero_point:**T2**
*in* past:**T3**
*out* output:**T3**
*out* present:**T3**|1+|**T1** = tensor(int8)
**T2** = tensor(int8)
**T3** = tensor(float), tensor(float16)
**T4** = tensor(int32)| diff --git a/docs/contrib_ops/cuda/dynamic_sparse_attention.md b/docs/contrib_ops/cuda/dynamic_sparse_attention.md new file mode 100644 index 0000000000000..86ca09455b0a3 --- /dev/null +++ b/docs/contrib_ops/cuda/dynamic_sparse_attention.md @@ -0,0 +1,100 @@ +# CUDA DynamicSparseAttention grouped decode + +`UseGroupedDecodeAttention` enables the split-K grouped decode attention kernel +for decode and multi-row speculative verification (`1 <= sequence_length <= 8`) +across both floating-point (`FP16`/`BF16`) and quantized (`INT8`) KV caches, +with `max_splits = 32` for quantized KV caches. + +The INT8 grouped-decode and vector-staging reference tests cover one through +four query rows (`sequence_length in {1, 2, 3, 4}`). + +## INT8 KV cache encoding + +The INT8 main cache is `tensor(int8)`: symmetric signed storage with no zero point, +`q = clamp(rint(x / scale), -128, 127)` and `x = q * scale`, with one float32 scale per +`kv_head * head_size` channel (`key_scale`, `value_scale`). This is the same encoding as the 8-bit +KV cache of `GroupQueryAttention` (`group_query_attention_qdq.cuh`) and `PagedAttention` / +`SparsePagedAttention` (`paged_attention_impl.cu`), which also use `int8`. `uint8` is used by those +operators only for packed 4-bit caches (`kv_cache_bit_width=4`), so `DynamicSparseAttention` does not +accept `tensor(uint8)`. + +## Validation status codes + +`validation_status` (and the CUDA strict-validation host check enabled with +`ORT_DYNAMIC_SPARSE_ATTENTION_STRICT_VALIDATION=1`) uses these codes; the first error found wins, +except that a cursor error (9) is replaced by any selection/sequence/position/scale error. + +| Code | Enumerator | Meaning | +|---|---|---| +| 0 | `kDynamicSparseAttentionValidationOk` | Valid. | +| 1 | `kDynamicSparseAttentionInvalidCount` | `selected_counts` outside `[0, max_selected]`. | +| 2 | `kDynamicSparseAttentionInvalidPadding` | Entries after `selected_counts` are not `-1`. | +| 3 | `kDynamicSparseAttentionNegativeIndex` | Active selected index is negative. | +| 4 | `kDynamicSparseAttentionIndexOutOfBounds` | Selected index outside the source sequence. | +| 5 | `kDynamicSparseAttentionDuplicateIndex` | Duplicate active selected index. | +| 6 | `kDynamicSparseAttentionNonCausalIndex` | Main selected index refers to a future key. | +| 7 | `kDynamicSparseAttentionInvalidSequenceLength` | `seqlens_k` incompatible with the sequence and cache capacity. | +| 8 | `kDynamicSparseAttentionInvalidPosition` | Rotary position outside the rotary cache. | +| 9 | `kDynamicSparseAttentionInvalidCommittedLength` | `committed_length` disagrees with `seqlens_k`, `sequence_length` or `cache_capacity`. | +| 10 | `kDynamicSparseAttentionInvalidCacheScale` | INT8 scale is not positive and finite. | + +With strict validation enabled every non-zero code, including 9, fails the kernel with an +`INVALID_ARGUMENT` status even when `validation_status` is not connected. Without strict validation +the code is only reported through `validation_status`. + +## CUDA dispatch switches + +`ORT_DSA_INT8_VECTOR_STAGING` and `ORT_DSA_SHARED_SCALE_MERGE` are read once per process (cached in +function-local statics) when the grouped decode path is first launched, so set them before the first +operator invocation. Only `0` and `1` are accepted. Neither changes schemas or cache layouts. +`ORT_DSA_PREFILL_KERNEL` (default `1`) is read on every BF16 call. + +Grouped decode is used when `1 <= sequence_length <= 8`, `num_heads / kv_num_heads >= 4` (and +divisible), `head_size == 256`, 16-bit `T` (FP16/BF16), more than 256 candidates +(`max_selected` plus the local window for `local_plus_selected`) and the grouped shared-memory +footprint fits the per-block limit. The kernel uses 12 query-head warps when the ratio is exactly 12 +and 8 otherwise. + +| Variable | Default | Applies to | Value | Kernels selected | +|---|---|---|---|---| +| `ORT_DSA_INT8_VECTOR_STAGING` | `1` | Grouped decode with an INT8 main cache | `1` | `GroupedDecodeDynamicSparseAttentionKernel<..., kQuantizedCache=true, kVectorizedStaging=true>`: 8-byte `uint2` cache loads with `float4` scale loads. | +| | | | `0` | Same kernel with `kVectorizedStaging=false`: scalar per-channel dequantization into shared memory. | +| `ORT_DSA_SHARED_SCALE_MERGE` | `1` | Grouped decode (INT8 and floating-point caches) when `split_count <= 32` (always for INT8, whose `max_splits` is 32; for floating-point caches `split_count = min(ceil(candidates / 64), ceil(128 / grouped_blocks))`) | `1` | `MergeGroupedDecodeDynamicSparseAttentionKernel`: one `expf` per split shared across channels; a split is active when its partial sum is non-zero. | +| | | | `0` | `MergeGroupedDecodeDynamicSparseAttentionKernel`: per-channel `expf` over all splits. | +| `ORT_DSA_PREFILL_KERNEL` | `1` | BF16, not grouped decode, `sequence_length >= 8`, `head_size == 256`, `selected_only` from the main cache, `smooth_softmax = 0`, SM80+ with enough opt-in shared memory | `1` | `DynamicSparseAttentionPrefillKernel` tensor-core prefill. | +| | | | `0` | Generic fused/split or grouped prefill kernels. | + +When `split_count > 32` (floating-point caches only) the generic +`MergeDynamicSparseAttentionKernel` is used regardless of `ORT_DSA_SHARED_SCALE_MERGE`. The tests +`Int8CacheGroupedDecodeAlternateSwitchesInFreshProcess_CUDA` re-run the INT8 grouped-decode and merge +cases in fresh processes with each non-default combination. + +### Buffer alignment + +The wide-load fast paths assume more alignment than a tensor's element type guarantees, and tensors +bound through IOBinding can start at any element offset of a larger allocation. The launcher therefore +checks the caller-provided buffers and uses an equivalent narrower path, with identical results, when a +buffer is misaligned: + +| Fast path | Buffers that must be aligned | Fallback | +|---|---|---| +| INT8 grouped decode vector staging (`ORT_DSA_INT8_VECTOR_STAGING=1`) | `key_scale`, `value_scale`, `auxiliary_key`, `auxiliary_value`: 16 bytes; `present_key`, `present_value`: 8 bytes | Scalar per-channel staging (`kVectorizedStaging=false`). | +| Floating-point grouped decode and grouped prefill | Each staged K/V row: 16 bytes (checked per row in the kernel) | Element-wise copy into shared memory. | +| Tensor-core BF16 prefill (`ORT_DSA_PREFILL_KERNEL=1`) | `present_key`, `present_value`: 16 bytes (8 for INT8); `key_scale`, `value_scale`: 16 bytes; `output`: 4 bytes | Generic fused/split or grouped prefill kernels. | + +Buffers allocated by ONNX Runtime are 256-byte aligned, so only IOBinding with offset buffers takes a +fallback. The `*MisalignedBuffers_CUDA` tests cover each case. + +### Benchmark + +rc3 model (INT4), H200, clean machine, 5 repetitions, baseline noise about +-0.03 ms, DFlash2 S=8 +decode ms/cycle at context 2K/8K/32K. Both defaults are the faster setting, so the switches remain +only as fallbacks: + +| Setting | Decode vs default | +|---|---| +| `ORT_DSA_INT8_VECTOR_STAGING=0` | about +0.3 ms (+2%) slower | +| `ORT_DSA_SHARED_SCALE_MERGE=0` | about +0.05 to +0.1 ms (+0.3 to 0.5%) slower | + +The corresponding indexer switch results are in +[../packed_sparse_attention_indexer.md](../packed_sparse_attention_indexer.md#benchmark). diff --git a/docs/contrib_ops/packed_sparse_attention_indexer.md b/docs/contrib_ops/packed_sparse_attention_indexer.md index 658930a42d55e..6c47de3386eb9 100644 --- a/docs/contrib_ops/packed_sparse_attention_indexer.md +++ b/docs/contrib_ops/packed_sparse_attention_indexer.md @@ -1,5 +1,44 @@ # PackedSparseAttentionIndexer โ€” Operator Documentation +## CUDA QSA dispatch switches + +Two process-level environment variables select alternate CUDA QSA kernels. Each value is read once +per process on the first eligible call and cached, so set them before the first operator invocation. +None changes schemas, cache layouts, causal visibility, stable tie ordering, selected counts or +state-update semantics. The companion `DynamicSparseAttention` switches are documented in +[cuda/dynamic_sparse_attention.md](cuda/dynamic_sparse_attention.md). + +Hierarchical QSA TopK is eligible when `1 <= total_tokens <= 64`, `state_capacity >= 2048`, +`head_size == 128`, `compress_ratio == 4`, `num_heads == 4` and `32 < block_topk <= 512`. Ragged QSA +prefill is eligible when `total_tokens > 64`, `head_size == 128`, `compress_ratio == 4` and +`num_heads == 4`. Other geometries never read these switches. + +| Variable | Default | Applies to | Value | Kernels selected | +|---|---|---|---|---| +| `ORT_PACKED_SPARSE_INDEXER_COMPACT_TOPK` | `1` (only `0`/`1` accepted) | hierarchical QSA TopK | `1` | Compacts each 2048-key chunk to 512 keys (`QsaCompactTileTopKKernel`). Up to five chunks use the fused `QsaFinalMergeAndEmitKernel`; more chunks use pairwise `QsaMergeTileTopKKernel` followed by `QsaEmitHierarchicalTopKKernel`. | +| | | | `0` | Tile-level merge: `QsaMergeTwoLevelsTopKKernel` (4-way) while two levels remain, then `QsaMergeTileTopKKernel`, then `QsaEmitHierarchicalTopKKernel`. | +| `ORT_PACKED_SPARSE_INDEXER_WARP_HEADS` | `1` (boolean) | ragged QSA prefill | `1` | `QsaRaggedPrefillScoreKernel<..., true>`: one warp per state block keeps all four heads (256 threads per CTA). | +| | | | `0` | `QsaRaggedPrefillScoreKernel<...>` with one warp per (state block, head) and CTA-wide barriers (1024 threads per CTA). | + +The packed-QSA tests re-run the hierarchical, stable-tie, partial-merge-group +and ragged-prefill cases in fresh processes for every non-default mode +(`COMPACT_TOPK=0`, `WARP_HEADS=0`; `QsaAlternateTopKModesInFreshProcess`). + +### Benchmark + +rc3 model (INT4), H200, clean machine, 5 repetitions, baseline noise about +-0.03 ms. DFlash2 S=8 +decode is ms/cycle at context 2K/8K/32K; TTFT is at 2K/8K/32K prompt. Defaults are fastest, so +both switches default to the fast path and remain as fallbacks: + +| Setting | Result vs default | +|---|---| +| `ORT_PACKED_SPARSE_INDEXER_COMPACT_TOPK=0` | decode about +0.2 to +0.27 ms (+1.5%) slower | +| `ORT_PACKED_SPARSE_INDEXER_WARP_HEADS=0` | TTFT +3.7% at 8K, +14% at 32K | + +A live-key radix selection alternative was benchmarked (default 14.93/15.07/15.30 ms versus +15.08/15.22/15.64 ms with it, i.e. +0.15 to +0.34 ms slower) and removed. Qualify accuracy and +performance on the target runtime, hardware and workload before changing a default. + This document describes the `com.microsoft::PackedSparseAttentionIndexer` contrib operator: the packed/variable-length counterpart of `com.microsoft::SparseAttentionIndexer`, built for continuous-batching (paged) inference engines such as an OgaEngine-style `PagedAttention` model. @@ -71,8 +110,8 @@ later slot keeps its fixed index. | 8 | `gate` | `(total_tokens, 2*head_size)` | T | csa only | | 9 | `position_bias` | `(compress_ratio, 2*head_size)` | T | csa only | | 10 | `head_weights` | `(total_tokens, num_heads)` | T | csa only | -| 11 | `position_ids` | `(total_tokens)` | int64 | omitted qsa / required csa | -| 12 | `past_key_state` | `(batch_size, state_capacity, head_size)` | T | both (generic) | +| 11 | `position_ids` | `(total_tokens)` | int64 | csa only | +| 12 | `past_key_state` | `(batch_size, state_capacity, head_size)` | T_KEY | both (generic) | | 13 | `past_kv_buffer` | `(batch_size, 2*compress_ratio-1, width)` | T | both (generic) | | 14 | `past_gate_buffer` | same shape as `past_kv_buffer` | T | csa only | | 15 | `past_state_lengths` | `(batch_size, 2)` | int32, device-resident | both (generic) | @@ -86,13 +125,17 @@ empty output name for `qsa`; `state_update` is declared only when QSA capture is |---|---|---|---|---| | 0 | `selected_indices` | `(total_tokens, capacity)` | int32 | both | | 1 | `selected_counts` | `(total_tokens)` | int32 | both | -| 2 | `present_key_state` | same shape as `past_key_state` | T | both | +| 2 | `present_key_state` | same shape as `past_key_state` | T_KEY | both | | 3 | `present_kv_buffer` | same shape as `past_kv_buffer` | T | both | | 4 | `present_gate_buffer` | same shape as `past_gate_buffer` | T | csa only | | 5 | `present_state_lengths` | same shape as `past_state_lengths` | int32 | both | | 6 | `state_update` | `(batch_size, state_update_capacity, head_size)` | T | qsa capture only | +| 7 | `index_share_status` | `(total_tokens)` | int32 | index sharing | | 8 | `state_status` | `(batch_size)` | int32 | optional, CUDA only | +CUDA supports `T_KEY = T` and, for QSA only, BF16 activations with FP32 key state. +The pending buffers and captured state updates remain in the activation type. + `capacity` is `token_budget + compress_ratio - 1` for `qsa` and `index_topk` for `csa`, exactly the same formula (`SelectedCapacity`) used by `SparseAttentionIndexer`. @@ -208,9 +251,7 @@ dense `qsa` policy but against fixed-capacity state instead of a growing cache: `past_sequence_lengths[b] + local_offset` โ€” i.e. the request's own absolute token position, which is exactly what a per-request main paged KV cache is addressed by. The output is therefore directly consumable by `SparsePagedAttention` configured with `attention_mode="selected_only"`, -`selected_kv_source="main"`. `position_ids` must be omitted for `qsa`; allowing an unrelated -absolute-position space would make these cache indices ambiguous and would rotate queries -inconsistently with prepared block keys. +`selected_kv_source="main"`. ## 6. Policy `csa` @@ -228,7 +269,7 @@ mapping are packed: rejects (caps) new windows beyond `state_capacity` as described in [ยง3](#3-generic-state-shared-by-both-policies). 2. **Score** (one launch per query token, per compressed entry): every entry is scored with `sum_h w_h * ReLU(q_h . k)` and masked by the causal threshold from `position_ids` (required for - `csa`; `qsa` always derives request-local logical positions from `past_sequence_lengths`). + `csa`; must be omitted for `qsa`). 3. **Select** (one launch per query token): keeps the `index_topk` highest scoring, causally visible entries and writes the exact active count to `selected_counts`. @@ -293,6 +334,9 @@ mechanics with no packed equivalent): the shared-memory TopK path for up to 32 selected blocks, bounded partial TopK for 33โ€“512 blocks, and a hierarchical TopK path for common long-context Qwen shapes. Values above 512 that do not match the hierarchical specialization retain the correctness-first repeated-scan fallback. +- The CUDA dispatch switches `ORT_PACKED_SPARSE_INDEXER_COMPACT_TOPK` + and `ORT_PACKED_SPARSE_INDEXER_WARP_HEADS` are described in + the "CUDA QSA dispatch switches" section at the top of this document. - OgaEngine / Model Builder integration (declaring `past_key_state` etc. as Engine-managed, per-request fixed-size state, analogous to a paged auxiliary cache) is out of scope for this operator definition and is expected in a follow-up to `microsoft/onnxruntime-genai`. diff --git a/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention.cc b/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention.cc index 878659302a19a..d3e020eb16e2e 100644 --- a/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention.cc +++ b/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention.cc @@ -19,19 +19,21 @@ namespace cuda { using namespace onnxruntime::cuda; -#define REGISTER_KERNEL_TYPED(T) \ - ONNX_OPERATOR_TYPED_KERNEL_EX( \ - DynamicSparseAttention, \ - kMSDomain, \ - 1, \ - T, \ - kCudaExecutionProvider, \ - (*KernelDefBuilder::Create()) \ - .TypeConstraint("T", DataTypeImpl::GetTensorType()) \ - .TypeConstraint("M", DataTypeImpl::GetTensorType()) \ - .MayInplace(3, 1) \ - .MayInplace(4, 2) \ - .InputMemoryType(OrtMemTypeCPUInput, 10), \ +#define REGISTER_KERNEL_TYPED(T) \ + ONNX_OPERATOR_TYPED_KERNEL_EX( \ + DynamicSparseAttention, \ + kMSDomain, \ + 1, \ + T, \ + kCudaExecutionProvider, \ + (*KernelDefBuilder::Create()) \ + .TypeConstraint("T", DataTypeImpl::GetTensorType()) \ + .TypeConstraint("TCache", BuildKernelDefConstraints()) \ + .TypeConstraint("S", DataTypeImpl::GetTensorType()) \ + .TypeConstraint("M", DataTypeImpl::GetTensorType()) \ + .MayInplace(3, 1) \ + .MayInplace(4, 2) \ + .InputMemoryType(OrtMemTypeCPUInput, 10), \ DynamicSparseAttention); REGISTER_KERNEL_TYPED(float) @@ -129,6 +131,19 @@ Status DynamicSparseAttention::ComputeInternal(OpKernelContext* context) cons const Tensor* q_norm_weight = context->Input(14); const Tensor* k_norm_weight = context->Input(15); const Tensor* head_sink = context->Input(16); + const Tensor* committed_length = + context->InputCount() > 17 ? context->Input(17) : nullptr; + const Tensor* key_scale = context->InputCount() > 18 ? context->Input(18) : nullptr; + const Tensor* value_scale = context->InputCount() > 19 ? context->Input(19) : nullptr; + const bool quantized_cache = past_key != nullptr && past_key->IsDataType(); + ORT_RETURN_IF_NOT((key_scale != nullptr) == (value_scale != nullptr), + "DynamicSparseAttention: key_scale and value_scale must be provided together."); + ORT_RETURN_IF_NOT(quantized_cache == (key_scale != nullptr), + "DynamicSparseAttention: INT8 caches require per-channel key_scale and value_scale."); + ORT_RETURN_IF_NOT((past_key == nullptr || past_key->IsDataType() || quantized_cache) && + (past_value == nullptr || (quantized_cache ? past_value->IsDataType() + : past_value->IsDataType())), + "DynamicSparseAttention: cache types must match activations or both be INT8."); DynamicSparseAttentionParameters parameters; ORT_RETURN_IF_ERROR(dynamic_sparse_attention_helper::CheckInputs( @@ -140,9 +155,19 @@ Status DynamicSparseAttention::ComputeInternal(OpKernelContext* context) cons qk_norm_epsilon_, parameters)); parameters.rotary_interleaved = rotary_interleaved_; parameters.use_smooth_softmax = use_smooth_softmax_ || head_sink != nullptr; + if (quantized_cache) { + ORT_RETURN_IF_NOT(past_value != nullptr && + key_scale->Shape() == TensorShape({parameters.kv_num_heads * parameters.head_size}) && + value_scale->Shape() == key_scale->Shape(), + "DynamicSparseAttention: INT8 cache scales must have shape [kv_num_heads * head_size]."); + } ORT_RETURN_IF_NOT(parameters.head_size <= GetDeviceProp().maxThreadsPerBlock, "DynamicSparseAttention: head_size exceeds the CUDA device thread-block limit."); + ORT_RETURN_IF(committed_length != nullptr && + committed_length->Shape().Size() != 1 && + committed_length->Shape().Size() != parameters.batch_size, + "DynamicSparseAttention: committed_length must have 1 or batch_size int32 elements."); TensorShape output_shape({parameters.batch_size, parameters.sequence_length, parameters.query_hidden_size}); @@ -153,6 +178,8 @@ Status DynamicSparseAttention::ComputeInternal(OpKernelContext* context) cons parameters.cache_capacity, parameters.head_size}); Tensor* present_key_output = context->OutputCount() > 1 ? context->Output(1, present_shape) : nullptr; Tensor* present_value_output = context->OutputCount() > 2 ? context->Output(2, present_shape) : nullptr; + Tensor* validation_status_output = + context->OutputCount() > 3 ? context->Output(3, TensorShape({1})) : nullptr; const size_t query_elements = SafeInt(parameters.batch_size) * parameters.sequence_length * parameters.query_hidden_size; auto prepared_query = GetScratchBuffer(query_elements, GetComputeStream(context)); @@ -160,16 +187,17 @@ Status DynamicSparseAttention::ComputeInternal(OpKernelContext* context) cons const size_t cache_elements = SafeInt(parameters.batch_size) * parameters.kv_num_heads * parameters.cache_capacity * parameters.head_size; - IAllocatorUniquePtr key_cache_scratch; - IAllocatorUniquePtr value_cache_scratch; - T* key_cache = present_key_output == nullptr ? nullptr : present_key_output->MutableData(); - T* value_cache = present_value_output == nullptr ? nullptr : present_value_output->MutableData(); + const size_t cache_bytes = SafeInt(cache_elements) * (quantized_cache ? 1 : sizeof(T)); + IAllocatorUniquePtr key_cache_scratch; + IAllocatorUniquePtr value_cache_scratch; + void* key_cache = present_key_output == nullptr ? nullptr : present_key_output->MutableDataRaw(); + void* value_cache = present_value_output == nullptr ? nullptr : present_value_output->MutableDataRaw(); if (key_cache == nullptr) { - key_cache_scratch = GetScratchBuffer(cache_elements, GetComputeStream(context)); + key_cache_scratch = GetScratchBuffer(cache_bytes, GetComputeStream(context)); key_cache = key_cache_scratch.get(); } if (value_cache == nullptr) { - value_cache_scratch = GetScratchBuffer(cache_elements, GetComputeStream(context)); + value_cache_scratch = GetScratchBuffer(cache_bytes, GetComputeStream(context)); value_cache = value_cache_scratch.get(); } @@ -178,8 +206,11 @@ Status DynamicSparseAttention::ComputeInternal(OpKernelContext* context) cons data.query = reinterpret_cast(query->Data()); data.key = key == nullptr ? nullptr : reinterpret_cast(key->Data()); data.value = value == nullptr ? nullptr : reinterpret_cast(value->Data()); - data.past_key = past_key == nullptr ? nullptr : reinterpret_cast(past_key->Data()); - data.past_value = past_value == nullptr ? nullptr : reinterpret_cast(past_value->Data()); + data.past_key = past_key == nullptr ? nullptr : reinterpret_cast(past_key->DataRaw()); + data.past_value = past_value == nullptr ? nullptr : reinterpret_cast(past_value->DataRaw()); + data.quantized_cache = quantized_cache; + data.key_scale = key_scale == nullptr ? nullptr : key_scale->Data(); + data.value_scale = value_scale == nullptr ? nullptr : value_scale->Data(); data.auxiliary_key = auxiliary_key == nullptr ? nullptr : reinterpret_cast(auxiliary_key->Data()); data.auxiliary_value = auxiliary_value == nullptr @@ -207,22 +238,38 @@ Status DynamicSparseAttention::ComputeInternal(OpKernelContext* context) cons CUDA_RETURN_IF_ERROR(cudaStreamIsCapturing(stream, &capture_status)); ORT_RETURN_IF_NOT(capture_status == cudaStreamCaptureStatusNone, "DynamicSparseAttention strict validation is not supported during CUDA graph capture."); + } - auto validation_error = GetScratchBuffer(1, GetComputeStream(context)); + IAllocatorUniquePtr validation_error_scratch; + int32_t* validation_error = nullptr; + if (validation_status_output != nullptr) { + validation_error = validation_status_output->MutableData(); + } else if (strict_validation_) { + validation_error_scratch = GetScratchBuffer(1, GetComputeStream(context)); + validation_error = validation_error_scratch.get(); + } + if (validation_error != nullptr) { const size_t validation_workspace_elements = GetDynamicSparseAttentionValidationWorkspaceSize( parameters, GetDeviceProp().sharedMemPerBlock); auto validation_workspace = GetScratchBuffer(validation_workspace_elements, GetComputeStream(context)); + const int committed_length_stride = + (committed_length != nullptr && committed_length->Shape().Size() == parameters.batch_size && + parameters.batch_size > 1) + ? 1 + : 0; ORT_RETURN_IF_ERROR(ValidateDynamicSparseAttentionOnDevice( stream, data.selected_indices, data.selected_counts, data.seqlens_k, - data.position_ids, parameters, validation_error.get(), validation_workspace.get(), - GetDeviceProp().sharedMemPerBlock, true)); + data.position_ids, parameters, validation_error, validation_workspace.get(), + GetDeviceProp().sharedMemPerBlock, strict_validation_, data.key_scale, data.value_scale, + committed_length == nullptr ? nullptr : committed_length->Data(), + committed_length_stride)); } const size_t attention_workspace_elements = GetDynamicSparseAttentionWorkspaceSize( - parameters, sizeof(CudaT), GetDeviceProp().sharedMemPerBlock); + parameters, sizeof(CudaT), GetDeviceProp().sharedMemPerBlock, quantized_cache); auto attention_workspace = GetScratchBuffer(attention_workspace_elements, GetComputeStream(context)); data.attention_workspace = attention_workspace.get(); diff --git a/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_impl.cu b/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_impl.cu index 25da163a21f97..a612c7071aab9 100644 --- a/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_impl.cu +++ b/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_impl.cu @@ -6,9 +6,11 @@ #include #include #include +#include #include #include +#include #include #include @@ -16,6 +18,8 @@ #include "core/common/safeint.h" #include "core/providers/cuda/cuda_common.h" +// Include env_var_utils.h after cuda_common.h: the latter transitively pulls in provider_api.h, +// which defines SHARED_PROVIDER and suppresses env_var_utils.h's own logging.h include. #include "core/platform/env_var_utils.h" namespace onnxruntime { @@ -28,6 +32,13 @@ constexpr int kSplitCandidateSize = 256; constexpr int kTargetSplitBlocks = 1024; constexpr size_t kMaxFusedSharedBytes = 16 * 1024; constexpr size_t kMaxValidationSharedBytes = 32 * 1024; +constexpr int kGroupedPrefillCandidatesPerTile = 8; +constexpr int kGroupedPrefillQueryHeadsPerBlock = 6; +constexpr int kGroupedDecodeQueryHeadsPerBlock = 8; +constexpr int kGroupedDecodeCandidatesPerSplit = 64; +constexpr int kTargetGroupedDecodeBlocks = 128; +constexpr int kGroupedQuantizedDecodeCandidatesPerSplit = 32; +constexpr int kTargetGroupedQuantizedDecodeBlocks = 512; template __device__ __forceinline__ float ToFloat(T value) { @@ -39,9 +50,54 @@ __device__ __forceinline__ T FromFloat(float value) { return static_cast(value); } +__device__ __forceinline__ int8_t QuantizeCacheValue(float value, float scale) { + return static_cast(__float2int_rn(fminf(127.0f, fmaxf(-128.0f, value / scale)))); +} + +__device__ __forceinline__ uint32_t PackMmaHalf2(half low, half high) { + return static_cast(__half_as_ushort(low)) | + (static_cast(__half_as_ushort(high)) << 16); +} + +__device__ __forceinline__ void MmaM16N8K16(float (&output)[4], + const uint32_t (&query_fragment)[4], + const uint32_t (&key_fragment)[2]) { +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800 + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32 " + "{%0,%1,%2,%3}, {%4,%5,%6,%7}, {%8,%9}, {%0,%1,%2,%3};\n" + : "+f"(output[0]), "+f"(output[1]), "+f"(output[2]), "+f"(output[3]) + : "r"(query_fragment[0]), "r"(query_fragment[1]), + "r"(query_fragment[2]), "r"(query_fragment[3]), + "r"(key_fragment[0]), "r"(key_fragment[1])); +#else + ORT_UNUSED_PARAMETER(output); + ORT_UNUSED_PARAMETER(query_fragment); + ORT_UNUSED_PARAMETER(key_fragment); +#endif +} + +// Copies one head_size == 256 K/V row into shared memory with one 16-byte chunk per lane. Tensors bound +// through IOBinding (cache outputs, auxiliary inputs) can start at any element offset, so a source that is +// not 16-byte aligned is copied element-wise instead of faulting with cudaErrorMisalignedAddress. +template +__device__ __forceinline__ void StageHeadRow(T* shared_row, const T* global_row, int lane, int head_size) { + if ((reinterpret_cast(global_row) & 15) == 0) { + reinterpret_cast(shared_row)[lane] = reinterpret_cast(global_row)[lane]; + } else { + for (int d = lane; d < head_size; d += 32) { + shared_row[d] = global_row[d]; + } + } +} + __device__ __forceinline__ void SetValidationError(int32_t* error_flag, DynamicSparseAttentionValidationError error) { - atomicCAS(error_flag, kDynamicSparseAttentionValidationOk, static_cast(error)); + const int32_t previous = + atomicCAS(error_flag, kDynamicSparseAttentionValidationOk, static_cast(error)); + if (previous == kDynamicSparseAttentionInvalidCommittedLength) { + atomicCAS(error_flag, kDynamicSparseAttentionInvalidCommittedLength, static_cast(error)); + } } __global__ void ValidateInputsKernel(const int32_t* selected_indices, @@ -57,21 +113,34 @@ __global__ void ValidateInputsKernel(const int32_t* selected_indices, int rotary_max_position, bool do_rotary, bool use_auxiliary, - uint32_t* validation_bitmap, - size_t validation_bitmap_words, - bool use_shared_bitmap, - int32_t* error_flag) { + uint32_t* validation_hash_table, + size_t validation_hash_entries, + bool use_shared_hash_table, + int32_t* error_flag, + const float* key_scale, + const float* value_scale, + int cache_channels, + const int32_t* committed_length, + int committed_length_stride) { const int row = static_cast(blockIdx.x); const int b = row / sequence_length; const int s = row - b * sequence_length; if (b >= batch_size) { return; } + if (row == 0 && key_scale != nullptr) { + for (int i = threadIdx.x; i < cache_channels; i += blockDim.x) { + if (!(key_scale[i] > 0.0f) || !isfinite(key_scale[i]) || + !(value_scale[i] > 0.0f) || !isfinite(value_scale[i])) { + SetValidationError(error_flag, kDynamicSparseAttentionInvalidCacheScale); + } + } + } - extern __shared__ uint32_t shared_bitmap[]; - if (use_shared_bitmap) { - for (size_t i = threadIdx.x; i < validation_bitmap_words; i += blockDim.x) { - shared_bitmap[i] = 0; + extern __shared__ uint32_t shared_hash_table[]; + if (use_shared_hash_table) { + for (size_t i = threadIdx.x; i < validation_hash_entries; i += blockDim.x) { + shared_hash_table[i] = std::numeric_limits::max(); } } __syncthreads(); @@ -120,10 +189,10 @@ __global__ void ValidateInputsKernel(const int32_t* selected_indices, const int32_t* row_indices = max_selected == 0 ? selected_indices : selected_indices + static_cast(row) * max_selected; - uint32_t* row_bitmap = - use_shared_bitmap - ? shared_bitmap - : validation_bitmap + static_cast(row) * validation_bitmap_words; + uint32_t* row_hash_table = + use_shared_hash_table + ? shared_hash_table + : validation_hash_table + static_cast(row) * validation_hash_entries; for (int i = static_cast(threadIdx.x); i < max_selected; i += static_cast(blockDim.x)) { const int index = row_indices[i]; DynamicSparseAttentionValidationError error = kDynamicSparseAttentionValidationOk; @@ -138,10 +207,19 @@ __global__ void ValidateInputsKernel(const int32_t* selected_indices, } else if (!use_auxiliary && index > query_position) { error = kDynamicSparseAttentionNonCausalIndex; } else { - const uint32_t mask = 1U << (index & 31); - const uint32_t previous = atomicOr(row_bitmap + (index >> 5), mask); - if ((previous & mask) != 0) { - error = kDynamicSparseAttentionDuplicateIndex; + const uint32_t hash_mask = static_cast(validation_hash_entries - 1); + uint32_t slot = static_cast(index) * 0x9E3779B9U & hash_mask; + for (size_t probe = 0; probe < validation_hash_entries; ++probe) { + const uint32_t previous = atomicCAS( + row_hash_table + slot, std::numeric_limits::max(), static_cast(index)); + if (previous == std::numeric_limits::max()) { + break; + } + if (previous == static_cast(index)) { + error = kDynamicSparseAttentionDuplicateIndex; + break; + } + slot = (slot + 1) & hash_mask; } } if (error != kDynamicSparseAttentionValidationOk) { @@ -169,6 +247,15 @@ __global__ void ValidateInputsKernel(const int32_t* selected_indices, } return; } + + if (s == 0 && threadIdx.x == 0 && committed_length != nullptr) { + const int64_t committed = committed_length[b * committed_length_stride]; + const int64_t total = committed + sequence_length; + if (committed < 0 || total > main_capacity || seqlens_k[b] != total - 1) { + atomicCAS(error_flag, kDynamicSparseAttentionValidationOk, + kDynamicSparseAttentionInvalidCommittedLength); + } + } } template @@ -305,7 +392,10 @@ __global__ void AppendKvKernel(const T* query, int rotary_offset, bool rotary_interleaved, bool is_packed, - float epsilon) { + float epsilon, + bool quantized_cache, + const float* key_scale, + const float* value_scale) { const int block = static_cast(blockIdx.x); if (block >= block_count) { return; @@ -371,8 +461,16 @@ __global__ void AppendKvKernel(const T* query, destination_sequence) * head_size + h; - present_key[cache_offset] = result; - present_value[cache_offset] = (is_packed ? query : value)[value_offset]; + if (quantized_cache) { + const int channel = kv_head * head_size + h; + reinterpret_cast(present_key)[cache_offset] = + QuantizeCacheValue(ToFloat(result), key_scale[channel]); + reinterpret_cast(present_value)[cache_offset] = + QuantizeCacheValue(ToFloat((is_packed ? query : value)[value_offset]), value_scale[channel]); + } else { + present_key[cache_offset] = result; + present_value[cache_offset] = (is_packed ? query : value)[value_offset]; + } } } @@ -400,7 +498,10 @@ __global__ void FusedDynamicSparseAttentionKernel(const T* query, float scale, bool local_plus_selected, bool selected_from_auxiliary, - bool use_smooth_softmax) { + bool use_smooth_softmax, + bool quantized_cache, + const float* key_scale, + const float* value_scale) { const int block = static_cast(blockIdx.x); if (block >= block_count) { return; @@ -436,8 +537,7 @@ __global__ void FusedDynamicSparseAttentionKernel(const T* query, extern __shared__ float shared[]; float* logits = shared; - float* valid_candidates = logits + candidate_capacity; - float* reduction = valid_candidates + candidate_capacity; + float* reduction = logits + candidate_capacity; T* shared_query = reinterpret_cast(reduction + blockDim.x); if (h < head_size) { shared_query[h] = query_head[h]; @@ -452,7 +552,7 @@ __global__ void FusedDynamicSparseAttentionKernel(const T* query, index = local_start + candidate; const int64_t offset = ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size; - key_head = main_key + offset; + key_head = quantized_cache ? nullptr : main_key + offset; } else { index = row_indices[candidate - local_count]; if (selected_from_auxiliary) { @@ -470,7 +570,7 @@ __global__ void FusedDynamicSparseAttentionKernel(const T* query, if (valid) { const int64_t offset = ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size; - key_head = main_key + offset; + key_head = quantized_cache ? nullptr : main_key + offset; } } } @@ -478,15 +578,21 @@ __global__ void FusedDynamicSparseAttentionKernel(const T* query, float dot = 0.0f; if (valid) { for (int d = lane; d < head_size; d += 32) { - dot += ToFloat(shared_query[d]) * ToFloat(key_head[d]); + const bool quantized = quantized_cache && (candidate < local_count || !selected_from_auxiliary); + const int64_t offset = + ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size + d; + const float key_value = quantized + ? static_cast(reinterpret_cast(main_key)[offset]) * + key_scale[kv_head * head_size + d] + : ToFloat(key_head[d]); + dot += ToFloat(shared_query[d]) * key_value; } for (int offset = 16; offset > 0; offset >>= 1) { dot += __shfl_down_sync(0xffffffff, dot, offset); } } if (lane == 0) { - logits[candidate] = valid ? dot * scale : 0.0f; - valid_candidates[candidate] = valid ? 1.0f : 0.0f; + logits[candidate] = valid ? dot * scale : -CUDART_INF_F; } } __syncthreads(); @@ -495,9 +601,7 @@ __global__ void FusedDynamicSparseAttentionKernel(const T* query, ? (head_sink == nullptr ? 0.0f : ToFloat(head_sink[head])) : -FLT_MAX; for (int candidate = h; candidate < candidate_count; candidate += static_cast(blockDim.x)) { - if (valid_candidates[candidate] != 0.0f) { - thread_max = fmaxf(thread_max, logits[candidate]); - } + thread_max = fmaxf(thread_max, logits[candidate]); } reduction[h] = thread_max; __syncthreads(); @@ -517,7 +621,7 @@ __global__ void FusedDynamicSparseAttentionKernel(const T* query, } for (int candidate = h; candidate < candidate_count; candidate += static_cast(blockDim.x)) { const float logit = logits[candidate]; - const float weight = valid_candidates[candidate] != 0.0f ? expf(logit - max_logit) : 0.0f; + const float weight = logit == -CUDART_INF_F ? 0.0f : expf(logit - max_logit); logits[candidate] = weight; thread_sum += weight; } @@ -536,19 +640,21 @@ __global__ void FusedDynamicSparseAttentionKernel(const T* query, float accumulator = 0.0f; for (int candidate = 0; candidate < candidate_count; ++candidate) { const float weight = logits[candidate]; - if (valid_candidates[candidate] == 0.0f) { - continue; - } - int index; const T* value_head; if (candidate < local_count) { index = local_start + candidate; const int64_t offset = ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size; - value_head = main_value + offset; + value_head = quantized_cache ? nullptr : main_value + offset; } else { index = row_indices[candidate - local_count]; + if (selected_from_auxiliary + ? index < 0 || index >= auxiliary_sequence_length + : index < 0 || index >= total_length || index > query_position || index >= main_capacity || + (local_plus_selected && index >= local_start)) { + continue; + } if (selected_from_auxiliary) { const int64_t offset = ((static_cast(b) * kv_num_heads + kv_head) * auxiliary_sequence_length + index) * head_size; @@ -556,10 +662,17 @@ __global__ void FusedDynamicSparseAttentionKernel(const T* query, } else { const int64_t offset = ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size; - value_head = main_value + offset; + value_head = quantized_cache ? nullptr : main_value + offset; } } - accumulator += weight * ToFloat(value_head[h]); + const bool quantized = quantized_cache && (candidate < local_count || !selected_from_auxiliary); + const int64_t offset = + ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size + h; + const float value = quantized + ? static_cast(reinterpret_cast(main_value)[offset]) * + value_scale[kv_head * head_size + h] + : ToFloat(value_head[h]); + accumulator += weight * value; } const int64_t output_offset = @@ -569,6 +682,599 @@ __global__ void FusedDynamicSparseAttentionKernel(const T* query, } } +template +__global__ void GroupedPrefillDynamicSparseAttentionKernel( + const T* query, + const T* main_key, + const T* main_value, + const T* auxiliary_key, + const T* auxiliary_value, + const int32_t* selected_indices, + const int32_t* selected_counts, + const int32_t* seqlens_k, + const T* head_sink, + T* output, + int block_count, + int sequence_length, + int num_heads, + int kv_num_heads, + int head_size, + int main_capacity, + int auxiliary_sequence_length, + int max_selected, + int local_window_size, + float scale, + bool local_plus_selected, + bool selected_from_auxiliary, + bool use_smooth_softmax) { + const int block = static_cast(blockIdx.x); + if (block >= block_count) { + return; + } + + const int query_heads_per_kv = num_heads / kv_num_heads; + const int head_tiles = (query_heads_per_kv + kQueryHeadsPerBlock - 1) / kQueryHeadsPerBlock; + const int row = block / (kv_num_heads * head_tiles); + const int kv_tile = block - row * kv_num_heads * head_tiles; + const int kv_head = kv_tile / head_tiles; + const int head_tile = kv_tile - kv_head * head_tiles; + const int group_head = head_tile * kQueryHeadsPerBlock + static_cast(threadIdx.x) / 32; + const bool active_head = group_head < query_heads_per_kv; + const int head = kv_head * query_heads_per_kv + group_head; + const int lane = static_cast(threadIdx.x) & 31; + const int b = row / sequence_length; + const int s = row - b * sequence_length; + const int64_t total_length_64 = static_cast(seqlens_k[b]) + 1; + const bool valid_length = total_length_64 >= sequence_length && total_length_64 <= main_capacity; + const int total_length = valid_length ? static_cast(total_length_64) : 0; + const int query_position = valid_length ? total_length - sequence_length + s : -1; + + int local_start = 0; + int local_count = 0; + if (local_plus_selected) { + local_start = max(0, query_position - local_window_size + 1); + const int local_end = min(query_position, min(total_length, main_capacity) - 1); + local_count = max(0, local_end - local_start + 1); + } + const int selected_count = max(0, min(selected_counts[row], max_selected)); + const int candidate_count = local_count + selected_count; + const int32_t* row_indices = + max_selected == 0 ? selected_indices : selected_indices + static_cast(row) * max_selected; + + extern __shared__ unsigned char shared_bytes[]; + T* shared_key = reinterpret_cast(shared_bytes); + T* shared_value = shared_key + kGroupedPrefillCandidatesPerTile * head_size; + __shared__ bool candidate_valid[kGroupedPrefillCandidatesPerTile]; + __shared__ half mma_query[kGroupedPrefillQueryHeadsPerBlock * 256]; + __shared__ float mma_scores[kGroupedPrefillQueryHeadsPerBlock * kGroupedPrefillCandidatesPerTile]; + + constexpr int kValuesPerLane = 8; + float query_values[kValuesPerLane]{}; + float accumulator[kValuesPerLane]{}; +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800 + if constexpr (std::is_same_v) { + const int group_head_base = head_tile * kQueryHeadsPerBlock; + for (int index = static_cast(threadIdx.x); + index < kGroupedPrefillQueryHeadsPerBlock * head_size; + index += static_cast(blockDim.x)) { + const int local_head = index / head_size; + const int head_offset = index - local_head * head_size; + const int staged_group_head = group_head_base + local_head; + mma_query[index] = staged_group_head < query_heads_per_kv + ? query[(static_cast(row) * num_heads + + kv_head * query_heads_per_kv + staged_group_head) * + head_size + + head_offset] + : half{0.0f}; + } + __syncthreads(); + } else +#endif + if (active_head) { + const T* query_head = query + (static_cast(row) * num_heads + head) * head_size; +#pragma unroll + for (int i = 0; i < kValuesPerLane; ++i) { + query_values[i] = ToFloat(query_head[lane + i * 32]); + } + } + + float running_max = -FLT_MAX; + float running_sum = 0.0f; + if (active_head && use_smooth_softmax) { + running_max = head_sink == nullptr ? 0.0f : ToFloat(head_sink[head]); + running_sum = 1.0f; + } + + for (int candidate_start = 0; candidate_start < candidate_count; + candidate_start += kGroupedPrefillCandidatesPerTile) { + const int tile_count = min(kGroupedPrefillCandidatesPerTile, candidate_count - candidate_start); + const int load_warp = static_cast(threadIdx.x) / 32; + const int load_lane = static_cast(threadIdx.x) & 31; + for (int load_candidate = load_warp; load_candidate < tile_count; + load_candidate += kQueryHeadsPerBlock) { + int candidate_index = 0; + int candidate_is_auxiliary = 0; + int is_valid = 0; + if (load_lane == 0) { + const int candidate = candidate_start + load_candidate; + is_valid = valid_length; + if (candidate < local_count) { + candidate_index = local_start + candidate; + } else { + candidate_index = row_indices[candidate - local_count]; + candidate_is_auxiliary = selected_from_auxiliary; + if (selected_from_auxiliary) { + is_valid = is_valid && candidate_index >= 0 && + candidate_index < auxiliary_sequence_length; + } else { + is_valid = is_valid && candidate_index >= 0 && candidate_index < total_length && + candidate_index <= query_position && candidate_index < main_capacity; + if (local_plus_selected && candidate_index >= local_start && + candidate_index <= query_position) { + is_valid = false; + } + } + } + candidate_valid[load_candidate] = is_valid; + } + candidate_index = __shfl_sync(0xffffffff, candidate_index, 0); + candidate_is_auxiliary = __shfl_sync(0xffffffff, candidate_is_auxiliary, 0); + is_valid = __shfl_sync(0xffffffff, is_valid, 0); + + const int shared_offset = load_candidate * head_size; + if (is_valid) { + const int64_t source_length = candidate_is_auxiliary + ? auxiliary_sequence_length + : main_capacity; + const int64_t offset = + ((static_cast(b) * kv_num_heads + kv_head) * source_length + + candidate_index) * + head_size; + const T* key_head = candidate_is_auxiliary + ? auxiliary_key + offset + : main_key + offset; + const T* value_head = candidate_is_auxiliary + ? auxiliary_value + offset + : main_value + offset; + StageHeadRow(shared_key + shared_offset, key_head, load_lane, head_size); + StageHeadRow(shared_value + shared_offset, value_head, load_lane, head_size); + } + } + __syncthreads(); + +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800 + if constexpr (std::is_same_v) { + if (threadIdx.x < 32) { + const int mma_lane = static_cast(threadIdx.x); + const int row_pair = mma_lane >> 2; + const int k_pair = (mma_lane & 3) * 2; + float score_fragment[4]{}; + for (int k_start = 0; k_start < head_size; k_start += 16) { + const int first_k = k_start + k_pair; + const int second_k = first_k + 8; + uint32_t query_fragment[4]{}; + if (row_pair < kGroupedPrefillQueryHeadsPerBlock) { + query_fragment[0] = *reinterpret_cast( + mma_query + row_pair * head_size + first_k); + query_fragment[2] = *reinterpret_cast( + mma_query + row_pair * head_size + second_k); + } + const uint32_t key_fragment[2]{ + *reinterpret_cast( + shared_key + row_pair * head_size + first_k), + *reinterpret_cast( + shared_key + row_pair * head_size + second_k)}; + MmaM16N8K16(score_fragment, query_fragment, key_fragment); + } + const int score_column = (mma_lane & 3) * 2; + if (row_pair < kGroupedPrefillQueryHeadsPerBlock) { + mma_scores[row_pair * kGroupedPrefillCandidatesPerTile + score_column] = score_fragment[0]; + mma_scores[row_pair * kGroupedPrefillCandidatesPerTile + score_column + 1] = score_fragment[1]; + } + } + __syncthreads(); + } +#endif + + for (int tile_candidate = 0; tile_candidate < tile_count; ++tile_candidate) { + if (active_head && candidate_valid[tile_candidate]) { + const int shared_offset = tile_candidate * head_size; + float score; +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800 + if constexpr (std::is_same_v) { + score = mma_scores[static_cast(threadIdx.x) / 32 * + kGroupedPrefillCandidatesPerTile + + tile_candidate] * + scale; + } else +#endif + { + float dot = 0.0f; +#pragma unroll + for (int i = 0; i < kValuesPerLane; ++i) { + dot += query_values[i] * ToFloat(shared_key[shared_offset + lane + i * 32]); + } + for (int offset = 16; offset > 0; offset >>= 1) { + dot += __shfl_down_sync(0xffffffff, dot, offset); + } + score = __shfl_sync(0xffffffff, dot, 0) * scale; + } + const float next_max = fmaxf(running_max, score); + const float old_scale = running_sum == 0.0f ? 0.0f : expf(running_max - next_max); + const float weight = expf(score - next_max); +#pragma unroll + for (int i = 0; i < kValuesPerLane; ++i) { + accumulator[i] = accumulator[i] * old_scale + + weight * ToFloat(shared_value[shared_offset + lane + i * 32]); + } + running_max = next_max; + running_sum = running_sum * old_scale + weight; + } + } + __syncthreads(); + } + + if (active_head) { + T* output_head = output + (static_cast(row) * num_heads + head) * head_size; +#pragma unroll + for (int i = 0; i < kValuesPerLane; ++i) { + output_head[lane + i * 32] = running_sum == 0.0f + ? FromFloat(0.0f) + : FromFloat(accumulator[i] / running_sum); + } + } +} + +__device__ __forceinline__ float4 DequantizeCacheWord(uint32_t packed, float4 scales) { + return make_float4( + static_cast(static_cast(packed << 24) >> 24) * scales.x, + static_cast(static_cast(packed << 16) >> 24) * scales.y, + static_cast(static_cast(packed << 8) >> 24) * scales.z, + static_cast(static_cast(packed) >> 24) * scales.w); +} + +template +__global__ void GroupedDecodeDynamicSparseAttentionKernel( + const T* query, + const T* main_key, + const T* main_value, + const T* auxiliary_key, + const T* auxiliary_value, + const int32_t* selected_indices, + const int32_t* selected_counts, + const int32_t* seqlens_k, + const T* head_sink, + float* partial_max, + float* partial_sum, + float* partial_output, + int grouped_block_count, + int split_count, + int sequence_length, + int num_heads, + int kv_num_heads, + int head_size, + int main_capacity, + int auxiliary_sequence_length, + int max_selected, + int local_window_size, + float scale, + bool local_plus_selected, + bool selected_from_auxiliary, + bool use_smooth_softmax, + const float* key_scale, + const float* value_scale) { + const int block = static_cast(blockIdx.x); + if (block >= grouped_block_count) { + return; + } + + const int split = block % split_count; + const int grouped_block = block / split_count; + const int query_heads_per_kv = num_heads / kv_num_heads; + const int head_tiles = (query_heads_per_kv + kQueryHeadsPerBlock - 1) / kQueryHeadsPerBlock; + const int row = grouped_block / (kv_num_heads * head_tiles); + const int b = row / sequence_length; + const int s = row - b * sequence_length; + const int kv_tile = grouped_block - row * kv_num_heads * head_tiles; + const int kv_head = kv_tile / head_tiles; + const int head_tile = kv_tile - kv_head * head_tiles; + const int group_head = head_tile * kQueryHeadsPerBlock + static_cast(threadIdx.x) / 32; + const bool active_head = group_head < query_heads_per_kv; + const int head = kv_head * query_heads_per_kv + group_head; + const int lane = static_cast(threadIdx.x) & 31; + const int64_t total_length_64 = static_cast(seqlens_k[b]) + 1; + const bool valid_length = total_length_64 >= sequence_length && total_length_64 <= main_capacity; + const int total_length = valid_length ? static_cast(total_length_64) : 0; + const int query_position = valid_length ? total_length - sequence_length + s : -1; + + int local_start = 0; + int local_count = 0; + if (local_plus_selected) { + local_start = max(0, query_position - local_window_size + 1); + const int local_end = min(query_position, min(total_length, main_capacity) - 1); + local_count = max(0, local_end - local_start + 1); + } + const int selected_count = max(0, min(selected_counts[row], max_selected)); + const int candidate_count = local_count + selected_count; + constexpr int kCandidatesPerSplit = kQuantizedCache ? kGroupedQuantizedDecodeCandidatesPerSplit + : kGroupedDecodeCandidatesPerSplit; + const int candidate_begin = split * kCandidatesPerSplit; + const int32_t* row_indices = + max_selected == 0 ? selected_indices : selected_indices + static_cast(row) * max_selected; + + extern __shared__ unsigned char shared_bytes[]; + using SharedT = std::conditional_t; + SharedT* shared_key = reinterpret_cast(shared_bytes); + SharedT* shared_value = shared_key + kGroupedPrefillCandidatesPerTile * head_size; + __shared__ int candidate_indices[kGroupedPrefillCandidatesPerTile]; + __shared__ bool candidate_valid[kGroupedPrefillCandidatesPerTile]; + __shared__ bool candidate_sources[kGroupedPrefillCandidatesPerTile]; + constexpr int kMmaHeads = (std::is_same_v && !kQuantizedCache) ? kQueryHeadsPerBlock : 1; + __shared__ half mma_query[kMmaHeads * 256]; + __shared__ float mma_scores[kMmaHeads * kGroupedPrefillCandidatesPerTile]; + + constexpr int kValuesPerLane = 8; + float query_values[kValuesPerLane]{}; + float accumulator[kValuesPerLane]{}; +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800 + if constexpr (std::is_same_v && !kQuantizedCache) { + const int group_head_base = head_tile * kQueryHeadsPerBlock; + for (int index = static_cast(threadIdx.x); + index < kQueryHeadsPerBlock * head_size; + index += static_cast(blockDim.x)) { + const int local_head = index / head_size; + const int head_offset = index - local_head * head_size; + const int staged_group_head = group_head_base + local_head; + mma_query[index] = staged_group_head < query_heads_per_kv + ? query[(static_cast(row) * num_heads + + kv_head * query_heads_per_kv + staged_group_head) * + head_size + + head_offset] + : half{0.0f}; + } + __syncthreads(); + } else +#endif + if (active_head) { + const T* query_head = query + (static_cast(row) * num_heads + head) * head_size; +#pragma unroll + for (int i = 0; i < kValuesPerLane; ++i) { + query_values[i] = ToFloat(query_head[lane + i * 32]); + } + } + + float running_max = -FLT_MAX; + float running_sum = 0.0f; + if (active_head && split == 0 && use_smooth_softmax) { + running_max = head_sink == nullptr ? 0.0f : ToFloat(head_sink[head]); + running_sum = 1.0f; + } + + for (int candidate_start = candidate_begin; candidate_start < candidate_count;) { + const int tile_count = min(kGroupedPrefillCandidatesPerTile, candidate_count - candidate_start); + const int load_candidate = static_cast(threadIdx.x) / 32; + const int load_lane = static_cast(threadIdx.x) & 31; + int candidate_index = 0; + int candidate_is_auxiliary = 0; + int is_valid = 0; + if constexpr (!kWarpLocalMetadata) { + if (threadIdx.x < tile_count) { + const int tile_candidate = static_cast(threadIdx.x); + const int candidate = candidate_start + tile_candidate; + candidate_valid[tile_candidate] = total_length > 0 && total_length <= main_capacity; + candidate_sources[tile_candidate] = false; + if (candidate < local_count) { + candidate_indices[tile_candidate] = local_start + candidate; + } else { + candidate_indices[tile_candidate] = row_indices[candidate - local_count]; + candidate_sources[tile_candidate] = selected_from_auxiliary; + if (selected_from_auxiliary) { + candidate_valid[tile_candidate] = + candidate_valid[tile_candidate] && candidate_indices[tile_candidate] >= 0 && + candidate_indices[tile_candidate] < auxiliary_sequence_length; + } else { + candidate_valid[tile_candidate] = + candidate_valid[tile_candidate] && candidate_indices[tile_candidate] >= 0 && + candidate_indices[tile_candidate] < total_length && + candidate_indices[tile_candidate] <= query_position; + if (local_plus_selected && candidate_indices[tile_candidate] >= local_start) { + candidate_valid[tile_candidate] = false; + } + } + } + } + __syncthreads(); + } + if (load_candidate < tile_count) { + if constexpr (kWarpLocalMetadata) { + if (load_lane == 0) { + const int candidate = candidate_start + load_candidate; + is_valid = total_length > 0 && total_length <= main_capacity; + if (candidate < local_count) { + candidate_index = local_start + candidate; + } else { + candidate_index = row_indices[candidate - local_count]; + candidate_is_auxiliary = selected_from_auxiliary; + if (selected_from_auxiliary) { + is_valid = is_valid && candidate_index >= 0 && + candidate_index < auxiliary_sequence_length; + } else { + is_valid = is_valid && candidate_index >= 0 && candidate_index < total_length && + candidate_index <= query_position; + if (local_plus_selected && candidate_index >= local_start) { + is_valid = false; + } + } + } + candidate_valid[load_candidate] = is_valid; + } + candidate_index = __shfl_sync(0xffffffff, candidate_index, 0); + candidate_is_auxiliary = __shfl_sync(0xffffffff, candidate_is_auxiliary, 0); + is_valid = __shfl_sync(0xffffffff, is_valid, 0); + } else { + candidate_index = candidate_indices[load_candidate]; + candidate_is_auxiliary = candidate_sources[load_candidate]; + is_valid = candidate_valid[load_candidate]; + } + + const int shared_offset = load_candidate * head_size; + if (is_valid) { + const int64_t source_length = candidate_is_auxiliary + ? auxiliary_sequence_length + : main_capacity; + const int64_t offset = + ((static_cast(b) * kv_num_heads + kv_head) * source_length + + candidate_index) * + head_size; + if constexpr (kQuantizedCache && kVectorizedStaging) { + const int d = load_lane * 8; + float4* staged_key = reinterpret_cast(shared_key + shared_offset + d); + float4* staged_value = reinterpret_cast(shared_value + shared_offset + d); + if (candidate_is_auxiliary) { + const uint4 packed_key = __ldg(reinterpret_cast(auxiliary_key + offset + d)); + const uint4 packed_value = __ldg(reinterpret_cast(auxiliary_value + offset + d)); + const T* key = reinterpret_cast(&packed_key); + const T* value = reinterpret_cast(&packed_value); + staged_key[0] = make_float4(ToFloat(key[0]), ToFloat(key[1]), ToFloat(key[2]), ToFloat(key[3])); + staged_key[1] = make_float4(ToFloat(key[4]), ToFloat(key[5]), ToFloat(key[6]), ToFloat(key[7])); + staged_value[0] = make_float4(ToFloat(value[0]), ToFloat(value[1]), ToFloat(value[2]), ToFloat(value[3])); + staged_value[1] = make_float4(ToFloat(value[4]), ToFloat(value[5]), ToFloat(value[6]), ToFloat(value[7])); + } else { + const uint2 packed_key = __ldg(reinterpret_cast( + reinterpret_cast(main_key) + offset + d)); + const uint2 packed_value = __ldg(reinterpret_cast( + reinterpret_cast(main_value) + offset + d)); + const float4* key_scales = reinterpret_cast(key_scale + kv_head * head_size + d); + const float4* value_scales = reinterpret_cast(value_scale + kv_head * head_size + d); + staged_key[0] = DequantizeCacheWord(packed_key.x, __ldg(key_scales)); + staged_key[1] = DequantizeCacheWord(packed_key.y, __ldg(key_scales + 1)); + staged_value[0] = DequantizeCacheWord(packed_value.x, __ldg(value_scales)); + staged_value[1] = DequantizeCacheWord(packed_value.y, __ldg(value_scales + 1)); + } + } else if constexpr (kQuantizedCache) { + for (int d = load_lane; d < head_size; d += 32) { + const int channel = kv_head * head_size + d; + shared_key[shared_offset + d] = + candidate_is_auxiliary + ? ToFloat(auxiliary_key[offset + d]) + : static_cast(reinterpret_cast(main_key)[offset + d]) * + key_scale[channel]; + shared_value[shared_offset + d] = + candidate_is_auxiliary + ? ToFloat(auxiliary_value[offset + d]) + : static_cast(reinterpret_cast(main_value)[offset + d]) * + value_scale[channel]; + } + } else { + const T* key_head = candidate_is_auxiliary ? auxiliary_key + offset : main_key + offset; + const T* value_head = candidate_is_auxiliary ? auxiliary_value + offset : main_value + offset; + StageHeadRow(shared_key + shared_offset, key_head, load_lane, head_size); + StageHeadRow(shared_value + shared_offset, value_head, load_lane, head_size); + } + } + } + __syncthreads(); + +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800 + if constexpr (std::is_same_v && !kQuantizedCache) { + if (threadIdx.x < 32) { + const int mma_lane = static_cast(threadIdx.x); + const int row_pair = mma_lane >> 2; + const int second_row = row_pair + 8; + const int k_pair = (mma_lane & 3) * 2; + float score_fragment[4]{}; + for (int k_start = 0; k_start < head_size; k_start += 16) { + const int first_k = k_start + k_pair; + const int second_k = first_k + 8; + const uint32_t query_fragment[4]{ + *reinterpret_cast( + mma_query + row_pair * head_size + first_k), + second_row < kQueryHeadsPerBlock + ? *reinterpret_cast( + mma_query + second_row * head_size + first_k) + : 0u, + *reinterpret_cast( + mma_query + row_pair * head_size + second_k), + second_row < kQueryHeadsPerBlock + ? *reinterpret_cast( + mma_query + second_row * head_size + second_k) + : 0u}; + const uint32_t key_fragment[2]{ + *reinterpret_cast( + shared_key + row_pair * head_size + first_k), + *reinterpret_cast( + shared_key + row_pair * head_size + second_k)}; + MmaM16N8K16(score_fragment, query_fragment, key_fragment); + } + const int score_column = (mma_lane & 3) * 2; + mma_scores[row_pair * kGroupedPrefillCandidatesPerTile + score_column] = score_fragment[0]; + mma_scores[row_pair * kGroupedPrefillCandidatesPerTile + score_column + 1] = score_fragment[1]; + if (second_row < kQueryHeadsPerBlock) { + mma_scores[second_row * kGroupedPrefillCandidatesPerTile + score_column] = score_fragment[2]; + mma_scores[second_row * kGroupedPrefillCandidatesPerTile + score_column + 1] = score_fragment[3]; + } + } + __syncthreads(); + } +#endif + + for (int tile_candidate = 0; tile_candidate < tile_count; ++tile_candidate) { + if (active_head && candidate_valid[tile_candidate]) { + const int shared_offset = tile_candidate * head_size; + float score; +#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800 + if constexpr (std::is_same_v && !kQuantizedCache) { + score = mma_scores[static_cast(threadIdx.x) / 32 * + kGroupedPrefillCandidatesPerTile + + tile_candidate] * + scale; + } else +#endif + { + float dot = 0.0f; +#pragma unroll + for (int i = 0; i < kValuesPerLane; ++i) { + dot += query_values[i] * ToFloat(shared_key[shared_offset + lane + i * 32]); + } + for (int offset = 16; offset > 0; offset >>= 1) { + dot += __shfl_down_sync(0xffffffff, dot, offset); + } + score = __shfl_sync(0xffffffff, dot, 0) * scale; + } + const float next_max = fmaxf(running_max, score); + const float old_scale = running_sum == 0.0f ? 0.0f : expf(running_max - next_max); + const float weight = expf(score - next_max); +#pragma unroll + for (int i = 0; i < kValuesPerLane; ++i) { + accumulator[i] = accumulator[i] * old_scale + + weight * ToFloat(shared_value[shared_offset + lane + i * 32]); + } + running_max = next_max; + running_sum = running_sum * old_scale + weight; + } + } + __syncthreads(); + + candidate_start += kGroupedPrefillCandidatesPerTile; + if (candidate_start % kCandidatesPerSplit == 0) { + candidate_start += (split_count - 1) * kCandidatesPerSplit; + } + } + + if (active_head) { + const int partial_block = (row * num_heads + head) * split_count + split; + if (lane == 0) { + partial_max[partial_block] = running_max; + partial_sum[partial_block] = running_sum; + } +#pragma unroll + for (int i = 0; i < kValuesPerLane; ++i) { + partial_output[static_cast(partial_block) * head_size + lane + i * 32] = accumulator[i]; + } + } +} + template __global__ void SplitDynamicSparseAttentionKernel(const T* query, const T* main_key, @@ -596,7 +1302,10 @@ __global__ void SplitDynamicSparseAttentionKernel(const T* query, float scale, bool local_plus_selected, bool selected_from_auxiliary, - bool use_smooth_softmax) { + bool use_smooth_softmax, + bool quantized_cache, + const float* key_scale, + const float* value_scale) { const int partial_block = static_cast(blockIdx.x); if (partial_block >= partial_block_count) { return; @@ -633,8 +1342,7 @@ __global__ void SplitDynamicSparseAttentionKernel(const T* query, extern __shared__ float shared[]; float* logits = shared; - float* valid_candidates = logits + kSplitCandidateSize; - float* reduction = valid_candidates + kSplitCandidateSize; + float* reduction = logits + kSplitCandidateSize; T* shared_query = reinterpret_cast(reduction + blockDim.x); if (h < head_size) { shared_query[h] = query_head[h]; @@ -663,7 +1371,7 @@ __global__ void SplitDynamicSparseAttentionKernel(const T* query, index = local_start + candidate; const int64_t offset = ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size; - key_head = main_key + offset; + key_head = quantized_cache ? nullptr : main_key + offset; } else { index = row_indices[candidate - local_count]; if (selected_from_auxiliary) { @@ -681,7 +1389,7 @@ __global__ void SplitDynamicSparseAttentionKernel(const T* query, if (valid) { const int64_t offset = ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size; - key_head = main_key + offset; + key_head = quantized_cache ? nullptr : main_key + offset; } } } @@ -689,15 +1397,21 @@ __global__ void SplitDynamicSparseAttentionKernel(const T* query, float dot = 0.0f; if (valid) { for (int d = lane; d < head_size; d += 32) { - dot += ToFloat(shared_query[d]) * ToFloat(key_head[d]); + const bool quantized = quantized_cache && (candidate < local_count || !selected_from_auxiliary); + const int64_t offset = + ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size + d; + const float key_value = quantized + ? static_cast(reinterpret_cast(main_key)[offset]) * + key_scale[kv_head * head_size + d] + : ToFloat(key_head[d]); + dot += ToFloat(shared_query[d]) * key_value; } for (int offset = 16; offset > 0; offset >>= 1) { dot += __shfl_down_sync(0xffffffff, dot, offset); } } if (lane == 0) { - logits[split_candidate] = valid ? dot * scale : 0.0f; - valid_candidates[split_candidate] = valid ? 1.0f : 0.0f; + logits[split_candidate] = valid ? dot * scale : -CUDART_INF_F; } } __syncthreads(); @@ -705,9 +1419,7 @@ __global__ void SplitDynamicSparseAttentionKernel(const T* query, float thread_max = -FLT_MAX; for (int candidate = h; candidate < split_candidate_count; candidate += static_cast(blockDim.x)) { - if (valid_candidates[candidate] != 0.0f) { - thread_max = fmaxf(thread_max, logits[candidate]); - } + thread_max = fmaxf(thread_max, logits[candidate]); } reduction[h] = thread_max; __syncthreads(); @@ -726,7 +1438,7 @@ __global__ void SplitDynamicSparseAttentionKernel(const T* query, for (int candidate = h; candidate < split_candidate_count; candidate += static_cast(blockDim.x)) { const float logit = logits[candidate]; - const float weight = valid_candidates[candidate] != 0.0f ? expf(logit - next_max) : 0.0f; + const float weight = logit == -CUDART_INF_F ? 0.0f : expf(logit - next_max); logits[candidate] = weight; thread_sum += weight; } @@ -745,10 +1457,6 @@ __global__ void SplitDynamicSparseAttentionKernel(const T* query, float tile_accumulator = 0.0f; for (int split_candidate = 0; split_candidate < split_candidate_count; ++split_candidate) { const float weight = logits[split_candidate]; - if (valid_candidates[split_candidate] == 0.0f) { - continue; - } - const int candidate = candidate_start + split_candidate; int index; const T* value_head; @@ -756,9 +1464,15 @@ __global__ void SplitDynamicSparseAttentionKernel(const T* query, index = local_start + candidate; const int64_t offset = ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size; - value_head = main_value + offset; + value_head = quantized_cache ? nullptr : main_value + offset; } else { index = row_indices[candidate - local_count]; + if (selected_from_auxiliary + ? index < 0 || index >= auxiliary_sequence_length + : index < 0 || index >= total_length || index > query_position || index >= main_capacity || + (local_plus_selected && index >= local_start)) { + continue; + } if (selected_from_auxiliary) { const int64_t offset = ((static_cast(b) * kv_num_heads + kv_head) * auxiliary_sequence_length + index) * head_size; @@ -766,10 +1480,17 @@ __global__ void SplitDynamicSparseAttentionKernel(const T* query, } else { const int64_t offset = ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size; - value_head = main_value + offset; + value_head = quantized_cache ? nullptr : main_value + offset; } } - tile_accumulator += weight * ToFloat(value_head[h]); + const bool quantized = quantized_cache && (candidate < local_count || !selected_from_auxiliary); + const int64_t offset = + ((static_cast(b) * kv_num_heads + kv_head) * main_capacity + index) * head_size + h; + const float value = quantized + ? static_cast(reinterpret_cast(main_value)[offset]) * + value_scale[kv_head * head_size + h] + : ToFloat(value_head[h]); + tile_accumulator += weight * value; } running_accumulator = running_accumulator * old_scale + tile_accumulator; @@ -832,7 +1553,9 @@ __global__ void MergeDynamicSparseAttentionKernel(const float* partial_max, float thread_sum = 0.0f; for (int split = h; split < split_count; split += static_cast(blockDim.x)) { const float split_max = partial_max[partial_start + split]; - thread_sum += partial_sum[partial_start + split] * expf(split_max - max_logit); + if (partial_sum[partial_start + split] != 0.0f) { + thread_sum += partial_sum[partial_start + split] * expf(split_max - max_logit); + } } reduction[h] = thread_sum; __syncthreads(); @@ -849,10 +1572,117 @@ __global__ void MergeDynamicSparseAttentionKernel(const float* partial_max, float accumulator = 0.0f; for (int split = 0; split < split_count; ++split) { const float split_max = partial_max[partial_start + split]; - const float merge_scale = expf(split_max - max_logit); - const int64_t partial_offset = - (static_cast(partial_start + split) * head_size) + h; - accumulator += partial_output[partial_offset] * merge_scale; + if (partial_sum[partial_start + split] != 0.0f) { + const float merge_scale = expf(split_max - max_logit); + const int64_t partial_offset = + (static_cast(partial_start + split) * head_size) + h; + accumulator += partial_output[partial_offset] * merge_scale; + } + } + output[static_cast(query_block) * head_size + h] = + denominator == 0.0f ? FromFloat(0.0f) : FromFloat(accumulator / denominator); + } +} + +template +__global__ void MergeGroupedDecodeDynamicSparseAttentionKernel(const float* partial_max, + const float* partial_sum, + const float* partial_output, + T* output, + int query_block_count, + int split_count, + int head_size) { + const int query_block = static_cast(blockIdx.x); + if (query_block >= query_block_count) { + return; + } + + const int h = static_cast(threadIdx.x); + const int lane = h & 31; + const int partial_start = query_block * split_count; + __shared__ float max_logit; + __shared__ float denominator; + __shared__ float split_scales[kSharedScales ? 32 : 1]; + __shared__ unsigned active_splits; + + if constexpr (kSharedScales) { + if (h < 32) { + const float split_max = lane < split_count ? partial_max[partial_start + lane] : -FLT_MAX; + float value = split_max; + for (int offset = 16; offset > 0; offset >>= 1) { + value = fmaxf(value, __shfl_down_sync(0xffffffff, value, offset)); + } + const float maximum = __shfl_sync(0xffffffff, value, 0); + const float sum = lane < split_count ? partial_sum[partial_start + lane] : 0.0f; + const bool active = lane < split_count && sum != 0.0f; + const float factor = active ? expf(split_max - maximum) : 0.0f; + split_scales[lane] = factor; + const unsigned mask = __ballot_sync(0xffffffff, active); + value = active ? sum * factor : 0.0f; + for (int offset = 16; offset > 0; offset >>= 1) { + value += __shfl_down_sync(0xffffffff, value, offset); + } + if (lane == 0) { + denominator = value; + active_splits = mask; + } + } + __syncthreads(); + + if (h < head_size) { + float accumulator = 0.0f; + const unsigned mask = active_splits; + const int end = mask == 0 ? 0 : 32 - __clz(mask); + for (int split = 0; split < end; ++split) { + // Do not skip an active split whose scale underflows: zero times a + // nonfinite partial must retain the generic path's behavior. + if (mask & (1u << split)) { + const int64_t partial_offset = + static_cast(partial_start + split) * head_size + h; + accumulator += partial_output[partial_offset] * split_scales[split]; + } + } + output[static_cast(query_block) * head_size + h] = + denominator == 0.0f ? FromFloat(0.0f) : FromFloat(accumulator / denominator); + } + return; + } + + if (h < 32) { + float value = lane < split_count ? partial_max[partial_start + lane] : -FLT_MAX; + for (int offset = 16; offset > 0; offset >>= 1) { + value = fmaxf(value, __shfl_down_sync(0xffffffff, value, offset)); + } + if (lane == 0) { + max_logit = value; + } + } + __syncthreads(); + + if (h < 32) { + float value = 0.0f; + if (lane < split_count && partial_sum[partial_start + lane] != 0.0f) { + value = partial_sum[partial_start + lane] * + expf(partial_max[partial_start + lane] - max_logit); + } + for (int offset = 16; offset > 0; offset >>= 1) { + value += __shfl_down_sync(0xffffffff, value, offset); + } + if (lane == 0) { + denominator = value; + } + } + __syncthreads(); + + if (h < head_size) { + float accumulator = 0.0f; + for (int split = 0; split < split_count; ++split) { + const float split_max = partial_max[partial_start + split]; + if (partial_sum[partial_start + split] != 0.0f) { + const int64_t partial_offset = + (static_cast(partial_start + split) * head_size) + h; + accumulator += partial_output[partial_offset] * expf(split_max - max_logit); + } } output[static_cast(query_block) * head_size + h] = denominator == 0.0f ? FromFloat(0.0f) : FromFloat(accumulator / denominator); @@ -892,6 +1722,11 @@ int GetAttentionSplitCount(const DynamicSparseAttentionParameters& parameters) { return static_cast(std::min(candidate_tiles, occupancy_splits)); } +// A null pointer is treated as aligned because kernels do not dereference it. +bool IsAligned(const void* pointer, size_t alignment) { + return reinterpret_cast(pointer) % alignment == 0; +} + bool UseFusedAttention(const DynamicSparseAttentionParameters& parameters, size_t element_size, size_t max_shared_memory_per_block) { @@ -902,27 +1737,92 @@ bool UseFusedAttention(const DynamicSparseAttentionParameters& parameters, const int threads = GetThreadsPerBlock(parameters.head_size); const int fused_threads = threads < 128 ? 128 : threads; const size_t fused_shared_bytes = - (2 * static_cast(GetCandidateCapacity(parameters)) + static_cast(fused_threads)) * sizeof(float) + + (static_cast(GetCandidateCapacity(parameters)) + static_cast(fused_threads)) * sizeof(float) + static_cast(parameters.head_size) * element_size; return fused_shared_bytes <= kMaxFusedSharedBytes && fused_shared_bytes < max_shared_memory_per_block; } -size_t GetValidationBitmapWords(const DynamicSparseAttentionParameters& parameters) { +int GetGroupedHeadTiles(const DynamicSparseAttentionParameters& parameters, + int query_heads_per_block) { + const int query_heads_per_kv = parameters.num_heads / parameters.kv_num_heads; + return (query_heads_per_kv + query_heads_per_block - 1) / query_heads_per_block; +} + +size_t GetGroupedAttentionSharedBytes(const DynamicSparseAttentionParameters& parameters, + size_t element_size) { + return 2 * kGroupedPrefillCandidatesPerTile * + static_cast(parameters.head_size) * element_size; +} + +bool SupportsGroupedAttention(const DynamicSparseAttentionParameters& parameters, + size_t element_size, + size_t max_shared_memory_per_block) { + return element_size == sizeof(half) && + parameters.head_size == 256 && + parameters.num_heads > parameters.kv_num_heads && + parameters.num_heads % parameters.kv_num_heads == 0 && + GetGroupedAttentionSharedBytes(parameters, element_size) < max_shared_memory_per_block; +} + +bool UseGroupedPrefillAttention(const DynamicSparseAttentionParameters& parameters, + size_t element_size, + size_t max_shared_memory_per_block) { + return parameters.sequence_length > 1 && + SupportsGroupedAttention(parameters, element_size, max_shared_memory_per_block); +} + +bool UseGroupedDecodeAttention(const DynamicSparseAttentionParameters& parameters, + size_t element_size, + size_t max_shared_memory_per_block, + bool quantized_cache = false) { + return parameters.sequence_length >= 1 && + parameters.sequence_length <= 8 && + parameters.num_heads / parameters.kv_num_heads >= 4 && + GetCandidateCapacity(parameters) > kSplitCandidateSize && + SupportsGroupedAttention(parameters, element_size, max_shared_memory_per_block) && + (!quantized_cache || + GetGroupedAttentionSharedBytes(parameters, sizeof(float)) < max_shared_memory_per_block); +} + +int GetGroupedDecodeHeadsPerBlock(const DynamicSparseAttentionParameters& parameters) { + const int query_heads_per_kv = parameters.num_heads / parameters.kv_num_heads; + return (query_heads_per_kv == 12) ? 12 : kGroupedDecodeQueryHeadsPerBlock; +} + +int GetGroupedDecodeSplitCount(const DynamicSparseAttentionParameters& parameters, bool quantized_cache = false) { + const int candidates_per_split = quantized_cache ? kGroupedQuantizedDecodeCandidatesPerSplit + : kGroupedDecodeCandidatesPerSplit; + const int target_blocks = quantized_cache ? kTargetGroupedQuantizedDecodeBlocks : kTargetGroupedDecodeBlocks; + const int64_t candidate_splits = + (GetCandidateCapacity(parameters) + candidates_per_split - 1) / candidates_per_split; + const int64_t grouped_head_blocks = + static_cast(parameters.batch_size) * parameters.sequence_length * parameters.kv_num_heads * + GetGroupedHeadTiles(parameters, GetGroupedDecodeHeadsPerBlock(parameters)); + const int64_t occupancy_splits = + std::max(1, (target_blocks + grouped_head_blocks - 1) / + grouped_head_blocks); + const int64_t max_splits = quantized_cache ? 32 : candidate_splits; + return static_cast(std::min({candidate_splits, occupancy_splits, max_splits})); +} + +size_t GetValidationHashEntries(const DynamicSparseAttentionParameters& parameters) { if (parameters.max_selected == 0) { return 0; } - const size_t source_length = - parameters.selected_kv_source == DynamicSparseAttentionKvSource::kAuxiliary - ? static_cast(parameters.auxiliary_sequence_length) - : static_cast(parameters.cache_capacity); - return (source_length + 31) / 32; + const size_t max_selected = static_cast(parameters.max_selected); + const size_t target_entries = max_selected + (max_selected + 1) / 2; + size_t hash_entries = 1; + while (hash_entries < target_entries) { + hash_entries <<= 1; + } + return hash_entries; } -bool UseSharedValidationBitmap(const DynamicSparseAttentionParameters& parameters, - size_t max_shared_memory_per_block) { - const size_t shared_bytes = GetValidationBitmapWords(parameters) * sizeof(uint32_t); +bool UseSharedValidationHashTable(const DynamicSparseAttentionParameters& parameters, + size_t max_shared_memory_per_block) { + const size_t shared_bytes = GetValidationHashEntries(parameters) * sizeof(uint32_t); return shared_bytes <= kMaxValidationSharedBytes && shared_bytes < max_shared_memory_per_block; } @@ -932,27 +1832,49 @@ bool UseSharedValidationBitmap(const DynamicSparseAttentionParameters& parameter size_t GetDynamicSparseAttentionValidationWorkspaceSize( const DynamicSparseAttentionParameters& parameters, size_t max_shared_memory_per_block) { - if (UseSharedValidationBitmap(parameters, max_shared_memory_per_block)) { + if (UseSharedValidationHashTable(parameters, max_shared_memory_per_block)) { return 0; } const size_t row_count = SafeInt(parameters.batch_size) * parameters.sequence_length; - return SafeInt(row_count) * GetValidationBitmapWords(parameters); + return SafeInt(row_count) * GetValidationHashEntries(parameters); } size_t GetDynamicSparseAttentionWorkspaceSize( const DynamicSparseAttentionParameters& parameters, size_t element_size, - size_t max_shared_memory_per_block) { - if (UseFusedAttention(parameters, element_size, max_shared_memory_per_block)) { - return 0; + size_t max_shared_memory_per_block, + bool quantized_cache) { + if (quantized_cache) { + if (GetCandidateCapacity(parameters) <= kSplitCandidateSize) return 0; + const size_t query_blocks = + SafeInt(parameters.batch_size) * parameters.sequence_length * parameters.num_heads; + const int split_count = + UseGroupedDecodeAttention(parameters, element_size, max_shared_memory_per_block, true) + ? GetGroupedDecodeSplitCount(parameters, true) + : GetAttentionSplitCount(parameters); + return SafeInt(query_blocks) * split_count * + (static_cast(parameters.head_size) + 2); + } + const bool grouped_decode = + UseGroupedDecodeAttention(parameters, element_size, max_shared_memory_per_block); + if (!grouped_decode) { + if (UseGroupedPrefillAttention(parameters, element_size, max_shared_memory_per_block)) { + return 0; + } + if (UseFusedAttention(parameters, element_size, max_shared_memory_per_block)) { + return 0; + } } const size_t query_blocks = static_cast(parameters.batch_size) * parameters.sequence_length * parameters.num_heads; - const size_t split_count = GetAttentionSplitCount(parameters); - if (split_count == 1) { + const size_t split_count = + grouped_decode + ? GetGroupedDecodeSplitCount(parameters) + : GetAttentionSplitCount(parameters); + if (split_count == 1 && !grouped_decode) { return 0; } return SafeInt(query_blocks) * split_count * @@ -969,24 +1891,31 @@ Status ValidateDynamicSparseAttentionOnDevice( int32_t* error_flag, uint32_t* validation_bitmap, size_t max_shared_memory_per_block, - bool copy_result_to_host) { + bool copy_result_to_host, + const float* key_scale, + const float* value_scale, + const int32_t* committed_length, + int committed_length_stride) { CUDA_RETURN_IF_ERROR(cudaMemsetAsync(error_flag, 0, sizeof(int32_t), stream)); const int64_t row_count = static_cast(parameters.batch_size) * parameters.sequence_length; - const size_t validation_bitmap_words = GetValidationBitmapWords(parameters); - const bool use_shared_bitmap = - UseSharedValidationBitmap(parameters, max_shared_memory_per_block); - const size_t validation_bitmap_elements = - use_shared_bitmap ? 0 : static_cast(row_count) * validation_bitmap_words; - if (validation_bitmap_elements > 0) { + const size_t validation_hash_entries = GetValidationHashEntries(parameters); + const bool use_shared_hash_table = + UseSharedValidationHashTable(parameters, max_shared_memory_per_block); + const size_t validation_hash_elements = + use_shared_hash_table ? 0 : static_cast(row_count) * validation_hash_entries; + if (validation_hash_elements > 0) { CUDA_RETURN_IF_ERROR(cudaMemsetAsync( - validation_bitmap, 0, validation_bitmap_elements * sizeof(uint32_t), stream)); + validation_bitmap, 0xff, validation_hash_elements * sizeof(uint32_t), stream)); } ORT_RETURN_IF_ERROR(CheckBlockCount(row_count, "validation")); - constexpr int kValidationThreads = 256; + int validation_threads = 32; + while (validation_threads < parameters.max_selected && validation_threads < 1024) { + validation_threads <<= 1; + } const size_t validation_shared_bytes = - use_shared_bitmap ? validation_bitmap_words * sizeof(uint32_t) : 0; - ValidateInputsKernel<<(row_count), kValidationThreads, + use_shared_hash_table ? validation_hash_entries * sizeof(uint32_t) : 0; + ValidateInputsKernel<<(row_count), validation_threads, validation_shared_bytes, stream>>>( selected_indices, selected_counts, seqlens_k, position_ids, parameters.batch_size, parameters.sequence_length, parameters.max_selected, @@ -994,7 +1923,9 @@ Status ValidateDynamicSparseAttentionOnDevice( parameters.cache_capacity, parameters.auxiliary_sequence_length, parameters.rotary_max_position, parameters.do_rotary, parameters.selected_kv_source == DynamicSparseAttentionKvSource::kAuxiliary, - validation_bitmap, validation_bitmap_words, use_shared_bitmap, error_flag); + validation_bitmap, validation_hash_entries, use_shared_hash_table, error_flag, + key_scale, value_scale, parameters.kv_num_heads * parameters.head_size, + committed_length, committed_length_stride); CUDA_RETURN_IF_ERROR(cudaGetLastError()); if (!copy_result_to_host) { @@ -1035,6 +1966,12 @@ Status ValidateDynamicSparseAttentionOnDevice( case kDynamicSparseAttentionInvalidPosition: message = "a rotary position is outside the rotary cache"; break; + case kDynamicSparseAttentionInvalidCommittedLength: + message = "committed_length is inconsistent with seqlens_k, sequence_length, or cache_capacity"; + break; + case kDynamicSparseAttentionInvalidCacheScale: + message = "INT8 cache scales must be positive and finite"; + break; default: break; } @@ -1059,7 +1996,7 @@ Status LaunchDynamicSparseAttention( static_cast(parameters.batch_size) * parameters.kv_num_heads * parameters.cache_capacity * parameters.head_size; const size_t cache_bytes = - SafeInt(cache_elements) * sizeof(T); + SafeInt(cache_elements) * (data.quantized_cache ? 1 : sizeof(T)); ORT_RETURN_IF_NOT( data.past_key == nullptr || parameters.past_cache_capacity == parameters.cache_capacity, @@ -1111,7 +2048,7 @@ Status LaunchDynamicSparseAttention( parameters.kv_hidden_size, packed_stride, parameters.cache_capacity, parameters.rotary_dim, parameters.rotary_max_position, parameters.rotary_offset, parameters.rotary_interleaved, parameters.is_packed_qkv, - parameters.qk_norm_epsilon); + parameters.qk_norm_epsilon, data.quantized_cache, data.key_scale, data.value_scale); CUDA_RETURN_IF_ERROR(cudaGetLastError()); ORT_RETURN_IF_ERROR(CheckBlockCount(query_blocks, "attention")); @@ -1125,43 +2062,237 @@ Status LaunchDynamicSparseAttention( const int candidate_capacity = static_cast(candidate_capacity_64); const int fused_threads = threads < 128 && max_threads_per_block >= 128 ? 128 : threads; const size_t fused_shared_bytes = - (2 * static_cast(candidate_capacity) + static_cast(fused_threads)) * sizeof(float) + + (static_cast(candidate_capacity) + static_cast(fused_threads)) * sizeof(float) + static_cast(parameters.head_size) * sizeof(T); + const int grouped_decode_heads_per_block = GetGroupedDecodeHeadsPerBlock(parameters); + const int grouped_decode_threads = 32 * grouped_decode_heads_per_block; + constexpr int kGroupedPrefillThreads = 32 * kGroupedPrefillQueryHeadsPerBlock; + const bool grouped_decode_threads_supported = grouped_decode_threads <= max_threads_per_block; + const bool grouped_prefill_threads_supported = kGroupedPrefillThreads <= max_threads_per_block; + const bool use_grouped_decode = + grouped_decode_threads_supported && + UseGroupedDecodeAttention(parameters, sizeof(T), max_shared_memory_per_block, data.quantized_cache); bool use_prefill_kernel = false; if constexpr (std::is_same_v) { - const bool enabled = ParseEnvironmentVariableWithDefault("ORT_DSA_PREFILL_KERNEL", 1) != 0; - // max_shared_memory_per_block is the default 48 KB limit; the kernel opts in to more and requires SM80+. - int device = 0; - int cc_major = 0; - int optin_shared = 0; - CUDA_RETURN_IF_ERROR(cudaGetDevice(&device)); - CUDA_RETURN_IF_ERROR(cudaDeviceGetAttribute(&cc_major, cudaDevAttrComputeCapabilityMajor, device)); - CUDA_RETURN_IF_ERROR(cudaDeviceGetAttribute(&optin_shared, cudaDevAttrMaxSharedMemoryPerBlockOptin, device)); - constexpr int kMaxGridDimYZ = 65535; - use_prefill_kernel = - enabled && cc_major >= 8 && parameters.sequence_length >= 8 && - parameters.head_size == dsa_prefill::kHeadDim && !local_plus_selected && - parameters.selected_kv_source == DynamicSparseAttentionKvSource::kMain && - !parameters.use_smooth_softmax && parameters.max_selected > 0 && - parameters.kv_num_heads > 0 && parameters.kv_num_heads <= kMaxGridDimYZ && - parameters.num_heads % parameters.kv_num_heads == 0 && - parameters.num_heads / parameters.kv_num_heads <= dsa_prefill::kMaxGroup && - static_cast(optin_shared) >= dsa_prefill::kSharedBytes; - } - if (use_prefill_kernel) { + if (!use_grouped_decode) { + const bool enabled = ParseEnvironmentVariableWithDefault("ORT_DSA_PREFILL_KERNEL", 1) != 0; + // max_shared_memory_per_block is the default 48 KB limit; the kernel opts in to more and requires SM80+. + int device = 0; + int cc_major = 0; + int optin_shared = 0; + CUDA_RETURN_IF_ERROR(cudaGetDevice(&device)); + CUDA_RETURN_IF_ERROR(cudaDeviceGetAttribute(&cc_major, cudaDevAttrComputeCapabilityMajor, device)); + CUDA_RETURN_IF_ERROR(cudaDeviceGetAttribute(&optin_shared, cudaDevAttrMaxSharedMemoryPerBlockOptin, device)); + constexpr int kMaxGridDimYZ = 65535; + // The kernel issues 16-byte (INT8: 8-byte) cp.async copies from the caches, float4 loads of the INT8 + // scales and bfloat162 stores to the output. Tensors bound through IOBinding can start at any element + // offset, so misaligned buffers use the generic kernels instead of faulting. + const size_t cache_alignment = data.quantized_cache ? 8 : 16; + const bool buffers_aligned = + IsAligned(data.present_key, cache_alignment) && IsAligned(data.present_value, cache_alignment) && + IsAligned(data.output, 4) && + (!data.quantized_cache || (IsAligned(data.key_scale, 16) && IsAligned(data.value_scale, 16))); + use_prefill_kernel = + enabled && buffers_aligned && cc_major >= 8 && parameters.sequence_length >= 8 && + parameters.head_size == dsa_prefill::kHeadDim && !local_plus_selected && + parameters.selected_kv_source == DynamicSparseAttentionKvSource::kMain && + !parameters.use_smooth_softmax && parameters.max_selected > 0 && + parameters.kv_num_heads > 0 && parameters.kv_num_heads <= kMaxGridDimYZ && + parameters.num_heads % parameters.kv_num_heads == 0 && + parameters.num_heads / parameters.kv_num_heads <= dsa_prefill::kMaxGroup && + static_cast(optin_shared) >= dsa_prefill::kSharedBytes; + } + } + if (use_grouped_decode) { + const int split_count = GetGroupedDecodeSplitCount(parameters, data.quantized_cache); + const int64_t partial_blocks = query_blocks * split_count; + const int64_t grouped_blocks = + static_cast(parameters.batch_size) * parameters.sequence_length * parameters.kv_num_heads * + GetGroupedHeadTiles(parameters, grouped_decode_heads_per_block) * split_count; + ORT_RETURN_IF_ERROR(CheckBlockCount(partial_blocks, "grouped decode partials")); + ORT_RETURN_IF_ERROR(CheckBlockCount(grouped_blocks, "grouped decode attention")); + ORT_RETURN_IF_NOT(data.attention_workspace != nullptr, + "DynamicSparseAttention: grouped decode workspace is required."); + float* partial_max = data.attention_workspace; + float* partial_sum = partial_max + partial_blocks; + float* partial_output = partial_sum + partial_blocks; + const size_t grouped_shared_bytes = + GetGroupedAttentionSharedBytes(parameters, data.quantized_cache ? sizeof(float) : sizeof(T)); + if (data.quantized_cache) { + static const int vectorized_staging = + ParseEnvironmentVariableWithDefault("ORT_DSA_INT8_VECTOR_STAGING", 1); + ORT_ENFORCE(vectorized_staging == 0 || vectorized_staging == 1, + "ORT_DSA_INT8_VECTOR_STAGING must be 0 or 1."); + // Vector staging loads 16-byte auxiliary K/V and scale chunks and 8-byte INT8 cache chunks. Tensors bound + // through IOBinding can start at any element offset, so misaligned buffers use scalar staging. + const bool use_vector_staging = + vectorized_staging != 0 && + IsAligned(data.key_scale, 16) && IsAligned(data.value_scale, 16) && + IsAligned(data.auxiliary_key, 16) && IsAligned(data.auxiliary_value, 16) && + IsAligned(data.present_key, 8) && IsAligned(data.present_value, 8); + const auto launch = [&](auto heads_tag, auto vectorized) { + constexpr int kHeads = decltype(heads_tag)::value; + GroupedDecodeDynamicSparseAttentionKernel + <<(grouped_blocks), 32 * kHeads, grouped_shared_bytes, stream>>>( + data.prepared_query, data.present_key, data.present_value, + data.auxiliary_key, data.auxiliary_value, data.selected_indices, + data.selected_counts, data.seqlens_k, data.head_sink, + partial_max, partial_sum, partial_output, + static_cast(grouped_blocks), split_count, parameters.sequence_length, + parameters.num_heads, parameters.kv_num_heads, parameters.head_size, + parameters.cache_capacity, parameters.auxiliary_sequence_length, + parameters.max_selected, parameters.local_window_size, parameters.scale, + local_plus_selected, + parameters.selected_kv_source == DynamicSparseAttentionKvSource::kAuxiliary, + parameters.use_smooth_softmax, data.key_scale, data.value_scale); + }; + if (grouped_decode_heads_per_block == 12) { + if (use_vector_staging) { + launch(std::integral_constant{}, std::true_type{}); + } else { + launch(std::integral_constant{}, std::false_type{}); + } + } else { + if (use_vector_staging) { + launch(std::integral_constant{}, std::true_type{}); + } else { + launch(std::integral_constant{}, std::false_type{}); + } + } + } else if (parameters.cache_capacity <= 131072) { + if (grouped_decode_heads_per_block == 12) { + GroupedDecodeDynamicSparseAttentionKernel + <<(grouped_blocks), 32 * 12, grouped_shared_bytes, stream>>>( + data.prepared_query, data.present_key, data.present_value, + data.auxiliary_key, data.auxiliary_value, data.selected_indices, + data.selected_counts, data.seqlens_k, data.head_sink, + partial_max, partial_sum, partial_output, + static_cast(grouped_blocks), split_count, parameters.sequence_length, + parameters.num_heads, parameters.kv_num_heads, parameters.head_size, + parameters.cache_capacity, parameters.auxiliary_sequence_length, + parameters.max_selected, parameters.local_window_size, parameters.scale, + local_plus_selected, + parameters.selected_kv_source == DynamicSparseAttentionKvSource::kAuxiliary, + parameters.use_smooth_softmax, nullptr, nullptr); + } else { + GroupedDecodeDynamicSparseAttentionKernel + <<(grouped_blocks), grouped_decode_threads, grouped_shared_bytes, stream>>>( + data.prepared_query, data.present_key, data.present_value, + data.auxiliary_key, data.auxiliary_value, data.selected_indices, + data.selected_counts, data.seqlens_k, data.head_sink, + partial_max, partial_sum, partial_output, + static_cast(grouped_blocks), split_count, parameters.sequence_length, + parameters.num_heads, parameters.kv_num_heads, parameters.head_size, + parameters.cache_capacity, parameters.auxiliary_sequence_length, + parameters.max_selected, parameters.local_window_size, parameters.scale, + local_plus_selected, + parameters.selected_kv_source == DynamicSparseAttentionKvSource::kAuxiliary, + parameters.use_smooth_softmax, nullptr, nullptr); + } + } else { + if (grouped_decode_heads_per_block == 12) { + GroupedDecodeDynamicSparseAttentionKernel + <<(grouped_blocks), 32 * 12, grouped_shared_bytes, stream>>>( + data.prepared_query, data.present_key, data.present_value, + data.auxiliary_key, data.auxiliary_value, data.selected_indices, + data.selected_counts, data.seqlens_k, data.head_sink, + partial_max, partial_sum, partial_output, + static_cast(grouped_blocks), split_count, parameters.sequence_length, + parameters.num_heads, parameters.kv_num_heads, parameters.head_size, + parameters.cache_capacity, parameters.auxiliary_sequence_length, + parameters.max_selected, parameters.local_window_size, parameters.scale, + local_plus_selected, + parameters.selected_kv_source == DynamicSparseAttentionKvSource::kAuxiliary, + parameters.use_smooth_softmax, nullptr, nullptr); + } else { + GroupedDecodeDynamicSparseAttentionKernel + <<(grouped_blocks), grouped_decode_threads, grouped_shared_bytes, stream>>>( + data.prepared_query, data.present_key, data.present_value, + data.auxiliary_key, data.auxiliary_value, data.selected_indices, + data.selected_counts, data.seqlens_k, data.head_sink, + partial_max, partial_sum, partial_output, + static_cast(grouped_blocks), split_count, parameters.sequence_length, + parameters.num_heads, parameters.kv_num_heads, parameters.head_size, + parameters.cache_capacity, parameters.auxiliary_sequence_length, + parameters.max_selected, parameters.local_window_size, parameters.scale, + local_plus_selected, + parameters.selected_kv_source == DynamicSparseAttentionKvSource::kAuxiliary, + parameters.use_smooth_softmax, nullptr, nullptr); + } + } + CUDA_RETURN_IF_ERROR(cudaGetLastError()); + + if (split_count <= 32) { + static const int shared_scales = + ParseEnvironmentVariableWithDefault("ORT_DSA_SHARED_SCALE_MERGE", 1); + ORT_ENFORCE(shared_scales == 0 || shared_scales == 1, + "ORT_DSA_SHARED_SCALE_MERGE must be 0 or 1."); + const auto merge = [&](auto specialized) { + MergeGroupedDecodeDynamicSparseAttentionKernel + <<(query_blocks), threads, 0, stream>>>( + partial_max, partial_sum, partial_output, data.output, + static_cast(query_blocks), split_count, parameters.head_size); + }; + if (shared_scales) { + merge(std::true_type{}); + } else { + merge(std::false_type{}); + } + } else { + const size_t merge_shared_bytes = static_cast(threads) * sizeof(float); + MergeDynamicSparseAttentionKernel<<(query_blocks), threads, merge_shared_bytes, stream>>>( + partial_max, partial_sum, partial_output, data.output, + static_cast(query_blocks), split_count, parameters.head_size); + } + } else if (use_prefill_kernel) { if constexpr (std::is_same_v) { - CUDA_RETURN_IF_ERROR(cudaFuncSetAttribute( - dsa_prefill::DynamicSparseAttentionPrefillKernel, cudaFuncAttributeMaxDynamicSharedMemorySize, - static_cast(dsa_prefill::kSharedBytes))); const dim3 grid(static_cast(parameters.batch_size * parameters.sequence_length), static_cast(parameters.kv_num_heads)); - dsa_prefill::DynamicSparseAttentionPrefillKernel<<>>( - data.prepared_query, data.present_key, data.present_value, data.selected_indices, data.selected_counts, - data.seqlens_k, data.output, parameters.sequence_length, parameters.num_heads, parameters.kv_num_heads, - parameters.cache_capacity, parameters.max_selected, parameters.scale); + if (data.quantized_cache) { + CUDA_RETURN_IF_ERROR(cudaFuncSetAttribute( + dsa_prefill::DynamicSparseAttentionPrefillKernel, cudaFuncAttributeMaxDynamicSharedMemorySize, + static_cast(dsa_prefill::kSharedBytes))); + dsa_prefill::DynamicSparseAttentionPrefillKernel<<>>( + data.prepared_query, data.present_key, data.present_value, data.selected_indices, data.selected_counts, + data.seqlens_k, data.output, parameters.sequence_length, parameters.num_heads, parameters.kv_num_heads, + parameters.cache_capacity, parameters.max_selected, parameters.scale, data.key_scale, data.value_scale); + } else { + CUDA_RETURN_IF_ERROR(cudaFuncSetAttribute( + dsa_prefill::DynamicSparseAttentionPrefillKernel, cudaFuncAttributeMaxDynamicSharedMemorySize, + static_cast(dsa_prefill::kSharedBytes))); + dsa_prefill::DynamicSparseAttentionPrefillKernel<<>>( + data.prepared_query, data.present_key, data.present_value, data.selected_indices, data.selected_counts, + data.seqlens_k, data.output, parameters.sequence_length, parameters.num_heads, parameters.kv_num_heads, + parameters.cache_capacity, parameters.max_selected, parameters.scale, nullptr, nullptr); + } } - } else if (UseFusedAttention(parameters, sizeof(T), max_shared_memory_per_block)) { + } else if (!data.quantized_cache && grouped_prefill_threads_supported && + UseGroupedPrefillAttention(parameters, sizeof(T), max_shared_memory_per_block)) { + const int64_t grouped_blocks = + static_cast(parameters.batch_size) * parameters.sequence_length * + parameters.kv_num_heads * GetGroupedHeadTiles(parameters, kGroupedPrefillQueryHeadsPerBlock); + ORT_RETURN_IF_ERROR(CheckBlockCount(grouped_blocks, "grouped prefill attention")); + const size_t grouped_shared_bytes = + GetGroupedAttentionSharedBytes(parameters, sizeof(T)); + GroupedPrefillDynamicSparseAttentionKernel + <<(grouped_blocks), kGroupedPrefillThreads, grouped_shared_bytes, stream>>>( + data.prepared_query, data.present_key, data.present_value, + data.auxiliary_key, data.auxiliary_value, data.selected_indices, + data.selected_counts, data.seqlens_k, data.head_sink, data.output, + static_cast(grouped_blocks), parameters.sequence_length, parameters.num_heads, + parameters.kv_num_heads, parameters.head_size, parameters.cache_capacity, + parameters.auxiliary_sequence_length, parameters.max_selected, + parameters.local_window_size, parameters.scale, local_plus_selected, + parameters.selected_kv_source == DynamicSparseAttentionKvSource::kAuxiliary, + parameters.use_smooth_softmax); + } else if (data.quantized_cache + ? candidate_capacity <= kSplitCandidateSize + : UseFusedAttention(parameters, sizeof(T), max_shared_memory_per_block)) { + ORT_RETURN_IF_NOT(fused_shared_bytes <= max_shared_memory_per_block, + "DynamicSparseAttention: quantized cache attention exceeds shared-memory capacity."); FusedDynamicSparseAttentionKernel<<(query_blocks), fused_threads, fused_shared_bytes, stream>>>( data.prepared_query, data.present_key, data.present_value, data.auxiliary_key, data.auxiliary_value, data.selected_indices, @@ -1172,7 +2303,7 @@ Status LaunchDynamicSparseAttention( parameters.local_window_size, candidate_capacity, parameters.scale, local_plus_selected, parameters.selected_kv_source == DynamicSparseAttentionKvSource::kAuxiliary, - parameters.use_smooth_softmax); + parameters.use_smooth_softmax, data.quantized_cache, data.key_scale, data.value_scale); } else { const int split_count = GetAttentionSplitCount(parameters); const int64_t partial_blocks = query_blocks * split_count; @@ -1184,7 +2315,7 @@ Status LaunchDynamicSparseAttention( float* partial_output = split_count == 1 ? nullptr : partial_sum + partial_blocks; const int split_threads = threads < 128 && max_threads_per_block >= 128 ? 128 : threads; const size_t split_shared_bytes = - (2 * static_cast(kSplitCandidateSize) + static_cast(split_threads)) * sizeof(float) + + (static_cast(kSplitCandidateSize) + static_cast(split_threads)) * sizeof(float) + static_cast(parameters.head_size) * sizeof(T); ORT_RETURN_IF_NOT(split_shared_bytes < max_shared_memory_per_block, "DynamicSparseAttention: split attention exceeds the CUDA shared-memory limit."); @@ -1200,7 +2331,7 @@ Status LaunchDynamicSparseAttention( parameters.local_window_size, parameters.scale, local_plus_selected, parameters.selected_kv_source == DynamicSparseAttentionKvSource::kAuxiliary, - parameters.use_smooth_softmax); + parameters.use_smooth_softmax, data.quantized_cache, data.key_scale, data.value_scale); CUDA_RETURN_IF_ERROR(cudaGetLastError()); if (split_count > 1) { diff --git a/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_impl.h b/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_impl.h index d60bc7b9c69f7..baa7e566eec17 100644 --- a/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_impl.h +++ b/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_impl.h @@ -22,6 +22,8 @@ enum DynamicSparseAttentionValidationError : int32_t { kDynamicSparseAttentionNonCausalIndex = 6, kDynamicSparseAttentionInvalidSequenceLength = 7, kDynamicSparseAttentionInvalidPosition = 8, + kDynamicSparseAttentionInvalidCommittedLength = 9, + kDynamicSparseAttentionInvalidCacheScale = 10, }; template @@ -47,6 +49,9 @@ struct DynamicSparseAttentionData { T* present_value = nullptr; T* output = nullptr; float* attention_workspace = nullptr; + bool quantized_cache = false; + const float* key_scale = nullptr; + const float* value_scale = nullptr; }; size_t GetDynamicSparseAttentionValidationWorkspaceSize( @@ -56,7 +61,8 @@ size_t GetDynamicSparseAttentionValidationWorkspaceSize( size_t GetDynamicSparseAttentionWorkspaceSize( const DynamicSparseAttentionParameters& parameters, size_t element_size, - size_t max_shared_memory_per_block); + size_t max_shared_memory_per_block, + bool quantized_cache = false); Status ValidateDynamicSparseAttentionOnDevice( cudaStream_t stream, @@ -68,7 +74,11 @@ Status ValidateDynamicSparseAttentionOnDevice( int32_t* error_flag, uint32_t* validation_bitmap, size_t max_shared_memory_per_block, - bool copy_result_to_host); + bool copy_result_to_host, + const float* key_scale = nullptr, + const float* value_scale = nullptr, + const int32_t* committed_length = nullptr, + int committed_length_stride = 0); template Status LaunchDynamicSparseAttention( diff --git a/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_prefill.cuh b/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_prefill.cuh index 03cc1f084793e..a353787bf4b41 100644 --- a/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_prefill.cuh +++ b/onnxruntime/contrib_ops/cuda/bert/dynamic_sparse_attention_prefill.cuh @@ -32,7 +32,8 @@ constexpr size_t kQBytes = kMaxGroup * kRowStride * sizeof(__nv_bfloat16); constexpr size_t kKBytes = kTile * kRowStride * sizeof(__nv_bfloat16); constexpr size_t kSBytes = kMaxGroup * kSStride * sizeof(float); constexpr size_t kPBytes = kMaxGroup * kPStride * sizeof(__nv_bfloat16); -constexpr size_t kSharedBytes = kQBytes + 2 * kKBytes + kSBytes + kPBytes + 2 * kMaxGroup * sizeof(float); +// sQ is only used to load qf[][] into registers before the tile loop and aliases sK thereafter. +constexpr size_t kSharedBytes = 2 * kKBytes + kSBytes + kPBytes + 2 * kMaxGroup * sizeof(float); #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800 __device__ __forceinline__ uint32_t SmemAddr(const void* p) { @@ -42,6 +43,9 @@ __device__ __forceinline__ uint32_t SmemAddr(const void* p) { __device__ __forceinline__ void CpAsync16(void* smem_dst, const void* gmem_src) { asm volatile("cp.async.cg.shared.global [%0], [%1], 16;\n" ::"r"(SmemAddr(smem_dst)), "l"(gmem_src)); } +__device__ __forceinline__ void CpAsync8(void* smem_dst, const void* gmem_src) { + asm volatile("cp.async.ca.shared.global [%0], [%1], 8;\n" ::"r"(SmemAddr(smem_dst)), "l"(gmem_src)); +} __device__ __forceinline__ void CpAsyncCommit() { asm volatile("cp.async.commit_group;\n" ::); } template __device__ __forceinline__ void CpAsyncWait() { @@ -65,11 +69,31 @@ __device__ __forceinline__ void MmaBf16(float (&c)[4], const uint32_t (&a)[4], u : "+f"(c[0]), "+f"(c[1]), "+f"(c[2]), "+f"(c[3]) : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "r"(b0), "r"(b1)); } + +__device__ __forceinline__ uint4 DequantizeInt8x8ToBf16(uint2 packed, float4 s0, float4 s1) { + const int32_t w0 = static_cast(packed.x); + const int32_t w1 = static_cast(packed.y); + const float b0 = static_cast(static_cast(w0 << 24) >> 24) * s0.x; + const float b1 = static_cast(static_cast(w0 << 16) >> 24) * s0.y; + const float b2 = static_cast(static_cast(w0 << 8) >> 24) * s0.z; + const float b3 = static_cast(static_cast(w0) >> 24) * s0.w; + const float b4 = static_cast(static_cast(w1 << 24) >> 24) * s1.x; + const float b5 = static_cast(static_cast(w1 << 16) >> 24) * s1.y; + const float b6 = static_cast(static_cast(w1 << 8) >> 24) * s1.z; + const float b7 = static_cast(static_cast(w1) >> 24) * s1.w; + uint4 out; + *reinterpret_cast<__nv_bfloat162*>(&out.x) = __floats2bfloat162_rn(b0, b1); + *reinterpret_cast<__nv_bfloat162*>(&out.y) = __floats2bfloat162_rn(b2, b3); + *reinterpret_cast<__nv_bfloat162*>(&out.z) = __floats2bfloat162_rn(b4, b5); + *reinterpret_cast<__nv_bfloat162*>(&out.w) = __floats2bfloat162_rn(b6, b7); + return out; +} #endif // grid = (batch * sequence_length, kv_num_heads), block = kThreads, dynamic smem = kSharedBytes. // query: [rows, num_heads, 256] (already normalized / rotated); key, value: [batch, kv_heads, capacity, 256]. -__global__ void __launch_bounds__(kThreads, 2) DynamicSparseAttentionPrefillKernel( +template +__global__ void __launch_bounds__(kThreads, 3) DynamicSparseAttentionPrefillKernel( const __nv_bfloat16* __restrict__ query, const __nv_bfloat16* __restrict__ key, const __nv_bfloat16* __restrict__ value, @@ -82,11 +106,13 @@ __global__ void __launch_bounds__(kThreads, 2) DynamicSparseAttentionPrefillKern int kv_num_heads, int capacity, int max_selected, - float scale) { + float scale, + const float* __restrict__ key_scale = nullptr, + const float* __restrict__ value_scale = nullptr) { #if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800 extern __shared__ __align__(16) unsigned char smem_raw[]; - __nv_bfloat16* sQ = reinterpret_cast<__nv_bfloat16*>(smem_raw); - __nv_bfloat16* sK = sQ + kMaxGroup * kRowStride; + __nv_bfloat16* sK = reinterpret_cast<__nv_bfloat16*>(smem_raw); + __nv_bfloat16* sQ = sK; __nv_bfloat16* sV = sK + kTile * kRowStride; float* sS = reinterpret_cast(sV + kTile * kRowStride); __nv_bfloat16* sP = reinterpret_cast<__nv_bfloat16*>(sS + kMaxGroup * kSStride); @@ -108,8 +134,23 @@ __global__ void __launch_bounds__(kThreads, 2) DynamicSparseAttentionPrefillKern const int total = valid_length ? static_cast(total_length) : 0; const int count = valid_length ? max(0, min(selected_counts[row], max_selected)) : 0; const int32_t* row_indices = selected_indices + static_cast(row) * max_selected; - const __nv_bfloat16* key_base = key + (static_cast(batch) * kv_num_heads + kv_head) * capacity * kHeadDim; - const __nv_bfloat16* value_base = value + (static_cast(batch) * kv_num_heads + kv_head) * capacity * kHeadDim; + const int64_t kv_head_offset = (static_cast(batch) * kv_num_heads + kv_head) * capacity * kHeadDim; + const __nv_bfloat16* key_base = key + kv_head_offset; + const __nv_bfloat16* value_base = value + kv_head_offset; + const int8_t* key_base_i8 = reinterpret_cast(key) + kv_head_offset; + const int8_t* value_base_i8 = reinterpret_cast(value) + kv_head_offset; + float4 ks0 = make_float4(1.0f, 1.0f, 1.0f, 1.0f); + float4 ks1 = make_float4(1.0f, 1.0f, 1.0f, 1.0f); + float4 vs0 = make_float4(1.0f, 1.0f, 1.0f, 1.0f); + float4 vs1 = make_float4(1.0f, 1.0f, 1.0f, 1.0f); + if constexpr (kQuantizedCache) { + const float* ks_ptr = key_scale + kv_head * kHeadDim + lane * 8; + const float* vs_ptr = value_scale + kv_head * kHeadDim + lane * 8; + ks0 = *reinterpret_cast(ks_ptr); + ks1 = *reinterpret_cast(ks_ptr + 4); + vs0 = *reinterpret_cast(vs_ptr); + vs1 = *reinterpret_cast(vs_ptr + 4); + } // Stage the group's query heads; the padding rows are zero so their (ignored) scores stay finite. const __nv_bfloat16* query_group = query + (static_cast(row) * num_heads + kv_head * group) * kHeadDim; @@ -129,6 +170,7 @@ __global__ void __launch_bounds__(kThreads, 2) DynamicSparseAttentionPrefillKern for (int k = 0; k < 16; ++k) { LdmatrixX4(qf[k], sQ + (lane & 15) * kRowStride + k * 16 + (lane >> 4) * 8); } + __syncthreads(); auto candidate_index = [&](int candidate) -> int { if (candidate >= count) return -1; @@ -136,26 +178,46 @@ __global__ void __launch_bounds__(kThreads, 2) DynamicSparseAttentionPrefillKern return (index >= 0 && index < total && index <= query_position && index < capacity) ? index : -1; }; - auto issue_rows = [&](__nv_bfloat16* dst, const __nv_bfloat16* base, int tile) { + auto issue_rows = [&](__nv_bfloat16* dst, const __nv_bfloat16* base, const int8_t* base_i8, int tile) { // 4 candidate rows per pass (one per warp), 32 lanes x 16 B = one 512 B row. -#pragma unroll 4 +#pragma unroll for (int it = 0; it < kTile / 4; ++it) { const int cand_in_tile = it * 4 + warp; const int index = candidate_index(tile * kTile + cand_in_tile); __nv_bfloat16* row_dst = dst + cand_in_tile * kRowStride + lane * 8; if (index >= 0) { - CpAsync16(row_dst, base + static_cast(index) * kHeadDim + lane * 8); + if constexpr (kQuantizedCache) { + CpAsync8(row_dst, base_i8 + static_cast(index) * kHeadDim + lane * 8); + } else { + CpAsync16(row_dst, base + static_cast(index) * kHeadDim + lane * 8); + } } else { - *reinterpret_cast(row_dst) = make_uint4(0, 0, 0, 0); + if constexpr (kQuantizedCache) { + *reinterpret_cast(row_dst) = make_uint2(0, 0); + } else { + *reinterpret_cast(row_dst) = make_uint4(0, 0, 0, 0); + } + } + } + }; + + auto dequantize_rows = [&](__nv_bfloat16* dst, float4 sc0, float4 sc1) { + if constexpr (kQuantizedCache) { +#pragma unroll + for (int it = 0; it < kTile / 4; ++it) { + const int cand_in_tile = it * 4 + warp; + __nv_bfloat16* row_dst = dst + cand_in_tile * kRowStride + lane * 8; + const uint2 packed = *reinterpret_cast(row_dst); + *reinterpret_cast(row_dst) = DequantizeInt8x8ToBf16(packed, sc0, sc1); } } }; const int tiles = (count + kTile - 1) / kTile; if (tiles > 0) { - issue_rows(sK, key_base, 0); + issue_rows(sK, key_base, key_base_i8, 0); CpAsyncCommit(); - issue_rows(sV, value_base, 0); + issue_rows(sV, value_base, value_base_i8, 0); CpAsyncCommit(); } @@ -172,6 +234,7 @@ __global__ void __launch_bounds__(kThreads, 2) DynamicSparseAttentionPrefillKern for (int tile = 0; tile < tiles; ++tile) { CpAsyncWait<1>(); // K(tile) has landed, V(tile) may still be in flight + dequantize_rows(sK, ks0, ks1); __syncthreads(); // S = Q K^T for this warp's 16 candidate columns. @@ -197,7 +260,7 @@ __global__ void __launch_bounds__(kThreads, 2) DynamicSparseAttentionPrefillKern __syncthreads(); // scores visible; sK is free if (tile + 1 < tiles) { - issue_rows(sK, key_base, tile + 1); + issue_rows(sK, key_base, key_base_i8, tile + 1); } CpAsyncCommit(); // K(tile+1) (possibly empty) @@ -247,6 +310,7 @@ __global__ void __launch_bounds__(kThreads, 2) DynamicSparseAttentionPrefillKern } CpAsyncWait<1>(); // V(tile) has landed (K(tile+1) may be in flight) + dequantize_rows(sV, vs0, vs1); __syncthreads(); // O += P V for this warp's 64 output dimensions. @@ -266,7 +330,7 @@ __global__ void __launch_bounds__(kThreads, 2) DynamicSparseAttentionPrefillKern __syncthreads(); // sV and sP are free if (tile + 1 < tiles) { - issue_rows(sV, value_base, tile + 1); + issue_rows(sV, value_base, value_base_i8, tile + 1); } CpAsyncCommit(); // V(tile+1) (possibly empty) } @@ -304,6 +368,8 @@ __global__ void __launch_bounds__(kThreads, 2) DynamicSparseAttentionPrefillKern (void)capacity; (void)max_selected; (void)scale; + (void)key_scale; + (void)value_scale; #endif } diff --git a/onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc b/onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc index e8f435f99b4d7..547c0df862948 100644 --- a/onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc +++ b/onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc @@ -280,9 +280,10 @@ class CUDA_MS_OP_TYPED_CLASS_NAME(1, BFloat16, SparseAttention); class CUDA_MS_OP_TYPED_CLASS_NAME(1, float, SparseAttentionIndexer); class CUDA_MS_OP_TYPED_CLASS_NAME(1, MLFloat16, SparseAttentionIndexer); class CUDA_MS_OP_TYPED_CLASS_NAME(1, BFloat16, SparseAttentionIndexer); -class CUDA_MS_OP_TYPED_CLASS_NAME(1, float, PackedSparseAttentionIndexer); -class CUDA_MS_OP_TYPED_CLASS_NAME(1, MLFloat16, PackedSparseAttentionIndexer); -class CUDA_MS_OP_TYPED_CLASS_NAME(1, BFloat16, PackedSparseAttentionIndexer); +class CUDA_MS_OP_TYPED_CLASS_NAME(1, float_float, PackedSparseAttentionIndexer); +class CUDA_MS_OP_TYPED_CLASS_NAME(1, MLFloat16_MLFloat16, PackedSparseAttentionIndexer); +class CUDA_MS_OP_TYPED_CLASS_NAME(1, BFloat16_BFloat16, PackedSparseAttentionIndexer); +class CUDA_MS_OP_TYPED_CLASS_NAME(1, BFloat16_float, PackedSparseAttentionIndexer); class CUDA_MS_OP_CLASS_NAME(1, PackedSparseAttentionIndexerMerge); class CUDA_MS_OP_THREE_TYPED_CLASS_NAME(1, uint8_t, float, int32_t, GatherBlockQuantized); @@ -653,9 +654,10 @@ Status RegisterCudaContribKernels(KernelRegistry& kernel_registry) { BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, - BuildKernelCreateInfo, - BuildKernelCreateInfo, - BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, diff --git a/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer.cc b/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer.cc index 2b192e7245ae6..d6008de18268f 100644 --- a/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer.cc +++ b/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include "contrib_ops/cuda/sparse/packed_sparse_attention_indexer_impl.h" #include "core/providers/cuda/cuda_common.h" @@ -19,15 +20,16 @@ namespace cuda { using namespace onnxruntime::cuda; namespace psai = onnxruntime::contrib::packed_sparse_attention_indexer; -#define REGISTER_KERNEL_TYPED(T) \ +#define REGISTER_KERNEL_TYPED(K, T, TState) \ ONNX_OPERATOR_TYPED_KERNEL_EX( \ PackedSparseAttentionIndexer, \ kMSDomain, \ 1, \ - T, \ + K, \ kCudaExecutionProvider, \ (*KernelDefBuilder::Create()) \ .TypeConstraint("T", DataTypeImpl::GetTensorType()) \ + .TypeConstraint("T_KEY", DataTypeImpl::GetTensorType()) \ .TypeConstraint("I", DataTypeImpl::GetTensorType()) \ .TypeConstraint("M", DataTypeImpl::GetTensorType()) \ .InputMemoryType(OrtMemTypeCPUInput, psai::kStateUpdateActive) \ @@ -36,14 +38,13 @@ namespace psai = onnxruntime::contrib::packed_sparse_attention_indexer; .MayInplace(psai::kMergedCounts, psai::kSelectedCounts) \ .MayInplace(psai::kMergedStatus, psai::kIndexShareStatus) \ .MayInplace(psai::kPastKeyState, psai::kPresentKeyState) \ - .MayInplace(psai::kPastKvBuffer, psai::kPresentKvBuffer) \ - .MayInplace(psai::kPastGateBuffer, psai::kPresentGateBuffer) \ .MayInplace(psai::kPastStateLengths, psai::kPresentStateLengths), \ - PackedSparseAttentionIndexer); + PackedSparseAttentionIndexer); -REGISTER_KERNEL_TYPED(float) -REGISTER_KERNEL_TYPED(MLFloat16) -REGISTER_KERNEL_TYPED(BFloat16) +REGISTER_KERNEL_TYPED(float_float, float, float) +REGISTER_KERNEL_TYPED(MLFloat16_MLFloat16, MLFloat16, MLFloat16) +REGISTER_KERNEL_TYPED(BFloat16_BFloat16, BFloat16, BFloat16) +REGISTER_KERNEL_TYPED(BFloat16_float, BFloat16, float) #undef REGISTER_KERNEL_TYPED @@ -100,13 +101,17 @@ Status CheckRotaryCache(const Tensor* cos_cache, const Tensor* sin_cache, int64_ } // namespace -template -PackedSparseAttentionIndexer::PackedSparseAttentionIndexer(const OpKernelInfo& info) : CudaKernel(info) { +template +PackedSparseAttentionIndexer::PackedSparseAttentionIndexer(const OpKernelInfo& info) : CudaKernel(info) { std::string policy_mode; ORT_ENFORCE(info.GetAttr("policy_mode", &policy_mode).IsOK(), "PackedSparseAttentionIndexer: policy_mode is required"); ORT_ENFORCE(psai::TryParsePolicy(policy_mode, policy_), "PackedSparseAttentionIndexer: policy_mode must be '", psai::kPolicyModeQsa, "' or '", psai::kPolicyModeCsa, "', got '", policy_mode, "'"); + if constexpr (!std::is_same_v) { + ORT_ENFORCE(policy_ == psai::Policy::kQsa, + "PackedSparseAttentionIndexer: mixed input/state types are supported only for policy_mode 'qsa'"); + } ORT_ENFORCE(info.GetAttr("compress_ratio", &compress_ratio_).IsOK(), "PackedSparseAttentionIndexer: compress_ratio is required"); @@ -169,8 +174,8 @@ PackedSparseAttentionIndexer::PackedSparseAttentionIndexer(const OpKernelInfo "PackedSparseAttentionIndexer: max_output_entries must cover the original selection capacity"); } -template -Status PackedSparseAttentionIndexer::ComputeInternal(OpKernelContext* context) const { +template +Status PackedSparseAttentionIndexer::ComputeInternal(OpKernelContext* context) const { bool reuse = false; const Tensor* mode = context->InputCount() > psai::kIndexShareMode ? context->Input(psai::kIndexShareMode) @@ -239,9 +244,10 @@ Status PackedSparseAttentionIndexer::ComputeInternal(OpKernelContext* context return is_qsa ? ComputeQsa(context) : ComputeCsa(context); } -template -Status PackedSparseAttentionIndexer::ComputeQsa(OpKernelContext* context) const { +template +Status PackedSparseAttentionIndexer::ComputeQsa(OpKernelContext* context) const { using CudaT = typename OrtToCudaType::type; + using CudaStateT = typename OrtToCudaType::type; const Tensor* query = context->Input(psai::kQuery); const Tensor* key = context->Input(psai::kKey); @@ -399,7 +405,7 @@ Status PackedSparseAttentionIndexer::ComputeQsa(OpKernelContext* context) con const CudaT* key_data = has_separate_key ? reinterpret_cast(key->Data()) : query_data + query_width; - return LaunchQsaPackedSparseAttentionIndexer( + return LaunchQsaPackedSparseAttentionIndexer( Stream(context), params, query_data, key_data, @@ -410,14 +416,14 @@ Status PackedSparseAttentionIndexer::ComputeQsa(OpKernelContext* context) con cumulative_sequence_lengths->Data(), past_sequence_lengths->Data(), position_ids != nullptr ? position_ids->Data() : nullptr, - reinterpret_cast(past_key_state->Data()), + reinterpret_cast(past_key_state->Data()), reinterpret_cast(past_kv_buffer->Data()), past_state_lengths->Data(), state_update_capture_count != nullptr ? state_update_capture_count->Data() : nullptr, state_update_active == nullptr || state_update_active->Data()[0] != 0, selected_indices->MutableData(), selected_counts->MutableData(), - reinterpret_cast(present_key_state->MutableData()), + reinterpret_cast(present_key_state->MutableData()), reinterpret_cast(present_kv_buffer->MutableData()), present_state_lengths->MutableData(), state_update != nullptr ? reinterpret_cast(state_update->MutableData()) : nullptr, @@ -425,8 +431,8 @@ Status PackedSparseAttentionIndexer::ComputeQsa(OpKernelContext* context) con overflow_flags); } -template -Status PackedSparseAttentionIndexer::ComputeCsa(OpKernelContext* context, const bool state_only) const { +template +Status PackedSparseAttentionIndexer::ComputeCsa(OpKernelContext* context, const bool state_only) const { using CudaT = typename OrtToCudaType::type; const Tensor* query = context->Input(psai::kQuery); @@ -623,8 +629,8 @@ Status PackedSparseAttentionIndexer::ComputeCsa(OpKernelContext* context, con static_cast(compress_ratio_), false, overflow_flags); } -template -Status PackedSparseAttentionIndexer::ComputeReuse(OpKernelContext* context) const { +template +Status PackedSparseAttentionIndexer::ComputeReuse(OpKernelContext* context) const { const Tensor* query = context->Input(psai::kQuery); const Tensor* lengths = context->Input(psai::kPastSequenceLengths); const Tensor* indices = context->Input(psai::kMergedIndices); @@ -732,9 +738,10 @@ Status PackedSparseAttentionIndexer::ComputeReuse(OpKernelContext* context) c static_cast(state_capacity_), static_cast(compress_ratio_), is_qsa); } -template class PackedSparseAttentionIndexer; -template class PackedSparseAttentionIndexer; -template class PackedSparseAttentionIndexer; +template class PackedSparseAttentionIndexer; +template class PackedSparseAttentionIndexer; +template class PackedSparseAttentionIndexer; +template class PackedSparseAttentionIndexer; } // namespace cuda } // namespace contrib diff --git a/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer.h b/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer.h index 9330edc01b0a8..1ce56f0fd229f 100644 --- a/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer.h +++ b/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer.h @@ -11,7 +11,7 @@ namespace onnxruntime { namespace contrib { namespace cuda { -template +template class PackedSparseAttentionIndexer final : public onnxruntime::cuda::CudaKernel { public: explicit PackedSparseAttentionIndexer(const OpKernelInfo& info); diff --git a/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer_impl.cu b/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer_impl.cu index 3ef581066873f..1d57295963567 100644 --- a/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer_impl.cu +++ b/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer_impl.cu @@ -26,6 +26,7 @@ // See docs/contrib_ops/cuda/packed_sparse_attention_indexer.md for the full operator contract. #include "contrib_ops/cuda/sparse/packed_sparse_attention_indexer_impl.h" +#include "contrib_ops/cuda/sparse/qsa_topk.cuh" #include #include @@ -36,6 +37,7 @@ #include "contrib_ops/cpu/sparse/packed_sparse_attention_indexer_common.h" #include "contrib_ops/cuda/sparse/sparse_attention_indexer_device_math.cuh" +#include "core/platform/env_var_utils.h" #include "core/providers/cuda/cu_inc/cuda_type_helper.cuh" #include "core/providers/cuda/cu_inc/topk_warp_sort.cuh" @@ -52,11 +54,15 @@ namespace { constexpr int kThreads = 128; constexpr int kWarpSize = 32; constexpr int kBoundedTopKMax = 512; +static_assert(kBoundedTopKMax == qsa_topk::kTopK); constexpr int kBoundedTopKItemsPerThread = kBoundedTopKMax / kThreads; constexpr int kDistributedTopKMaxRows = 64; constexpr int kDistributedTopKMinBlocks = 2048; constexpr int kHierarchicalTileBlocks = 8; +constexpr int kHierarchicalTopKTileBlocks = 32; constexpr int kHierarchicalScoreThreads = 1024; +constexpr int kHierarchicalTileScoreThreads = 512; +constexpr int kHierarchicalScoreMaxBlocksPerRow = 128; constexpr int kHierarchicalMergeThreads = 256; constexpr int kQwenHeadSize = 128; constexpr int kQwenCompressRatio = 4; @@ -67,10 +73,8 @@ Status ConfigureDynamicSharedMemory(Kernel kernel, size_t bytes, const char* ker int device = 0; int max_bytes = 0; CUDA_RETURN_IF_ERROR(cudaGetDevice(&device)); - CUDA_RETURN_IF_ERROR( - cudaDeviceGetAttribute(&max_bytes, cudaDevAttrMaxSharedMemoryPerBlockOptin, device)); - ORT_RETURN_IF_ERROR(psai::ValidateDynamicSharedMemory( - bytes, static_cast(max_bytes), kernel_name)); + CUDA_RETURN_IF_ERROR(cudaDeviceGetAttribute(&max_bytes, cudaDevAttrMaxSharedMemoryPerBlockOptin, device)); + ORT_RETURN_IF_ERROR(psai::ValidateDynamicSharedMemory(bytes, static_cast(max_bytes), kernel_name)); return CUDA_CALL(cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, max_bytes)); } @@ -89,7 +93,7 @@ bool UseRaggedQsaPrefill(const PackedSparseAttentionIndexerParams& params) { } int GetHierarchicalTileCount(const PackedSparseAttentionIndexerParams& params) { - return (params.state_capacity + kHierarchicalTileBlocks - 1) / kHierarchicalTileBlocks; + return (params.state_capacity + kHierarchicalTopKTileBlocks - 1) / kHierarchicalTopKTileBlocks; } template @@ -134,17 +138,101 @@ __global__ void ElementwiseCopyKernel(const T* src, T* dst, int64_t count) { // policy_mode = "qsa" // --------------------------------------------------------------------------------------------- +template +__global__ void QsaUpdateStateBlocksKernel(const T* key, const T* key_norm_weight, const T* cos_cache, + const T* sin_cache, const int32_t* cumulative_sequence_lengths, + const int32_t* past_sequence_lengths, const int32_t* past_state_lengths, + const int32_t* state_update_capture_count, bool state_update_active, + TState* present_key_state, T* state_update, + PackedSparseAttentionIndexerParams params) { + extern __shared__ float shared[]; + float* pooled = shared; + float* reduction = shared + params.head_size; + + for (int b = static_cast(blockIdx.x); b < params.batch_size; b += static_cast(gridDim.x)) { + const int req_start = cumulative_sequence_lengths[b]; + const int req_end = cumulative_sequence_lengths[b + 1]; + const int raw_key_len = past_state_lengths[b * 2 + psai::kKeyStateLength]; + const int raw_buf_len = past_state_lengths[b * 2 + psai::kBufferLength]; + const int past_sequence_length = past_sequence_lengths[b]; + const bool invalid_metadata = + cumulative_sequence_lengths[0] != 0 || + cumulative_sequence_lengths[params.batch_size] != params.total_tokens || + req_start < 0 || req_end < req_start || req_end > params.total_tokens || + past_sequence_length < 0 || raw_key_len < 0 || raw_key_len > params.state_capacity || + raw_buf_len < 0 || raw_buf_len >= params.compress_ratio || + raw_key_len != past_sequence_length / params.compress_ratio || + raw_buf_len != past_sequence_length % params.compress_ratio; + const int req_len = invalid_metadata ? 0 : req_end - req_start; + const int old_key_len = min(max(raw_key_len, 0), params.state_capacity); + const int old_buf_len = min(max(raw_buf_len, 0), params.compress_ratio - 1); + const int pending = old_buf_len + req_len; + const int full_new_block_count = pending / params.compress_ratio; + const int capacity_left = params.state_capacity - old_key_len; + const bool rejected = invalid_metadata || full_new_block_count > capacity_left; + const int new_block_count = rejected ? 0 : full_new_block_count; + const bool capture_active = !rejected && state_update != nullptr && state_update_capture_count != nullptr && + state_update_active; + const int capture_count = capture_active + ? min(max(state_update_capture_count[b], 0), + min(req_len, params.state_update_capacity)) + : 0; + + for (int k = 1 + static_cast(blockIdx.y); k < new_block_count; k += static_cast(gridDim.y)) { + float sum_squares = 0.0f; + for (int d = static_cast(threadIdx.x); d < params.head_size; d += static_cast(blockDim.x)) { + float sum = 0.0f; + for (int t = 0; t < params.compress_ratio; ++t) { + const int virtual_pos = k * params.compress_ratio + t; + sum += to_float(key[(static_cast(req_start) + (virtual_pos - old_buf_len)) * + params.key_row_stride + + d]); + } + const float p = sum / static_cast(params.compress_ratio); + pooled[d] = p; + sum_squares += p * p; + } + sum_squares = SaiBlockSum(sum_squares, reduction); + const float inverse_rms = rsqrtf(sum_squares / static_cast(params.head_size) + params.epsilon); + for (int d = static_cast(threadIdx.x); d < params.head_size; d += static_cast(blockDim.x)) { + pooled[d] = pooled[d] * inverse_rms * to_float(key_norm_weight[d]); + } + __syncthreads(); + + const int entry = old_key_len + k; + const int rope_position = + SaiClampPosition(static_cast(entry) * params.compress_ratio, params.max_rotary_length); + const int64_t cache_offset = + (static_cast(params.cos_cache_batched ? b : 0) * params.max_rotary_length + rope_position) * + params.rotary_width; + const int64_t out_base = (static_cast(b) * params.state_capacity + entry) * params.head_size; + const int completion_token = (k + 1) * params.compress_ratio - old_buf_len - 1; + const int64_t capture_base = + (static_cast(b) * params.state_update_capacity + completion_token) * params.head_size; + for (int d = static_cast(threadIdx.x); d < params.head_size; d += static_cast(blockDim.x)) { + const float rot = SaiLeadingRope(pooled, params.rotary_width, cos_cache + cache_offset, + sin_cache + cache_offset, d); + present_key_state[out_base + d] = from_float(rot); + if (completion_token < capture_count) { + state_update[capture_base + d] = from_float(rot); + } + } + __syncthreads(); + } + } +} + // One block per request: forms every newly-closed compress_ratio block (mean-pool -> RMSNorm -> // leading RoPE -> append into fixed-capacity key_state) and publishes the raw trailing buffer. // Reads only past_kv_buffer / key (never present_kv_buffer / present_key_state), so it is correct // whether or not the present/past tensors are the same aliased allocation. -template +template __global__ void QsaUpdateStateKernel(const T* key, const T* key_norm_weight, const T* cos_cache, const T* sin_cache, const int32_t* cumulative_sequence_lengths, const int32_t* past_sequence_lengths, const T* past_kv_buffer, const int32_t* past_state_lengths, const int32_t* state_update_capture_count, bool state_update_active, - T* present_key_state, T* present_kv_buffer, + TState* present_key_state, T* present_kv_buffer, int32_t* present_state_lengths, T* state_update, int32_t* overflow_flags, PackedSparseAttentionIndexerParams params) { extern __shared__ float shared[]; @@ -210,7 +298,9 @@ __global__ void QsaUpdateStateKernel(const T* key, const T* key_norm_weight, con } __syncthreads(); - for (int k = 0; k < new_block_count; ++k) { + const int loop_limit = kFirstBlockOnly ? min(new_block_count, 1) : new_block_count; + for (int k = 0; k < loop_limit; ++k) { + float sum_squares = 0.0f; for (int d = static_cast(threadIdx.x); d < params.head_size; d += static_cast(blockDim.x)) { float sum = 0.0f; for (int t = 0; t < params.compress_ratio; ++t) { @@ -223,13 +313,9 @@ __global__ void QsaUpdateStateKernel(const T* key, const T* key_norm_weight, con params.key_row_stride + d]); } - pooled[d] = sum / static_cast(params.compress_ratio); - } - __syncthreads(); - - float sum_squares = 0.0f; - for (int d = static_cast(threadIdx.x); d < params.head_size; d += static_cast(blockDim.x)) { - sum_squares += pooled[d] * pooled[d]; + const float p = sum / static_cast(params.compress_ratio); + pooled[d] = p; + sum_squares += p * p; } sum_squares = SaiBlockSum(sum_squares, reduction); const float inverse_rms = rsqrtf(sum_squares / static_cast(params.head_size) + params.epsilon); @@ -244,24 +330,21 @@ __global__ void QsaUpdateStateKernel(const T* key, const T* key_norm_weight, con const int64_t cache_offset = (static_cast(params.cos_cache_batched ? b : 0) * params.max_rotary_length + rope_position) * params.rotary_width; - for (int d = static_cast(threadIdx.x); d < params.head_size; d += static_cast(blockDim.x)) { - rotated[d] = SaiLeadingRope(pooled, params.rotary_width, cos_cache + cache_offset, - sin_cache + cache_offset, d); - } - __syncthreads(); - const int64_t out_base = (static_cast(b) * params.state_capacity + entry) * params.head_size; const int completion_token = (k + 1) * params.compress_ratio - old_buf_len - 1; const int64_t capture_base = (static_cast(b) * params.state_update_capacity + completion_token) * params.head_size; for (int d = static_cast(threadIdx.x); d < params.head_size; d += static_cast(blockDim.x)) { - present_key_state[out_base + d] = from_float(rotated[d]); + const float rot = SaiLeadingRope(pooled, params.rotary_width, cos_cache + cache_offset, + sin_cache + cache_offset, d); + present_key_state[out_base + d] = from_float(rot); if (completion_token < capture_count) { - state_update[capture_base + d] = from_float(rotated[d]); + state_update[capture_base + d] = from_float(rot); } } __syncthreads(); } + (void)rotated; // Publish the raw trailing buffer. Skipped entirely on overflow: present_kv_buffer already // holds past_kv_buffer's contents unchanged (from the baseline copy in the Launch function @@ -347,8 +430,8 @@ __global__ void PackedRotateQueryKernel(const T* query, const T* query_norm_weig // One block per (token, key_state slot). Scores are directly dotted against the already-prepared // present_key_state entry (unlike the dense op, no per-query pooling/normalize/rotate is repeated // here because the packed contract stores fully-prepared blocks in key_state). -template -__global__ void QsaBlockScoreKernel(const T* present_key_state, const float* query_rotated, +template +__global__ void QsaBlockScoreKernel(const TState* present_key_state, const float* query_rotated, const int32_t* cumulative_sequence_lengths, const int32_t* past_sequence_lengths, const int64_t* position_ids, const int32_t* present_state_lengths, float* block_scores, @@ -388,7 +471,7 @@ __global__ void QsaBlockScoreKernel(const T* present_key_state, const float* que query_rotated + (static_cast(token) * params.num_heads + head) * params.head_size; float partial = 0.0f; for (int d = static_cast(threadIdx.x); d < params.head_size; d += static_cast(blockDim.x)) { - partial += query_head[d] * to_float(present_key_state[key_base + d]); + partial += query_head[d] * to_float(present_key_state[key_base + d]); } score += fmaxf(SaiBlockSum(partial, reduction), 0.0f); } @@ -402,8 +485,9 @@ __global__ void QsaBlockScoreKernel(const T* present_key_state, const float* que // One CTA per packed query row. Eight state blocks are scored concurrently, with one warp per // (state block, query head). Unlike the generic rectangular launch, each CTA stops at its row's // live causal block count and never launches or writes work for unused fixed-capacity slots. -template -__global__ void QsaRaggedPrefillScoreKernel(const T* present_key_state, const float* query_rotated, +// WarpHeads instead keeps all heads in one warp to reuse keys without CTA-wide barriers. +template +__global__ void QsaRaggedPrefillScoreKernel(const TState* present_key_state, const float* query_rotated, const int32_t* cumulative_sequence_lengths, const int32_t* past_sequence_lengths, const int64_t* position_ids, const int32_t* present_state_lengths, const int32_t* overflow_flags, @@ -412,7 +496,7 @@ __global__ void QsaRaggedPrefillScoreKernel(const T* present_key_state, const fl const int warp = static_cast(threadIdx.x) / kWarpSize; const int lane = static_cast(threadIdx.x) % kWarpSize; - const int block_slot = warp / kQwenNumHeads; + const int block_slot = WarpHeads ? warp : warp / kQwenNumHeads; const int head = warp % kQwenNumHeads; for (int token = static_cast(blockIdx.x); token < params.total_tokens; token += static_cast(gridDim.x)) { const int batch = PackedBatchOfToken(cumulative_sequence_lengths, params.batch_size, token); @@ -425,55 +509,84 @@ __global__ void QsaRaggedPrefillScoreKernel(const T* present_key_state, const fl const int64_t visible_64 = causal_count < key_len_after ? causal_count : static_cast(key_len_after); const int block_count = overflow_flags[batch] == 0 ? static_cast(visible_64 < 0 ? 0 : visible_64) : 0; - for (int tile_start = 0; tile_start < block_count; tile_start += kHierarchicalTileBlocks) { - const int block_index = tile_start + block_slot; - float score = 0.0f; - if (block_index < block_count) { - const int d = lane * 4; - const int64_t query_base = - (static_cast(token) * kQwenNumHeads + head) * kQwenHeadSize; + if constexpr (WarpHeads) { + float4 queries[kQwenNumHeads]; +#pragma unroll + for (int query_head = 0; query_head < kQwenNumHeads; ++query_head) { + queries[query_head] = LoadQsaVector4( + query_rotated + (static_cast(token) * kQwenNumHeads + query_head) * kQwenHeadSize + lane * 4); + } + for (int block_index = block_slot; block_index < block_count; block_index += kHierarchicalTileBlocks) { const int64_t key_base = (static_cast(batch) * params.state_capacity + block_index) * kQwenHeadSize; - const float4 query_value = LoadQsaVector4(query_rotated + query_base + d); - const float4 key_value = LoadQsaVector4(present_key_state + key_base + d); - score = query_value.x * key_value.x + query_value.y * key_value.y + - query_value.z * key_value.z + query_value.w * key_value.w; - score = topk::WarpReduceSum(score); - } - if (lane == 0) { - head_scores[block_slot][head] = fmaxf(score, 0.0f); - } - __syncthreads(); - if (threadIdx.x < kHierarchicalTileBlocks && tile_start + threadIdx.x < block_count) { + const float4 key_value = LoadQsaVector4(present_key_state + key_base + lane * 4); float total_score = 0.0f; #pragma unroll for (int query_head = 0; query_head < kQwenNumHeads; ++query_head) { - total_score += head_scores[threadIdx.x][query_head]; + const float4 query_value = queries[query_head]; + float score = query_value.x * key_value.x + query_value.y * key_value.y + + query_value.z * key_value.z + query_value.w * key_value.w; + score = topk::WarpReduceSum(score); + total_score += fmaxf(score, 0.0f); + } + if (lane == 0) { + const float scaled_score = total_score * params.scale; + block_scores[static_cast(token) * params.state_capacity + block_index] = + scaled_score == 0.0f ? 0.0f : scaled_score; } - const float scaled_score = total_score * params.scale; - block_scores[static_cast(token) * params.state_capacity + tile_start + threadIdx.x] = - scaled_score == 0.0f ? 0.0f : scaled_score; } - __syncthreads(); + } else { + for (int tile_start = 0; tile_start < block_count; tile_start += kHierarchicalTileBlocks) { + const int block_index = tile_start + block_slot; + float score = 0.0f; + if (block_index < block_count) { + const int d = lane * 4; + const int64_t query_base = + (static_cast(token) * kQwenNumHeads + head) * kQwenHeadSize; + const int64_t key_base = + (static_cast(batch) * params.state_capacity + block_index) * kQwenHeadSize; + const float4 query_value = LoadQsaVector4(query_rotated + query_base + d); + const float4 key_value = LoadQsaVector4(present_key_state + key_base + d); + score = query_value.x * key_value.x + query_value.y * key_value.y + + query_value.z * key_value.z + query_value.w * key_value.w; + score = topk::WarpReduceSum(score); + } + if (lane == 0) { + head_scores[block_slot][head] = fmaxf(score, 0.0f); + } + __syncthreads(); + if (threadIdx.x < kHierarchicalTileBlocks && tile_start + threadIdx.x < block_count) { + float total_score = 0.0f; +#pragma unroll + for (int query_head = 0; query_head < kQwenNumHeads; ++query_head) { + total_score += head_scores[threadIdx.x][query_head]; + } + const float scaled_score = total_score * params.scale; + block_scores[static_cast(token) * params.state_capacity + tile_start + threadIdx.x] = + scaled_score == 0.0f ? 0.0f : scaled_score; + } + __syncthreads(); + } } } } -template -__global__ void QsaScoreTileTopKKernel(const T* present_key_state, const float* query_rotated, +template +__global__ void QsaScoreTileTopKKernel(const TState* present_key_state, const float* query_rotated, const int32_t* cumulative_sequence_lengths, const int32_t* past_sequence_lengths, const int64_t* position_ids, const int32_t* present_state_lengths, const int32_t* overflow_flags, uint64_t* tile_keys, int tile_count, PackedSparseAttentionIndexerParams params) { - __shared__ uint64_t keys[kHierarchicalTileBlocks]; - __shared__ float head_scores[kHierarchicalTileBlocks][kQwenNumHeads]; + __shared__ uint64_t keys[kHierarchicalTopKTileBlocks]; + __shared__ float head_scores[kHierarchicalTopKTileBlocks][kQwenNumHeads]; const int warp = static_cast(threadIdx.x) / kWarpSize; const int lane = static_cast(threadIdx.x) % kWarpSize; - const int block_slot = warp / kQwenNumHeads; - const int head = warp % kQwenNumHeads; - const int token = static_cast(blockIdx.y); + const int block_slot = warp; + const int blocks_per_row = min(tile_count, kHierarchicalScoreMaxBlocksPerRow); + const int first_tile = static_cast(blockIdx.x) % blocks_per_row; + const int token = static_cast(blockIdx.x) / blocks_per_row; const int batch = PackedBatchOfToken(cumulative_sequence_lengths, params.batch_size, token); const int key_len_after = present_state_lengths[batch * 2 + psai::kKeyStateLength]; const int64_t abs_position = @@ -483,35 +596,54 @@ __global__ void QsaScoreTileTopKKernel(const T* present_key_state, const float* const int64_t causal_count = SaiCausalThreshold(abs_position, kQwenCompressRatio); const int64_t visible_64 = causal_count < key_len_after ? causal_count : static_cast(key_len_after); const int block_count = overflow_flags[batch] == 0 ? static_cast(visible_64 < 0 ? 0 : visible_64) : 0; - const int active_tiles = (block_count + kHierarchicalTileBlocks - 1) / kHierarchicalTileBlocks; - const int key_stride = tile_count * kHierarchicalTileBlocks; + + const int live_tiles = (block_count + kHierarchicalTopKTileBlocks - 1) / kHierarchicalTopKTileBlocks; + const int key_stride = tile_count * kHierarchicalTopKTileBlocks; const int64_t row_base = static_cast(token) * key_stride; - for (int index = active_tiles * kHierarchicalTileBlocks + blockIdx.x * blockDim.x + threadIdx.x; - index < key_stride; index += gridDim.x * blockDim.x) { + // Initialize only the fully padded suffix; scoring writes every key in live tiles. + for (int index = live_tiles * kHierarchicalTopKTileBlocks + first_tile * blockDim.x + threadIdx.x; + index < key_stride; index += blocks_per_row * blockDim.x) { tile_keys[row_base + index] = topk::kPaddingSortKey; } - - for (int tile = static_cast(blockIdx.x); tile < active_tiles; tile += static_cast(gridDim.x)) { - const int block_index = tile * kHierarchicalTileBlocks + block_slot; - float score = 0.0f; - if (block_index < block_count) { - const int d = lane * 4; - const int64_t query_base = - (static_cast(token) * kQwenNumHeads + head) * kQwenHeadSize; - const int64_t key_base = - (static_cast(batch) * params.state_capacity + block_index) * kQwenHeadSize; - const float4 query_value = LoadQsaVector4(query_rotated + query_base + d); - const float4 key_value = LoadQsaVector4(present_key_state + key_base + d); - score = query_value.x * key_value.x + query_value.y * key_value.y + - query_value.z * key_value.z + query_value.w * key_value.w; - score = topk::WarpReduceSum(score); - } - if (lane == 0) { - head_scores[block_slot][head] = fmaxf(score, 0.0f); + if (first_tile >= live_tiles) { + return; + } + const int d = lane * 4; + float4 query_values[kQwenNumHeads]; +#pragma unroll + for (int head = 0; head < kQwenNumHeads; ++head) { + const int64_t query_base = + (static_cast(token) * kQwenNumHeads + head) * kQwenHeadSize; + query_values[head] = LoadQsaVector4(query_rotated + query_base + d); + } + for (int tile = first_tile; tile < live_tiles; tile += blocks_per_row) { + const int64_t output_base = row_base + tile * kHierarchicalTopKTileBlocks; + for (int slot = block_slot; slot < kHierarchicalTopKTileBlocks; + slot += kHierarchicalTileScoreThreads / kWarpSize) { + const int block_index = tile * kHierarchicalTopKTileBlocks + slot; + float4 key_value = make_float4(0.0f, 0.0f, 0.0f, 0.0f); + if (block_index < block_count) { + const int64_t key_base = + (static_cast(batch) * params.state_capacity + block_index) * kQwenHeadSize; + key_value = LoadQsaVector4(present_key_state + key_base + d); + } +#pragma unroll + for (int head = 0; head < kQwenNumHeads; ++head) { + float score = 0.0f; + if (block_index < block_count) { + const float4 query_value = query_values[head]; + score = query_value.x * key_value.x + query_value.y * key_value.y + + query_value.z * key_value.z + query_value.w * key_value.w; + score = topk::WarpReduceSum(score); + } + if (lane == 0) { + head_scores[slot][head] = fmaxf(score, 0.0f); + } + } } __syncthreads(); - if (threadIdx.x < kHierarchicalTileBlocks) { - const int tile_block = tile * kHierarchicalTileBlocks + static_cast(threadIdx.x); + if (threadIdx.x < kHierarchicalTopKTileBlocks) { + const int tile_block = tile * kHierarchicalTopKTileBlocks + static_cast(threadIdx.x); uint64_t key = topk::kPaddingSortKey; if (tile_block < block_count) { float total_score = 0.0f; @@ -525,123 +657,22 @@ __global__ void QsaScoreTileTopKKernel(const T* present_key_state, const float* keys[threadIdx.x] = key; } __syncthreads(); - if (threadIdx.x == 0) { - for (int index = 1; index < kHierarchicalTileBlocks; ++index) { - const uint64_t key = keys[index]; - int insertion = index; - while (insertion > 0 && key > keys[insertion - 1]) { - keys[insertion] = keys[insertion - 1]; - --insertion; - } - keys[insertion] = key; - } - } - __syncthreads(); - const int64_t output_base = row_base + tile * kHierarchicalTileBlocks; - if (threadIdx.x < kHierarchicalTileBlocks) { - tile_keys[output_base + threadIdx.x] = keys[threadIdx.x]; - } - __syncthreads(); - } -} - -template -__global__ void QsaPairedScoreTileTopKKernel(const T* present_key_state, const float* query_rotated, - const int32_t* cumulative_sequence_lengths, - const int32_t* past_sequence_lengths, const int64_t* position_ids, - const int32_t* present_state_lengths, const int32_t* overflow_flags, - uint64_t* tile_keys, int tile_count, - PackedSparseAttentionIndexerParams params) { - __shared__ uint64_t keys[2][kHierarchicalTileBlocks]; - __shared__ int block_counts[2]; - const int first_token = static_cast(blockIdx.y) * 2; - const int warp = static_cast(threadIdx.x) / kWarpSize; - const int lane = static_cast(threadIdx.x) % kWarpSize; - if (threadIdx.x < 2) { - const int metadata_token = first_token + static_cast(threadIdx.x); - int count = 0; - if (metadata_token < params.total_tokens && overflow_flags[0] == 0) { - const int64_t position = params.has_position_ids ? position_ids[metadata_token] - : static_cast(past_sequence_lengths[0]) + metadata_token; - const int64_t causal = SaiCausalThreshold(position, kQwenCompressRatio); - const int64_t visible = min(causal, static_cast(present_state_lengths[psai::kKeyStateLength])); - count = static_cast(max(static_cast(0), visible)); - } - block_counts[threadIdx.x] = count; - } - __syncthreads(); - float4 query_values[2][kQwenNumHeads]; + if (warp == 0) { + uint64_t key = keys[lane]; #pragma unroll - for (int query_index = 0; query_index < 2; ++query_index) { + for (int width = 2; width <= kHierarchicalTopKTileBlocks; width *= 2) { #pragma unroll - for (int head = 0; head < kQwenNumHeads; ++head) { - const int token = first_token + query_index; - query_values[query_index][head] = token < params.total_tokens - ? LoadQsaVector4(query_rotated + (static_cast(token) * kQwenNumHeads + head) * kQwenHeadSize + lane * 4) - : make_float4(0.0f, 0.0f, 0.0f, 0.0f); - } - } - const int key_stride = tile_count * kHierarchicalTileBlocks; - for (int query_index = 0; query_index < 2; ++query_index) { - if (first_token + query_index < params.total_tokens) { - const int active = (block_counts[query_index] + kHierarchicalTileBlocks - 1) / kHierarchicalTileBlocks; - const int64_t row_base = static_cast(first_token + query_index) * key_stride; - for (int index = active * kHierarchicalTileBlocks + blockIdx.x * blockDim.x + threadIdx.x; - index < key_stride; index += gridDim.x * blockDim.x) { - tile_keys[row_base + index] = topk::kPaddingSortKey; - } - } - } - const int visible = max(block_counts[0], block_counts[1]); - const int active_tiles = (visible + kHierarchicalTileBlocks - 1) / kHierarchicalTileBlocks; - for (int tile = static_cast(blockIdx.x); tile < active_tiles; tile += static_cast(gridDim.x)) { - const int block = tile * kHierarchicalTileBlocks + warp; - const float4 key_value = block < visible - ? LoadQsaVector4(present_key_state + static_cast(block) * kQwenHeadSize + lane * 4) - : make_float4(0.0f, 0.0f, 0.0f, 0.0f); -#pragma unroll - for (int query_index = 0; query_index < 2; ++query_index) { - float total = 0.0f; -#pragma unroll - for (int head = 0; head < kQwenNumHeads; ++head) { - const float4 query_value = query_values[query_index][head]; - float score = query_value.x * key_value.x + query_value.y * key_value.y + - query_value.z * key_value.z + query_value.w * key_value.w; - score = topk::WarpReduceSum(score); - total += fmaxf(score, 0.0f); - } - uint64_t key = topk::kPaddingSortKey; - if (block < block_counts[query_index]) { - const float scaled = total * params.scale; - key = topk::PackStableSortKey(scaled == 0.0f ? 0.0f : scaled, block); - } - if (lane == 0) { - keys[query_index][warp] = key; - } - } - __syncthreads(); - if (threadIdx.x < 2) { - const int query_index = static_cast(threadIdx.x); - for (int index = 1; index < kHierarchicalTileBlocks; ++index) { - const uint64_t key = keys[query_index][index]; - int insertion = index; - while (insertion > 0 && key > keys[query_index][insertion - 1]) { - keys[query_index][insertion] = keys[query_index][insertion - 1]; - --insertion; + for (int stride = width / 2; stride > 0; stride /= 2) { + const uint64_t other = __shfl_xor_sync(0xffffffffU, key, stride); + const bool take_maximum = ((lane & width) == 0) == ((lane & stride) == 0); + key = take_maximum ? max(key, other) : min(key, other); } - keys[query_index][insertion] = key; } + keys[lane] = key; } __syncthreads(); - if (threadIdx.x < 2 * kHierarchicalTileBlocks) { - const int query_index = static_cast(threadIdx.x) / kHierarchicalTileBlocks; - const int slot = static_cast(threadIdx.x) % kHierarchicalTileBlocks; - if (first_token + query_index < params.total_tokens && - tile * kHierarchicalTileBlocks < block_counts[query_index]) { - const int64_t output_base = static_cast(first_token + query_index) * key_stride + - tile * kHierarchicalTileBlocks; - tile_keys[output_base + slot] = keys[query_index][slot]; - } + if (threadIdx.x < kHierarchicalTopKTileBlocks) { + tile_keys[output_base + threadIdx.x] = keys[threadIdx.x]; } __syncthreads(); } @@ -667,47 +698,6 @@ __device__ __forceinline__ uint64_t QsaMergeRank(const uint64_t* left, int left_ return topk::kPaddingSortKey; } -__global__ void QsaCompactTileTopKKernel(const uint64_t* input_keys, uint64_t* output_keys, - int input_stride, int output_stride) { - constexpr int kChunkSize = 2048; - static_assert(kChunkSize % kHierarchicalTileBlocks == 0); - __shared__ uint64_t shared_keys[2][kChunkSize]; - const int chunk_start = static_cast(blockIdx.x) * kChunkSize; - const int64_t input_base = static_cast(blockIdx.y) * input_stride + chunk_start; - const int64_t output_base = static_cast(blockIdx.y) * output_stride + - static_cast(blockIdx.x) * kBoundedTopKMax; - if (input_keys[input_base] == topk::kPaddingSortKey) { - for (int rank = threadIdx.x; rank < kBoundedTopKMax; rank += blockDim.x) { - output_keys[output_base + rank] = topk::kPaddingSortKey; - } - return; - } - for (int index = threadIdx.x; index < kChunkSize; index += blockDim.x) { - shared_keys[0][index] = chunk_start + index < input_stride - ? input_keys[input_base + index] - : topk::kPaddingSortKey; - } - __syncthreads(); - int input_bank = 0; - for (int width = kHierarchicalTileBlocks; width < kChunkSize; width *= 2) { - const int count = min(width, kBoundedTopKMax); - const int output_count = min(count * 2, kBoundedTopKMax); - const int pairs = kChunkSize / (width * 2); - for (int index = threadIdx.x; index < pairs * output_count; index += blockDim.x) { - const int pair_start = (index / output_count) * width * 2; - const int rank = index % output_count; - shared_keys[1 - input_bank][pair_start + rank] = - QsaMergeRank(shared_keys[input_bank] + pair_start, count, - shared_keys[input_bank] + pair_start + width, count, rank); - } - __syncthreads(); - input_bank = 1 - input_bank; - } - for (int rank = threadIdx.x; rank < kBoundedTopKMax; rank += blockDim.x) { - output_keys[output_base + rank] = shared_keys[input_bank][rank]; - } -} - __global__ void QsaMergeTileTopKKernel(const uint64_t* input_keys, uint64_t* output_keys, int key_stride, int list_width, int rows) { const int pairs_per_row = (key_stride + 2 * list_width - 1) / (2 * list_width); @@ -721,12 +711,121 @@ __global__ void QsaMergeTileTopKKernel(const uint64_t* input_keys, uint64_t* out const int right_count = min(min(list_width, kBoundedTopKMax), max(0, key_stride - right_start)); const int output_count = min(left_count + right_count, kBoundedTopKMax); const int64_t row_base = row * key_stride; + const uint64_t* left = input_keys + row_base + left_start; + const uint64_t* right = input_keys + row_base + right_start; + const bool left_empty = left_count == 0 || left[0] == topk::kPaddingSortKey; + const bool right_empty = right_count == 0 || right[0] == topk::kPaddingSortKey; for (int rank = threadIdx.x; rank < output_count; rank += blockDim.x) { - output_keys[row_base + left_start + rank] = - QsaMergeRank(input_keys + row_base + left_start, left_count, - input_keys + row_base + right_start, right_count, rank); + uint64_t key = topk::kPaddingSortKey; + if (left_empty) { + key = rank < right_count ? right[rank] : topk::kPaddingSortKey; + } else if (right_empty) { + key = rank < left_count ? left[rank] : topk::kPaddingSortKey; + } else { + key = QsaMergeRank(left, left_count, right, right_count, rank); + } + output_keys[row_base + left_start + rank] = key; + } + } +} + +__global__ void QsaMergeTwoLevelsTopKKernel(const uint64_t* input_keys, uint64_t* output_keys, + int key_stride, int list_width, int rows) { + __shared__ uint64_t inputs[4 * kBoundedTopKMax]; + __shared__ uint64_t intermediate[2 * kBoundedTopKMax]; + const int groups_per_row = (key_stride + 4 * list_width - 1) / (4 * list_width); + const int input_width = min(list_width, kBoundedTopKMax); + const int intermediate_width = min(2 * list_width, kBoundedTopKMax); + const int64_t total_groups = static_cast(rows) * groups_per_row; + for (int64_t work = blockIdx.x; work < total_groups; work += gridDim.x) { + const int start = static_cast(work % groups_per_row) * 4 * list_width; + const int64_t row_base = (work / groups_per_row) * key_stride; + for (int index = threadIdx.x; index < 4 * input_width; index += blockDim.x) { + const int source = start + (index / input_width) * list_width + index % input_width; + inputs[index] = source < key_stride ? input_keys[row_base + source] : topk::kPaddingSortKey; + } + __syncthreads(); + for (int index = threadIdx.x; index < 2 * intermediate_width; index += blockDim.x) { + const int half = index / intermediate_width; + const int rank = index % intermediate_width; + const int left_start = start + half * 2 * list_width; + const int right_start = left_start + list_width; + const int left_count = min(min(list_width, kBoundedTopKMax), max(0, key_stride - left_start)); + const int right_count = min(min(list_width, kBoundedTopKMax), max(0, key_stride - right_start)); + uint64_t key = topk::kPaddingSortKey; + if (rank < left_count + right_count) { + const uint64_t* left = inputs + half * 2 * input_width; + const uint64_t* right = left + input_width; + if (left_count == 0 || left[0] == topk::kPaddingSortKey) { + key = rank < right_count ? right[rank] : topk::kPaddingSortKey; + } else if (right_count == 0 || right[0] == topk::kPaddingSortKey) { + key = rank < left_count ? left[rank] : topk::kPaddingSortKey; + } else { + key = QsaMergeRank(left, left_count, right, right_count, rank); + } + } + intermediate[index] = key; + } + __syncthreads(); + const int left_count = min(intermediate_width, key_stride - start); + const int right_count = min(intermediate_width, max(0, key_stride - start - 2 * list_width)); + const int output_count = min(left_count + right_count, kBoundedTopKMax); + const uint64_t* right = intermediate + intermediate_width; + for (int rank = threadIdx.x; rank < output_count; rank += blockDim.x) { + uint64_t key; + if (intermediate[0] == topk::kPaddingSortKey) { + key = rank < right_count ? right[rank] : topk::kPaddingSortKey; + } else if (right_count == 0 || right[0] == topk::kPaddingSortKey) { + key = rank < left_count ? intermediate[rank] : topk::kPaddingSortKey; + } else { + key = QsaMergeRank(intermediate, left_count, right, right_count, rank); + } + output_keys[row_base + start + rank] = key; + } + __syncthreads(); + } +} + +__device__ __forceinline__ void QsaEmitTopKRow(const uint64_t* top_keys, int token, + const int32_t* cumulative_sequence_lengths, + const int32_t* past_sequence_lengths, + const int64_t* position_ids, + const int32_t* present_state_lengths, + const int32_t* overflow_flags, + int32_t* selected_indices, int32_t* selected_counts, + PackedSparseAttentionIndexerParams params) { + int32_t* output = selected_indices + static_cast(token) * params.output_capacity; + const int batch = PackedBatchOfToken(cumulative_sequence_lengths, params.batch_size, token); + const int64_t abs_position = + params.has_position_ids + ? position_ids[token] + : static_cast(past_sequence_lengths[batch]) + (token - cumulative_sequence_lengths[batch]); + const int key_len_after = present_state_lengths[batch * 2 + psai::kKeyStateLength]; + const int64_t causal_count = SaiCausalThreshold(abs_position, params.compress_ratio); + const int64_t visible_64 = causal_count < key_len_after ? causal_count : static_cast(key_len_after); + const int block_count = overflow_flags[batch] == 0 ? static_cast(visible_64 < 0 ? 0 : visible_64) : 0; + const int selected = min(params.block_topk, block_count); + for (int rank = threadIdx.x; rank < selected; rank += blockDim.x) { + const int selected_block = topk::UnpackStableSortIndex(top_keys[rank]); + for (int t = 0; t < params.compress_ratio; ++t) { + output[rank * params.compress_ratio + t] = selected_block * params.compress_ratio + t; } } + const int64_t block_start = static_cast(block_count) * params.compress_ratio; + const int64_t natural_tail = abs_position >= block_start ? abs_position - block_start + 1 : 0; + const int remaining_capacity = params.capacity - selected * params.compress_ratio; + const int64_t tail_count_64 = natural_tail < remaining_capacity ? natural_tail : remaining_capacity; + const int tail_count = overflow_flags[batch] == 0 ? static_cast(tail_count_64 < 0 ? 0 : tail_count_64) : 0; + for (int t = threadIdx.x; t < tail_count; t += blockDim.x) { + output[selected * params.compress_ratio + t] = static_cast(block_start + t); + } + for (int position = selected * params.compress_ratio + tail_count + threadIdx.x; + position < params.output_capacity; position += blockDim.x) { + output[position] = -1; + } + if (threadIdx.x == 0) { + selected_counts[token] = selected * params.compress_ratio + tail_count; + } } __global__ void QsaEmitHierarchicalTopKKernel(const uint64_t* tile_keys, @@ -738,38 +837,37 @@ __global__ void QsaEmitHierarchicalTopKKernel(const uint64_t* tile_keys, int32_t* selected_indices, int32_t* selected_counts, PackedSparseAttentionIndexerParams params) { for (int token = static_cast(blockIdx.x); token < params.total_tokens; token += static_cast(gridDim.x)) { - int32_t* output = selected_indices + static_cast(token) * params.output_capacity; - const int batch = PackedBatchOfToken(cumulative_sequence_lengths, params.batch_size, token); - const int64_t abs_position = - params.has_position_ids - ? position_ids[token] - : static_cast(past_sequence_lengths[batch]) + (token - cumulative_sequence_lengths[batch]); - const int key_len_after = present_state_lengths[batch * 2 + psai::kKeyStateLength]; - const int64_t causal_count = SaiCausalThreshold(abs_position, params.compress_ratio); - const int64_t visible_64 = causal_count < key_len_after ? causal_count : static_cast(key_len_after); - const int block_count = overflow_flags[batch] == 0 ? static_cast(visible_64 < 0 ? 0 : visible_64) : 0; - const int selected = min(params.block_topk, block_count); - for (int rank = threadIdx.x; rank < selected; rank += blockDim.x) { - const int selected_block = topk::UnpackStableSortIndex(tile_keys[static_cast(token) * key_stride + rank]); - for (int t = 0; t < params.compress_ratio; ++t) { - output[rank * params.compress_ratio + t] = selected_block * params.compress_ratio + t; - } - } - const int64_t block_start = static_cast(block_count) * params.compress_ratio; - const int64_t natural_tail = abs_position >= block_start ? abs_position - block_start + 1 : 0; - const int remaining_capacity = params.capacity - selected * params.compress_ratio; - const int64_t tail_count_64 = natural_tail < remaining_capacity ? natural_tail : remaining_capacity; - const int tail_count = overflow_flags[batch] == 0 ? static_cast(tail_count_64 < 0 ? 0 : tail_count_64) : 0; - for (int t = threadIdx.x; t < tail_count; t += blockDim.x) { - output[selected * params.compress_ratio + t] = static_cast(block_start + t); - } - for (int position = selected * params.compress_ratio + tail_count + threadIdx.x; - position < params.output_capacity; position += blockDim.x) { - output[position] = -1; - } - if (threadIdx.x == 0) { - selected_counts[token] = selected * params.compress_ratio + tail_count; + QsaEmitTopKRow(tile_keys + static_cast(token) * key_stride, token, + cumulative_sequence_lengths, past_sequence_lengths, position_ids, + present_state_lengths, overflow_flags, selected_indices, selected_counts, params); + } +} + +__global__ void QsaFinalMergeAndEmitKernel(const uint64_t* compacted_keys, + const int32_t* cumulative_sequence_lengths, + const int32_t* past_sequence_lengths, + const int64_t* position_ids, + const int32_t* present_state_lengths, + const int32_t* overflow_flags, int compact_stride, int chunks, + int32_t* selected_indices, int32_t* selected_counts, + PackedSparseAttentionIndexerParams params) { + __shared__ uint64_t merged[kBoundedTopKMax]; + const int tid = static_cast(threadIdx.x); + for (int token = static_cast(blockIdx.x); token < params.total_tokens; token += static_cast(gridDim.x)) { + const uint64_t* row = compacted_keys + static_cast(token) * compact_stride; + merged[tid] = row[tid]; + __syncthreads(); + for (int chunk = 1; chunk < chunks; ++chunk) { + const uint64_t* next = row + chunk * kBoundedTopKMax; + if (next[0] == topk::kPaddingSortKey) break; + const uint64_t key = QsaMergeRank(merged, kBoundedTopKMax, next, kBoundedTopKMax, tid); + __syncthreads(); + merged[tid] = key; + __syncthreads(); } + QsaEmitTopKRow(merged, token, cumulative_sequence_lengths, past_sequence_lengths, position_ids, + present_state_lengths, overflow_flags, selected_indices, selected_counts, params); + __syncthreads(); } } @@ -1215,13 +1313,10 @@ __global__ void CsaUpdateStateKernel(const T* key, const T* gate, const T* key_n // (from the baseline copy in the Launch function below), which is exactly the prior valid // buffer this rejected step must preserve. if (!rejected) { - // Process slots in ascending order with a barrier between slots. When past and present alias, - // the destination slot precedes its source slot, so this is the device-wide equivalent of - // a forward memmove and prevents one thread from overwriting another thread's source. - for (int t = 0; t < present_buffer_length; ++t) { + for (int t = static_cast(threadIdx.x); t < present_buffer_length; t += static_cast(blockDim.x)) { const int virtual_pos = present_buffer_start + t; const int64_t out_base = (static_cast(b) * params.buffer_capacity + t) * width; - for (int c = static_cast(threadIdx.x); c < width; c += static_cast(blockDim.x)) { + for (int c = 0; c < width; ++c) { const float key_value = virtual_pos < old_buf_len ? to_float( @@ -1236,9 +1331,9 @@ __global__ void CsaUpdateStateKernel(const T* key, const T* gate, const T* key_n present_kv_buffer[out_base + c] = from_float(key_value); present_gate_buffer[out_base + c] = from_float(gate_value); } - __syncthreads(); } } + __syncthreads(); } } @@ -1412,7 +1507,7 @@ size_t GetQsaPackedWorkspaceFloatCount(const PackedSparseAttentionIndexerParams& const size_t rows = static_cast(params.total_tokens); const size_t query_count = rows * params.num_heads * params.head_size; if (UseHierarchicalQsaTopK(params)) { - const size_t key_count = rows * GetHierarchicalTileCount(params) * kHierarchicalTileBlocks; + const size_t key_count = rows * GetHierarchicalTileCount(params) * kHierarchicalTopKTileBlocks; return query_count + 4 * key_count; } return query_count + rows * static_cast(std::max(params.state_capacity, 1)); @@ -1424,21 +1519,21 @@ size_t GetCsaPackedWorkspaceFloatCount(const PackedSparseAttentionIndexerParams& return rows * params.num_heads * params.head_size + rows * static_cast(std::max(params.state_capacity, 1)); } -template +template Status LaunchQsaPackedSparseAttentionIndexer( cudaStream_t stream, const PackedSparseAttentionIndexerParams& params, const T* query, const T* key, const T* query_norm_weight, const T* key_norm_weight, const T* cos_cache, const T* sin_cache, const int32_t* cumulative_sequence_lengths, - const int32_t* past_sequence_lengths, const int64_t* position_ids, const T* past_key_state, + const int32_t* past_sequence_lengths, const int64_t* position_ids, const TState* past_key_state, const T* past_kv_buffer, const int32_t* past_state_lengths, const int32_t* state_update_capture_count, bool state_update_active, int32_t* selected_indices, - int32_t* selected_counts, T* present_key_state, T* present_kv_buffer, int32_t* present_state_lengths, + int32_t* selected_counts, TState* present_key_state, T* present_kv_buffer, int32_t* present_state_lengths, T* state_update, float* float_workspace, int32_t* overflow_flags) { if (params.batch_size > 0) { const int64_t key_state_elems = static_cast(params.batch_size) * params.state_capacity * params.head_size; if (key_state_elems > 0 && present_key_state != past_key_state) { - ElementwiseCopyKernel<<>>( + ElementwiseCopyKernel<<>>( past_key_state, present_key_state, key_state_elems); } const int64_t buffer_elems = static_cast(params.batch_size) * params.buffer_capacity * params.head_size; @@ -1458,14 +1553,33 @@ Status LaunchQsaPackedSparseAttentionIndexer( } const size_t value_bytes = static_cast(params.head_size) * sizeof(float); - const size_t update_shared_bytes = 2 * value_bytes + kThreads * sizeof(float); - ORT_RETURN_IF_ERROR( - ConfigureDynamicSharedMemory(QsaUpdateStateKernel, update_shared_bytes, "QsaUpdateStateKernel")); const int state_blocks = static_cast(std::min(params.batch_size, kSaiMaxGridDimX)); - QsaUpdateStateKernel<<>>( - key, key_norm_weight, cos_cache, sin_cache, cumulative_sequence_lengths, past_sequence_lengths, past_kv_buffer, - past_state_lengths, state_update_capture_count, state_update_active, present_key_state, present_kv_buffer, - present_state_lengths, state_update, overflow_flags, params); + if (params.total_tokens >= 4 * params.compress_ratio && params.compress_ratio > 0) { + const int parallel_blocks = + std::min((params.total_tokens + params.compress_ratio - 1) / params.compress_ratio, 512); + ORT_RETURN_IF_ERROR(ConfigureDynamicSharedMemory( + QsaUpdateStateBlocksKernel, value_bytes + kThreads * sizeof(float), "QsaUpdateStateBlocksKernel")); + QsaUpdateStateBlocksKernel + <<>>( + key, key_norm_weight, cos_cache, sin_cache, cumulative_sequence_lengths, past_sequence_lengths, + past_state_lengths, state_update_capture_count, state_update_active, present_key_state, state_update, + params); + ORT_RETURN_IF_ERROR(ConfigureDynamicSharedMemory( + QsaUpdateStateKernel, 2 * value_bytes + kThreads * sizeof(float), "QsaUpdateStateKernel")); + QsaUpdateStateKernel + <<>>( + key, key_norm_weight, cos_cache, sin_cache, cumulative_sequence_lengths, past_sequence_lengths, + past_kv_buffer, past_state_lengths, state_update_capture_count, state_update_active, present_key_state, + present_kv_buffer, present_state_lengths, state_update, overflow_flags, params); + } else { + ORT_RETURN_IF_ERROR(ConfigureDynamicSharedMemory( + QsaUpdateStateKernel, 2 * value_bytes + kThreads * sizeof(float), "QsaUpdateStateKernel")); + QsaUpdateStateKernel + <<>>( + key, key_norm_weight, cos_cache, sin_cache, cumulative_sequence_lengths, past_sequence_lengths, + past_kv_buffer, past_state_lengths, state_update_capture_count, state_update_active, present_key_state, + present_kv_buffer, present_state_lengths, state_update, overflow_flags, params); + } if (params.total_tokens == 0) { return CUDA_CALL(cudaGetLastError()); @@ -1480,63 +1594,92 @@ Status LaunchQsaPackedSparseAttentionIndexer( const int64_t rotate_rows = static_cast(params.total_tokens) * params.num_heads; const int rotate_blocks = static_cast(std::min(rotate_rows, kSaiMaxGridDimX)); - const size_t rotate_shared_bytes = value_bytes + kThreads * sizeof(float); ORT_RETURN_IF_ERROR(ConfigureDynamicSharedMemory( - PackedRotateQueryKernel, rotate_shared_bytes, "PackedRotateQueryKernel")); - PackedRotateQueryKernel<<>>( + PackedRotateQueryKernel, value_bytes + kThreads * sizeof(float), "PackedRotateQueryKernel")); + PackedRotateQueryKernel<<>>( query, query_norm_weight, cos_cache, sin_cache, cumulative_sequence_lengths, past_sequence_lengths, position_ids, query_rotated, params); const int token_blocks = static_cast(std::min(params.total_tokens, kSaiMaxGridDimX)); if (use_hierarchical_topk) { const int tile_count = GetHierarchicalTileCount(params); - const int key_stride = tile_count * kHierarchicalTileBlocks; + const int key_stride = tile_count * kHierarchicalTopKTileBlocks; const size_t key_count = static_cast(params.total_tokens) * key_stride; uint64_t* merge_input = reinterpret_cast(selection_workspace); uint64_t* merge_output = merge_input + key_count; - if (params.batch_size == 1 && params.total_tokens >= 4) { - const dim3 score_grid(static_cast(std::min(tile_count, 128)), - static_cast((params.total_tokens + 1) / 2)); - QsaPairedScoreTileTopKKernel<<>>( - present_key_state, query_rotated, cumulative_sequence_lengths, past_sequence_lengths, position_ids, - present_state_lengths, overflow_flags, merge_input, tile_count, params); - } else { - const dim3 score_grid(static_cast(std::min(tile_count, 128)), - static_cast(params.total_tokens)); - QsaScoreTileTopKKernel<<>>( - present_key_state, query_rotated, cumulative_sequence_lengths, past_sequence_lengths, position_ids, - present_state_lengths, overflow_flags, merge_input, tile_count, params); - } - int compact_stride = key_stride; - int first_list_width = kHierarchicalTileBlocks; - if (params.total_tokens >= 4) { - const int chunks = (key_stride + 2047) / 2048; - compact_stride = chunks * kBoundedTopKMax; - QsaCompactTileTopKKernel<<(chunks), static_cast(params.total_tokens)), - kHierarchicalMergeThreads, 0, stream>>>( + const int64_t tile_work = + static_cast(params.total_tokens) * std::min(tile_count, kHierarchicalScoreMaxBlocksPerRow); + QsaScoreTileTopKKernel<<(std::min(tile_work, kSaiMaxGridDimX)), + kHierarchicalTileScoreThreads, 0, stream>>>( + present_key_state, query_rotated, cumulative_sequence_lengths, past_sequence_lengths, position_ids, + present_state_lengths, overflow_flags, merge_input, tile_count, params); + static const int compact_topk = + ParseEnvironmentVariableWithDefault("ORT_PACKED_SPARSE_INDEXER_COMPACT_TOPK", 1); + ORT_ENFORCE(compact_topk == 0 || compact_topk == 1, + "ORT_PACKED_SPARSE_INDEXER_COMPACT_TOPK must be 0 or 1."); + if (compact_topk) { + const int chunks = (key_stride + qsa_topk::kChunkSize - 1) / qsa_topk::kChunkSize; + const int compact_stride = chunks * kBoundedTopKMax; + qsa_topk::QsaCompactTileTopKKernel<<>>( merge_input, merge_output, key_stride, compact_stride); std::swap(merge_input, merge_output); - first_list_width = kBoundedTopKMax; - } - for (int list_width = first_list_width; list_width < compact_stride; list_width *= 2) { - const int pairs_per_row = (compact_stride + 2 * list_width - 1) / (2 * list_width); - const int64_t merge_work = static_cast(params.total_tokens) * pairs_per_row; - QsaMergeTileTopKKernel<<(std::min(merge_work, kSaiMaxGridDimX)), - kHierarchicalMergeThreads, 0, stream>>>( - merge_input, merge_output, compact_stride, list_width, params.total_tokens); - std::swap(merge_input, merge_output); + if (chunks <= 5) { + QsaFinalMergeAndEmitKernel<<>>( + merge_input, cumulative_sequence_lengths, past_sequence_lengths, position_ids, + present_state_lengths, overflow_flags, compact_stride, chunks, selected_indices, selected_counts, params); + } else { + for (int list_width = kBoundedTopKMax; list_width < compact_stride; list_width *= 2) { + const int pairs_per_row = (compact_stride + 2 * list_width - 1) / (2 * list_width); + const int64_t merge_work = static_cast(params.total_tokens) * pairs_per_row; + QsaMergeTileTopKKernel<<(std::min(merge_work, kSaiMaxGridDimX)), + kHierarchicalMergeThreads, 0, stream>>>( + merge_input, merge_output, compact_stride, list_width, params.total_tokens); + std::swap(merge_input, merge_output); + } + QsaEmitHierarchicalTopKKernel<<>>( + merge_input, cumulative_sequence_lengths, past_sequence_lengths, position_ids, present_state_lengths, + overflow_flags, compact_stride, selected_indices, selected_counts, params); + } + return CUDA_CALL(cudaGetLastError()); + } else { + for (int list_width = kHierarchicalTopKTileBlocks; list_width < key_stride;) { + if (2 * list_width < key_stride) { + const int groups_per_row = (key_stride + 4 * list_width - 1) / (4 * list_width); + const int64_t merge_work = static_cast(params.total_tokens) * groups_per_row; + QsaMergeTwoLevelsTopKKernel<<(std::min(merge_work, kSaiMaxGridDimX)), + kHierarchicalMergeThreads, 0, stream>>>( + merge_input, merge_output, key_stride, list_width, params.total_tokens); + list_width *= 4; + } else { + const int pairs_per_row = (key_stride + 2 * list_width - 1) / (2 * list_width); + const int64_t merge_work = static_cast(params.total_tokens) * pairs_per_row; + QsaMergeTileTopKKernel<<(std::min(merge_work, kSaiMaxGridDimX)), + kHierarchicalMergeThreads, 0, stream>>>( + merge_input, merge_output, key_stride, list_width, params.total_tokens); + list_width *= 2; + } + std::swap(merge_input, merge_output); + } } QsaEmitHierarchicalTopKKernel<<>>( merge_input, cumulative_sequence_lengths, past_sequence_lengths, position_ids, present_state_lengths, - overflow_flags, compact_stride, selected_indices, selected_counts, params); + overflow_flags, key_stride, selected_indices, selected_counts, params); } else if (use_ragged_prefill) { - QsaRaggedPrefillScoreKernel<<>>( - present_key_state, query_rotated, cumulative_sequence_lengths, past_sequence_lengths, position_ids, - present_state_lengths, overflow_flags, block_scores, params); + static const bool warp_heads = + ParseEnvironmentVariableWithDefault("ORT_PACKED_SPARSE_INDEXER_WARP_HEADS", true); + if (warp_heads) { + QsaRaggedPrefillScoreKernel<<>>( + present_key_state, query_rotated, cumulative_sequence_lengths, past_sequence_lengths, position_ids, + present_state_lengths, overflow_flags, block_scores, params); + } else { + QsaRaggedPrefillScoreKernel<<>>( + present_key_state, query_rotated, cumulative_sequence_lengths, past_sequence_lengths, position_ids, + present_state_lengths, overflow_flags, block_scores, params); + } } else if (params.state_capacity > 0) { const int64_t score_work = static_cast(params.total_tokens) * params.state_capacity; const int score_blocks = static_cast(std::min(score_work, kSaiMaxGridDimX)); - QsaBlockScoreKernel<<>>( + QsaBlockScoreKernel<<>>( present_key_state, query_rotated, cumulative_sequence_lengths, past_sequence_lengths, position_ids, present_state_lengths, block_scores, params); } @@ -1602,11 +1745,10 @@ Status LaunchCsaPackedSparseAttentionIndexer( } const size_t value_bytes = static_cast(params.head_size) * sizeof(float); - const size_t update_shared_bytes = value_bytes + kThreads * sizeof(float); - ORT_RETURN_IF_ERROR( - ConfigureDynamicSharedMemory(CsaUpdateStateKernel, update_shared_bytes, "CsaUpdateStateKernel")); const int state_blocks = static_cast(std::min(params.batch_size, kSaiMaxGridDimX)); - CsaUpdateStateKernel<<>>( + ORT_RETURN_IF_ERROR(ConfigureDynamicSharedMemory( + CsaUpdateStateKernel, value_bytes + kThreads * sizeof(float), "CsaUpdateStateKernel")); + CsaUpdateStateKernel<<>>( key, gate, key_norm_weight, cos_cache, sin_cache, position_bias, cumulative_sequence_lengths, past_sequence_lengths, past_kv_buffer, past_gate_buffer, past_state_lengths, present_key_state, present_kv_buffer, present_gate_buffer, present_state_lengths, overflow_flags, params); @@ -1620,10 +1762,9 @@ Status LaunchCsaPackedSparseAttentionIndexer( const int64_t rotate_rows = static_cast(params.total_tokens) * params.num_heads; const int rotate_blocks = static_cast(std::min(rotate_rows, kSaiMaxGridDimX)); - const size_t rotate_shared_bytes = value_bytes + kThreads * sizeof(float); ORT_RETURN_IF_ERROR(ConfigureDynamicSharedMemory( - PackedRotateQueryKernel, rotate_shared_bytes, "PackedRotateQueryKernel")); - PackedRotateQueryKernel<<>>( + PackedRotateQueryKernel, value_bytes + kThreads * sizeof(float), "PackedRotateQueryKernel")); + PackedRotateQueryKernel<<>>( query, query_norm_weight, cos_cache, sin_cache, cumulative_sequence_lengths, past_sequence_lengths, position_ids, query_rotated, params); @@ -1643,21 +1784,29 @@ Status LaunchCsaPackedSparseAttentionIndexer( return CUDA_CALL(cudaGetLastError()); } -#define INSTANTIATE_PACKED_SPARSE_ATTENTION_INDEXER(T) \ - template Status LaunchQsaPackedSparseAttentionIndexer( \ - cudaStream_t, const PackedSparseAttentionIndexerParams&, const T*, const T*, const T*, const T*, \ - const T*, const T*, const int32_t*, const int32_t*, const int64_t*, const T*, const T*, const int32_t*, \ - const int32_t*, bool, int32_t*, int32_t*, T*, T*, int32_t*, T*, float*, int32_t*); \ +#define INSTANTIATE_QSA(T, TState) \ + template Status LaunchQsaPackedSparseAttentionIndexer( \ + cudaStream_t, const PackedSparseAttentionIndexerParams&, const T*, const T*, const T*, const T*, \ + const T*, const T*, const int32_t*, const int32_t*, const int64_t*, const TState*, const T*, \ + const int32_t*, const int32_t*, bool, int32_t*, int32_t*, TState*, T*, int32_t*, T*, float*, int32_t*); + +#define INSTANTIATE_CSA(T) \ template Status LaunchCsaPackedSparseAttentionIndexer( \ cudaStream_t, const PackedSparseAttentionIndexerParams&, const T*, const T*, const T*, const T*, \ const T*, const T*, const T*, const T*, const T*, const int32_t*, const int32_t*, const int64_t*, const T*, \ const T*, const T*, const int32_t*, int32_t*, int32_t*, T*, T*, T*, int32_t*, float*, int32_t*); -INSTANTIATE_PACKED_SPARSE_ATTENTION_INDEXER(float) -INSTANTIATE_PACKED_SPARSE_ATTENTION_INDEXER(half) -INSTANTIATE_PACKED_SPARSE_ATTENTION_INDEXER(__nv_bfloat16) +INSTANTIATE_QSA(float, float) +INSTANTIATE_QSA(half, half) +INSTANTIATE_QSA(__nv_bfloat16, __nv_bfloat16) +INSTANTIATE_QSA(__nv_bfloat16, float) + +INSTANTIATE_CSA(float) +INSTANTIATE_CSA(half) +INSTANTIATE_CSA(__nv_bfloat16) -#undef INSTANTIATE_PACKED_SPARSE_ATTENTION_INDEXER +#undef INSTANTIATE_QSA +#undef INSTANTIATE_CSA } // namespace cuda } // namespace contrib diff --git a/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer_impl.h b/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer_impl.h index 8bde6e1cca07b..0d2f49ada9f88 100644 --- a/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer_impl.h +++ b/onnxruntime/contrib_ops/cuda/sparse/packed_sparse_attention_indexer_impl.h @@ -61,7 +61,7 @@ Status LaunchValidatePackedIndexerReuse(cudaStream_t stream, const int32_t* cumu // unchanged and present_state_lengths is left as past_state_lengths clamped to valid bounds) and // the select kernels force that request's selected_indices/selected_counts to the safe empty // result (-1 / 0) rather than selecting against a partially updated state. -template +template Status LaunchQsaPackedSparseAttentionIndexer( cudaStream_t stream, const PackedSparseAttentionIndexerParams& params, @@ -74,14 +74,14 @@ Status LaunchQsaPackedSparseAttentionIndexer( const int32_t* cumulative_sequence_lengths, const int32_t* past_sequence_lengths, const int64_t* position_ids, - const T* past_key_state, + const TState* past_key_state, const T* past_kv_buffer, const int32_t* past_state_lengths, const int32_t* state_update_capture_count, bool state_update_active, int32_t* selected_indices, int32_t* selected_counts, - T* present_key_state, + TState* present_key_state, T* present_kv_buffer, int32_t* present_state_lengths, T* state_update, diff --git a/onnxruntime/contrib_ops/cuda/sparse/qsa_topk.cuh b/onnxruntime/contrib_ops/cuda/sparse/qsa_topk.cuh new file mode 100644 index 0000000000000..6b8133c78b54c --- /dev/null +++ b/onnxruntime/contrib_ops/cuda/sparse/qsa_topk.cuh @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include "core/providers/cuda/cu_inc/topk_warp_sort.cuh" + +namespace onnxruntime::contrib::cuda::qsa_topk { + +constexpr int kChunkSize = 2048; +constexpr int kTopK = 512; +constexpr int kThreads = 512; + +namespace topk = onnxruntime::cuda::topk; + +template +using ChunkSort = cub::BlockMergeSort; + +template +__device__ __forceinline__ void SortChunk(const uint64_t* input, uint64_t* output, int input_count, + typename ChunkSort::TempStorage& temp) { + uint64_t keys[ItemsPerThread]; +#pragma unroll + for (int item = 0; item < ItemsPerThread; ++item) { + const int index = threadIdx.x * ItemsPerThread + item; + keys[item] = index < input_count ? input[index] : topk::kPaddingSortKey; + } + ChunkSort(temp).Sort(keys, topk::Greater()); +#pragma unroll + for (int item = 0; item < ItemsPerThread; ++item) { + const int rank = threadIdx.x * ItemsPerThread + item; + if (rank < kTopK) output[rank] = keys[item]; + } +} + +__global__ void QsaCompactTileTopKKernel(const uint64_t* input_keys, uint64_t* output_keys, + int input_stride, int output_stride) { + __shared__ union { + ChunkSort<1>::TempStorage s1; + ChunkSort<2>::TempStorage s2; + ChunkSort<4>::TempStorage s4; + } temp; + const int chunk_start = static_cast(blockIdx.x) * kChunkSize; + const uint64_t* input = input_keys + static_cast(blockIdx.y) * input_stride + chunk_start; + uint64_t* output = output_keys + static_cast(blockIdx.y) * output_stride + blockIdx.x * kTopK; + const int count = min(kChunkSize, input_stride - chunk_start); + + // Scoring emits sorted 32-key tiles in a live prefix followed by zero padding. + // Tile-aligned probes adapt to device context growth without graph recapture. + if (input[0] == topk::kPaddingSortKey) { + output[threadIdx.x] = topk::kPaddingSortKey; + } else if (count > 1024 && input[1024] != topk::kPaddingSortKey) { + SortChunk<4>(input, output, count, temp.s4); + } else if (count > 512 && input[512] != topk::kPaddingSortKey) { + SortChunk<2>(input, output, min(count, 1024), temp.s2); + } else { + SortChunk<1>(input, output, min(count, 512), temp.s1); + } +} + +} // namespace onnxruntime::contrib::cuda::qsa_topk diff --git a/onnxruntime/contrib_ops/webgpu/bert/dynamic_sparse_attention.cc b/onnxruntime/contrib_ops/webgpu/bert/dynamic_sparse_attention.cc index ee4b4d299a40f..3d796a25fe71c 100644 --- a/onnxruntime/contrib_ops/webgpu/bert/dynamic_sparse_attention.cc +++ b/onnxruntime/contrib_ops/webgpu/bert/dynamic_sparse_attention.cc @@ -66,6 +66,7 @@ ONNX_OPERATOR_KERNEL_EX( kWebGpuExecutionProvider, (*KernelDefBuilder::Create()) .TypeConstraint("T", WebGpuSupportedFloatTypes()) + .TypeConstraint("TCache", WebGpuSupportedFloatTypes()) .TypeConstraint("M", DataTypeImpl::GetTensorType()) .MayInplace(3, 1) .MayInplace(4, 2) @@ -550,11 +551,22 @@ DynamicSparseAttention::DynamicSparseAttention(const OpKernelInfo& info) : WebGp } Status DynamicSparseAttention::ComputeInternal(onnxruntime::webgpu::ComputeContext& context) const { + ORT_RETURN_IF(context.InputCount() > 17 && context.Input(17) != nullptr, + "DynamicSparseAttention: committed_length is only supported by CUDA."); + Tensor* validation_status = context.OutputCount() > 3 ? context.Output(3, TensorShape({0})) : nullptr; + ORT_RETURN_IF(validation_status != nullptr, + "DynamicSparseAttention: validation_status is only supported by CUDA."); const Tensor* query = context.Input(0); const Tensor* key = context.Input(1); const Tensor* value = context.Input(2); const Tensor* past_key = context.Input(3); const Tensor* past_value = context.Input(4); + ORT_RETURN_IF_NOT((past_key == nullptr || past_key->GetElementType() == query->GetElementType()) && + (past_value == nullptr || past_value->GetElementType() == query->GetElementType()), + "DynamicSparseAttention: WebGPU cache types must match the query."); + ORT_RETURN_IF_NOT((context.InputCount() <= 18 || context.Input(18) == nullptr) && + (context.InputCount() <= 19 || context.Input(19) == nullptr), + "DynamicSparseAttention: INT8 cache scales are only supported by CUDA."); const Tensor* auxiliary_key = context.Input(5); const Tensor* auxiliary_value = context.Input(6); const Tensor* selected_indices = context.Input(7); diff --git a/onnxruntime/contrib_ops/webgpu/bert/packed_sparse_attention_indexer.cc b/onnxruntime/contrib_ops/webgpu/bert/packed_sparse_attention_indexer.cc index df0dff40ec3dd..99d36d449f504 100644 --- a/onnxruntime/contrib_ops/webgpu/bert/packed_sparse_attention_indexer.cc +++ b/onnxruntime/contrib_ops/webgpu/bert/packed_sparse_attention_indexer.cc @@ -36,6 +36,7 @@ ONNX_OPERATOR_KERNEL_EX( kWebGpuExecutionProvider, (*KernelDefBuilder::Create()) .TypeConstraint("T", WebGpuSupportedFloatTypes()) + .TypeConstraint("T_KEY", WebGpuSupportedFloatTypes()) .TypeConstraint("I", DataTypeImpl::GetTensorType()) .TypeConstraint("M", DataTypeImpl::GetTensorType()) .MayInplace(psai::kPastKeyState, psai::kPresentKeyState) @@ -895,6 +896,10 @@ PackedSparseAttentionIndexer::PackedSparseAttentionIndexer(const OpKernelInfo& i Status PackedSparseAttentionIndexer::ComputeInternal(onnxruntime::webgpu::ComputeContext& context) const { ORT_RETURN_IF(context.InputCount() > psai::kIndexShareMode && context.Input(psai::kIndexShareMode), "PackedSparseAttentionIndexer IndexShare requires the CUDA provider"); + const Tensor* query = context.Input(psai::kQuery); + const Tensor* key_state = context.Input(psai::kPastKeyState); + ORT_RETURN_IF(query != nullptr && key_state != nullptr && query->DataType() != key_state->DataType(), + "PackedSparseAttentionIndexer: mixed input/state types require the CUDA provider"); ORT_RETURN_IF( context.OutputCount() > psai::kStateStatus && context.Output(psai::kStateStatus, TensorShape({0})) != nullptr, "PackedSparseAttentionIndexer state_status requires the CUDA provider"); @@ -1148,9 +1153,9 @@ Status PackedSparseAttentionIndexer::ComputeQsa(onnxruntime::webgpu::ComputeCont .AddInputs({{&query_metadata, ProgramTensorMetadataDependency::Type}, {present_key_state, ProgramTensorMetadataDependency::Type}, {&rotary_cache, ProgramTensorMetadataDependency::Type}, - {&sequence_metadata, ProgramTensorMetadataDependency::Type}}); - select.AddInputs({{present_state_lengths, ProgramTensorMetadataDependency::Type}, - {&overflow_flags, ProgramTensorMetadataDependency::Type}}) + {&sequence_metadata, ProgramTensorMetadataDependency::Type}, + {present_state_lengths, ProgramTensorMetadataDependency::Type}, + {&overflow_flags, ProgramTensorMetadataDependency::Type}}) .AddOutputs({{selected_indices, ProgramTensorMetadataDependency::Type}, {selected_counts, ProgramTensorMetadataDependency::Type}}) .SetDispatchGroupSize(ToUint32(total_tokens)) diff --git a/onnxruntime/core/graph/contrib_ops/bert_defs.cc b/onnxruntime/core/graph/contrib_ops/bert_defs.cc index b18836cf0b764..de18f6c841462 100644 --- a/onnxruntime/core/graph/contrib_ops/bert_defs.cc +++ b/onnxruntime/core/graph/contrib_ops/bert_defs.cc @@ -483,7 +483,6 @@ void GroupQueryAttentionTypeAndShapeInference(ONNX_NAMESPACE::InferenceContext& void DynamicSparseAttentionTypeAndShapeInference(ONNX_NAMESPACE::InferenceContext& ctx) { ONNX_NAMESPACE::propagateElemTypeFromInputToOutput(ctx, 0, 0); - const int64_t num_heads = getAttribute(ctx, "num_heads", 0); const int64_t kv_num_heads = getAttribute(ctx, "kv_num_heads", 0); if (num_heads <= 0 || num_heads > std::numeric_limits::max()) { @@ -524,7 +523,8 @@ void DynamicSparseAttentionTypeAndShapeInference(ONNX_NAMESPACE::InferenceContex continue; } - ONNX_NAMESPACE::propagateElemTypeFromInputToOutput(ctx, 0, output_index); + ONNX_NAMESPACE::propagateElemTypeFromInputToOutput( + ctx, ctx.getInputType(input_index) != nullptr ? input_index : 0, output_index); if (hasInputShape(ctx, input_index)) { ONNX_NAMESPACE::propagateShapeFromInputToOutput(ctx, input_index, output_index); continue; @@ -589,6 +589,14 @@ The main cache uses BNSH layout and ordinary contiguous append semantics. Select Auxiliary K/V use BNSH layout and are read-only. When `auxiliary_kv_shared=1`, auxiliary_value may be omitted and auxiliary_key is used as both K and V. DeepSeek-style post-attention inverse/conjugate RoPE and output projection remain the exporter's responsibility. + +Main caches may instead use INT8 with positive finite float32 key_scale and value_scale vectors, one scale per +KV-head channel. The cache is symmetric signed int8 without a zero point, like the 8-bit KV cache of GroupQueryAttention +and PagedAttention; tensor(uint8) is reserved for packed 4-bit caches in those operators and is not accepted here. +Newly appended keys (after optional normalization and RoPE) and values are divided by their scales, +rounded to nearest with ties to even, and saturated to [-128, 127]. Attention reads the quantized representation, +including newly appended rows. Auxiliary caches remain floating point. Invalid scale values produce validation_status=10 +when validation is requested; CUDA strict validation also reports them as an error. )DOC"; ONNX_MS_OPERATOR_SET_SCHEMA( @@ -629,8 +637,8 @@ ONNX_MS_OPERATOR_SET_SCHEMA( OpSchema::Optional) .Input(2, "value", "Current main value [batch, sequence, kv_num_heads * head_size].", "T", OpSchema::Optional) - .Input(3, "past_key", "Main key cache in BNSH layout.", "T", OpSchema::Optional) - .Input(4, "past_value", "Main value cache in BNSH layout.", "T", OpSchema::Optional) + .Input(3, "past_key", "Main key cache in BNSH layout, same type as query or INT8.", "TCache", OpSchema::Optional) + .Input(4, "past_value", "Main value cache in BNSH layout, same type as past_key.", "TCache", OpSchema::Optional) .Input(5, "auxiliary_key", "Read-only auxiliary key sequence in BNSH layout.", "T", OpSchema::Optional) .Input(6, "auxiliary_value", "Read-only auxiliary value sequence in BNSH layout.", "T", @@ -652,13 +660,38 @@ ONNX_MS_OPERATOR_SET_SCHEMA( .Input(16, "head_sink", "Optional sink logit [num_heads]. Participates in the shared softmax denominator in both attention modes.", "T", OpSchema::Optional) + .Input(17, "committed_length", + "Optional per-request committed token count ([1] or [batch]) for cursor validation. When supplied, " + "validation_status is 9 if seqlens_k, sequence_length, or cache_capacity is inconsistent, and " + "CUDA strict validation fails with a cursor-specific error. CUDA only.", + "M", OpSchema::Optional) + .Input(18, "key_scale", "Positive finite per-channel INT8 key-cache scales [kv_num_heads * head_size].", + "S", OpSchema::Optional) + .Input(19, "value_scale", "Positive finite per-channel INT8 value-cache scales [kv_num_heads * head_size].", + "S", OpSchema::Optional) .Output(0, "output", "Attention output [batch, sequence, num_heads * head_size].", "T") - .Output(1, "present_key", "Updated main key cache in BNSH layout.", "T", OpSchema::Optional) - .Output(2, "present_value", "Updated main value cache in BNSH layout.", "T", OpSchema::Optional) + .Output(1, "present_key", "Updated main key cache in BNSH layout.", "TCache", OpSchema::Optional) + .Output(2, "present_value", "Updated main value cache in BNSH layout.", "TCache", OpSchema::Optional) + .Output(3, "validation_status", + "Optional CUDA-only int32 status: 0 when valid, otherwise a validation code (1-8: selection, " + "sequence-length or rotary-position errors; 9: inconsistent committed_length cursor; " + "10: invalid INT8 cache scale).", + "M", OpSchema::Optional) .TypeConstraint("T", {"tensor(float)", "tensor(float16)", "tensor(bfloat16)"}, "Constrain all floating-point inputs and outputs to one element type.") + .TypeConstraint("TCache", {"tensor(float)", "tensor(float16)", "tensor(bfloat16)", "tensor(int8)"}, + "Cache type must match T unless INT8 per-channel scales are supplied.") + .TypeConstraint("S", {"tensor(float)"}, "INT8 cache scales use float32.") .TypeConstraint("M", {"tensor(int32)"}, "Constrain selection and sequence metadata to int32.") - .TypeAndShapeInferenceFunction(DynamicSparseAttentionTypeAndShapeInference)); + .TypeAndShapeInferenceFunction([](ONNX_NAMESPACE::InferenceContext& ctx) { + DynamicSparseAttentionTypeAndShapeInference(ctx); + if (ctx.getNumOutputs() > 3 && ctx.hasOutput(3)) { + updateOutputElemType(ctx, 3, ONNX_NAMESPACE::TensorProto_DataType_INT32); + ONNX_NAMESPACE::TensorShapeProto status_shape; + status_shape.add_dim()->set_dim_value(1); + updateOutputShape(ctx, 3, status_shape); + } + })); constexpr const char* Attention_ver1_doc = R"DOC( Multi-Head Attention that can be either unidirectional (like GPT-2) or bidirectional (like BERT). @@ -2914,7 +2947,7 @@ void PackedSparseAttentionIndexerTypeAndShapeInference(ONNX_NAMESPACE::Inference propagateElemTypeFromInputToOutput(ctx, psai::kPastGateBuffer, psai::kPresentGateBuffer); } if (has_state_update_output) { - propagateElemTypeFromInputToOutput(ctx, psai::kPastKeyState, psai::kStateUpdate); + propagateElemTypeFromInputToOutput(ctx, psai::kPastKvBuffer, psai::kStateUpdate); } const auto* query_shape = PackedSparseAttentionIndexerShape(ctx, psai::kQuery, 2); @@ -3195,8 +3228,8 @@ request's own packed token range and fixed-capacity state slice: sum_h ReLU(q_h . k), the token_budget / compress_ratio highest scoring blocks are kept, and their token indices are emitted (request-local logical positions, i.e. the same numbering as past_sequence_lengths + local offset) followed by the causally visible tokens of the trailing - incomplete block. QSA positions are the request-local logical cache positions derived from - past_sequence_lengths and cumulative_sequence_lengths; position_ids must be omitted. + incomplete block. There is no position_ids input: QSA positions are the request-local logical + cache positions derived from past_sequence_lengths and cumulative_sequence_lengths. policy_mode = "csa" ("compressed sparse attention" block indexer) Applies the same window-plan arithmetic as SparseAttentionIndexer (overlap/leftover/new window @@ -3346,8 +3379,9 @@ ONNX_MS_OPERATOR_SET_SCHEMA( "past_key_state", "Generic fixed-capacity state: policy_mode 'qsa' stores prepared complete-block keys; " "policy_mode 'csa' stores compressed keys. Shape is (batch_size, state_capacity, head_size) and " - "never changes across calls.", - "T") + "never changes across calls. CUDA QSA also supports FLOAT key state with BFLOAT16 " + "query/key tensors. WebGPU requires key state and activations to have the same type.", + "T_KEY") .Input(13, "past_kv_buffer", "Generic fixed-capacity pending-token buffer. Shape is (batch_size, 2 * compress_ratio - 1, " @@ -3401,7 +3435,7 @@ ONNX_MS_OPERATOR_SET_SCHEMA( .Output(2, "present_key_state", "Updated generic key state, with the same fixed shape as past_key_state.", - "T") + "T_KEY") .Output(3, "present_kv_buffer", "Updated generic pending-token buffer, with the same fixed shape as past_kv_buffer.", @@ -3440,6 +3474,9 @@ ONNX_MS_OPERATOR_SET_SCHEMA( .TypeConstraint("T", {"tensor(float)", "tensor(float16)", "tensor(bfloat16)"}, "Constrain floating point tensors to float, float16 and bfloat16.") + .TypeConstraint("T_KEY", + {"tensor(float)", "tensor(float16)", "tensor(bfloat16)"}, + "Constrain the generic key-state tensor to float, float16 and bfloat16.") .TypeConstraint("I", {"tensor(int64)"}, "Constrain position ids to 64-bit integer tensors.") .TypeConstraint("M", {"tensor(int32)"}, "Constrain packed metadata, generic state lengths and selected indices/counts to 32-bit " diff --git a/onnxruntime/test/contrib_ops/dynamic_sparse_attention_op_test.cc b/onnxruntime/test/contrib_ops/dynamic_sparse_attention_op_test.cc index 668be084ea1c2..003012a5a6a1b 100644 --- a/onnxruntime/test/contrib_ops/dynamic_sparse_attention_op_test.cc +++ b/onnxruntime/test/contrib_ops/dynamic_sparse_attention_op_test.cc @@ -29,8 +29,11 @@ #endif #ifdef USE_CUDA +#include "core/framework/to_tensor_proto_element_type.h" #include "core/providers/cuda/cuda_provider_options.h" #include "test/common/cuda_op_test_utils.h" +// Uses std::system, which is unavailable on iOS, so only CUDA builds (Windows/Linux) include it. +#include "test/contrib_ops/fresh_process_test_util.h" #endif #ifdef USE_WEBGPU @@ -88,6 +91,10 @@ struct DynamicSparseAttentionCase { std::vector q_norm_weight; std::vector k_norm_weight; std::vector head_sink; + std::vector key_scale; + std::vector value_scale; + std::optional committed_length; + std::optional expected_validation_status; std::vector expected_output; std::vector expected_present_key; @@ -102,13 +109,13 @@ std::vector ToTensorData(const std::vector& values) { std::vector converted; converted.reserve(values.size()); for (float value : values) { - converted.emplace_back(value); + converted.emplace_back(static_cast(value)); } return converted; } } -template +template void RunDynamicSparseAttentionCase( const DynamicSparseAttentionCase& c, std::unique_ptr execution_provider, @@ -163,12 +170,12 @@ void RunDynamicSparseAttentionCase( ToTensorData(c.value)); } if (c.has_past) { - tester.AddInput("past_key", - {c.batch_size, c.kv_num_heads, c.cache_sequence_length, c.head_size}, - ToTensorData(c.past_key)); - tester.AddInput("past_value", - {c.batch_size, c.kv_num_heads, c.cache_sequence_length, c.head_size}, - ToTensorData(c.past_value)); + tester.AddInput("past_key", + {c.batch_size, c.kv_num_heads, c.cache_sequence_length, c.head_size}, + ToTensorData(c.past_key)); + tester.AddInput("past_value", + {c.batch_size, c.kv_num_heads, c.cache_sequence_length, c.head_size}, + ToTensorData(c.past_value)); } else { tester.AddOptionalInputEdge(); tester.AddOptionalInputEdge(); @@ -229,15 +236,33 @@ void RunDynamicSparseAttentionCase( } else { tester.AddInput("head_sink", {c.num_heads}, ToTensorData(c.head_sink)); } + if (c.committed_length.has_value()) { + tester.AddInput("committed_length", {1}, {*c.committed_length}); + } else { + tester.AddOptionalInputEdge(); + } + if (!c.key_scale.empty() || !c.value_scale.empty()) { + if (c.key_scale.empty()) + tester.AddOptionalInputEdge(); + else + tester.AddInput("key_scale", {int64_t(c.key_scale.size())}, c.key_scale); + if (c.value_scale.empty()) + tester.AddOptionalInputEdge(); + else + tester.AddInput("value_scale", {int64_t(c.value_scale.size())}, c.value_scale); + } tester.AddOutput("output", {c.batch_size, c.sequence_length, hidden_size}, ToTensorData(c.expected_output)); - tester.AddOutput("present_key", - {c.batch_size, c.kv_num_heads, c.cache_sequence_length, c.head_size}, - ToTensorData(c.expected_present_key)); - tester.AddOutput("present_value", - {c.batch_size, c.kv_num_heads, c.cache_sequence_length, c.head_size}, - ToTensorData(c.expected_present_value)); + tester.AddOutput("present_key", + {c.batch_size, c.kv_num_heads, c.cache_sequence_length, c.head_size}, + ToTensorData(c.expected_present_key)); + tester.AddOutput("present_value", + {c.batch_size, c.kv_num_heads, c.cache_sequence_length, c.head_size}, + ToTensorData(c.expected_present_value)); + if (c.expected_validation_status.has_value()) { + tester.AddOutput("validation_status", {1}, {*c.expected_validation_status}); + } tester.SetOutputTolerance(0.005f); std::vector> execution_providers; @@ -276,6 +301,450 @@ DynamicSparseAttentionCase MakeSingleTokenSelectedValueCase(float value = 9.0f) } #ifdef USE_CUDA +DynamicSparseAttentionCase MakeInt8CacheCase() { + DynamicSparseAttentionCase c; + c.sequence_length = 2; + c.num_heads = 4; + c.kv_num_heads = 2; + c.cache_sequence_length = 4; + c.max_selected = 3; + c.query.assign(64, 0.0f); + c.key.resize(32); + c.value.resize(32); + c.key_scale.resize(16); + c.value_scale.resize(16); + c.past_key.resize(64); + c.past_value.resize(64); + for (size_t i = 0; i < 64; ++i) { + c.past_key[i] = float(int(i % 13) - 6); + c.past_value[i] = float(int(i % 17) - 8); + } + for (size_t i = 0; i < 16; ++i) { + c.key_scale[i] = i < 8 ? 0.5f : 0.25f; + c.value_scale[i] = i < 8 ? 0.25f : 0.5f; + } + for (size_t i = 0; i < 32; ++i) { + c.key[i] = i % 3 == 0 ? -300.0f : 0.25f * float(i); + c.value[i] = i % 3 == 0 ? 300.0f : 0.125f * float(i); + } + c.key[0] = -1e20f; + c.value[0] = 1e20f; + c.selected_indices = {0, 2, -1, 1, 2, 3}; + c.selected_counts = {2, 3}; + c.seqlens_k = {3}; + c.total_sequence_length = 4; + c.expected_present_key = c.past_key; + c.expected_present_value = c.past_value; + for (int s = 0; s < 2; ++s) { + for (int h = 0; h < 2; ++h) { + for (int d = 0; d < 8; ++d) { + const int channel = h * 8 + d; + const int src = s * 16 + channel; + const int dst = (h * 4 + 2 + s) * 8 + d; + c.expected_present_key[dst] = std::clamp(std::nearbyint(c.key[src] / c.key_scale[channel]), -128.0f, 127.0f); + c.expected_present_value[dst] = std::clamp(std::nearbyint(c.value[src] / c.value_scale[channel]), -128.0f, 127.0f); + } + } + } + c.expected_output.assign(64, 0.0f); + for (int s = 0; s < 2; ++s) { + for (int h = 0; h < 4; ++h) { + const int kv = h / 2; + for (int d = 0; d < 8; ++d) { + float sum = 0.0f; + for (int i = 0; i < c.selected_counts[s]; ++i) { + const int pos = c.selected_indices[s * 3 + i]; + sum += c.expected_present_value[(kv * 4 + pos) * 8 + d] * c.value_scale[kv * 8 + d]; + } + c.expected_output[(s * 4 + h) * 8 + d] = sum / c.selected_counts[s]; + } + } + } + return c; +} + +TEST(DynamicSparseAttentionTest, Int8CachePerChannelGqaAppendAndAttention_CUDA) { + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP(); + RunDynamicSparseAttentionCase(MakeInt8CacheCase(), std::move(cuda_ep)); +} + +TEST(DynamicSparseAttentionTest, Int8CacheBFloat16_CUDA) { + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP(); + RunDynamicSparseAttentionCase(MakeInt8CacheCase(), std::move(cuda_ep)); +} + +TEST(DynamicSparseAttentionTest, Int8CacheSplitAttention_CUDA) { + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP(); + auto c = MakeInt8CacheCase(); + c.cache_sequence_length = 1024; + c.total_sequence_length = 1024; + c.seqlens_k = {1023}; + c.max_selected = 512; + c.selected_counts = {512, 512}; + c.selected_indices.resize(1024); + std::iota(c.selected_indices.begin(), c.selected_indices.end(), 0); + c.past_key.assign(2 * 1024 * 8, 0.0f); + c.past_value.assign(2 * 1024 * 8, 3.0f); + auto key = c.past_key; + auto value = c.past_value; + for (int h = 0; h < 2; ++h) { + for (int s = 0; s < 2; ++s) { + for (int d = 0; d < 8; ++d) { + key[(h * 1024 + 1022 + s) * 8 + d] = c.expected_present_key[(h * 4 + 2 + s) * 8 + d]; + value[(h * 1024 + 1022 + s) * 8 + d] = c.expected_present_value[(h * 4 + 2 + s) * 8 + d]; + } + } + } + c.expected_present_key = key; + c.expected_present_value = value; + for (int s = 0; s < 2; ++s) { + for (int h = 0; h < 4; ++h) { + for (int d = 0; d < 8; ++d) { + float sum = 0.0f; + for (int i = 0; i < 512; ++i) { + sum += value[((h / 2) * 1024 + s * 512 + i) * 8 + d] * c.value_scale[(h / 2) * 8 + d]; + } + c.expected_output[(s * 4 + h) * 8 + d] = sum / 512.0f; + } + } + } + RunDynamicSparseAttentionCase(c, std::move(cuda_ep)); +} + +TEST(DynamicSparseAttentionTest, Int8CacheRejectsMissingOrMalformedScales_CUDA) { + for (int variant = 0; variant < 3; ++variant) { + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP(); + auto c = MakeInt8CacheCase(); + if (variant == 0) { + c.key_scale.clear(); + c.value_scale.clear(); + } else if (variant == 1) { + c.value_scale.clear(); + } else { + c.key_scale.resize(8); + } + const char* error = variant == 0 ? "INT8 caches require per-channel" + : variant == 1 ? "must be provided together" + : "INT8 cache scales must have shape"; + RunDynamicSparseAttentionCase(c, std::move(cuda_ep), + OpTester::ExpectResult::kExpectFailure, error); + } +} + +DynamicSparseAttentionCase MakeGroupedCacheCase(int sequence_length, bool auxiliary, + bool floating_cache = false, int batch_size = 2, + int num_heads = 20, int kv_num_heads = 2, + bool varied_channels = false, int selected_count = 509) { + DynamicSparseAttentionCase c; + c.batch_size = batch_size; + c.sequence_length = sequence_length; + c.num_heads = num_heads; + c.kv_num_heads = kv_num_heads; + c.head_size = 256; + c.cache_sequence_length = 520; + c.max_selected = 512; + c.attention_mode = auxiliary ? "local_plus_selected" : "selected_only"; + c.local_window_size = auxiliary ? 16 : -1; + c.smooth_softmax = 1; + c.scale = 0.0625f; + c.head_sink.assign(c.num_heads, 0.25f); + c.seqlens_k.resize(batch_size); + for (int b = 0; b < batch_size; ++b) c.seqlens_k[b] = 519 - b % 2; + c.total_sequence_length = 520; + const int channels = static_cast(c.kv_num_heads * c.head_size); + const int rows = static_cast(c.batch_size * c.sequence_length); + c.query.resize(rows * c.num_heads * c.head_size); + c.key.resize(rows * channels); + c.value.resize(rows * channels); + c.key_scale.resize(channels); + c.value_scale.resize(channels); + for (int d = 0; d < channels; ++d) { + c.key_scale[d] = floating_cache ? 1.0f : (varied_channels ? (d % 8 + 1) * 0.03125f : (d % 2 ? 0.125f : 0.0625f)); + c.value_scale[d] = floating_cache ? 1.0f : (varied_channels ? (8 - d % 8) * 0.0625f : (d % 2 ? 0.25f : 0.125f)); + } + for (size_t i = 0; i < c.query.size(); ++i) { + c.query[i] = float(int(i % 7) - 3) * (varied_channels ? 0.00390625f : 0.125f); + } + for (size_t i = 0; i < c.key.size(); ++i) { + c.key[i] = float(int(i % 11) - 5) * c.key_scale[i % channels]; + c.value[i] = float(int(i % 13) - 6) * c.value_scale[i % channels]; + } + c.past_key.resize(c.batch_size * c.cache_sequence_length * channels); + c.past_value.resize(c.past_key.size()); + constexpr int8_t kSignedBytes[] = {-128, -127, -65, -1, 0, 1, 64, 127}; + for (size_t i = 0; i < c.past_key.size(); ++i) { + c.past_key[i] = varied_channels ? float(kSignedBytes[i % 8]) : float(int(i % 17) - 8); + c.past_value[i] = varied_channels ? float(kSignedBytes[7 - i % 8]) : float(int(i % 19) - 9); + } + c.expected_present_key = c.past_key; + c.expected_present_value = c.past_value; + for (int row = 0; row < rows; ++row) { + const int b = row / sequence_length; + const int pos = c.seqlens_k[b] + 1 - sequence_length + row % sequence_length; + for (int d = 0; d < channels; ++d) { + const int64_t dst = + ((b * c.kv_num_heads + d / c.head_size) * c.cache_sequence_length + pos) * c.head_size + + d % c.head_size; + c.expected_present_key[dst] = c.key[row * channels + d] / c.key_scale[d]; + c.expected_present_value[dst] = c.value[row * channels + d] / c.value_scale[d]; + } + } + if (auxiliary) { + c.selected_kv_source = "auxiliary"; + c.auxiliary_sequence_length = 512; + c.auxiliary_key.resize(c.batch_size * c.auxiliary_sequence_length * channels); + c.auxiliary_value.resize(c.auxiliary_key.size()); + for (size_t i = 0; i < c.auxiliary_key.size(); ++i) { + c.auxiliary_key[i] = float(int(i % 17) - 8) * 0.125f; + c.auxiliary_value[i] = float(int(i % 19) - 9) * 0.25f; + } + } + c.selected_counts.resize(rows); + c.selected_indices.assign(rows * c.max_selected, -1); + c.expected_output.assign(c.query.size(), 0.0f); + for (int row = 0; row < rows; ++row) { + const int b = row / sequence_length; + const int pos = c.seqlens_k[b] + 1 - sequence_length + row % sequence_length; + c.selected_counts[row] = row == 0 ? 0 : selected_count; + std::iota(c.selected_indices.begin() + row * c.max_selected, + c.selected_indices.begin() + row * c.max_selected + c.selected_counts[row], 0); + for (int h = 0; h < c.num_heads; ++h) { + const int64_t kv = h / (c.num_heads / c.kv_num_heads); + const int64_t out = (row * c.num_heads + h) * c.head_size; + std::vector numerator(c.head_size, 0.0f); + double denominator = std::exp(double(c.head_sink[h])); + const int local_count = auxiliary ? static_cast(c.local_window_size) : 0; + const int local_start = pos - local_count + 1; + for (int i = 0; i < local_count + c.selected_counts[row]; ++i) { + const bool selected = i >= local_count; + const int index = selected ? i - local_count : local_start + i; + if (selected && !auxiliary && index >= local_start) continue; + const bool from_auxiliary = selected && auxiliary; + const int64_t length = from_auxiliary ? c.auxiliary_sequence_length : c.cache_sequence_length; + const int64_t offset = ((b * c.kv_num_heads + kv) * length + index) * c.head_size; + double dot = 0.0; + for (int d = 0; d < c.head_size; ++d) { + const float key = from_auxiliary ? c.auxiliary_key[offset + d] + : c.expected_present_key[offset + d] * c.key_scale[kv * c.head_size + d]; + dot += c.query[out + d] * key; + } + const double weight = std::exp(dot * c.scale); + denominator += weight; + for (int d = 0; d < c.head_size; ++d) { + const float value = from_auxiliary ? c.auxiliary_value[offset + d] + : c.expected_present_value[offset + d] * c.value_scale[kv * c.head_size + d]; + numerator[d] += static_cast(weight * value); + } + } + for (int d = 0; d < c.head_size; ++d) { + c.expected_output[out + d] = static_cast(numerator[d] / denominator); + } + } + } + if (floating_cache) { + c.key_scale.clear(); + c.value_scale.clear(); + } + return c; +} + +TEST(DynamicSparseAttentionTest, FloatingCacheGroupedReferenceParity_CUDA) { + for (int sequence_length : {1, 3, 9}) { + for (bool auxiliary : {false, true}) { + for (bool bf16 : {false, true}) { + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP() << "CUDA EP not available."; + SCOPED_TRACE("sequence_length=" + std::to_string(sequence_length) + + ", auxiliary=" + std::to_string(auxiliary) + ", bf16=" + std::to_string(bf16)); + auto c = MakeGroupedCacheCase(sequence_length, auxiliary, true); + // Ten heads per KV head exercise partial groups of both six (prefill) and eight (decode). + if (bf16) { + RunDynamicSparseAttentionCase(c, std::move(cuda_ep)); + } else { + RunDynamicSparseAttentionCase(c, std::move(cuda_ep)); + } + } + } + } +} + +TEST(DynamicSparseAttentionTest, GroupedDecodeRatio12ReferenceParity_CUDA) { + for (int sequence_length : {1, 3}) { + for (bool auxiliary : {false, true}) { + SCOPED_TRACE("sequence_length=" + std::to_string(sequence_length) + + ", auxiliary=" + std::to_string(auxiliary)); + const auto floating_case = MakeGroupedCacheCase(sequence_length, auxiliary, true, 2, 24, 2); + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP() << "CUDA EP not available."; + RunDynamicSparseAttentionCase(floating_case, std::move(cuda_ep)); + cuda_ep = DefaultCudaExecutionProvider(); + RunDynamicSparseAttentionCase(floating_case, std::move(cuda_ep)); + + const auto int8_case = MakeGroupedCacheCase(sequence_length, auxiliary, false, 2, 24, 2, true); + cuda_ep = DefaultCudaExecutionProvider(); + RunDynamicSparseAttentionCase(int8_case, std::move(cuda_ep)); + cuda_ep = DefaultCudaExecutionProvider(); + RunDynamicSparseAttentionCase(int8_case, std::move(cuda_ep)); + } + } +} + +// Selected-only prefill without a softmax sink, the configuration the tensor-core BF16 prefill kernel serves. +DynamicSparseAttentionCase MakeSelectedOnlyPrefillCase(bool int8_cache) { + auto c = int8_cache ? MakeGroupedCacheCase(9, false, false, 2, 20, 2, true) + : MakeGroupedCacheCase(9, false, true, 2, 20, 2); + c.head_sink.clear(); + c.smooth_softmax = 0; + for (int row = 0; row < c.batch_size * c.sequence_length; ++row) { + const int64_t b = row / c.sequence_length; + for (int h = 0; h < c.num_heads; ++h) { + const int64_t kv = h / (c.num_heads / c.kv_num_heads); + const int64_t out = (row * c.num_heads + h) * c.head_size; + std::vector numerator(c.head_size, 0.0f); + double denominator = 0.0; + for (int i = 0; i < c.selected_counts[row]; ++i) { + const int index = c.selected_indices[row * c.max_selected + i]; + const int64_t offset = ((b * c.kv_num_heads + kv) * c.cache_sequence_length + index) * c.head_size; + double dot = 0.0; + for (int d = 0; d < c.head_size; ++d) { + const float key_scale = int8_cache ? c.key_scale[kv * c.head_size + d] : 1.0f; + dot += c.query[out + d] * c.expected_present_key[offset + d] * key_scale; + } + const double weight = std::exp(dot * c.scale); + denominator += weight; + for (int d = 0; d < c.head_size; ++d) { + const float value_scale = int8_cache ? c.value_scale[kv * c.head_size + d] : 1.0f; + numerator[d] += static_cast(weight * c.expected_present_value[offset + d] * value_scale); + } + } + for (int d = 0; d < c.head_size; ++d) { + c.expected_output[out + d] = denominator == 0.0 ? 0.0f : static_cast(numerator[d] / denominator); + } + } + } + return c; +} + +TEST(DynamicSparseAttentionTest, Int8CachePrefillBFloat16_CUDA) { + if (!DefaultCudaExecutionProvider()) GTEST_SKIP() << "CUDA EP not available."; + if (!CudaHasBF16Support()) GTEST_SKIP() << "CUDA device does not support BFloat16."; + const auto c = MakeSelectedOnlyPrefillCase(true); + for (const char* prefill_env : {"1", "0"}) { + SCOPED_TRACE(prefill_env); + ScopedEnvironmentVariables scoped_env_vars{ + EnvVarMap{{"ORT_DSA_PREFILL_KERNEL", prefill_env}}}; + RunDynamicSparseAttentionCase(c, DefaultCudaExecutionProvider()); + } +} + +TEST(DynamicSparseAttentionTest, FloatingCacheGroupedSingleSplitWorkspace_CUDA) { + for (bool bf16 : {false, true}) { + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP() << "CUDA EP not available."; + auto c = MakeGroupedCacheCase(1, false, true, 128, 4, 1); + if (bf16) { + RunDynamicSparseAttentionCase(c, std::move(cuda_ep)); + } else { + RunDynamicSparseAttentionCase(c, std::move(cuda_ep)); + } + } +} + +TEST(DynamicSparseAttentionTest, Int8CacheGroupedDecode_CUDA) { + for (int sequence_length : {1, 2, 3, 4}) { + for (bool auxiliary : {false, true}) { + SCOPED_TRACE("sequence_length=" + std::to_string(sequence_length) + + ", auxiliary=" + std::to_string(auxiliary)); + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP(); + RunDynamicSparseAttentionCase( + MakeGroupedCacheCase(sequence_length, auxiliary), std::move(cuda_ep)); + } + } +} + +TEST(DynamicSparseAttentionTest, Int8CacheGroupedDecodeFloat16_CUDA) { + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP(); + RunDynamicSparseAttentionCase( + MakeGroupedCacheCase(2, false), std::move(cuda_ep)); +} + +TEST(DynamicSparseAttentionTest, Int8CacheGroupedDecodeVectorStagingChannels_CUDA) { + for (int sequence_length : {1, 2, 3, 4}) { + for (bool auxiliary : {false, true}) { + SCOPED_TRACE("sequence_length=" + std::to_string(sequence_length) + + ", auxiliary=" + std::to_string(auxiliary)); + const auto c = MakeGroupedCacheCase(sequence_length, auxiliary, false, 2, 20, 2, true); + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP(); + RunDynamicSparseAttentionCase(c, std::move(cuda_ep)); + cuda_ep = DefaultCudaExecutionProvider(); + RunDynamicSparseAttentionCase(c, std::move(cuda_ep)); + } + } +} + +TEST(DynamicSparseAttentionTest, GroupedMergeActiveSplitBoundaries_CUDA) { + for (int count : {1, 31, 32, 33, 255, 256, 257, 511}) { + for (bool auxiliary : {false, true}) { + SCOPED_TRACE("selected_count=" + std::to_string(count) + + ", auxiliary=" + std::to_string(auxiliary)); + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP(); + RunDynamicSparseAttentionCase( + MakeGroupedCacheCase(2, auxiliary, false, 2, 20, 2, true, count), std::move(cuda_ep)); + } + } +} + +// ORT_DSA_INT8_VECTOR_STAGING and ORT_DSA_SHARED_SCALE_MERGE are cached in function-local statics, so +// the non-default INT8 grouped-decode staging and the per-channel merge are checked in fresh processes. +TEST(DynamicSparseAttentionTest, Int8CacheGroupedDecodeAlternateSwitchesInFreshProcess_CUDA) { + if (!DefaultCudaExecutionProvider()) GTEST_SKIP() << "CUDA EP not available."; + + const std::string filter = + "DynamicSparseAttentionTest.Int8CacheGroupedDecode_CUDA:" + "DynamicSparseAttentionTest.Int8CacheGroupedDecodeFloat16_CUDA:" + "DynamicSparseAttentionTest.Int8CacheGroupedDecodeVectorStagingChannels_CUDA:" + "DynamicSparseAttentionTest.GroupedMergeActiveSplitBoundaries_CUDA:" + "DynamicSparseAttentionTest.GroupedDecodeRatio12ReferenceParity_CUDA"; + + struct Mode { + const char* vector_staging; + const char* shared_scale_merge; + }; + const Mode modes[] = {{"0", "1"}, {"1", "0"}, {"0", "0"}}; + for (const Mode& mode : modes) { + SCOPED_TRACE(std::string("INT8_VECTOR_STAGING=") + mode.vector_staging + + " SHARED_SCALE_MERGE=" + mode.shared_scale_merge); + ScopedEnvironmentVariables scoped_env_vars{EnvVarMap{ + {"ORT_DSA_INT8_VECTOR_STAGING", mode.vector_staging}, + {"ORT_DSA_SHARED_SCALE_MERGE", mode.shared_scale_merge}, + }}; + ASSERT_EQ(RunGTestFilterInFreshProcess(filter), 0); + } +} + +TEST(DynamicSparseAttentionTest, Int8CacheRejectsNonpositiveOrNonfiniteScales_CUDA) { + ScopedEnvironmentVariables scoped_env_vars{ + EnvVarMap{{"ORT_DYNAMIC_SPARSE_ATTENTION_STRICT_VALIDATION", "1"}}}; + for (float invalid : {0.0f, -1.0f, std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}) { + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) GTEST_SKIP(); + auto c = MakeInt8CacheCase(); + c.value_scale[15] = invalid; + RunDynamicSparseAttentionCase( + c, std::move(cuda_ep), OpTester::ExpectResult::kExpectFailure, + "INT8 cache scales must be positive and finite"); + } +} + Status ResolveDynamicSparseAttentionGraph(size_t output_count, int64_t query_width = 8, int64_t num_heads = 1, int64_t kv_num_heads = 1, bool packed_qkv = false, const char* query_width_symbol = nullptr, @@ -450,6 +919,32 @@ TEST(DynamicSparseAttentionTest, SelectedOnlyMainVariableCountsGqaAndCacheAppend RunDynamicSparseAttentionCase(c, std::move(cuda_ep)); } +TEST(DynamicSparseAttentionTest, ValidationStatusReportsCommittedCursor_CUDA) { + auto cuda_ep = DefaultCudaExecutionProvider(); + if (!cuda_ep) { + GTEST_SKIP() << "CUDA EP not available."; + } + + auto c = MakeSingleTokenSelectedOnlyCase(); + c.cache_sequence_length = 2; + c.total_sequence_length = 2; + c.past_key.assign(16, 0.0f); + c.past_value.assign(16, 0.0f); + c.key.assign(8, 0.0f); + c.value.assign(8, 0.0f); + c.expected_present_key.assign(16, 0.0f); + c.expected_present_value.assign(16, 0.0f); + c.committed_length = 0; + c.expected_validation_status = 0; + RunDynamicSparseAttentionCase(c, std::move(cuda_ep)); + + cuda_ep = DefaultCudaExecutionProvider(); + ASSERT_NE(cuda_ep, nullptr); + c.committed_length = 1; + c.expected_validation_status = 9; + RunDynamicSparseAttentionCase(c, std::move(cuda_ep)); +} + TEST(DynamicSparseAttentionTest, LocalPlusSelectedAuxiliaryJointSoftmaxSinkSharedKv_CUDA) { auto cuda_ep = DefaultCudaExecutionProvider(); if (!cuda_ep) { @@ -1665,6 +2160,19 @@ TEST(DynamicSparseAttentionTest, RejectsInvalidSelectionMetadata_CUDA) { RunDynamicSparseAttentionCase(c, DefaultCudaExecutionProvider(), OpTester::ExpectResult::kExpectFailure, "incompatible with the current sequence"); } + { + SCOPED_TRACE("committed cursor disagrees with seqlens_k and no validation_status output"); + auto c = MakeSingleTokenSelectedOnlyCase(); + c.cache_sequence_length = 2; + c.total_sequence_length = 2; + c.past_key.assign(16, 0.0f); + c.past_value.assign(16, 0.0f); + c.expected_present_key.assign(16, 0.0f); + c.expected_present_value.assign(16, 0.0f); + c.committed_length = 1; + RunDynamicSparseAttentionCase(c, DefaultCudaExecutionProvider(), OpTester::ExpectResult::kExpectFailure, + "committed_length is inconsistent"); + } } #ifndef ORT_NO_EXCEPTIONS @@ -1690,10 +2198,365 @@ TEST(DynamicSparseAttentionTest, RejectsUnknownModeAndSource_CUDA) { } #endif +// Tensors bound through IOBinding can start at any element offset inside a larger allocation, so the kernels +// must not assume more than element alignment from caller-provided buffers. The tests below place each such +// buffer at a misaligned address and expect the aligned-buffer results instead of cudaErrorMisalignedAddress. +namespace { + +// Byte offsets of the caller-provided device buffers from a 256-byte aligned allocation. +struct BufferOffsets { + size_t key_cache = 0; + size_t value_cache = 0; + size_t auxiliary_key = 0; + size_t auxiliary_value = 0; + size_t key_scale = 0; + size_t value_scale = 0; + size_t output = 0; +}; + +struct OffsetBufferCase { + const char* name; + BufferOffsets offsets; +}; + +OffsetBufferCase MisalignedBuffer(const char* name, size_t BufferOffsets::* buffer, size_t bytes) { + OffsetBufferCase offset_case{name, {}}; + offset_case.offsets.*buffer = bytes; + return offset_case; +} + +// Offsets are the smallest that keep each buffer naturally aligned for its element type. +std::vector MakeOffsetBufferCases(bool int8_cache, bool auxiliary) { + const size_t cache_bytes = int8_cache ? 1 : 2; + const size_t scale_bytes = 4; + const size_t half_bytes = 2; + std::vector cases{ + {"aligned", {}}, + MisalignedBuffer("key_cache", &BufferOffsets::key_cache, cache_bytes), + MisalignedBuffer("value_cache", &BufferOffsets::value_cache, cache_bytes), + MisalignedBuffer("output", &BufferOffsets::output, half_bytes)}; + BufferOffsets all; + all.key_cache = all.value_cache = cache_bytes; + all.output = half_bytes; + if (int8_cache) { + cases.push_back(MisalignedBuffer("key_scale", &BufferOffsets::key_scale, scale_bytes)); + cases.push_back(MisalignedBuffer("value_scale", &BufferOffsets::value_scale, scale_bytes)); + all.key_scale = all.value_scale = scale_bytes; + } + if (auxiliary) { + cases.push_back(MisalignedBuffer("auxiliary_key", &BufferOffsets::auxiliary_key, half_bytes)); + cases.push_back(MisalignedBuffer("auxiliary_value", &BufferOffsets::auxiliary_value, half_bytes)); + all.auxiliary_key = all.auxiliary_value = half_bytes; + } + cases.push_back({"all", all}); + return cases; +} + +// Allocates device tensors whose data pointers sit a chosen number of bytes into their allocations. +class OffsetDeviceBuffers { + public: + OffsetDeviceBuffers(IExecutionProvider& ep, AllocatorPtr device_allocator, AllocatorPtr host_allocator) + : ep_(ep), device_allocator_(std::move(device_allocator)), host_allocator_(std::move(host_allocator)) {} + + template + OrtValue Empty(const TensorShape& shape, size_t byte_offset) { + const size_t bytes = static_cast(shape.Size()) * sizeof(Element); + allocations_.push_back(IAllocator::MakeUniquePtr(device_allocator_, bytes + byte_offset)); + uint8_t* base = allocations_.back().get(); + ORT_ENFORCE(reinterpret_cast(base) % 256 == 0, "Device allocation is not 256-byte aligned."); + OrtValue value; + Tensor::InitOrtValue(DataTypeImpl::GetType(), shape, base + byte_offset, + device_allocator_->Info(), value); + return value; + } + + template + OrtValue FromHost(const std::vector& values, const TensorShape& shape, size_t byte_offset = 0) { + OrtValue value = Empty(shape, byte_offset); + Tensor host_tensor(DataTypeImpl::GetType(), shape, const_cast(values.data()), + host_allocator_->Info()); + ORT_THROW_IF_ERROR(ep_.GetDataTransfer()->CopyTensor(host_tensor, *value.GetMutable())); + return value; + } + + template + std::vector ToHost(const OrtValue& device_value) { + const Tensor& device_tensor = device_value.Get(); + Tensor host_tensor(DataTypeImpl::GetType(), device_tensor.Shape(), host_allocator_); + ORT_THROW_IF_ERROR(ep_.GetDataTransfer()->CopyTensor(device_tensor, host_tensor)); + const auto values = host_tensor.DataAsSpan(); + return std::vector(values.begin(), values.end()); + } + + private: + IExecutionProvider& ep_; + AllocatorPtr device_allocator_; + AllocatorPtr host_allocator_; + std::vector> allocations_; +}; + +template +float ElementToFloat(Element value) { + if constexpr (std::is_arithmetic_v) { + return static_cast(value); + } else { + return value.ToFloat(); + } +} + +template +void ExpectMatchesExpected(const std::vector& actual, const std::vector& expected, + const char* name) { + ASSERT_EQ(actual.size(), expected.size()) << name; + // Like OpTester, compare against the expected values rounded to the tensor type. + const std::vector rounded = ToTensorData(expected); + for (size_t i = 0; i < actual.size(); ++i) { + const float expected_value = ElementToFloat(rounded[i]); + ASSERT_NEAR(ElementToFloat(actual[i]), expected_value, 0.005f + 0.01f * std::abs(expected_value)) + << name << " at " << i; + } +} + +// Runs the case through IOBinding with the cache buffers bound in place (present aliases past). +template +void RunCaseWithOffsetBuffers(const DynamicSparseAttentionCase& c, const BufferOffsets& offsets) { + auto ep = DefaultCudaExecutionProvider(); + ASSERT_NE(ep, nullptr); + + const bool has_auxiliary = c.auxiliary_sequence_length > 0; + const bool has_scales = !c.key_scale.empty(); + const int64_t query_count = c.batch_size * c.sequence_length; + const TensorShape query_shape{c.batch_size, c.sequence_length, c.num_heads * c.head_size}; + const TensorShape kv_shape{c.batch_size, c.sequence_length, c.kv_num_heads * c.head_size}; + const TensorShape cache_shape{c.batch_size, c.kv_num_heads, c.cache_sequence_length, c.head_size}; + const TensorShape auxiliary_shape{c.batch_size, c.kv_num_heads, c.auxiliary_sequence_length, c.head_size}; + + Model model("dynamic_sparse_attention_offset_buffers", true, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), {{kOnnxDomain, 17}, {kMSDomain, 1}}, + {}, DefaultLoggingManager().DefaultLogger(), ModelOptions(true, true)); + auto& graph = model.MainGraph(); + const auto make_type = [](ONNX_NAMESPACE::TensorProto_DataType element_type) { + ONNX_NAMESPACE::TypeProto type; + type.mutable_tensor_type()->set_elem_type(element_type); + return type; + }; + const auto t_type = make_type(utils::ToTensorProtoElementType()); + const auto cache_type = make_type(utils::ToTensorProtoElementType()); + const auto int32_type = make_type(ONNX_NAMESPACE::TensorProto_DataType_INT32); + const auto float_type = make_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + auto& empty = graph.GetOrCreateNodeArg("", nullptr); + const auto arg = [&graph](const char* name, const ONNX_NAMESPACE::TypeProto& type) { + return &graph.GetOrCreateNodeArg(name, &type); + }; + + std::vector inputs{ + arg("query", t_type), + arg("key", t_type), + arg("value", t_type), + arg("past_key", cache_type), + arg("past_value", cache_type), + has_auxiliary ? arg("auxiliary_key", t_type) : &empty, + has_auxiliary ? arg("auxiliary_value", t_type) : &empty, + arg("selected_indices", int32_type), + arg("selected_counts", int32_type), + arg("seqlens_k", int32_type), + arg("total_sequence_length", int32_type), + &empty, // cos_cache + &empty, // sin_cache + &empty, // position_ids + &empty, // q_norm_weight + &empty, // k_norm_weight + c.head_sink.empty() ? &empty : arg("head_sink", t_type)}; + if (has_scales) { + inputs.push_back(&empty); // committed_length + inputs.push_back(arg("key_scale", float_type)); + inputs.push_back(arg("value_scale", float_type)); + } + std::vector outputs{ + arg("output", t_type), arg("present_key", cache_type), arg("present_value", cache_type)}; + auto& node = graph.AddNode("dynamic_sparse_attention", "DynamicSparseAttention", "", + inputs, outputs, nullptr, kMSDomain); + node.AddAttribute("num_heads", c.num_heads); + node.AddAttribute("kv_num_heads", c.kv_num_heads); + node.AddAttribute("scale", c.scale); + node.AddAttribute("local_window_size", c.local_window_size); + node.AddAttribute("attention_mode", c.attention_mode); + node.AddAttribute("selected_kv_source", c.selected_kv_source); + node.AddAttribute("smooth_softmax", c.smooth_softmax); + ASSERT_STATUS_OK(graph.Resolve()); + + std::string model_data; + ASSERT_TRUE(model.ToProto().SerializeToString(&model_data)); + SessionOptions session_options; + InferenceSession session(session_options, GetEnvironment()); + IExecutionProvider* ep_ptr = ep.get(); + ASSERT_STATUS_OK(session.RegisterExecutionProvider(std::move(ep))); + std::istringstream model_stream(model_data); + ASSERT_STATUS_OK(session.Load(model_stream)); + ASSERT_STATUS_OK(session.Initialize()); + + auto device_allocators = ep_ptr->CreatePreferredAllocators(); + auto device_allocator = std::find_if(device_allocators.begin(), device_allocators.end(), [](const auto& allocator) { + return allocator->Info().device.Type() == OrtDevice::GPU && + allocator->Info().alloc_type != OrtAllocatorType::OrtReadOnlyAllocator && + allocator->Info().mem_type == OrtMemTypeDefault; + }); + ASSERT_NE(device_allocator, device_allocators.end()); + auto allocator = session.GetAllocator((*device_allocator)->Info()); + ASSERT_NE(allocator, nullptr); + auto cpu_allocator = TestCPUExecutionProvider()->CreatePreferredAllocators()[0]; + + // Declared after the session so the device allocations are released before it is destroyed. + OffsetDeviceBuffers buffers(*ep_ptr, allocator, cpu_allocator); + std::unique_ptr binding; + ASSERT_STATUS_OK(session.NewIOBinding(&binding)); + const auto bind_input = [&](const char* name, const OrtValue& value) { + ORT_THROW_IF_ERROR(binding->BindInput(name, value)); + }; + bind_input("query", buffers.FromHost(ToTensorData(c.query), query_shape)); + bind_input("key", buffers.FromHost(ToTensorData(c.key), kv_shape)); + bind_input("value", buffers.FromHost(ToTensorData(c.value), kv_shape)); + OrtValue key_cache = buffers.FromHost(ToTensorData(c.past_key), cache_shape, offsets.key_cache); + OrtValue value_cache = buffers.FromHost(ToTensorData(c.past_value), cache_shape, offsets.value_cache); + bind_input("past_key", key_cache); + bind_input("past_value", value_cache); + if (has_auxiliary) { + ASSERT_EQ(c.auxiliary_value.size(), c.auxiliary_key.size()); + bind_input("auxiliary_key", + buffers.FromHost(ToTensorData(c.auxiliary_key), auxiliary_shape, offsets.auxiliary_key)); + bind_input("auxiliary_value", + buffers.FromHost(ToTensorData(c.auxiliary_value), auxiliary_shape, offsets.auxiliary_value)); + } + bind_input("selected_indices", + buffers.FromHost(c.selected_indices, TensorShape{query_count, c.max_selected})); + bind_input("selected_counts", buffers.FromHost(c.selected_counts, TensorShape{query_count})); + bind_input("seqlens_k", buffers.FromHost(c.seqlens_k, TensorShape{c.batch_size})); + std::vector total_length_data{c.total_sequence_length}; + OrtValue total_length_value; + Tensor::InitOrtValue(DataTypeImpl::GetType(), TensorShape{}, total_length_data.data(), + cpu_allocator->Info(), total_length_value); + bind_input("total_sequence_length", total_length_value); + if (!c.head_sink.empty()) { + bind_input("head_sink", buffers.FromHost(ToTensorData(c.head_sink), TensorShape{c.num_heads})); + } + if (has_scales) { + const TensorShape scale_shape{static_cast(c.key_scale.size())}; + bind_input("key_scale", buffers.FromHost(c.key_scale, scale_shape, offsets.key_scale)); + bind_input("value_scale", buffers.FromHost(c.value_scale, scale_shape, offsets.value_scale)); + } + ASSERT_STATUS_OK(binding->BindOutput("output", buffers.Empty(query_shape, offsets.output))); + ASSERT_STATUS_OK(binding->BindOutput("present_key", key_cache)); + ASSERT_STATUS_OK(binding->BindOutput("present_value", value_cache)); + + ASSERT_STATUS_OK(binding->SynchronizeInputs()); + RunOptions run_options; + ASSERT_STATUS_OK(session.Run(run_options, *binding)); + ASSERT_STATUS_OK(binding->SynchronizeOutputs()); + + const auto& results = binding->GetOutputs(); + ASSERT_EQ(results.size(), 3u); + ExpectMatchesExpected(buffers.ToHost(results[0]), c.expected_output, "output"); + ExpectMatchesExpected(buffers.ToHost(results[1]), c.expected_present_key, "present_key"); + ExpectMatchesExpected(buffers.ToHost(results[2]), c.expected_present_value, "present_value"); +} + +template +void ExpectMisalignedBuffersMatch(const DynamicSparseAttentionCase& c, bool int8_cache) { + for (const auto& offset_case : MakeOffsetBufferCases(int8_cache, c.auxiliary_sequence_length > 0)) { + SCOPED_TRACE(offset_case.name); + RunCaseWithOffsetBuffers(c, offset_case.offsets); + // A misaligned access poisons the CUDA context, so later cases would only add noise. + if (::testing::Test::HasFailure()) return; + } +} + +} // namespace + +TEST(DynamicSparseAttentionTest, Int8CacheGroupedDecodeMisalignedBuffers_CUDA) { + if (!DefaultCudaExecutionProvider()) GTEST_SKIP() << "CUDA EP not available."; + for (bool bf16 : {true, false}) { + if (bf16 && !CudaHasBF16Support()) continue; + for (bool auxiliary : {false, true}) { + SCOPED_TRACE("bf16=" + std::to_string(bf16) + ", auxiliary=" + std::to_string(auxiliary)); + // Twelve query heads share one KV head: the grouped decode kernel with INT8 vector staging. + const auto c = MakeGroupedCacheCase(2, auxiliary, false, 1, 12, 1, true); + if (bf16) { + ExpectMisalignedBuffersMatch(c, true); + } else { + ExpectMisalignedBuffersMatch(c, true); + } + if (::testing::Test::HasFailure()) return; + } + } +} + +TEST(DynamicSparseAttentionTest, Int8CachePrefillMisalignedBuffers_CUDA) { + if (!DefaultCudaExecutionProvider()) GTEST_SKIP() << "CUDA EP not available."; + if (!CudaHasBF16Support()) GTEST_SKIP() << "CUDA device does not support BFloat16."; + // Aligned buffers use the tensor-core prefill kernel; misaligned ones must fall back to the generic kernels. + ExpectMisalignedBuffersMatch(MakeSelectedOnlyPrefillCase(true), true); +} + +TEST(DynamicSparseAttentionTest, FloatingCacheGroupedDecodeMisalignedBuffers_CUDA) { + if (!DefaultCudaExecutionProvider()) GTEST_SKIP() << "CUDA EP not available."; + for (bool bf16 : {true, false}) { + if (bf16 && !CudaHasBF16Support()) continue; + for (bool auxiliary : {false, true}) { + SCOPED_TRACE("bf16=" + std::to_string(bf16) + ", auxiliary=" + std::to_string(auxiliary)); + const auto c = MakeGroupedCacheCase(2, auxiliary, true, 1, 12, 1); + if (bf16) { + ExpectMisalignedBuffersMatch(c, false); + } else { + ExpectMisalignedBuffersMatch(c, false); + } + if (::testing::Test::HasFailure()) return; + } + } +} + +TEST(DynamicSparseAttentionTest, FloatingCachePrefillMisalignedBuffers_CUDA) { + if (!DefaultCudaExecutionProvider()) GTEST_SKIP() << "CUDA EP not available."; + if (CudaHasBF16Support()) { + SCOPED_TRACE("bf16 selected-only without sink"); + // Aligned buffers use the tensor-core prefill kernel; misaligned ones fall back to grouped prefill. + ExpectMisalignedBuffersMatch(MakeSelectedOnlyPrefillCase(false), false); + if (::testing::Test::HasFailure()) return; + } + for (bool bf16 : {true, false}) { + if (bf16 && !CudaHasBF16Support()) continue; + for (bool auxiliary : {false, true}) { + SCOPED_TRACE("grouped prefill: bf16=" + std::to_string(bf16) + ", auxiliary=" + std::to_string(auxiliary)); + const auto c = MakeGroupedCacheCase(9, auxiliary, true, 2, 20, 2); + if (bf16) { + ExpectMisalignedBuffersMatch(c, false); + } else { + ExpectMisalignedBuffersMatch(c, false); + } + if (::testing::Test::HasFailure()) return; + } + } +} + #endif // USE_CUDA #ifdef USE_WEBGPU +TEST(DynamicSparseAttentionTest, RejectsCudaValidationOptions_WebGPU) { + for (bool committed : {false, true}) { + auto webgpu_ep = DefaultWebGpuExecutionProvider(); + if (!webgpu_ep) GTEST_SKIP() << "WebGPU EP not available."; + auto c = MakeSingleTokenSelectedOnlyCase(); + if (committed) { + c.committed_length = 0; + } else { + c.expected_validation_status = 0; + } + RunDynamicSparseAttentionCase( + c, std::move(webgpu_ep), OpTester::ExpectResult::kExpectFailure, + committed ? "committed_length is only supported by CUDA" : "validation_status is only supported by CUDA"); + } +} + TEST(DynamicSparseAttentionTest, TokenDecodeWithAliasedCache_WebGPU) { auto webgpu_ep = DefaultWebGpuExecutionProvider(); if (!webgpu_ep) { diff --git a/onnxruntime/test/contrib_ops/fresh_process_test_util.h b/onnxruntime/test/contrib_ops/fresh_process_test_util.h new file mode 100644 index 0000000000000..65a800ed39a22 --- /dev/null +++ b/onnxruntime/test/contrib_ops/fresh_process_test_util.h @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +#include "core/common/common.h" + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +namespace onnxruntime { +namespace test { + +// Kernels cache some environment switches in function-local statics, so a switch can only be +// changed before the first eligible call in a process. This helper re-runs the selected gtest cases +// in a new process of the current test binary. The caller sets the environment variables (for +// example with ScopedEnvironmentVariables) before calling; the child inherits them. Returns the +// child exit status (0 on success). +// +// Only Windows and Linux are supported (std::system is unavailable on iOS and /proc/self/exe is +// Linux-specific), so include this header only from tests compiled under USE_CUDA. +inline int RunGTestFilterInFreshProcess(const std::string& gtest_filter) { +#ifdef _WIN32 + std::string path(MAX_PATH, '\0'); + const DWORD length = GetModuleFileNameA(nullptr, path.data(), static_cast(path.size())); + ORT_ENFORCE(length != 0 && length < path.size(), "GetModuleFileNameA failed."); + path.resize(length); +#else + std::string path(PATH_MAX, '\0'); + const ssize_t length = readlink("/proc/self/exe", path.data(), path.size()); + ORT_ENFORCE(length > 0 && static_cast(length) < path.size(), "readlink(/proc/self/exe) failed."); + path.resize(static_cast(length)); +#endif + const std::string command = "\"" + path + "\" --gtest_filter=" + gtest_filter + + " --gtest_fail_if_no_test_selected --gtest_color=no"; + return std::system(command.c_str()); +} + +} // namespace test +} // namespace onnxruntime diff --git a/onnxruntime/test/contrib_ops/packed_sparse_attention_indexer_op_test.cc b/onnxruntime/test/contrib_ops/packed_sparse_attention_indexer_op_test.cc index 3e22177db6904..31fbcdac79262 100644 --- a/onnxruntime/test/contrib_ops/packed_sparse_attention_indexer_op_test.cc +++ b/onnxruntime/test/contrib_ops/packed_sparse_attention_indexer_op_test.cc @@ -37,6 +37,12 @@ #include "test/unittest_util/graph_transform_test_builder.h" #include "test/util/include/asserts.h" #include "test/util/include/default_providers.h" +#include "test/util/include/scoped_env_vars.h" + +#ifdef USE_CUDA +// Uses std::system, which is unavailable on iOS, so only CUDA builds (Windows/Linux) include it. +#include "test/contrib_ops/fresh_process_test_util.h" +#endif #ifdef USE_WEBGPU #include "core/providers/webgpu/webgpu_provider_options.h" @@ -124,7 +130,7 @@ struct GraphOptions { int64_t BufferCapacity(int64_t compress_ratio) { return 2 * compress_ratio - 1; } // Builds a fixed-input node; csa-only slots are left empty for policy_mode "qsa", as the schema -// requires. position_ids (slot 11) is omitted for "qsa" and forced on for "csa". +// requires. position_ids (slot 11) is optional for "qsa" and forced on for "csa". void AddNode(ModelTestBuilder& builder, const GraphOptions& options) { const bool is_csa = options.policy_mode == psai::kPolicyModeCsa; const int64_t width = is_csa ? 2 * options.head_size : options.head_size; @@ -621,10 +627,12 @@ std::unique_ptr CreateProvider(ProviderKind provider_kind, #endif } -void RunOnProvider(OpTester& test, std::unique_ptr provider) { +void RunOnProvider(OpTester& test, std::unique_ptr provider, + OpTester::ExpectResult expected_result = OpTester::ExpectResult::kExpectSuccess, + const std::string& expected_error = "") { std::vector> providers; providers.push_back(std::move(provider)); - test.Run(OpTester::ExpectResult::kExpectSuccess, "", {}, nullptr, &providers); + test.Run(expected_result, expected_error, {}, nullptr, &providers); } std::vector MakeWave(size_t count, float phase, float step) { @@ -752,6 +760,7 @@ struct QsaPackedProblem { std::vector state_update_capture_count; std::optional state_update_active; bool report_state_status = false; + bool report_mid_state = false; std::vector query; std::vector key; @@ -977,10 +986,11 @@ QsaPackedProblem MakeQsaPackedProblem(QsaPackedProblem problem = {}) { return problem; } -template +template void RunQsaPackedTest(float tolerance, QsaPackedProblem problem = MakeQsaPackedProblem(), ProviderKind provider_kind = ProviderKind::Cuda, QsaPackedResult* actual = nullptr, - bool packed_qk = false, const ConfigOptions* webgpu_options = nullptr, int padding = 0) { + bool packed_qk = false, const ConfigOptions* webgpu_options = nullptr, int padding = 0, + const std::string& expected_error = "") { auto provider = CreateProvider(provider_kind, webgpu_options); if (provider == nullptr) { GTEST_SKIP() << (provider_kind == ProviderKind::Cuda ? "CUDA" : "WebGPU") @@ -993,7 +1003,7 @@ void RunQsaPackedTest(float tolerance, QsaPackedProblem problem = MakeQsaPackedP problem.key_norm_weight = RoundTrip(problem.key_norm_weight); problem.cos_cache = RoundTrip(problem.cos_cache); problem.sin_cache = RoundTrip(problem.sin_cache); - problem.past_key_state = RoundTrip(problem.past_key_state); + problem.past_key_state = RoundTrip(problem.past_key_state); problem.past_kv_buffer = RoundTrip(problem.past_kv_buffer); QsaPackedResult expected; @@ -1054,8 +1064,8 @@ void RunQsaPackedTest(float tolerance, QsaPackedProblem problem = MakeQsaPackedP } else { test.AddInput("position_ids", {total_tokens}, problem.position_ids); } - test.AddInput("past_key_state", {batch_size, problem.state_capacity, head_size}, - ToElementType(problem.past_key_state)); + test.AddInput("past_key_state", {batch_size, problem.state_capacity, head_size}, + ToElementType(problem.past_key_state)); test.AddInput("past_kv_buffer", {batch_size, buffer_capacity, head_size}, ToElementType(problem.past_kv_buffer)); test.AddOptionalInputEdge(); // past_gate_buffer @@ -1078,8 +1088,8 @@ void RunQsaPackedTest(float tolerance, QsaPackedProblem problem = MakeQsaPackedP } test.AddOutput("selected_indices", {total_tokens, output_capacity}, expected.selected_indices); test.AddOutput("selected_counts", {total_tokens}, expected.selected_counts); - test.AddOutput("present_key_state", {batch_size, problem.state_capacity, head_size}, - ToElementType(expected.present_key_state), false, 0.0f, tolerance); + test.AddOutput("present_key_state", {batch_size, problem.state_capacity, head_size}, + ToElementType(expected.present_key_state), false, 0.0f, tolerance); test.AddOutput("present_kv_buffer", {batch_size, buffer_capacity, head_size}, ToElementType(expected.present_kv_buffer), false, 0.0f, tolerance); test.AddOptionalOutputEdge(); // present_gate_buffer @@ -1099,14 +1109,16 @@ void RunQsaPackedTest(float tolerance, QsaPackedProblem problem = MakeQsaPackedP } test.AddOutput("state_status", {batch_size}, expected.state_status); } - RunOnProvider(test, std::move(provider)); + RunOnProvider(test, std::move(provider), + expected_error.empty() ? OpTester::ExpectResult::kExpectSuccess : OpTester::ExpectResult::kExpectFailure, + expected_error); if (actual != nullptr) { const auto& fetches = test.GetFetches(); actual->selected_indices.assign(fetches[0].Get().DataAsSpan().begin(), fetches[0].Get().DataAsSpan().end()); actual->selected_counts.assign(fetches[1].Get().DataAsSpan().begin(), fetches[1].Get().DataAsSpan().end()); - actual->present_key_state = CopyTensorToFloat(fetches[2].Get()); + actual->present_key_state = CopyTensorToFloat(fetches[2].Get()); actual->present_kv_buffer = CopyTensorToFloat(fetches[3].Get()); actual->present_state_lengths.assign(fetches[4].Get().DataAsSpan().begin(), fetches[4].Get().DataAsSpan().end()); @@ -1121,6 +1133,10 @@ TEST(PackedSparseAttentionIndexerTest, QsaFloat16) { RunQsaPackedTest TEST(PackedSparseAttentionIndexerTest, QsaBFloat16) { RunQsaPackedTest(2.0e-2f); } +TEST(PackedSparseAttentionIndexerTest, QsaBFloat16FloatState) { + RunQsaPackedTest(2.0e-2f); +} + TEST(PackedSparseAttentionIndexerTest, QsaPackedQueryKey) { RunQsaPackedTest(1.0e-5f, MakeQsaPackedProblem(), ProviderKind::Cuda, nullptr, true); } @@ -1238,6 +1254,17 @@ TEST(PackedSparseAttentionIndexerTest, QsaStateUpdateCapturesRawAndBlockTransiti RunQsaPackedTest(1.0e-5f, MakeQsaPackedProblem(std::move(problem)), ProviderKind::Cuda); } +TEST(PackedSparseAttentionIndexerTest, QsaBFloat16FloatStateCapture) { + QsaPackedProblem problem; + problem.batch_size = 2; + problem.compress_ratio = 4; + problem.cumulative_sequence_lengths = {0, 8, 13}; + problem.past_sequence_lengths = {0, 3}; + problem.state_update_capacity = 8; + problem.state_update_capture_count = {8, 3}; + RunQsaPackedTest(2.0e-2f, MakeQsaPackedProblem(std::move(problem)), ProviderKind::Cuda); +} + TEST(PackedSparseAttentionIndexerTest, QsaStateUpdateActiveZeroClearsAllSlots) { QsaPackedProblem problem; problem.batch_size = 2; @@ -1354,6 +1381,28 @@ TEST(PackedSparseAttentionIndexerTest, QsaHierarchicalPairedVerification) { } } +TEST(PackedSparseAttentionIndexerTest, QsaCompactChunkAndFallbackBoundaries) { + for (int live_blocks : {511, 512, 513, 1023, 1024, 1025, 2047, 2048, 2049, 8193, 10240, 10241}) { + SCOPED_TRACE(live_blocks); + QsaPackedProblem problem; + problem.batch_size = 2; + problem.cumulative_sequence_lengths = {0, 2, 3}; + problem.past_sequence_lengths = {live_blocks * 4, 0}; + problem.head_size = 128; + problem.num_heads = 4; + problem.rotary_width = 64; + problem.compress_ratio = 4; + problem.token_budget = 2048; + problem.state_capacity = std::max(2051, live_blocks + 1); + problem.max_position = live_blocks * 4 + 4; + problem = MakeQsaPackedProblem(std::move(problem)); + std::fill(problem.query.begin(), problem.query.end(), 0.0f); + std::fill(problem.cos_cache.begin(), problem.cos_cache.end(), 1.0f); + std::fill(problem.sin_cache.begin(), problem.sin_cache.end(), 0.0f); + RunQsaPackedTest(2.0e-2f, std::move(problem), ProviderKind::Cuda); + } +} + TEST(PackedSparseAttentionIndexerTest, QsaRaggedPrefill) { QsaPackedProblem problem; problem.batch_size = 1; @@ -1383,6 +1432,158 @@ TEST(PackedSparseAttentionIndexerTest, QsaMalformedStateLengthsAreClamped) { RunQsaPackedTest(1.0e-5f, MakeQsaPackedProblem(std::move(problem))); } +TEST(PackedSparseAttentionIndexerTest, QsaHierarchicalTileBoundariesPreserveStableTies) { + for (int live_blocks : {0, 31, 32, 33, 511, 512, 513, 1023, 1024, 1025, 2047, 2048, 2049, 2051}) { + SCOPED_TRACE(live_blocks); + QsaPackedProblem problem; + problem.batch_size = 2; + problem.cumulative_sequence_lengths = {0, 1, 2}; + problem.past_sequence_lengths = {live_blocks * 4, std::max(0, live_blocks - 1) * 4}; + problem.head_size = 128; + problem.num_heads = 4; + problem.rotary_width = 64; + problem.compress_ratio = 4; + problem.token_budget = 2048; + problem.state_capacity = 2051; + problem.max_position = std::max(32, live_blocks * 4 + 2); + problem = MakeQsaPackedProblem(std::move(problem)); + std::fill(problem.query.begin(), problem.query.end(), 0.0f); + std::fill(problem.cos_cache.begin(), problem.cos_cache.end(), 1.0f); + std::fill(problem.sin_cache.begin(), problem.sin_cache.end(), 0.0f); + RunQsaPackedTest(2.0e-3f, problem); + RunQsaPackedTest(2.0e-2f, std::move(problem)); + } +} + +TEST(PackedSparseAttentionIndexerTest, QsaHierarchicalPackedRowsWithLargeCapacity) { + for (int tokens : {1, 2, 3, 8, 64}) { + SCOPED_TRACE(tokens); + QsaPackedProblem problem; + problem.batch_size = 1; + problem.cumulative_sequence_lengths = {0, tokens}; + problem.past_sequence_lengths = {4097 * 4 + 3}; + problem.head_size = 128; + problem.num_heads = 4; + problem.rotary_width = 64; + problem.compress_ratio = 4; + problem.token_budget = 2048; + problem.state_capacity = 65536; + problem.max_position = problem.past_sequence_lengths[0] + tokens + 1; + problem = MakeQsaPackedProblem(std::move(problem)); + std::fill(problem.query.begin(), problem.query.end(), 0.0f); + std::fill(problem.cos_cache.begin(), problem.cos_cache.end(), 1.0f); + std::fill(problem.sin_cache.begin(), problem.sin_cache.end(), 0.0f); + RunQsaPackedTest(2.0e-3f, problem, ProviderKind::Cuda); + RunQsaPackedTest(2.0e-2f, std::move(problem), ProviderKind::Cuda); + } +} + +TEST(PackedSparseAttentionIndexerTest, QsaHierarchicalMergeLevelsWithPartialGroups) { + for (int capacity : {513, 1025, 2051, 4097}) { + SCOPED_TRACE(capacity); + QsaPackedProblem problem; + problem.batch_size = 2; + problem.cumulative_sequence_lengths = {0, 2, 3}; + problem.past_sequence_lengths = {(capacity - 1) * 4, 33 * 4}; + problem.head_size = 128; + problem.num_heads = 4; + problem.rotary_width = 64; + problem.compress_ratio = 4; + problem.token_budget = 2048; + problem.state_capacity = capacity; + problem.max_position = capacity * 4 + 4; + problem = MakeQsaPackedProblem(std::move(problem)); + std::fill(problem.query.begin(), problem.query.end(), 1.0f); + std::fill(problem.query_norm_weight.begin(), problem.query_norm_weight.end(), 1.0f); + std::fill(problem.cos_cache.begin(), problem.cos_cache.end(), 1.0f); + std::fill(problem.sin_cache.begin(), problem.sin_cache.end(), 0.0f); + for (int batch = 0; batch < problem.batch_size; ++batch) { + for (int block = 0; block < capacity; ++block) { + const float value = static_cast((block * 73) % capacity + 1) / capacity; + std::fill_n(problem.past_key_state.begin() + + (static_cast(batch) * capacity + block) * problem.head_size, + problem.head_size, value); + } + } + RunQsaPackedTest(2.0e-3f, problem, ProviderKind::Cuda); + RunQsaPackedTest(2.0e-2f, std::move(problem), ProviderKind::Cuda); + } +} + +TEST(PackedSparseAttentionIndexerTest, QsaHierarchicalScoringReusesKeysAcrossHeads) { + for (int live_blocks : {4095, 4096, 4097, 8191}) { + SCOPED_TRACE(live_blocks); + QsaPackedProblem problem; + problem.batch_size = 2; + problem.cumulative_sequence_lengths = {0, 2, 3}; + problem.past_sequence_lengths = {live_blocks * 4, 33 * 4}; + problem.head_size = 128; + problem.num_heads = 4; + problem.rotary_width = 64; + problem.compress_ratio = 4; + problem.token_budget = 2048; + problem.state_capacity = 8193; + problem.max_position = live_blocks * 4 + 4; + problem = MakeQsaPackedProblem(std::move(problem)); + std::fill(problem.query.begin(), problem.query.end(), 1.0f); + std::fill(problem.query_norm_weight.begin(), problem.query_norm_weight.end(), 1.0f); + std::fill(problem.cos_cache.begin(), problem.cos_cache.end(), 1.0f); + std::fill(problem.sin_cache.begin(), problem.sin_cache.end(), 0.0f); + for (int batch = 0; batch < problem.batch_size; ++batch) { + for (int block = 0; block < problem.state_capacity; ++block) { + const float value = static_cast((block * 73) % problem.state_capacity + 1) / + problem.state_capacity; + std::fill_n(problem.past_key_state.begin() + + (static_cast(batch) * problem.state_capacity + block) * problem.head_size, + problem.head_size, value); + } + } + RunQsaPackedTest(1.0e-5f, problem, ProviderKind::Cuda); + RunQsaPackedTest(2.0e-3f, problem, ProviderKind::Cuda); + RunQsaPackedTest(2.0e-2f, problem, ProviderKind::Cuda); + RunQsaPackedTest(2.0e-2f, std::move(problem), ProviderKind::Cuda); + } +} + +#ifdef USE_CUDA +// The CUDA indexer caches ORT_PACKED_SPARSE_INDEXER_{COMPACT_TOPK,WARP_HEADS} in function-local +// statics, so each non-default mode (tile-level two-level merge, non-warp ragged prefill scorer) is +// exercised by re-running the hierarchical top-k and ragged-prefill cases (nonuniform scores, stable +// ties, partial merge groups) in a fresh process that has the switch set before the first eligible call. +TEST(PackedSparseAttentionIndexerTest, QsaAlternateTopKModesInFreshProcess) { + if (DefaultCudaExecutionProvider() == nullptr) { + GTEST_SKIP() << "CUDA EP not available."; + } + + const std::string filter = + "PackedSparseAttentionIndexerTest.QsaHierarchicalTopK:" + "PackedSparseAttentionIndexerTest.QsaHierarchicalPairedVerification:" + "PackedSparseAttentionIndexerTest.QsaCompactChunkAndFallbackBoundaries:" + "PackedSparseAttentionIndexerTest.QsaHierarchicalTileBoundariesPreserveStableTies:" + "PackedSparseAttentionIndexerTest.QsaHierarchicalPackedRowsWithLargeCapacity:" + "PackedSparseAttentionIndexerTest.QsaHierarchicalMergeLevelsWithPartialGroups:" + "PackedSparseAttentionIndexerTest.QsaHierarchicalScoringReusesKeysAcrossHeads:" + "PackedSparseAttentionIndexerTest.QsaRaggedPrefill"; + + struct Mode { + const char* compact; + const char* warp_heads; + }; + const Mode modes[] = { + {"0", "1"}, // tile-level two-level merge + {"1", "0"}, // non-warp ragged prefill scorer + }; + for (const Mode& mode : modes) { + SCOPED_TRACE(std::string("COMPACT_TOPK=") + mode.compact + " WARP_HEADS=" + mode.warp_heads); + ScopedEnvironmentVariables scoped_env_vars{EnvVarMap{ + {"ORT_PACKED_SPARSE_INDEXER_COMPACT_TOPK", mode.compact}, + {"ORT_PACKED_SPARSE_INDEXER_WARP_HEADS", mode.warp_heads}, + }}; + ASSERT_EQ(RunGTestFilterInFreshProcess(filter), 0); + } +} +#endif // USE_CUDA + // state_status is optional: requesting it must not change any other output, and every healthy // request reports 0. TEST(PackedSparseAttentionIndexerTest, QsaStateStatusOkForValidRequests) {