diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dca6a88b093d8..e5fb682992f2a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -52,8 +52,11 @@ jobs: - name: Setup Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: - # Use the version configured in target-version of [tool.black] section in pyproject.toml. - python-version: "3.10" + # Use a version pre-installed in the runner pool's read-only tool cache; requesting an + # uncached version makes setup-python fail trying to write to /opt/hostedtoolcache. + # This is only the interpreter lintrunner runs on. The Python syntax the linters target + # is set by target-version under [tool.ruff] in pyproject.toml, independently of this. + python-version: "3.12" - name: Setup Rust uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1.0.7 with: diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index abb3ca86596b2..c7b755150233d 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -33,7 +33,8 @@ jobs: - name: Setup Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: - python-version: "3.10" + # Keep in sync with lint.yml; must be a version cached on the runner pool. + python-version: "3.12" - name: Setup Rust uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1.0.7 with: diff --git a/cmake/onnxruntime_providers_coreml.cmake b/cmake/onnxruntime_providers_coreml.cmake index bf46a73e43839..f3425b9272102 100644 --- a/cmake/onnxruntime_providers_coreml.cmake +++ b/cmake/onnxruntime_providers_coreml.cmake @@ -13,9 +13,9 @@ add_compile_definitions(COREML_ENABLE_MLPROGRAM=1) if(LINUX) find_library(LibUUID_LIBRARY NAMES uuid) find_path(LibUUID_INCLUDE_DIR NAMES uuid/uuid.h) - if (NOT LibUUID_INCLUDE_DIR) - message(FATAL "uuid/uuid.h was not found as is required for ML Program support. " - "Run `sudo apt install uuid-dev` if you need to test ML Program related CoreML EP code. ") + if (NOT LibUUID_INCLUDE_DIR OR NOT LibUUID_LIBRARY) + message(FATAL_ERROR "libuuid (uuid/uuid.h) was not found and is required for ML Program support. " + "Run `sudo apt install uuid-dev`, or build with `--use_vcpkg` so the libuuid port is used. ") endif() endif() @@ -194,7 +194,8 @@ target_include_directories(onnxruntime_providers_coreml PRIVATE ) if (LINUX) - target_link_libraries(onnxruntime_providers_coreml PRIVATE uuid) + target_include_directories(onnxruntime_providers_coreml PRIVATE ${LibUUID_INCLUDE_DIR}) + target_link_libraries(onnxruntime_providers_coreml PRIVATE ${LibUUID_LIBRARY}) endif() diff --git a/cmake/vcpkg.json b/cmake/vcpkg.json index 7c58604b9c97a..429e07aafa772 100644 --- a/cmake/vcpkg.json +++ b/cmake/vcpkg.json @@ -86,7 +86,13 @@ }, "coreml-ep": { "description": "Build with CoreML EP", - "dependencies": ["fp16"] + "dependencies": [ + "fp16", + { + "name": "libuuid", + "platform": "linux" + } + ] }, "dml-ep": { "description": "Build with DirectML EP", diff --git a/docs/ContribOperators.md b/docs/ContribOperators.md index 5b23a58c865c2..308546d783298 100644 --- a/docs/ContribOperators.md +++ b/docs/ContribOperators.md @@ -31,6 +31,7 @@ Do not modify directly.* * com.microsoft.DynamicTimeWarping * com.microsoft.EPContext * com.microsoft.EmbedLayerNormalization + * com.microsoft.EngramGate * com.microsoft.ExpandDims * com.microsoft.FastGelu * com.microsoft.FusedConv @@ -71,6 +72,7 @@ Do not modify directly.* * com.microsoft.MulInteger * com.microsoft.MultiHeadAttention * com.microsoft.MurmurHash3 + * com.microsoft.NGramHashMapping * com.microsoft.NGramRepeatBlock * com.microsoft.NhwcConv * com.microsoft.NhwcFusedConv @@ -930,6 +932,23 @@ This version of the operator has been available since version 1 of the 'com.micr enforced on the last spatial dimension only. The optional activation attribute supports fused SiLU/Swish activation. + + The dilation attribute spaces the kernel taps along the causal axis: output position t reads + input positions t - (k_1 - 1 - j) * dilation for tap j. The receptive field therefore spans + (k_1 - 1) * dilation positions before the current one, and the carry state grows to match: + past_state and present_state hold (k_1 - 1) * dilation positions instead of k_1 - 1. Dilation 1 + (the default) is the undilated case and keeps the original state length, so models exported + before the attribute existed are unaffected. + + The channels_last attribute selects a sequence-major layout for the activations and the carry + state, so a model that already produces channels-last activations does not have to transpose into + and out of the channels-first layout. With channels_last = 1 and ndim = 1, input and output are + (batch_size, sequence_length, d_1, ..., d_n) and the state tensors are + (batch_size, state_length, d_1, ..., d_n), where channels = d_1 * ... * d_n. Any number of trailing + channel axes is accepted, so an activation that keeps hyper-connections and hidden size as separate + axes needs no reshape either. weight and bias keep their channels-first (channels, 1, k_1) and + (channels) shapes because they have no sequence axis. The computed values are identical to the + channels-first layout; only the memory layout differs. #### Version @@ -940,23 +959,27 @@ This version of the operator has been available since version 1 of the 'com.micr
activation : string
Fused activation function. One of: 'silu', 'swish', 'none'. Default is 'none'.
+
channels_last : int
+
When 1, input, output, past_state and present_state use a sequence-major, channels-last layout: input and output are (batch_size, sequence_length, d_1, ..., d_n) and the state tensors are (batch_size, state_length, d_1, ..., d_n), where channels = d_1 * ... * d_n. weight and bias keep their channels-first shapes. Requires ndim = 1. Default is 0 (channels-first).
+
dilation : int
+
Spacing between kernel taps along the causal (last spatial) axis. The receptive field spans (k_1 - 1) * dilation positions before the current one, and past_state / present_state hold that many positions. Must be >= 1. Default is 1 (undilated).
ndim : int
Spatial dimensionality: 1, 2, or 3. Default is 1.
state_window : int
-
Number of trailing per-position carry states held by past_state and present_state. When 0 (default) the state tensors have no window axis and hold only the state after the last position, i.e. the backward-compatible (batch_size, channels, k_1 - 1). When W > 0 both gain a LEADING axis of extent W, right-aligned: slot j is the state after position (seq_len - W + j), so slot W-1 is always the state after the last position (identical to the W = 0 tensor) and is the slot past_state is read from. The window axis leads the batch axis so that each slot is one contiguous (batch_size, channels, k_1 - 1) block. Slots below max(0, W - seq_len) hold no position from this call and are filled with zeros. A window lets a speculative decoder roll the state back to an accepted prefix without replaying the forward. Valid range is [0, 8].
+
Number of trailing per-position carry states held by past_state and present_state. When 0 (default) the state tensors have no window axis and hold only the state after the last position, i.e. the backward-compatible (batch_size, channels, state_length) where state_length = (k_1 - 1) * dilation. When W > 0 both gain a LEADING axis of extent W, right-aligned: slot j is the state after position (seq_len - W + j), so slot W-1 is always the state after the last position (identical to the W = 0 tensor) and is the slot past_state is read from. The window axis leads the batch axis so that each slot is one contiguous (batch_size, channels, state_length) block. Slots below max(0, W - seq_len) hold no position from this call and are filled with zeros. A window lets a speculative decoder roll the state back to an accepted prefix without replaying the forward. Valid range is [0, 8].
#### Inputs (2 - 4)
input : T
-
Input tensor with shape (batch_size, channels, ...). Channels-first layout. Spatial dims: 1D: (L,); 2D: (H, W); 3D: (D, H, W).
+
Input tensor with shape (batch_size, channels, ...) in the default channels-first layout. Spatial dims: 1D: (L,); 2D: (H, W); 3D: (D, H, W). When channels_last = 1 the shape is (batch_size, sequence_length, d_1, ..., d_n) instead.
weight : T
Depthwise convolution kernel with shape (channels, 1, k_1, ...). Spatial kernel sizes: (k_1, ..., k_ndim).
bias (optional) : T
Optional per-channel bias with shape (channels).
past_state (optional) : T
-
Carry state from previous step. For ndim=1: (batch_size, channels, k_1 - 1), or (W, batch_size, channels, k_1 - 1) when state_window = W > 0, in which case only slot W-1 is read. If not provided, padding is zero.
+
Carry state from previous step. For ndim=1: (batch_size, channels, state_length), or (W, batch_size, channels, state_length) when state_window = W > 0, in which case only slot W-1 is read, where state_length = (k_1 - 1) * dilation. When channels_last = 1 each slot is (batch_size, state_length, d_1, ..., d_n) instead. If not provided, padding is zero.
#### Outputs @@ -965,7 +988,7 @@ This version of the operator has been available since version 1 of the 'com.micr
output : T
Convolution output with same shape as input.
present_state : T
-
Updated carry state. For ndim=1: (batch_size, channels, k_1 - 1), or (W, batch_size, channels, k_1 - 1) when state_window = W > 0. Slot W-1 contains the last (k-1) values from the virtual input along the causal axis; slot j contains the same for the prefix ending at position (seq_len - W + j).
+
Updated carry state. For ndim=1: (batch_size, channels, state_length), or (W, batch_size, channels, state_length) when state_window = W > 0, and (batch_size, state_length, d_1, ..., d_n) per slot when channels_last = 1. Slot W-1 contains the last state_length values from the virtual input along the causal axis; slot j contains the same for the prefix ending at position (seq_len - W + j).
#### Type Constraints @@ -1772,6 +1795,66 @@ This version of the operator has been available since version 1 of the 'com.micr +### **com.microsoft.EngramGate** + + Fuses the Engram gate. + + The op consumes already projected keys in (batch_size, sequence_length, hc_mult, hidden_size) layout, + the hidden-state queries in the same layout, an already projected value in + (batch_size, sequence_length, hidden_size) layout that is shared by every hyper-connection, and the two + RMSNorm scales. The key and value projections stay outside the op so they can run on the execution + provider's tuned MatMul (weight prepacking, tensor cores, quantized weights) and so the value + projection is computed once per token instead of once per hyper-connection. + + It computes the Engram gate: + + gate = sigmoid(sign(dot) * sqrt(max(abs(dot), 1e-6))) where + dot = sum(RMSNorm(key) * RMSNorm(query)) / sqrt(hidden_size). + + The output is gate * value, broadcast across the hyper-connections. The final Engram residual + value + short_conv(value) is then expressed with RMSNorm, CausalConvWithState and Add. + +#### Version + +This version of the operator has been available since version 1 of the 'com.microsoft' operator set. + +#### Attributes + +
+
epsilon : float
+
Epsilon used by both RMS normalization steps. Default is 1e-5.
+
+ +#### Inputs + +
+
key : T
+
Projected Engram keys with shape (batch_size, sequence_length, hc_mult, hidden_size).
+
query : T
+
Hidden-state queries with shape (batch_size, sequence_length, hc_mult, hidden_size).
+
value : T
+
Projected Engram value shared by every hyper-connection, with shape (batch_size, sequence_length, hidden_size).
+
key_norm_scale : T
+
RMSNorm scale for keys with shape (hc_mult, hidden_size).
+
query_norm_scale : T
+
RMSNorm scale for queries with shape (hc_mult, hidden_size).
+
+ +#### Outputs + +
+
output : T
+
Gated value tensor with shape (batch_size, sequence_length, hc_mult, hidden_size).
+
+ +#### Type Constraints + +
+
T : tensor(float), tensor(float16), tensor(bfloat16)
+
Constrain input and output types to float tensors.
+
+ + ### **com.microsoft.ExpandDims** ExpandDims echo operator. @@ -4182,6 +4265,73 @@ This version of the operator has been available since version 1 of the 'com.micr +### **com.microsoft.NGramHashMapping** + + Computes Engram n-gram hash ids from pre-compressed tokenizer ids. + + For n in [2, max_ngram_size], the op creates causal shifts of input_ids, padding positions before the + sequence with pad_id, and computes + mix = shifted_0 * multipliers[0] xor ... xor shifted_(n-1) * multipliers[n-1]. + For every head of that n-gram order it emits mix modulo the corresponding head vocabulary size. + The output layout is (batch_size, sequence_length, (max_ngram_size - 1) * n_head_per_ngram), with + heads for n=2 first, then n=3, and so on. + + An n-gram window reaches max_ngram_size - 1 positions before the current token. To keep the op causal + across invocations (chunked prefill or autoregressive decode), the optional past_ids input carries + those preceding ids and present_ids returns the ids to pass to the next call. Both have shape + (batch_size, max_ngram_size - 1) and are right-aligned, so the last slot is the most recent id. + Positions before the start of the whole sequence use pad_id. Running the op once over a full sequence + and running it over consecutive chunks while threading present_ids into past_ids produce identical + hash ids. When past_ids is omitted the missing history is pad_id, which matches a fresh sequence. + past_ids and present_ids may use the same allocation. Such in-place execution is transaction-safe + only when the whole operator call is unconditionally committed; a caller that may select a prefix or + roll back must preserve past_ids. + +#### Version + +This version of the operator has been available since version 1 of the 'com.microsoft' operator set. + +#### Attributes + +
+
max_ngram_size : int (required)
+
Maximum n-gram order. Must be at least 2.
+
n_head_per_ngram : int (required)
+
Number of hash heads emitted for each n-gram order.
+
pad_id : int (required)
+
Compressed tokenizer id used to pad causal shifts before the beginning of a sequence.
+
+ +#### Inputs (3 - 4) + +
+
input_ids : M
+
Compressed tokenizer ids with shape (batch_size, sequence_length).
+
multipliers : M
+
Per-shift hash multipliers with shape (max_ngram_size). Conventionally odd, but any value is accepted.
+
vocab_sizes : M
+
Per-output-head vocabulary sizes, conventionally prime, with shape ((max_ngram_size - 1) * n_head_per_ngram). Every entry must be strictly positive. The CPU implementation rejects a non-positive entry; GPU implementations guard the modulo to avoid a device-side division by zero and emit a hash id of 0 for that head.
+
past_ids (optional) : M
+
Optional compressed tokenizer ids for the max_ngram_size - 1 positions that precede this call, with shape (batch_size, max_ngram_size - 1). Right-aligned, so the last slot is the most recent id. If omitted the history is pad_id.
+
+ +#### Outputs (1 - 2) + +
+
hash_ids : M
+
Hash ids with shape (batch_size, sequence_length, (max_ngram_size - 1) * n_head_per_ngram).
+
present_ids (optional) : M
+
Trailing max_ngram_size - 1 ids of past_ids followed by input_ids, with shape (batch_size, max_ngram_size - 1). Feed this back as past_ids on the next call.
+
+ +#### Type Constraints + +
+
M : tensor(int32), tensor(int64)
+
Constrain ids, multipliers, vocabulary sizes, and output ids to integer tensors.
+
+ + ### **com.microsoft.NGramRepeatBlock** Enforce no repetition of n-grams. Scores are set to `-inf` for tokens that form a repeated n-gram if added to the back of the input_ids. @@ -7165,7 +7315,8 @@ This version of the operator has been available since version 1 of the 'com.micr at least one token. weight has shape (channels, 1, kernel_size), and optional bias has shape (channels). The convolution never reads across a sequence boundary. - initial_state is required and has shape (batch_size, channels, kernel_size - 1). It contains + initial_state is required and has shape (batch_size, channels, state_length), where + state_length = (kernel_size - 1) * dilation. It contains the committed raw activation samples immediately preceding this call. final_state has the same shape and type and is fully written with the state after each sequence's final token. State uses the activation type because it stores raw samples, not accumulated convolution values. @@ -7187,6 +7338,14 @@ This version of the operator has been available since version 1 of the 'com.micr This device-side containment is not a synchronous validation or rejection mechanism. The optional activation attribute supports none, SiLU, and Swish. + + The dilation attribute spaces the kernel taps along the sequence axis: local token t of a request + reads that request's local positions t - (kernel_size - 1 - j) * dilation for tap j, and positions + before the request's first token come from the carry state. The carry state therefore holds + state_length = (kernel_size - 1) * dilation positions per request instead of kernel_size - 1. + Dilation 1 (the default) is the undilated case and keeps the original state length, so models + exported before the attribute existed are unaffected. input and output are already token-major + (sequence-major, channels-last), so this op needs no separate layout attribute. #### Version @@ -7197,6 +7356,8 @@ This version of the operator has been available since version 1 of the 'com.micr
activation : string
Fused activation function. One of: 'silu', 'swish', 'none'. Default is 'none'.
+
dilation : int
+
Spacing between kernel taps along the sequence axis. The receptive field spans (kernel_size - 1) * dilation positions before the current token, and initial_state / final_state hold that many positions per request. Must be >= 1. Default is 1 (undilated).
state_update_capacity : int
Static number of compact contiguous-prefix transition values to expose per request. Valid range is [0, 8]. capture_count is required exactly when this is positive.
@@ -7213,7 +7374,7 @@ This version of the operator has been available since version 1 of the 'com.micr
bias (optional) : T
Optional per-channel bias with shape (channels). Because the following initial_state input is required, an omitted bias must still occupy this position as an empty input name so initial_state stays at input index 4.
initial_state : T
-
Required committed carry state with shape (batch_size, channels, kernel_size - 1).
+
Required committed carry state with shape (batch_size, channels, (kernel_size - 1) * dilation).
capture_count (optional) : M
Optional device int32 tensor with shape (batch_size). For each request, captures that many local tokens from the contiguous prefix, clamped to the sequence length and state_update_capacity. Required exactly when state_update_capacity is positive.
@@ -7224,7 +7385,7 @@ This version of the operator has been available since version 1 of the 'com.micr
output : T
Token-major convolution output with the same shape as input.
final_state : T
-
Fully written state after each sequence's final token, with shape (batch_size, channels, kernel_size - 1).
+
Fully written state after each sequence's final token, with shape (batch_size, channels, (kernel_size - 1) * dilation).
state_update (optional) : T
Optional compact transition values with shape (batch_size, state_update_capacity, channels). Inactive slots are zero.
diff --git a/docs/OperatorKernels.md b/docs/OperatorKernels.md index 96a6c76f63c09..a49a82a073b26 100644 --- a/docs/OperatorKernels.md +++ b/docs/OperatorKernels.md @@ -582,6 +582,7 @@ The **OpSet Version** column uses the following notation: |DynamicQuantizeMatMul|*in* A:**T1**
*in* B:**T2**
*in* b_scale:**T1**
*in* b_zero_point:**T2**
*in* bias:**T1**
*out* Y:**T1**|1+|**T1** = tensor(float)
**T2** = tensor(int8), tensor(uint8)| |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)| +|EngramGate|*in* key:**T**
*in* query:**T**
*in* value:**T**
*in* key_norm_scale:**T**
*in* query_norm_scale:**T**
*out* output:**T**|1+|**T** = tensor(float), tensor(float16)| |ExpandDims|*in* X:**T**
*in* axis:**tensor(int32)**
*out* Y:**T**|1+|**T** = tensor(bfloat16), tensor(bool), tensor(double), tensor(float), tensor(float16), tensor(int16), tensor(int32), tensor(int64), tensor(int8), tensor(string), tensor(uint16), tensor(uint32), tensor(uint64), tensor(uint8)
**axis** = tensor(int32)| |FastGelu|*in* X:**T**
*in* bias:**T**
*out* Y:**T**|1+|**T** = tensor(float)| |FusedConv|*in* X:**T**
*in* W:**T**
*in* B:**T**
*in* Z:**T**
*out* Y:**T**|1+|**T** = tensor(float)| @@ -608,6 +609,7 @@ The **OpSet Version** column uses the following notation: |MoE|*in* input:**T**
*in* router_probs:**T**
*in* fc1_experts_weights:**T**
*in* fc1_experts_bias:**T**
*in* fc2_experts_weights:**T**
*in* fc2_experts_bias:**T**
*in* fc3_experts_weights:**T**
*in* fc3_experts_bias:**T**
*out* output:**T**|1+|**T** = tensor(float)| |MultiHeadAttention|*in* query:**T**
*in* key:**T**
*in* value:**T**
*in* bias:**T**
*in* key_padding_mask:**M**
*in* attention_bias:**T**
*in* past_key:**T**
*in* past_value:**T**
*in* past_sequence_length:**M**
*in* cache_indirection:**M**
*out* output:**T**
*out* present_key:**T**
*out* present_value:**T**
*out* qk:**QK**|1+|**T** = tensor(float)| |MurmurHash3|*in* X:**T1**
*out* Y:**T2**|1+|**T1** = tensor(double), tensor(float), tensor(int32), tensor(int64), tensor(string), tensor(uint32), tensor(uint64)
**T2** = tensor(int32), tensor(uint32)| +|NGramHashMapping|*in* input_ids:**M**
*in* multipliers:**M**
*in* vocab_sizes:**M**
*in* past_ids:**M**
*out* hash_ids:**M**
*out* present_ids:**M**|1+|**M** = tensor(int32), tensor(int64)| |NGramRepeatBlock|*in* input_ids:**Tid**
*in* scores:**T**
*out* scores_out:**T**|1+|**T** = tensor(float)
**Tid** = tensor(int64)| |NhwcMaxPool|*in* x:**T**
*out* y:**T**|1+|**T** = tensor(int8), tensor(uint8)| |Pad|*in* data:**T**
*in* pads:**tensor(int64)**
*in* value:**T**
*out* output:**T**|1+|**T** = tensor(float)| @@ -1087,6 +1089,7 @@ The **OpSet Version** column uses the following notation: |DequantizeWithOrder|*in* input:**Q**
*in* scale_input:**S**
*out* output:**F**|1+|**F** = tensor(float), tensor(float16)
**Q** = tensor(int8)
**S** = tensor(float)| |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**
*out* output:**T**|1+|**T** = tensor(bfloat16), tensor(float), tensor(float16)| |FastGelu|*in* X:**T**
*in* bias:**T**
*out* Y:**T**|1+|**T** = tensor(bfloat16), tensor(double), tensor(float), tensor(float16)| |FusedConv|*in* X:**T**
*in* W:**T**
*in* B:**T**
*in* Z:**T**
*out* Y:**T**|1+|**T** = tensor(float)| |FusedMatMul|*in* A:**T**
*in* B:**T**
*out* Y:**T**|1+|**T** = tensor(bfloat16), tensor(double), tensor(float), tensor(float16)| @@ -1114,6 +1117,7 @@ The **OpSet Version** column uses the following notation: |MatMulNBits|*in* A:**T1**
*in* B:**T2**
*in* scales:**T1**
*in* zero_points:**T3**
*in* g_idx:**T4**
*in* bias:**T1**
*out* Y:**T1**|1+|**T1** = tensor(bfloat16), tensor(float), tensor(float16)
**T2** = tensor(uint8)
**T3** = tensor(bfloat16), tensor(float), tensor(float16), tensor(uint8)| |MoE|*in* input:**T**
*in* router_probs:**T**
*in* fc1_experts_weights:**T**
*in* fc1_experts_bias:**T**
*in* fc2_experts_weights:**T**
*in* fc2_experts_bias:**T**
*in* fc3_experts_weights:**T**
*in* fc3_experts_bias:**T**
*out* output:**T**|1+|**T** = tensor(bfloat16), tensor(float), tensor(float16)| |MultiHeadAttention|*in* query:**T**
*in* key:**T**
*in* value:**T**
*in* bias:**T**
*in* key_padding_mask:**M**
*in* attention_bias:**T**
*in* past_key:**T**
*in* past_value:**T**
*in* past_sequence_length:**M**
*in* cache_indirection:**M**
*out* output:**T**
*out* present_key:**T**
*out* present_value:**T**
*out* qk:**QK**|1+|**QK** = tensor(bfloat16), tensor(float), tensor(float16)
**T** = tensor(bfloat16), tensor(float), tensor(float16)| +|NGramHashMapping|*in* input_ids:**M**
*in* multipliers:**M**
*in* vocab_sizes:**M**
*in* past_ids:**M**
*out* hash_ids:**M**
*out* present_ids:**M**|1+|**M** = tensor(int32), tensor(int64)| |NGramRepeatBlock|*in* input_ids:**Tid**
*in* scores:**T**
*out* scores_out:**T**|1+|**T** = tensor(float)
**Tid** = tensor(int64)| |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)| diff --git a/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state.cc b/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state.cc index d48ba7baa46c2..7585993088805 100644 --- a/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state.cc +++ b/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state.cc @@ -49,6 +49,10 @@ CausalConvWithState::CausalConvWithState(const OpKernelInfo& info) : OpKernel ORT_ENFORCE(activation_ == "none" || activation_ == "silu" || activation_ == "swish", "activation must be one of: none, silu, swish"); + ORT_THROW_IF_ERROR(causal_conv_with_state_helper::ParseDilation(info, dilation_)); + ORT_THROW_IF_ERROR(causal_conv_with_state_helper::ParseChannelsLast(info, channels_last_)); + ORT_ENFORCE(!channels_last_ || ndim_ == 1, "channels_last requires ndim = 1"); + ORT_ENFORCE(info.GetAttrOrDefault("state_window", 0) == 0, "CPU CausalConvWithState does not support state_window > 0 (CUDA EP only)"); state_window_ = 0; @@ -97,84 +101,121 @@ inline void ProcessChannelDecodeFixedK( } } +// A channel's positions are contiguous in the channels-first layout (stride 1) and `channels` +// apart in the channels-last one, so every per-channel walk goes through these two helpers. +inline void GatherStrided(float* dst, const float* src, int64_t stride, int64_t count) { + if (stride == 1) { + std::memcpy(dst, src, static_cast(count) * sizeof(float)); + return; + } + for (int64_t i = 0; i < count; ++i) { + dst[i] = src[i * stride]; + } +} + +inline void ScatterStrided(float* dst, int64_t stride, const float* src, int64_t count) { + if (stride == 1) { + std::memcpy(dst, src, static_cast(count) * sizeof(float)); + return; + } + for (int64_t i = 0; i < count; ++i) { + dst[i * stride] = src[i]; + } +} + // Decode fast-path: L=1, no padded buffer needed. -// The "visible window" for position 0 is [past_state(K-1 values), input(1 value)] = K values. -// Compute dot(weight, window), shift state left by 1, append new input. +// The "visible window" is [past_state(pad values), input(1 value)] with pad = (K-1)*dilation. +// Tap k reads window position k*dilation, so the last tap is the current input. The state is then +// shifted left by one position and the new input appended. void ProcessChannelDecode( - const float* past_row, // past_state for this (b,c): [K-1] or nullptr - const float* input_val, // &input[b,c,0] — single value + const float* past_row, // past_state for this (b,c): [pad] strided, or nullptr + const float* input_val, // input for this (b,c) — single value const float* w, // weight for this channel: [K] float bias_val, bool apply_silu, - float* out_val, // &output[b,c,0] — single value - float* present_row, // present_state for this (b,c): [K-1] - int64_t K) { - int64_t pad = K - 1; + float* out_val, // output for this (b,c) — single value + float* present_row, // present_state for this (b,c): [pad] strided + int64_t state_stride, + int64_t K, + int64_t dilation) { + const int64_t pad = (K - 1) * dilation; // Dot product over the window: [past_state..., input] float sum = bias_val; - // First K-1 elements come from past_state + // The first K-1 taps land in past_state, spaced `dilation` apart. if (past_row != nullptr) { - for (int64_t k = 0; k < pad; ++k) { - sum += w[k] * past_row[k]; + for (int64_t k = 0; k < K - 1; ++k) { + sum += w[k] * past_row[k * dilation * state_stride]; } } - // Last element is the current input - sum += w[pad] * input_val[0]; + // Last tap is the current input + sum += w[K - 1] * input_val[0]; if (apply_silu) { sum = ApplySilu(sum); } out_val[0] = sum; - // Update present_state: shift past_state left by 1, append input + // Update present_state: shift past_state left by 1, append input. The copy runs forward from a + // higher source index, so it stays correct even if past_state and present_state are the same + // buffer. if (pad > 0) { - if (past_row != nullptr && pad > 1) { - std::memcpy(present_row, past_row + 1, static_cast(pad - 1) * sizeof(float)); - } else if (pad > 1) { - std::memset(present_row, 0, static_cast(pad - 1) * sizeof(float)); + for (int64_t s = 0; s < pad - 1; ++s) { + present_row[s * state_stride] = + (past_row != nullptr) ? past_row[(s + 1) * state_stride] : 0.0f; } - present_row[pad - 1] = input_val[0]; + present_row[(pad - 1) * state_stride] = input_val[0]; } } // Prefill path: L>1, uses padded buffer for the convolution window. void ProcessChannelPrefill( - const float* past_row, // past_state for this (b,c): [K-1] or nullptr - const float* in_row, // input for this (b,c): [L] + const float* past_row, // past_state for this (b,c): [pad] strided, or nullptr + const float* in_row, // input for this (b,c): [L] strided const float* w, // weight for this channel: [K] float bias_val, bool apply_silu, - float* out_row, // output for this (b,c): [L] - float* present_row, // present_state for this (b,c): [K-1] - float* padded_row, // scratch buffer: [K-1 + L] + float* out_row, // output for this (b,c): [L] strided + float* present_row, // present_state for this (b,c): [pad] strided + float* padded_row, // scratch buffer: [pad + L] + float* out_buf, // scratch buffer: [L], only used when act_stride != 1 + int64_t act_stride, + int64_t state_stride, int64_t L, - int64_t K) { - int64_t pad = K - 1; - int64_t padded_len = pad + L; + int64_t K, + int64_t dilation) { + const int64_t pad = (K - 1) * dilation; + const int64_t padded_len = pad + L; // Build padded window: [past_state | input] if (past_row != nullptr) { - std::memcpy(padded_row, past_row, static_cast(pad) * sizeof(float)); + GatherStrided(padded_row, past_row, state_stride, pad); } else { std::memset(padded_row, 0, static_cast(pad) * sizeof(float)); } - std::memcpy(padded_row + pad, in_row, static_cast(L) * sizeof(float)); + GatherStrided(padded_row + pad, in_row, act_stride, L); - // Depthwise 1D convolution + // Depthwise 1D convolution. Tap k of output position l reads padded_row[l + k*dilation]; at + // k = K-1 that is padded_row[l + pad], i.e. the current input position. + // A contiguous output row is written in place; only a strided one needs the scratch round trip, + // so the channels-first layout keeps the exact write pattern it had before strides existed. + float* conv_dst = act_stride == 1 ? out_row : out_buf; for (int64_t l = 0; l < L; ++l) { float sum = bias_val; for (int64_t k = 0; k < K; ++k) { - sum += w[k] * padded_row[l + k]; + sum += w[k] * padded_row[l + k * dilation]; } if (apply_silu) { sum = ApplySilu(sum); } - out_row[l] = sum; + conv_dst[l] = sum; + } + if (act_stride != 1) { + ScatterStrided(out_row, act_stride, out_buf, L); } - // Save present_state: last K-1 elements of (past_state | input) - std::memcpy(present_row, padded_row + padded_len - pad, static_cast(pad) * sizeof(float)); + // Save present_state: last pad elements of (past_state | input) + ScatterStrided(present_row, state_stride, padded_row + padded_len - pad, pad); } } // anonymous namespace @@ -192,13 +233,22 @@ Status CausalConvWithState::Compute(OpKernelContext* context) const { const auto& input_shape = input_tensor->Shape(); const auto& weight_shape = weight_tensor->Shape(); - ORT_RETURN_IF_NOT(static_cast(input_shape.NumDimensions()) == 2 + ndim_, - "input must have ", 2 + ndim_, " dimensions for ndim=", ndim_); + if (channels_last_) { + // (batch_size, sequence_length, d_1, ..., d_n): any number of trailing channel axes, so a + // caller that keeps hyper-connections and hidden size separate needs no reshape. + ORT_RETURN_IF_NOT(input_shape.NumDimensions() >= 3, + "input must have at least 3 dimensions when channels_last = 1"); + } else { + ORT_RETURN_IF_NOT(static_cast(input_shape.NumDimensions()) == 2 + ndim_, + "input must have ", 2 + ndim_, " dimensions for ndim=", ndim_); + } ORT_RETURN_IF_NOT(static_cast(weight_shape.NumDimensions()) == 2 + ndim_, "weight must have ", 2 + ndim_, " dimensions for ndim=", ndim_); const int64_t batch_size = input_shape[0]; - const int64_t channels = input_shape[1]; + const int64_t channels = channels_last_ + ? input_shape.SizeFromDimension(2) + : input_shape[1]; ORT_RETURN_IF_NOT(weight_shape[0] == channels, "weight channels must match input channels"); ORT_RETURN_IF_NOT(weight_shape[1] == 1, "weight must be depthwise (group=1)"); @@ -211,28 +261,26 @@ Status CausalConvWithState::Compute(OpKernelContext* context) const { // ==== ndim=1 implementation: (B, C, L) with kernel (C, 1, K) ==== if (ndim_ == 1) { - const int64_t L = input_shape[2]; + const int64_t L = channels_last_ ? input_shape[1] : input_shape[2]; const int64_t K = weight_shape[2]; - const int64_t pad = K - 1; - - if (past_state_tensor != nullptr) { - const auto& ps_shape = past_state_tensor->Shape(); - ORT_RETURN_IF_NOT(ps_shape.NumDimensions() == 3 && - ps_shape[0] == batch_size && - ps_shape[1] == channels && - ps_shape[2] == pad, - "past_state must be (B, C, K-1)"); - } + const int64_t dilation = dilation_; + const int64_t pad = (K - 1) * dilation; // ==== Allocate outputs ==== Tensor* output_tensor = context->Output(0, input_shape); float* output_data = output_tensor->MutableData(); - // state_window_ is always 0 on CPU, so this is the legacy (B, C, K-1) shape. + // state_window_ is always 0 on CPU, so the state has no leading window axis. TensorShape state_shape; - ORT_RETURN_IF_ERROR(causal_conv_with_state_helper::CheckInputs( - state_window_, static_cast(batch_size), static_cast(channels), - static_cast(pad), past_state_tensor, state_shape, "CausalConvWithState")); + if (channels_last_) { + ORT_RETURN_IF_ERROR(causal_conv_with_state_helper::CheckInputsChannelsLast( + state_window_, input_shape, static_cast(pad), past_state_tensor, state_shape, + "CausalConvWithState")); + } else { + ORT_RETURN_IF_ERROR(causal_conv_with_state_helper::CheckInputs( + state_window_, static_cast(batch_size), static_cast(channels), + static_cast(pad), past_state_tensor, state_shape, "CausalConvWithState")); + } Tensor* present_state_tensor = context->Output(1, state_shape); float* present_data = present_state_tensor->MutableData(); @@ -242,6 +290,11 @@ Status CausalConvWithState::Compute(OpKernelContext* context) const { const float* past_data = past_state_tensor ? past_state_tensor->Data() : nullptr; bool apply_silu = (activation_ == "silu" || activation_ == "swish"); + // Both layouts are dense, so one strided (batch, position, channel) view covers them. + const auto act_layout = causal_conv_with_state_helper::MakeLayout(channels_last_, channels, L); + const auto state_layout = + causal_conv_with_state_helper::MakeLayout(channels_last_, channels, pad); + // ==== Thread-parallel over (batch, channel) pairs ==== // Depthwise conv: each channel is fully independent. int64_t total_tasks = batch_size * channels; @@ -260,15 +313,19 @@ Status CausalConvWithState::Compute(OpKernelContext* context) const { int64_t b = task / channels; int64_t c = task % channels; - const float* past_row = past_data - ? past_data + (b * channels + c) * pad - : nullptr; - const float* input_val = input_data + (b * channels + c) * L; + const int64_t act_offset = act_layout.Offset(b, 0, c); + const int64_t state_offset = state_layout.Offset(b, 0, c); + + const float* past_row = past_data ? past_data + state_offset : nullptr; + const float* input_val = input_data + act_offset; const float* w = weight_data + c * K; float bias_val = bias_data ? bias_data[c] : 0.0f; - float* out_val = output_data + (b * channels + c) * L; - float* present_row = present_data + (b * channels + c) * pad; - switch (K) { + float* out_val = output_data + act_offset; + float* present_row = present_data + state_offset; + // ProcessChannelDecodeFixedK assumes pad == K - 1 and contiguous state, so it only + // applies to the undilated channels-first case. + const int64_t fixed_k = (dilation == 1 && state_layout.pos_stride == 1) ? K : 0; + switch (fixed_k) { case 2: ProcessChannelDecodeFixedK<2>(past_row, input_val, w, bias_val, apply_silu, out_val, present_row); @@ -287,7 +344,7 @@ Status CausalConvWithState::Compute(OpKernelContext* context) const { break; default: ProcessChannelDecode(past_row, input_val, w, bias_val, apply_silu, - out_val, present_row, K); + out_val, present_row, state_layout.pos_stride, K, dilation); break; } } @@ -299,24 +356,28 @@ Status CausalConvWithState::Compute(OpKernelContext* context) const { static_cast(total_tasks), cost_per_task, [&](std::ptrdiff_t first, std::ptrdiff_t last) { - // Per-thread scratch buffer for padded input + // Per-thread scratch buffers for the padded input window and, only when the output row + // is strided, the contiguous convolution result that is then scattered into it. std::vector padded_buf(static_cast(pad + L)); + std::vector out_buf(act_layout.pos_stride == 1 ? 0 : static_cast(L)); for (std::ptrdiff_t task = first; task < last; ++task) { int64_t b = task / channels; int64_t c = task % channels; - const float* past_row = past_data - ? past_data + (b * channels + c) * pad - : nullptr; - const float* in_row = input_data + (b * channels + c) * L; + const int64_t act_offset = act_layout.Offset(b, 0, c); + const int64_t state_offset = state_layout.Offset(b, 0, c); + + const float* past_row = past_data ? past_data + state_offset : nullptr; + const float* in_row = input_data + act_offset; const float* w = weight_data + c * K; float bias_val = bias_data ? bias_data[c] : 0.0f; - float* out_row = output_data + (b * channels + c) * L; - float* present_row = present_data + (b * channels + c) * pad; + float* out_row = output_data + act_offset; + float* present_row = present_data + state_offset; ProcessChannelPrefill(past_row, in_row, w, bias_val, apply_silu, - out_row, present_row, padded_buf.data(), L, K); + out_row, present_row, padded_buf.data(), out_buf.data(), + act_layout.pos_stride, state_layout.pos_stride, L, K, dilation); } }); } diff --git a/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state.h b/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state.h index 0e552e7bd27dd..19467ac50a918 100644 --- a/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state.h +++ b/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state.h @@ -19,6 +19,8 @@ class CausalConvWithState final : public OpKernel { private: int ndim_; + int dilation_; + bool channels_last_; std::string activation_; // Always 0 on CPU (a state window is CUDA-only), but kept so the shared shape helper in // causal_conv_with_state_helper.h is driven the same way on every EP. diff --git a/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state_helper.h b/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state_helper.h index 9e5c737dabaec..2d1c96dc8175b 100644 --- a/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state_helper.h +++ b/onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state_helper.h @@ -30,8 +30,91 @@ Status ParseStateWindow(const TKernelInfo& info, int& state_window) { return Status::OK(); } +// Reads and validates the optional `dilation` attribute. +// +// 1 (the default, i.e. attribute absent) is the undilated case, which is what every model exported +// before the attribute existed uses, so this must stay the default. +template +Status ParseDilation(const TKernelInfo& info, int& dilation) { + const int64_t value = info.template GetAttrOrDefault("dilation", 1); + if (value < 1) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "dilation must be >= 1, got ", value); + } + dilation = static_cast(value); + return Status::OK(); +} + +// Reads and validates the optional `channels_last` attribute. +// +// 0 (the default, i.e. attribute absent) is the channels-first (batch_size, channels, seq_len) +// layout that every model exported before the attribute existed uses. +template +Status ParseChannelsLast(const TKernelInfo& info, bool& channels_last) { + const int64_t value = info.template GetAttrOrDefault("channels_last", 0); + if (value != 0 && value != 1) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "channels_last must be 0 or 1, got ", + value); + } + channels_last = (value == 1); + return Status::OK(); +} + +// Element strides of a (batch, position, channel) view over an activation or state tensor. +// Both supported layouts are dense, so a single strided view covers them and every kernel can be +// written once against (batch, position, channel) coordinates. +struct Layout { + int64_t batch_stride; + int64_t pos_stride; + int64_t chan_stride; + + int64_t Offset(int64_t b, int64_t pos, int64_t c) const { + return b * batch_stride + pos * pos_stride + c * chan_stride; + } +}; + +// `length` is the extent of the position axis: seq_len for activations, state_length for state. +constexpr Layout MakeLayout(bool channels_last, int64_t channels, int64_t length) { + return channels_last ? Layout{channels * length, channels, 1} + : Layout{channels * length, 1, length}; +} + +// Derives the expected channels-last past_state / present_state shape, +// (batch_size, state_length, d_1, ..., d_n), from the input shape and validates past_state. +// The trailing channel axes are copied verbatim from the input, so a caller that keeps +// hyper-connections and hidden size as separate axes gets the same split back and needs no reshape. +template +Status CheckInputsChannelsLast(int state_window, + const TensorShape& input_shape, + int state_length, + const T* past_state, + TensorShape& state_shape, + std::string_view op_name) { + TensorShapeVector dims; + if (state_window > 0) { + dims.push_back(state_window); + } + dims.push_back(input_shape[0]); + dims.push_back(state_length); + for (size_t i = 2; i < input_shape.NumDimensions(); ++i) { + dims.push_back(input_shape[i]); + } + state_shape = TensorShape(dims); + + if (past_state != nullptr && past_state->Shape() != state_shape) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Input 'past_state' is expected to have shape ", state_shape.ToString(), + ", got ", past_state->Shape().ToString(), + ". ", op_name, + " with channels_last = 1 uses " + "(batch_size, (kernel_size - 1) * dilation, d_1, ..., d_n), optionally " + "led by a state_window axis."); + } + + return Status::OK(); +} + // Derives the expected past_state / present_state shape and validates past_state against it. -// `state_length` is the carry length along the causal axis, i.e. kernel_size - 1. +// `state_length` is the carry length along the causal axis, i.e. (kernel_size - 1) * dilation. // // state_window == 0 -> (batch_size, channels, state_length). A single state with no window axis: // the backward-compatible layout that models exported before the attribute existed use. @@ -62,9 +145,10 @@ Status CheckInputs(int state_window, "Input 'past_state' is expected to have shape ", state_shape.ToString(), ", got ", past_state->Shape().ToString(), ". ", op_name, - " uses (batch_size, channels, kernel_size - 1) when " + " uses (batch_size, channels, (kernel_size - 1) * dilation) when " "the state_window attribute is absent or 0, and " - "(state_window, batch_size, channels, kernel_size - 1) otherwise."); + "(state_window, batch_size, channels, (kernel_size - 1) * dilation) " + "otherwise."); } return Status::OK(); diff --git a/onnxruntime/contrib_ops/cpu/bert/engram_gate.cc b/onnxruntime/contrib_ops/cpu/bert/engram_gate.cc new file mode 100644 index 0000000000000..bd7d9da8c241b --- /dev/null +++ b/onnxruntime/contrib_ops/cpu/bert/engram_gate.cc @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "contrib_ops/cpu/bert/engram_gate.h" + +#include + +#include "contrib_ops/cpu/bert/engram_helper.h" +#include "core/common/narrow.h" +#include "core/platform/threadpool.h" + +using onnxruntime::concurrency::ThreadPool; + +namespace onnxruntime { +namespace contrib { + +#define REGISTER_ENGRAM_GATE_TYPED(T) \ + ONNX_OPERATOR_TYPED_KERNEL_EX( \ + EngramGate, \ + kMSDomain, \ + 1, \ + T, \ + kCpuExecutionProvider, \ + KernelDefBuilder() \ + .TypeConstraint("T", DataTypeImpl::GetTensorType()), \ + EngramGate); + +REGISTER_ENGRAM_GATE_TYPED(float) +REGISTER_ENGRAM_GATE_TYPED(MLFloat16) + +#undef REGISTER_ENGRAM_GATE_TYPED + +template +EngramGate::EngramGate(const OpKernelInfo& info) : OpKernel(info) { + epsilon_ = info.GetAttrOrDefault("epsilon", 1.0e-5f); +} + +template +Status EngramGate::Compute(OpKernelContext* context) const { + const Tensor* key = context->Input(0); + const Tensor* query = context->Input(1); + const Tensor* value = context->Input(2); + const Tensor* key_norm_scale = context->Input(3); + const Tensor* query_norm_scale = context->Input(4); + + const TensorShape& key_shape = key->Shape(); + ORT_RETURN_IF_NOT(key_shape.NumDimensions() == 4, + "key must have shape (batch_size, sequence_length, hc_mult, hidden_size)"); + const int64_t batch_size = key_shape[0]; + const int64_t sequence_length = key_shape[1]; + const int64_t hc_mult = key_shape[2]; + const int64_t hidden_size = key_shape[3]; + + ORT_RETURN_IF_NOT(query->Shape() == key_shape, "query must have the same shape as key"); + ORT_RETURN_IF_NOT(value->Shape() == TensorShape({batch_size, sequence_length, hidden_size}), + "value must have shape (batch_size, sequence_length, hidden_size)"); + ORT_RETURN_IF_NOT(key_norm_scale->Shape() == TensorShape({hc_mult, hidden_size}), + "key_norm_scale must have shape (hc_mult, hidden_size)"); + ORT_RETURN_IF_NOT(query_norm_scale->Shape() == TensorShape({hc_mult, hidden_size}), + "query_norm_scale must have shape (hc_mult, hidden_size)"); + + Tensor* output = context->Output(0, key_shape); + if (key_shape.Size() == 0) { + return Status::OK(); + } + + const T* key_data = key->Data(); + const T* query_data = query->Data(); + const T* value_data = value->Data(); + const T* key_scale_data = key_norm_scale->Data(); + const T* query_scale_data = query_norm_scale->Data(); + T* output_data = output->MutableData(); + + const int64_t rows = batch_size * sequence_length * hc_mult; + ThreadPool::TryParallelFor( + // Each row makes one fused reduction pass and one output pass over hidden_size, plus a + // handful of scalar transcendentals. Costing it as a single pass would over-partition. + context->GetOperatorThreadPool(), narrow(rows), + static_cast(2 * hidden_size + 32), + [&](ptrdiff_t begin, ptrdiff_t end) { + for (int64_t row = begin; row < end; ++row) { + const int64_t g = row % hc_mult; + const int64_t token = row / hc_mult; + const T* key_row = key_data + row * hidden_size; + const T* query_row = query_data + row * hidden_size; + const T* value_row = value_data + token * hidden_size; + + // Both inverse RMS factors are scalars, so they can be pulled out of the dot product and + // applied afterwards. That folds the two reductions into one pass over key_row and + // query_row, which is what the CUDA and WGSL kernels already do. + const T* key_scale_row = key_scale_data + g * hidden_size; + const T* query_scale_row = query_scale_data + g * hidden_size; + float key_sum_sq = 0.0f; + float query_sum_sq = 0.0f; + float dot_numerator = 0.0f; + for (int64_t c = 0; c < hidden_size; ++c) { + const float key_value = static_cast(key_row[c]); + const float query_value = static_cast(query_row[c]); + key_sum_sq += key_value * key_value; + query_sum_sq += query_value * query_value; + dot_numerator += key_value * static_cast(key_scale_row[c]) * + query_value * static_cast(query_scale_row[c]); + } + + const float key_inv_rms = 1.0f / std::sqrt(key_sum_sq / static_cast(hidden_size) + epsilon_); + const float query_inv_rms = 1.0f / std::sqrt(query_sum_sq / static_cast(hidden_size) + epsilon_); + const float dot = + dot_numerator * key_inv_rms * query_inv_rms / std::sqrt(static_cast(hidden_size)); + const float gate = engram_helper::SigmoidFloat(engram_helper::EngramGateArg(dot)); + + T* output_row = output_data + row * hidden_size; + for (int64_t c = 0; c < hidden_size; ++c) { + output_row[c] = static_cast(gate * static_cast(value_row[c])); + } + } + }); + + return Status::OK(); +} + +template class EngramGate; +template class EngramGate; + +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cpu/bert/engram_gate.h b/onnxruntime/contrib_ops/cpu/bert/engram_gate.h new file mode 100644 index 0000000000000..ec92da027dde4 --- /dev/null +++ b/onnxruntime/contrib_ops/cpu/bert/engram_gate.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "core/common/common.h" +#include "core/framework/op_kernel.h" + +namespace onnxruntime { +namespace contrib { + +template +class EngramGate final : public OpKernel { + public: + explicit EngramGate(const OpKernelInfo& info); + Status Compute(OpKernelContext* context) const override; + + private: + float epsilon_; +}; + +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cpu/bert/engram_helper.h b/onnxruntime/contrib_ops/cpu/bert/engram_helper.h new file mode 100644 index 0000000000000..72abdf3ee2368 --- /dev/null +++ b/onnxruntime/contrib_ops/cpu/bert/engram_helper.h @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include + +namespace onnxruntime { +namespace contrib { +namespace engram_helper { + +// Numerically stable logistic function. +inline float SigmoidFloat(float x) { + if (x > 0.0f) { + return 1.0f / (1.0f + std::exp(-x)); + } + const float exp_x = std::exp(x); + return exp_x / (1.0f + exp_x); +} + +// Engram gate pre-activation: sign(dot) * sqrt(max(abs(dot), 1e-6)). +// std::copysign cannot be used here because it maps a zero dot product to +sqrt(1e-6) instead of +// zero, which would disagree with the schema formula and with the other execution providers. +inline float EngramGateArg(float dot) { + if (dot == 0.0f) { + return 0.0f; + } + const float magnitude = std::sqrt(std::max(std::abs(dot), 1.0e-6f)); + return dot < 0.0f ? -magnitude : magnitude; +} + +// Euclidean modulo: the result always has the sign of `mod`, which must be positive. +template +inline T PositiveMod(T value, T mod) { + T result = static_cast(value % mod); + if (result < 0) { + result = static_cast(result + mod); + } + return result; +} + +// Multiplies through the unsigned counterpart of T so that overflow wraps around instead of +// being undefined behavior. +template +inline T WrappedMultiply(T a, T b) { + using UnsignedT = typename std::make_unsigned::type; + return static_cast(static_cast(a) * static_cast(b)); +} + +} // namespace engram_helper +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cpu/bert/ngram_hash_mapping.cc b/onnxruntime/contrib_ops/cpu/bert/ngram_hash_mapping.cc new file mode 100644 index 0000000000000..2b8250e9df817 --- /dev/null +++ b/onnxruntime/contrib_ops/cpu/bert/ngram_hash_mapping.cc @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "contrib_ops/cpu/bert/ngram_hash_mapping.h" + +#include +#include + +#include "contrib_ops/cpu/bert/engram_helper.h" +#include "core/common/narrow.h" +#include "core/platform/threadpool.h" + +using onnxruntime::concurrency::ThreadPool; + +namespace onnxruntime { +namespace contrib { + +#define REGISTER_NGRAM_HASH_TYPED(T) \ + ONNX_OPERATOR_TYPED_KERNEL_EX( \ + NGramHashMapping, \ + kMSDomain, \ + 1, \ + T, \ + kCpuExecutionProvider, \ + KernelDefBuilder() \ + .MayInplace(3, 1) \ + .TypeConstraint("M", DataTypeImpl::GetTensorType()), \ + NGramHashMapping); + +REGISTER_NGRAM_HASH_TYPED(int32_t) +REGISTER_NGRAM_HASH_TYPED(int64_t) + +#undef REGISTER_NGRAM_HASH_TYPED + +template +NGramHashMapping::NGramHashMapping(const OpKernelInfo& info) : OpKernel(info) { + ORT_ENFORCE(info.GetAttr("max_ngram_size", &max_ngram_size_).IsOK(), + "max_ngram_size attribute is required"); + ORT_ENFORCE(info.GetAttr("n_head_per_ngram", &n_head_per_ngram_).IsOK(), + "n_head_per_ngram attribute is required"); + int64_t pad_id = 0; + ORT_ENFORCE(info.GetAttr("pad_id", &pad_id).IsOK(), "pad_id attribute is required"); + ORT_ENFORCE(max_ngram_size_ >= 2, "max_ngram_size must be at least 2"); + ORT_ENFORCE(n_head_per_ngram_ >= 1, "n_head_per_ngram must be positive"); + ORT_ENFORCE(pad_id >= static_cast(std::numeric_limits::min()) && + pad_id <= static_cast(std::numeric_limits::max()), + "pad_id is out of range for the input id type"); + pad_id_ = static_cast(pad_id); +} + +// Reads the id at right-aligned history slot `slot` of past_ids. Slots outside the provided history +// (or a missing past_ids) are positions before the start of the whole sequence, so they use pad_id. +template +T NGramHashMapping::HistoryId(const T* past_data, int64_t b, int64_t slot, int64_t state_length) const { + if (past_data == nullptr || slot < 0 || slot >= state_length) { + return pad_id_; + } + return past_data[b * state_length + slot]; +} + +template +Status NGramHashMapping::Compute(OpKernelContext* context) const { + const Tensor* input_ids = context->Input(0); + const Tensor* multipliers = context->Input(1); + const Tensor* vocab_sizes = context->Input(2); + const Tensor* past_ids = context->Input(3); + + const TensorShape& input_shape = input_ids->Shape(); + ORT_RETURN_IF_NOT(input_shape.NumDimensions() == 2, "input_ids must have rank 2"); + ORT_RETURN_IF_NOT(multipliers->Shape().NumDimensions() == 1 && + multipliers->Shape()[0] == max_ngram_size_, + "multipliers must have shape (max_ngram_size)"); + const int64_t num_heads = (max_ngram_size_ - 1) * n_head_per_ngram_; + ORT_RETURN_IF_NOT(vocab_sizes->Shape().NumDimensions() == 1 && vocab_sizes->Shape()[0] == num_heads, + "vocab_sizes must have shape ((max_ngram_size - 1) * n_head_per_ngram)"); + + const int64_t batch_size = input_shape[0]; + const int64_t sequence_length = input_shape[1]; + // An n-gram window reaches this many positions before the current token. + const int64_t state_length = max_ngram_size_ - 1; + if (past_ids != nullptr) { + ORT_RETURN_IF_NOT(past_ids->Shape() == TensorShape({batch_size, state_length}), + "past_ids must have shape (batch_size, max_ngram_size - 1)"); + } + + Tensor* output = context->Output(0, TensorShape({batch_size, sequence_length, num_heads})); + Tensor* present_ids = context->Output(1, TensorShape({batch_size, state_length})); + + const T* input_data = input_ids->Data(); + const T* multiplier_data = multipliers->Data(); + const T* vocab_data = vocab_sizes->Data(); + const T* past_data = past_ids == nullptr ? nullptr : past_ids->Data(); + + // A non-positive head vocabulary size has no meaningful modulo. Every EP guards the division to + // avoid a device-side divide-by-zero, which turns the mistake into a constant hash id of 0 for that + // head rather than a crash. That is a silent wrong answer, so validate it here where vocab_sizes is + // already resident on the host and the check costs one pass over a tiny tensor. + for (int64_t h = 0; h < num_heads; ++h) { + ORT_RETURN_IF_NOT(vocab_data[h] > 0, + "vocab_sizes must be positive; entry ", h, " is ", static_cast(vocab_data[h])); + } + + if (input_shape.Size() != 0) { + T* output_data = output->MutableData(); + const int64_t total = batch_size * sequence_length; + ThreadPool::TryParallelFor( + context->GetOperatorThreadPool(), narrow(total), static_cast(max_ngram_size_ * n_head_per_ngram_), + [&](ptrdiff_t begin, ptrdiff_t end) { + for (int64_t linear = begin; linear < end; ++linear) { + const int64_t t = linear % sequence_length; + const int64_t b = linear / sequence_length; + const int64_t input_base = b * sequence_length; + const int64_t output_base = linear * num_heads; + + for (int64_t n = 2; n <= max_ngram_size_; ++n) { + T mix = 0; + for (int64_t k = 0; k < n; ++k) { + const int64_t source_t = t - k; + const T token = source_t >= 0 ? input_data[input_base + source_t] + : HistoryId(past_data, b, state_length + source_t, state_length); + const T product = engram_helper::WrappedMultiply(token, multiplier_data[k]); + mix = k == 0 ? product : static_cast(mix ^ product); + } + + const int64_t ngram_offset = (n - 2) * n_head_per_ngram_; + for (int64_t h = 0; h < n_head_per_ngram_; ++h) { + const int64_t out_h = ngram_offset + h; + // vocab_sizes was validated to be positive above, so the modulo is always well defined. + output_data[output_base + out_h] = engram_helper::PositiveMod(mix, vocab_data[out_h]); + } + } + } + }); + } + + // present_ids is the right-aligned trailing window of (past_ids ++ input_ids), so it is well defined + // even when this call is shorter than the window. It is written last because past_ids may share + // its allocation, and the hash loop above still needs the original history. Within this loop the + // aliased case is safe too: slot j writes index j and reads index j + sequence_length, so the walk + // is strictly ahead of itself. + if (present_ids != nullptr) { + T* present_data = present_ids->MutableData(); + for (int64_t b = 0; b < batch_size; ++b) { + for (int64_t j = 0; j < state_length; ++j) { + // Virtual position of slot j relative to the end of input_ids. + const int64_t source_t = sequence_length - state_length + j; + present_data[b * state_length + j] = + source_t >= 0 ? input_data[b * sequence_length + source_t] + : HistoryId(past_data, b, state_length + source_t, state_length); + } + } + } + + return Status::OK(); +} + +template class NGramHashMapping; +template class NGramHashMapping; + +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cpu/bert/ngram_hash_mapping.h b/onnxruntime/contrib_ops/cpu/bert/ngram_hash_mapping.h new file mode 100644 index 0000000000000..77b9c1cd524fe --- /dev/null +++ b/onnxruntime/contrib_ops/cpu/bert/ngram_hash_mapping.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "core/common/common.h" +#include "core/framework/op_kernel.h" + +namespace onnxruntime { +namespace contrib { + +template +class NGramHashMapping final : public OpKernel { + public: + explicit NGramHashMapping(const OpKernelInfo& info); + Status Compute(OpKernelContext* context) const override; + + private: + T HistoryId(const T* past_data, int64_t b, int64_t slot, int64_t state_length) const; + + int64_t max_ngram_size_; + int64_t n_head_per_ngram_; + T pad_id_; +}; + +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cpu/cpu_contrib_kernels.cc b/onnxruntime/contrib_ops/cpu/cpu_contrib_kernels.cc index 1d323b18af6fe..a3ce50516fae2 100644 --- a/onnxruntime/contrib_ops/cpu/cpu_contrib_kernels.cc +++ b/onnxruntime/contrib_ops/cpu/cpu_contrib_kernels.cc @@ -39,6 +39,10 @@ class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, MLFloat16, LinearAttentionGate); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, float, GatedRMSNorm); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, MLFloat16, GatedRMSNorm); +class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, float, EngramGate); +class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, MLFloat16, EngramGate); +class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, int32_t, NGramHashMapping); +class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, int64_t, NGramHashMapping); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, float, CausalConvWithState); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, float, RotaryEmbedding); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kMSDomain, 1, MLFloat16, RotaryEmbedding); @@ -343,6 +347,10 @@ Status RegisterCpuContribKernels(KernelRegistry& kernel_registry) { BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, diff --git a/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state.cc b/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state.cc index 817097ac34ad4..c79859f735c7f 100644 --- a/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state.cc +++ b/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state.cc @@ -38,6 +38,10 @@ CausalConvWithState::CausalConvWithState(const OpKernelInfo& info) : CudaKern ORT_ENFORCE(activation_ == "none" || activation_ == "silu" || activation_ == "swish", "activation must be one of: none, silu, swish"); + ORT_THROW_IF_ERROR(causal_conv_with_state_helper::ParseDilation(info, dilation_)); + ORT_THROW_IF_ERROR(causal_conv_with_state_helper::ParseChannelsLast(info, channels_last_)); + ORT_ENFORCE(!channels_last_ || ndim_ == 1, "channels_last requires ndim = 1"); + // See LinearAttention: only the trailing per-position states are ever consumed, so a window // caps the allocation and the write traffic for long prompts. 0 keeps the plain single state. ORT_THROW_IF_ERROR(causal_conv_with_state_helper::ParseStateWindow(info, state_window_)); @@ -57,16 +61,26 @@ Status CausalConvWithState::ComputeInternal(OpKernelContext* context) const { const auto& weight_shape = weight_tensor->Shape(); // Validate input rank and weight rank - ORT_RETURN_IF_NOT(input_shape.NumDimensions() == 3, - "input must be rank 3 (batch, channels, length), got rank ", input_shape.NumDimensions()); + if (channels_last_) { + // (batch_size, sequence_length, d_1, ..., d_n): any number of trailing channel axes, so a + // caller that keeps hyper-connections and hidden size separate needs no reshape. + ORT_RETURN_IF_NOT(input_shape.NumDimensions() >= 3, + "input must have rank >= 3 (batch, length, ...channels) when " + "channels_last = 1, got rank ", + input_shape.NumDimensions()); + } else { + ORT_RETURN_IF_NOT(input_shape.NumDimensions() == 3, + "input must be rank 3 (batch, channels, length), got rank ", input_shape.NumDimensions()); + } ORT_RETURN_IF_NOT(weight_shape.NumDimensions() == 3, "weight must be rank 3 (channels, 1, kernel_size), got rank ", weight_shape.NumDimensions()); const int batch_size = static_cast(input_shape[0]); - const int channels = static_cast(input_shape[1]); - const int L = static_cast(input_shape[2]); + const int channels = static_cast(channels_last_ ? input_shape.SizeFromDimension(2) + : input_shape[1]); + const int L = static_cast(channels_last_ ? input_shape[1] : input_shape[2]); const int K = static_cast(weight_shape[2]); - const int pad = K - 1; + const int pad = (K - 1) * dilation_; ORT_RETURN_IF_NOT(L > 0, "input length must be positive, got ", L); @@ -83,13 +97,19 @@ Status CausalConvWithState::ComputeInternal(OpKernelContext* context) const { "bias must have shape (", channels, "), got ", bias_shape.ToString()); } - // past_state / present_state are [B, C, K-1], or [W, B, C, K-1] when state_window_ = W > 0. + // past_state / present_state are [B, C, pad], or [W, B, C, pad] when state_window_ = W > 0, + // where pad = (K-1)*dilation. // Right-aligned: token t lands in slot t + W - L, so slot W-1 always holds the state after the // last token (and is the slot past_state is read from). const int state_slots = state_window_ > 0 ? state_window_ : 1; TensorShape state_shape; - ORT_RETURN_IF_ERROR(causal_conv_with_state_helper::CheckInputs( - state_window_, batch_size, channels, pad, past_state_tensor, state_shape, "CausalConvWithState")); + if (channels_last_) { + ORT_RETURN_IF_ERROR(causal_conv_with_state_helper::CheckInputsChannelsLast( + state_window_, input_shape, pad, past_state_tensor, state_shape, "CausalConvWithState")); + } else { + ORT_RETURN_IF_ERROR(causal_conv_with_state_helper::CheckInputs( + state_window_, batch_size, channels, pad, past_state_tensor, state_shape, "CausalConvWithState")); + } // Allocate outputs Tensor* output_tensor = context->Output(0, input_shape); @@ -125,6 +145,9 @@ Status CausalConvWithState::ComputeInternal(OpKernelContext* context) const { channels, L, K, + dilation_, + MakeCausalConvLayout(channels_last_, channels, L), + MakeCausalConvLayout(channels_last_, channels, pad), apply_silu, GetDeviceProp().maxThreadsPerBlock, state_slots); diff --git a/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state.h b/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state.h index f0fb66e8485b9..0a1f23f3abfd3 100644 --- a/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state.h +++ b/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state.h @@ -20,6 +20,8 @@ class CausalConvWithState final : public onnxruntime::cuda::CudaKernel { private: int ndim_; + int dilation_; + bool channels_last_; std::string activation_; // Leading (axis-0) extent of past_state / present_state; 0 means no window axis (single state). int state_window_; diff --git a/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state_impl.cu b/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state_impl.cu index 8c34e672a4797..0c0e1da836521 100644 --- a/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state_impl.cu +++ b/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state_impl.cu @@ -5,8 +5,9 @@ // // Design: One thread block per (batch, channel). Two execution paths: // -// 1. Decode (L=1): The convolution window is [past_state(K-1), input(1)]. -// Load K values into registers, compute a single dot product, shift state. +// 1. Decode (L=1): The convolution window is [past_state(pad), input(1)] with +// pad = (K-1)*dilation. Load K values into registers, compute a single dot product, +// shift state. // One thread block does the entire operation — zero shared memory needed. // // 2. Prefill (L>1): Load past_state + input into shared memory as a padded buffer, @@ -47,6 +48,9 @@ __global__ void CausalConvDecodeKernel( int batch_channels, // = batch_size * channels (actual element count) int channels, int kernel_size, + int dilation, + CausalConvLayout act_layout, + CausalConvLayout state_layout, bool apply_silu, int state_window) { // W: axis-0 extent of past_state / present_state (>= 1) const int bc = blockIdx.x * blockDim.x + threadIdx.x; @@ -54,14 +58,17 @@ __global__ void CausalConvDecodeKernel( const int b = bc / channels; const int c = bc % channels; - const int pad = kernel_size - 1; + const int pad = (kernel_size - 1) * dilation; + const int64_t state_pos_stride = state_layout.pos_stride; // Cache input value in register — avoids redundant global reads - const float input_val = to_float(input[(int64_t)b * channels + c]); + const int64_t act_offset = act_layout.Offset(b, 0, c); + const float input_val = to_float(input[act_offset]); // seq_len == 1, so the single position is window slot W-1 for both the read and the write. - // Window-major [W, B, C, K-1]: slot stride is batch_channels*pad and (b, c) flattens to bc. - const int64_t state_offset = (int64_t)(state_window - 1) * batch_channels * pad + (int64_t)bc * pad; + // Window-major: one slot is a whole [B, ...] block of batch_channels*pad elements. + const int64_t state_offset = + (int64_t)(state_window - 1) * batch_channels * pad + state_layout.Offset(b, 0, c); // Cache past_state base pointer for this (b, c) const T* ps_in = (past_state != nullptr) ? past_state + state_offset : nullptr; @@ -70,27 +77,28 @@ __global__ void CausalConvDecodeKernel( // weight layout: [C, 1, K], so channel c starts at c * K float sum = (bias != nullptr) ? to_float(bias[c]) : 0.0f; - // Convolution window: [past_state[0..K-2], input[0]] - for (int k = 0; k < pad; ++k) { + // Convolution window: [past_state(pad), input[0]]. Tap k reads window slot k*dilation. + for (int k = 0; k < kernel_size - 1; ++k) { float wk = to_float(weight[c * kernel_size + k]); - float xk = (ps_in != nullptr) ? to_float(ps_in[k]) : 0.0f; + float xk = (ps_in != nullptr) ? to_float(ps_in[k * dilation * state_pos_stride]) : 0.0f; sum += wk * xk; } - // Last element of window is current input - sum += to_float(weight[c * kernel_size + pad]) * input_val; + // Last tap is the current input + sum += to_float(weight[c * kernel_size + kernel_size - 1]) * input_val; if (apply_silu) { sum = silu_fn(sum); } - output[(int64_t)b * channels + c] = from_float(sum); + output[act_offset] = from_float(sum); // Update present_state: shift left by 1, append input T* ps_out = present_state + state_offset; for (int k = 0; k < pad - 1; ++k) { - ps_out[k] = (ps_in != nullptr) ? ps_in[k + 1] : from_float(0.0f); + ps_out[k * state_pos_stride] = + (ps_in != nullptr) ? ps_in[(k + 1) * state_pos_stride] : from_float(0.0f); } if (pad > 0) { - ps_out[pad - 1] = from_float(input_val); + ps_out[(pad - 1) * state_pos_stride] = from_float(input_val); } } @@ -162,6 +170,9 @@ __global__ void CausalConvPrefillKernel( int seq_len, int channels, int kernel_size, + int dilation, + CausalConvLayout act_layout, + CausalConvLayout state_layout, bool apply_silu, int batch_size, int state_window) { // W: axis-0 extent of past_state / present_state (>= 1) @@ -169,14 +180,15 @@ __global__ void CausalConvPrefillKernel( const int c = blockIdx.y; const int tid = threadIdx.x; - const int pad = kernel_size - 1; + const int pad = (kernel_size - 1) * dilation; const int padded_len = pad + seq_len; + const int64_t state_pos_stride = state_layout.pos_stride; // Slot W-1 holds the state after the last token; that is what past_state is read from. // Window-major, so one slot spans the whole batch. const int64_t slot_stride = (int64_t)batch_size * channels * pad; const int64_t last_slot_offset = - (int64_t)(state_window - 1) * slot_stride + ((int64_t)b * channels + c) * pad; + (int64_t)(state_window - 1) * slot_stride + state_layout.Offset(b, 0, c); // Shared memory: padded input [pad + L] floats + weight [K] floats extern __shared__ float smem[]; @@ -187,14 +199,14 @@ __global__ void CausalConvPrefillKernel( // Past state portion: [0..pad-1] for (int i = tid; i < pad; i += blockDim.x) { if (past_state != nullptr) { - s_padded[i] = to_float(past_state[last_slot_offset + i]); + s_padded[i] = to_float(past_state[last_slot_offset + (int64_t)i * state_pos_stride]); } else { s_padded[i] = 0.0f; } } // Current input portion: [pad..pad+L-1] for (int i = tid; i < seq_len; i += blockDim.x) { - s_padded[pad + i] = to_float(input[((int64_t)b * channels + c) * seq_len + i]); + s_padded[pad + i] = to_float(input[act_layout.Offset(b, i, c)]); } // Load weight into shared memory for (int i = tid; i < kernel_size; i += blockDim.x) { @@ -207,12 +219,12 @@ __global__ void CausalConvPrefillKernel( for (int l = tid; l < seq_len; l += blockDim.x) { float sum = bias_val; for (int k = 0; k < kernel_size; ++k) { - sum += s_weight[k] * s_padded[l + k]; + sum += s_weight[k] * s_padded[l + k * dilation]; } if (apply_silu) { sum = silu_fn(sum); } - output[((int64_t)b * channels + c) * seq_len + l] = from_float(sum); + output[act_layout.Offset(b, l, c)] = from_float(sum); } // Save present_state. The carry state after token t is the pad-length window ending at position @@ -223,9 +235,9 @@ __global__ void CausalConvPrefillKernel( const int first = seq_len > state_window ? seq_len - state_window : 0; for (int t = first + tid; t < seq_len; t += blockDim.x) { T* ps = present_state + (int64_t)(t + state_window - seq_len) * slot_stride + - ((int64_t)b * channels + c) * pad; + state_layout.Offset(b, 0, c); for (int p = 0; p < pad; ++p) { - ps[p] = from_float(s_padded[t + 1 + p]); + ps[(int64_t)p * state_pos_stride] = from_float(s_padded[t + 1 + p]); } } } @@ -251,6 +263,9 @@ __global__ void CausalConvPrefillKernelBatched( int seq_len, int channels, int kernel_size, + int dilation, + CausalConvLayout act_layout, + CausalConvLayout state_layout, bool apply_silu, int batch_size, int state_window) { // W: axis-0 extent of past_state / present_state (>= 1) @@ -258,10 +273,11 @@ __global__ void CausalConvPrefillKernelBatched( const int c_base = blockIdx.y * CPB; const int tid = threadIdx.x; - const int pad = kernel_size - 1; + const int pad = (kernel_size - 1) * dilation; const int padded_len = pad + seq_len; + const int64_t state_pos_stride = state_layout.pos_stride; - // Window-major [W, B, C, K-1]: one slot spans the whole batch. + // Window-major: one slot spans the whole batch. const int64_t slot_stride = (int64_t)batch_size * channels * pad; // Which channel within this block's CPB group does this thread serve? @@ -279,17 +295,17 @@ __global__ void CausalConvPrefillKernelBatched( if (c < channels) { // Load past state from window slot W-1 (the state after the last token of the previous step) const int64_t last_slot_offset = - (int64_t)(state_window - 1) * slot_stride + ((int64_t)b * channels + c) * pad; + (int64_t)(state_window - 1) * slot_stride + state_layout.Offset(b, 0, c); for (int i = local_tid; i < pad; i += threads_per_channel) { if (past_state != nullptr) { - s_padded[i] = to_float(past_state[last_slot_offset + i]); + s_padded[i] = to_float(past_state[last_slot_offset + (int64_t)i * state_pos_stride]); } else { s_padded[i] = 0.0f; } } // Load input for (int i = local_tid; i < seq_len; i += threads_per_channel) { - s_padded[pad + i] = to_float(input[((int64_t)b * channels + c) * seq_len + i]); + s_padded[pad + i] = to_float(input[act_layout.Offset(b, i, c)]); } // Load weight for (int i = local_tid; i < kernel_size; i += threads_per_channel) { @@ -303,12 +319,12 @@ __global__ void CausalConvPrefillKernelBatched( for (int l = local_tid; l < seq_len; l += threads_per_channel) { float sum = bias_val; for (int k = 0; k < kernel_size; ++k) { - sum += s_weight[k] * s_padded[l + k]; + sum += s_weight[k] * s_padded[l + k * dilation]; } if (apply_silu) { sum = silu_fn(sum); } - output[((int64_t)b * channels + c) * seq_len + l] = from_float(sum); + output[act_layout.Offset(b, l, c)] = from_float(sum); } } @@ -323,14 +339,93 @@ __global__ void CausalConvPrefillKernelBatched( const int first = seq_len > state_window ? seq_len - state_window : 0; for (int t = first + local_tid; t < seq_len; t += threads_per_channel) { T* ps = present_state + (int64_t)(t + state_window - seq_len) * slot_stride + - ((int64_t)b * channels + c) * pad; + state_layout.Offset(b, 0, c); for (int p = 0; p < pad; ++p) { - ps[p] = from_float(s_padded[t + 1 + p]); + ps[(int64_t)p * state_pos_stride] = from_float(s_padded[t + 1 + p]); } } } } +// ============================================================================= +// Channels-last prefill kernel: L>1, one thread per (batch, position, channel) +// with the channel as the fastest-moving thread axis. +// +// Grid: (ceil(channels / threads), seq_len, batch_size) +// Block: (threads, 1, 1) +// Shared memory: none +// +// The shared-memory kernels above stage one channel per block and walk positions, which is +// coalesced only when positions are contiguous. Under channels_last the contiguous axis is the +// channel, so that access pattern turns every load and store into a strided gather. Here adjacent +// threads hold adjacent channels, so each convolution tap, each state read and every store is a +// single contiguous transaction. Staging is unnecessary because the overlapping taps of +// neighbouring positions are served by L1/L2 rather than shared memory. +// ============================================================================= +template +__global__ void CausalConvPrefillKernelChannelsLast( + const T* __restrict__ input, // [B, L, C] + const T* __restrict__ weight, // [C, 1, K] + const T* __restrict__ bias, // [C] or nullptr + const T* __restrict__ past_state, // [W, B, K-1, C] or nullptr + T* __restrict__ output, // [B, L, C] + T* __restrict__ present_state, // [W, B, K-1, C] + int seq_len, + int channels, + int kernel_size, + int dilation, + CausalConvLayout act_layout, + CausalConvLayout state_layout, + bool apply_silu, + int batch_size, + int state_window) { // W: axis-0 extent of past_state / present_state (>= 1) + const int c = blockIdx.x * blockDim.x + threadIdx.x; + if (c >= channels) { + return; + } + const int l = blockIdx.y; + const int b = blockIdx.z; + + const int pad = (kernel_size - 1) * dilation; + const int64_t state_pos_stride = state_layout.pos_stride; + // Window-major: one slot spans the whole batch. Slot W-1 holds the state after the last token of + // the previous step, which is the only slot past_state is read from. + const int64_t slot_stride = (int64_t)batch_size * channels * pad; + const int64_t last_slot_offset = + (int64_t)(state_window - 1) * slot_stride + state_layout.Offset(b, 0, c); + + // Reads the virtual stream [past_state (pad samples), input (seq_len samples)] at index `vp`. + auto sample = [&](int vp) -> float { + if (vp >= pad) { + return to_float(input[act_layout.Offset(b, vp - pad, c)]); + } + return past_state != nullptr + ? to_float(past_state[last_slot_offset + (int64_t)vp * state_pos_stride]) + : 0.0f; + }; + + float sum = (bias != nullptr) ? to_float(bias[c]) : 0.0f; + for (int k = 0; k < kernel_size; ++k) { + sum += to_float(weight[(int64_t)c * kernel_size + k]) * sample(l + k * dilation); + } + if (apply_silu) { + sum = silu_fn(sum); + } + output[act_layout.Offset(b, l, c)] = from_float(sum); + + // The carry state after token l is the pad-length window ending at that token, i.e. virtual + // stream positions [l + 1, l + pad]. It goes into the right-aligned slot l + W - seq_len; + // earlier tokens fall outside the window. The last token always maps to slot W-1. + const int first = seq_len > state_window ? seq_len - state_window : 0; + if (l >= first) { + T* ps = present_state + (int64_t)(l + state_window - seq_len) * slot_stride + + state_layout.Offset(b, 0, c); + for (int p = 0; p < pad; ++p) { + ps[(int64_t)p * state_pos_stride] = from_float(sample(l + 1 + p)); + } + } +} + } // anonymous namespace template @@ -346,6 +441,9 @@ Status LaunchCausalConvWithStateKernel( int channels, int seq_len, int kernel_size, + int dilation, + CausalConvLayout act_layout, + CausalConvLayout state_layout, bool apply_silu, int max_threads_per_block, int state_window) { @@ -354,7 +452,10 @@ Status LaunchCausalConvWithStateKernel( int total = batch_size * channels; int threads = 256; int blocks = (total + threads - 1) / threads; - switch (kernel_size) { + // The fixed-K decode kernels hard-code pad == K - 1 and a contiguous state row, so they only + // apply to the undilated channels-first case (pos_stride is 1 there, and `channels` in the + // channels-last one). + switch ((dilation == 1 && state_layout.pos_stride == 1) ? kernel_size : 0) { case 2: CausalConvDecodeKernelFixedK<<>>( input, weight, bias, past_state, output, present_state, @@ -378,12 +479,29 @@ Status LaunchCausalConvWithStateKernel( default: CausalConvDecodeKernel<<>>( input, weight, bias, past_state, output, present_state, - total, channels, kernel_size, apply_silu, state_window); + total, channels, kernel_size, dilation, act_layout, state_layout, apply_silu, + state_window); break; } } else { // Prefill path: choose between batched (short seq) or single-channel (long seq) kernel - int pad = kernel_size - 1; + int pad = (kernel_size - 1) * dilation; + + // Under channels_last the contiguous axis is the channel, so use the kernel whose fastest + // thread axis is the channel; the shared-memory kernels below would gather every access. + // gridDim.y is capped at 65535; longer sequences fall through to the shared-memory kernels, + // which handle either layout correctly (just less efficiently). + constexpr int kMaxGridDimY = 65535; + if (act_layout.chan_stride == 1 && seq_len <= kMaxGridDimY) { + int threads = channels >= 256 ? 256 : ((channels + 31) / 32) * 32; + threads = std::min(threads, max_threads_per_block); + const dim3 grid((channels + threads - 1) / threads, seq_len, batch_size); + CausalConvPrefillKernelChannelsLast<<>>( + input, weight, bias, past_state, output, present_state, + seq_len, channels, kernel_size, dilation, act_layout, state_layout, apply_silu, + batch_size, state_window); + return CUDA_CALL(cudaGetLastError()); + } // For short sequences, batch multiple channels per block to improve occupancy. // CPB=4: each block handles 4 channels, reducing block count by 4x. @@ -416,7 +534,8 @@ Status LaunchCausalConvWithStateKernel( CausalConvPrefillKernelBatched<<>>( input, weight, bias, past_state, output, present_state, - seq_len, channels, kernel_size, apply_silu, batch_size, state_window); + seq_len, channels, kernel_size, dilation, act_layout, state_layout, apply_silu, + batch_size, state_window); } else { // Original single-channel-per-block path for long sequences const dim3 grid(batch_size, channels, 1); @@ -440,7 +559,8 @@ Status LaunchCausalConvWithStateKernel( CausalConvPrefillKernel<<>>( input, weight, bias, past_state, output, present_state, - seq_len, channels, kernel_size, apply_silu, batch_size, state_window); + seq_len, channels, kernel_size, dilation, act_layout, state_layout, apply_silu, + batch_size, state_window); } } @@ -450,16 +570,17 @@ Status LaunchCausalConvWithStateKernel( // Explicit instantiations template Status LaunchCausalConvWithStateKernel( cudaStream_t, const float*, const float*, const float*, const float*, - float*, float*, int, int, int, int, bool, int, int); + float*, float*, int, int, int, int, int, CausalConvLayout, CausalConvLayout, bool, int, int); template Status LaunchCausalConvWithStateKernel( cudaStream_t, const half*, const half*, const half*, const half*, - half*, half*, int, int, int, int, bool, int, int); + half*, half*, int, int, int, int, int, CausalConvLayout, CausalConvLayout, bool, int, int); #if __CUDA_ARCH__ >= 800 || !defined(__CUDA_ARCH__) template Status LaunchCausalConvWithStateKernel<__nv_bfloat16>( cudaStream_t, const __nv_bfloat16*, const __nv_bfloat16*, const __nv_bfloat16*, const __nv_bfloat16*, - __nv_bfloat16*, __nv_bfloat16*, int, int, int, int, bool, int, int); + __nv_bfloat16*, __nv_bfloat16*, int, int, int, int, int, CausalConvLayout, CausalConvLayout, + bool, int, int); #endif } // namespace cuda diff --git a/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state_impl.h b/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state_impl.h index b07730e6c0d98..968cb9507339d 100644 --- a/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state_impl.h +++ b/onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state_impl.h @@ -10,6 +10,27 @@ namespace onnxruntime { namespace contrib { namespace cuda { +// Element strides of a (batch, position, channel) view over an activation or state tensor. +// Both supported layouts are dense, so a single strided view covers them and one kernel body +// serves the channels-first (batch, channels, length) and channels-last (batch, length, channels) +// layouts. For the state tensors these are the strides *within* one state_window slot. +struct CausalConvLayout { + int64_t batch_stride; + int64_t pos_stride; + int64_t chan_stride; + + __host__ __device__ int64_t Offset(int b, int pos, int c) const { + return static_cast(b) * batch_stride + static_cast(pos) * pos_stride + + static_cast(c) * chan_stride; + } +}; + +// `length` is the extent of the position axis: seq_len for activations, state_length for state. +inline CausalConvLayout MakeCausalConvLayout(bool channels_last, int64_t channels, int64_t length) { + return channels_last ? CausalConvLayout{channels * length, channels, 1} + : CausalConvLayout{channels * length, 1, length}; +} + // Fused causal depthwise conv1d + activation + state management. // One thread block per (batch, channel). For decode (L=1), this is a simple // dot product from shared memory. For prefill (L>1), each thread handles @@ -20,17 +41,20 @@ Status LaunchCausalConvWithStateKernel( const T* input, // [B, C, L] const T* weight, // [C, 1, K] const T* bias, // [C] or nullptr - const T* past_state, // [W, B, C, K-1] or nullptr + const T* past_state, // [W, B, C, (K-1)*dilation] or nullptr T* output, // [B, C, L] - T* present_state, // [W, B, C, K-1] + T* present_state, // [W, B, C, (K-1)*dilation] int batch_size, int channels, int seq_len, int kernel_size, + int dilation, // spacing between kernel taps along the causal axis (>= 1) + CausalConvLayout act_layout, // strides of input / output + CausalConvLayout state_layout, // strides within one past_state / present_state slot bool apply_silu, int max_threads_per_block, // Axis-0 extent W of past_state / present_state (>= 1). The window axis leads the batch axis - // so that a slot is one contiguous [B, C, K-1] block. Right-aligned: token t writes slot + // so that a slot is one contiguous [B, C, (K-1)*dilation] block. Right-aligned: token t writes slot // t + W - seq_len and negative slots are skipped, so slot W-1 always holds the state after the // last token and is the slot past_state is read from. Pass 1 for a plain single-state tensor // with no window axis. diff --git a/onnxruntime/contrib_ops/cuda/bert/engram_gate.cc b/onnxruntime/contrib_ops/cuda/bert/engram_gate.cc new file mode 100644 index 0000000000000..a108ab3b9ae46 --- /dev/null +++ b/onnxruntime/contrib_ops/cuda/bert/engram_gate.cc @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "contrib_ops/cuda/bert/engram_gate.h" +#include "contrib_ops/cuda/bert/engram_gate_impl.h" +#include "core/providers/cuda/cuda_common.h" +#include "core/providers/cuda/cuda_type_conversion.h" + +namespace onnxruntime { +namespace contrib { +namespace cuda { + +using namespace onnxruntime::cuda; + +#define REGISTER_KERNEL_TYPED(T) \ + ONNX_OPERATOR_TYPED_KERNEL_EX( \ + EngramGate, \ + kMSDomain, \ + 1, \ + T, \ + kCudaExecutionProvider, \ + (*KernelDefBuilder::Create()) \ + .TypeConstraint("T", DataTypeImpl::GetTensorType()), \ + EngramGate); + +REGISTER_KERNEL_TYPED(float) +REGISTER_KERNEL_TYPED(MLFloat16) +REGISTER_KERNEL_TYPED(BFloat16) + +#undef REGISTER_KERNEL_TYPED + +template +EngramGate::EngramGate(const OpKernelInfo& info) : CudaKernel(info) { + epsilon_ = info.GetAttrOrDefault("epsilon", 1.0e-5f); +} + +template +Status EngramGate::ComputeInternal(OpKernelContext* context) const { + using CudaT = typename OrtToCudaType::type; + const Tensor* key = context->Input(0); + const Tensor* query = context->Input(1); + const Tensor* value = context->Input(2); + const Tensor* key_norm_scale = context->Input(3); + const Tensor* query_norm_scale = context->Input(4); + + const TensorShape& key_shape = key->Shape(); + ORT_RETURN_IF_NOT(key_shape.NumDimensions() == 4, + "key must have shape (batch_size, sequence_length, hc_mult, hidden_size)"); + const int64_t batch_size = key_shape[0]; + const int64_t sequence_length = key_shape[1]; + const int64_t hc_mult = key_shape[2]; + const int64_t hidden_size = key_shape[3]; + + ORT_RETURN_IF_NOT(query->Shape() == key_shape, "query must have the same shape as key"); + ORT_RETURN_IF_NOT(value->Shape() == TensorShape({batch_size, sequence_length, hidden_size}), + "value must have shape (batch_size, sequence_length, hidden_size)"); + ORT_RETURN_IF_NOT(key_norm_scale->Shape() == TensorShape({hc_mult, hidden_size}), + "key_norm_scale must have shape (hc_mult, hidden_size)"); + ORT_RETURN_IF_NOT(query_norm_scale->Shape() == TensorShape({hc_mult, hidden_size}), + "query_norm_scale must have shape (hc_mult, hidden_size)"); + + Tensor* output = context->Output(0, key_shape); + if (key_shape.Size() == 0) { + return Status::OK(); + } + + return LaunchEngramGateKernel( + Stream(context), + reinterpret_cast(key->Data()), + reinterpret_cast(query->Data()), + reinterpret_cast(value->Data()), + reinterpret_cast(key_norm_scale->Data()), + reinterpret_cast(query_norm_scale->Data()), + reinterpret_cast(output->MutableData()), + batch_size, + sequence_length, + hc_mult, + hidden_size, + epsilon_); +} + +template class EngramGate; +template class EngramGate; +template class EngramGate; + +} // namespace cuda +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cuda/bert/engram_gate.h b/onnxruntime/contrib_ops/cuda/bert/engram_gate.h new file mode 100644 index 0000000000000..09aa7018008f2 --- /dev/null +++ b/onnxruntime/contrib_ops/cuda/bert/engram_gate.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "core/common/common.h" +#include "core/providers/cuda/cuda_kernel.h" + +namespace onnxruntime { +namespace contrib { +namespace cuda { + +template +class EngramGate final : public onnxruntime::cuda::CudaKernel { + public: + explicit EngramGate(const OpKernelInfo& info); + Status ComputeInternal(OpKernelContext* context) const override; + + private: + float epsilon_; +}; + +} // namespace cuda +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cuda/bert/engram_gate_impl.cu b/onnxruntime/contrib_ops/cuda/bert/engram_gate_impl.cu new file mode 100644 index 0000000000000..e0dd2a7e31a56 --- /dev/null +++ b/onnxruntime/contrib_ops/cuda/bert/engram_gate_impl.cu @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "contrib_ops/cuda/bert/engram_gate_impl.h" + +#include +#include +#include + +#include + +#include "contrib_ops/cuda/bert/engram_helper.cuh" +#include "core/providers/cuda/cu_inc/cuda_type_helper.cuh" + +namespace onnxruntime { +namespace contrib { +namespace cuda { + +namespace { + +// One block per (token, g) row. The gate is a scalar for the whole row, so it is reduced once by the +// block and then broadcast over the value channels. +template +__global__ void EngramGateKernel( + const T* key, + const T* query, + const T* value, + const T* key_norm_scale, + const T* query_norm_scale, + T* output, + int64_t rows, + int64_t hc_mult, + int64_t hidden_size, + float epsilon) { + extern __shared__ float shared[]; + + for (int64_t row = blockIdx.x; row < rows; row += gridDim.x) { + const int64_t g = row % hc_mult; + const int64_t token = row / hc_mult; + const T* key_row = key + row * hidden_size; + const T* query_row = query + row * hidden_size; + const T* value_row = value + token * hidden_size; + const T* key_scale_g = key_norm_scale + g * hidden_size; + const T* query_scale_g = query_norm_scale + g * hidden_size; + + float key_sum_sq = 0.0f; + float query_sum_sq = 0.0f; + float dot_numerator = 0.0f; + + for (int64_t d = threadIdx.x; d < hidden_size; d += blockDim.x) { + const float key_value = to_float(key_row[d]); + const float query_value = to_float(query_row[d]); + key_sum_sq += key_value * key_value; + query_sum_sq += query_value * query_value; + dot_numerator += key_value * to_float(key_scale_g[d]) * query_value * to_float(query_scale_g[d]); + } + + // The three partials are independent and available at the same point, so fuse them into one tree + // reduction instead of paying three sets of barriers per row. + engram_helper::BlockSum3(&key_sum_sq, &query_sum_sq, &dot_numerator, shared); + + const float key_inv_rms = rsqrtf(key_sum_sq / static_cast(hidden_size) + epsilon); + const float query_inv_rms = rsqrtf(query_sum_sq / static_cast(hidden_size) + epsilon); + const float dot = dot_numerator * key_inv_rms * query_inv_rms / sqrtf(static_cast(hidden_size)); + const float gate = engram_helper::SigmoidFloat(engram_helper::EngramGateArg(dot)); + + T* output_row = output + row * hidden_size; + for (int64_t c = threadIdx.x; c < hidden_size; c += blockDim.x) { + output_row[c] = from_float(gate * to_float(value_row[c])); + } + } +} + +} // namespace + +template +Status LaunchEngramGateKernel( + cudaStream_t stream, + const T* key, + const T* query, + const T* value, + const T* key_norm_scale, + const T* query_norm_scale, + T* output, + int64_t batch_size, + int64_t sequence_length, + int64_t hc_mult, + int64_t hidden_size, + float epsilon) { + const int64_t rows = batch_size * sequence_length * hc_mult; + if (rows == 0 || hidden_size == 0) { + return Status::OK(); + } + const int blocks = static_cast(std::min(rows, engram_helper::kMaxGridDimX)); + const size_t shared_bytes = 3 * static_cast(engram_helper::kThreads) * sizeof(float); + EngramGateKernel<<>>( + key, query, value, key_norm_scale, query_norm_scale, output, rows, hc_mult, hidden_size, epsilon); + return CUDA_CALL(cudaGetLastError()); +} + +#define INSTANTIATE_ENGRAM_GATE(T) \ + template Status LaunchEngramGateKernel(cudaStream_t, const T*, const T*, const T*, const T*, \ + const T*, T*, int64_t, int64_t, int64_t, int64_t, float); + +INSTANTIATE_ENGRAM_GATE(float) +INSTANTIATE_ENGRAM_GATE(half) +INSTANTIATE_ENGRAM_GATE(__nv_bfloat16) + +#undef INSTANTIATE_ENGRAM_GATE + +} // namespace cuda +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cuda/bert/engram_gate_impl.h b/onnxruntime/contrib_ops/cuda/bert/engram_gate_impl.h new file mode 100644 index 0000000000000..83e2ec8fe77ac --- /dev/null +++ b/onnxruntime/contrib_ops/cuda/bert/engram_gate_impl.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include "core/providers/cuda/cuda_common.h" + +namespace onnxruntime { +namespace contrib { +namespace cuda { + +template +Status LaunchEngramGateKernel( + cudaStream_t stream, + const T* key, + const T* query, + const T* value, + const T* key_norm_scale, + const T* query_norm_scale, + T* output, + int64_t batch_size, + int64_t sequence_length, + int64_t hc_mult, + int64_t hidden_size, + float epsilon); + +} // namespace cuda +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cuda/bert/engram_helper.cuh b/onnxruntime/contrib_ops/cuda/bert/engram_helper.cuh new file mode 100644 index 0000000000000..872e6955aaf4f --- /dev/null +++ b/onnxruntime/contrib_ops/cuda/bert/engram_helper.cuh @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include + +namespace onnxruntime { +namespace contrib { +namespace cuda { +namespace engram_helper { + +constexpr int kThreads = 256; +// grid.x is limited to 2^31 - 1 since compute capability 3.0 (the 65535 limit applies to grid.y and +// grid.z only). All kernels launched through GridSize() use a grid-stride loop, so the clamp only +// bounds the launch; correctness does not depend on it. +constexpr int64_t kMaxGridDimX = 2147483647; + +// Number of blocks for a grid-stride loop over `count` elements, clamped to the maximum grid size. +inline int GridSize(int64_t count) { + const int64_t blocks = (count + kThreads - 1) / kThreads; + return static_cast(std::min(blocks, kMaxGridDimX)); +} + +// Sums three independent per-thread partials across the block in a single tree reduction, so a row +// that needs three reductions pays one set of barriers instead of three. `shared` must point to at +// least 3 * blockDim.x floats, and blockDim.x must be a power of two. All threads must call this. +__device__ __forceinline__ void BlockSum3(float* a, float* b, float* c, float* shared) { + float* shared_a = shared; + float* shared_b = shared + blockDim.x; + float* shared_c = shared + 2 * blockDim.x; + shared_a[threadIdx.x] = *a; + shared_b[threadIdx.x] = *b; + shared_c[threadIdx.x] = *c; + __syncthreads(); + for (unsigned int stride = blockDim.x / 2; stride > 0; stride >>= 1) { + if (threadIdx.x < stride) { + shared_a[threadIdx.x] += shared_a[threadIdx.x + stride]; + shared_b[threadIdx.x] += shared_b[threadIdx.x + stride]; + shared_c[threadIdx.x] += shared_c[threadIdx.x + stride]; + } + __syncthreads(); + } + *a = shared_a[0]; + *b = shared_b[0]; + *c = shared_c[0]; + __syncthreads(); +} + +// Numerically stable logistic function. +__device__ __forceinline__ float SigmoidFloat(float x) { + return x > 0.0f ? 1.0f / (1.0f + expf(-x)) : expf(x) / (1.0f + expf(x)); +} + +// Engram gate pre-activation: sign(dot) * sqrt(max(abs(dot), 1e-6)). +// copysignf cannot be used here because it maps a zero dot product to +sqrt(1e-6) instead of zero, +// which would disagree with the schema formula and with the other execution providers. +__device__ __forceinline__ float EngramGateArg(float dot) { + if (dot == 0.0f) { + return 0.0f; + } + const float magnitude = sqrtf(fmaxf(fabsf(dot), 1.0e-6f)); + return dot < 0.0f ? -magnitude : magnitude; +} + +// Euclidean modulo: the result always has the sign of `mod`, which must be positive. +template +__device__ __forceinline__ T PositiveMod(T value, T mod) { + const T result = value % mod; + return result < 0 ? static_cast(result + mod) : result; +} + +// Multiplies through the unsigned counterpart of T so that overflow wraps around instead of +// being undefined behavior. +template +__device__ __forceinline__ T WrappedMultiply(T a, T b); + +template <> +__device__ __forceinline__ int32_t WrappedMultiply(int32_t a, int32_t b) { + return static_cast(static_cast(a) * static_cast(b)); +} + +template <> +__device__ __forceinline__ int64_t WrappedMultiply(int64_t a, int64_t b) { + return static_cast(static_cast(a) * static_cast(b)); +} + +} // namespace engram_helper +} // namespace cuda +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping.cc b/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping.cc new file mode 100644 index 0000000000000..36ef5f6c40ba1 --- /dev/null +++ b/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping.cc @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "contrib_ops/cuda/bert/ngram_hash_mapping.h" +#include "contrib_ops/cuda/bert/ngram_hash_mapping_impl.h" +#include "core/providers/cuda/cuda_common.h" + +#include + +namespace onnxruntime { +namespace contrib { +namespace cuda { + +using namespace onnxruntime::cuda; + +#define REGISTER_KERNEL_TYPED(T) \ + ONNX_OPERATOR_TYPED_KERNEL_EX( \ + NGramHashMapping, \ + kMSDomain, \ + 1, \ + T, \ + kCudaExecutionProvider, \ + (*KernelDefBuilder::Create()) \ + .MayInplace(3, 1) \ + .TypeConstraint("M", DataTypeImpl::GetTensorType()), \ + NGramHashMapping); + +REGISTER_KERNEL_TYPED(int32_t) +REGISTER_KERNEL_TYPED(int64_t) + +#undef REGISTER_KERNEL_TYPED + +template +NGramHashMapping::NGramHashMapping(const OpKernelInfo& info) : CudaKernel(info) { + ORT_ENFORCE(info.GetAttr("max_ngram_size", &max_ngram_size_).IsOK(), + "max_ngram_size attribute is required"); + ORT_ENFORCE(info.GetAttr("n_head_per_ngram", &n_head_per_ngram_).IsOK(), + "n_head_per_ngram attribute is required"); + int64_t pad_id = 0; + ORT_ENFORCE(info.GetAttr("pad_id", &pad_id).IsOK(), "pad_id attribute is required"); + ORT_ENFORCE(max_ngram_size_ >= 2, "max_ngram_size must be at least 2"); + ORT_ENFORCE(n_head_per_ngram_ >= 1, "n_head_per_ngram must be positive"); + ORT_ENFORCE(pad_id >= static_cast(std::numeric_limits::min()) && + pad_id <= static_cast(std::numeric_limits::max()), + "pad_id is out of range for the input id type"); + pad_id_ = static_cast(pad_id); +} + +template +Status NGramHashMapping::ComputeInternal(OpKernelContext* context) const { + const Tensor* input_ids = context->Input(0); + const Tensor* multipliers = context->Input(1); + const Tensor* vocab_sizes = context->Input(2); + const Tensor* past_ids = context->Input(3); + const TensorShape& input_shape = input_ids->Shape(); + ORT_RETURN_IF_NOT(input_shape.NumDimensions() == 2, "input_ids must have rank 2"); + ORT_RETURN_IF_NOT(multipliers->Shape().NumDimensions() == 1 && + multipliers->Shape()[0] == max_ngram_size_, + "multipliers must have shape (max_ngram_size)"); + const int64_t num_heads = (max_ngram_size_ - 1) * n_head_per_ngram_; + ORT_RETURN_IF_NOT(vocab_sizes->Shape().NumDimensions() == 1 && vocab_sizes->Shape()[0] == num_heads, + "vocab_sizes must have shape ((max_ngram_size - 1) * n_head_per_ngram)"); + + const int64_t batch_size = input_shape[0]; + const int64_t sequence_length = input_shape[1]; + // An n-gram window reaches this many positions before the current token. + const int64_t state_length = max_ngram_size_ - 1; + if (past_ids != nullptr) { + ORT_RETURN_IF_NOT(past_ids->Shape() == TensorShape({batch_size, state_length}), + "past_ids must have shape (batch_size, max_ngram_size - 1)"); + } + + Tensor* output = context->Output(0, TensorShape({batch_size, sequence_length, num_heads})); + Tensor* present_ids = context->Output(1, TensorShape({batch_size, state_length})); + return LaunchNGramHashMappingKernel( + Stream(context), + input_ids->Data(), + multipliers->Data(), + vocab_sizes->Data(), + past_ids == nullptr ? nullptr : past_ids->Data(), + output->MutableData(), + present_ids == nullptr ? nullptr : present_ids->MutableData(), + batch_size, + sequence_length, + max_ngram_size_, + n_head_per_ngram_, + pad_id_); +} + +template class NGramHashMapping; +template class NGramHashMapping; + +} // namespace cuda +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping.h b/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping.h new file mode 100644 index 0000000000000..dbc5d344d10b4 --- /dev/null +++ b/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "core/common/common.h" +#include "core/providers/cuda/cuda_kernel.h" + +namespace onnxruntime { +namespace contrib { +namespace cuda { + +template +class NGramHashMapping final : public onnxruntime::cuda::CudaKernel { + public: + explicit NGramHashMapping(const OpKernelInfo& info); + Status ComputeInternal(OpKernelContext* context) const override; + + private: + int64_t max_ngram_size_; + int64_t n_head_per_ngram_; + T pad_id_; +}; + +} // namespace cuda +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping_impl.cu b/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping_impl.cu new file mode 100644 index 0000000000000..4ff27c27ba97e --- /dev/null +++ b/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping_impl.cu @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "contrib_ops/cuda/bert/ngram_hash_mapping_impl.h" + +#include + +#include +#include +#include + +#include "contrib_ops/cuda/bert/engram_helper.cuh" +#include "core/providers/cuda/cu_inc/cuda_type_helper.cuh" + +namespace onnxruntime { +namespace contrib { +namespace cuda { + +namespace { + +// Reads the id at right-aligned history slot `slot`. Slots outside the provided history (or a missing +// past_ids) are positions before the start of the whole sequence, so they use pad_id. +template +__device__ __forceinline__ T HistoryId(const T* past_ids, int64_t b, int64_t slot, int64_t state_length, + T pad_id) { + if (past_ids == nullptr || slot < 0 || slot >= state_length) { + return pad_id; + } + return past_ids[b * state_length + slot]; +} + +template +__global__ void NGramHashMappingKernel( + const T* __restrict__ input_ids, + const T* __restrict__ multipliers, + const T* __restrict__ vocab_sizes, + const T* __restrict__ past_ids, + T* output, + int64_t total, + int64_t sequence_length, + int64_t max_ngram_size, + int64_t n_head_per_ngram, + T pad_id, + bool stage_tables) { + const int64_t num_heads = (max_ngram_size - 1) * n_head_per_ngram; + const int64_t state_length = max_ngram_size - 1; + + // multipliers and vocab_sizes are uniform across the whole grid and tiny, but they are read in the + // two innermost loops. Stage them into shared memory once per block so those reads never leave the + // SM. The launch clears stage_tables if the tables would not fit, in which case the __restrict__ + // pointers let the compiler serve them from the read-only cache instead. + extern __shared__ char ngram_shared_bytes[]; + T* shared_multipliers = reinterpret_cast(ngram_shared_bytes); + T* shared_vocab_sizes = shared_multipliers + max_ngram_size; + if (stage_tables) { + for (int64_t i = threadIdx.x; i < max_ngram_size; i += blockDim.x) { + shared_multipliers[i] = multipliers[i]; + } + for (int64_t i = threadIdx.x; i < num_heads; i += blockDim.x) { + shared_vocab_sizes[i] = vocab_sizes[i]; + } + __syncthreads(); + } + const T* multiplier_table = stage_tables ? shared_multipliers : multipliers; + const T* vocab_table = stage_tables ? shared_vocab_sizes : vocab_sizes; + + for (int64_t linear = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + linear < total; + linear += static_cast(gridDim.x) * blockDim.x) { + const int64_t t = linear % sequence_length; + const int64_t b = linear / sequence_length; + const int64_t input_base = b * sequence_length; + const int64_t output_base = linear * num_heads; + + for (int64_t n = 2; n <= max_ngram_size; ++n) { + T mix = 0; + for (int64_t k = 0; k < n; ++k) { + const int64_t source_t = t - k; + const T token = source_t >= 0 + ? input_ids[input_base + source_t] + : HistoryId(past_ids, b, state_length + source_t, state_length, pad_id); + const T product = engram_helper::WrappedMultiply(token, multiplier_table[k]); + mix = k == 0 ? product : static_cast(mix ^ product); + } + + const int64_t ngram_offset = (n - 2) * n_head_per_ngram; + for (int64_t h = 0; h < n_head_per_ngram; ++h) { + const int64_t out_h = ngram_offset + h; + const T mod = vocab_table[out_h]; + output[output_base + out_h] = mod <= 0 ? T{} : engram_helper::PositiveMod(mix, mod); + } + } + } +} + +// present_ids is the right-aligned trailing window of (past_ids ++ input_ids), so it is well defined +// even when this call is shorter than the window. +// +// past_ids and present_ids may be the same allocation, which is what a decode loop that feeds +// present_ids straight back as past_ids naturally produces. Slot `slot` writes index `slot` and may +// read index `slot + sequence_length`, so the write range overlaps the read range and the two must be +// separated. One block owns one batch row and processes it in ascending blockDim.x-sized chunks: a +// barrier separates the whole chunk's reads from the whole chunk's writes, and a chunk only ever +// writes indices strictly below the read indices of every later chunk. +template +__global__ void NGramPresentIdsKernel( + const T* input_ids, + const T* past_ids, + T* present_ids, + int64_t sequence_length, + int64_t state_length, + T pad_id) { + const int64_t b = blockIdx.x; + const int64_t row_base = b * state_length; + for (int64_t chunk = 0; chunk < state_length; chunk += blockDim.x) { + const int64_t slot = chunk + threadIdx.x; + T token = pad_id; + if (slot < state_length) { + const int64_t source_t = sequence_length - state_length + slot; + token = source_t >= 0 + ? input_ids[b * sequence_length + source_t] + : HistoryId(past_ids, b, state_length + source_t, state_length, pad_id); + } + __syncthreads(); + if (slot < state_length) { + present_ids[row_base + slot] = token; + } + __syncthreads(); + } +} + +} // namespace + +template +Status LaunchNGramHashMappingKernel( + cudaStream_t stream, + const T* input_ids, + const T* multipliers, + const T* vocab_sizes, + const T* past_ids, + T* output, + T* present_ids, + int64_t batch_size, + int64_t sequence_length, + int64_t max_ngram_size, + int64_t n_head_per_ngram, + T pad_id) { + const int64_t state_length = max_ngram_size - 1; + + // The hash kernel reads past_ids and the present kernel writes present_ids, so when the caller + // aliases the two the hash kernel must run first. Both launches are on the same stream, which + // orders them. + const int64_t total = batch_size * sequence_length; + if (total > 0) { + // Shared-memory staging for the two lookup tables. 16 KB keeps occupancy unaffected on every + // architecture ORT targets; realistic Engram configurations need only a few hundred bytes. + constexpr size_t kMaxStagedTableBytes = 16 * 1024; + const int64_t num_heads = (max_ngram_size - 1) * n_head_per_ngram; + const size_t table_bytes = static_cast(max_ngram_size + num_heads) * sizeof(T); + const bool stage_tables = table_bytes <= kMaxStagedTableBytes; + const size_t shared_bytes = stage_tables ? table_bytes : 0; + NGramHashMappingKernel<<>>( + input_ids, multipliers, vocab_sizes, past_ids, output, total, sequence_length, max_ngram_size, + n_head_per_ngram, pad_id, stage_tables); + CUDA_RETURN_IF_ERROR(cudaGetLastError()); + } + + if (present_ids != nullptr && batch_size * state_length > 0) { + // One block per batch row; the kernel walks the row in chunks so state_length may exceed the + // block size. + const int threads = static_cast(std::min(state_length, engram_helper::kThreads)); + NGramPresentIdsKernel<<(batch_size), threads, 0, stream>>>( + input_ids, past_ids, present_ids, sequence_length, state_length, pad_id); + CUDA_RETURN_IF_ERROR(cudaGetLastError()); + } + return Status::OK(); +} + +#define INSTANTIATE_NGRAM_HASH_MAPPING(T) \ + template Status LaunchNGramHashMappingKernel(cudaStream_t, const T*, const T*, const T*, \ + const T*, T*, T*, int64_t, int64_t, int64_t, \ + int64_t, T); + +INSTANTIATE_NGRAM_HASH_MAPPING(int32_t) +INSTANTIATE_NGRAM_HASH_MAPPING(int64_t) + +#undef INSTANTIATE_NGRAM_HASH_MAPPING + +} // namespace cuda +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping_impl.h b/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping_impl.h new file mode 100644 index 0000000000000..8e7bb62a0735d --- /dev/null +++ b/onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping_impl.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include +#include "core/providers/cuda/cuda_common.h" + +namespace onnxruntime { +namespace contrib { +namespace cuda { + +template +Status LaunchNGramHashMappingKernel( + cudaStream_t stream, + const T* input_ids, + const T* multipliers, + const T* vocab_sizes, + const T* past_ids, + T* output, + T* present_ids, + int64_t batch_size, + int64_t sequence_length, + int64_t max_ngram_size, + int64_t n_head_per_ngram, + T pad_id); + +} // namespace cuda +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state.cc b/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state.cc index e4e331d494fce..d1a912fd90faf 100644 --- a/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state.cc +++ b/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state.cc @@ -41,6 +41,8 @@ VarlenCausalConvWithState::VarlenCausalConvWithState(const OpKernelInfo& info ORT_ENFORCE(state_update_capacity >= 0 && state_update_capacity <= kMaxStateWindow, "state_update_capacity must be in [0, ", kMaxStateWindow, "]"); state_update_capacity_ = static_cast(state_update_capacity); + + ORT_THROW_IF_ERROR(causal_conv_with_state_helper::ParseDilation(info, dilation_)); } template @@ -106,7 +108,10 @@ Status VarlenCausalConvWithState::ComputeInternal(OpKernelContext* context) c ORT_RETURN_IF_NOT(kernel_size_64 >= 1 && kernel_size_64 <= std::numeric_limits::max(), "weight last dim (kernel_size) must be positive, got ", kernel_size_64); const int kernel_size = static_cast(kernel_size_64); - const int pad = kernel_size - 1; + const int64_t pad_64 = (kernel_size_64 - 1) * dilation_; + ORT_RETURN_IF_NOT(pad_64 <= std::numeric_limits::max(), + "(kernel_size - 1) * dilation is too large for the CUDA kernel"); + const int pad = static_cast(pad_64); if (bias_tensor != nullptr) { const auto& bias_shape = bias_tensor->Shape(); @@ -155,6 +160,7 @@ Status VarlenCausalConvWithState::ComputeInternal(OpKernelContext* context) c all_ones, channels, kernel_size, + dilation_, apply_silu, GetDeviceProp().maxThreadsPerBlock, state_update_capacity_); diff --git a/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state.h b/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state.h index 8be730e55b7b9..af02d414fa82a 100644 --- a/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state.h +++ b/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state.h @@ -22,6 +22,7 @@ class VarlenCausalConvWithState final : public onnxruntime::cuda::CudaKernel { private: std::string activation_; int state_update_capacity_; + int dilation_; }; // Launches the packed varlen causal-conv recurrence. @@ -37,9 +38,9 @@ Status LaunchVarlenCausalConvWithStateKernel( const T* input, // [total_tokens, channels] const T* weight, // [channels, 1, kernel_size] const T* bias, // [channels] or nullptr - const T* initial_state, // [batch_size, channels, kernel_size - 1], required + const T* initial_state, // [batch_size, channels, (kernel_size - 1) * dilation], required T* output, // [total_tokens, channels] - T* final_state, // [batch_size, channels, kernel_size - 1] + T* final_state, // [batch_size, channels, (kernel_size - 1) * dilation] T* state_update, // [batch_size, state_update_capacity, channels] or nullptr const int32_t* cu_seqlens, // [batch_size + 1], device-resident const int32_t* capture_count, // [batch_size] or nullptr @@ -48,6 +49,7 @@ Status LaunchVarlenCausalConvWithStateKernel( bool all_ones, int channels, int kernel_size, + int dilation, bool apply_silu, int max_threads_per_block, int state_update_capacity); diff --git a/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state_impl.cu b/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state_impl.cu index 54c166c8408fe..d1de1360226e0 100644 --- a/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state_impl.cu +++ b/onnxruntime/contrib_ops/cuda/bert/varlen_causal_conv_with_state_impl.cu @@ -46,6 +46,7 @@ __global__ void VarlenCausalConvDecodeKernel( int total_tokens, int channels, int kernel_size, + int dilation, bool apply_silu, int state_update_capacity) { const int bc = blockIdx.x * blockDim.x + threadIdx.x; @@ -63,7 +64,7 @@ __global__ void VarlenCausalConvDecodeKernel( return; } - const int pad = kernel_size - 1; + const int pad = (kernel_size - 1) * dilation; const int64_t state_offset = static_cast(bc) * pad; const int64_t weight_offset = static_cast(c) * kernel_size; const T input_value = input[static_cast(b) * channels + c]; @@ -75,21 +76,21 @@ __global__ void VarlenCausalConvDecodeKernel( if (pad == 0) { // K=1 has no state storage and therefore no aliasing access. sum += to_float(weight[weight_offset]) * to_float(input_value); - } else if (pad == 1) { - // K=2 consumes the only old state value before replacing it. - const T old = initial_state[state_offset]; - sum += to_float(weight[weight_offset]) * to_float(old); - sum += to_float(weight[weight_offset + 1]) * to_float(input_value); - final_state[state_offset] = input_value; } else { - // K>2 computes the complete result from old state before shifting in - // ascending order. Each destination is written only after its source was - // consumed by the dot product, and ps[k + 1] is read before ps[k] is - // overwritten, so initial_state == final_state is explicitly safe. - for (int k = 0; k < pad; ++k) { - sum += to_float(weight[weight_offset + k]) * to_float(initial_state[state_offset + k]); + // Compute the complete result from the old state before shifting in ascending order. Every + // destination is written only after its source was consumed by the dot product, and + // state[k + 1] is read before state[k] is overwritten, so initial_state == final_state is + // explicitly safe. Tap k reads the state element dilation positions apart; the newest tap + // (k = kernel_size - 1) is the incoming token itself. + // + // The state stores every one of the last pad raw samples, not only the dilated tap positions, + // so the shift is always by one sample regardless of dilation: a slot that is between two taps + // now becomes a tap slot on a later token. Shifting by dilation would drop those samples. + for (int k = 0; k < kernel_size - 1; ++k) { + sum += to_float(weight[weight_offset + k]) * + to_float(initial_state[state_offset + static_cast(k) * dilation]); } - sum += to_float(weight[weight_offset + pad]) * to_float(input_value); + sum += to_float(weight[weight_offset + kernel_size - 1]) * to_float(input_value); for (int k = 0; k < pad - 1; ++k) { final_state[state_offset + k] = initial_state[state_offset + k + 1]; } @@ -141,6 +142,7 @@ __global__ void VarlenCausalConvKernel( int total_tokens, int channels, int kernel_size, + int dilation, bool apply_silu, int state_update_capacity) { const int tid = threadIdx.x; @@ -153,7 +155,7 @@ __global__ void VarlenCausalConvKernel( const int64_t c64 = tile_first_channel + tid; const bool channel_active = c64 < channels; const int c = channel_active ? static_cast(c64) : 0; - const int pad = kernel_size - 1; + const int pad = (kernel_size - 1) * dilation; // These values are block-uniform. A malformed interval returns the complete // block before any input, state, or output access. @@ -202,7 +204,8 @@ __global__ void VarlenCausalConvKernel( float sum = bias_value; for (int k = 0; k < kernel_size; ++k) { sum += to_float(weight[weight_offset + k]) * - to_float(ReadStateOrInput(input, channel_state, start, channels, c, pad, t - pad + k)); + to_float(ReadStateOrInput(input, channel_state, start, channels, c, pad, + t - pad + k * dilation)); } if (apply_silu) { sum = VarlenSilu(sum); @@ -246,6 +249,7 @@ Status LaunchVarlenCausalConvWithStateKernel( bool all_ones, int channels, int kernel_size, + int dilation, bool apply_silu, int max_threads_per_block, int state_update_capacity) { @@ -258,12 +262,13 @@ Status LaunchVarlenCausalConvWithStateKernel( VarlenCausalConvDecodeKernel<<>>( input, weight, bias, initial_state, output, final_state, state_update, cu_seqlens, capture_count, static_cast(batch_channels), batch_size, total_tokens, - channels, kernel_size, apply_silu, state_update_capacity); + channels, kernel_size, dilation, apply_silu, state_update_capacity); return CUDA_CALL(cudaGetLastError()); } constexpr size_t kMaxStagedStateBytes = 48 * 1024; - const size_t state_bytes_per_channel = static_cast(kernel_size - 1) * sizeof(T); + const size_t state_bytes_per_channel = + static_cast(kernel_size - 1) * static_cast(dilation) * sizeof(T); // Stay within CUDA's portable 48-KiB per-block shared-memory budget instead // of requiring a device-specific dynamic-shared-memory opt-in. Practical // Qwen kernels use only a few state elements per channel and retain the full @@ -296,25 +301,25 @@ Status LaunchVarlenCausalConvWithStateKernel( VarlenCausalConvKernel<<(general_blocks), threads, shared_memory_bytes, stream>>>( input, weight, bias, initial_state, output, final_state, state_update, - cu_seqlens, capture_count, batch_size, total_tokens, channels, kernel_size, apply_silu, - state_update_capacity); + cu_seqlens, capture_count, batch_size, total_tokens, channels, kernel_size, dilation, + apply_silu, state_update_capacity); return CUDA_CALL(cudaGetLastError()); } template Status LaunchVarlenCausalConvWithStateKernel( cudaStream_t, const float*, const float*, const float*, const float*, float*, float*, float*, const int32_t*, const int32_t*, - int, int, bool, int, int, bool, int, int); + int, int, bool, int, int, int, bool, int, int); template Status LaunchVarlenCausalConvWithStateKernel( cudaStream_t, const half*, const half*, const half*, const half*, half*, half*, half*, const int32_t*, const int32_t*, - int, int, bool, int, int, bool, int, int); + int, int, bool, int, int, int, bool, int, int); template Status LaunchVarlenCausalConvWithStateKernel<__nv_bfloat16>( cudaStream_t, const __nv_bfloat16*, const __nv_bfloat16*, const __nv_bfloat16*, const __nv_bfloat16*, __nv_bfloat16*, __nv_bfloat16*, __nv_bfloat16*, - const int32_t*, const int32_t*, int, int, bool, int, int, bool, int, int); + const int32_t*, const int32_t*, int, int, bool, int, int, int, bool, int, int); } // namespace cuda } // namespace contrib diff --git a/onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc b/onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc index 05954f6cc8a31..95d81e53a3d49 100644 --- a/onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc +++ b/onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc @@ -168,6 +168,11 @@ class CUDA_MS_OP_TYPED_CLASS_NAME(1, BFloat16, LinearAttentionGate); class CUDA_MS_OP_TYPED_CLASS_NAME(1, float, GatedRMSNorm); class CUDA_MS_OP_TYPED_CLASS_NAME(1, MLFloat16, GatedRMSNorm); class CUDA_MS_OP_TYPED_CLASS_NAME(1, BFloat16, GatedRMSNorm); +class CUDA_MS_OP_TYPED_CLASS_NAME(1, float, EngramGate); +class CUDA_MS_OP_TYPED_CLASS_NAME(1, MLFloat16, EngramGate); +class CUDA_MS_OP_TYPED_CLASS_NAME(1, BFloat16, EngramGate); +class CUDA_MS_OP_TYPED_CLASS_NAME(1, int32_t, NGramHashMapping); +class CUDA_MS_OP_TYPED_CLASS_NAME(1, int64_t, NGramHashMapping); class CUDA_MS_OP_TYPED_CLASS_NAME(1, float, GatedAdd); class CUDA_MS_OP_TYPED_CLASS_NAME(1, MLFloat16, GatedAdd); class CUDA_MS_OP_TYPED_CLASS_NAME(1, BFloat16, GatedAdd); @@ -459,6 +464,11 @@ Status RegisterCudaContribKernels(KernelRegistry& kernel_registry) { BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, diff --git a/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.cc b/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.cc index 813d0338019bf..a47b313e482fd 100644 --- a/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.cc +++ b/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.cc @@ -6,6 +6,7 @@ #include "core/providers/webgpu/shader_helper.h" #include "core/providers/webgpu/webgpu_supported_types.h" #include "contrib_ops/webgpu/webgpu_contrib_kernels.h" +#include "contrib_ops/cpu/bert/causal_conv_with_state_helper.h" using namespace onnxruntime::webgpu; @@ -40,6 +41,9 @@ CausalConvWithState::CausalConvWithState(const OpKernelInfo& info) std::string activation_str = info.GetAttrOrDefault("activation", "none"); activation_ = ParseCausalConvActivation(activation_str); ORT_ENFORCE(info.GetAttr("ndim", &ndim_).IsOK(), "Attribute 'ndim' is required"); + ORT_THROW_IF_ERROR(causal_conv_with_state_helper::ParseDilation(info, dilation_)); + ORT_THROW_IF_ERROR(causal_conv_with_state_helper::ParseChannelsLast(info, channels_last_)); + ORT_ENFORCE(!channels_last_ || ndim_ == 1, "channels_last requires ndim = 1"); ORT_ENFORCE(info.GetAttrOrDefault("state_window", 0) == 0, "WebGPU CausalConvWithState does not support state_window > 0 (CUDA EP only)"); } @@ -61,6 +65,7 @@ Status CausalConvWithStateProgram::GenerateShaderCode(ShaderHelper& shader) cons } return WGSL_TEMPLATE_APPLY(shader, "bert/causal_conv_with_state.wgsl.template", + WGSL_TEMPLATE_PARAMETER(channels_last, channels_last_), WGSL_TEMPLATE_PARAMETER(has_bias, has_bias_), WGSL_TEMPLATE_PARAMETER(has_conv_state, has_conv_state_), WGSL_TEMPLATE_PARAMETER(output_present_state, output_present_state_), @@ -71,25 +76,46 @@ Status CausalConvUpdateStateProgram::GenerateShaderCode(ShaderHelper& shader) co shader.AddInput("input", ShaderUsage::UseElementTypeAlias); shader.AddOutput("present_state", ShaderUsage::UseUniform); - shader.MainFunctionBody() << shader.GuardAgainstOutOfBoundsWorkgroupSizes("uniforms.update_size") - << " let base_state = global_idx * uniforms.state_length;\n" - " let base_input = global_idx * uniforms.input_length;\n" - "\n" - " if (uniforms.input_length >= uniforms.state_length) {\n" - " let input_offset = uniforms.input_length - uniforms.state_length;\n" - " for (var s = 0u; s < uniforms.state_length; s++) {\n" - " present_state[base_state + s] = input[base_input + input_offset + s];\n" - " }\n" - " } else {\n" - " let preserved_state = uniforms.state_length - uniforms.input_length;\n" - " for (var s = 0u; s < uniforms.state_length; s++) {\n" - " if (s < preserved_state) {\n" - " present_state[base_state + s] = present_state[base_state + s + uniforms.input_length];\n" - " } else {\n" - " present_state[base_state + s] = input[base_input + s - preserved_state];\n" - " }\n" - " }\n" - " }\n"; + // global_idx enumerates (batch, channel) pairs. Both layouts are dense, so a (base, stride) + // pair per tensor covers them: channels-first walks a contiguous row, channels-last strides by + // `channels`. + shader.MainFunctionBody() << shader.GuardAgainstOutOfBoundsWorkgroupSizes("uniforms.update_size"); + if (channels_last_) { + shader.MainFunctionBody() + << " let batch_idx = global_idx / uniforms.channels;\n" + " let channel_idx = global_idx % uniforms.channels;\n" + " let base_state = batch_idx * uniforms.channels * uniforms.state_length + channel_idx;\n" + " let state_stride = uniforms.channels;\n" + " let base_input = batch_idx * uniforms.channels * uniforms.input_length + channel_idx;\n" + " let input_stride = uniforms.channels;\n"; + } else { + shader.MainFunctionBody() + << " let base_state = global_idx * uniforms.state_length;\n" + " let state_stride = 1u;\n" + " let base_input = global_idx * uniforms.input_length;\n" + " let input_stride = 1u;\n"; + } + + shader.MainFunctionBody() + << "\n" + " if (uniforms.input_length >= uniforms.state_length) {\n" + " let input_offset = uniforms.input_length - uniforms.state_length;\n" + " for (var s = 0u; s < uniforms.state_length; s++) {\n" + " present_state[base_state + s * state_stride] =\n" + " input[base_input + (input_offset + s) * input_stride];\n" + " }\n" + " } else {\n" + " let preserved_state = uniforms.state_length - uniforms.input_length;\n" + " for (var s = 0u; s < uniforms.state_length; s++) {\n" + " if (s < preserved_state) {\n" + " present_state[base_state + s * state_stride] =\n" + " present_state[base_state + (s + uniforms.input_length) * state_stride];\n" + " } else {\n" + " present_state[base_state + s * state_stride] =\n" + " input[base_input + (s - preserved_state) * input_stride];\n" + " }\n" + " }\n" + " }\n"; return Status::OK(); } @@ -98,23 +124,28 @@ Status CausalConvWithState::ComputeInternal(ComputeContext& context) const { const Tensor* input = context.Input(0); // (B, D, L) const Tensor* weight = context.Input(1); // (D, 1, K) const Tensor* bias = context.Input(2); // optional (D,) - const Tensor* conv_state = context.Input(3); // optional (B, D, K-1) — past_state + const Tensor* conv_state = context.Input(3); // optional (B, D, (K-1)*dilation) — past_state ORT_RETURN_IF(activation_ == CausalConvActivation::Invalid, "Invalid activation type"); ORT_RETURN_IF(ndim_ != 1, "Only 1D convolution is supported"); const auto& input_shape = input->Shape(); const auto& weight_shape = weight->Shape(); - ORT_RETURN_IF(input_shape.NumDimensions() != 3, - "Input must be 3D (batch_size, channels, length)"); + if (channels_last_) { + ORT_RETURN_IF(input_shape.NumDimensions() < 3, + "Input must have rank >= 3 (batch_size, sequence_length, ...channels) when channels_last = 1"); + } else { + ORT_RETURN_IF(input_shape.NumDimensions() != 3, + "Input must be 3D (batch_size, channels, length)"); + } ORT_RETURN_IF(weight_shape.NumDimensions() != 3, "Weight must be 3D (channels, 1, kernel_size)"); const int64_t batch_size = input_shape[0]; - const int64_t channels = input_shape[1]; - const int64_t input_length = input_shape[2]; + const int64_t channels = channels_last_ ? input_shape.SizeFromDimension(2) : input_shape[1]; + const int64_t input_length = channels_last_ ? input_shape[1] : input_shape[2]; const int64_t kernel_size = weight_shape[2]; - const int64_t state_length = kernel_size - 1; + const int64_t state_length = (kernel_size - 1) * dilation_; ORT_RETURN_IF(weight_shape[0] != channels, "Weight first dim must match input channels"); ORT_RETURN_IF(weight_shape[1] != 1, "Weight second dim must be 1 for depthwise convolution"); @@ -124,15 +155,22 @@ Status CausalConvWithState::ComputeInternal(ComputeContext& context) const { ORT_RETURN_IF(bias->Shape()[0] != channels, "Bias size must match channels"); } + TensorShapeVector state_dims; + if (channels_last_) { + state_dims.push_back(batch_size); + state_dims.push_back(state_length); + for (size_t i = 2; i < input_shape.NumDimensions(); ++i) { + state_dims.push_back(input_shape[i]); + } + } else { + state_dims = TensorShapeVector{batch_size, channels, state_length}; + } + const TensorShape state_shape(state_dims); + if (conv_state != nullptr) { - ORT_RETURN_IF(conv_state->Shape().NumDimensions() != 3, - "conv_state must be 3D (batch_size, channels, kernel_size - 1)"); - ORT_RETURN_IF(conv_state->Shape()[0] != batch_size, - "conv_state batch_size must match input"); - ORT_RETURN_IF(conv_state->Shape()[1] != channels, - "conv_state channels must match input"); - ORT_RETURN_IF(conv_state->Shape()[2] != state_length, - "conv_state last dim must be kernel_size - 1"); + ORT_RETURN_IF(conv_state->Shape() != state_shape, + "conv_state is expected to have shape ", state_shape.ToString(), + ", got ", conv_state->Shape().ToString()); } const bool has_bias = (bias != nullptr); @@ -142,9 +180,8 @@ Status CausalConvWithState::ComputeInternal(ComputeContext& context) const { // Output 0: (B, D, L) Tensor* output = context.Output(0, input_shape); - // Output 1: present_state (B, D, K-1) - std::vector state_dims{batch_size, channels, state_length}; - Tensor* present_state = context.Output(1, TensorShape(state_dims)); + // Output 1: present_state, matching the layout selected by channels_last + Tensor* present_state = context.Output(1, state_shape); const bool conv_state_in_present_state = has_conv_state && conv_state->DataRaw() == present_state->DataRaw(); if (input_shape.Size() == 0) { @@ -159,12 +196,13 @@ Status CausalConvWithState::ComputeInternal(ComputeContext& context) const { } // Create and run the shader program - CausalConvWithStateProgram program{activation_, has_bias, has_conv_state, !conv_state_in_present_state}; + CausalConvWithStateProgram program{activation_, has_bias, has_conv_state, !conv_state_in_present_state, + channels_last_}; uint32_t output_size = static_cast(batch_size * channels * input_length); program.CacheHint(has_bias, has_conv_state, !conv_state_in_present_state, - kernel_size, static_cast(activation_)); + kernel_size, dilation_, static_cast(activation_), channels_last_); program.AddInput({input, ProgramTensorMetadataDependency::Type}) .AddInput({weight, ProgramTensorMetadataDependency::None}); @@ -186,18 +224,21 @@ Status CausalConvWithState::ComputeInternal(ComputeContext& context) const { .AddUniformVariable({static_cast(channels)}) .AddUniformVariable({static_cast(input_length)}) .AddUniformVariable({static_cast(kernel_size)}) + .AddUniformVariable({static_cast(dilation_)}) .AddUniformVariable({static_cast(state_length)}) .AddUniformVariable({output_size}); ORT_RETURN_IF_ERROR(context.RunProgram(program)); if (conv_state_in_present_state) { - CausalConvUpdateStateProgram update_state_program; + CausalConvUpdateStateProgram update_state_program{channels_last_}; const uint32_t update_size = static_cast(batch_size * channels); + update_state_program.CacheHint(channels_last_); update_state_program.AddInput({input, ProgramTensorMetadataDependency::Type}) .AddOutput({present_state, ProgramTensorMetadataDependency::None}) .SetDispatchGroupSize((update_size + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE) - .AddUniformVariables({{static_cast(input_length)}, + .AddUniformVariables({{static_cast(channels)}, + {static_cast(input_length)}, {static_cast(state_length)}, {update_size}}); diff --git a/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.h b/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.h index 3ccd5f9a67f2b..d724261645a44 100644 --- a/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.h +++ b/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.h @@ -28,12 +28,13 @@ CausalConvActivation ParseCausalConvActivation(const std::string& activation_str class CausalConvWithStateProgram final : public Program { public: CausalConvWithStateProgram(CausalConvActivation activation, bool has_bias, bool has_conv_state, - bool output_present_state) + bool output_present_state, bool channels_last) : Program{"CausalConvWithState"}, activation_(activation), has_bias_(has_bias), has_conv_state_(has_conv_state), - output_present_state_(output_present_state) {} + output_present_state_(output_present_state), + channels_last_(channels_last) {} Status GenerateShaderCode(ShaderHelper& sh) const override; @@ -42,6 +43,7 @@ class CausalConvWithStateProgram final : public Program { public: - CausalConvUpdateStateProgram() : Program{"CausalConvUpdateState"} {} + explicit CausalConvUpdateStateProgram(bool channels_last) + : Program{"CausalConvUpdateState"}, channels_last_(channels_last) {} Status GenerateShaderCode(ShaderHelper& sh) const override; WEBGPU_PROGRAM_DEFINE_UNIFORM_VARIABLES( + {"channels", ProgramUniformVariableDataType::Uint32}, {"input_length", ProgramUniformVariableDataType::Uint32}, {"state_length", ProgramUniformVariableDataType::Uint32}, {"update_size", ProgramUniformVariableDataType::Uint32}); + + private: + bool channels_last_; }; // Kernel for CausalConvWithState @@ -73,6 +81,8 @@ class CausalConvWithState final : public WebGpuKernel { private: CausalConvActivation activation_; int64_t ndim_; + int dilation_; + bool channels_last_; }; } // namespace webgpu diff --git a/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.wgsl.template b/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.wgsl.template index 9498aab0a9da0..1f95a2fd1b382 100644 --- a/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.wgsl.template +++ b/onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.wgsl.template @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#param channels_last #param has_bias #param has_conv_state #param output_present_state @@ -21,22 +22,50 @@ $MAIN { let channels = uniforms.channels; let input_length = uniforms.input_length; let kernel_size = uniforms.kernel_size; - let state_length = uniforms.state_length; // = kernel_size - 1 - + let dilation = uniforms.dilation; + let state_length = uniforms.state_length; // = (kernel_size - 1) * dilation + + // Decompose global_idx so that adjacent invocations touch adjacent memory in the active layout. + // The full (batch, channel, position) index space is covered either way; only the order changes. +#if channels_last + // Channels are the fastest-moving axis, so they must also be the fastest-moving invocation axis. + let channel_idx = global_idx % channels; + let bp_idx = global_idx / channels; + let pos = bp_idx % input_length; + let batch_idx = bp_idx / input_length; +#else let pos = global_idx % input_length; let bc_idx = global_idx / input_length; let batch_idx = bc_idx / channels; let channel_idx = bc_idx % channels; +#endif + + // Both layouts are dense, so a strided (batch, position, channel) view covers them: the base is + // the (batch, channel) origin and successive positions are pos_stride elements apart. +#if channels_last + // (batch_size, sequence_length, d_1, ..., d_n), channels = d_1 * ... * d_n + let input_base = batch_idx * channels * input_length + channel_idx; + let input_pos_stride = channels; + let state_base = batch_idx * channels * state_length + channel_idx; + let state_pos_stride = channels; +#else + // (batch_size, channels, sequence_length) + let input_base = (batch_idx * channels + channel_idx) * input_length; + let input_pos_stride = 1u; + let state_base = (batch_idx * channels + channel_idx) * state_length; + let state_pos_stride = 1u; +#endif // Perform depthwise causal convolution for this (batch, channel, pos). - // The convolution window looks back kernel_size-1 positions. + // The convolution window looks back state_length positions. // With conv_state providing the history before position 0, the // "virtual" input is: [conv_state[0..state_length-1], input[0..L-1]] // // For output position pos: - // output[pos] = sum_{j=0}^{kernel_size-1} weight[j] * virtual_input[pos + j] + // output[pos] = sum_{j=0}^{kernel_size-1} weight[j] * virtual_input[pos + j * dilation] // where virtual_input is state_length positions of conv_state - // followed by input_length positions of input. + // followed by input_length positions of input. At j = kernel_size-1 the tap lands on + // virtual_input[pos + state_length], i.e. the current position. var acc: input_element_t = 0.0; @@ -45,27 +74,24 @@ $MAIN { for (var j: u32 = 0; j < kernel_size; j = j + 1) { // virtual_pos is the position in the concatenated [conv_state, input] - let virtual_pos = pos + j; + let virtual_pos = pos + j * dilation; var val: input_element_t = 0.0; #if has_conv_state if (virtual_pos < state_length) { - // Read from conv_state: (B, D, state_length) - let state_idx = (batch_idx * channels + channel_idx) * state_length + virtual_pos; - val = conv_state[state_idx]; + // Read from conv_state + val = conv_state[state_base + virtual_pos * state_pos_stride]; } else { - // Read from input: (B, D, L) + // Read from input let input_pos = virtual_pos - state_length; - let input_idx = (batch_idx * channels + channel_idx) * input_length + input_pos; - val = input[input_idx]; + val = input[input_base + input_pos * input_pos_stride]; } #else // No conv_state: pad with zeros for positions before the input if (virtual_pos >= state_length) { let input_pos = virtual_pos - state_length; - let input_idx = (batch_idx * channels + channel_idx) * input_length + input_pos; - val = input[input_idx]; + val = input[input_base + input_pos * input_pos_stride]; } #endif @@ -81,12 +107,11 @@ $MAIN { acc = silu(acc); #endif - // Write output: (B, D, L) - let out_idx = (batch_idx * channels + channel_idx) * input_length + pos; - output[out_idx] = acc; + // Write output + output[input_base + pos * input_pos_stride] = acc; #if output_present_state - // Write present_state: the last (kernel_size - 1) elements from the + // Write present_state: the last state_length elements from the // virtual input [conv_state, input]. We only write present_state once // per (batch, channel), using the thread at pos == 0. if (pos == 0u) { @@ -98,23 +123,19 @@ $MAIN { #if has_conv_state if (vp < state_length) { - let si = (batch_idx * channels + channel_idx) * state_length + vp; - state_val = conv_state[si]; + state_val = conv_state[state_base + vp * state_pos_stride]; } else { let ip = vp - state_length; - let ii = (batch_idx * channels + channel_idx) * input_length + ip; - state_val = input[ii]; + state_val = input[input_base + ip * input_pos_stride]; } #else if (vp >= state_length) { let ip = vp - state_length; - let ii = (batch_idx * channels + channel_idx) * input_length + ip; - state_val = input[ii]; + state_val = input[input_base + ip * input_pos_stride]; } #endif - let ps_idx = (batch_idx * channels + channel_idx) * state_length + s; - present_state[ps_idx] = state_val; + present_state[state_base + s * state_pos_stride] = state_val; } } #endif diff --git a/onnxruntime/contrib_ops/webgpu/bert/engram_gate.cc b/onnxruntime/contrib_ops/webgpu/bert/engram_gate.cc new file mode 100644 index 0000000000000..35ad0235cce2b --- /dev/null +++ b/onnxruntime/contrib_ops/webgpu/bert/engram_gate.cc @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "contrib_ops/webgpu/bert/engram_gate.h" + +#include "contrib_ops/webgpu/bert/engram_helper.h" +#include "contrib_ops/webgpu/webgpu_contrib_kernels.h" +#include "core/providers/webgpu/shader_helper.h" +#include "core/providers/webgpu/webgpu_supported_types.h" +#include "core/providers/webgpu/webgpu_utils.h" + +#include + +namespace onnxruntime { +namespace contrib { +namespace webgpu { + +ONNX_OPERATOR_KERNEL_EX( + EngramGate, + kMSDomain, + 1, + kWebGpuExecutionProvider, + (*KernelDefBuilder::Create()) + .TypeConstraint("T", WebGpuSupportedFloatTypes()), + EngramGate); + +namespace { +constexpr uint32_t kGateWorkgroupSize = 64; +} // namespace + +Status EngramGateScalarProgram::GenerateShaderCode(ShaderHelper& shader) const { + const auto& key = shader.AddInput("key", ShaderUsage::UseUniform | ShaderUsage::UseElementTypeAlias); + const auto& query = shader.AddInput("query", ShaderUsage::UseUniform | ShaderUsage::UseElementTypeAlias); + const auto& key_norm_scale = shader.AddInput("key_norm_scale", ShaderUsage::UseUniform | ShaderUsage::UseElementTypeAlias); + const auto& query_norm_scale = shader.AddInput("query_norm_scale", ShaderUsage::UseUniform | ShaderUsage::UseElementTypeAlias); + const auto& gate = shader.AddOutput("gate", ShaderUsage::UseUniform); + + // key, query and both norm scales are all contiguous over the hidden dimension and share its + // length, so one component count vectorizes every load in the reduction. + const int components = key.NumComponents(); + + shader.AdditionalImplementation() + << engram_helper::kStableSigmoidWgsl << engram_helper::kEngramGateArgWgsl + << "alias gate_f32_t = " << MakeScalarOrVectorType(components, "f32") << ";\n" + << "var key_partials: array;\n" + << "var query_partials: array;\n" + << "var dot_partials: array;\n"; + + shader.MainFunctionBody() + << " let row = workgroup_idx;\n" + << " if (row >= uniforms.rows) { return; }\n" + << " let g = row % uniforms.hc_mult;\n" + << " let row_base = row * uniforms.hidden_vec_size;\n" + << " let scale_base = g * uniforms.hidden_vec_size;\n" + << " var key_sum_sq = 0.0;\n" + << " var query_sum_sq = 0.0;\n" + << " var dot_numerator = 0.0;\n" + << " for (var d = local_idx; d < uniforms.hidden_vec_size; d += " << kGateWorkgroupSize << "u) {\n" + << " let key_value = gate_f32_t(" << key.GetByOffset("row_base + d") << ");\n" + << " let query_value = gate_f32_t(" << query.GetByOffset("row_base + d") << ");\n" + << " let key_squared = key_value * key_value;\n" + << " let query_squared = query_value * query_value;\n" + << " let dot_terms = key_value * gate_f32_t(" << key_norm_scale.GetByOffset("scale_base + d") + << ") * query_value * gate_f32_t(" << query_norm_scale.GetByOffset("scale_base + d") << ");\n" + << " key_sum_sq += " << SumVector("key_squared", components) << ";\n" + << " query_sum_sq += " << SumVector("query_squared", components) << ";\n" + << " dot_numerator += " << SumVector("dot_terms", components) << ";\n" + << " }\n" + << " key_partials[local_idx] = key_sum_sq;\n" + << " query_partials[local_idx] = query_sum_sq;\n" + << " dot_partials[local_idx] = dot_numerator;\n" + << " workgroupBarrier();\n" + << " for (var stride = " << (kGateWorkgroupSize / 2) << "u; stride > 0u; stride >>= 1u) {\n" + << " if (local_idx < stride) {\n" + << " key_partials[local_idx] += key_partials[local_idx + stride];\n" + << " query_partials[local_idx] += query_partials[local_idx + stride];\n" + << " dot_partials[local_idx] += dot_partials[local_idx + stride];\n" + << " }\n" + << " workgroupBarrier();\n" + << " }\n" + << " if (local_idx == 0u) {\n" + << " let key_inv_rms = inverseSqrt(key_partials[0] / f32(uniforms.hidden_size) + uniforms.epsilon);\n" + << " let query_inv_rms = inverseSqrt(query_partials[0] / f32(uniforms.hidden_size) + uniforms.epsilon);\n" + << " let dot_value = dot_partials[0] * key_inv_rms * query_inv_rms / sqrt(f32(uniforms.hidden_size));\n" + << " " << gate.SetByOffset("row", "stable_sigmoid(engram_gate_arg(dot_value))") << "\n" + << " }\n"; + return Status::OK(); +} + +Status EngramGateProgram::GenerateShaderCode(ShaderHelper& shader) const { + const auto& value = shader.AddInput("value", ShaderUsage::UseUniform | ShaderUsage::UseElementTypeAlias); + const auto& gate = shader.AddInput("gate", ShaderUsage::UseUniform); + const auto& output = shader.AddOutput("output", ShaderUsage::UseUniform | ShaderUsage::UseValueTypeAlias); + + // value and output are both contiguous over the hidden dimension, and the gate is constant across + // it, so one invocation can broadcast the gate over a whole vecN of channels. + const int components = value.NumComponents(); + + shader.AdditionalImplementation() << "alias gate_f32_t = " << MakeScalarOrVectorType(components, "f32") << ";\n"; + + shader.MainFunctionBody() + << shader.GuardAgainstOutOfBoundsWorkgroupSizes("uniforms.total") + << " let c = global_idx % uniforms.hidden_vec_size;\n" + << " let row = global_idx / uniforms.hidden_vec_size;\n" + << " let token = row / uniforms.hc_mult;\n" + << " let value_element = gate_f32_t(" << value.GetByOffset("token * uniforms.hidden_vec_size + c") << ");\n" + << " " << output.SetByOffset("global_idx", "output_value_t(" + gate.GetByOffset("row") + " * value_element)") + << "\n"; + return Status::OK(); +} + +EngramGate::EngramGate(const OpKernelInfo& info) : WebGpuKernel(info) { + epsilon_ = info.GetAttrOrDefault("epsilon", 1.0e-5f); +} + +Status EngramGate::ComputeInternal(ComputeContext& context) const { + const auto* key = context.Input(0); + const auto* query = context.Input(1); + const auto* value = context.Input(2); + const auto* key_norm_scale = context.Input(3); + const auto* query_norm_scale = context.Input(4); + + const auto& key_shape = key->Shape(); + ORT_RETURN_IF_NOT(key_shape.NumDimensions() == 4, + "key must have shape (batch_size, sequence_length, hc_mult, hidden_size)"); + const int64_t batch_size = key_shape[0]; + const int64_t sequence_length = key_shape[1]; + const int64_t hc_mult = key_shape[2]; + const int64_t hidden_size = key_shape[3]; + + ORT_RETURN_IF_NOT(query->Shape() == key_shape, "query must have the same shape as key"); + ORT_RETURN_IF_NOT(value->Shape() == TensorShape({batch_size, sequence_length, hidden_size}), + "value must have shape (batch_size, sequence_length, hidden_size)"); + ORT_RETURN_IF_NOT(key_norm_scale->Shape() == TensorShape({hc_mult, hidden_size}), + "key_norm_scale must have shape (hc_mult, hidden_size)"); + ORT_RETURN_IF_NOT(query_norm_scale->Shape() == TensorShape({hc_mult, hidden_size}), + "query_norm_scale must have shape (hc_mult, hidden_size)"); + + auto* output = context.Output(0, key_shape); + const int64_t total = key_shape.Size(); + if (total == 0) { + return Status::OK(); + } + + // First pass: one scalar gate per (token, g) row. + const int64_t rows = batch_size * sequence_length * hc_mult; + const int components = onnxruntime::webgpu::GetMaxComponents(hidden_size); + const int64_t hidden_vec_size = hidden_size / components; + Tensor gate = context.CreateGPUTensor(DataTypeImpl::GetType(), TensorShape({rows})); + EngramGateScalarProgram gate_program; + gate_program + .CacheHint(components) + .AddInputs({{key, ProgramTensorMetadataDependency::Type, ProgramInput::Flatten, components}, + {query, ProgramTensorMetadataDependency::Type, ProgramInput::Flatten, components}, + {key_norm_scale, ProgramTensorMetadataDependency::Type, ProgramInput::Flatten, components}, + {query_norm_scale, ProgramTensorMetadataDependency::Type, ProgramInput::Flatten, components}}) + .AddOutput({&gate, ProgramTensorMetadataDependency::None}) + .SetWorkgroupSize(kGateWorkgroupSize) + .SetDispatchGroupSize(onnxruntime::narrow(rows)) + .AddUniformVariables({{onnxruntime::narrow(rows)}, + {onnxruntime::narrow(hc_mult)}, + {onnxruntime::narrow(hidden_size)}, + {onnxruntime::narrow(hidden_vec_size)}, + {epsilon_}}); + ORT_RETURN_IF_ERROR(context.RunProgram(gate_program)); + + // Second pass: broadcast the shared gate over the value channels. + const int64_t total_vec = total / components; + EngramGateProgram program; + program + .CacheHint(components) + .AddInputs({{value, ProgramTensorMetadataDependency::Type, ProgramInput::Flatten, components}, + {&gate, ProgramTensorMetadataDependency::Type}}) + .AddOutput({output, ProgramTensorMetadataDependency::None, ProgramOutput::Flatten, components}) + .SetDispatchGroupSize((onnxruntime::narrow(total_vec) + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE) + .AddUniformVariables({{onnxruntime::narrow(total_vec)}, + {onnxruntime::narrow(hc_mult)}, + {onnxruntime::narrow(hidden_vec_size)}}); + return context.RunProgram(program); +} + +} // namespace webgpu +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/webgpu/bert/engram_gate.h b/onnxruntime/contrib_ops/webgpu/bert/engram_gate.h new file mode 100644 index 0000000000000..7e60c04e04813 --- /dev/null +++ b/onnxruntime/contrib_ops/webgpu/bert/engram_gate.h @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "core/providers/webgpu/program.h" +#include "core/providers/webgpu/webgpu_kernel.h" + +namespace onnxruntime { +namespace contrib { +namespace webgpu { + +using namespace onnxruntime::webgpu; +using onnxruntime::webgpu::ComputeContext; + +// Computes the scalar gate for each (token, g) row. The gate does not depend on the output channel, +// so one workgroup reduces it once per row instead of every channel repeating the reduction. +class EngramGateScalarProgram final : public Program { + public: + EngramGateScalarProgram() : Program{"EngramGateScalar"} {} + Status GenerateShaderCode(ShaderHelper& shader) const override; + WEBGPU_PROGRAM_DEFINE_UNIFORM_VARIABLES({"rows", ProgramUniformVariableDataType::Uint32}, + {"hc_mult", ProgramUniformVariableDataType::Uint32}, + {"hidden_size", ProgramUniformVariableDataType::Uint32}, + {"hidden_vec_size", ProgramUniformVariableDataType::Uint32}, + {"epsilon", ProgramUniformVariableDataType::Float32}); +}; + +// Broadcasts the per-row gate over the value channels, one invocation per vecN of output channels. +class EngramGateProgram final : public Program { + public: + EngramGateProgram() : Program{"EngramGate"} {} + Status GenerateShaderCode(ShaderHelper& shader) const override; + WEBGPU_PROGRAM_DEFINE_UNIFORM_VARIABLES({"total", ProgramUniformVariableDataType::Uint32}, + {"hc_mult", ProgramUniformVariableDataType::Uint32}, + {"hidden_vec_size", ProgramUniformVariableDataType::Uint32}); +}; + +class EngramGate final : public WebGpuKernel { + public: + explicit EngramGate(const OpKernelInfo& info); + Status ComputeInternal(ComputeContext& context) const override; + + private: + float epsilon_; +}; + +} // namespace webgpu +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/webgpu/bert/engram_helper.h b/onnxruntime/contrib_ops/webgpu/bert/engram_helper.h new file mode 100644 index 0000000000000..eaebc702809da --- /dev/null +++ b/onnxruntime/contrib_ops/webgpu/bert/engram_helper.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace onnxruntime { +namespace contrib { +namespace webgpu { +namespace engram_helper { + +// WGSL snippets shared by the contrib kernels. Append them to ShaderHelper::AdditionalImplementation(). + +// Numerically stable logistic function. +constexpr std::string_view kStableSigmoidWgsl = + "fn stable_sigmoid(x: f32) -> f32 {\n" + " if (x > 0.0) { return 1.0 / (1.0 + exp(-x)); }\n" + " let e = exp(x);\n" + " return e / (1.0 + e);\n" + "}\n"; + +// Engram gate pre-activation: sign(dot) * sqrt(max(abs(dot), 1e-6)). WGSL sign() already maps zero +// to zero, so a zero dot product yields a zero argument (and therefore a gate of exactly 0.5). +constexpr std::string_view kEngramGateArgWgsl = + "fn engram_gate_arg(dot_value: f32) -> f32 {\n" + " return sign(dot_value) * sqrt(max(abs(dot_value), 0.000001));\n" + "}\n"; + +// Euclidean modulo: the result always has the sign of `mod_value`, which must be positive. +constexpr std::string_view kPositiveModWgsl = + "fn positive_mod(value: i32, mod_value: i32) -> i32 {\n" + " var result = value % mod_value;\n" + " if (result < 0i) { result += mod_value; }\n" + " return result;\n" + "}\n"; + +} // namespace engram_helper +} // namespace webgpu +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/webgpu/bert/ngram_hash_mapping.cc b/onnxruntime/contrib_ops/webgpu/bert/ngram_hash_mapping.cc new file mode 100644 index 0000000000000..9f0eba5e60169 --- /dev/null +++ b/onnxruntime/contrib_ops/webgpu/bert/ngram_hash_mapping.cc @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "contrib_ops/webgpu/bert/ngram_hash_mapping.h" + +#include "contrib_ops/webgpu/bert/engram_helper.h" +#include "contrib_ops/webgpu/webgpu_contrib_kernels.h" +#include "core/providers/webgpu/shader_helper.h" +#include "core/providers/webgpu/webgpu_supported_types.h" + +#include + +namespace onnxruntime { +namespace contrib { +namespace webgpu { + +ONNX_OPERATOR_KERNEL_EX( + NGramHashMapping, + kMSDomain, + 1, + kWebGpuExecutionProvider, + (*KernelDefBuilder::Create()) + .MayInplace(3, 1) + .TypeConstraint("M", DataTypeImpl::GetTensorType()), + NGramHashMapping); + +Status NGramHashMappingProgram::GenerateShaderCode(ShaderHelper& shader) const { + const auto& input_ids = shader.AddInput("input_ids", ShaderUsage::UseUniform); + const auto& multipliers = shader.AddInput("multipliers", ShaderUsage::UseUniform); + const auto& vocab_sizes = shader.AddInput("vocab_sizes", ShaderUsage::UseUniform); + const ShaderVariableHelper* past_ids = nullptr; + if (has_past_ids_) { + past_ids = &shader.AddInput("past_ids", ShaderUsage::UseUniform); + } + const auto& output = shader.AddOutput("output", ShaderUsage::UseUniform); + + shader.AdditionalImplementation() << engram_helper::kPositiveModWgsl; + + shader.MainFunctionBody() + << shader.GuardAgainstOutOfBoundsWorkgroupSizes("uniforms.total") + << " let num_heads = (uniforms.max_ngram_size - 1u) * uniforms.n_head_per_ngram;\n" + << " let t = global_idx % uniforms.sequence_length;\n" + << " let b = global_idx / uniforms.sequence_length;\n" + << " let input_base = b * uniforms.sequence_length;\n" + << " let output_base = global_idx * num_heads;\n" + << " let state_length = uniforms.max_ngram_size - 1u;\n" + << " let past_base = b * state_length;\n" + << " for (var n = 2u; n <= uniforms.max_ngram_size; n++) {\n" + << " var mix = 0i;\n" + << " for (var k = 0u; k < n; k++) {\n" + << " var token = uniforms.pad_id;\n" + << " if (t >= k) {\n" + << " token = " << input_ids.GetByOffset("input_base + t - k") << ";\n" + << " }\n"; + if (has_past_ids_) { + // past_ids is right-aligned, so position -1 is its last slot. k <= max_ngram_size - 1 keeps the + // slot inside the window, so no additional bounds check is needed here. + shader.MainFunctionBody() + << " if (t < k) {\n" + << " token = " << past_ids->GetByOffset("past_base + state_length + t - k") << ";\n" + << " }\n"; + } + shader.MainFunctionBody() + << " let product = token * " << multipliers.GetByOffset("k") << ";\n" + << " if (k == 0u) { mix = product; } else { mix = mix ^ product; }\n" + << " }\n" + << " let ngram_offset = (n - 2u) * uniforms.n_head_per_ngram;\n" + << " for (var h = 0u; h < uniforms.n_head_per_ngram; h++) {\n" + << " let out_h = ngram_offset + h;\n" + << " let mod_value = " << vocab_sizes.GetByOffset("out_h") << ";\n" + << " var result = 0i;\n" + << " if (mod_value > 0i) {\n" + << " result = positive_mod(mix, mod_value);\n" + << " }\n" + << " " << output.SetByOffset("output_base + out_h", "result") << "\n" + << " }\n" + << " }\n"; + return Status::OK(); +} + +Status NGramPresentIdsProgram::GenerateShaderCode(ShaderHelper& shader) const { + const ShaderVariableHelper* input_ids = nullptr; + if (has_input_ids_) { + input_ids = &shader.AddInput("input_ids", ShaderUsage::UseUniform); + } + const ShaderVariableHelper* past_ids = nullptr; + if (has_past_ids_ && !past_aliases_present_) { + past_ids = &shader.AddInput("past_ids", ShaderUsage::UseUniform); + } + const auto& present_ids = shader.AddOutput("present_ids", ShaderUsage::UseUniform); + // When past_ids aliases present_ids the history lives in the output buffer itself, and reading it + // through the read_write binding is the only spec-legal way to reach it. + const ShaderVariableHelper* history = past_aliases_present_ ? &present_ids : past_ids; + + // past_ids and present_ids may be the same buffer, which is what threading present_ids straight + // back into past_ids produces; `history` above then points at the output binding. Slot `slot` + // writes index `slot` and reads index `slot + sequence_length`, so the read and write ranges + // overlap and must be separated. One workgroup owns one batch row and walks it in ascending + // workgroup-sized chunks: a barrier separates the chunk's reads from its writes, and a chunk only + // writes indices strictly below the read indices of every later chunk. + shader.MainFunctionBody() + << " let b = workgroup_idx;\n" + // NormalizeDispatchGroupSize reshapes an oversized 1-D dispatch to a 2-D grid that rounds up, + // so a large batch_size can produce workgroups past the last row. + << " if (b >= uniforms.batch_size) { return; }\n" + << " let row_base = b * uniforms.state_length;\n" + << " for (var chunk = 0u; chunk < uniforms.state_length; chunk += workgroup_size_x) {\n" + << " let slot = chunk + local_idx;\n" + << " var token = uniforms.pad_id;\n" + << " if (slot < uniforms.state_length) {\n"; + if (has_input_ids_) { + shader.MainFunctionBody() + << " if (slot + uniforms.sequence_length >= uniforms.state_length) {\n" + << " let source_t = slot + uniforms.sequence_length - uniforms.state_length;\n" + << " token = " << input_ids->GetByOffset("b * uniforms.sequence_length + source_t") << ";\n" + << " }\n"; + } + if (has_past_ids_) { + shader.MainFunctionBody() + << " if (slot + uniforms.sequence_length < uniforms.state_length) {\n" + << " token = " << history->GetByOffset("b * uniforms.state_length + slot + uniforms.sequence_length") + << ";\n" + << " }\n"; + } + shader.MainFunctionBody() + << " }\n" + << " workgroupBarrier();\n" + << " if (slot < uniforms.state_length) {\n" + << " " << present_ids.SetByOffset("row_base + slot", "token") << "\n" + << " }\n" + << " workgroupBarrier();\n" + << " }\n"; + return Status::OK(); +} + +NGramHashMapping::NGramHashMapping(const OpKernelInfo& info) : WebGpuKernel(info) { + ORT_ENFORCE(info.GetAttr("max_ngram_size", &max_ngram_size_).IsOK(), + "max_ngram_size attribute is required"); + ORT_ENFORCE(info.GetAttr("n_head_per_ngram", &n_head_per_ngram_).IsOK(), + "n_head_per_ngram attribute is required"); + ORT_ENFORCE(info.GetAttr("pad_id", &pad_id_).IsOK(), "pad_id attribute is required"); + ORT_ENFORCE(max_ngram_size_ >= 2, "max_ngram_size must be at least 2"); + ORT_ENFORCE(n_head_per_ngram_ >= 1, "n_head_per_ngram must be positive"); + ORT_ENFORCE(pad_id_ >= std::numeric_limits::min() && pad_id_ <= std::numeric_limits::max(), + "WebGPU NGramHashMapping only supports int32 ids"); +} + +Status NGramHashMapping::ComputeInternal(ComputeContext& context) const { + const auto* input_ids = context.Input(0); + const auto* multipliers = context.Input(1); + const auto* vocab_sizes = context.Input(2); + const auto* past_ids = context.Input(3); + const auto& input_shape = input_ids->Shape(); + ORT_RETURN_IF_NOT(input_shape.NumDimensions() == 2, "input_ids must have rank 2"); + ORT_RETURN_IF_NOT(multipliers->Shape().NumDimensions() == 1 && multipliers->Shape()[0] == max_ngram_size_, + "multipliers must have shape (max_ngram_size)"); + const int64_t num_heads = (max_ngram_size_ - 1) * n_head_per_ngram_; + ORT_RETURN_IF_NOT(vocab_sizes->Shape() == TensorShape({num_heads}), + "vocab_sizes must have shape ((max_ngram_size - 1) * n_head_per_ngram)"); + const int64_t batch_size = input_shape[0]; + const int64_t sequence_length = input_shape[1]; + // An n-gram window reaches this many positions before the current token. + const int64_t state_length = max_ngram_size_ - 1; + if (past_ids != nullptr) { + ORT_RETURN_IF_NOT(past_ids->Shape() == TensorShape({batch_size, state_length}), + "past_ids must have shape (batch_size, max_ngram_size - 1)"); + } + const bool has_past_ids = past_ids != nullptr; + + auto* output = context.Output(0, TensorShape({batch_size, sequence_length, num_heads})); + auto* present_ids = context.Output(1, TensorShape({batch_size, state_length})); + + // The hash program reads past_ids and the present program writes present_ids, so when the caller + // aliases the two the hash program must be queued first. + const int64_t total = input_shape.Size(); + if (total > 0) { + NGramHashMappingProgram program{has_past_ids}; + program.CacheHint(has_past_ids) + .AddInputs({{input_ids, ProgramTensorMetadataDependency::None}, + {multipliers, ProgramTensorMetadataDependency::None}, + {vocab_sizes, ProgramTensorMetadataDependency::None}}); + if (has_past_ids) { + program.AddInput({past_ids, ProgramTensorMetadataDependency::None}); + } + program.AddOutput({output, ProgramTensorMetadataDependency::None}) + .SetDispatchGroupSize((onnxruntime::narrow(total) + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE) + .AddUniformVariables({{onnxruntime::narrow(total)}, + {onnxruntime::narrow(sequence_length)}, + {onnxruntime::narrow(max_ngram_size_)}, + {onnxruntime::narrow(n_head_per_ngram_)}, + {onnxruntime::narrow(pad_id_)}}); + ORT_RETURN_IF_ERROR(context.RunProgram(program)); + } + + if (present_ids != nullptr && batch_size * state_length > 0) { + // WebGPU rejects zero-sized storage buffer bindings, so an empty input_ids tensor must not be + // bound. When sequence_length == 0 every present slot comes from history (or pad_id), so the + // input_ids branch of the shader is dead anyway. + const bool has_input_ids = sequence_length > 0; + // WebGPU rejects a bind group that exposes one buffer as both read-only and read-write storage + // in the same compute pass, so an aliased past_ids must not be bound a second time. + const bool past_aliases_present = has_past_ids && past_ids->DataRaw() == present_ids->DataRaw(); + NGramPresentIdsProgram present_program{has_input_ids, has_past_ids, past_aliases_present}; + present_program.CacheHint(has_input_ids, has_past_ids, past_aliases_present); + if (has_input_ids) { + present_program.AddInput({input_ids, ProgramTensorMetadataDependency::None}); + } + if (has_past_ids && !past_aliases_present) { + present_program.AddInput({past_ids, ProgramTensorMetadataDependency::None}); + } + // One workgroup per batch row, so the shader can use a workgroup barrier to order its reads + // against its writes. + present_program.AddOutput({present_ids, ProgramTensorMetadataDependency::None}) + .SetDispatchGroupSize(onnxruntime::narrow(batch_size)) + .AddUniformVariables({{onnxruntime::narrow(batch_size)}, + {onnxruntime::narrow(sequence_length)}, + {onnxruntime::narrow(state_length)}, + {onnxruntime::narrow(pad_id_)}}); + ORT_RETURN_IF_ERROR(context.RunProgram(present_program)); + } + + return Status::OK(); +} + +} // namespace webgpu +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/webgpu/bert/ngram_hash_mapping.h b/onnxruntime/contrib_ops/webgpu/bert/ngram_hash_mapping.h new file mode 100644 index 0000000000000..b03c25cb1282c --- /dev/null +++ b/onnxruntime/contrib_ops/webgpu/bert/ngram_hash_mapping.h @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "core/providers/webgpu/program.h" +#include "core/providers/webgpu/webgpu_kernel.h" + +namespace onnxruntime { +namespace contrib { +namespace webgpu { + +using namespace onnxruntime::webgpu; +using onnxruntime::webgpu::ComputeContext; + +class NGramHashMappingProgram final : public Program { + public: + explicit NGramHashMappingProgram(bool has_past_ids) + : Program{"NGramHashMapping"}, has_past_ids_(has_past_ids) {} + Status GenerateShaderCode(ShaderHelper& shader) const override; + WEBGPU_PROGRAM_DEFINE_UNIFORM_VARIABLES({"total", ProgramUniformVariableDataType::Uint32}, + {"sequence_length", ProgramUniformVariableDataType::Uint32}, + {"max_ngram_size", ProgramUniformVariableDataType::Uint32}, + {"n_head_per_ngram", ProgramUniformVariableDataType::Uint32}, + {"pad_id", ProgramUniformVariableDataType::Int32}); + + private: + bool has_past_ids_; +}; + +// Emits the right-aligned trailing window of (past_ids ++ input_ids) so the next call can continue +// the n-gram windows across invocations. +class NGramPresentIdsProgram final : public Program { + public: + NGramPresentIdsProgram(bool has_input_ids, bool has_past_ids, bool past_aliases_present) + : Program{"NGramPresentIds"}, + has_input_ids_(has_input_ids), + has_past_ids_(has_past_ids), + past_aliases_present_(past_aliases_present) {} + Status GenerateShaderCode(ShaderHelper& shader) const override; + WEBGPU_PROGRAM_DEFINE_UNIFORM_VARIABLES({"batch_size", ProgramUniformVariableDataType::Uint32}, + {"sequence_length", ProgramUniformVariableDataType::Uint32}, + {"state_length", ProgramUniformVariableDataType::Uint32}, + {"pad_id", ProgramUniformVariableDataType::Int32}); + + private: + // False when sequence_length == 0. WebGPU cannot bind a zero-sized buffer, and in that case every + // present slot is history or pad_id, so the input_ids branch is omitted entirely. + bool has_input_ids_; + bool has_past_ids_; + // True when the caller threaded present_ids straight back into past_ids. WebGPU forbids binding + // one buffer as both read-only and read-write storage in a single compute pass, so the history is + // then read back through the present_ids (read_write) binding rather than a second binding. + bool past_aliases_present_; +}; + +class NGramHashMapping final : public WebGpuKernel { + public: + explicit NGramHashMapping(const OpKernelInfo& info); + Status ComputeInternal(ComputeContext& context) const override; + + private: + int64_t max_ngram_size_; + int64_t n_head_per_ngram_; + int64_t pad_id_; +}; + +} // namespace webgpu +} // namespace contrib +} // namespace onnxruntime diff --git a/onnxruntime/contrib_ops/webgpu/webgpu_contrib_kernels.cc b/onnxruntime/contrib_ops/webgpu/webgpu_contrib_kernels.cc index 6d1e283eae13d..d1d9c589727ec 100644 --- a/onnxruntime/contrib_ops/webgpu/webgpu_contrib_kernels.cc +++ b/onnxruntime/contrib_ops/webgpu/webgpu_contrib_kernels.cc @@ -3,6 +3,8 @@ #include "contrib_ops/webgpu/webgpu_contrib_kernels.h" #include "contrib_ops/webgpu/bert/causal_conv_with_state.h" +#include "contrib_ops/webgpu/bert/engram_gate.h" +#include "contrib_ops/webgpu/bert/ngram_hash_mapping.h" #include "contrib_ops/webgpu/bert/gated_add.h" #include "contrib_ops/webgpu/bert/group_query_attention.h" #include "contrib_ops/webgpu/bert/linear_attention.h" @@ -33,6 +35,8 @@ static const BuildKernelCreateInfoFn build_kernel_create_info_function_table[] = BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, + BuildKernelCreateInfo, + BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, diff --git a/onnxruntime/core/graph/contrib_ops/bert_defs.cc b/onnxruntime/core/graph/contrib_ops/bert_defs.cc index bd63ee4348836..f57ec84b0ad24 100644 --- a/onnxruntime/core/graph/contrib_ops/bert_defs.cc +++ b/onnxruntime/core/graph/contrib_ops/bert_defs.cc @@ -2571,8 +2571,211 @@ The ndim attribute generalizes the op to 1D, 2D, or 3D spatial dimensions. Causa enforced on the last spatial dimension only. The optional activation attribute supports fused SiLU/Swish activation. + +The dilation attribute spaces the kernel taps along the causal axis: output position t reads +input positions t - (k_1 - 1 - j) * dilation for tap j. The receptive field therefore spans +(k_1 - 1) * dilation positions before the current one, and the carry state grows to match: +past_state and present_state hold (k_1 - 1) * dilation positions instead of k_1 - 1. Dilation 1 +(the default) is the undilated case and keeps the original state length, so models exported +before the attribute existed are unaffected. + +The channels_last attribute selects a sequence-major layout for the activations and the carry +state, so a model that already produces channels-last activations does not have to transpose into +and out of the channels-first layout. With channels_last = 1 and ndim = 1, input and output are +(batch_size, sequence_length, d_1, ..., d_n) and the state tensors are +(batch_size, state_length, d_1, ..., d_n), where channels = d_1 * ... * d_n. Any number of trailing +channel axes is accepted, so an activation that keeps hyper-connections and hidden size as separate +axes needs no reshape either. weight and bias keep their channels-first (channels, 1, k_1) and +(channels) shapes because they have no sequence axis. The computed values are identical to the +channels-first layout; only the memory layout differs. +)DOC"; + +constexpr const char* NGramHashMapping_ver1_doc = R"DOC( +Computes Engram n-gram hash ids from pre-compressed tokenizer ids. + +For n in [2, max_ngram_size], the op creates causal shifts of input_ids, padding positions before the +sequence with pad_id, and computes +mix = shifted_0 * multipliers[0] xor ... xor shifted_(n-1) * multipliers[n-1]. +For every head of that n-gram order it emits mix modulo the corresponding head vocabulary size. +The output layout is (batch_size, sequence_length, (max_ngram_size - 1) * n_head_per_ngram), with +heads for n=2 first, then n=3, and so on. + +An n-gram window reaches max_ngram_size - 1 positions before the current token. To keep the op causal +across invocations (chunked prefill or autoregressive decode), the optional past_ids input carries +those preceding ids and present_ids returns the ids to pass to the next call. Both have shape +(batch_size, max_ngram_size - 1) and are right-aligned, so the last slot is the most recent id. +Positions before the start of the whole sequence use pad_id. Running the op once over a full sequence +and running it over consecutive chunks while threading present_ids into past_ids produce identical +hash ids. When past_ids is omitted the missing history is pad_id, which matches a fresh sequence. +past_ids and present_ids may use the same allocation. Such in-place execution is transaction-safe +only when the whole operator call is unconditionally committed; a caller that may select a prefix or +roll back must preserve past_ids. +)DOC"; + +ONNX_MS_OPERATOR_SET_SCHEMA( + NGramHashMapping, 1, + OpSchema() + .SetDoc(NGramHashMapping_ver1_doc) + .Attr("max_ngram_size", + "Maximum n-gram order. Must be at least 2.", + AttributeProto::INT) + .Attr("n_head_per_ngram", + "Number of hash heads emitted for each n-gram order.", + AttributeProto::INT) + .Attr("pad_id", + "Compressed tokenizer id used to pad causal shifts before the beginning of a sequence.", + AttributeProto::INT) + .Input(0, + "input_ids", + "Compressed tokenizer ids with shape (batch_size, sequence_length).", + "M") + .Input(1, + "multipliers", + "Per-shift hash multipliers with shape (max_ngram_size). Conventionally odd, but any " + "value is accepted.", + "M") + .Input(2, + "vocab_sizes", + "Per-output-head vocabulary sizes, conventionally prime, with shape " + "((max_ngram_size - 1) * n_head_per_ngram). Every entry must be strictly positive. " + "The CPU implementation rejects a non-positive entry; GPU implementations guard the " + "modulo to avoid a device-side division by zero and emit a hash id of 0 for that head.", + "M") + .Input(3, + "past_ids", + "Optional compressed tokenizer ids for the max_ngram_size - 1 positions that precede " + "this call, with shape (batch_size, max_ngram_size - 1). Right-aligned, so the last " + "slot is the most recent id. If omitted the history is pad_id.", + "M", + OpSchema::Optional) + .Output(0, + "hash_ids", + "Hash ids with shape (batch_size, sequence_length, " + "(max_ngram_size - 1) * n_head_per_ngram).", + "M") + .Output(1, + "present_ids", + "Trailing max_ngram_size - 1 ids of past_ids followed by input_ids, with shape " + "(batch_size, max_ngram_size - 1). Feed this back as past_ids on the next call.", + "M", + OpSchema::Optional) + .TypeConstraint("M", + {"tensor(int32)", "tensor(int64)"}, + "Constrain ids, multipliers, vocabulary sizes, and output ids to integer tensors.") + .TypeAndShapeInferenceFunction([](ONNX_NAMESPACE::InferenceContext& ctx) { + propagateElemTypeFromInputToOutput(ctx, 0, 0); + if (ctx.getNumOutputs() > 1) { + propagateElemTypeFromInputToOutput(ctx, 0, 1); + } + + const int64_t max_ngram_size = getAttribute(ctx, "max_ngram_size", int64_t{-1}); + const int64_t n_head_per_ngram = getAttribute(ctx, "n_head_per_ngram", int64_t{-1}); + if (max_ngram_size < 2) { + fail_shape_inference("NGramHashMapping: max_ngram_size must be at least 2"); + } + if (n_head_per_ngram < 1) { + fail_shape_inference("NGramHashMapping: n_head_per_ngram must be positive"); + } + + if (hasInputShape(ctx, 0)) { + const auto& input_shape = getInputShape(ctx, 0); + if (input_shape.dim_size() != 2) { + fail_shape_inference("NGramHashMapping: input_ids must have rank 2"); + } + TensorShapeProto output_shape; + *output_shape.add_dim() = input_shape.dim(0); + *output_shape.add_dim() = input_shape.dim(1); + output_shape.add_dim()->set_dim_value((max_ngram_size - 1) * n_head_per_ngram); + updateOutputShape(ctx, 0, output_shape); + + if (ctx.getNumOutputs() > 1) { + TensorShapeProto present_shape; + *present_shape.add_dim() = input_shape.dim(0); + present_shape.add_dim()->set_dim_value(max_ngram_size - 1); + updateOutputShape(ctx, 1, present_shape); + } + } + })); + +constexpr const char* EngramGate_ver1_doc = R"DOC( +Fuses the Engram gate. + +The op consumes already projected keys in (batch_size, sequence_length, hc_mult, hidden_size) layout, +the hidden-state queries in the same layout, an already projected value in +(batch_size, sequence_length, hidden_size) layout that is shared by every hyper-connection, and the two +RMSNorm scales. The key and value projections stay outside the op so they can run on the execution +provider's tuned MatMul (weight prepacking, tensor cores, quantized weights) and so the value +projection is computed once per token instead of once per hyper-connection. + +It computes the Engram gate: + +gate = sigmoid(sign(dot) * sqrt(max(abs(dot), 1e-6))) where +dot = sum(RMSNorm(key) * RMSNorm(query)) / sqrt(hidden_size). + +The output is gate * value, broadcast across the hyper-connections. The final Engram residual +value + short_conv(value) is then expressed with RMSNorm, CausalConvWithState and Add. )DOC"; +ONNX_MS_OPERATOR_SET_SCHEMA( + EngramGate, 1, + OpSchema() + .SetDoc(EngramGate_ver1_doc) + .Attr("epsilon", + "Epsilon used by both RMS normalization steps. Default is 1e-5.", + AttributeProto::FLOAT, + 1.0e-5f) + .Input(0, + "key", + "Projected Engram keys with shape (batch_size, sequence_length, hc_mult, hidden_size).", + "T") + .Input(1, + "query", + "Hidden-state queries with shape (batch_size, sequence_length, hc_mult, hidden_size).", + "T") + .Input(2, + "value", + "Projected Engram value shared by every hyper-connection, with shape " + "(batch_size, sequence_length, hidden_size).", + "T") + .Input(3, + "key_norm_scale", + "RMSNorm scale for keys with shape (hc_mult, hidden_size).", + "T") + .Input(4, + "query_norm_scale", + "RMSNorm scale for queries with shape (hc_mult, hidden_size).", + "T") + .Output(0, + "output", + "Gated value tensor with shape (batch_size, sequence_length, hc_mult, hidden_size).", + "T") + .TypeConstraint("T", + {"tensor(float)", "tensor(float16)", "tensor(bfloat16)"}, + "Constrain input and output types to float tensors.") + .TypeAndShapeInferenceFunction([](ONNX_NAMESPACE::InferenceContext& ctx) { + propagateElemTypeFromInputToOutput(ctx, 0, 0); + + if (hasInputShape(ctx, 0)) { + const auto& key_shape = getInputShape(ctx, 0); + if (key_shape.dim_size() != 4) { + fail_shape_inference("EngramGate: key must have rank 4"); + } + propagateShapeFromInputToOutput(ctx, 0, 0); + } + if (hasInputShape(ctx, 1)) { + const auto& query_shape = getInputShape(ctx, 1); + if (query_shape.dim_size() != 4) { + fail_shape_inference("EngramGate: query must have rank 4"); + } + } + if (hasInputShape(ctx, 2)) { + const auto& value_shape = getInputShape(ctx, 2); + if (value_shape.dim_size() != 3) { + fail_shape_inference("EngramGate: value must have rank 3"); + } + } + })); + ONNX_MS_OPERATOR_SET_SCHEMA( CausalConvWithState, 1, OpSchema() @@ -2586,15 +2789,31 @@ ONNX_MS_OPERATOR_SET_SCHEMA( "Spatial dimensionality: 1, 2, or 3. Default is 1.", AttributeProto::INT, static_cast(1)) + .Attr("dilation", + "Spacing between kernel taps along the causal (last spatial) axis. The receptive " + "field spans (k_1 - 1) * dilation positions before the current one, and past_state / " + "present_state hold that many positions. Must be >= 1. Default is 1 (undilated).", + AttributeProto::INT, + static_cast(1)) + .Attr("channels_last", + "When 1, input, output, past_state and present_state use a sequence-major, " + "channels-last layout: input and output are " + "(batch_size, sequence_length, d_1, ..., d_n) and the state tensors are " + "(batch_size, state_length, d_1, ..., d_n), where channels = d_1 * ... * d_n. " + "weight and bias keep their channels-first shapes. Requires ndim = 1. " + "Default is 0 (channels-first).", + AttributeProto::INT, + static_cast(0)) .Attr("state_window", "Number of trailing per-position carry states held by past_state and present_state. " "When 0 (default) the state tensors have no window axis and hold only the state after " - "the last position, i.e. the backward-compatible (batch_size, channels, k_1 - 1). " + "the last position, i.e. the backward-compatible (batch_size, channels, state_length) " + "where state_length = (k_1 - 1) * dilation. " "When W > 0 both gain a LEADING axis of extent W, right-aligned: slot j is the state " "after position (seq_len - W + j), so slot W-1 is always the state after the last " "position (identical to the W = 0 tensor) and is the slot past_state is read from. " "The window axis leads the batch axis so that each slot is one contiguous " - "(batch_size, channels, k_1 - 1) block. Slots below max(0, W - seq_len) hold no " + "(batch_size, channels, state_length) block. Slots below max(0, W - seq_len) hold no " "position from this call and are filled with zeros. A window lets a speculative " "decoder roll the state back to an accepted prefix without replaying the forward. " "Valid range is [0, 8].", @@ -2602,8 +2821,9 @@ ONNX_MS_OPERATOR_SET_SCHEMA( static_cast(0)) .Input(0, "input", - "Input tensor with shape (batch_size, channels, ...). Channels-first layout. " - "Spatial dims: 1D: (L,); 2D: (H, W); 3D: (D, H, W).", + "Input tensor with shape (batch_size, channels, ...) in the default channels-first " + "layout. Spatial dims: 1D: (L,); 2D: (H, W); 3D: (D, H, W). When channels_last = 1 " + "the shape is (batch_size, sequence_length, d_1, ..., d_n) instead.", "T") .Input(1, "weight", @@ -2617,9 +2837,11 @@ ONNX_MS_OPERATOR_SET_SCHEMA( OpSchema::Optional) .Input(3, "past_state", - "Carry state from previous step. For ndim=1: (batch_size, channels, k_1 - 1), or " - "(W, batch_size, channels, k_1 - 1) when state_window = W > 0, in which case only " - "slot W-1 is read. If not provided, padding is zero.", + "Carry state from previous step. For ndim=1: (batch_size, channels, state_length), " + "or (W, batch_size, channels, state_length) when state_window = W > 0, in which case " + "only slot W-1 is read, where state_length = (k_1 - 1) * dilation. When " + "channels_last = 1 each slot is (batch_size, state_length, d_1, ..., d_n) instead. " + "If not provided, padding is zero.", "T", OpSchema::Optional) .Output(0, @@ -2628,10 +2850,12 @@ ONNX_MS_OPERATOR_SET_SCHEMA( "T") .Output(1, "present_state", - "Updated carry state. For ndim=1: (batch_size, channels, k_1 - 1), or " - "(W, batch_size, channels, k_1 - 1) when state_window = W > 0. Slot W-1 contains " - "the last (k-1) values from the virtual input along the causal axis; slot j contains " - "the same for the prefix ending at position (seq_len - W + j).", + "Updated carry state. For ndim=1: (batch_size, channels, state_length), or " + "(W, batch_size, channels, state_length) when state_window = W > 0, and " + "(batch_size, state_length, d_1, ..., d_n) per slot when channels_last = 1. Slot " + "W-1 contains the last state_length values from the virtual input along the causal " + "axis; slot j contains the same for the prefix ending at position " + "(seq_len - W + j).", "T") .TypeConstraint("T", {"tensor(float)", "tensor(float16)", "tensor(bfloat16)"}, @@ -2646,6 +2870,20 @@ ONNX_MS_OPERATOR_SET_SCHEMA( "], got ", state_window); } + const int64_t dilation = getAttribute(ctx, "dilation", 1); + if (dilation < 1) { + fail_shape_inference("CausalConvWithState: dilation must be >= 1, got ", dilation); + } + + const int64_t channels_last = getAttribute(ctx, "channels_last", 0); + if (channels_last != 0 && channels_last != 1) { + fail_shape_inference("CausalConvWithState: channels_last must be 0 or 1, got ", + channels_last); + } + if (channels_last == 1 && getAttribute(ctx, "ndim", 1) != 1) { + fail_shape_inference("CausalConvWithState: channels_last requires ndim = 1"); + } + // Output 0: same shape as input (batch_size, channels, ...) propagateShapeFromInputToOutput(ctx, 0, 0); @@ -2663,6 +2901,31 @@ ONNX_MS_OPERATOR_SET_SCHEMA( fail_shape_inference("CausalConvWithState: weight must have rank >= 2"); } int64_t ndim = getAttribute(ctx, "ndim", 1); + // (kernel_size - 1) * dilation, or an unset dim when kernel_size is symbolic. + const int last_kernel_dim = weight_shape.dim_size() - 1; + TensorShapeProto::Dimension state_length; + if (weight_shape.dim(last_kernel_dim).has_dim_value()) { + state_length.set_dim_value((weight_shape.dim(last_kernel_dim).dim_value() - 1) * + dilation); + } + + if (channels_last == 1) { + // (batch_size, state_length, d_1, ..., d_n), optionally led by the window axis. + // The trailing channel axes are copied verbatim from the input, so a caller that + // keeps hyper-connections and hidden size separate gets the same split back. + TensorShapeProto cl_state_shape; + if (state_window > 0) { + cl_state_shape.add_dim()->set_dim_value(state_window); + } + *cl_state_shape.add_dim() = input_shape.dim(0); + *cl_state_shape.add_dim() = state_length; + for (int i = 2; i < input_shape.dim_size(); ++i) { + *cl_state_shape.add_dim() = input_shape.dim(i); + } + updateOutputShape(ctx, 1, cl_state_shape); + return; + } + // state_window = W > 0 prepends a window axis, holding the carry state after each of // the last W positions (slot W-1 == the W = 0 tensor). The window axis leads the batch // axis so a slot is one contiguous (batch_size, channels, ...) block. W = 0 keeps the @@ -2677,13 +2940,8 @@ ONNX_MS_OPERATOR_SET_SCHEMA( for (int64_t i = 0; i < ndim - 1; ++i) { *state_shape.add_dim() = input_shape.dim(static_cast(2 + i)); } - // Causal (last) spatial dim: kernel_size - 1 - int last_kernel_dim = weight_shape.dim_size() - 1; - if (weight_shape.dim(last_kernel_dim).has_dim_value()) { - state_shape.add_dim()->set_dim_value(weight_shape.dim(last_kernel_dim).dim_value() - 1); - } else { - state_shape.add_dim(); // unknown - } + // Causal (last) spatial dim: (kernel_size - 1) * dilation + *state_shape.add_dim() = state_length; updateOutputShape(ctx, 1, state_shape); } })); @@ -2698,7 +2956,8 @@ device-resident int32 tensor of shape (batch_size + 1); sequence i occupies at least one token. weight has shape (channels, 1, kernel_size), and optional bias has shape (channels). The convolution never reads across a sequence boundary. -initial_state is required and has shape (batch_size, channels, kernel_size - 1). It contains +initial_state is required and has shape (batch_size, channels, state_length), where +state_length = (kernel_size - 1) * dilation. It contains the committed raw activation samples immediately preceding this call. final_state has the same shape and type and is fully written with the state after each sequence's final token. State uses the activation type because it stores raw samples, not accumulated convolution values. @@ -2720,6 +2979,14 @@ Malformed offsets cause affected work to return without those accesses; outputs This device-side containment is not a synchronous validation or rejection mechanism. The optional activation attribute supports none, SiLU, and Swish. + +The dilation attribute spaces the kernel taps along the sequence axis: local token t of a request +reads that request's local positions t - (kernel_size - 1 - j) * dilation for tap j, and positions +before the request's first token come from the carry state. The carry state therefore holds +state_length = (kernel_size - 1) * dilation positions per request instead of kernel_size - 1. +Dilation 1 (the default) is the undilated case and keeps the original state length, so models +exported before the attribute existed are unaffected. input and output are already token-major +(sequence-major, channels-last), so this op needs no separate layout attribute. )DOC"; ONNX_MS_OPERATOR_SET_SCHEMA( @@ -2731,6 +2998,13 @@ ONNX_MS_OPERATOR_SET_SCHEMA( "Default is 'none'.", AttributeProto::STRING, std::string("none")) + .Attr("dilation", + "Spacing between kernel taps along the sequence axis. The receptive field spans " + "(kernel_size - 1) * dilation positions before the current token, and " + "initial_state / final_state hold that many positions per request. " + "Must be >= 1. Default is 1 (undilated).", + AttributeProto::INT, + static_cast(1)) .Attr("state_update_capacity", "Static number of compact contiguous-prefix transition values to expose per request. " "Valid range is [0, 8]. capture_count is required exactly when this is positive.", @@ -2759,7 +3033,7 @@ ONNX_MS_OPERATOR_SET_SCHEMA( .Input(4, "initial_state", "Required committed carry state with shape " - "(batch_size, channels, kernel_size - 1).", + "(batch_size, channels, (kernel_size - 1) * dilation).", "T") .Input(5, "capture_count", @@ -2775,7 +3049,7 @@ ONNX_MS_OPERATOR_SET_SCHEMA( .Output(1, "final_state", "Fully written state after each sequence's final token, with shape " - "(batch_size, channels, kernel_size - 1).", + "(batch_size, channels, (kernel_size - 1) * dilation).", "T") .Output(2, "state_update", @@ -2801,6 +3075,11 @@ ONNX_MS_OPERATOR_SET_SCHEMA( kMaxStateWindow, "], got ", state_update_capacity); } + const int64_t dilation = getAttribute(ctx, "dilation", 1); + if (dilation < 1) { + fail_shape_inference("VarlenCausalConvWithState: dilation must be >= 1, got ", dilation); + } + // Output 0: same shape as input (total_tokens, channels) propagateShapeFromInputToOutput(ctx, 0, 0); @@ -2854,9 +3133,9 @@ ONNX_MS_OPERATOR_SET_SCHEMA( } *state_shape.add_dim() = input_shape.dim(1); // channels if (weight_shape.dim(2).has_dim_value()) { - state_shape.add_dim()->set_dim_value(weight_shape.dim(2).dim_value() - 1); + state_shape.add_dim()->set_dim_value((weight_shape.dim(2).dim_value() - 1) * dilation); } else { - state_shape.add_dim(); // unknown kernel_size - 1 + state_shape.add_dim(); // unknown (kernel_size - 1) * dilation } updateOutputShape(ctx, 1, state_shape); diff --git a/onnxruntime/core/graph/contrib_ops/ms_opset.h b/onnxruntime/core/graph/contrib_ops/ms_opset.h index 57c4b5af588ad..50e421b90125e 100644 --- a/onnxruntime/core/graph/contrib_ops/ms_opset.h +++ b/onnxruntime/core/graph/contrib_ops/ms_opset.h @@ -93,6 +93,8 @@ class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, LinearAttentionGate); class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, GatedDeltaNet); class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, GatedRMSNorm); class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, GatedAdd); +class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, NGramHashMapping); +class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, EngramGate); class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, CausalConvWithState); class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, VarlenCausalConvWithState); class ONNX_OPERATOR_SET_SCHEMA_CLASS_NAME(Microsoft, 1, MurmurHash3); @@ -214,6 +216,8 @@ class OpSet_Microsoft_ver1 { fn(GetOpSchema()); fn(GetOpSchema()); fn(GetOpSchema()); + fn(GetOpSchema()); + fn(GetOpSchema()); fn(GetOpSchema()); fn(GetOpSchema()); fn(GetOpSchema()); diff --git a/onnxruntime/core/providers/cpu/controlflow/scan_8.cc b/onnxruntime/core/providers/cpu/controlflow/scan_8.cc index cea3217578129..ac33abc74d672 100644 --- a/onnxruntime/core/providers/cpu/controlflow/scan_8.cc +++ b/onnxruntime/core/providers/cpu/controlflow/scan_8.cc @@ -140,6 +140,13 @@ void Scan<8>::Init(const OpKernelInfo& info) { ORT_ENFORCE(info.GetAttr("num_scan_inputs", &num_scan_inputs_).IsOK()); + // Validate 'num_scan_inputs' before it is used below to size 'directions', so an out-of-range + // attribute value (from an untrusted model) can't reach a narrowing cast or vector allocation + // sized from an attacker-controlled count. + const int64_t num_variadic_inputs = static_cast(info.GetInputCount()) - 1; // exclude sequence_lens + scan::detail::ValidateNumScanInputs(num_scan_inputs_, num_variadic_inputs, + static_cast(info.GetOutputCount())); + ReadDirections(info, "directions", input_directions_, onnxruntime::narrow(num_scan_inputs_)); device_helpers_.transpose_func = [](const gsl::span&, const Tensor&, Tensor&, Stream*) -> Status { @@ -160,8 +167,11 @@ Status Scan<8>::SetupSubgraphExecutionInfo(const SessionState& session_state, ORT_UNUSED_PARAMETER(attribute_name); const auto& node = Node(); + // 'num_scan_inputs_' was already validated in Init(); narrow (rather than static_cast) is + // an inexpensive extra guard so an out-of-int-range value still fails predictably here instead + // of silently wrapping, even if that earlier validation is ever bypassed or refactored away. info_ = std::make_unique::Info>(node, subgraph_session_state.GetGraphViewer(), - static_cast(num_scan_inputs_)); + onnxruntime::narrow(num_scan_inputs_)); auto status = scan::detail::CreateFeedsFetchesManager(node, *info_, session_state, subgraph_session_state, /* is_v8 */ true, feeds_fetches_manager_); diff --git a/onnxruntime/core/providers/cpu/controlflow/scan_9.cc b/onnxruntime/core/providers/cpu/controlflow/scan_9.cc index 2fdd7fc564fe4..3ad9cd2ceeb73 100644 --- a/onnxruntime/core/providers/cpu/controlflow/scan_9.cc +++ b/onnxruntime/core/providers/cpu/controlflow/scan_9.cc @@ -164,6 +164,12 @@ void Scan<9>::Init(const OpKernelInfo& info) { ORT_ENFORCE(info.GetAttr("num_scan_inputs", &num_scan_inputs_).IsOK()); + // Validate 'num_scan_inputs' before it is used below to derive num_loop_state_vars/num_scan_outputs + // and size the directions/axes vectors, so an out-of-range attribute value (from an untrusted + // model) can't underflow those derived counts or reach a vector allocation sized from them. + scan::detail::ValidateNumScanInputs(num_scan_inputs_, static_cast(info.GetInputCount()), + static_cast(info.GetOutputCount())); + auto num_loop_state_vars = info.GetInputCount() - num_scan_inputs_; auto num_scan_outputs = info.GetOutputCount() - num_loop_state_vars; @@ -204,8 +210,11 @@ Status Scan<9>::SetupSubgraphExecutionInfo(const SessionState& session_state, ORT_UNUSED_PARAMETER(attribute_name); const auto& node = Node(); + // 'num_scan_inputs_' was already validated in Init(); narrow (rather than static_cast) is + // an inexpensive extra guard so an out-of-int-range value still fails predictably here instead + // of silently wrapping, even if that earlier validation is ever bypassed or refactored away. info_ = std::make_unique::Info>(node, subgraph_session_state.GetGraphViewer(), - static_cast(num_scan_inputs_)); + onnxruntime::narrow(num_scan_inputs_)); auto status = scan::detail::CreateFeedsFetchesManager(node, *info_, session_state, subgraph_session_state, /* is_v8 */ false, feeds_fetches_manager_); diff --git a/onnxruntime/core/providers/cpu/controlflow/scan_utils.cc b/onnxruntime/core/providers/cpu/controlflow/scan_utils.cc index 0909082edb96e..4a30351696520 100644 --- a/onnxruntime/core/providers/cpu/controlflow/scan_utils.cc +++ b/onnxruntime/core/providers/cpu/controlflow/scan_utils.cc @@ -31,13 +31,30 @@ namespace onnxruntime { namespace scan { namespace detail { +void ValidateNumScanInputs(int64_t num_scan_inputs, int64_t num_variadic_inputs, int64_t num_outputs) { + // The ONNX Scan spec requires one or more scan_input tensors, so 'num_scan_inputs' must be at + // least 1 (as well as no more than the number of variadic inputs), otherwise the derived loop + // state variable count below could go out of the valid range and later indexing built on top of + // it would operate on invalid indices. + ORT_ENFORCE(num_scan_inputs >= 1 && num_scan_inputs <= num_variadic_inputs, + "Invalid 'num_scan_inputs' of ", num_scan_inputs, ". Value must be between 1 and ", + num_variadic_inputs, " (the number of variadic inputs) inclusive."); + + const int64_t num_loop_state_variables = num_variadic_inputs - num_scan_inputs; + ORT_ENFORCE(num_loop_state_variables <= num_outputs, + "Scan has ", num_outputs, " output(s), which is fewer than the ", num_loop_state_variables, + " loop state variable(s) implied by a 'num_scan_inputs' of ", num_scan_inputs, "."); +} + Info::Info(const Node& node, const GraphViewer& subgraph_in, int num_scan_inputs_in, bool is_v8) : subgraph(subgraph_in), num_scan_inputs(num_scan_inputs_in) { num_inputs = static_cast(node.InputDefs().size()); num_variadic_inputs = is_v8 ? num_inputs - 1 : num_inputs; // allow for sequence_lens input in v8 - num_loop_state_variables = num_variadic_inputs - num_scan_inputs; - num_outputs = static_cast(node.OutputDefs().size()); + + ValidateNumScanInputs(num_scan_inputs, num_variadic_inputs, num_outputs); + + num_loop_state_variables = num_variadic_inputs - num_scan_inputs; num_scan_outputs = num_outputs - num_loop_state_variables; num_implicit_inputs = static_cast(node.ImplicitInputDefs().size()); diff --git a/onnxruntime/core/providers/cpu/controlflow/scan_utils.h b/onnxruntime/core/providers/cpu/controlflow/scan_utils.h index 38def699744c2..4e8be1610fbda 100644 --- a/onnxruntime/core/providers/cpu/controlflow/scan_utils.h +++ b/onnxruntime/core/providers/cpu/controlflow/scan_utils.h @@ -168,6 +168,13 @@ class OutputIterator { void ReadDirections(const OpKernelInfo& info, const std::string& attr_name, TensorShapeVector& directions, size_t num_entries); +// Validates the node's 'num_scan_inputs' attribute against the actual number of variadic inputs +// and outputs before it is used to derive the loop state variable and scan output counts, so a +// value outside the valid range can't produce a negative count that later code uses as an index +// or size without further checks. Called both at kernel construction time, before the attribute +// value is used to size any directions/axes vectors, and again when building Info. +void ValidateNumScanInputs(int64_t num_scan_inputs, int64_t num_variadic_inputs, int64_t num_outputs); + Status AllocateOutput(OpKernelContextInternal& context, const GraphViewer& subgraph, int output_index, bool is_loop_state_var, int64_t batch_size, int64_t sequence_len, std::unique_ptr& output_iterator, diff --git a/onnxruntime/core/providers/cuda/math/cumsum.cc b/onnxruntime/core/providers/cuda/math/cumsum.cc index a7b3a19ff85e2..3971056d16f05 100644 --- a/onnxruntime/core/providers/cuda/math/cumsum.cc +++ b/onnxruntime/core/providers/cuda/math/cumsum.cc @@ -129,13 +129,16 @@ Status CumSum::ComputeInternal(OpKernelContext* ctx) const { exclusive_, reverse_); } else if (input->IsDataType()) { - CumSumImpl(Stream(ctx), reinterpret_cast::MappedType*>(input->Data()), - fast_divmod_input_dim_along_axis, - fast_divmod_input_stride_along_axis, - reinterpret_cast::MappedType*>(output.MutableData()), - output_shape.Size(), - exclusive_, - reverse_); + ORT_RETURN_IF_ERROR(CumSumInt64Impl( + Stream(ctx), + reinterpret_cast::MappedType*>(input->Data()), + fast_divmod_input_dim_along_axis, + fast_divmod_input_stride_along_axis, + reinterpret_cast::MappedType*>(output.MutableData()), + output_shape.Size(), + exclusive_, + reverse_, + GetDeviceProp().multiProcessorCount)); } else if (input->IsDataType()) { CumSumImpl(Stream(ctx), reinterpret_cast::MappedType*>(input->Data()), fast_divmod_input_dim_along_axis, diff --git a/onnxruntime/core/providers/cuda/math/cumsum_impl.cu b/onnxruntime/core/providers/cuda/math/cumsum_impl.cu index ad530a4a6dfd8..5d9753c332fc3 100644 --- a/onnxruntime/core/providers/cuda/math/cumsum_impl.cu +++ b/onnxruntime/core/providers/cuda/math/cumsum_impl.cu @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "core/providers/cuda/cu_inc/common.cuh" +#include "core/providers/cuda/cu_inc/cub.cuh" #include "core/providers/cuda/shared_inc/fast_divmod.h" #include "cumsum_impl.h" @@ -9,6 +10,9 @@ namespace onnxruntime { namespace cuda { +constexpr int kCumSumBlockSize = 256; +constexpr int kCumSumBlockMinWidth = 4; + template __global__ void _CumSumKernel( const T* input_data, @@ -68,6 +72,52 @@ __global__ void _CumSumKernel( output_data[indices_index] = sum; } +template +__global__ void _CumSumInt64BlockKernel( + const int64_t* input_data, + int64_t* output_data, + const int width, + const int inner, + const bool exclusive, + const bool reverse) { + using BlockScan = cub::BlockScan; + __shared__ typename BlockScan::TempStorage temp_storage; + __shared__ uint64_t running_total; + + const int64_t lane = blockIdx.x; + const int64_t outer = lane / inner; + const int64_t inner_index = lane % inner; + const int tid = threadIdx.x; + + if (tid == 0) { + running_total = 0; + } + __syncthreads(); + + for (int64_t tile = 0; tile < width; tile += BlockSize) { + const int64_t axis_offset = tile + tid; + const bool is_valid = axis_offset < width; + const int64_t axis_index = reverse ? width - 1 - axis_offset : axis_offset; + const int64_t input_index = + is_valid ? (outer * width + axis_index) * inner + inner_index : 0; + const uint64_t value = is_valid ? static_cast(input_data[input_index]) : 0; + + uint64_t prefix = 0; + uint64_t aggregate = 0; + BlockScan(temp_storage).InclusiveSum(value, prefix, aggregate); + + if (is_valid) { + reinterpret_cast(output_data)[input_index] = + running_total + prefix - (exclusive ? value : 0); + } + __syncthreads(); + if (tid == 0) { + running_total += aggregate; + } + __syncthreads(); + } +} + template void CumSumImpl( cudaStream_t stream, @@ -91,6 +141,48 @@ void CumSumImpl( } } +Status CumSumInt64Impl( + cudaStream_t stream, + const int64_t* input_data, + const fast_divmod& input_dim_along_axis, + const fast_divmod& input_stride_along_axis, + int64_t* output_data, + int64_t output_size, + bool exclusive, + bool reverse, + int multiprocessor_count) { + if (output_size <= 0) { + return Status::OK(); + } + + const int width = input_dim_along_axis.d_; + ORT_RETURN_IF_NOT(width > 0, "CumSum scan axis must have positive length when output is non-empty."); + const int64_t lanes = output_size / width; + + // The generic kernel recomputes every prefix independently. For low-lane scans, use one + // cooperative block per lane to perform linear rather than quadratic work along the axis. + if (width >= kCumSumBlockMinWidth && lanes <= multiprocessor_count) { + _CumSumInt64BlockKernel<<(lanes), kCumSumBlockSize, 0, stream>>>( + input_data, + output_data, + width, + input_stride_along_axis.d_, + exclusive, + reverse); + return CUDA_CALL(cudaGetLastError()); + } + + CumSumImpl(stream, + input_data, + input_dim_along_axis, + input_stride_along_axis, + output_data, + output_size, + exclusive, + reverse); + return CUDA_CALL(cudaGetLastError()); +} + template void CumSumImpl( cudaStream_t stream, const int32_t* input_data, diff --git a/onnxruntime/core/providers/cuda/math/cumsum_impl.h b/onnxruntime/core/providers/cuda/math/cumsum_impl.h index ad77f748b0d2c..9f99093ac0c77 100644 --- a/onnxruntime/core/providers/cuda/math/cumsum_impl.h +++ b/onnxruntime/core/providers/cuda/math/cumsum_impl.h @@ -20,5 +20,16 @@ void CumSumImpl( bool exclusive, bool reverse); +Status CumSumInt64Impl( + cudaStream_t stream, + const int64_t* input_data, + const fast_divmod& input_dim_along_axis, + const fast_divmod& input_stride_along_axis, + int64_t* output_data, + int64_t output_size, + bool exclusive, + bool reverse, + int multiprocessor_count); + } // namespace cuda } // namespace onnxruntime diff --git a/onnxruntime/core/providers/webgpu/nn/conv.cc b/onnxruntime/core/providers/webgpu/nn/conv.cc index 64b380043e730..97c43bb1591ea 100644 --- a/onnxruntime/core/providers/webgpu/nn/conv.cc +++ b/onnxruntime/core/providers/webgpu/nn/conv.cc @@ -164,12 +164,13 @@ Status Conv::ComputeInternal(ComputeContext& context if (CanApplyIm2ColMatMulProgram(context, is_channels_last, - activation_.activation_kind_ != ActivationKind::None, + activation_, kernel_shape, onnxruntime::narrow(conv_attrs_.group), kernel->DataType())) { return ApplyIm2ColMatMulProgram(context, is_channels_last, + activation_, dilations, pads, strides, @@ -340,7 +341,7 @@ Status Conv::PrePackInternal(ComputeContextBase& con // Im2ColMatMul path uses a different transpose (OIHW -> OHWI) and reads // kernel directly from context.Input(1), ignoring prepacked weights. // Skip prepacking when this path will be used at runtime. - if (CanApplyIm2ColMatMulProgram(context, is_channels_last, activation_.activation_kind_ != ActivationKind::None, + if (CanApplyIm2ColMatMulProgram(context, is_channels_last, activation_, kernel_shape, onnxruntime::narrow(conv_attrs_.group), tensor.DataType())) { return Status::OK(); diff --git a/onnxruntime/core/providers/webgpu/nn/fuse_utils.h b/onnxruntime/core/providers/webgpu/nn/fuse_utils.h index 4426ef44dcb54..629239220029c 100644 --- a/onnxruntime/core/providers/webgpu/nn/fuse_utils.h +++ b/onnxruntime/core/providers/webgpu/nn/fuse_utils.h @@ -14,6 +14,7 @@ class OpKernelInfo; namespace webgpu { +// Values are mirrored by im2col_matmul.wgsl.template; append without reordering. enum class ActivationKind { None, Relu, diff --git a/onnxruntime/core/providers/webgpu/nn/im2col_matmul.cc b/onnxruntime/core/providers/webgpu/nn/im2col_matmul.cc index 8c33539253055..1e0ba1a2a41b7 100644 --- a/onnxruntime/core/providers/webgpu/nn/im2col_matmul.cc +++ b/onnxruntime/core/providers/webgpu/nn/im2col_matmul.cc @@ -54,8 +54,33 @@ bool IsDeviceSupported(const ComputeContextBase& context) { return false; } +// Keep this list synchronized with the activation_kind branches in the WGSL template. +bool IsActivationSupported(const Activation& activation) { + switch (activation.activation_kind_) { + case ActivationKind::None: + case ActivationKind::Relu: + case ActivationKind::Sigmoid: + case ActivationKind::Clip: + case ActivationKind::HardSigmoid: + case ActivationKind::LeakyRelu: + case ActivationKind::Tanh: + return true; + default: + return false; + } +} + } // namespace +// The template dispatches on the numeric enum values. +static_assert(static_cast(ActivationKind::None) == 0, "im2col_matmul.wgsl.template mirrors ActivationKind"); +static_assert(static_cast(ActivationKind::Relu) == 1, "im2col_matmul.wgsl.template mirrors ActivationKind"); +static_assert(static_cast(ActivationKind::Sigmoid) == 2, "im2col_matmul.wgsl.template mirrors ActivationKind"); +static_assert(static_cast(ActivationKind::Clip) == 3, "im2col_matmul.wgsl.template mirrors ActivationKind"); +static_assert(static_cast(ActivationKind::HardSigmoid) == 4, "im2col_matmul.wgsl.template mirrors ActivationKind"); +static_assert(static_cast(ActivationKind::LeakyRelu) == 5, "im2col_matmul.wgsl.template mirrors ActivationKind"); +static_assert(static_cast(ActivationKind::Tanh) == 6, "im2col_matmul.wgsl.template mirrors ActivationKind"); + Status Im2ColMatMulProgram::GenerateShaderCode(ShaderHelper& shader) const { const auto& src = shader.AddInput("src", ShaderUsage::UseValueTypeAlias | ShaderUsage::UseElementTypeAlias); const auto& weight = shader.AddInput("weight", ShaderUsage::UseValueTypeAlias | ShaderUsage::UseElementTypeAlias); @@ -69,6 +94,7 @@ Status Im2ColMatMulProgram::GenerateShaderCode(ShaderHelper& shader) const { ORT_ENFORCE(vec_size_ == 1 || vec_size_ == 2 || vec_size_ == 4, "vec_size must be 1, 2 or 4."); return WGSL_TEMPLATE_APPLY(shader, "nn/im2col_matmul.wgsl.template", + WGSL_TEMPLATE_PARAMETER(activation_kind, static_cast(activation_kind_)), WGSL_TEMPLATE_PARAMETER(has_bias, has_bias_), WGSL_TEMPLATE_PARAMETER(tile_m, tile_m_), WGSL_TEMPLATE_PARAMETER(tile_n, tile_n_), @@ -81,6 +107,7 @@ Status Im2ColMatMulProgram::GenerateShaderCode(ShaderHelper& shader) const { Status ApplyIm2ColMatMulProgram(ComputeContext& context, bool is_channels_last, + const Activation& activation, const std::vector& dilations, const std::vector& pads, const std::vector& strides, @@ -125,7 +152,8 @@ Status ApplyIm2ColMatMulProgram(ComputeContext& context, // If the status of this condition is uncertain, the feature must be disabled. const bool use_subgroup = false; const uint32_t vec_size = channel_input % 4 == 0 ? 4 : (channel_input % 2 == 0 ? 2 : 1); - Im2ColMatMulProgram im2col_mm_program{has_bias, tile_m, tile_n, vec_size, use_subgroup}; + Im2ColMatMulProgram im2col_mm_program{has_bias, tile_m, tile_n, vec_size, use_subgroup, + activation.activation_kind_}; im2col_mm_program.SetWorkgroupSize(workgroup_size); const uint32_t M_tiles = CeilDiv(im2col_m, tile_m); @@ -161,14 +189,15 @@ Status ApplyIm2ColMatMulProgram(ComputeContext& context, {dilations}, {pads}, {strides}}); - im2col_mm_program.CacheHint(has_bias, tile_m, tile_n, vec_size, use_subgroup); + AppendActivationUniformsData(activation, im2col_mm_program); + im2col_mm_program.CacheHint(has_bias, tile_m, tile_n, vec_size, use_subgroup, activation.CacheKey()); return context.RunProgram(im2col_mm_program); } bool CanApplyIm2ColMatMulProgram(ComputeContextBase& context, const bool is_channels_last, - const bool is_fused, + const Activation& activation, const TensorShape weight_shape, const uint32_t group, const MLDataType data_type) { @@ -183,9 +212,12 @@ bool CanApplyIm2ColMatMulProgram(ComputeContextBase& context, } // TODO: Support !is_channels_last - // TODO: Support fuse // TODO: Support group conv - if (!is_channels_last || is_fused || group != 1) { + if (!is_channels_last || group != 1) { + return false; + } + + if (!IsActivationSupported(activation)) { return false; } diff --git a/onnxruntime/core/providers/webgpu/nn/im2col_matmul.h b/onnxruntime/core/providers/webgpu/nn/im2col_matmul.h index de46689bda921..25206d071585e 100644 --- a/onnxruntime/core/providers/webgpu/nn/im2col_matmul.h +++ b/onnxruntime/core/providers/webgpu/nn/im2col_matmul.h @@ -24,12 +24,14 @@ class Im2ColMatMulProgram final : public Program { uint32_t tile_m, uint32_t tile_n, uint32_t vec_size, - bool use_subgroup) : Program("Im2ColMatMul"), - has_bias_(has_bias), - tile_m_(tile_m), - tile_n_(tile_n), - vec_size_(vec_size), - use_subgroup_(use_subgroup) {} + bool use_subgroup, + ActivationKind activation_kind) : Program("Im2ColMatMul"), + has_bias_(has_bias), + tile_m_(tile_m), + tile_n_(tile_n), + vec_size_(vec_size), + use_subgroup_(use_subgroup), + activation_kind_(activation_kind) {} Status GenerateShaderCode(ShaderHelper& shader) const override; @@ -50,7 +52,8 @@ class Im2ColMatMulProgram final : public Program { {"K_tiles", ProgramUniformVariableDataType::Uint32}, {"dilations", ProgramUniformVariableDataType::Uint32}, {"pads", ProgramUniformVariableDataType::Uint32}, - {"strides", ProgramUniformVariableDataType::Uint32}); + {"strides", ProgramUniformVariableDataType::Uint32}, + WEBGPU_PROGRAM_ACTIVATION_UNIFORM_VARIABLES); private: bool has_bias_; @@ -59,17 +62,19 @@ class Im2ColMatMulProgram final : public Program { uint32_t tile_n_; uint32_t vec_size_; bool use_subgroup_; + ActivationKind activation_kind_; }; bool CanApplyIm2ColMatMulProgram(ComputeContextBase& context, const bool is_channels_last, - const bool is_fused, + const Activation& activation, const TensorShape kernel_shape, const uint32_t group, const MLDataType data_type); Status ApplyIm2ColMatMulProgram(ComputeContext& context, const bool is_channels_last, + const Activation& activation, const std::vector& dilations, const std::vector& pads, const std::vector& strides, diff --git a/onnxruntime/core/providers/webgpu/nn/im2col_matmul.wgsl.template b/onnxruntime/core/providers/webgpu/nn/im2col_matmul.wgsl.template index d11e413ac4b16..429d6aea8b612 100644 --- a/onnxruntime/core/providers/webgpu/nn/im2col_matmul.wgsl.template +++ b/onnxruntime/core/providers/webgpu/nn/im2col_matmul.wgsl.template @@ -6,6 +6,10 @@ #param tile_n #param use_subgroup #param vec_size +// Mirrors ActivationKind; static_asserts in im2col_matmul.cc enforce these values. +// 0=None, 1=Relu, 2=Sigmoid, 3=Clip, 4=HardSigmoid, 5=LeakyRelu, 6=Tanh. +// Keep branches synchronized with IsActivationSupported(). +#param activation_kind #use .getByOffset .setByOffset @@ -142,6 +146,19 @@ $MAIN { let bias = load_bias(n_base); for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { var output_data = results[m_idx] + bias; +#if activation_kind == 1 + output_data = max(output_data, output_element_t(0)); +#elif activation_kind == 2 + output_data = output_element_t(1) / (output_element_t(1) + exp(-output_data)); +#elif activation_kind == 3 + output_data = clamp(output_data, output_element_t(uniforms.activation_param_0), output_element_t(uniforms.activation_param_1)); +#elif activation_kind == 4 + output_data = clamp(output_element_t(uniforms.activation_param_0) * output_data + output_element_t(uniforms.activation_param_1), output_element_t(0), output_element_t(1)); +#elif activation_kind == 5 + output_data = select(output_element_t(uniforms.activation_param_0) * output_data, output_data, output_data >= output_element_t(0)); +#elif activation_kind == 6 + output_data = tanh(output_data); +#endif write_output(batch, m_base + m_idx, n_base, output_data); } } // MAIN diff --git a/onnxruntime/python/onnxruntime_inference_collection.py b/onnxruntime/python/onnxruntime_inference_collection.py index 70f699768e92c..4a4a32b08b82b 100644 --- a/onnxruntime/python/onnxruntime_inference_collection.py +++ b/onnxruntime/python/onnxruntime_inference_collection.py @@ -9,6 +9,7 @@ import os import typing import warnings +import weakref from collections.abc import Callable, Sequence from enum import IntEnum from typing import Any @@ -60,10 +61,53 @@ def get_vendor_id_for_device_type(device_type: str) -> OrtDeviceVendorId | None: return OrtDeviceVendorId.MICROSOFT elif device_type == "cann": return OrtDeviceVendorId.HUAWEI + elif device_type == "webgpu": + return OrtDeviceVendorId.NONE else: return None +_GPU_GRAPH_ID_RUN_CONFIG_KEY = "gpu_graph_id" +# Mirrors InferenceSession::kGraphAnnotationSkip; core skips capture and replay for this ID. +_GRAPH_ANNOTATION_SKIP = -1 + + +def _graph_annotation_id(run_options) -> int: + """Return the effective ``gpu_graph_id`` for a run, defaulting to 0 when unset.""" + if run_options is None: + return 0 + try: + entry = run_options.get_run_config_entry(_GPU_GRAPH_ID_RUN_CONFIG_KEY) + except RuntimeError: + return 0 + if not entry: + return 0 + try: + return int(entry) + except ValueError: + raise ValueError(f"Run option '{_GPU_GRAPH_ID_RUN_CONFIG_KEY}' must be an integer, got {entry!r}.") from None + + +def _is_ortvalue_session_compatible(ortvalue, target_session) -> bool: + """Whether ``target_session`` can share ``ortvalue``'s buffer. + + Sessionless values come from a shared allocator. A session-owned WebGPU buffer is + usable by any session on the same WebGPU context. + """ + if ortvalue._session is None or target_session is None or ortvalue._session is target_session: + return True + return ortvalue._is_webgpu_buffer and ortvalue._session.webgpu_context_id() == target_session.webgpu_context_id() + + +def _validate_ortvalue_session_compatibility(ortvalue, target_session, action) -> None: + """Reject an OrtValue that the target session cannot share buffers with. + + ``action`` is the verb used in the message, e.g. ``"used with"`` or ``"bound to"``. + """ + if not _is_ortvalue_session_compatible(ortvalue, target_session): + raise ValueError(f"Session-scoped OrtValue must be {action} the session that created it.") + + class AdapterFormat: """ This class is used to create adapter files from python structures @@ -208,6 +252,8 @@ def __init__(self, enable_fallback: bool = True): # self._sess is managed by the derived class and relies on bindings from C.InferenceSession self._sess = None self._enable_fallback = enable_fallback + # Captured graphs retain buffer signatures without extending the IOBinding lifetime. + self._captured_graph_bindings: dict[int, tuple[weakref.ref, tuple]] = {} def get_session_options(self) -> C.SessionOptions: "Return the session options. See :class:`onnxruntime.SessionOptions`." @@ -305,10 +351,31 @@ def _validate_input(self, feed_input_names): f"Required inputs ({missing_input_names}) are missing from input feed ({feed_input_names})." ) + def _validate_ortvalue_ownership(self, values): + for value in values: + if not isinstance(value, OrtValue): + continue + _validate_ortvalue_session_compatibility(value, self._sess, "used with") + + def _validate_graph_capture_run_api(self, run_options=None): + if not self._sess.is_webgpu_graph_capture_enabled(): + return + if _graph_annotation_id(run_options) == _GRAPH_ANNOTATION_SKIP: + # gpu_graph_id=-1 skips capture, so transient feeds remain valid. + return + raise ValueError( + "WebGPU graph capture requires fixed device OrtValues and run_with_iobinding. " + f"Set the '{_GPU_GRAPH_ID_RUN_CONFIG_KEY}' run option to " + f"{_GRAPH_ANNOTATION_SKIP} to opt a single run out of capture." + ) + def run(self, output_names, input_feed, run_options=None) -> Sequence[np.ndarray | SparseTensor | list | dict]: """ Compute the predictions. + WebGPU graph capture requires fixed device OrtValues bound with + :meth:`run_with_iobinding`; do not use this convenience API for captured replay. + :param output_names: name of the outputs :param input_feed: dictionary ``{ input_name: input_value }`` :param run_options: See :class:`onnxruntime.RunOptions`. @@ -319,7 +386,9 @@ def run(self, output_names, input_feed, run_options=None) -> Sequence[np.ndarray sess.run([output_name], {input_name: x}) """ + self._validate_graph_capture_run_api(run_options) self._validate_input(list(input_feed.keys())) + self._validate_ortvalue_ownership(input_feed.values()) if not output_names: output_names = [output.name for output in self._outputs_meta] try: @@ -331,6 +400,7 @@ def run(self, output_names, input_feed, run_options=None) -> Sequence[np.ndarray self.set_providers(self._fallback_providers) # Fallback only once. self.disable_fallback() + self._validate_ortvalue_ownership(input_feed.values()) return self._sess.run(output_names, input_feed, run_options) raise @@ -359,7 +429,9 @@ def callback(results: np.ndarray, user_data: MyData, err: str) -> None: sess.run_async([output_name], {input_name: x}, callback) """ + self._validate_graph_capture_run_api(run_options) self._validate_input(list(input_feed.keys())) + self._validate_ortvalue_ownership(input_feed.values()) if not output_names: output_names = [output.name for output in self._outputs_meta] return self._sess.run_async(output_names, input_feed, callback, user_data, run_options) @@ -390,7 +462,9 @@ def invoke(sess, output_names, input_dict_ort_values, run_options): ort_values = [OrtValue(v) for v in result] return ort_values + self._validate_graph_capture_run_api(run_options) self._validate_input(list(input_dict_ort_values.keys())) + self._validate_ortvalue_ownership(input_dict_ort_values.values()) if not output_names: output_names = [output.name for output in self._outputs_meta] try: @@ -402,6 +476,7 @@ def invoke(sess, output_names, input_dict_ort_values, run_options): self.set_providers(self._fallback_providers) # Fallback only once. self.disable_fallback() + self._validate_ortvalue_ownership(input_dict_ort_values.values()) return invoke(self._sess, output_names, input_dict_ort_values, run_options) raise @@ -427,15 +502,85 @@ def io_binding(self) -> IOBinding: "Return an onnxruntime.IOBinding object`." return IOBinding(self) + def create_ortvalue_from_shape_and_type( + self, + shape: Sequence[int], + element_type, + device_type: str, + device_id: int = 0, + vendor_id: int | OrtDeviceVendorId = -1, + ) -> OrtValue: + """Create an OrtValue using this session's allocator. + + The value retains this session and may only be updated or bound through it. + A matching allocator and a numeric tensor type are required. + """ + device = OrtDevice.make(device_type, device_id, vendor_id)._get_c_device() + if isinstance(element_type, int): + ortvalue = self._sess.create_ortvalue_from_shape_and_onnx_type(shape, element_type, device) + else: + ortvalue = self._sess.create_ortvalue_from_shape_and_type(shape, element_type, device) + return OrtValue(ortvalue, session=self._sess) + + def release_captured_graph(self, graph_annotation_id: int = 0) -> None: + """Release a captured graph and unpin its IOBinding. + + ``graph_annotation_id`` matches the capture's ``gpu_graph_id`` and defaults to zero. + EPs without captured-graph release support treat this as a no-op. + """ + self._sess.release_captured_graph(graph_annotation_id) + pinned = self._captured_graph_bindings.pop(graph_annotation_id, None) + if pinned is not None: + iobinding = pinned[0]() + if iobinding is not None: + iobinding._pinned_graph_ids.discard(graph_annotation_id) + def run_with_iobinding(self, iobinding, run_options=None): """ Compute the predictions. :param iobinding: the iobinding object that has graph inputs/outputs bind. :param run_options: See :class:`onnxruntime.RunOptions`. - """ + + WebGPU capture requires static shapes, disabled memory patterns, fixed WebGPU OrtValues + (session-owned or shared), and no CPU compute nodes other than shape-only nodes. Each + ``gpu_graph_id`` pins one IOBinding and its buffers until :meth:`release_captured_graph`. + Update inputs in place and use :meth:`IOBinding.copy_outputs_to_cpu` for readback; + ``gpu_graph_id=-1`` disables capture. + """ + if iobinding._session is not self._sess: + raise ValueError("IOBinding must be used with the session that created it.") + + graph_annotation_id = _graph_annotation_id(run_options) + capturing = self._sess.is_webgpu_graph_capture_enabled() and graph_annotation_id != _GRAPH_ANNOTATION_SKIP + if capturing: + iobinding._validate_capture_bindings() + signature = iobinding._capture_signature() + pinned = self._captured_graph_bindings.get(graph_annotation_id) + if pinned is not None: + pinned_iobinding, pinned_signature = pinned[0](), pinned[1] + if pinned_iobinding is not iobinding: + raise ValueError( + f"WebGPU graph {graph_annotation_id} was captured with a different " + "IOBinding. Replay re-issues the buffers recorded at capture and would " + "silently ignore this binding. Reuse the original IOBinding, or call " + f"release_captured_graph({graph_annotation_id}) first." + ) + if pinned_signature != signature: + raise ValueError( + f"WebGPU graph {graph_annotation_id} was captured with different I/O " + "buffers. Replay writes to the buffers recorded at capture, so the " + "rebound values would never be read or written. Update the original " + "buffers in place, or call " + f"release_captured_graph({graph_annotation_id}) first." + ) + self._sess.run_with_iobinding(iobinding._iobinding, run_options) + if capturing and graph_annotation_id not in self._captured_graph_bindings: + self._captured_graph_bindings[graph_annotation_id] = (weakref.ref(iobinding), signature) + iobinding._pinned_graph_ids.add(graph_annotation_id) + def set_ep_dynamic_options(self, options: dict[str, str]): """ Set dynamic options for execution providers. @@ -463,6 +608,10 @@ def run_with_ortvaluevector(self, run_options, feed_names, feeds, fetch_names, f :param fetches: list of output OrtValue. :param fetch_devices: list of output devices. """ + # Same capture check as run(), run_with_ort_values() and run_async(): replay re-issues the + # buffers recorded at capture, so transient vectors would be silently ignored. Nothing about + # a raw vector is unsafe outside capture, including on a WebGPU session. + self._validate_graph_capture_run_api(run_options) self._sess.run_with_ortvaluevector(run_options, feed_names, feeds, fetch_names, fetches, fetch_devices) @@ -652,8 +801,23 @@ def _create_inference_session(self, providers, provider_options, disabled_optimi self._provider_options = self._sess.get_provider_options() self._profiling_start_time_ns = self._sess.get_profiling_start_time_ns + def _release_captured_graphs(self) -> None: + """Release every graph captured by the current session handle and unpin its IOBinding. + + A captured graph belongs to the ``C.InferenceSession`` that captured it, so it must be + released before that handle is replaced. Otherwise the stale bookkeeping would reject an + IOBinding created by the replacement session and a later ``release_captured_graph`` would + act on the replacement session while unpinning the old binding. + """ + for graph_annotation_id in sorted(self._captured_graph_bindings): + self.release_captured_graph(graph_annotation_id) + def _reset_session(self, providers, provider_options) -> None: "release underlying session object." + # Captured graphs outlive neither the session handle nor its bookkeeping. Release them + # first so a failure here leaves the session intact instead of half torn down. + self._release_captured_graphs() + # meta data references session internal structures # so they must be set to None to decrement _sess reference count. self._sess_options = None @@ -891,8 +1055,46 @@ class IOBinding: """ def __init__(self, session: Session): + self._session = session._sess + self._is_webgpu_session = "WebGpuExecutionProvider" in session.get_providers() + self._is_webgpu_graph_capture_enabled = self._session.is_webgpu_graph_capture_enabled() self._iobinding = C.SessionIOBinding(session._sess) self._numpy_obj_references = {} + # Capture tracks fixed OrtValues; None marks host or raw-pointer bindings. + self._bound_inputs: dict[str, OrtValue | None] = {} + self._bound_outputs: dict[str, OrtValue | None] = {} + # Captured graph IDs freeze these bindings until release. + self._pinned_graph_ids: set[int] = set() + + def _reject_if_pinned(self, action: str) -> None: + if self._pinned_graph_ids: + pinned = ", ".join(str(graph_id) for graph_id in sorted(self._pinned_graph_ids)) + raise ValueError( + f"Cannot {action} while captured WebGPU graph(s) [{pinned}] still reference this " + "IOBinding. Replay re-issues the buffers recorded at capture, so a change here " + "would not affect replay and releasing the buffers could invalidate it. Call " + "release_captured_graph(id) first." + ) + + def _capture_signature(self): + """Return bound OrtValues whose identity and lifetime must remain fixed during replay.""" + return ( + tuple(sorted(self._bound_inputs.items(), key=lambda item: item[0])), + tuple(sorted(self._bound_outputs.items(), key=lambda item: item[0])), + ) + + def _validate_capture_bindings(self) -> None: + """Every binding participating in capture must be a fixed WebGPU device OrtValue.""" + for kind, bound in (("input", self._bound_inputs), ("output", self._bound_outputs)): + for name, value in bound.items(): + if value is None or not value._is_webgpu_buffer: + raise ValueError( + f"WebGPU graph capture requires fixed WebGPU device OrtValues; {kind} " + f"'{name}' is not one. Bind values created by this session or backed by an " + f"environment-registered shared allocator, or set the " + f"'{_GPU_GRAPH_ID_RUN_CONFIG_KEY}' run option to {_GRAPH_ANNOTATION_SKIP} " + "to run without capture." + ) def bind_cpu_input(self, name, arr_on_cpu): """ @@ -900,11 +1102,14 @@ def bind_cpu_input(self, name, arr_on_cpu): :param name: input name :param arr_on_cpu: input values as a python array on CPU """ + self._reject_if_pinned("rebind inputs") + # Hold a reference to the numpy object as the bound OrtValue is backed # directly by the data buffer of the numpy object and so the numpy object # must be around until this IOBinding instance is around self._numpy_obj_references[name] = arr_on_cpu self._iobinding.bind_input(name, arr_on_cpu) + self._bound_inputs[name] = None def bind_input(self, name, device_type, device_id, element_type, shape, buffer_ptr): """ @@ -915,24 +1120,25 @@ def bind_input(self, name, device_type, device_id, element_type, shape, buffer_p :param shape: input shape :param buffer_ptr: memory pointer to input data """ + self._reject_if_pinned("rebind inputs") self._iobinding.bind_input( name, - C.OrtDevice( - get_ort_device_type(device_type), - C.OrtDevice.default_memory(), - device_id, - ), + OrtDevice.make(device_type, device_id)._get_c_device(), element_type, shape, buffer_ptr, ) + self._bound_inputs[name] = None def bind_ortvalue_input(self, name, ortvalue): """ :param name: input name :param ortvalue: OrtValue instance to bind """ + _validate_ortvalue_session_compatibility(ortvalue, self._session, "bound to") + self._reject_if_pinned("rebind inputs") self._iobinding.bind_ortvalue_input(name, ortvalue._ortvalue) + self._bound_inputs[name] = ortvalue def synchronize_inputs(self): self._iobinding.synchronize_inputs() @@ -955,6 +1161,8 @@ def bind_output( :param buffer_ptr: memory pointer to output data """ + self._reject_if_pinned("rebind outputs") + # Follow the `if` path when the user has not provided any pre-allocated buffer but still # would like to bind an output to a specific device (e.g. cuda). # Pre-allocating an output buffer may not be an option for the user as : @@ -964,33 +1172,29 @@ def bind_output( if buffer_ptr is None: self._iobinding.bind_output( name, - C.OrtDevice( - get_ort_device_type(device_type), - C.OrtDevice.default_memory(), - device_id, - ), + OrtDevice.make(device_type, device_id)._get_c_device(), ) else: if element_type is None or shape is None: raise ValueError("`element_type` and `shape` are to be provided if pre-allocated memory is provided") self._iobinding.bind_output( name, - C.OrtDevice( - get_ort_device_type(device_type), - C.OrtDevice.default_memory(), - device_id, - ), + OrtDevice.make(device_type, device_id)._get_c_device(), element_type, shape, buffer_ptr, ) + self._bound_outputs[name] = None def bind_ortvalue_output(self, name, ortvalue): """ :param name: output name :param ortvalue: OrtValue instance to bind """ + _validate_ortvalue_session_compatibility(ortvalue, self._session, "bound to") + self._reject_if_pinned("rebind outputs") self._iobinding.bind_ortvalue_output(name, ortvalue._ortvalue) + self._bound_outputs[name] = ortvalue def synchronize_outputs(self): self._iobinding.synchronize_outputs() @@ -1003,9 +1207,24 @@ def get_outputs(self): outputs = self._iobinding.get_outputs() if not isinstance(outputs, C.OrtValueVector): raise TypeError("get_outputs() must return an instance of type 'OrtValueVector'.") - return [OrtValue(ortvalue) for ortvalue in outputs] + result = [] + for index in range(len(outputs)): + ortvalue = outputs[index] + result.append( + OrtValue( + ortvalue, + session=self._session if ortvalue._is_webgpu_buffer() else None, + ) + ) + return result def get_outputs_as_ortvaluevector(self): + """Return the raw OrtValueVector of outputs from the Run() that preceded the call. + + The vector is a reference into this IOBinding (pybind ``reference_internal``), so it keeps the + IOBinding alive, which in turn keeps the session alive. Device-resident outputs are therefore + safe to hold past the session going out of scope. + """ return self._iobinding.get_outputs() def copy_outputs_to_cpu(self): @@ -1013,10 +1232,14 @@ def copy_outputs_to_cpu(self): return self._iobinding.copy_outputs_to_cpu() def clear_binding_inputs(self): + self._reject_if_pinned("clear input bindings") self._iobinding.clear_binding_inputs() + self._bound_inputs.clear() def clear_binding_outputs(self): + self._reject_if_pinned("clear output bindings") self._iobinding.clear_binding_outputs() + self._bound_outputs.clear() class OrtValue: @@ -1026,18 +1249,35 @@ class OrtValue: This class provides APIs to construct and deal with OrtValues. """ - def __init__(self, ortvalue: C.OrtValue, numpy_obj: np.ndarray | None = None): + def __init__( + self, + ortvalue: C.OrtValue, + numpy_obj: np.ndarray | None = None, + session: C.InferenceSession | None = None, + ): if isinstance(ortvalue, C.OrtValue): self._ortvalue = ortvalue # Hold a ref count to the numpy object if the OrtValue is backed directly # by its data buffer so that it isn't destroyed when the OrtValue is in use self._numpy_obj = numpy_obj + # Session-scoped device allocators can be invalidated when their session is destroyed. + self._session = session else: # An end user won't hit this error raise ValueError( "`Provided ortvalue` needs to be of type `onnxruntime.capi.onnxruntime_pybind11_state.OrtValue`" ) + @property + def _is_webgpu_buffer(self) -> bool: + """Whether this value is backed by a WebGPU buffer. + + Resolved from the native OrtValue on every access and deliberately read-only: graph-capture + validation and the copy-path checks below refuse host memory, so a settable attribute would + let a CPU tensor pass itself off as a device tensor. + """ + return self._ortvalue._is_webgpu_buffer() + def _get_c_value(self) -> C.OrtValue: return self._ortvalue @@ -1160,12 +1400,14 @@ def as_sparse_tensor(self) -> SparseTensor: def data_ptr(self) -> int: """ Returns the address of the first element in the OrtValue's data buffer + + WebGPU buffers are opaque handles and do not expose a data pointer. """ return self._ortvalue.data_ptr() def device_name(self) -> str: """ - Returns the name of the device where the OrtValue's data buffer resides e.g. cpu, cuda, cann + Returns the name of the device where the OrtValue's data buffer resides e.g. cpu, cuda, cann, webgpu """ return self._ortvalue.device_name().lower() @@ -1225,6 +1467,8 @@ def numpy(self) -> np.ndarray: Returns a Numpy object from the OrtValue. Valid only for OrtValues holding Tensors. Throws for OrtValues holding non-Tensors. Use accessors to gain a reference to non-Tensor objects such as SparseTensor + WebGPU device values require explicit readback with + :meth:`IOBinding.copy_outputs_to_cpu`. """ return self._ortvalue.numpy() @@ -1266,6 +1510,8 @@ def __dlpack__(self, *, stream=None): The OrtValue must hold a contiguous tensor. No data is copied; the consumer shares memory with this OrtValue, which must remain alive while the capsule is in use. + WebGPU OrtValues cannot be exported through DLPack because their data + is stored in opaque WebGPU buffers rather than CUDA-addressable memory. :param stream: Optional stream on which the tensor data is accessible. Currently unused; included for protocol compliance. @@ -1279,6 +1525,8 @@ def __dlpack_device__(self) -> tuple[int, int]: resides (part of the `DLPack protocol `_). + WebGPU OrtValues do not expose a DLPack device. + :return: Tuple of ``(device_type, device_id)`` as ints following DLPack ``DLDeviceType`` enum values. """ @@ -1341,19 +1589,74 @@ def update_inplace(self, data) -> None: GPU to GPU) without going through the CPU. """ if isinstance(data, OrtValue): + if self._is_webgpu_buffer != data._is_webgpu_buffer: + raise ValueError( + "WebGPU OrtValue copies require WebGPU source and destination values; " + "use IOBinding.copy_outputs_to_cpu for readback." + ) + if not _is_ortvalue_session_compatible(data, self._session): + raise ValueError("Session-scoped OrtValues must originate from the same session.") + self._ortvalue.update_inplace(data._ortvalue) return if not isinstance(data, np.ndarray): raise TypeError("data must be a numpy.ndarray or an OrtValue.") + # Every copy path requires contiguous source storage. + if not data.flags.c_contiguous: + data = np.ascontiguousarray(data) + self._ortvalue.update_inplace(data) +_DEFAULT_WEBGPU_CONTEXT_ID = 0 + + +def _session_webgpu_context_id(session: C.InferenceSession) -> int: + """Return a session's WebGPU context id, or -1 when it has no WebGPU EP.""" + return session.webgpu_context_id() + + +def _has_foreign_webgpu_context( + values: Sequence[OrtValue], + context_id_getter=_session_webgpu_context_id, +) -> bool: + """Whether any value provably belongs to a WebGPU context other than the default one. + + The environment data transfer resolves the default WebGPU context, so copying a buffer that + belongs to a caller-supplied context would silently run on the wrong device. A value with no + owning session cannot be attributed to a context and is treated as default-context, as is a + session with no WebGPU EP. + + TODO: this can only attribute a value that carries session provenance, because every WebGPU + allocation reports OrtDevice(GPU, VendorIds::NONE, 0) regardless of its context. Supporting a + custom external WebGPU context requires the shared allocator and its data transfer to retain and + use that external device instead of resolving WebGpuContextFactory::DefaultContext(), which + would also let the transfer itself reject the copy instead of relying on this Python-side guard. + """ + for value in values: + if not value._is_webgpu_buffer or value._session is None: + continue + if context_id_getter(value._session) not in (-1, _DEFAULT_WEBGPU_CONTEXT_ID): + return True + return False + + def copy_tensors(src: Sequence[OrtValue], dst: Sequence[OrtValue], stream=None) -> None: """ Copy tensor data from source OrtValue sequence to destination OrtValue sequence. + + WebGPU values belonging to the default context are supported in either direction. A value from a + caller-supplied WebGPU context is not, because the shared data transfer is bound to the default + context. """ + if _has_foreign_webgpu_context([*src, *dst]): + raise ValueError( + "copy_tensors cannot copy a WebGPU OrtValue that belongs to a custom WebGPU context " + "because the shared data transfer is bound to the default context; use IOBinding " + "readback instead." + ) c_sources = [s._get_c_value() for s in src] c_dsts = [d._get_c_value() for d in dst] C.copy_tensors(c_sources, c_dsts, stream) diff --git a/onnxruntime/python/onnxruntime_pybind_iobinding.cc b/onnxruntime/python/onnxruntime_pybind_iobinding.cc index d960444f240a4..a53fee42669d7 100644 --- a/onnxruntime/python/onnxruntime_pybind_iobinding.cc +++ b/onnxruntime/python/onnxruntime_pybind_iobinding.cc @@ -43,7 +43,7 @@ void BindOutput(SessionIOBinding* io_binding, const std::string& name, const Ort } OrtValue ml_value; - OrtMemoryInfo info(GetDeviceName(device), OrtDeviceAllocator, device); + OrtMemoryInfo info(GetDeviceAllocatorName(device), OrtDeviceAllocator, device); Tensor::InitOrtValue(element_type, gsl::make_span(shape), reinterpret_cast(data_ptr), info, ml_value); auto status = io_binding->Get()->BindOutput(name, ml_value); @@ -57,9 +57,10 @@ void addIoBindingMethods(pybind11::module& m) { py::class_ session_io_binding(m, "SessionIOBinding"); session_io_binding .def(py::init([](PyInferenceSession* sess) { - auto sess_io_binding = std::make_unique(sess->GetSessionHandle()); - return sess_io_binding; - })) + auto sess_io_binding = std::make_unique(sess->GetSessionHandle()); + return sess_io_binding; + }), + py::keep_alive<1, 2>()) // May create Tensor/Sequence based OrtValues. Use bind_ortvalue_input for universal binding. .def("bind_input", [](SessionIOBinding* io_binding, const std::string& name, py::object& arr_on_cpu) -> void { InferenceSession* sess = io_binding->GetInferenceSession(); @@ -100,7 +101,7 @@ void addIoBindingMethods(pybind11::module& m) { } auto ml_type = OnnxTypeToOnnxRuntimeTensorType(element_type); OrtValue ml_value; - OrtMemoryInfo info(GetDeviceName(device), OrtDeviceAllocator, device); + OrtMemoryInfo info(GetDeviceAllocatorName(device), OrtDeviceAllocator, device); Tensor::InitOrtValue(ml_type, gsl::make_span(shape), reinterpret_cast(data_ptr), info, ml_value); auto status = io_binding->Get()->BindInput(name, ml_value); @@ -117,7 +118,7 @@ void addIoBindingMethods(pybind11::module& m) { int type_num = dtype->type_num; Py_DECREF(dtype); - OrtMemoryInfo info(GetDeviceName(device), OrtDeviceAllocator, device); + OrtMemoryInfo info(GetDeviceAllocatorName(device), OrtDeviceAllocator, device); auto ml_type = NumpyTypeToOnnxRuntimeTensorType(type_num); // See comment in the int32_t element_type overload above: string tensors are not safe // to bind via a raw, non-owning pointer because no std::string objects are constructed diff --git a/onnxruntime/python/onnxruntime_pybind_ortvalue.cc b/onnxruntime/python/onnxruntime_pybind_ortvalue.cc index 7bf9325cf2208..4e295f9b58b8c 100644 --- a/onnxruntime/python/onnxruntime_pybind_ortvalue.cc +++ b/onnxruntime/python/onnxruntime_pybind_ortvalue.cc @@ -19,6 +19,11 @@ namespace python { namespace py = pybind11; namespace { +bool IsWebGpuBuffer(const OrtValue& ort_value) { + return ort_value.IsTensor() && + ort_value.Get().Location().name == WEBGPU_BUFFER; +} + std::unique_ptr OrtValueFromShapeAndType(const std::vector& shape, MLDataType element_type, const OrtDevice& device) { @@ -376,6 +381,9 @@ void addOrtValueMethods(pybind11::module& m) { }) .def("device_name", [](const OrtValue* ort_value) -> std::string { if (ort_value->IsTensor()) { + if (IsWebGpuBuffer(*ort_value)) { + return "webgpu"; + } return std::string(GetDeviceName(ort_value->Get().Location().device)); } #if !defined(DISABLE_SPARSE_TENSORS) @@ -446,6 +454,7 @@ void addOrtValueMethods(pybind11::module& m) { .def("is_tensor", [](const OrtValue* ort_value) -> bool { return ort_value->IsTensor(); }) .def("is_sparse_tensor", [](const OrtValue* ort_value) -> bool { return ort_value->IsSparseTensor(); }) .def("is_tensor_sequence", [](const OrtValue* ort_value) -> bool { return ort_value->IsTensorSequence(); }) + .def("_is_webgpu_buffer", [](const OrtValue* ort_value) -> bool { return IsWebGpuBuffer(*ort_value); }) // Converts Tensor into a numpy array .def("numpy", [](const OrtValue* ml_value) -> py::object { ORT_ENFORCE(ml_value->IsTensor(), "Only OrtValues that are Tensors are convertible to Numpy objects"); @@ -496,17 +505,22 @@ void addOrtValueMethods(pybind11::module& m) { #endif }) #if defined(ENABLE_DLPACK) - .def("to_dlpack", [](OrtValue* ort_value) -> py::object { return py::reinterpret_steal(ToDlpack(*ort_value)); }, + .def("to_dlpack", [](OrtValue* ort_value) -> py::object { + ORT_ENFORCE(!IsWebGpuBuffer(*ort_value), "DLPack export is not supported for WebGPU OrtValues."); + return py::reinterpret_steal(ToDlpack(*ort_value)); }, "Returns a DLPack representing the tensor. This method does not copy the pointer shape, " "instead, it copies the pointer value. The OrtValue must be persist until the dlpack structure " "is consumed.") .def_static("from_dlpack", [](py::object data, bool is_bool_tensor) { return FromDlpack(data.ptr(), is_bool_tensor); }, py::arg("data"), py::arg("is_bool_tensor") = false, "Converts a tensor from a external library into an OrtValue by means of the __dlpack__ protocol.") - .def("__dlpack__", [](OrtValue* ort_value, py::object /* stream */) -> py::object { return py::reinterpret_steal(ToDlpack(*ort_value)); }, py::arg("stream") = py::none(), + .def("__dlpack__", [](OrtValue* ort_value, py::object /* stream */) -> py::object { + ORT_ENFORCE(!IsWebGpuBuffer(*ort_value), "DLPack export is not supported for WebGPU OrtValues."); + return py::reinterpret_steal(ToDlpack(*ort_value)); }, py::arg("stream") = py::none(), "Returns a DLPack representing the tensor (part of __dlpack__ protocol). " "This method does not copy the pointer shape, instead, it copies the pointer value. " "The OrtValue must persist until the dlpack structure is consumed.") .def("__dlpack_device__", [](const OrtValue* ort_value) -> py::tuple { ORT_ENFORCE(ort_value->IsTensor(), "Only tensor type OrtValues are supported"); + ORT_ENFORCE(!IsWebGpuBuffer(*ort_value), "DLPack export is not supported for WebGPU OrtValues."); const onnxruntime::Tensor& tensor = ort_value->Get(); DLDevice device = onnxruntime::dlpack::GetDlpackDevice(*ort_value, tensor.Location().device.Id()); return py::make_tuple(static_cast(device.device_type), device.device_id); }, "Returns a tuple of integers, (device, device index) (part of __dlpack__ protocol).") @@ -516,6 +530,16 @@ void addOrtValueMethods(pybind11::module& m) { py::class_>(m, "OrtValueVector") .def(py::init<>()) .def("push_back", [](std::vector* v, const OrtValue& ortvalue) { + // A standalone OrtValueVector has no parent object, so unlike the vector returned by + // SessionIOBinding::get_outputs it cannot keep the owning session alive. A WebGPU buffer + // allocated by a session's EP is freed through GpuBufferAllocator, whose buffer-manager + // getter captures that EP, so releasing it after the session is gone is a use-after-free. + // TODO: remove once GpuBufferAllocator owns a reference to whatever owns its BufferManager, + // at which point a WebGPU OrtValue is safe to hold independently of any session. + ORT_ENFORCE(!IsWebGpuBuffer(ortvalue), + "A WebGPU OrtValue cannot be stored in a standalone OrtValueVector because the " + "vector cannot keep the session that allocated the buffer alive. Use IOBinding, " + "or IOBinding.get_outputs_as_ortvaluevector() for device-resident outputs."); v->push_back(ortvalue); }) #if defined(ENABLE_DLPACK) @@ -537,6 +561,14 @@ void addOrtValueMethods(pybind11::module& m) { auto ml_type = NumpyTypeToOnnxRuntimeTensorType(type_num); auto device = devices.at(i); + // This overload wraps foreign (PyTorch) storage by raw address. A WebGPU allocation is + // an opaque WGPUBuffer handle rather than an addressable pointer, so no torch tensor + // can back one. The OrtMemoryInfo built below would also be mislabelled, since it uses + // GetDeviceName rather than the WEBGPU_BUFFER allocator name the binding paths expect. + ORT_ENFORCE(!(device.Type() == OrtDevice::GPU && + device.Vendor() == OrtDevice::VendorIds::NONE), + "OrtValueVector.push_back_batch cannot wrap WebGPU memory: a WebGPU " + "allocation is an opaque buffer handle, not a raw data pointer."); OrtMemoryInfo info(GetDeviceName(device), OrtDeviceAllocator, device); OrtValue ml_value; Tensor::InitOrtValue(ml_type, gsl::make_span(shape), reinterpret_cast(data_ptr), info, ml_value); @@ -547,7 +579,7 @@ void addOrtValueMethods(pybind11::module& m) { .def("shrink_to_fit", [](std::vector* v) { v->shrink_to_fit(); }) .def("__len__", [](const std::vector& v) { return v.size(); }) .def("__iter__", [](const std::vector& v) { return py::make_iterator(v.cbegin(), v.cend()); }, py::keep_alive<0, 1>()) - .def("__getitem__", [](const std::vector& v, const size_t idx) { return v.at(idx); }) + .def("__getitem__", [](const std::vector& v, const size_t idx) { return v.at(idx); }, py::keep_alive<0, 1>()) .def("bool_tensor_indices", [](std::vector* v) -> std::vector { std::vector indices; for (size_t i = 0; i < v->size(); ++i) { @@ -561,7 +593,9 @@ void addOrtValueMethods(pybind11::module& m) { "If torch consumes the dlpack structure, `.to(torch.bool)` must be applied to the torch tensor " "to get a boolean tensor.") #if defined(ENABLE_DLPACK) - .def("dlpack_at", [](std::vector* v, const size_t idx) { return py::reinterpret_steal(ToDlpack(v->at(idx))); }) + .def("dlpack_at", [](std::vector* v, const size_t idx) { + ORT_ENFORCE(!IsWebGpuBuffer(v->at(idx)), "DLPack export is not supported for WebGPU OrtValues."); + return py::reinterpret_steal(ToDlpack(v->at(idx))); }) #endif .def("element_type_at", [](std::vector* v, const size_t idx) -> int32_t { return GetTensorProtoType(v->at(idx)); }, "Returns an integer equal to the ONNX proto type of the tensor at position i. " @@ -583,6 +617,7 @@ void addOrtValueMethods(pybind11::module& m) { DLManagedTensor* dlmanaged_tensor; for (auto it : v) { + ORT_ENFORCE(!IsWebGpuBuffer(it), "DLPack export is not supported for WebGPU OrtValues."); dlmanaged_tensor = dlpack::OrtValueToDlpack(it); py::capsule capsule(dlmanaged_tensor, "dltensor", DlpackCapsuleDestructor); list_dlpacks.append(capsule); @@ -595,6 +630,7 @@ void addOrtValueMethods(pybind11::module& m) { for (auto it : v) { // A new instance of dlpack needs to be created. The object which consumes it // is responsible for its deletion. + ORT_ENFORCE(!IsWebGpuBuffer(it), "DLPack export is not supported for WebGPU OrtValues."); dlmanaged_tensor = dlpack::OrtValueToDlpack(it); if (capsule == NULL) { capsule = PyCapsule_New(dlmanaged_tensor, "dltensor", NULL); diff --git a/onnxruntime/python/onnxruntime_pybind_state.cc b/onnxruntime/python/onnxruntime_pybind_state.cc index 404604bb1fd69..0c210269704c3 100644 --- a/onnxruntime/python/onnxruntime_pybind_state.cc +++ b/onnxruntime/python/onnxruntime_pybind_state.cc @@ -27,6 +27,7 @@ #include "core/framework/provider_options_utils.h" #include "core/framework/random_seed.h" #include "core/framework/sparse_tensor.h" +#include "core/framework/tensor.h" #include "core/framework/tensorprotoutils.h" #include "core/framework/TensorSeq.h" #include "core/graph/graph_viewer.h" @@ -92,6 +93,25 @@ namespace { constexpr std::string_view kEpCudaProviderOptionPrefix{"ep.cuda."}; +std::unique_ptr CreateSessionOrtValue(PyInferenceSession* session, + const std::vector& shape, + MLDataType element_type, + const OrtDevice& device) { + // Accessing session state enforces that initialization has completed. + (void)session->GetSessionHandle()->GetSessionState(); + + const char* allocator_name = GetDeviceAllocatorName(device); + OrtMemoryInfo memory_info{allocator_name, OrtDeviceAllocator, device}; + auto allocator = session->GetSessionHandle()->GetAllocator(memory_info); + if (!allocator) { + throw std::runtime_error("No session allocator found for " + device.ToString()); + } + + auto ort_value = std::make_unique(); + Tensor::InitOrtValue(element_type, gsl::make_span(shape), std::move(allocator), *ort_value); + return ort_value; +} + struct AdaptedProviderOptions { std::vector keys; std::vector values; @@ -540,6 +560,13 @@ const char* GetDeviceName(const OrtDevice& device) { } } +const char* GetDeviceAllocatorName(const OrtDevice& device) { + if (device.Type() == OrtDevice::GPU && device.Vendor() == OrtDevice::VendorIds::NONE) { + return WEBGPU_BUFFER; + } + return GetDeviceName(device); +} + py::object GetPyObjectFromSparseTensor(size_t pos, const OrtValue& ort_value, const DataTransferManager* data_transfer_manager) { #if !defined(DISABLE_SPARSE_TENSORS) if (!ort_value.IsSparseTensor()) { @@ -3121,6 +3148,42 @@ including arg name, arg type (contains both type and shape).)pbdoc") }) .def("get_providers", [](const PyInferenceSession* sess) -> const std::vector& { return sess->GetSessionHandle()->GetRegisteredProviderTypes(); }, py::return_value_policy::reference_internal) .def("get_provider_options", [](const PyInferenceSession* sess) -> const ProviderOptionsMap& { return sess->GetSessionHandle()->GetAllProviderOptions(); }, py::return_value_policy::reference_internal) + .def("is_webgpu_graph_capture_enabled", [](const PyInferenceSession* sess) { + const auto* webgpu_ep = + sess->GetSessionHandle()->GetExecutionProviders().Get(kWebGpuExecutionProvider); + return webgpu_ep != nullptr && webgpu_ep->IsGraphCaptureEnabled(); }) + .def("webgpu_context_id", [](const PyInferenceSession* sess) { + // WebGpuExecutionProvider::GetDeviceId() returns its WebGPU context id. Context 0 is the + // default context, which is the only one the environment-registered shared data transfer + // can serve; a caller-supplied instance/device gets a context id > 0. + // Returns -1 when the session has no WebGPU EP. + const auto* webgpu_ep = + sess->GetSessionHandle()->GetExecutionProviders().Get(kWebGpuExecutionProvider); + return webgpu_ep != nullptr ? webgpu_ep->GetDeviceId() : -1; }) + .def("create_ortvalue_from_shape_and_type", [](PyInferenceSession* sess, const std::vector& shape, py::object& numpy_element_type, const OrtDevice& device) { + PyArray_Descr* dtype; + if (!PyArray_DescrConverter(numpy_element_type.ptr(), &dtype)) { + throw std::runtime_error("Not a valid numpy type"); + } + + int type_num = dtype->type_num; + Py_DECREF(dtype); + if (!IsNumericNumpyType(type_num)) { + throw std::runtime_error("Creation of OrtValues is currently only supported for numeric tensor types"); + } + + py::gil_scoped_release release; + return CreateSessionOrtValue(sess, shape, NumpyTypeToOnnxRuntimeTensorType(type_num), device); }, py::keep_alive<0, 1>()) + .def("create_ortvalue_from_shape_and_onnx_type", [](PyInferenceSession* sess, const std::vector& shape, int32_t onnx_element_type, const OrtDevice& device) { + if (onnx_element_type == ONNX_NAMESPACE::TensorProto_DataType_STRING) { + throw std::runtime_error("Creation of OrtValues is currently only supported for numeric tensor types"); + } + + py::gil_scoped_release release; + return CreateSessionOrtValue(sess, shape, OnnxTypeToOnnxRuntimeTensorType(onnx_element_type), device); }, py::keep_alive<0, 1>()) + .def("release_captured_graph", [](PyInferenceSession* sess, int graph_annotation_id) { + py::gil_scoped_release release; + OrtPybindThrowIfError(sess->GetSessionHandle()->ReleaseCapturedGraph(graph_annotation_id)); }) .def("get_provider_graph_assignment_info", [](const PyInferenceSession* sess) -> const std::vector& { #if !defined(ORT_MINIMAL_BUILD) const auto* inference_session = sess->GetSessionHandle(); diff --git a/onnxruntime/python/onnxruntime_pybind_state_common.h b/onnxruntime/python/onnxruntime_pybind_state_common.h index aa248e776e5ef..877cc3b9be5c2 100644 --- a/onnxruntime/python/onnxruntime_pybind_state_common.h +++ b/onnxruntime/python/onnxruntime_pybind_state_common.h @@ -445,6 +445,10 @@ void addOpSchemaSubmodule(pybind11::module& m); const char* GetDeviceName(const OrtDevice& device); +// Allocator name for an OrtMemoryInfo. Differs from GetDeviceName only for WebGPU, whose +// GPU device carries no vendor id and so would otherwise be named CUDA. +const char* GetDeviceAllocatorName(const OrtDevice& device); + bool IsCudaDeviceIdValid(const onnxruntime::logging::Logger& logger, int id); AllocatorPtr GetCudaAllocator(OrtDevice::DeviceId id); diff --git a/onnxruntime/test/contrib_ops/causal_conv_with_state_op_test.cc b/onnxruntime/test/contrib_ops/causal_conv_with_state_op_test.cc index cc3b3cd6382ca..e260f70faac61 100644 --- a/onnxruntime/test/contrib_ops/causal_conv_with_state_op_test.cc +++ b/onnxruntime/test/contrib_ops/causal_conv_with_state_op_test.cc @@ -38,10 +38,10 @@ enum class TensorType { // Input: (B, D, L) channels-first // Weight: (D, 1, K) depthwise // Bias: (D,) optional -// past_state: (B, D, K-1) optional carry state +// past_state: (B, D, (K-1)*dilation) optional carry state // // Output: (B, D, L) convolution output (with optional activation) -// present_state: (B, D, K-1) updated carry state +// present_state: (B, D, (K-1)*dilation) updated carry state void CausalConvWithStateReference( const std::vector& input, const std::vector& weight, @@ -53,8 +53,9 @@ void CausalConvWithStateReference( int channels, int input_length, int kernel_size, - const std::string& activation) { - int state_length = kernel_size - 1; + const std::string& activation, + int dilation = 1) { + int state_length = (kernel_size - 1) * dilation; int total_virtual_length = state_length + input_length; output.resize(batch_size * channels * input_length); @@ -79,7 +80,7 @@ void CausalConvWithStateReference( for (int pos = 0; pos < input_length; ++pos) { float acc = 0.0f; for (int j = 0; j < kernel_size; ++j) { - float val = virtual_input[pos + j]; + float val = virtual_input[pos + j * dilation]; float w = weight[d * kernel_size + j]; acc += val * w; } @@ -103,24 +104,57 @@ void CausalConvWithStateReference( } } -// Returns a WebGPU EP if it is available and has the CausalConvWithState kernel registered, -// or nullptr otherwise. -std::unique_ptr TryGetEpWithCausalConvWithState() { - auto ep = DefaultWebGpuExecutionProvider(); - if (!ep) { - ep = DefaultCpuExecutionProvider(); +bool EpHasCausalConvWithState(const IExecutionProvider& ep) { + auto kernel_registry = ep.GetKernelRegistry(); + if (!kernel_registry) { + return true; } + const KernelCreateInfo* info = nullptr; + KernelRegistry::TypeConstraintMap type_constraints; + auto status = kernel_registry->TryFindKernel( + ep.Type(), "CausalConvWithState", kMSDomain, 1, + type_constraints, DefaultLoggingManager().DefaultLogger(), &info); + return status.IsOK(); +} + +// Returns every locally available EP that registers the CausalConvWithState kernel, so a single +// test case covers CUDA, WebGPU and CPU in whichever build it runs in rather than picking one. +// OpTester silently skips a case whose type is unsupported by the EP (e.g. fp16 on CPU), so the +// same list is used for both element types. +std::vector> GetEpsWithCausalConvWithState() { + std::vector> eps; + + auto add = [&eps](std::unique_ptr ep) { + if (ep && EpHasCausalConvWithState(*ep)) { + eps.push_back(std::move(ep)); + } + }; - auto kernel_registry = ep->GetKernelRegistry(); - if (kernel_registry) { - const KernelCreateInfo* info = nullptr; - KernelRegistry::TypeConstraintMap type_constraints; - auto status = kernel_registry->TryFindKernel( - ep->Type(), "CausalConvWithState", kMSDomain, 1, - type_constraints, DefaultLoggingManager().DefaultLogger(), &info); - if (!status.IsOK()) return nullptr; +#ifdef USE_CUDA + if (HasCudaEnvironment(0)) { + add(DefaultCudaExecutionProvider()); } - return ep; +#endif + add(DefaultWebGpuExecutionProvider()); + add(DefaultCpuExecutionProvider()); + + return eps; +} + +// Rewrites channels-first (batch_size, channels, length) data into the channels-last +// (batch_size, length, channels) layout that channels_last = 1 consumes. +std::vector ToChannelsLast(const std::vector& data, int batch_size, int channels, + int length) { + std::vector out(data.size()); + for (int b = 0; b < batch_size; ++b) { + for (int c = 0; c < channels; ++c) { + for (int l = 0; l < length; ++l) { + out[(static_cast(b) * length + l) * channels + c] = + data[(static_cast(b) * channels + c) * length + l]; + } + } + } + return out; } } // anonymous namespace @@ -137,27 +171,65 @@ static void RunCausalConvWithStateTest( int input_length, int kernel_size, const std::string& activation, - TensorType tensor_type) { - auto ep = TryGetEpWithCausalConvWithState(); - if (!ep) { + TensorType tensor_type, + int dilation = 1, + bool channels_last = false, + const std::vector* channel_dims = nullptr) { + auto eps = GetEpsWithCausalConvWithState(); + if (eps.empty()) { GTEST_SKIP() << "CausalConvWithState kernel not registered"; return; } - int state_length = kernel_size - 1; + const int state_length = (kernel_size - 1) * dilation; - std::vector input_shape = {batch_size, channels, input_length}; - std::vector weight_shape = {channels, 1, kernel_size}; - std::vector bias_shape = {channels}; - std::vector state_shape = {batch_size, channels, state_length}; - std::vector output_shape = {batch_size, channels, input_length}; + // The trailing channel axes are only meaningful for channels_last; a caller that keeps + // hyper-connections and hidden size separate passes them here instead of reshaping. + std::vector trailing_channel_dims = + channel_dims != nullptr ? *channel_dims : std::vector{channels}; + + std::vector input_shape; + std::vector state_shape; + if (channels_last) { + input_shape = {batch_size, input_length}; + state_shape = {batch_size, state_length}; + input_shape.insert(input_shape.end(), trailing_channel_dims.begin(), trailing_channel_dims.end()); + state_shape.insert(state_shape.end(), trailing_channel_dims.begin(), trailing_channel_dims.end()); + } else { + input_shape = {batch_size, channels, input_length}; + state_shape = {batch_size, channels, state_length}; + } + const std::vector weight_shape = {channels, 1, kernel_size}; + const std::vector bias_shape = {channels}; + const std::vector output_shape = input_shape; + + // The reference always produces channels-first data; convert once so both layouts are checked + // against the same numbers. + const std::vector input_values = + channels_last ? ToChannelsLast(input_data, batch_size, channels, input_length) : input_data; + const std::vector output_values = + channels_last ? ToChannelsLast(expected_output, batch_size, channels, input_length) : expected_output; + const std::vector state_values = + channels_last ? ToChannelsLast(expected_state, batch_size, channels, state_length) : expected_state; + std::vector conv_state_values; + if (conv_state_data != nullptr) { + conv_state_values = channels_last + ? ToChannelsLast(*conv_state_data, batch_size, channels, state_length) + : *conv_state_data; + } - { + for (auto& ep : eps) { OpTester test("CausalConvWithState", 1, onnxruntime::kMSDomain); test.AddAttribute("activation", activation); + if (dilation != 1) { + test.AddAttribute("dilation", static_cast(dilation)); + } + if (channels_last) { + test.AddAttribute("channels_last", static_cast(1)); + } if (tensor_type == TensorType::kFloat) { - test.AddInput("input", input_shape, input_data); + test.AddInput("input", input_shape, input_values); test.AddInput("weight", weight_shape, weight_data); if (bias_data != nullptr) { @@ -167,15 +239,15 @@ static void RunCausalConvWithStateTest( } if (conv_state_data != nullptr) { - test.AddInput("past_state", state_shape, *conv_state_data); + test.AddInput("past_state", state_shape, conv_state_values); } else { test.AddOptionalInputEdge(); } - test.AddOutput("output", output_shape, expected_output); - test.AddOutput("present_state", state_shape, expected_state); + test.AddOutput("output", output_shape, output_values); + test.AddOutput("present_state", state_shape, state_values); } else { - test.AddInput("input", input_shape, ToFloat16(input_data)); + test.AddInput("input", input_shape, ToFloat16(input_values)); test.AddInput("weight", weight_shape, ToFloat16(weight_data)); if (bias_data != nullptr) { @@ -185,13 +257,13 @@ static void RunCausalConvWithStateTest( } if (conv_state_data != nullptr) { - test.AddInput("past_state", state_shape, ToFloat16(*conv_state_data)); + test.AddInput("past_state", state_shape, ToFloat16(conv_state_values)); } else { test.AddOptionalInputEdge(); } - test.AddOutput("output", output_shape, ToFloat16(expected_output)); - test.AddOutput("present_state", state_shape, ToFloat16(expected_state)); + test.AddOutput("output", output_shape, ToFloat16(output_values)); + test.AddOutput("present_state", state_shape, ToFloat16(state_values)); } test.SetOutputAbsErr("output", 0.01f); @@ -212,28 +284,31 @@ static void RunCausalConvWithStateTests( int channels, int input_length, int kernel_size, - const std::string& activation = "silu") { + const std::string& activation = "silu", + int dilation = 1, + bool channels_last = false, + const std::vector* channel_dims = nullptr) { // Compute expected output using reference implementation std::vector expected_output; std::vector expected_state; CausalConvWithStateReference( input_data, weight_data, bias_data, conv_state_data, expected_output, expected_state, - batch_size, channels, input_length, kernel_size, activation); + batch_size, channels, input_length, kernel_size, activation, dilation); // FP32 test RunCausalConvWithStateTest( input_data, weight_data, bias_data, conv_state_data, expected_output, expected_state, batch_size, channels, input_length, kernel_size, activation, - TensorType::kFloat); + TensorType::kFloat, dilation, channels_last, channel_dims); // FP16 test RunCausalConvWithStateTest( input_data, weight_data, bias_data, conv_state_data, expected_output, expected_state, batch_size, channels, input_length, kernel_size, activation, - TensorType::kFloat16); + TensorType::kFloat16, dilation, channels_last, channel_dims); } // ============================================================================= @@ -664,6 +739,335 @@ TEST(CausalConvWithStateTest, LargerDimensions) { batch_size, channels, input_length, kernel_size, "silu"); } +// ============================================================================= +// Dilation tests +// +// dilation spaces the kernel taps along the causal axis: output position t reads input positions +// t - (K - 1 - j) * dilation for tap j, so the carry state grows to (K - 1) * dilation. +// ============================================================================= + +TEST(CausalConvWithStateTest, DilatedNoState) { + // B=1, D=2, L=6, K=3, dilation=2 -> receptive field spans 4 positions back + int batch_size = 1, channels = 2, input_length = 6, kernel_size = 3, dilation = 2; + + std::vector input_data = { + 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, + 0.5f, 1.5f, 2.5f, 3.5f, 4.5f, 5.5f}; + std::vector weight_data = { + 0.1f, 0.2f, 0.3f, + 0.4f, 0.5f, 0.6f}; + + RunCausalConvWithStateTests( + input_data, weight_data, nullptr, nullptr, + batch_size, channels, input_length, kernel_size, "none", dilation); +} + +TEST(CausalConvWithStateTest, DilatedWithStateAndBias) { + // K=3, dilation=2 -> state length is (3 - 1) * 2 = 4 + int batch_size = 2, channels = 2, input_length = 5, kernel_size = 3, dilation = 2; + + std::vector input_data(batch_size * channels * input_length); + for (int i = 0; i < static_cast(input_data.size()); ++i) { + input_data[i] = std::sin(static_cast(i) * 0.4f); + } + std::vector weight_data = { + 0.1f, 0.2f, 0.3f, + 0.4f, 0.5f, 0.6f}; + std::vector bias_data = {0.05f, -0.05f}; + + const int state_length = (kernel_size - 1) * dilation; + std::vector conv_state_data(batch_size * channels * state_length); + for (int i = 0; i < static_cast(conv_state_data.size()); ++i) { + conv_state_data[i] = std::cos(static_cast(i) * 0.25f) * 0.5f; + } + + RunCausalConvWithStateTests( + input_data, weight_data, &bias_data, &conv_state_data, + batch_size, channels, input_length, kernel_size, "silu", dilation); +} + +TEST(CausalConvWithStateTest, DilatedSingleTokenDecode) { + // L=1 exercises the decode path; dilation != 1 must fall back off the fixed-K specializations. + int batch_size = 1, channels = 2, input_length = 1, kernel_size = 3, dilation = 3; + + std::vector input_data = {1.0f, -2.0f}; + std::vector weight_data = { + 0.1f, 0.2f, 0.3f, + 0.4f, 0.5f, 0.6f}; + // state length is (3 - 1) * 3 = 6 + std::vector conv_state_data = { + 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, + -0.1f, -0.2f, -0.3f, -0.4f, -0.5f, -0.6f}; + + RunCausalConvWithStateTests( + input_data, weight_data, nullptr, &conv_state_data, + batch_size, channels, input_length, kernel_size, "none", dilation); +} + +TEST(CausalConvWithStateTest, DilatedLargerDimensions) { + int batch_size = 2, channels = 8, input_length = 16, kernel_size = 4, dilation = 2; + + std::vector input_data(batch_size * channels * input_length); + for (int i = 0; i < static_cast(input_data.size()); ++i) { + input_data[i] = std::sin(static_cast(i) * 0.1f); + } + std::vector weight_data(channels * kernel_size); + for (int i = 0; i < static_cast(weight_data.size()); ++i) { + weight_data[i] = std::cos(static_cast(i) * 0.2f) * 0.5f; + } + std::vector bias_data(channels); + for (int i = 0; i < channels; ++i) { + bias_data[i] = 0.01f * static_cast(i); + } + + const int state_length = (kernel_size - 1) * dilation; + std::vector conv_state_data(batch_size * channels * state_length); + for (int i = 0; i < static_cast(conv_state_data.size()); ++i) { + conv_state_data[i] = std::sin(static_cast(i) * 0.3f) * 0.5f; + } + + RunCausalConvWithStateTests( + input_data, weight_data, &bias_data, &conv_state_data, + batch_size, channels, input_length, kernel_size, "silu", dilation); +} + +// dilation=1 must stay byte-for-byte the undilated behavior so pre-existing models are unaffected. +TEST(CausalConvWithStateTest, DilationOneMatchesDefault) { + int batch_size = 1, channels = 2, input_length = 4, kernel_size = 3; + + std::vector input_data = { + 1.0f, 2.0f, 3.0f, 4.0f, + 0.5f, 1.5f, 2.5f, 3.5f}; + std::vector weight_data = { + 0.1f, 0.2f, 0.3f, + 0.4f, 0.5f, 0.6f}; + std::vector conv_state_data = {-1.0f, 0.5f, 0.3f, -0.7f}; + + std::vector expected_output; + std::vector expected_state; + CausalConvWithStateReference(input_data, weight_data, nullptr, &conv_state_data, + expected_output, expected_state, + batch_size, channels, input_length, kernel_size, "none"); + + // Explicit dilation=1 against the reference computed without any dilation. + RunCausalConvWithStateTest( + input_data, weight_data, nullptr, &conv_state_data, + expected_output, expected_state, + batch_size, channels, input_length, kernel_size, "none", + TensorType::kFloat, /*dilation=*/1); +} + +// A dilated prefill must produce the same result as feeding the same tokens one at a time. +TEST(CausalConvWithStateTest, DilatedSequenceVsTokenByToken) { + int batch_size = 1, channels = 2, kernel_size = 3, dilation = 2; + const int seq_len = 5; + const int state_length = (kernel_size - 1) * dilation; + + std::vector weight_data = { + 0.1f, 0.2f, 0.3f, + 0.4f, 0.5f, 0.6f}; + std::vector bias_data = {0.05f, -0.05f}; + std::vector conv_state(batch_size * channels * state_length, 0.0f); + + std::vector full_input = { + 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, + 0.5f, 1.5f, 2.5f, 3.5f, 4.5f}; + + std::vector full_output; + std::vector full_final_state; + CausalConvWithStateReference(full_input, weight_data, &bias_data, &conv_state, + full_output, full_final_state, + batch_size, channels, seq_len, kernel_size, "none", dilation); + + std::vector current_state = conv_state; + std::vector token_outputs; + for (int t = 0; t < seq_len; ++t) { + std::vector token_input = {full_input[0 * seq_len + t], full_input[1 * seq_len + t]}; + std::vector token_output; + std::vector next_state; + CausalConvWithStateReference(token_input, weight_data, &bias_data, ¤t_state, + token_output, next_state, + batch_size, channels, 1, kernel_size, "none", dilation); + for (int d = 0; d < channels; ++d) { + token_outputs.push_back(token_output[d]); + } + current_state = next_state; + } + + for (int t = 0; t < seq_len; ++t) { + for (int d = 0; d < channels; ++d) { + EXPECT_NEAR(full_output[d * seq_len + t], token_outputs[t * channels + d], 1e-5f) + << "Mismatch at token " << t << " channel " << d; + } + } + for (int i = 0; i < channels * state_length; ++i) { + EXPECT_NEAR(full_final_state[i], current_state[i], 1e-5f) << "State mismatch at index " << i; + } + + // The reference chain above is what the kernels are checked against, so verify the kernel too. + RunCausalConvWithStateTests(full_input, weight_data, &bias_data, &conv_state, + batch_size, channels, seq_len, kernel_size, "none", dilation); +} + +// ============================================================================= +// channels_last tests +// +// channels_last = 1 consumes the (batch_size, sequence_length, ...channels) layout that a +// short-convolution block naturally produces, so the graph does not need a Transpose before and +// after the op. The tests below feed the same numbers through both layouts and compare against +// the same channels-first reference. +// ============================================================================= + +TEST(CausalConvWithStateTest, ChannelsLastNoState) { + int batch_size = 2, channels = 3, input_length = 4, kernel_size = 3; + + std::vector input_data(static_cast(batch_size) * channels * input_length); + for (size_t i = 0; i < input_data.size(); ++i) { + input_data[i] = 0.5f * std::sin(static_cast(i) * 0.37f); + } + std::vector weight_data(static_cast(channels) * kernel_size); + for (size_t i = 0; i < weight_data.size(); ++i) { + weight_data[i] = 0.25f * std::cos(static_cast(i) * 0.21f); + } + + RunCausalConvWithStateTests(input_data, weight_data, nullptr, nullptr, + batch_size, channels, input_length, kernel_size, "none", 1, + /*channels_last=*/true); +} + +TEST(CausalConvWithStateTest, ChannelsLastWithStateAndBias) { + int batch_size = 2, channels = 3, input_length = 5, kernel_size = 3; + int state_length = kernel_size - 1; + + std::vector input_data(static_cast(batch_size) * channels * input_length); + for (size_t i = 0; i < input_data.size(); ++i) { + input_data[i] = 0.5f * std::sin(static_cast(i) * 0.29f); + } + std::vector weight_data(static_cast(channels) * kernel_size); + for (size_t i = 0; i < weight_data.size(); ++i) { + weight_data[i] = 0.25f * std::cos(static_cast(i) * 0.21f); + } + std::vector bias_data(channels); + for (int c = 0; c < channels; ++c) bias_data[c] = 0.01f * static_cast(c) - 0.02f; + std::vector conv_state(static_cast(batch_size) * channels * state_length); + for (size_t i = 0; i < conv_state.size(); ++i) { + conv_state[i] = 0.1f * std::cos(static_cast(i) * 0.3f); + } + + RunCausalConvWithStateTests(input_data, weight_data, &bias_data, &conv_state, + batch_size, channels, input_length, kernel_size, "silu", 1, + /*channels_last=*/true); +} + +// sequence_length == 1 is the decode step. In this layout a token's channels are contiguous, so +// the decode path reads one dense row per sequence. +TEST(CausalConvWithStateTest, ChannelsLastSingleTokenDecode) { + int batch_size = 2, channels = 4, input_length = 1, kernel_size = 4; + int state_length = kernel_size - 1; + + std::vector input_data(static_cast(batch_size) * channels); + for (size_t i = 0; i < input_data.size(); ++i) input_data[i] = 0.3f * static_cast(i) - 0.5f; + std::vector weight_data(static_cast(channels) * kernel_size); + for (size_t i = 0; i < weight_data.size(); ++i) { + weight_data[i] = 0.2f * std::cos(static_cast(i) * 0.4f); + } + std::vector conv_state(static_cast(batch_size) * channels * state_length); + for (size_t i = 0; i < conv_state.size(); ++i) conv_state[i] = 0.05f * static_cast(i) - 0.1f; + + RunCausalConvWithStateTests(input_data, weight_data, nullptr, &conv_state, + batch_size, channels, input_length, kernel_size, "silu", 1, + /*channels_last=*/true); +} + +TEST(CausalConvWithStateTest, ChannelsLastDilated) { + int batch_size = 1, channels = 3, input_length = 6, kernel_size = 3, dilation = 2; + int state_length = (kernel_size - 1) * dilation; + + std::vector input_data(static_cast(batch_size) * channels * input_length); + for (size_t i = 0; i < input_data.size(); ++i) { + input_data[i] = 0.4f * std::sin(static_cast(i) * 0.51f); + } + std::vector weight_data(static_cast(channels) * kernel_size); + for (size_t i = 0; i < weight_data.size(); ++i) { + weight_data[i] = 0.3f * std::cos(static_cast(i) * 0.17f); + } + std::vector conv_state(static_cast(batch_size) * channels * state_length); + for (size_t i = 0; i < conv_state.size(); ++i) conv_state[i] = 0.07f * static_cast(i) - 0.2f; + + RunCausalConvWithStateTests(input_data, weight_data, nullptr, &conv_state, + batch_size, channels, input_length, kernel_size, "silu", dilation, + /*channels_last=*/true); +} + +// A short-convolution block that keeps hyper-connections and hidden size as separate axes feeds +// (batch, sequence, hc_mult, hidden) directly: every trailing axis is a channel axis, so the +// caller needs no Reshape either. +TEST(CausalConvWithStateTest, ChannelsLastMultipleChannelAxes) { + int batch_size = 2, hc_mult = 2, hidden = 3, input_length = 4, kernel_size = 3; + int channels = hc_mult * hidden; + int state_length = kernel_size - 1; + + std::vector input_data(static_cast(batch_size) * channels * input_length); + for (size_t i = 0; i < input_data.size(); ++i) { + input_data[i] = 0.35f * std::sin(static_cast(i) * 0.23f); + } + std::vector weight_data(static_cast(channels) * kernel_size); + for (size_t i = 0; i < weight_data.size(); ++i) { + weight_data[i] = 0.25f * std::cos(static_cast(i) * 0.31f); + } + std::vector bias_data(channels); + for (int c = 0; c < channels; ++c) bias_data[c] = 0.02f * static_cast(c) - 0.05f; + std::vector conv_state(static_cast(batch_size) * channels * state_length); + for (size_t i = 0; i < conv_state.size(); ++i) conv_state[i] = 0.04f * static_cast(i) - 0.15f; + + const std::vector channel_dims = {hc_mult, hidden}; + RunCausalConvWithStateTests(input_data, weight_data, &bias_data, &conv_state, + batch_size, channels, input_length, kernel_size, "silu", 1, + /*channels_last=*/true, &channel_dims); +} + +// channels_last only defines a 1-D causal axis, so it must be rejected for ndim != 1 rather than +// silently reinterpreting the trailing axes. +TEST(CausalConvWithStateTest, ChannelsLastRejectsNdimAboveOne) { + OpTester test("CausalConvWithState", 1, onnxruntime::kMSDomain); + test.AddAttribute("activation", "none"); + test.AddAttribute("channels_last", 1); + test.AddAttribute("ndim", 2); + test.AddInput("input", {1, 2, 1}, {1.0f, 2.0f}); + test.AddInput("weight", {1, 1, 2}, {0.5f, 0.25f}); + test.AddOptionalInputEdge(); + test.AddOptionalInputEdge(); + test.AddOutput("output", {1, 2, 1}, {0.5f, 1.25f}); + test.AddOutput("present_state", {1, 1, 1}, {2.0f}); + test.Run(OpTester::ExpectResult::kExpectFailure, ""); +} + +TEST(CausalConvWithStateTest, ChannelsLastOutOfRangeIsRejected) { + OpTester test("CausalConvWithState", 1, onnxruntime::kMSDomain); + test.AddAttribute("activation", "none"); + test.AddAttribute("channels_last", 2); + test.AddInput("input", {1, 2, 1}, {1.0f, 2.0f}); + test.AddInput("weight", {1, 1, 2}, {0.5f, 0.25f}); + test.AddOptionalInputEdge(); + test.AddOptionalInputEdge(); + test.AddOutput("output", {1, 2, 1}, {0.5f, 1.25f}); + test.AddOutput("present_state", {1, 1, 1}, {2.0f}); + test.Run(OpTester::ExpectResult::kExpectFailure, "channels_last must be 0 or 1"); +} + +TEST(CausalConvWithStateTest, DilationBelowOneIsRejected) { + OpTester test("CausalConvWithState", 1, onnxruntime::kMSDomain); + test.AddAttribute("activation", "none"); + test.AddAttribute("dilation", 0); + test.AddInput("input", {1, 1, 2}, {1.0f, 2.0f}); + test.AddInput("weight", {1, 1, 2}, {0.5f, 0.25f}); + test.AddOptionalInputEdge(); + test.AddOptionalInputEdge(); + test.AddOutput("output", {1, 1, 2}, {0.5f, 1.25f}); + test.AddOutput("present_state", {1, 1, 1}, {2.0f}); + test.Run(OpTester::ExpectResult::kExpectFailure, "dilation must be >= 1"); +} + // The state tensors grow linearly with state_window, so the schema caps it at 8. TEST(CausalConvWithStateTest, StateWindowAboveMaxIsRejected) { OpTester test("CausalConvWithState", 1, onnxruntime::kMSDomain); @@ -711,14 +1115,15 @@ TEST(CausalConvWithStateTest, StateWindowRejectsEmptySequence) { // shape below. When `window` > `input_length` the slots below W - L hold no position from this // call and are zero-filled, with or without a past_state. static void RunCausalConvStateWindowTest(int batch_size, int channels, int input_length, - int kernel_size, int window, bool with_past_state) { + int kernel_size, int window, bool with_past_state, + int dilation = 1, bool channels_last = false) { auto ep = DefaultCudaExecutionProvider(); if (!ep) { GTEST_SKIP() << "CUDA execution provider not available"; return; } - const int state_length = kernel_size - 1; + const int state_length = (kernel_size - 1) * dilation; const std::string activation = "silu"; std::vector input_data(static_cast(batch_size) * channels * input_length); @@ -754,7 +1159,7 @@ static void RunCausalConvStateWindowTest(int batch_size, int channels, int input CausalConvWithStateReference( input_data, weight_data, &bias_data, past, expected_output, expected_state, - batch_size, channels, input_length, kernel_size, activation); + batch_size, channels, input_length, kernel_size, activation, dilation); // Slot j holds the state after the first (input_length - window + j + 1) positions; slots for // non-positive prefixes are never computed by the kernel and stay zero. The window axis leads @@ -773,17 +1178,49 @@ static void RunCausalConvStateWindowTest(int batch_size, int channels, int input CausalConvWithStateReference( slice_prefix(input_data, prefix), weight_data, &bias_data, past, prefix_output, prefix_state, - batch_size, channels, prefix, kernel_size, activation); + batch_size, channels, prefix, kernel_size, activation, dilation); } std::copy_n(prefix_state.begin(), batch_slot_elems, expected_state_window.begin() + static_cast(j) * batch_slot_elems); } + // Every (B, C, length) block above is channels-first; channels_last only permutes the memory + // layout of the activation and state tensors, so convert them here and leave the math alone. + auto to_layout = [&](const std::vector& data, int length) { + return channels_last ? ToChannelsLast(data, batch_size, channels, length) : data; + }; + // The window axis leads the batch axis, so a windowed state tensor is `window` independent + // (B, C, state_length) blocks that each convert on their own. + auto to_layout_windowed = [&](const std::vector& data) { + if (!channels_last) return data; + std::vector out(data.size()); + for (int j = 0; j < window; ++j) { + std::vector slot(data.begin() + static_cast(j) * batch_slot_elems, + data.begin() + static_cast(j + 1) * batch_slot_elems); + std::vector converted = ToChannelsLast(slot, batch_size, channels, state_length); + std::copy(converted.begin(), converted.end(), + out.begin() + static_cast(j) * batch_slot_elems); + } + return out; + }; + const std::vector act_dims = + channels_last ? std::vector{batch_size, input_length, channels} + : std::vector{batch_size, channels, input_length}; + const std::vector state_dims = + channels_last ? std::vector{window, batch_size, state_length, channels} + : std::vector{window, batch_size, channels, state_length}; + OpTester test("CausalConvWithState", 1, onnxruntime::kMSDomain); test.AddAttribute("activation", activation); test.AddAttribute("state_window", static_cast(window)); + if (dilation != 1) { + test.AddAttribute("dilation", static_cast(dilation)); + } + if (channels_last) { + test.AddAttribute("channels_last", static_cast(1)); + } - test.AddInput("input", {batch_size, channels, input_length}, input_data); + test.AddInput("input", act_dims, to_layout(input_data, input_length)); test.AddInput("weight", {channels, 1, kernel_size}, weight_data); test.AddInput("bias", {channels}, bias_data); if (with_past_state) { @@ -791,14 +1228,13 @@ static void RunCausalConvStateWindowTest(int batch_size, int channels, int input std::vector past_state_window(static_cast(window) * batch_slot_elems, -1e4f); std::copy_n(conv_state_data.begin(), batch_slot_elems, past_state_window.begin() + static_cast(window - 1) * batch_slot_elems); - test.AddInput("past_state", {window, batch_size, channels, state_length}, past_state_window); + test.AddInput("past_state", state_dims, to_layout_windowed(past_state_window)); } else { test.AddOptionalInputEdge(); } - test.AddOutput("output", {batch_size, channels, input_length}, expected_output); - test.AddOutput("present_state", {window, batch_size, channels, state_length}, - expected_state_window); + test.AddOutput("output", act_dims, to_layout(expected_output, input_length)); + test.AddOutput("present_state", state_dims, to_layout_windowed(expected_state_window)); test.SetOutputAbsErr("output", 0.01f); test.SetOutputAbsErr("present_state", 0.01f); @@ -839,6 +1275,51 @@ TEST(CausalConvWithStateTest, StateWindow_DecodeGenericK) { /*kernel_size=*/7, /*window=*/3, /*with_past_state=*/false); } +// state_window composes with dilation: state_length becomes (K-1)*dilation, so every slot is +// wider and the prefill kernel's per-slot stride changes with it. +TEST(CausalConvWithStateTest, StateWindow_Dilated) { + RunCausalConvStateWindowTest(/*batch_size=*/2, /*channels=*/8, /*input_length=*/6, + /*kernel_size=*/3, /*window=*/3, /*with_past_state=*/true, + /*dilation=*/2); +} + +// L > 128 with dilation routes to the single-channel prefill kernel instead of the batched one. +TEST(CausalConvWithStateTest, StateWindow_DilatedLongPrefill) { + RunCausalConvStateWindowTest(/*batch_size=*/1, /*channels=*/8, /*input_length=*/140, + /*kernel_size=*/3, /*window=*/4, /*with_past_state=*/true, + /*dilation=*/3); +} + +// dilation > 1 disables the fixed-K decode specialization, so this exercises the generic decode +// kernel's windowed state writes. +TEST(CausalConvWithStateTest, StateWindow_DilatedDecode) { + RunCausalConvStateWindowTest(/*batch_size=*/2, /*channels=*/8, /*input_length=*/1, + /*kernel_size=*/3, /*window=*/3, /*with_past_state=*/true, + /*dilation=*/2); +} + +// state_window composes with channels_last, which selects the channels-last prefill kernel and +// changes the position stride of both the activation and the windowed state tensors. +TEST(CausalConvWithStateTest, StateWindow_ChannelsLast) { + RunCausalConvStateWindowTest(/*batch_size=*/2, /*channels=*/8, /*input_length=*/6, + /*kernel_size=*/4, /*window=*/3, /*with_past_state=*/true, + /*dilation=*/1, /*channels_last=*/true); +} + +// channels_last decode: the strided state layout also disables the fixed-K decode specialization. +TEST(CausalConvWithStateTest, StateWindow_ChannelsLastDecode) { + RunCausalConvStateWindowTest(/*batch_size=*/2, /*channels=*/8, /*input_length=*/1, + /*kernel_size=*/4, /*window=*/3, /*with_past_state=*/true, + /*dilation=*/1, /*channels_last=*/true); +} + +// Both layout attributes at once, on the long-prefill shape. +TEST(CausalConvWithStateTest, StateWindow_ChannelsLastDilated) { + RunCausalConvStateWindowTest(/*batch_size=*/2, /*channels=*/8, /*input_length=*/140, + /*kernel_size=*/3, /*window=*/4, /*with_past_state=*/true, + /*dilation=*/2, /*channels_last=*/true); +} + // W > L with a past_state: the kernel writes only slot W-1, so the leading W-1 slots must come // back zeroed rather than as uninitialized device memory. TEST(CausalConvWithStateTest, StateWindow_DecodeFixedKWithPastState) { @@ -939,7 +1420,7 @@ TEST(ContribOpVarlenCausalConvWithStateTest, SchemaResolution) { namespace { // Returns a CUDA EP with the VarlenCausalConvWithState kernel registered, or nullptr. Unlike the -// dense op (also servable from WebGPU/CPU via TryGetEpWithCausalConvWithState), Varlen* ops are +// dense op (also servable from WebGPU/CPU via GetEpsWithCausalConvWithState), Varlen* ops are // CUDA-only, so tests skip outright instead of falling back to another EP. std::unique_ptr TryGetCudaEpWithVarlenCausalConvWithState() { auto ep = DefaultCudaExecutionProvider(); @@ -992,6 +1473,7 @@ struct VarlenCausalConvCase { std::vector seq_lens; int channels = 4; int kernel_size = 3; + int dilation = 1; std::string activation = "silu"; bool with_bias = true; bool with_initial_state = false; @@ -1017,7 +1499,7 @@ void RunVarlenCausalConvCase(const VarlenCausalConvCase& c) { const int B = static_cast(c.seq_lens.size()); const int D = c.channels; const int K = c.kernel_size; - const int pad = K - 1; + const int pad = (K - 1) * c.dilation; const int C = c.state_update_capacity; const size_t slot_elems = static_cast(D) * pad; @@ -1070,7 +1552,7 @@ void RunVarlenCausalConvCase(const VarlenCausalConvCase& c) { std::vector output_i, final_state_i; CausalConvWithStateReference(input, weight, bias_ptr, past, output_i, final_state_i, - 1, D, L, K, c.activation); + 1, D, L, K, c.activation, c.dilation); std::vector input_td = TransposeDL_to_LD(input, D, L); std::vector output_td = TransposeDL_to_LD(output_i, D, L); @@ -1086,7 +1568,7 @@ void RunVarlenCausalConvCase(const VarlenCausalConvCase& c) { std::vector prefix_output, prefix_state; const std::vector input_prefix = SliceCausalConvPrefix(input, D, L, t + 1); CausalConvWithStateReference(input_prefix, weight, bias_ptr, past, prefix_output, prefix_state, - 1, D, t + 1, K, c.activation); + 1, D, t + 1, K, c.activation, c.dilation); std::copy(prefix_state.begin(), prefix_state.end(), sequential_states.begin() + (static_cast(i) * C + t) * slot_elems); @@ -1102,6 +1584,9 @@ void RunVarlenCausalConvCase(const VarlenCausalConvCase& c) { OpTester tester("VarlenCausalConvWithState", 1, onnxruntime::kMSDomain); tester.AddAttribute("activation", c.activation); + if (c.dilation != 1) { + tester.AddAttribute("dilation", static_cast(c.dilation)); + } if (C > 0) { tester.AddAttribute("state_update_capacity", static_cast(C)); } @@ -1277,6 +1762,55 @@ TEST(ContribOpVarlenCausalConvWithStateTest, NoActivation) { RunVarlenCausalConvCase(c); } +// dilation widens the carry state to (kernel_size - 1) * dilation while keeping the packed +// token-major layout, so the ragged path must stride its taps rather than read adjacent tokens. +TEST(ContribOpVarlenCausalConvWithStateTest, DilatedRagged) { + VarlenCausalConvCase c; + c.seq_lens = {5, 1, 3}; + c.kernel_size = 3; + c.dilation = 2; + c.with_initial_state = true; + RunVarlenCausalConvCase(c); +} + +// One token per request selects the decode fast path, which must apply the same tap stride. +TEST(ContribOpVarlenCausalConvWithStateTest, DilatedDecode) { + VarlenCausalConvCase c; + c.seq_lens = {1, 1, 1}; + c.kernel_size = 4; + c.dilation = 3; + c.with_initial_state = true; + c.state_update_capacity = 1; + c.capture_count = {1, 1, 1}; + RunVarlenCausalConvCase(c); +} + +// Multi-token requests with a positive state_update_capacity: the ragged (non-decode) path must +// capture the compact state update while striding its taps. DilatedRagged leaves the capacity at +// zero and DilatedDecode only reaches the decode kernel, so this is the only case that runs the +// general kernel's capture with dilation > 1. The capture counts deliberately straddle both the +// capacity and the per-request sequence length so the clamping is exercised too. +TEST(ContribOpVarlenCausalConvWithStateTest, DilatedRaggedWithStateUpdate) { + VarlenCausalConvCase c; + c.seq_lens = {5, 1, 3}; + c.kernel_size = 3; + c.dilation = 2; + c.with_initial_state = true; + c.state_update_capacity = 4; + c.capture_count = {5, 0, 2}; + RunVarlenCausalConvCase(c); +} + +TEST(ContribOpVarlenCausalConvWithStateTest, DilatedFp16) { + VarlenCausalConvCase c; + c.seq_lens = {4, 2}; + c.kernel_size = 3; + c.dilation = 2; + c.with_initial_state = true; + c.use_fp16 = true; + RunVarlenCausalConvCase(c); +} + TEST(ContribOpVarlenCausalConvWithStateTest, SwishActivation) { VarlenCausalConvCase c; c.seq_lens = {2, 3}; diff --git a/onnxruntime/test/contrib_ops/engram_ops_test.cc b/onnxruntime/test/contrib_ops/engram_ops_test.cc new file mode 100644 index 0000000000000..2c79cefd853bb --- /dev/null +++ b/onnxruntime/test/contrib_ops/engram_ops_test.cc @@ -0,0 +1,720 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "gtest/gtest.h" +#include "core/framework/execution_provider.h" +#include "core/framework/sequential_execution_plan.h" +#include "core/framework/session_state.h" +#include "core/graph/model.h" +#include "core/graph/node_attr_utils.h" +#include "core/session/inference_session.h" +#include "test/common/tensor_op_test_utils.h" +#include "test/providers/provider_test_utils.h" +#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/inference_session_wrapper.h" +#include "test/util/include/test_environment.h" + +namespace onnxruntime { +namespace test { + +namespace { + +constexpr float kEpsilon = 1.0e-5f; + +float Sigmoid(float x) { + if (x > 0.0f) { + return 1.0f / (1.0f + std::exp(-x)); + } + const float exp_x = std::exp(x); + return exp_x / (1.0f + exp_x); +} + +template +std::vector ToTensorType(const std::vector& data) { + if constexpr (std::is_same_v) { + return ToFloat16(data); + } else if constexpr (std::is_same_v) { + return ToBFloat16(data); + } else { + return data; + } +} + +// Returns false when the execution provider required by T is unavailable. This must be checked before +// an OpTester is constructed: BaseTester's destructor traps when a tester is destroyed without running. +template +bool IsTypeSupported() { + if constexpr (std::is_same_v) { + return DefaultCudaExecutionProvider() != nullptr; + } else { + return true; + } +} + +// Runs the tester on CUDA only for BFloat16, otherwise on the default set of execution providers. +template +void RunOnSupportedProviders(OpTester& test) { + if constexpr (std::is_same_v) { + std::vector> providers; + providers.push_back(DefaultCudaExecutionProvider()); + test.Run(OpTester::ExpectResult::kExpectSuccess, "", {}, nullptr, &providers); + } else { + test.Run(); + } +} + +// Deterministic values bounded to [-1, 1]. Deterministic inputs keep the expectations in the +// vectorized tests reproducible, and the bound matters because a plain ramp would grow without +// limit over the hundreds of channels the multi-iteration reduction test needs, saturating the gate +// and hiding exactly the accumulation errors that case is meant to catch. +std::vector MakeWave(size_t count, float phase, float step) { + std::vector values(count); + for (size_t i = 0; i < count; ++i) { + values[i] = std::sin(phase + step * static_cast(i)); + } + return values; +} + +// Reference for the gate pre-activation, sign(dot) * sqrt(max(abs(dot), 1e-6)). +// std::copysign is deliberately avoided: it maps a zero dot product to +sqrt(1e-6) instead of zero. +float GateArg(float dot) { + if (dot == 0.0f) { + return 0.0f; + } + const float magnitude = std::sqrt(std::max(std::abs(dot), 1.0e-6f)); + return dot < 0.0f ? -magnitude : magnitude; +} + +// --------------------------------------------------------------------------------------------- +// EngramGate +// --------------------------------------------------------------------------------------------- + +// Reference EngramGate for a single (token, hyper-connection) row with unit norm scales. +float EngramGateReference(const std::vector& key, const std::vector& query) { + const auto hidden_size = static_cast(key.size()); + float key_sum_sq = 0.0f; + float query_sum_sq = 0.0f; + for (size_t c = 0; c < key.size(); ++c) { + key_sum_sq += key[c] * key[c]; + query_sum_sq += query[c] * query[c]; + } + const float key_inv = 1.0f / std::sqrt(key_sum_sq / hidden_size + kEpsilon); + const float query_inv = 1.0f / std::sqrt(query_sum_sq / hidden_size + kEpsilon); + float dot = 0.0f; + for (size_t c = 0; c < key.size(); ++c) { + dot += key[c] * key_inv * query[c] * query_inv; + } + dot /= std::sqrt(hidden_size); + return Sigmoid(GateArg(dot)); +} + +template +void RunEngramGateTest(float tolerance) { + if (!IsTypeSupported()) { + GTEST_SKIP() << "No execution provider available for this type"; + } + const std::vector key{0.0f, 2.5f}; + const std::vector query{3.0f, 4.0f}; + const std::vector value{2.0f, -1.5f}; + const std::vector unit_scale{1.0f, 1.0f}; + + const float gate = EngramGateReference(key, query); + const std::vector expected{gate * value[0], gate * value[1]}; + + OpTester test("EngramGate", 1, kMSDomain); + test.AddAttribute("epsilon", kEpsilon); + test.AddInput("key", {1, 1, 1, 2}, ToTensorType(key)); + test.AddInput("query", {1, 1, 1, 2}, ToTensorType(query)); + test.AddInput("value", {1, 1, 2}, ToTensorType(value)); + test.AddInput("key_norm_scale", {1, 2}, ToTensorType(unit_scale)); + test.AddInput("query_norm_scale", {1, 2}, ToTensorType(unit_scale)); + test.AddOutput("output", {1, 1, 1, 2}, ToTensorType(expected), false, tolerance, tolerance); + RunOnSupportedProviders(test); +} + +// Exercises hc_mult > 1 and non-unit norm scales for an arbitrary hidden_size. hidden_size == 4 +// selects the WebGPU vec4 component path through the gate reduction and the broadcast pass, and +// hc_mult > 1 makes a per-row rather than per-token scale lookup observable. +// +// Both GPU reductions stride over hidden_size (CUDA by blockDim.x == 256, WGSL by the workgroup size +// 64 over hidden_size / components), so only a hidden_size above those strides takes a second +// iteration and actually accumulates into the per-thread partials. +template +void RunEngramGateVectorizedTest(float tolerance, int64_t hidden) { + if (!IsTypeSupported()) { + GTEST_SKIP() << "No execution provider available for this type"; + } + constexpr int64_t kBatch = 1; + constexpr int64_t kSequence = 2; + constexpr int64_t kHcMult = 2; + const int64_t rows = kBatch * kSequence * kHcMult; + + const std::vector key = MakeWave(static_cast(rows * hidden), -0.9f, 0.3f); + const std::vector query = MakeWave(static_cast(rows * hidden), 1.2f, -0.25f); + const std::vector value = MakeWave(static_cast(kBatch * kSequence * hidden), 0.4f, 0.35f); + const std::vector key_scale = MakeWave(static_cast(kHcMult * hidden), 0.6f, 0.1f); + const std::vector query_scale = MakeWave(static_cast(kHcMult * hidden), 1.4f, -0.15f); + + std::vector expected(static_cast(rows * hidden)); + for (int64_t row = 0; row < rows; ++row) { + const int64_t g = row % kHcMult; + const int64_t token = row / kHcMult; + float key_sum_sq = 0.0f; + float query_sum_sq = 0.0f; + for (int64_t c = 0; c < hidden; ++c) { + const float k = key[static_cast(row * hidden + c)]; + const float q = query[static_cast(row * hidden + c)]; + key_sum_sq += k * k; + query_sum_sq += q * q; + } + const float key_inv = 1.0f / std::sqrt(key_sum_sq / static_cast(hidden) + kEpsilon); + const float query_inv = 1.0f / std::sqrt(query_sum_sq / static_cast(hidden) + kEpsilon); + float dot = 0.0f; + for (int64_t c = 0; c < hidden; ++c) { + const auto scale_index = static_cast(g * hidden + c); + const float normed_key = key[static_cast(row * hidden + c)] * key_inv * key_scale[scale_index]; + const float normed_query = + query[static_cast(row * hidden + c)] * query_inv * query_scale[scale_index]; + dot += normed_key * normed_query; + } + dot /= std::sqrt(static_cast(hidden)); + const float gate = Sigmoid(GateArg(dot)); + for (int64_t c = 0; c < hidden; ++c) { + expected[static_cast(row * hidden + c)] = gate * value[static_cast(token * hidden + c)]; + } + } + + OpTester test("EngramGate", 1, kMSDomain); + test.AddAttribute("epsilon", kEpsilon); + test.AddInput("key", {kBatch, kSequence, kHcMult, hidden}, ToTensorType(key)); + test.AddInput("query", {kBatch, kSequence, kHcMult, hidden}, ToTensorType(query)); + test.AddInput("value", {kBatch, kSequence, hidden}, ToTensorType(value)); + test.AddInput("key_norm_scale", {kHcMult, hidden}, ToTensorType(key_scale)); + test.AddInput("query_norm_scale", {kHcMult, hidden}, ToTensorType(query_scale)); + test.AddOutput("output", {kBatch, kSequence, kHcMult, hidden}, ToTensorType(expected), false, tolerance, + tolerance); + RunOnSupportedProviders(test); +} + +// --------------------------------------------------------------------------------------------- +// NGramHashMapping +// --------------------------------------------------------------------------------------------- + +constexpr int64_t kMaxNGramSize = 3; +constexpr int64_t kHeadsPerNGram = 2; +constexpr int64_t kPadId = 9; + +// Reference NGramHashMapping for a single batch row. `history` holds the kMaxNGramSize - 1 ids that +// precede `ids`, right-aligned, and lets the same reference cover both the full and the chunked runs. +template +std::vector NGramHashMappingReference(const std::vector& ids, + const std::vector& history, + const std::vector& multipliers, + const std::vector& vocab_sizes, + int64_t pad_id = kPadId) { + const int64_t sequence_length = static_cast(ids.size()); + const int64_t state_length = kMaxNGramSize - 1; + const int64_t num_heads = state_length * kHeadsPerNGram; + std::vector output(static_cast(sequence_length * num_heads)); + + auto id_at = [&](int64_t t) -> T { + if (t >= 0) { + return ids[static_cast(t)]; + } + const int64_t slot = state_length + t; + if (history.empty() || slot < 0) { + return static_cast(pad_id); + } + return history[static_cast(slot)]; + }; + + for (int64_t t = 0; t < sequence_length; ++t) { + for (int64_t n = 2; n <= kMaxNGramSize; ++n) { + T mix = 0; + for (int64_t k = 0; k < n; ++k) { + // Multiplication wraps on overflow, matching the kernel's unsigned arithmetic. + using U = std::make_unsigned_t; + const T product = static_cast(static_cast(id_at(t - k)) * + static_cast(multipliers[static_cast(k)])); + mix = k == 0 ? product : static_cast(mix ^ product); + } + for (int64_t h = 0; h < kHeadsPerNGram; ++h) { + const int64_t out_h = (n - 2) * kHeadsPerNGram + h; + const T mod = vocab_sizes[static_cast(out_h)]; + T value = static_cast(mix % mod); + if (value < 0) { + value = static_cast(value + mod); + } + output[static_cast(t * num_heads + out_h)] = value; + } + } + } + return output; +} + +// Negative ids and a negative pad_id are the only way to reach two branches that the positive-id +// tests leave dead on every EP: the `result < 0 -> result + mod` correction in PositiveMod, and the +// sign handling in WrappedMultiply. WGSL's `%` in particular follows C truncation for negative +// operands, which is worth pinning rather than assuming. +template +void RunNGramHashMappingNegativeIdsTest() { + constexpr int64_t kNegativePadId = -4; + const std::vector ids{-5, 7, -3, 2}; + const std::vector multipliers{11, 13, 17}; + const std::vector vocab_sizes{101, 103, 107, 109}; + const std::vector expected = + NGramHashMappingReference(ids, {}, multipliers, vocab_sizes, kNegativePadId); + // Pins the reference, and the values themselves: every entry is a positive residue even though + // most of the underlying mixes are negative, which is exactly the PositiveMod correction. + ASSERT_EQ(expected, (std::vector{5, 5, 36, 38, + 87, 89, 78, 78, + 78, 82, 47, 47, + 52, 54, 35, 37})); + for (const T value : expected) { + ASSERT_GE(value, 0); + } + + OpTester test("NGramHashMapping", 1, kMSDomain); + test.AddAttribute("max_ngram_size", kMaxNGramSize); + test.AddAttribute("n_head_per_ngram", kHeadsPerNGram); + test.AddAttribute("pad_id", kNegativePadId); + test.AddInput("input_ids", {1, 4}, ids); + test.AddInput("multipliers", {3}, multipliers); + test.AddInput("vocab_sizes", {4}, vocab_sizes); + test.AddOptionalInputEdge(); + test.AddOutput("hash_ids", {1, 4, 4}, expected); + test.AddOutput("present_ids", {1, 2}, {ids[2], ids[3]}); + test.Run(); +} + +// A non-positive head vocabulary size has no meaningful modulo. The CPU kernel rejects it rather +// than silently emitting a constant hash id of 0 for that head. +template +void RunNGramHashMappingNonPositiveVocabTest() { + const std::vector ids{3, 4, 5, 6}; + const std::vector multipliers{11, 13, 17}; + // Head 2 is invalid; the other three are the usual primes. + const std::vector vocab_sizes{101, 103, 0, 109}; + + OpTester test("NGramHashMapping", 1, kMSDomain); + test.AddAttribute("max_ngram_size", kMaxNGramSize); + test.AddAttribute("n_head_per_ngram", kHeadsPerNGram); + test.AddAttribute("pad_id", kPadId); + test.AddInput("input_ids", {1, 4}, ids); + test.AddInput("multipliers", {3}, multipliers); + test.AddInput("vocab_sizes", {4}, vocab_sizes); + test.AddOptionalInputEdge(); + test.AddOutput("hash_ids", {1, 4, 4}, std::vector(16, T{0})); + test.AddOutput("present_ids", {1, 2}, {ids[2], ids[3]}); + // The validation is CPU-only by design: on GPU EPs vocab_sizes lives on the device and checking it + // would force a synchronization on every Compute call. + std::vector> execution_providers; + execution_providers.push_back(DefaultCpuExecutionProvider()); + test.Run(OpTester::ExpectResult::kExpectFailure, "vocab_sizes must be positive", {}, nullptr, + &execution_providers); +} + +template +void RunNGramHashMappingTest() { + const std::vector ids{3, 4, 5, 6}; + const std::vector multipliers{11, 13, 17}; + const std::vector vocab_sizes{101, 103, 107, 109}; + const std::vector expected = NGramHashMappingReference(ids, {}, multipliers, vocab_sizes); + // Guards the reference itself against silent drift. + ASSERT_EQ(expected, (std::vector{84, 84, 98, 96, + 11, 11, 39, 37, + 3, 3, 48, 48, + 3, 3, 71, 71})); + + OpTester test("NGramHashMapping", 1, kMSDomain); + test.AddAttribute("max_ngram_size", kMaxNGramSize); + test.AddAttribute("n_head_per_ngram", kHeadsPerNGram); + test.AddAttribute("pad_id", kPadId); + test.AddInput("input_ids", {1, 4}, ids); + test.AddInput("multipliers", {3}, multipliers); + test.AddInput("vocab_sizes", {4}, vocab_sizes); + test.AddOptionalInputEdge(); + test.AddOutput("hash_ids", {1, 4, 4}, expected); + test.AddOutput("present_ids", {1, 2}, {ids[2], ids[3]}); + test.Run(); +} + +// A decode step must hash the same n-gram window as the corresponding position of a full-sequence +// run. Without past_ids the preceding tokens would silently fall back to pad_id. +template +void RunNGramHashMappingChunkedTest() { + const std::vector ids{3, 4, 5, 6}; + const std::vector multipliers{11, 13, 17}; + const std::vector vocab_sizes{101, 103, 107, 109}; + const std::vector full = NGramHashMappingReference(ids, {}, multipliers, vocab_sizes); + + auto run_chunk = [&](const std::vector& chunk, const std::vector& past, + const std::vector& expected_hash_ids, const std::vector& expected_present) { + OpTester test("NGramHashMapping", 1, kMSDomain); + test.AddAttribute("max_ngram_size", kMaxNGramSize); + test.AddAttribute("n_head_per_ngram", kHeadsPerNGram); + test.AddAttribute("pad_id", kPadId); + test.AddInput("input_ids", {1, static_cast(chunk.size())}, chunk); + test.AddInput("multipliers", {3}, multipliers); + test.AddInput("vocab_sizes", {4}, vocab_sizes); + if (past.empty()) { + test.AddOptionalInputEdge(); + } else { + test.AddInput("past_ids", {1, 2}, past); + } + test.AddOutput("hash_ids", {1, static_cast(chunk.size()), 4}, expected_hash_ids); + test.AddOutput("present_ids", {1, 2}, expected_present); + test.Run(); + }; + + // Prefill of the first two tokens, then a decode step per remaining token, threading present_ids. + const std::vector prefill{ids[0], ids[1]}; + run_chunk(prefill, {}, std::vector(full.begin(), full.begin() + 8), {ids[0], ids[1]}); + + // Decode token 2 with the prefill history. + run_chunk({ids[2]}, {ids[0], ids[1]}, std::vector(full.begin() + 8, full.begin() + 12), + {ids[1], ids[2]}); + + // Decode token 3 with the history returned by the previous step. + run_chunk({ids[3]}, {ids[1], ids[2]}, std::vector(full.begin() + 12, full.end()), + {ids[2], ids[3]}); +} + +// An empty input_ids tensor must still thread history through present_ids unchanged. This is the +// only case that reaches the WebGPU kernel's sequence_length == 0 specialization, which drops the +// input_ids binding entirely because WebGPU rejects zero-sized storage bindings. +template +void RunNGramHashMappingEmptySequenceTest() { + const std::vector multipliers{11, 13, 17}; + const std::vector vocab_sizes{101, 103, 107, 109}; + const std::vector past{3, 4}; + + auto run = [&](bool with_past) { + OpTester test("NGramHashMapping", 1, kMSDomain); + test.AddAttribute("max_ngram_size", kMaxNGramSize); + test.AddAttribute("n_head_per_ngram", kHeadsPerNGram); + test.AddAttribute("pad_id", kPadId); + test.AddInput("input_ids", {1, 0}, {}); + test.AddInput("multipliers", {3}, multipliers); + test.AddInput("vocab_sizes", {4}, vocab_sizes); + if (with_past) { + test.AddInput("past_ids", {1, 2}, past); + } else { + test.AddOptionalInputEdge(); + } + test.AddOutput("hash_ids", {1, 0, 4}, {}); + // With no new tokens the window is unchanged; without a past_ids it is all pad_id. + test.AddOutput("present_ids", {1, 2}, + with_past ? past : std::vector{static_cast(kPadId), static_cast(kPadId)}); + test.Run(); + }; + + run(/*with_past=*/true); + run(/*with_past=*/false); +} + +// Batch strides are only observable when batch_size > 1: with a single row every `b * stride` term +// is zero, so a wrong stride in the hash kernel or in the present-state walk is invisible. +template +void RunNGramHashMappingBatchedTest() { + constexpr int64_t kBatch = 3; + constexpr int64_t kSequence = 4; + const std::vector multipliers{11, 13, 17}; + const std::vector vocab_sizes{101, 103, 107, 109}; + // Distinct per-row values so a row picked up from the wrong batch offset changes the result. + const std::vector> rows{{3, 4, 5, 6}, {17, 2, 31, 8}, {40, 41, 42, 43}}; + const std::vector> past{{1, 2}, {19, 23}, {29, 37}}; + + std::vector ids; + std::vector past_ids; + std::vector expected_hash; + std::vector expected_present; + for (int64_t b = 0; b < kBatch; ++b) { + const auto& row = rows[static_cast(b)]; + const auto& history = past[static_cast(b)]; + const std::vector row_hash = NGramHashMappingReference(row, history, multipliers, vocab_sizes); + ids.insert(ids.end(), row.begin(), row.end()); + past_ids.insert(past_ids.end(), history.begin(), history.end()); + expected_hash.insert(expected_hash.end(), row_hash.begin(), row_hash.end()); + expected_present.insert(expected_present.end(), row.end() - (kMaxNGramSize - 1), row.end()); + } + + OpTester test("NGramHashMapping", 1, kMSDomain); + test.AddAttribute("max_ngram_size", kMaxNGramSize); + test.AddAttribute("n_head_per_ngram", kHeadsPerNGram); + test.AddAttribute("pad_id", kPadId); + test.AddInput("input_ids", {kBatch, kSequence}, ids); + test.AddInput("multipliers", {3}, multipliers); + test.AddInput("vocab_sizes", {4}, vocab_sizes); + test.AddInput("past_ids", {kBatch, kMaxNGramSize - 1}, past_ids); + test.AddOutput("hash_ids", {kBatch, kSequence, (kMaxNGramSize - 1) * kHeadsPerNGram}, expected_hash); + test.AddOutput("present_ids", {kBatch, kMaxNGramSize - 1}, expected_present); + test.Run(); +} + +// `MayInplace(3, 1)` is only a hint: the allocation planner honors it when past_ids is an +// intermediate whose last consumer is the node, and never for a graph input or a graph output. +// OpTester always feeds past_ids as a graph input, so the barrier-separated read/write design in the +// CUDA and WebGPU present-state kernels is unreachable from those tests. Chaining three decode steps +// makes the middle node's past_ids an intermediate with a single consumer, which is exactly the +// shape the planner aliases -- so the middle node runs with past_ids and present_ids on one buffer. +template +void RunNGramHashMappingInPlaceTest(std::unique_ptr ep) { + constexpr int64_t kBatch = 2; + constexpr int64_t kStateLength = kMaxNGramSize - 1; + constexpr int64_t kNumHeads = kStateLength * kHeadsPerNGram; + constexpr size_t kSteps = 3; + + const std::vector multipliers{11, 13, 17}; + const std::vector vocab_sizes{101, 103, 107, 109}; + // Per batch row: the initial window, then one new token per decode step. + const std::vector> initial_past{{1, 2}, {19, 23}}; + const std::vector> step_tokens{{3, 31}, {4, 37}, {5, 41}}; + + std::vector past_ids_feed; + for (const auto& row : initial_past) { + past_ids_feed.insert(past_ids_feed.end(), row.begin(), row.end()); + } + + // Reference: replay the decode loop on the host, one batch row at a time. + std::vector> history = initial_past; + std::vector> expected_hash(kSteps); + std::vector expected_final_present; + for (size_t step = 0; step < kSteps; ++step) { + for (int64_t b = 0; b < kBatch; ++b) { + const std::vector chunk{step_tokens[step][static_cast(b)]}; + const std::vector row_hash = + NGramHashMappingReference(chunk, history[static_cast(b)], multipliers, vocab_sizes); + expected_hash[step].insert(expected_hash[step].end(), row_hash.begin(), row_hash.end()); + auto& row_history = history[static_cast(b)]; + row_history.erase(row_history.begin()); + row_history.push_back(chunk[0]); + } + } + for (const auto& row : history) { + expected_final_present.insert(expected_final_present.end(), row.begin(), row.end()); + } + + std::unordered_map domain_to_version{{kOnnxDomain, 17}, {kMSDomain, 1}}; + Model model("NGramHashMappingInPlace", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), domain_to_version, {}, + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + ModelTestBuilder builder(graph); + + NodeAttributes attributes; + attributes["max_ngram_size"] = utils::MakeAttribute(std::string("max_ngram_size"), kMaxNGramSize); + attributes["n_head_per_ngram"] = utils::MakeAttribute(std::string("n_head_per_ngram"), kHeadsPerNGram); + attributes["pad_id"] = utils::MakeAttribute(std::string("pad_id"), kPadId); + + auto* multipliers_arg = builder.MakeInput({kMaxNGramSize}, multipliers); + auto* vocab_sizes_arg = builder.MakeInput({kNumHeads}, vocab_sizes); + NodeArg* past_arg = builder.MakeInput({kBatch, kStateLength}, past_ids_feed); + + std::vector present_names; + for (size_t step = 0; step < kSteps; ++step) { + auto* input_ids_arg = builder.MakeInput({kBatch, 1}, step_tokens[step]); + auto* hash_arg = builder.MakeOutput(); + // Only the last step's present_ids is a graph output; the planner refuses to alias those. + const bool is_last = step + 1 == kSteps; + auto* present_arg = is_last ? builder.MakeOutput() : builder.MakeIntermediate(); + builder.AddNode("NGramHashMapping", {input_ids_arg, multipliers_arg, vocab_sizes_arg, past_arg}, + {hash_arg, present_arg}, kMSDomain, &attributes); + present_names.push_back(present_arg->Name()); + past_arg = present_arg; + } + builder.SetGraphOutputs(); + ASSERT_STATUS_OK(graph.Resolve()); + + std::string model_data; + model.ToProto().SerializeToString(&model_data); + + SessionOptions session_options; + InferenceSessionWrapper session{session_options, GetEnvironment()}; + if (ep != nullptr) { + ASSERT_STATUS_OK(session.RegisterExecutionProvider(std::move(ep))); + } + std::istringstream model_istream(model_data); + ASSERT_STATUS_OK(session.Load(model_istream)); + ASSERT_STATUS_OK(session.Initialize()); + + // Pin the aliasing itself: without this the test would silently degrade to a plain chained run if + // the hint were dropped or the planner changed. + const SessionState& session_state = session.GetSessionState(); + int middle_present = -1; + int middle_past = -1; + ASSERT_STATUS_OK(session_state.GetOrtValueNameIdxMap().GetIdx(present_names[1], middle_present)); + ASSERT_STATUS_OK(session_state.GetOrtValueNameIdxMap().GetIdx(present_names[0], middle_past)); + const auto& alloc_plan = session_state.GetPerValueAllocPlan(); + EXPECT_EQ(alloc_plan[static_cast(middle_present)].alloc_kind, AllocKind::kReuse); + EXPECT_EQ(alloc_plan[static_cast(middle_present)].reused_buffer, middle_past); + + std::vector fetches; + ASSERT_STATUS_OK(session.Run(RunOptions{}, builder.feeds_, builder.output_names_, &fetches)); + + // MakeOutput() appends to output_names_ in creation order, so the fetches are hash_0, hash_1, + // hash_2, then the final present_ids. + ASSERT_EQ(fetches.size(), kSteps + 1); + for (size_t step = 0; step < kSteps; ++step) { + const Tensor& hash = fetches[step].Get(); + ASSERT_EQ(hash.Shape(), TensorShape({kBatch, 1, kNumHeads})); + const auto span = hash.DataAsSpan(); + EXPECT_EQ(std::vector(span.begin(), span.end()), expected_hash[step]) << "step " << step; + } + const Tensor& present = fetches[kSteps].Get(); + ASSERT_EQ(present.Shape(), TensorShape({kBatch, kStateLength})); + const auto present_span = present.DataAsSpan(); + EXPECT_EQ(std::vector(present_span.begin(), present_span.end()), expected_final_present); +} + +} // namespace + +TEST(EngramOpsTest, NGramHashMappingEmptySequenceInt64) { + RunNGramHashMappingEmptySequenceTest(); +} + +// int32 is the only type the WebGPU kernel supports, so this is what covers its zero-length +// specialization. +TEST(EngramOpsTest, NGramHashMappingEmptySequenceInt32) { + RunNGramHashMappingEmptySequenceTest(); +} + +TEST(EngramOpsTest, NGramHashMappingInt64) { + RunNGramHashMappingTest(); +} + +// int32 is the only type the WebGPU kernel supports, so it must be covered explicitly. +TEST(EngramOpsTest, NGramHashMappingInt32) { + RunNGramHashMappingTest(); +} + +TEST(EngramOpsTest, NGramHashMappingChunkedMatchesFullSequenceInt64) { + RunNGramHashMappingChunkedTest(); +} + +// int32 is the only type the WebGPU kernel supports, so this is the case that gives the WebGPU +// past_ids/present_ids shaders any execution coverage at all. +TEST(EngramOpsTest, NGramHashMappingChunkedMatchesFullSequenceInt32) { + RunNGramHashMappingChunkedTest(); +} + +TEST(EngramOpsTest, NGramHashMappingNegativeIdsInt64) { + RunNGramHashMappingNegativeIdsTest(); +} + +TEST(EngramOpsTest, NGramHashMappingNegativeIdsInt32) { + RunNGramHashMappingNegativeIdsTest(); +} + +TEST(EngramOpsTest, NGramHashMappingRejectsNonPositiveVocabSizeInt64) { + RunNGramHashMappingNonPositiveVocabTest(); +} + +TEST(EngramOpsTest, NGramHashMappingRejectsNonPositiveVocabSizeInt32) { + RunNGramHashMappingNonPositiveVocabTest(); +} + +TEST(EngramOpsTest, NGramHashMappingBatchedInt64) { + RunNGramHashMappingBatchedTest(); +} + +TEST(EngramOpsTest, NGramHashMappingBatchedInt32) { + RunNGramHashMappingBatchedTest(); +} + +TEST(EngramOpsTest, NGramHashMappingInPlaceCpu) { + RunNGramHashMappingInPlaceTest(nullptr); + RunNGramHashMappingInPlaceTest(nullptr); +} + +#ifdef USE_CUDA +TEST(EngramOpsTest, NGramHashMappingInPlaceCuda) { + if (DefaultCudaExecutionProvider() == nullptr) { + GTEST_SKIP() << "CUDA execution provider is not available"; + } + RunNGramHashMappingInPlaceTest(DefaultCudaExecutionProvider()); + RunNGramHashMappingInPlaceTest(DefaultCudaExecutionProvider()); +} +#endif + +#ifdef USE_WEBGPU +// int32 is the only type the WebGPU kernel registers. +TEST(EngramOpsTest, NGramHashMappingInPlaceWebGpu) { + // A null provider is the CPU convention in RunNGramHashMappingInPlaceTest, so fail closed here + // instead of silently degrading into a CPU run that never exercises NGramPresentIdsProgram. + auto webgpu_ep = DefaultWebGpuExecutionProvider(); + if (webgpu_ep == nullptr) { + GTEST_SKIP() << "WebGPU execution provider is not available"; + } + RunNGramHashMappingInPlaceTest(std::move(webgpu_ep)); +} +#endif + +TEST(EngramOpsTest, EngramGateFloat) { + RunEngramGateTest(1e-4f); +} + +TEST(EngramOpsTest, EngramGateFloat16) { + RunEngramGateTest(2e-3f); +} + +TEST(EngramOpsTest, EngramGateVectorizedFloat) { + RunEngramGateVectorizedTest(1e-4f, 4); +} + +TEST(EngramOpsTest, EngramGateVectorizedFloat16) { + RunEngramGateVectorizedTest(3e-3f, 4); +} + +// 260 channels is the smallest multiple of 4 above both accumulation strides (CUDA's blockDim.x of +// 256 and, with components == 4, the WGSL workgroup size of 64 over hidden_size / 4 == 65), so this +// is the only case where either strided reduction loop runs more than once per thread. +TEST(EngramOpsTest, EngramGateMultiIterationReductionFloat) { + RunEngramGateVectorizedTest(1e-4f, 260); +} + +TEST(EngramOpsTest, EngramGateMultiIterationReductionFloat16) { + RunEngramGateVectorizedTest(5e-3f, 260); +} + +TEST(EngramOpsTest, EngramGateBFloat16) { + RunEngramGateTest(2e-2f); +} + +// A zero dot product must produce a gate of exactly 0.5 on every EP. Orthogonal key/query rows make +// the dot product vanish, which would silently become sigmoid(sqrt(1e-6)) if copysign were used. +TEST(EngramOpsTest, EngramGateZeroDotProduct) { + const std::vector key{1.0f, 0.0f}; + const std::vector query{0.0f, 1.0f}; + const std::vector value{1.0f, -1.0f}; + const std::vector unit_scale{1.0f, 1.0f}; + const std::vector expected{0.5f * value[0], 0.5f * value[1]}; + + OpTester test("EngramGate", 1, kMSDomain); + test.AddAttribute("epsilon", kEpsilon); + test.AddInput("key", {1, 1, 1, 2}, key); + test.AddInput("query", {1, 1, 1, 2}, query); + test.AddInput("value", {1, 1, 2}, value); + test.AddInput("key_norm_scale", {1, 2}, unit_scale); + test.AddInput("query_norm_scale", {1, 2}, unit_scale); + test.AddOutput("output", {1, 1, 1, 2}, expected, false, 1e-5f, 1e-5f); + test.Run(); +} + +} // namespace test +} // namespace onnxruntime diff --git a/onnxruntime/test/optimizer/graph_transform_test.cc b/onnxruntime/test/optimizer/graph_transform_test.cc index 2f27d900dce00..5b7bfca0ca5a1 100644 --- a/onnxruntime/test/optimizer/graph_transform_test.cc +++ b/onnxruntime/test/optimizer/graph_transform_test.cc @@ -6,6 +6,7 @@ #endif #include +#include #include #include @@ -4153,6 +4154,108 @@ ConvActivationBuilder SimpleActivation(const std::string& op_type, }; } +// Use fp16, group 1, NHWC, and a non-1x1 kernel to select Im2ColMatMulProgram. +void RunWebGpuIm2ColActivationParity(const ConvActivationBuilder& add_activation, + const std::string& expected_activation, + int opset_version) { + if (!DefaultWebGpuExecutionProvider()) { + GTEST_SKIP() << "WebGPU EP unavailable in this build."; + } + + const std::vector input_shape{1, 4, 14, 14}; + const std::vector weight_shape{8, 4, 3, 3}; + + auto to_fp16 = [](const std::vector& values) { + std::vector converted; + converted.reserve(values.size()); + for (float v : values) { + converted.push_back(MLFloat16(v)); + } + return converted; + }; + + // Use deterministic signed inputs for stable fp16 comparisons. + auto ramp = [](size_t count, float lo, float hi) { + std::vector values(count); + for (size_t i = 0; i < count; ++i) { + values[i] = lo + (hi - lo) * (static_cast(i % 32) / 31.0f); + } + return values; + }; + + auto build_test_case = [&](ModelTestBuilder& builder) { + auto* input = builder.MakeInput(input_shape, to_fp16(ramp(1 * 4 * 14 * 14, -3.0f, 3.0f))); + auto* weight = builder.MakeInitializer(weight_shape, to_fp16(ramp(8 * 4 * 3 * 3, -0.5f, 0.5f))); + auto* bias = builder.MakeInitializer({weight_shape[0]}, to_fp16(ramp(8, -0.5f, 0.5f))); + auto* conv_out = builder.MakeIntermediate(); + auto* output = builder.MakeOutput(); + + builder.AddNode("Conv", {input, weight, bias}, {conv_out}); + add_activation(builder, conv_out, output); + }; + + bool im2col_selected = false; + std::string observed_conv_programs; + auto check_transformed_graph = [&](InferenceSessionWrapper& session) { + bool fused = false; + std::ostringstream graph_description; + for (const auto& node : session.GetGraph().Nodes()) { + graph_description << " " << node.Domain() << "." << node.OpType() + << "[" << node.GetExecutionProviderType() << "]"; + if (node.OpType() != "Conv") { + continue; + } + const auto* activation_attr = graph_utils::GetNodeAttribute(node, "activation"); + if (activation_attr != nullptr && activation_attr->s() == expected_activation) { + fused = true; + } + } + ASSERT_TRUE(fused) << "Conv did not absorb " << expected_activation + << ", so no fused kernel ran at all. Graph was:" << graph_description.str(); + + // Confirm profiling observed Im2ColMatMulProgram rather than the fallback Conv path. + const std::string profile_path = session.EndProfiling(); + ASSERT_FALSE(profile_path.empty()) << "profiling produced no file, so program selection is unverifiable"; + std::ifstream profile_stream(profile_path); + ASSERT_TRUE(profile_stream.good()) << "cannot read profile " << profile_path; + std::stringstream buffer; + buffer << profile_stream.rdbuf(); + const std::string profile_contents = buffer.str(); + + im2col_selected = profile_contents.find("Im2ColMatMul") != std::string::npos; + for (size_t pos = profile_contents.find("&Conv&"); pos != std::string::npos; + pos = profile_contents.find("&Conv&", pos + 1)) { + const size_t end = profile_contents.find('"', pos); + if (end != std::string::npos) { + observed_conv_programs += " " + profile_contents.substr(pos + 6, end - pos - 6); + } + } + // GPU timestamp events carry a "cache_key" argument. A device without timestamp query support + // emits none at all, which is not the same as dispatching an unexpected program. + if (profile_contents.find("cache_key") == std::string::npos) { + GTEST_SKIP() << "device reported no GPU timestamps, so program selection is unobservable here"; + } + ASSERT_FALSE(observed_conv_programs.empty()) + << "GPU kernels were profiled but none was a Conv dispatch, so this test can no longer tell " + "whether the im2col program was selected"; + }; + + RunWebGpuFusionTransformerTest(build_test_case, check_transformed_graph, TransformerLevel::Level1, TransformerLevel::Level2, opset_version, + /*per_sample_tolerance=*/2e-2, + /*relative_per_sample_tolerance=*/2e-2, + /*transformer=*/nullptr, []() { return DefaultWebGpuExecutionProvider(); }, [](SessionOptions& session_options) { + session_options.enable_profiling = true; + session_options.profile_file_prefix = ORT_TSTR("webgpu_im2col_activation"); }); + + // Im2ColMatMulProgram is restricted to Intel Xe-2/Xe-3. + if (!im2col_selected) { + GTEST_SKIP() << "Im2ColMatMul did not run on this adapter, so the im2col activation epilogue was " + "not exercised. Conv dispatched to:" + << observed_conv_programs + << ". Requires an Intel Xe-2/Xe-3 GPU per IsDeviceSupported()."; + } +} + } // namespace TEST_F(GraphTransformationTests, WebGpuConvReluFusionMatchesUnfusedResults) { @@ -4248,6 +4351,38 @@ TEST_F(GraphTransformationTests, WebGpuConvLeakyReluParityAcrossAlphaValues) { "LeakyRelu", 17, input_shape, weight_shape); } +TEST_F(GraphTransformationTests, WebGpuIm2ColConvReluFusionMatchesUnfusedResults) { + RunWebGpuIm2ColActivationParity(SimpleActivation("Relu"), "Relu", 17); +} + +TEST_F(GraphTransformationTests, WebGpuIm2ColConvLeakyReluFusionMatchesUnfusedResults) { + RunWebGpuIm2ColActivationParity( + SimpleActivation("LeakyRelu", kOnnxDomain, [](Node& node) { node.AddAttribute("alpha", 0.25f); }), + "LeakyRelu", 17); +} + +TEST_F(GraphTransformationTests, WebGpuIm2ColConvHardSigmoidFusionMatchesUnfusedResults) { + RunWebGpuIm2ColActivationParity( + SimpleActivation("HardSigmoid", kOnnxDomain, + [](Node& node) { + node.AddAttribute("alpha", 0.3f); + node.AddAttribute("beta", 0.7f); + }), + "HardSigmoid", 17); +} + +// Clip is the only two-slot activation whose uniform slots mean {min, max} rather than +// {alpha, beta}, so it is the one case where swapping or mis-indexing activation_param_0/1 +// would survive the HardSigmoid test above. +TEST_F(GraphTransformationTests, WebGpuIm2ColConvClipFusionMatchesUnfusedResults) { + auto add_clip = [](ModelTestBuilder& builder, NodeArg* conv_out, NodeArg* output) { + // min/max must match the fp16 tensor type this path requires. + auto* min_value = builder.MakeScalarInitializer(MLFloat16(-0.25f)); + auto* max_value = builder.MakeScalarInitializer(MLFloat16(0.75f)); + builder.AddNode("Clip", {conv_out, min_value, max_value}, {output}); + }; + RunWebGpuIm2ColActivationParity(add_clip, "Clip", 17); +} #endif // defined(USE_WEBGPU) #endif // !defined(DISABLE_CONTRIB_OPS) diff --git a/onnxruntime/test/providers/cpu/controlflow/scan_test.cc b/onnxruntime/test/providers/cpu/controlflow/scan_test.cc index c7de8d4cba83d..4583b772bb30c 100644 --- a/onnxruntime/test/providers/cpu/controlflow/scan_test.cc +++ b/onnxruntime/test/providers/cpu/controlflow/scan_test.cc @@ -1025,6 +1025,151 @@ static void InvalidInput(bool is_v8) { TEST_8_AND_9(InvalidInput); +#if !defined(ORT_NO_EXCEPTIONS) +// 'num_scan_inputs' is a required attribute specifying how many of the node's variadic inputs are +// scan inputs (the rest are loop state variables). A value outside [0, num_variadic_inputs] must be +// rejected up front, otherwise the derived loop-state-variable count silently goes out of the valid +// range and later indexing built on top of it would operate on invalid indices. +// +// RunTest_v8/v9 hard-code the 'num_scan_inputs' attribute to the valid value of 2, so exercise the +// invalid-attribute path by building the model directly instead of going through those helpers. +TEST(Scan8, NumScanInputsExceedsVariadicInputs) { + RunOptions options{}; + options.is_v8 = true; + + Model model("NumScanInputsExceedsVariadicInputs_v8", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), {{"", 8}}, {}, DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + ASSERT_STATUS_OK(CreateSubgraph(graph, options)); + auto& proto = graph.ToGraphProto(); + + ScanOpTester test{8}; + test.AddAttribute("body", proto); + // The subgraph has 1 loop state variable and 2 scan inputs -> 3 variadic inputs. Requesting 10 + // scan inputs is invalid regardless of how many inputs the Scan node itself declares. + test.AddAttribute("num_scan_inputs", 10); + test.AddOptionalInputEdge(); // sequence_lens + test.AddShapeToTensorData(options.include_dim_values_in_main_graph); + + test.AddInput("scan_loop_state_in_0", {1, 1}, {0.f}); + test.AddInput("scan_input_0", {1, 2, 2}, {1.f, 2.f, 3.f, 4.f}); + test.AddInput("scan_input_1", {1, 2, 2}, {-1.f, -2.f, -3.f, -4.f}); + + test.AddOutput("scan_loop_state_out_0", {1, 1}, {1.f}); + test.AddOutput("scan_output_0", {1, 2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_1", {1, 2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_2", {1, 2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_3", {1, 2, 1}, {0.f, 0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, "Invalid 'num_scan_inputs' of 10. Value must be between 1 and 3", + options.excluded_provider_types); +} + +// The ONNX Scan spec requires one or more scan_input tensors, so 'num_scan_inputs' of 0 (all variadic +// inputs treated as loop state variables, no scan inputs) must also be rejected. +TEST(Scan8, NumScanInputsIsZero) { + RunOptions options{}; + options.is_v8 = true; + + Model model("NumScanInputsIsZero_v8", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), {{"", 8}}, {}, DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + ASSERT_STATUS_OK(CreateSubgraph(graph, options)); + auto& proto = graph.ToGraphProto(); + + ScanOpTester test{8}; + test.AddAttribute("body", proto); + test.AddAttribute("num_scan_inputs", 0); + test.AddOptionalInputEdge(); // sequence_lens + test.AddShapeToTensorData(options.include_dim_values_in_main_graph); + + test.AddInput("scan_loop_state_in_0", {1, 1}, {0.f}); + test.AddInput("scan_input_0", {1, 2, 2}, {1.f, 2.f, 3.f, 4.f}); + test.AddInput("scan_input_1", {1, 2, 2}, {-1.f, -2.f, -3.f, -4.f}); + + test.AddOutput("scan_loop_state_out_0", {1, 1}, {1.f}); + test.AddOutput("scan_output_0", {1, 2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_1", {1, 2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_2", {1, 2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_3", {1, 2, 1}, {0.f, 0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, "Invalid 'num_scan_inputs' of 0. Value must be between 1 and 3", + options.excluded_provider_types); +} + +// A negative 'num_scan_inputs' is invalid the same way an excessive one is; check it doesn't slip +// through the lower-bound half of the range check. +TEST(Scan8, NumScanInputsIsNegative) { + RunOptions options{}; + options.is_v8 = true; + + Model model("NumScanInputsIsNegative_v8", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), {{"", 8}}, {}, DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + ASSERT_STATUS_OK(CreateSubgraph(graph, options)); + auto& proto = graph.ToGraphProto(); + + ScanOpTester test{8}; + test.AddAttribute("body", proto); + test.AddAttribute("num_scan_inputs", -1); + test.AddOptionalInputEdge(); // sequence_lens + test.AddShapeToTensorData(options.include_dim_values_in_main_graph); + + test.AddInput("scan_loop_state_in_0", {1, 1}, {0.f}); + test.AddInput("scan_input_0", {1, 2, 2}, {1.f, 2.f, 3.f, 4.f}); + test.AddInput("scan_input_1", {1, 2, 2}, {-1.f, -2.f, -3.f, -4.f}); + + test.AddOutput("scan_loop_state_out_0", {1, 1}, {1.f}); + test.AddOutput("scan_output_0", {1, 2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_1", {1, 2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_2", {1, 2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_3", {1, 2, 1}, {0.f, 0.f}); + + // A negative attribute value fails narrowing to size_t during ONNX shape inference before the + // kernel is even constructed, so the message differs from the other invalid-value tests above. + test.Run(OpTester::ExpectResult::kExpectFailure, "narrow: value -1 cannot be represented in target type", + options.excluded_provider_types); +} + +TEST(Scan9, NumScanInputsExceedsVariadicInputs) { + RunOptions options{}; + options.is_v8 = false; + + Model model("NumScanInputsExceedsVariadicInputs_v9", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), {{"", 9}}, {}, DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + ASSERT_STATUS_OK(CreateSubgraph(graph, options)); + auto& proto = graph.ToGraphProto(); + + ScanOpTester test{9}; + test.AddAttribute("body", proto); + // The subgraph has 1 loop state variable and 2 scan inputs -> 3 variadic inputs (no 'sequence_lens' + // in opset 9+). Requesting 10 scan inputs is invalid regardless of the Scan node's actual inputs. + test.AddAttribute("num_scan_inputs", 10); + + test.AddInput("scan_loop_state_in_0", {1}, {0.f}); + test.AddInput("scan_input_0", {2, 2}, {1.f, 2.f, 3.f, 4.f}); + test.AddInput("scan_input_1", {2, 2}, {-1.f, -2.f, -3.f, -4.f}); + + test.AddOutput("scan_loop_state_out_0", {1}, {1.f}); + test.AddOutput("scan_output_0", {2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_1", {2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_2", {2, 1}, {0.f, 0.f}); + test.AddOutput("scan_output_3", {2, 1}, {0.f, 0.f}); + + // Unlike opset 8, opset 9+ Scan is type/shape-inferred via the standard ONNX inferencing path. With + // 'num_scan_inputs' wrongly set to 10, the loop-state-variable/scan-input split used to propagate + // input types into the body subgraph is wrong too, so the subgraph's own output shape inference ends + // up disagreeing with its declared output shape and graph resolution fails before the node's kernel + // (and its own attribute validation) is ever constructed. The failure surfaces as a graph attribute + // inferencing error wrapping a shape-inference error, rather than the kernel-level message produced + // by the opset 8 test above. + test.Run(OpTester::ExpectResult::kExpectFailure, + "[ShapeInferenceError] Mismatch between number of inferred and declared dimensions", + options.excluded_provider_types); +} +#endif // !defined(ORT_NO_EXCEPTIONS) + // Test usage of multiple inputs of different types for variadic inputs void MixedTypeInputs(bool is_v8) { // Construct scan body subgraph with 2 state variables, 2 scan inputs, 2 scan outputs diff --git a/onnxruntime/test/providers/cpu/math/cumsum_test.cc b/onnxruntime/test/providers/cpu/math/cumsum_test.cc index 40ece3a3fb064..507482ef7dd0a 100644 --- a/onnxruntime/test/providers/cpu/math/cumsum_test.cc +++ b/onnxruntime/test/providers/cpu/math/cumsum_test.cc @@ -2,9 +2,9 @@ // Licensed under the MIT License. #include "gtest/gtest.h" +#include "core/session/onnxruntime_session_options_config_keys.h" #ifdef USE_WEBGPU #include "core/providers/webgpu/webgpu_provider_options.h" -#include "core/session/onnxruntime_session_options_config_keys.h" #endif #include "test/providers/provider_test_utils.h" #include "test/util/include/default_providers.h" @@ -24,6 +24,49 @@ void RunEmptyAxisFailureTest(std::vector> ex test.Run(OpTester::ExpectResult::kExpectFailure, "", {}, nullptr, &execution_providers); } +void RunCudaInt64Test(int64_t outer, int64_t width, int64_t inner) { + const int64_t element_count = outer * width * inner; + std::vector input(element_count); + for (int64_t i = 0; i < element_count; ++i) { + input[i] = i % 7 - 3; + } + + for (const bool exclusive : {false, true}) { + for (const bool reverse : {false, true}) { + std::vector expected(element_count); + for (int64_t outer_index = 0; outer_index < outer; ++outer_index) { + for (int64_t inner_index = 0; inner_index < inner; ++inner_index) { + int64_t total = 0; + for (int64_t i = 0; i < width; ++i) { + const int64_t axis_index = reverse ? width - 1 - i : i; + const int64_t index = (outer_index * width + axis_index) * inner + inner_index; + if (exclusive) { + expected[index] = total; + } + total += input[index]; + if (!exclusive) { + expected[index] = total; + } + } + } + } + + OpTester test("CumSum", 11, onnxruntime::kOnnxDomain); + test.AddAttribute("exclusive", exclusive); + test.AddAttribute("reverse", reverse); + test.AddInput("x", {outer, width, inner}, input); + test.AddInput("axis", {}, {1}); + test.AddOutput("y", {outer, width, inner}, expected); + + SessionOptions session_options; + ASSERT_STATUS_OK(session_options.config_options.AddConfigEntry(kOrtSessionOptionsDisableCPUEPFallback, "1")); + test.Config(session_options) + .ConfigEp(DefaultCudaExecutionProvider()) + .RunWithConfig(); + } + } +} + } // namespace TEST(CumSumTest, _1DTest) { @@ -283,6 +326,24 @@ TEST(CumSumTest, _1DTestInt64) { test.AddOutput("y", {5}, {1, 3, 6, 10, 15}); test.Run(OpTester::ExpectResult::kExpectSuccess, "", {kTensorrtExecutionProvider}); } + +TEST(CumSumTest, CudaInt64BlockScanMultiTile) { + if (!DefaultCudaExecutionProvider()) { + GTEST_SKIP() << "CUDA execution provider is not available"; + } + + RunCudaInt64Test(1, 1000, 1); + RunCudaInt64Test(2, 513, 3); +} + +TEST(CumSumTest, CudaInt64GenericKernelWidthTwo) { + if (!DefaultCudaExecutionProvider()) { + GTEST_SKIP() << "CUDA execution provider is not available"; + } + + RunCudaInt64Test(1, 2, 1); +} + TEST(CumSumTest, _1DTestdouble) { OpTester test("CumSum", 11, onnxruntime::kOnnxDomain); test.AddInput("x", {5}, {1., 2., 3., 4., 5.}); diff --git a/onnxruntime/test/python/onnxruntime_test_python.py b/onnxruntime/test/python/onnxruntime_test_python.py index 5fbf908e7f3d6..9a5b474699b9e 100644 --- a/onnxruntime/test/python/onnxruntime_test_python.py +++ b/onnxruntime/test/python/onnxruntime_test_python.py @@ -21,6 +21,11 @@ import onnxruntime as onnxrt from onnxruntime.capi import _pybind_state as C +from onnxruntime.capi.onnxruntime_inference_collection import ( + _GRAPH_ANNOTATION_SKIP, + _graph_annotation_id, + _has_foreign_webgpu_context, +) from onnxruntime.capi.onnxruntime_pybind11_state import Fail, OrtValueVector, RunOptions # handle change from python 3.8 and on where loading a dll from the current directory needs to be explicitly allowed. @@ -59,6 +64,22 @@ ] +def device_ortvalue_from_numpy(session, array, device_type, device_id=0, vendor_id=-1): + """Allocate a device OrtValue from the session allocator and upload host data into it. + + This is the supported composition: allocate with Session.create_ortvalue_from_shape_and_type, + then upload with the environment-level onnxruntime.copy_tensors. For the default WebGPU context + the session allocator and the environment data transfer both resolve context 0. + """ + if not array.flags.c_contiguous: + array = np.ascontiguousarray(array) + device_value = session.create_ortvalue_from_shape_and_type( + array.shape, array.dtype, device_type, device_id, vendor_id + ) + onnxrt.copy_tensors([onnxrt.OrtValue.ortvalue_from_numpy(array)], [device_value]) + return device_value + + class TestInferenceSession(unittest.TestCase): def run_model(self, session_object, run_options): x = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], dtype=np.float32) @@ -1816,6 +1837,9 @@ def test_ort_device(self): self.assertEqual(cuda_device.device_vendor_id(), onnxrt.OrtDeviceVendorId.NVIDIA) self.assertEqual(onnxrt.OrtDeviceVendorId.NVIDIA, 0x10DE) + webgpu_device = onnxrt.OrtDevice.make("webgpu", 0) + self.assertEqual(webgpu_device.device_vendor_id(), onnxrt.OrtDeviceVendorId.NONE) + def test_ort_memory_info(self): cpu_memory_info = onnxrt.OrtMemoryInfo( "Cpu", @@ -1899,6 +1923,686 @@ def test_shared_allocator_using_create_and_register_allocator(self): providers=onnxrt.get_available_providers(), ) + def test_session_scoped_cpu_ortvalue(self): + """A session allocator value is usable only with the session that created it. + + Population goes through onnxruntime.copy_tensors rather than a session-specific update path, + so this covers allocation and provenance only; the copy itself is covered by the WebGPU + graph-capture flow, where a device data transfer is always registered. + """ + session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + input_metadata = session.get_inputs()[0] + input_value = np.arange(np.prod(input_metadata.shape), dtype=np.float32).reshape(input_metadata.shape) + + session_value = session.create_ortvalue_from_shape_and_type(input_metadata.shape, np.float32, "cpu") + self.assertFalse(session_value._is_webgpu_buffer) + self.assertIs(session_value._session, session._sess) + self.assertEqual(session_value.shape(), list(input_metadata.shape)) + + other_session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + other_session_value = other_session.create_ortvalue_from_shape_and_type(input_metadata.shape, np.float32, "cpu") + with self.assertRaisesRegex(ValueError, "same session"): + session_value.update_inplace(other_session_value) + with self.assertRaisesRegex(ValueError, "session that created it"): + other_session.run(None, {input_metadata.name: session_value}) + with self.assertRaisesRegex(ValueError, "session that created it"): + other_session.run_with_ort_values(None, {input_metadata.name: session_value}) + with self.assertRaisesRegex(ValueError, "session that created it"): + session.io_binding().bind_ortvalue_input(input_metadata.name, other_session_value) + with self.assertRaisesRegex(ValueError, "session that created it"): + other_session.run_with_iobinding(session.io_binding()) + + reset_session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + stale_value = reset_session.create_ortvalue_from_shape_and_type(input_metadata.shape, np.float32, "cpu") + stale_binding = reset_session.io_binding() + reset_session.set_providers(["CPUExecutionProvider"]) + with self.assertRaisesRegex(ValueError, "session that created it"): + reset_session.run(None, {input_metadata.name: stale_value}) + with self.assertRaisesRegex(ValueError, "session that created it"): + reset_session.run_with_iobinding(stale_binding) + + scalar_value = session.create_ortvalue_from_shape_and_type([], np.float32, "cpu") + self.assertEqual(scalar_value.shape(), []) + # A plain run with host inputs is unaffected by any of the above. + np.testing.assert_array_equal( + session.run(None, {input_metadata.name: input_value})[0], + session.run(None, {input_metadata.name: input_value})[0], + ) + + def test_is_webgpu_buffer_is_read_only(self): + """OrtValue._is_webgpu_buffer must always come from the native value and never be settable. + + Graph-capture validation and the OrtValue copy paths refuse host memory based on it, so a + settable attribute would let a CPU tensor pass itself off as a WebGPU device tensor. + """ + cpu_value = onnxrt.OrtValue.ortvalue_from_numpy(np.zeros((2, 2), dtype=np.float32)) + self.assertFalse(cpu_value._is_webgpu_buffer) + self.assertEqual(cpu_value._is_webgpu_buffer, cpu_value._get_c_value()._is_webgpu_buffer()) + + with self.assertRaises(AttributeError): + cpu_value._is_webgpu_buffer = True + self.assertFalse(cpu_value._is_webgpu_buffer) + + # A spoofed value must not be able to satisfy graph-capture validation either. + session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + io_binding = session.io_binding() + io_binding.bind_ortvalue_input(session.get_inputs()[0].name, cpu_value) + with self.assertRaisesRegex(ValueError, "requires fixed WebGPU device OrtValues"): + io_binding._validate_capture_bindings() + + def test_device_ortvalue_provenance_rules(self): + """Session-scoped OrtValues are usable only with the session that created them.""" + session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + other_session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + input_metadata = session.get_inputs()[0] + input_value = np.arange(np.prod(input_metadata.shape), dtype=np.float32).reshape(input_metadata.shape) + + # Copy CPU storage so in-place updates remain isolated. + unowned = onnxrt.OrtValue.ortvalue_from_numpy(input_value.copy()) + owned = session.create_ortvalue_from_shape_and_type(input_metadata.shape, np.float32, "cpu") + foreign = other_session.create_ortvalue_from_shape_and_type(input_metadata.shape, np.float32, "cpu") + + # Shared-allocator and locally owned values are both accepted. + session._validate_ortvalue_ownership([unowned, owned]) + + with self.assertRaises(ValueError) as raised: + session._validate_ortvalue_ownership([foreign]) + self.assertIn("session that created it", str(raised.exception)) + + io_binding = session.io_binding() + io_binding.bind_ortvalue_input(input_metadata.name, unowned) + io_binding.bind_ortvalue_input(input_metadata.name, owned) + with self.assertRaisesRegex(ValueError, "must be bound to the session that created it"): + io_binding.bind_ortvalue_input(input_metadata.name, foreign) + with self.assertRaisesRegex(ValueError, "must be bound to the session that created it"): + io_binding.bind_ortvalue_output(session.get_outputs()[0].name, foreign) + + # Shared values remain writable through the environment transfer. + updated_input = input_value + 1.0 + unowned.update_inplace(updated_input) + np.testing.assert_array_equal(unowned.numpy(), updated_input) + unowned.update_inplace(onnxrt.OrtValue.ortvalue_from_numpy(input_value.copy())) + np.testing.assert_array_equal(unowned.numpy(), input_value) + + with self.assertRaisesRegex(ValueError, "same session"): + owned.update_inplace(foreign) + + def test_ortvalue_ownership_allows_matching_webgpu_context(self): + """run() and IOBinding share one rule: a WebGPU buffer from another session is + accepted only when the WebGPU contexts match. + + Context ids are injected because a second WebGPU context needs caller-supplied Dawn + handles, which Python cannot reach. + """ + + class WebGpuValue(onnxrt.OrtValue): + _is_webgpu_buffer = True + + class FakeSession: + def __init__(self, context_id): + self._context_id = context_id + + def webgpu_context_id(self): + return self._context_id + + class FakeOwner: + def __init__(self, session): + self._sess = session + + session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + input_name = session.get_inputs()[0].name + output_name = session.get_outputs()[0].name + source = onnxrt.OrtValue.ortvalue_from_numpy(np.zeros(session.get_inputs()[0].shape, dtype=np.float32)) + + def owned_by(context_id): + value = WebGpuValue(source._get_c_value()) + value._session = FakeSession(context_id) + return value + + target = FakeOwner(FakeSession(0)) + onnxrt.InferenceSession._validate_ortvalue_ownership(target, [owned_by(0)]) + with self.assertRaisesRegex(ValueError, "must be used with the session that created it"): + onnxrt.InferenceSession._validate_ortvalue_ownership(target, [owned_by(1)]) + + # The binding paths apply the same rule, so they must accept the same value. + io_binding = session.io_binding() + io_binding._session = FakeSession(0) + io_binding.bind_ortvalue_input(input_name, owned_by(0)) + io_binding.bind_ortvalue_output(output_name, owned_by(0)) + with self.assertRaisesRegex(ValueError, "must be bound to the session that created it"): + io_binding.bind_ortvalue_input(input_name, owned_by(1)) + with self.assertRaisesRegex(ValueError, "must be bound to the session that created it"): + io_binding.bind_ortvalue_output(output_name, owned_by(1)) + + # update_inplace shares the rule, so a same-context source must copy through. + payload = np.arange(np.prod(session.get_inputs()[0].shape), dtype=np.float32).reshape( + session.get_inputs()[0].shape + ) + destination = WebGpuValue(onnxrt.OrtValue.ortvalue_from_numpy(payload.copy())._get_c_value()) + destination._session = FakeSession(0) + destination.update_inplace(owned_by(0)) + np.testing.assert_array_equal(destination.numpy(), np.zeros_like(payload)) + with self.assertRaisesRegex(ValueError, "must originate from the same session"): + destination.update_inplace(owned_by(1)) + + def test_foreign_webgpu_context_predicate(self): + """Unit-test the copy_tensors context predicate without needing a second WebGPU context. + + A real custom context requires caller-supplied Dawn instance and device handles, which are + not reachable from Python, so the context id is injected here instead. This is the only + coverage of the rejection branch that runs on a CPU-only machine. + """ + + class FakeValue: + def __init__(self, is_webgpu, session): + self._is_webgpu_buffer = is_webgpu + self._session = session + + default_session = object() + custom_session = object() + no_webgpu_session = object() + context_ids = {id(default_session): 0, id(custom_session): 1, id(no_webgpu_session): -1} + + def context_id_getter(session): + return context_ids[id(session)] + + cpu_unowned = FakeValue(False, None) + cpu_owned = FakeValue(False, custom_session) + webgpu_unowned = FakeValue(True, None) + webgpu_default = FakeValue(True, default_session) + webgpu_custom = FakeValue(True, custom_session) + webgpu_no_ep = FakeValue(True, no_webgpu_session) + + # Only a WebGPU value that can be attributed to a non-default context is rejected. + for allowed in (cpu_unowned, cpu_owned, webgpu_unowned, webgpu_default, webgpu_no_ep): + self.assertFalse(_has_foreign_webgpu_context([allowed], context_id_getter)) + self.assertTrue(_has_foreign_webgpu_context([webgpu_custom], context_id_getter)) + + # One offending value anywhere in the batch is enough, in either direction. + self.assertTrue(_has_foreign_webgpu_context([webgpu_default, webgpu_custom], context_id_getter)) + self.assertTrue(_has_foreign_webgpu_context([cpu_unowned, webgpu_custom], context_id_getter)) + self.assertFalse(_has_foreign_webgpu_context([], context_id_getter)) + + def test_webgpu_context_id_reports_default_for_cpu_session(self): + """A session with no WebGPU EP reports -1, which the copy_tensors predicate treats as unknown.""" + session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + self.assertEqual(session._sess.webgpu_context_id(), -1) + + def test_graph_annotation_id_run_option(self): + """gpu_graph_id defaults to 0, accepts integer strings, and uses -1 to skip capture.""" + self.assertEqual(_GRAPH_ANNOTATION_SKIP, -1) + # No run options, and run options without the entry, both mean the default graph. + self.assertEqual(_graph_annotation_id(None), 0) + self.assertEqual(_graph_annotation_id(onnxrt.RunOptions()), 0) + + for value, expected in (("-1", -1), ("0", 0), ("1", 1), ("7", 7)): + run_options = onnxrt.RunOptions() + run_options.add_run_config_entry("gpu_graph_id", value) + self.assertEqual(_graph_annotation_id(run_options), expected) + + invalid_options = onnxrt.RunOptions() + invalid_options.add_run_config_entry("gpu_graph_id", "not-an-int") + with self.assertRaisesRegex(ValueError, "must be an integer"): + _graph_annotation_id(invalid_options) + + def test_reset_session_releases_captured_graphs(self): + """set_providers() must not leave captured-graph bookkeeping pointing at the replaced session. + + The dict is keyed by graph annotation id and holds a weakref to the IOBinding that captured + it, so a stale entry would reject a binding from the replacement session and a later + release_captured_graph() would act on the replacement session while unpinning the old + binding. + """ + session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + input_metadata = session.get_inputs()[0] + output_name = session.get_outputs()[0].name + input_value = np.arange(np.prod(input_metadata.shape), dtype=np.float32).reshape(input_metadata.shape) + + stale_binding = session.io_binding() + stale_binding.bind_cpu_input(input_metadata.name, input_value) + stale_binding.bind_output(output_name) + # Reproduce exactly what run_with_iobinding() records once a graph has been captured. + # Capture itself needs a WebGPU device, but the bookkeeping this exercises does not. + stale_signature = stale_binding._capture_signature() + session._captured_graph_bindings[0] = (weakref.ref(stale_binding), stale_signature) + stale_binding._pinned_graph_ids.add(0) + + session.set_providers(["CPUExecutionProvider"]) + + # Both sides of the bookkeeping must be cleared, and the old binding usable again. + self.assertEqual(session._captured_graph_bindings, {}) + self.assertEqual(stale_binding._pinned_graph_ids, set()) + stale_binding.clear_binding_inputs() + stale_binding.clear_binding_outputs() + + # The same graph id must be capturable again through the replacement session. + fresh_binding = session.io_binding() + fresh_binding.bind_cpu_input(input_metadata.name, input_value) + fresh_binding.bind_output(output_name) + session.run_with_iobinding(fresh_binding) + np.testing.assert_allclose( + fresh_binding.copy_outputs_to_cpu()[0], + session.run(None, {input_metadata.name: input_value})[0], + ) + session.release_captured_graph() + + def test_run_with_ortvaluevector_is_gated_only_on_capture(self): + """run_with_ortvaluevector must be gated on graph capture, not on provider identity. + + It is the only run API that used a provider-name check; run(), run_with_ort_values() and + run_async() all gate on _validate_graph_capture_run_api. A raw vector is unsafe only while + capture is armed, because replay re-issues the buffers recorded at capture. + """ + session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + input_metadata = session.get_inputs()[0] + output_metadata = session.get_outputs()[0] + input_value = np.arange(np.prod(input_metadata.shape), dtype=np.float32).reshape(input_metadata.shape) + + feeds = OrtValueVector() + feeds.push_back(onnxrt.OrtValue.ortvalue_from_numpy(input_value)._get_c_value()) + fetches = OrtValueVector() + session.run_with_ortvaluevector( + onnxrt.RunOptions(), + [input_metadata.name], + feeds, + [output_metadata.name], + fetches, + [onnxrt.OrtDevice.make("cpu", 0)._get_c_device()], + ) + np.testing.assert_allclose( + fetches[0].numpy(), + session.run(None, {input_metadata.name: input_value})[0], + ) + + @unittest.skipIf( + "WebGpuExecutionProvider" not in onnxrt.get_available_providers(), + "WebGpuExecutionProvider is not available", + ) + def test_webgpu_run_with_ortvaluevector_without_capture(self): + """A WebGPU session that is not capturing must accept raw OrtValue vectors. + + The previous guard rejected every WebGPU session regardless of capture state or of whether + the vectors were even device-backed, which broke a pre-existing CPU-only use. + """ + so = onnxrt.SessionOptions() + so.enable_mem_pattern = False + + try: + session = onnxrt.InferenceSession( + get_name("mul_1.onnx"), + sess_options=so, + providers=["WebGpuExecutionProvider"], + ) + except RuntimeError as error: + if "Failed to get a WebGPU" in str(error): + self.skipTest(str(error)) + raise + + self.assertIn("WebGpuExecutionProvider", session.get_providers()) + self.assertFalse(session._sess.is_webgpu_graph_capture_enabled()) + + input_metadata = session.get_inputs()[0] + output_metadata = session.get_outputs()[0] + input_value = np.arange(np.prod(input_metadata.shape), dtype=np.float32).reshape(input_metadata.shape) + reference_session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + expected = reference_session.run(None, {input_metadata.name: input_value})[0] + + feeds = OrtValueVector() + feeds.push_back(onnxrt.OrtValue.ortvalue_from_numpy(input_value)._get_c_value()) + fetches = OrtValueVector() + session.run_with_ortvaluevector( + onnxrt.RunOptions(), + [input_metadata.name], + feeds, + [output_metadata.name], + fetches, + [onnxrt.OrtDevice.make("cpu", 0)._get_c_device()], + ) + np.testing.assert_allclose(fetches[0].numpy(), expected, rtol=1e-5, atol=1e-5) + + @unittest.skipIf( + "WebGpuExecutionProvider" not in onnxrt.get_available_providers(), + "WebGpuExecutionProvider is not available", + ) + def test_webgpu_graph_capture_session_ortvalues(self): + so = onnxrt.SessionOptions() + so.enable_mem_pattern = False + so.add_session_config_entry("session.disable_cpu_ep_fallback", "1") + so.add_session_config_entry("ep.webgpuexecutionprovider.enableGraphCapture", "1") + + try: + session = onnxrt.InferenceSession( + get_name("mul_1.onnx"), + sess_options=so, + providers=["WebGpuExecutionProvider"], + ) + except RuntimeError as error: + if "Failed to get a WebGPU" in str(error): + self.skipTest(str(error)) + raise + + input_metadata = session.get_inputs()[0] + output_metadata = session.get_outputs()[0] + input_value = np.arange(np.prod(input_metadata.shape), dtype=np.float32).reshape(input_metadata.shape) + reference_session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + + # Step 1: allocate fixed device tensors from the session allocator. + gpu_input = session.create_ortvalue_from_shape_and_type(input_metadata.shape, np.float32, "webgpu") + gpu_output = session.create_ortvalue_from_shape_and_type(output_metadata.shape, np.float32, "webgpu") + # Step 2: upload the host input with the environment-level copy_tensors. + onnxrt.copy_tensors([onnxrt.OrtValue.ortvalue_from_numpy(input_value)], [gpu_input]) + self.assertTrue(gpu_input._is_webgpu_buffer) + self.assertEqual(gpu_input.device_name(), "webgpu") + gpu_alias_input = device_ortvalue_from_numpy( + session, + input_value, + "gpu", + vendor_id=onnxrt.OrtDeviceVendorId.NONE, + ) + self.assertTrue(gpu_alias_input._is_webgpu_buffer) + # data_ptr() returns the opaque WGPUBuffer handle, which is what interop callers want. + self.assertNotEqual(gpu_input.data_ptr(), 0) + # numpy() reads back through the environment-registered WebGPU data transfer. + np.testing.assert_allclose(gpu_input.numpy(), input_value) + with self.assertRaisesRegex(RuntimeError, "DLPack export"): + gpu_input.__dlpack__() + with self.assertRaisesRegex(RuntimeError, "DLPack export"): + gpu_input.__dlpack_device__() + with self.assertRaisesRegex(ValueError, "graph capture requires"): + session.run(None, {input_metadata.name: gpu_input}) + with self.assertRaisesRegex(ValueError, "graph capture requires"): + session.run_with_ort_values(None, {input_metadata.name: gpu_input}) + with self.assertRaisesRegex(ValueError, "graph capture requires"): + session.run_async( + None, + {input_metadata.name: input_value}, + lambda *_: None, + None, + ) + with self.assertRaisesRegex(ValueError, "graph capture requires"): + session.run_with_ortvaluevector(None, [], None, [], None, None) + # Default-context WebGPU values copy in both directions through the shared data transfer. + onnxrt.copy_tensors([gpu_input], [gpu_output]) + np.testing.assert_allclose(gpu_output.numpy(), input_value) + + raw_vector = OrtValueVector() + # A standalone vector has no parent to keep the session alive; the IOBinding one does. + raw_vector.push_back(onnxrt.OrtValue.ortvalue_from_numpy(input_value)._get_c_value()) + with self.assertRaisesRegex(RuntimeError, "standalone OrtValueVector"): + raw_vector.push_back(gpu_input._get_c_value()) + + unsafe_io_binding = session.io_binding() + global_cpu_input = onnxrt.OrtValue.ortvalue_from_numpy(input_value) + global_cpu_output = onnxrt.OrtValue.ortvalue_from_numpy(np.zeros(output_metadata.shape, dtype=np.float32)) + with self.assertRaisesRegex(ValueError, "WebGPU source and destination"): + gpu_input.update_inplace(global_cpu_input) + with self.assertRaisesRegex(ValueError, "WebGPU source and destination"): + global_cpu_input.update_inplace(gpu_input) + # Shared-allocator WebGPU values have no session owner but remain bindable. + shared_provenance_source = device_ortvalue_from_numpy(session, input_value, "webgpu") + unowned_webgpu_input = onnxrt.OrtValue(shared_provenance_source._get_c_value()) + self.assertIsNone(unowned_webgpu_input._session) + self.assertTrue(unowned_webgpu_input._is_webgpu_buffer) + scratch_webgpu_value = session.create_ortvalue_from_shape_and_type(input_metadata.shape, np.float32, "webgpu") + onnxrt.copy_tensors([unowned_webgpu_input], [scratch_webgpu_value]) + onnxrt.copy_tensors([scratch_webgpu_value], [unowned_webgpu_input]) + unsafe_io_binding.bind_ortvalue_input(input_metadata.name, unowned_webgpu_input) + unsafe_io_binding.clear_binding_inputs() + + # Host upload into a shared-allocator device value goes through copy_tensors. + onnxrt.copy_tensors([onnxrt.OrtValue.ortvalue_from_numpy(input_value)], [unowned_webgpu_input]) + np.testing.assert_allclose(unowned_webgpu_input.numpy(), input_value) + + # WebGPU -> CPU readback through the shared data transfer is supported for the default context. + onnxrt.copy_tensors([unowned_webgpu_input], [global_cpu_input]) + np.testing.assert_allclose(global_cpu_input.numpy(), input_value) + del unowned_webgpu_input + del shared_provenance_source + del scratch_webgpu_value + # Run-time validation keeps gpu_graph_id=-1 usable with ordinary bindings. + unsafe_io_binding.bind_cpu_input(input_metadata.name, input_value) + unsafe_io_binding.bind_output(output_metadata.name) + with self.assertRaisesRegex(ValueError, "requires fixed WebGPU device OrtValues"): + session.run_with_iobinding(unsafe_io_binding) + skip_capture_options = onnxrt.RunOptions() + skip_capture_options.add_run_config_entry("gpu_graph_id", "-1") + session.run_with_iobinding(unsafe_io_binding, skip_capture_options) + np.testing.assert_allclose( + unsafe_io_binding.copy_outputs_to_cpu()[0], + reference_session.run(None, {input_metadata.name: input_value})[0], + rtol=1e-5, + atol=1e-5, + ) + unsafe_io_binding.clear_binding_inputs() + unsafe_io_binding.clear_binding_outputs() + unsafe_io_binding.bind_ortvalue_input(input_metadata.name, global_cpu_input) + unsafe_io_binding.bind_ortvalue_output(output_metadata.name, global_cpu_output) + with self.assertRaisesRegex(ValueError, "requires fixed WebGPU device OrtValues"): + session.run_with_iobinding(unsafe_io_binding) + unsafe_io_binding.clear_binding_inputs() + unsafe_io_binding.clear_binding_outputs() + + # gpu_graph_id=-1 keeps convenience APIs on the uncaptured path. + with self.assertRaisesRegex(ValueError, "requires fixed device OrtValues"): + session.run(None, {input_metadata.name: input_value}) + np.testing.assert_allclose( + session.run(None, {input_metadata.name: input_value}, skip_capture_options)[0], + reference_session.run(None, {input_metadata.name: input_value})[0], + rtol=1e-5, + atol=1e-5, + ) + + io_binding = session.io_binding() + io_binding.bind_ortvalue_input(input_metadata.name, gpu_input) + io_binding.bind_ortvalue_output(output_metadata.name, gpu_output) + + def run_and_copy_output(current_session, current_io_binding, run_options=None): + current_session.run_with_iobinding(current_io_binding, run_options) + return current_io_binding.copy_outputs_to_cpu()[0] + + expected = reference_session.run(None, {input_metadata.name: input_value})[0] + np.testing.assert_allclose(run_and_copy_output(session, io_binding), expected) + raw_outputs = io_binding._iobinding.get_outputs() + with self.assertRaisesRegex(RuntimeError, "DLPack export"): + raw_outputs.dlpack_at(0) + with self.assertRaisesRegex(RuntimeError, "DLPack export"): + raw_outputs.to_dlpacks(None) + indexed_raw_output = raw_outputs[0] + del raw_outputs + bound_output = io_binding.get_outputs()[0] + self.assertIs(bound_output._session, session._sess) + np.testing.assert_allclose(bound_output.numpy(), expected) + vector_outputs = io_binding.get_outputs_as_ortvaluevector() + self.assertTrue(vector_outputs[0]._is_webgpu_buffer()) + del vector_outputs + + updated_input = input_value + 10.0 + onnxrt.copy_tensors([onnxrt.OrtValue.ortvalue_from_numpy(updated_input)], [gpu_input]) + updated_expected = reference_session.run(None, {input_metadata.name: updated_input})[0] + alternate_gpu_output = device_ortvalue_from_numpy( + session, np.zeros(output_metadata.shape, dtype=np.float32), "webgpu" + ) + alternate_io_binding = session.io_binding() + alternate_io_binding.bind_ortvalue_input(input_metadata.name, gpu_input) + alternate_io_binding.bind_ortvalue_output(output_metadata.name, alternate_gpu_output) + + # Replay rejects a different IOBinding because it targets the captured buffers. + with self.assertRaisesRegex(ValueError, "captured with a different IOBinding"): + session.run_with_iobinding(alternate_io_binding) + + # Captured bindings remain immutable until release. + with self.assertRaisesRegex(ValueError, "still reference this IOBinding"): + io_binding.bind_ortvalue_output(output_metadata.name, alternate_gpu_output) + with self.assertRaisesRegex(ValueError, "still reference this IOBinding"): + io_binding.clear_binding_inputs() + with self.assertRaisesRegex(ValueError, "still reference this IOBinding"): + io_binding.clear_binding_outputs() + + np.testing.assert_allclose(run_and_copy_output(session, io_binding), updated_expected) + np.testing.assert_array_equal( + alternate_io_binding.copy_outputs_to_cpu()[0], + np.zeros(output_metadata.shape, dtype=np.float32), + ) + + ortvalue_update_input = input_value + 20.0 + onnxrt.copy_tensors([device_ortvalue_from_numpy(session, ortvalue_update_input, "webgpu")], [gpu_input]) + ortvalue_update_expected = reference_session.run(None, {input_metadata.name: ortvalue_update_input})[0] + np.testing.assert_allclose(run_and_copy_output(session, io_binding), ortvalue_update_expected) + + session.release_captured_graph() + + graph_one_run_options = onnxrt.RunOptions() + graph_one_run_options.add_run_config_entry("gpu_graph_id", "1") + np.testing.assert_allclose( + run_and_copy_output(session, io_binding, graph_one_run_options), + ortvalue_update_expected, + ) + repeated_capture_input = input_value + 30.0 + onnxrt.copy_tensors([onnxrt.OrtValue.ortvalue_from_numpy(repeated_capture_input)], [gpu_input]) + repeated_capture_expected = reference_session.run(None, {input_metadata.name: repeated_capture_input})[0] + np.testing.assert_allclose( + run_and_copy_output(session, io_binding, graph_one_run_options), + repeated_capture_expected, + ) + session.release_captured_graph(1) + + # Skipped runs use current inputs instead of replaying captured commands. + no_capture_run_options = onnxrt.RunOptions() + no_capture_run_options.add_run_config_entry("gpu_graph_id", "-1") + for offset in (40.0, 50.0): + uncaptured_input = input_value + offset + onnxrt.copy_tensors([onnxrt.OrtValue.ortvalue_from_numpy(uncaptured_input)], [gpu_input]) + np.testing.assert_allclose( + run_and_copy_output(session, io_binding, no_capture_run_options), + reference_session.run(None, {input_metadata.name: uncaptured_input})[0], + ) + + io_binding.clear_binding_inputs() + io_binding.clear_binding_outputs() + alternate_io_binding.clear_binding_inputs() + alternate_io_binding.clear_binding_outputs() + self.assertEqual(bound_output.shape(), output_metadata.shape) + del io_binding + del alternate_io_binding + del session + gc.collect() + self.assertTrue(indexed_raw_output._is_webgpu_buffer()) + del indexed_raw_output + gc.collect() + + @unittest.skipIf( + "WebGpuExecutionProvider" not in onnxrt.get_available_providers(), + "WebGpuExecutionProvider is not available", + ) + def test_webgpu_graph_capture_across_set_providers(self): + """A real capture must not outlive the session handle that set_providers() replaces.""" + so = onnxrt.SessionOptions() + so.enable_mem_pattern = False + so.add_session_config_entry("session.disable_cpu_ep_fallback", "1") + so.add_session_config_entry("ep.webgpuexecutionprovider.enableGraphCapture", "1") + + try: + session = onnxrt.InferenceSession( + get_name("mul_1.onnx"), + sess_options=so, + providers=["WebGpuExecutionProvider"], + ) + except RuntimeError as error: + if "Failed to get a WebGPU" in str(error): + self.skipTest(str(error)) + raise + + input_metadata = session.get_inputs()[0] + output_metadata = session.get_outputs()[0] + input_value = np.arange(np.prod(input_metadata.shape), dtype=np.float32).reshape(input_metadata.shape) + reference_session = onnxrt.InferenceSession(get_name("mul_1.onnx"), providers=["CPUExecutionProvider"]) + expected = reference_session.run(None, {input_metadata.name: input_value})[0] + + def capture(current_session): + io_binding = current_session.io_binding() + io_binding.bind_ortvalue_input( + input_metadata.name, + device_ortvalue_from_numpy(current_session, input_value, "webgpu"), + ) + io_binding.bind_ortvalue_output( + output_metadata.name, + current_session.create_ortvalue_from_shape_and_type(output_metadata.shape, np.float32, "webgpu"), + ) + current_session.run_with_iobinding(io_binding) + return io_binding + + first_binding = capture(session) + np.testing.assert_allclose(first_binding.copy_outputs_to_cpu()[0], expected, rtol=1e-5, atol=1e-5) + self.assertEqual(set(session._captured_graph_bindings), {0}) + self.assertEqual(first_binding._pinned_graph_ids, {0}) + + session.set_providers(["WebGpuExecutionProvider"]) + + # The replaced handle's capture must be gone from both sides of the bookkeeping. + self.assertEqual(session._captured_graph_bindings, {}) + self.assertEqual(first_binding._pinned_graph_ids, set()) + first_binding.clear_binding_inputs() + first_binding.clear_binding_outputs() + + # Graph 0 must be capturable again through the replacement session. + second_binding = capture(session) + np.testing.assert_allclose(second_binding.copy_outputs_to_cpu()[0], expected, rtol=1e-5, atol=1e-5) + session.release_captured_graph() + second_binding.clear_binding_inputs() + second_binding.clear_binding_outputs() + + @unittest.skipIf( + "WebGpuExecutionProvider" not in onnxrt.get_available_providers(), + "WebGpuExecutionProvider is not available", + ) + def test_webgpu_raw_output_vector_keeps_session_alive(self): + """A device OrtValue reached through the raw vector must survive its session. + + This pins the pybind keepalive chain that makes get_outputs_as_ortvaluevector() safe: + + OrtValue --keep_alive<0,1> on OrtValueVector.__getitem__--> + OrtValueVector --reference_internal on SessionIOBinding.get_outputs--> + SessionIOBinding --keep_alive<1,2> on SessionIOBinding.__init__--> InferenceSession + + Without the full chain, freeing the buffer after the session is gone would call + GpuBufferAllocator::Free() through a buffer-manager getter that captured the dead EP. + """ + so = onnxrt.SessionOptions() + so.enable_mem_pattern = False + + try: + session = onnxrt.InferenceSession( + get_name("mul_1.onnx"), + sess_options=so, + providers=["WebGpuExecutionProvider"], + ) + except RuntimeError as error: + if "Failed to get a WebGPU" in str(error): + self.skipTest(str(error)) + raise + + input_metadata = session.get_inputs()[0] + output_metadata = session.get_outputs()[0] + input_value = np.arange(np.prod(input_metadata.shape), dtype=np.float32).reshape(input_metadata.shape) + + io_binding = session.io_binding() + io_binding.bind_cpu_input(input_metadata.name, input_value) + io_binding.bind_output(output_metadata.name, "webgpu") + session.run_with_iobinding(io_binding) + + held = io_binding.get_outputs_as_ortvaluevector()[0] + self.assertTrue(held._is_webgpu_buffer()) + + del io_binding + del session + gc.collect() + + # The value is still valid with no session in scope ... + self.assertTrue(held._is_webgpu_buffer()) + # ... and releasing the WebGPU buffer afterwards must not fault. + del held + gc.collect() + def test_memory_arena_shrinkage(self): if ( platform.architecture()[0] == "32bit" diff --git a/tools/ci_build/github/azure-pipelines/c-api-noopenmp-packaging-pipelines.yml b/tools/ci_build/github/azure-pipelines/c-api-noopenmp-packaging-pipelines.yml index 4be1173701fff..59f5e872a3d38 100644 --- a/tools/ci_build/github/azure-pipelines/c-api-noopenmp-packaging-pipelines.yml +++ b/tools/ci_build/github/azure-pipelines/c-api-noopenmp-packaging-pipelines.yml @@ -67,6 +67,20 @@ parameters: resources: repositories: + # DAWN dependencies + - repository: abseil-cpp + type: git + name: Lotus-Dependencies/abseil-cpp + - repository: jinja2 + type: git + name: Lotus-Dependencies/jinja2 + - repository: markupsafe + type: git + name: Lotus-Dependencies/markupsafe + - repository: protobuf + type: git + name: Lotus-Dependencies/protobuf + # testing - repository: onnxruntime-inference-examples # The name used to reference this repository in the checkout step type: github endpoint: ort-examples @@ -119,6 +133,12 @@ extends: ignoreDirectories: '$(Build.Repository.LocalPath)/cmake/external/emsdk/upstream/emscripten/tests,$(Build.Repository.LocalPath)/cmake/external/onnx/third_party/benchmark,$(Build.Repository.LocalPath)/cmake/external/onnx/third_party/pybind11,$(Build.Repository.LocalPath)/cmake/external/onnx/third_party/pybind11/tests,$(Build.Repository.LocalPath)/cmake/external/onnxruntime-extensions,$(Build.Repository.LocalPath)/js/react_native/e2e/node_modules,$(Build.Repository.LocalPath)/js/node_modules,$(Build.Repository.LocalPath)/onnxruntime-inference-examples,$(Build.SourcesDirectory)/cmake/external/emsdk/upstream/emscripten/tests,$(Build.SourcesDirectory)/cmake/external/onnx/third_party/benchmark,$(Build.SourcesDirectory)/cmake/external/onnx/third_party/pybind11,$(Build.SourcesDirectory)/cmake/external/onnx/third_party/pybind11/tests,$(Build.SourcesDirectory)/cmake/external/onnxruntime-extensions,$(Build.SourcesDirectory)/js/react_native/e2e/node_modules,$(Build.SourcesDirectory)/js/node_modules,$(Build.SourcesDirectory)/onnxruntime-inference-examples,$(Build.BinariesDirectory)' sourceRepositoriesToScan: exclude: + # dependencies used by DAWN + - repository: abseil-cpp + - repository: jinja2 + - repository: markupsafe + - repository: protobuf + # tests - repository: onnxruntime-inference-examples spotBugs: enabled: false diff --git a/tools/ci_build/github/azure-pipelines/c-api-noopenmp-test-pipelines.yml b/tools/ci_build/github/azure-pipelines/c-api-noopenmp-test-pipelines.yml index 87ec14379046a..c3a9fd05c510f 100644 --- a/tools/ci_build/github/azure-pipelines/c-api-noopenmp-test-pipelines.yml +++ b/tools/ci_build/github/azure-pipelines/c-api-noopenmp-test-pipelines.yml @@ -233,9 +233,7 @@ stages: pool: onnxruntime-Win2022-GPU-A10 timeoutInMinutes: 180 steps: - - checkout: self - clean: true - submodules: none + - template: templates/jobs/checkout-and-git-redirect.yml - download: build artifact: 'Windows_Packaging_cuda_build_artifacts' @@ -288,9 +286,7 @@ stages: pool: onnxruntime-Win2022-GPU-A10 timeoutInMinutes: 180 steps: - - checkout: self - clean: true - submodules: none + - template: templates/jobs/checkout-and-git-redirect.yml - download: build artifact: 'Windows_Packaging_tensorrt_build_artifacts' diff --git a/tools/ci_build/github/azure-pipelines/linux-gpu-tensorrt-cuda-minimal-ci-pipeline.yml b/tools/ci_build/github/azure-pipelines/linux-gpu-tensorrt-cuda-minimal-ci-pipeline.yml index 142d9d5636d72..391eca73cefe7 100644 --- a/tools/ci_build/github/azure-pipelines/linux-gpu-tensorrt-cuda-minimal-ci-pipeline.yml +++ b/tools/ci_build/github/azure-pipelines/linux-gpu-tensorrt-cuda-minimal-ci-pipeline.yml @@ -62,10 +62,7 @@ jobs: clean: all pool: onnxruntime-tensorrt-linuxbuild-T4 steps: - - - checkout: self - clean: true - submodules: none + - template: templates/jobs/checkout-and-git-redirect.yml - template: templates/setup-feeds-and-python-steps.yml parameters: @@ -87,22 +84,27 @@ jobs: - task: CmdLine@2 inputs: script: | - docker run -e SYSTEM_COLLECTIONURI --gpus all --rm \ - --volume /data/onnx:/data/onnx:ro \ - --volume $(Build.SourcesDirectory):/onnxruntime_src \ - --volume $(Build.BinariesDirectory):/build \ - --volume /data/models:/build/models:ro \ - --volume $HOME/.onnx:/home/onnxruntimedev/.onnx \ - -e NPM_CONFIG_USERCONFIG=/tmp/.npmrc \ - -e PIP_INDEX_URL \ + docker run \ + --gpus all \ + --rm \ + --volume "/data/models:/build/models:ro" \ + --volume "/data/onnx:/data/onnx:ro" \ + --volume "${BUILD_BINARIESDIRECTORY}:/build" \ + --volume "${BUILD_SOURCESDIRECTORY}:/onnxruntime_src" \ + --volume "${HOME}/.gitconfig:/home/onnxruntimedev/.gitconfig:ro" \ + --volume "${HOME}/.gradle:/home/onnxruntimedev/.gradle" \ + --volume "${HOME}/.m2:/home/onnxruntimedev/.m2:ro" \ + --volume "${HOME}/.onnx:/home/onnxruntimedev/.onnx" \ --volume "${NPM_CONFIG_USERCONFIG}:/tmp/.npmrc:ro" \ - --volume $HOME/.m2:/home/onnxruntimedev/.m2:ro \ - --volume $HOME/.gradle:/home/onnxruntimedev/.gradle \ -e ALLOW_RELEASED_ONNX_OPSET_ONLY=0 \ - -e NIGHTLY_BUILD \ -e BUILD_BUILDNUMBER \ + -e NIGHTLY_BUILD \ + -e NPM_CONFIG_USERCONFIG=/tmp/.npmrc \ + -e PIP_INDEX_URL \ + -e SYSTEM_COLLECTIONURI \ -w /onnxruntime_src \ - onnxruntimetensorrtcudaminimalbuild tools/ci_build/github/linux/build_tensorrt_ci.sh --cuda_minimal=ON + onnxruntimetensorrtcudaminimalbuild \ + tools/ci_build/github/linux/build_tensorrt_ci.sh --cuda_minimal=ON workingDirectory: $(Build.SourcesDirectory) - template: templates/explicitly-defined-final-tasks.yml diff --git a/tools/ci_build/github/azure-pipelines/main-release-pipeline.yml b/tools/ci_build/github/azure-pipelines/main-release-pipeline.yml index 995c586066c67..224f17ae9db95 100644 --- a/tools/ci_build/github/azure-pipelines/main-release-pipeline.yml +++ b/tools/ci_build/github/azure-pipelines/main-release-pipeline.yml @@ -109,7 +109,7 @@ extends: os: linux steps: - - checkout: self + - template: templates/jobs/checkout-and-git-redirect.yml - task: PipAuthenticate@1 displayName: 'Pip Authenticate' diff --git a/tools/ci_build/github/azure-pipelines/nuget/templates/dml-vs-2022.yml b/tools/ci_build/github/azure-pipelines/nuget/templates/dml-vs-2022.yml index 70ef80fb02606..b715bcf675f7a 100644 --- a/tools/ci_build/github/azure-pipelines/nuget/templates/dml-vs-2022.yml +++ b/tools/ci_build/github/azure-pipelines/nuget/templates/dml-vs-2022.yml @@ -43,10 +43,7 @@ stages: build_py_lto_flag: --enable_lto steps: - - checkout: self - clean: true - submodules: none - + - template: ../../templates/jobs/checkout-and-git-redirect.yml - template: ../../templates/setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/post-merge-jobs.yml b/tools/ci_build/github/azure-pipelines/post-merge-jobs.yml index 4cdaea73b07a9..0f7b7040dc1d6 100644 --- a/tools/ci_build/github/azure-pipelines/post-merge-jobs.yml +++ b/tools/ci_build/github/azure-pipelines/post-merge-jobs.yml @@ -395,8 +395,7 @@ stages: variables: dockerImageTag: onnxruntime-android-custom-build steps: - - checkout: self - submodules: false + - template: templates/jobs/checkout-and-git-redirect.yml - template: templates/setup-feeds-and-python-steps.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/stages/jobs/py-linux-cuda-package-test-job.yml b/tools/ci_build/github/azure-pipelines/stages/jobs/py-linux-cuda-package-test-job.yml index 8fc59c4f06db3..4a4784b15e811 100644 --- a/tools/ci_build/github/azure-pipelines/stages/jobs/py-linux-cuda-package-test-job.yml +++ b/tools/ci_build/github/azure-pipelines/stages/jobs/py-linux-cuda-package-test-job.yml @@ -54,7 +54,8 @@ jobs: value: ${{ variables.linux_trt_version_cuda12 }} pool: ${{ parameters.machine_pool }} steps: - - checkout: self + - template: ../../templates/jobs/checkout-and-git-redirect.yml + - task: DownloadPipelineArtifact@2 inputs: artifact: 'linux_gpu_wheel_x86_64' diff --git a/tools/ci_build/github/azure-pipelines/stages/nodejs-linux-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/nodejs-linux-packaging-stage.yml index 1a326e9dbd584..765d1653970b6 100644 --- a/tools/ci_build/github/azure-pipelines/stages/nodejs-linux-packaging-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/nodejs-linux-packaging-stage.yml @@ -30,9 +30,7 @@ stages: ${{ if eq(parameters.CudaVersion, '12.8') }}: value: ${{ variables.linux_trt_version_cuda12 }} steps: - - checkout: self - clean: true - submodules: recursive + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/stages/nodejs-npm-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/nodejs-npm-packaging-stage.yml index 59424c83a113f..45fc073942241 100644 --- a/tools/ci_build/github/azure-pipelines/stages/nodejs-npm-packaging-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/nodejs-npm-packaging-stage.yml @@ -51,8 +51,7 @@ stages: NpmPackagingMode: '' steps: - - checkout: self - submodules: true + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/stages/nodejs-win-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/nodejs-win-packaging-stage.yml index 3f3da4943f3f0..3bdd9922d1a37 100644 --- a/tools/ci_build/github/azure-pipelines/stages/nodejs-win-packaging-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/nodejs-win-packaging-stage.yml @@ -64,9 +64,7 @@ stages: build_py_lto_flag: --enable_lto steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/stages/nuget-cuda-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/nuget-cuda-packaging-stage.yml index 20105b467d001..5524edba88713 100644 --- a/tools/ci_build/github/azure-pipelines/stages/nuget-cuda-packaging-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/nuget-cuda-packaging-stage.yml @@ -36,8 +36,7 @@ stages: ReleaseVersionSuffix: $[stageDependencies.Setup.Set_Variables.outputs['Set_Release_Version_Suffix.ReleaseVersionSuffix']] steps: - - checkout: self - submodules: true + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/stages/nuget-linux-cuda-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/nuget-linux-cuda-packaging-stage.yml index a4bef2dd16831..e13fa24ef374f 100644 --- a/tools/ci_build/github/azure-pipelines/stages/nuget-linux-cuda-packaging-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/nuget-linux-cuda-packaging-stage.yml @@ -84,9 +84,7 @@ stages: ${{ if eq(parameters.CudaVersion, '12.8') }}: value: ${{ variables.linux_trt_version_cuda12 }} steps: - - checkout: self - clean: true - submodules: recursive + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml - template: ../templates/get-docker-image-steps.yml parameters: @@ -146,9 +144,8 @@ stages: ${{ if eq(parameters.CudaVersion, '12.8') }}: value: ${{ variables.linux_trt_version_cuda12 }} steps: - - checkout: self # due to checkout multiple repos, the root directory is $(Build.SourcesDirectory)/onnxruntime - submodules: false - - checkout: onnxruntime-inference-examples # due to checkout multiple repos, the root directory is $(Build.SourcesDirectory)/onnxruntime-inference-examples + - template: ../templates/jobs/checkout-and-git-redirect.yml # due to checkout multiple repos, the root directory is $(Build.SourcesDirectory)/onnxruntime + - checkout: onnxruntime-inference-examples # due to checkout multiple repos, the root directory is $(Build.SourcesDirectory)/onnxruntime-inference-examples submodules: false - script: | diff --git a/tools/ci_build/github/azure-pipelines/stages/nuget-win-cuda-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/nuget-win-cuda-packaging-stage.yml index 554863b7b9ec2..c270058cc1d73 100644 --- a/tools/ci_build/github/azure-pipelines/stages/nuget-win-cuda-packaging-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/nuget-win-cuda-packaging-stage.yml @@ -128,8 +128,8 @@ stages: ${{ if eq(parameters.CudaVersion, '12.8') }}: CUDA_VERSION_MAJOR: '12' steps: - - checkout: self # due to checkout multiple repos, the root directory is $(Build.SourcesDirectory)/onnxruntime - - checkout: onnxruntime-inference-examples # due to checkout multiple repos, the root directory is $(Build.SourcesDirectory)/onnxruntime-inference-examples + - template: ../templates/jobs/checkout-and-git-redirect.yml # due to checkout multiple repos, the root directory is $(Build.SourcesDirectory)/onnxruntime + - checkout: onnxruntime-inference-examples # due to checkout multiple repos, the root directory is $(Build.SourcesDirectory)/onnxruntime-inference-examples submodules: false - task: PowerShell@2 diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-cuda-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-cuda-nuget-packaging-stage.yml index 3f0f1f06c0a82..5ab41e824bb11 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-cuda-nuget-packaging-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-cuda-nuget-packaging-stage.yml @@ -66,9 +66,7 @@ stages: --configuration Release --nuget-config "$(Build.SourcesDirectory)\NuGet.config" steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-cuda-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-cuda-stage.yml index 70114ccfcc52a..89625e049265e 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-cuda-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-cuda-stage.yml @@ -96,9 +96,7 @@ stages: ${{ else }}: value: 'onnxruntime_USE_FPA_INTB_GEMM=OFF' steps: - - checkout: self - clean: true - submodules: recursive + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/set-nightly-build-option-variable-step.yml @@ -194,9 +192,7 @@ stages: variables: - template: ../templates/common-variables.yml steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/set-nightly-build-option-variable-step.yml diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-cuda-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-cuda-test-stage.yml index 834bb11809c6a..a798e469a87e9 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-cuda-test-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-cuda-test-stage.yml @@ -15,9 +15,7 @@ stages: name: ${{ parameters.machine_pool }} os: linux steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml index 8c5962f1b6ada..e1b6b94d4f15c 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-stage.yml @@ -50,9 +50,7 @@ stages: variables: - template: ../templates/common-variables.yml steps: - - checkout: self - clean: true - submodules: recursive + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/set-nightly-build-option-variable-step.yml @@ -109,9 +107,7 @@ stages: variables: - template: ../templates/common-variables.yml steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/set-nightly-build-option-variable-step.yml diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml index d777131428c56..f305241c07832 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-linux-webgpu-test-stage.yml @@ -36,9 +36,7 @@ stages: workspace: clean: all steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml index 455121df57a77..e9147200d31f4 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-stage.yml @@ -38,9 +38,7 @@ stages: value: '14.0' - template: ../templates/common-variables.yml steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/use-xcode-version.yml @@ -134,9 +132,7 @@ stages: variables: - template: ../templates/common-variables.yml steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml index ab58eaf2b296f..ff84f34abc235 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-mac-webgpu-test-stage.yml @@ -12,9 +12,7 @@ stages: demands: - ImageOverride -equals ACES_VM_SharedPool_Sequoia steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml index 549dae5bde22b..c7dfcd50a4e68 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-webgpu-nuget-packaging-stage.yml @@ -64,9 +64,7 @@ stages: --configuration Release --nuget-config "$(Build.SourcesDirectory)\NuGet.config" steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-cuda-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-cuda-stage.yml index 58fd31384ef8f..ef1842670bc18 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-cuda-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-cuda-stage.yml @@ -102,9 +102,7 @@ stages: ${{ else }}: value: '--cmake_extra_defines onnxruntime_USE_FPA_INTB_GEMM=OFF' steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: @@ -379,9 +377,7 @@ stages: variables: - template: ../templates/common-variables.yml steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-cuda-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-cuda-test-stage.yml index b66be25e071c2..000a0bd221e67 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-cuda-test-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-cuda-test-stage.yml @@ -12,9 +12,7 @@ stages: variables: - template: ../templates/common-variables.yml steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml @@ -101,8 +99,7 @@ stages: - name: CudaTestProject value: '$(Build.SourcesDirectory)\plugin-ep-cuda\csharp\test\CudaEpNuGetTest\CudaEpNuGetTest.csproj' steps: - - checkout: self - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml index ccd602c261a5b..7a31dfe309842 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-stage.yml @@ -62,9 +62,7 @@ stages: - name: VSGenerator value: 'Visual Studio 17 2022' steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: @@ -207,9 +205,7 @@ stages: variables: - template: ../templates/common-variables.yml steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml index 83fcceac72dd2..7f09aaf1ea049 100644 --- a/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/plugin-win-webgpu-test-stage.yml @@ -22,9 +22,7 @@ stages: name: onnxruntime-Win2022-VS2022-webgpu-A10 os: windows steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: @@ -75,8 +73,7 @@ stages: variables: WebGpuTestProject: '$(Build.SourcesDirectory)\plugin-ep-webgpu\csharp\test\WebGpuEpNuGetTest\WebGpuEpNuGetTest.csproj' steps: - - checkout: self - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/stages/py-gpu-packaging-stage.yml b/tools/ci_build/github/azure-pipelines/stages/py-gpu-packaging-stage.yml index fcc14f971d9bb..930c6ec86744e 100644 --- a/tools/ci_build/github/azure-pipelines/stages/py-gpu-packaging-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/py-gpu-packaging-stage.yml @@ -160,9 +160,7 @@ stages: targetPath: $(Build.ArtifactStagingDirectory)/onnxruntime_gpu artifactName: onnxruntime_gpu steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - ${{ each config in parameters.LinuxPythonConfigurations }}: - task: DownloadPipelineArtifact@2 diff --git a/tools/ci_build/github/azure-pipelines/stages/py-linux-gpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/py-linux-gpu-stage.yml index 2f22bb6918fb3..4b18d6ec54194 100644 --- a/tools/ci_build/github/azure-pipelines/stages/py-linux-gpu-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/py-linux-gpu-stage.yml @@ -99,9 +99,7 @@ stages: ${{ else }}: value: '' steps: - - checkout: self - clean: true - submodules: recursive + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/set-nightly-build-option-variable-step.yml diff --git a/tools/ci_build/github/azure-pipelines/stages/py-linux-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/py-linux-webgpu-stage.yml index b0b22f1392615..d00c0dfe263fc 100644 --- a/tools/ci_build/github/azure-pipelines/stages/py-linux-webgpu-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/py-linux-webgpu-stage.yml @@ -48,9 +48,9 @@ stages: value: '' - template: ../templates/common-variables.yml steps: - - checkout: self - clean: true - submodules: recursive + - template: ../templates/jobs/checkout-and-git-redirect.yml + parameters: + submodules: recursive - template: ../templates/set-nightly-build-option-variable-step.yml diff --git a/tools/ci_build/github/azure-pipelines/stages/py-win-gpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/py-win-gpu-stage.yml index fd6fccd0aff7c..bceffd8e05819 100644 --- a/tools/ci_build/github/azure-pipelines/stages/py-win-gpu-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/py-win-gpu-stage.yml @@ -91,9 +91,7 @@ stages: ${{ if contains(parameters.EP_BUILD_FLAGS, 'use_dml') }}: value: '' steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: @@ -198,9 +196,7 @@ stages: artifactName: win_${{ parameters.EP_NAME }}_wheel_${{ parameters.PYTHON_VERSION }} targetPath: '$(Build.ArtifactStagingDirectory)' steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/stages/py-win-webgpu-stage.yml b/tools/ci_build/github/azure-pipelines/stages/py-win-webgpu-stage.yml index 742178e75b33d..0b6e4479ef597 100644 --- a/tools/ci_build/github/azure-pipelines/stages/py-win-webgpu-stage.yml +++ b/tools/ci_build/github/azure-pipelines/stages/py-win-webgpu-stage.yml @@ -60,9 +60,7 @@ stages: - name: VSGenerator value: 'Visual Studio 17 2022' steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-build-tools.yml parameters: @@ -151,9 +149,7 @@ stages: artifactName: win_webgpu_wheel_${{ parameters.PYTHON_VERSION }} targetPath: '$(Build.ArtifactStagingDirectory)' steps: - - checkout: self - clean: true - submodules: none + - template: ../templates/jobs/checkout-and-git-redirect.yml - template: ../templates/setup-feeds-and-python-steps.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/templates/android-binary-size-check-stage.yml b/tools/ci_build/github/azure-pipelines/templates/android-binary-size-check-stage.yml index 06075d3c30aa5..b4a3a3b98ebbb 100644 --- a/tools/ci_build/github/azure-pipelines/templates/android-binary-size-check-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/android-binary-size-check-stage.yml @@ -33,9 +33,7 @@ stages: clean: all pool: onnxruntime-Ubuntu2204-AMD-CPU steps: - - checkout: self - clean: true - submodules: none + - template: jobs/checkout-and-git-redirect.yml - template: use-android-ndk.yml - template: get-docker-image-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/templates/android-java-api-aar.yml b/tools/ci_build/github/azure-pipelines/templates/android-java-api-aar.yml index 1ade70c8e56d3..275273dd77adb 100644 --- a/tools/ci_build/github/azure-pipelines/templates/android-java-api-aar.yml +++ b/tools/ci_build/github/azure-pipelines/templates/android-java-api-aar.yml @@ -80,9 +80,7 @@ jobs: targetPath: $(Build.BinariesDirectory)/.artifacts artifactName: ${{parameters.artifactName}} steps: - - checkout: self - clean: true - submodules: none + - template: jobs/checkout-and-git-redirect.yml - task: CmdLine@2 displayName: Create artifacts directory diff --git a/tools/ci_build/github/azure-pipelines/templates/c-api-cpu.yml b/tools/ci_build/github/azure-pipelines/templates/c-api-cpu.yml index b396cbb658260..15f3ac57b2858 100644 --- a/tools/ci_build/github/azure-pipelines/templates/c-api-cpu.yml +++ b/tools/ci_build/github/azure-pipelines/templates/c-api-cpu.yml @@ -165,7 +165,7 @@ stages: artifactName: 'onnxruntime-ios-full-xcframework-test' targetPath: '$(Build.BinariesDirectory)/artifacts/test' steps: - - checkout: self + - template: jobs/checkout-and-git-redirect.yml - template: use-xcode-version.yml parameters: @@ -329,8 +329,9 @@ stages: ReleaseVersionSuffix: $[stageDependencies.Setup.Set_Variables.outputs['Set_Release_Version_Suffix.ReleaseVersionSuffix']] steps: - - checkout: self - submodules: true + - template: jobs/checkout-and-git-redirect.yml + parameters: + submodules: true - template: setup-feeds-and-python-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/templates/c-api-linux-cpu.yml b/tools/ci_build/github/azure-pipelines/templates/c-api-linux-cpu.yml index fceeeb07a95a0..d8efcf11be121 100644 --- a/tools/ci_build/github/azure-pipelines/templates/c-api-linux-cpu.yml +++ b/tools/ci_build/github/azure-pipelines/templates/c-api-linux-cpu.yml @@ -42,9 +42,7 @@ jobs: ${{ if eq(parameters.OnnxruntimeArch, 'aarch64') }}: hostArchitecture: Arm64 steps: - - checkout: self - clean: true - submodules: none + - template: jobs/checkout-and-git-redirect.yml # Authenticate to Lotus to pull pip in UsePythonVersion task - template: setup-feeds-and-python-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/templates/final-jar-testing-gpu.yml b/tools/ci_build/github/azure-pipelines/templates/final-jar-testing-gpu.yml index 22f3621c89c64..bd93331dd84d5 100644 --- a/tools/ci_build/github/azure-pipelines/templates/final-jar-testing-gpu.yml +++ b/tools/ci_build/github/azure-pipelines/templates/final-jar-testing-gpu.yml @@ -91,8 +91,7 @@ stages: value: onnxruntimebuildcache.azurecr.io/internal/azureml/onnxruntime/build/cuda12_x64_almalinux8_gcc14:20251017.1 timeoutInMinutes: 60 steps: - - checkout: self - submodules: false + - template: jobs/checkout-and-git-redirect.yml - template: set-version-number-variables-step.yml diff --git a/tools/ci_build/github/azure-pipelines/templates/jar-packaging.yml b/tools/ci_build/github/azure-pipelines/templates/jar-packaging.yml index 4d60544f5f8d9..715d2758ed16b 100644 --- a/tools/ci_build/github/azure-pipelines/templates/jar-packaging.yml +++ b/tools/ci_build/github/azure-pipelines/templates/jar-packaging.yml @@ -12,8 +12,7 @@ parameters: - 'gpu' steps: -- checkout: self - submodules: false +- template: jobs/checkout-and-git-redirect.yml - template: setup-feeds-and-python-steps.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/templates/jobs/checkout-and-git-redirect.yml b/tools/ci_build/github/azure-pipelines/templates/jobs/checkout-and-git-redirect.yml new file mode 100644 index 0000000000000..dee8c604dd4c6 --- /dev/null +++ b/tools/ci_build/github/azure-pipelines/templates/jobs/checkout-and-git-redirect.yml @@ -0,0 +1,16 @@ +parameters: +- name: submodules + type: string + default: false + +steps: +- pwsh: | + git config --global --add url."https://PAT:${env:TOKEN}@aiinfra.visualstudio.com/Lotus-Dependencies/_git/".insteadOf "https://chromium.googlesource.com/chromium/src/third_party/" + git config --global --add url."https://github.com/".insteadOf "https://chromium.googlesource.com/external/github.com/" + displayName: Redirect chromium hosted dependencies to GitHub and Lotus-Dependencies + env: + TOKEN: $(System.AccessToken) + +- checkout: self + clean: true + submodules: ${{ parameters.submodules }} diff --git a/tools/ci_build/github/azure-pipelines/templates/jobs/win-ci-vs-2022-job.yml b/tools/ci_build/github/azure-pipelines/templates/jobs/win-ci-vs-2022-job.yml index eb6492f779b94..2394150714036 100644 --- a/tools/ci_build/github/azure-pipelines/templates/jobs/win-ci-vs-2022-job.yml +++ b/tools/ci_build/github/azure-pipelines/templates/jobs/win-ci-vs-2022-job.yml @@ -86,9 +86,7 @@ jobs: pool: ${{ parameters.MachinePool }} timeoutInMinutes: 300 steps: - - checkout: self - clean: true - submodules: none + - template: checkout-and-git-redirect.yml - template: win-ci-prebuild-steps.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/templates/linux-wasm-ci.yml b/tools/ci_build/github/azure-pipelines/templates/linux-wasm-ci.yml index 849a54345a33b..3e0e24f7d3e97 100644 --- a/tools/ci_build/github/azure-pipelines/templates/linux-wasm-ci.yml +++ b/tools/ci_build/github/azure-pipelines/templates/linux-wasm-ci.yml @@ -65,7 +65,8 @@ jobs: workspace: clean: all steps: - - checkout: self + - template: jobs/checkout-and-git-redirect.yml + - task: DownloadPipelineArtifact@2 inputs: artifact: '__commit' diff --git a/tools/ci_build/github/azure-pipelines/templates/mac-cpu-packaging-pipeline.yml b/tools/ci_build/github/azure-pipelines/templates/mac-cpu-packaging-pipeline.yml index 0bc0a94fdd6e3..7261dfbf2b309 100644 --- a/tools/ci_build/github/azure-pipelines/templates/mac-cpu-packaging-pipeline.yml +++ b/tools/ci_build/github/azure-pipelines/templates/mac-cpu-packaging-pipeline.yml @@ -48,7 +48,7 @@ stages: targetPath: $(Build.ArtifactStagingDirectory) artifactName: 'onnxruntime-osx' # The files in this artifact are signed steps: - - checkout: self + - template: jobs/checkout-and-git-redirect.yml - task: UsePythonVersion@0 inputs: diff --git a/tools/ci_build/github/azure-pipelines/templates/mac-cpu-packing-jobs.yml b/tools/ci_build/github/azure-pipelines/templates/mac-cpu-packing-jobs.yml index 0928a19559cd5..973403d9aa69f 100644 --- a/tools/ci_build/github/azure-pipelines/templates/mac-cpu-packing-jobs.yml +++ b/tools/ci_build/github/azure-pipelines/templates/mac-cpu-packing-jobs.yml @@ -27,10 +27,7 @@ jobs: - ImageOverride -equals ACES_VM_SharedPool_Sequoia timeoutInMinutes: 300 steps: - - checkout: self - clean: true - submodules: none - + - template: jobs/checkout-and-git-redirect.yml - template: use-xcode-version.yml - template: setup-build-tools.yml @@ -39,11 +36,12 @@ jobs: - template: set-version-number-variables-step.yml - - script: | - set -e -x + - bash: | + set -euxo pipefail export ONNX_ML=1 export CMAKE_ARGS="-DONNX_GEN_PB_TYPE_STUBS=ON -DONNX_WERROR=OFF" - python3 -m pip install -r '$(Build.SourcesDirectory)/tools/ci_build/github/linux/docker/scripts/requirements.txt' + python3 -m pip install -r "${BUILD_SOURCESDIRECTORY}/tools/ci_build/github/linux/docker/scripts/requirements.txt" + displayName: "PIP install docker script requirements" - script: | set -e -x diff --git a/tools/ci_build/github/azure-pipelines/templates/publish-nuget-steps.yml b/tools/ci_build/github/azure-pipelines/templates/publish-nuget-steps.yml index f26d5da7faaf7..e0f53ce43d67d 100644 --- a/tools/ci_build/github/azure-pipelines/templates/publish-nuget-steps.yml +++ b/tools/ci_build/github/azure-pipelines/templates/publish-nuget-steps.yml @@ -27,8 +27,7 @@ stages: echo $(Build.Reason) displayName: 'Print triggering sourceBranch Name in resources' - - checkout: self - submodules: false + - template: jobs/checkout-and-git-redirect.yml - template: setup-feeds-and-python-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/templates/py-linux.yml b/tools/ci_build/github/azure-pipelines/templates/py-linux.yml index e74dea27c38fb..7078daaefbfe0 100644 --- a/tools/ci_build/github/azure-pipelines/templates/py-linux.yml +++ b/tools/ci_build/github/azure-pipelines/templates/py-linux.yml @@ -73,9 +73,7 @@ jobs: value: '' steps: - - checkout: self - clean: true - submodules: none + - template: jobs/checkout-and-git-redirect.yml - template: set-nightly-build-option-variable-step.yml - template: setup-feeds-and-python-steps.yml diff --git a/tools/ci_build/github/azure-pipelines/templates/py-macos.yml b/tools/ci_build/github/azure-pipelines/templates/py-macos.yml index 8e1557cbc1c65..3ab3c3c1affe7 100644 --- a/tools/ci_build/github/azure-pipelines/templates/py-macos.yml +++ b/tools/ci_build/github/azure-pipelines/templates/py-macos.yml @@ -39,9 +39,7 @@ jobs: value: '14.0' steps: - - checkout: self - clean: true - submodules: none + - template: jobs/checkout-and-git-redirect.yml - template: use-xcode-version.yml diff --git a/tools/ci_build/github/azure-pipelines/templates/py-packaging-linux-test-cpu.yml b/tools/ci_build/github/azure-pipelines/templates/py-packaging-linux-test-cpu.yml index ca83d4da8f8fa..522b0d37fdf23 100644 --- a/tools/ci_build/github/azure-pipelines/templates/py-packaging-linux-test-cpu.yml +++ b/tools/ci_build/github/azure-pipelines/templates/py-packaging-linux-test-cpu.yml @@ -42,9 +42,8 @@ jobs: clean: all pool: ${{ parameters.machine_pool }} steps: - - checkout: self - clean: true - submodules: none + - template: jobs/checkout-and-git-redirect.yml + - download: build # pipeline resource identifier. artifact: 'drop-linux-cpu-${{ parameters.arch }}-${{parameters.ep}}' diff --git a/tools/ci_build/github/azure-pipelines/templates/py-packaging-linux-test-cuda.yml b/tools/ci_build/github/azure-pipelines/templates/py-packaging-linux-test-cuda.yml index 165f66340e7c8..b2adfc69d4afc 100644 --- a/tools/ci_build/github/azure-pipelines/templates/py-packaging-linux-test-cuda.yml +++ b/tools/ci_build/github/azure-pipelines/templates/py-packaging-linux-test-cuda.yml @@ -60,9 +60,7 @@ jobs: clean: all pool: ${{ parameters.machine_pool }} steps: - - checkout: self - clean: true - submodules: none + - template: jobs/checkout-and-git-redirect.yml - template: setup-feeds-and-python-steps.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/templates/py-win-cpu.yml b/tools/ci_build/github/azure-pipelines/templates/py-win-cpu.yml index 92e335d123759..817fcb38a1e36 100644 --- a/tools/ci_build/github/azure-pipelines/templates/py-win-cpu.yml +++ b/tools/ci_build/github/azure-pipelines/templates/py-win-cpu.yml @@ -72,9 +72,9 @@ jobs: clean: all steps: - - checkout: self - clean: true - submodules: recursive + - template: jobs/checkout-and-git-redirect.yml + parameters: + submodules: recursive - template: setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/templates/py-win-x64-qnn.yml b/tools/ci_build/github/azure-pipelines/templates/py-win-x64-qnn.yml index 0363f2a5ffa0d..bdc80a41bbc85 100644 --- a/tools/ci_build/github/azure-pipelines/templates/py-win-x64-qnn.yml +++ b/tools/ci_build/github/azure-pipelines/templates/py-win-x64-qnn.yml @@ -45,9 +45,9 @@ jobs: GRADLE_OPTS: '-Dorg.gradle.daemon=false' VSGenerator: 'Visual Studio 17 2022' steps: - - checkout: self - clean: true - submodules: recursive + - template: jobs/checkout-and-git-redirect.yml + parameters: + submodules: recursive - template: setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/templates/test-binary-archive-stage.yml b/tools/ci_build/github/azure-pipelines/templates/test-binary-archive-stage.yml index b9b9cdc6b0eb3..2dfbbb27641f9 100644 --- a/tools/ci_build/github/azure-pipelines/templates/test-binary-archive-stage.yml +++ b/tools/ci_build/github/azure-pipelines/templates/test-binary-archive-stage.yml @@ -35,9 +35,7 @@ stages: value: "." steps: - - checkout: self - clean: true - submodules: none + - template: jobs/checkout-and-git-redirect.yml - ${{ each agentSetupStep in parameters.agentSetupSteps }}: - ${{ agentSetupStep }} diff --git a/tools/ci_build/github/azure-pipelines/templates/web-browserstack-ci.yml b/tools/ci_build/github/azure-pipelines/templates/web-browserstack-ci.yml index 8f4962698b827..c4ab1a2d6034b 100644 --- a/tools/ci_build/github/azure-pipelines/templates/web-browserstack-ci.yml +++ b/tools/ci_build/github/azure-pipelines/templates/web-browserstack-ci.yml @@ -10,8 +10,8 @@ jobs: workspace: clean: all steps: - - checkout: self - submodules: false + - template: jobs/checkout-and-git-redirect.yml + - task: DownloadPipelineArtifact@2 inputs: artifact: '__commit' diff --git a/tools/ci_build/github/azure-pipelines/templates/web-ci.yml b/tools/ci_build/github/azure-pipelines/templates/web-ci.yml index d155e3e09dc80..e10c4fbe6ed1e 100644 --- a/tools/ci_build/github/azure-pipelines/templates/web-ci.yml +++ b/tools/ci_build/github/azure-pipelines/templates/web-ci.yml @@ -76,8 +76,8 @@ stages: workspace: clean: all steps: - - checkout: self - submodules: false + - template: jobs/checkout-and-git-redirect.yml + - script: | git submodule sync -- cmake/external/onnx git submodule update --init -- cmake/external/onnx diff --git a/tools/ci_build/github/azure-pipelines/templates/win-ci.yml b/tools/ci_build/github/azure-pipelines/templates/win-ci.yml index 48495221a8dd0..b4e8d0420c2b8 100644 --- a/tools/ci_build/github/azure-pipelines/templates/win-ci.yml +++ b/tools/ci_build/github/azure-pipelines/templates/win-ci.yml @@ -148,9 +148,7 @@ stages: timeoutInMinutes: 360 steps: - - checkout: self - clean: true - submodules: none + - template: jobs/checkout-and-git-redirect.yml - template: setup-build-tools.yml parameters: diff --git a/tools/ci_build/github/azure-pipelines/templates/win-wasm-ci.yml b/tools/ci_build/github/azure-pipelines/templates/win-wasm-ci.yml index b9099e001a3d5..2ee53f248e77c 100644 --- a/tools/ci_build/github/azure-pipelines/templates/win-wasm-ci.yml +++ b/tools/ci_build/github/azure-pipelines/templates/win-wasm-ci.yml @@ -52,7 +52,8 @@ jobs: workspace: clean: all steps: - - checkout: self + - template: jobs/checkout-and-git-redirect.yml + - task: DownloadPipelineArtifact@2 inputs: artifact: '__commit' diff --git a/tools/ci_build/github/azure-pipelines/templates/win-web-ci.yml b/tools/ci_build/github/azure-pipelines/templates/win-web-ci.yml index 895e87a73ded0..7789b7052d306 100644 --- a/tools/ci_build/github/azure-pipelines/templates/win-web-ci.yml +++ b/tools/ci_build/github/azure-pipelines/templates/win-web-ci.yml @@ -43,8 +43,8 @@ jobs: workspace: clean: all steps: - - checkout: self - submodules: false + - template: jobs/checkout-and-git-redirect.yml + - task: DownloadPipelineArtifact@2 inputs: artifact: '__commit' diff --git a/tools/ci_build/github/azure-pipelines/templates/win-web-multi-browsers.yml b/tools/ci_build/github/azure-pipelines/templates/win-web-multi-browsers.yml index 1123c71600345..f16eb4c943ad8 100644 --- a/tools/ci_build/github/azure-pipelines/templates/win-web-multi-browsers.yml +++ b/tools/ci_build/github/azure-pipelines/templates/win-web-multi-browsers.yml @@ -12,8 +12,8 @@ jobs: workspace: clean: all steps: - - checkout: self - submodules: false + - template: jobs/checkout-and-git-redirect.yml + - task: DownloadPipelineArtifact@2 inputs: artifact: '__commit' diff --git a/tools/ci_build/github/linux/build_nodejs_package.sh b/tools/ci_build/github/linux/build_nodejs_package.sh index 01a9159cf7a44..961c0a9e68e62 100755 --- a/tools/ci_build/github/linux/build_nodejs_package.sh +++ b/tools/ci_build/github/linux/build_nodejs_package.sh @@ -12,19 +12,44 @@ else fi mkdir -p "$HOME/.onnx" -docker run -e SYSTEM_COLLECTIONURI --rm --network=host --volume /data/onnx:/data/onnx:ro --volume "$BUILD_SOURCESDIRECTORY:/onnxruntime_src" \ ---volume "$BUILD_BINARIESDIRECTORY:/build" --volume /data/models:/build/models:ro \ --e NPM_CONFIG_USERCONFIG=/tmp/.npmrc \ --e PIP_INDEX_URL \ ---volume "${NPM_CONFIG_USERCONFIG}:/tmp/.npmrc:ro" \ ---volume "$HOME/.m2:/home/onnxruntimedev/.m2:ro" \ ---volume "$HOME/.gradle:/home/onnxruntimedev/.gradle" \ ---volume "$HOME/.onnx:/home/onnxruntimedev/.onnx" -e NIGHTLY_BUILD "onnxruntimecuda${CUDA_VERSION_MAJOR}xtrt86build" \ -/bin/bash -c "/usr/bin/python3 /onnxruntime_src/tools/ci_build/build.py --build_dir /build --config Release \ ---skip_tests --skip_submodule_sync \ ---parallel --nvcc_threads 1 --flash_nvcc_threads 1 \ ---use_binskim_compliant_compile_flags --build_shared_lib --build_nodejs \ ---use_webgpu --use_tensorrt --cuda_version=$CUDA_VERSION --cuda_home=/usr/local/cuda-$CUDA_VERSION \ ---cudnn_home=/usr --tensorrt_home=/usr \ ---cmake_extra_defines 'CMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHS}' --use_vcpkg --use_vcpkg_ms_internal_asset_cache \ -&& cd /build/Release && make install DESTDIR=/build/installed" +docker run \ + --network=host \ + --rm \ + --volume "/data/models:/build/models:ro" \ + --volume "/data/onnx:/data/onnx:ro" \ + --volume "${BUILD_BINARIESDIRECTORY}:/build" \ + --volume "${BUILD_SOURCESDIRECTORY}:/onnxruntime_src" \ + --volume "${HOME}/.gitconfig:/home/onnxruntimedev/.gitconfig:ro" \ + --volume "${HOME}/.gradle:/home/onnxruntimedev/.gradle" \ + --volume "${HOME}/.m2:/home/onnxruntimedev/.m2:ro" \ + --volume "${HOME}/.onnx:/home/onnxruntimedev/.onnx" \ + --volume "${NPM_CONFIG_USERCONFIG}:/tmp/.npmrc:ro" \ + -e NIGHTLY_BUILD \ + -e NPM_CONFIG_USERCONFIG=/tmp/.npmrc \ + -e PIP_INDEX_URL \ + -e SYSTEM_COLLECTIONURI \ + "onnxruntimecuda${CUDA_VERSION_MAJOR}xtrt86build" \ + /bin/bash -c "\ +/usr/bin/python3 /onnxruntime_src/tools/ci_build/build.py \ + --build_dir /build \ + --build_nodejs \ + --build_shared_lib \ + --cmake_extra_defines 'CMAKE_CUDA_ARCHITECTURES=${CUDA_ARCHS}' \ + --cmake_extra_defines 'CMAKE_MESSAGE_LOG_LEVEL=VERBOSE' \ + --config Release \ + --cuda_home '/usr/local/cuda-${CUDA_VERSION}' \ + --cuda_version '${CUDA_VERSION}' \ + --cudnn_home '/usr' \ + --flash_nvcc_threads 1 \ + --nvcc_threads 1 \ + --parallel \ + --skip_submodule_sync \ + --skip_tests \ + --tensorrt_home '/usr' \ + --use_binskim_compliant_compile_flags \ + --use_tensorrt \ + --use_vcpkg \ + --use_vcpkg_ms_internal_asset_cache \ + --use_webgpu \ +&& cd /build/Release \ +&& make install DESTDIR=/build/installed" diff --git a/tools/ci_build/github/linux/build_webgpu_plugin_package.sh b/tools/ci_build/github/linux/build_webgpu_plugin_package.sh index 29f5ec75a8d40..8d9b69e66b4ae 100755 --- a/tools/ci_build/github/linux/build_webgpu_plugin_package.sh +++ b/tools/ci_build/github/linux/build_webgpu_plugin_package.sh @@ -7,34 +7,38 @@ set -e -x BUILD_CONFIG="Release" DOCKER_IMAGE="onnxruntimewebgpuplugin" -while getopts "i:c:" parameter_Option -do case "${parameter_Option}" -in -i) DOCKER_IMAGE=${OPTARG};; -c) BUILD_CONFIG=${OPTARG};; -*) echo "Usage: $0 -i [-c ]" - exit 1;; -esac +while getopts "i:c:" parameter_Option; do + case "${parameter_Option}" in + i) DOCKER_IMAGE=${OPTARG} ;; + c) BUILD_CONFIG=${OPTARG} ;; + *) + echo "Usage: $0 -i [-c ]" + exit 1 + ;; + esac done mkdir -p "${HOME}/.onnx" -docker run --rm \ - --volume /data/onnx:/data/onnx:ro \ - --volume "${BUILD_SOURCESDIRECTORY}:/onnxruntime_src" \ +docker run \ + --rm \ --volume "${BUILD_BINARIESDIRECTORY}:/build" \ - --volume /data/models:/build/models:ro \ + --volume "${BUILD_SOURCESDIRECTORY}:/onnxruntime_src" \ + --volume "${HOME}/.gitconfig:/home/onnxruntimedev/.gitconfig:ro" \ + --volume "${HOME}/.gradle:/home/onnxruntimedev/.gradle" \ + --volume "${HOME}/.m2:/home/onnxruntimedev/.m2:ro" \ --volume "${HOME}/.onnx:/home/onnxruntimedev/.onnx" \ - -e NPM_CONFIG_USERCONFIG=/tmp/.npmrc \ - -e PIP_INDEX_URL \ --volume "${NPM_CONFIG_USERCONFIG}:/tmp/.npmrc:ro" \ - --volume "$HOME/.m2:/home/onnxruntimedev/.m2:ro" \ - --volume "$HOME/.gradle:/home/onnxruntimedev/.gradle" \ - -e NIGHTLY_BUILD \ + --volume /data/models:/build/models:ro \ + --volume /data/onnx:/data/onnx:ro \ -e BUILD_BUILDNUMBER \ + -e NIGHTLY_BUILD \ + -e NPM_CONFIG_USERCONFIG=/tmp/.npmrc \ + -e PIP_INDEX_URL \ -e SYSTEM_COLLECTIONURI \ "$DOCKER_IMAGE" \ - /bin/bash -c "/usr/bin/python3 /onnxruntime_src/tools/ci_build/build.py \ + /bin/bash -c "\ + /usr/bin/python3 /onnxruntime_src/tools/ci_build/build.py \ --build_dir /build \ --config ${BUILD_CONFIG} \ --skip_submodule_sync \ diff --git a/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/generated/nn/im2col_matmul.h b/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/generated/nn/im2col_matmul.h index 739ca2b734954..e98111efef2e6 100644 --- a/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/generated/nn/im2col_matmul.h +++ b/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/generated/nn/im2col_matmul.h @@ -10,6 +10,7 @@ Status ApplyTemplate<"nn/im2col_matmul.wgsl.template">(ShaderHelper& shader_help [[maybe_unused]] auto& ss = shader_helper.AdditionalImplementation(); // Extract parameters + auto& __param_activation_kind = params.param_activation_kind; auto& __param_has_bias = params.param_has_bias; auto& __param_tile_m = params.param_tile_m; auto& __param_tile_n = params.param_tile_n; @@ -29,294 +30,324 @@ Status ApplyTemplate<"nn/im2col_matmul.wgsl.template">(ShaderHelper& shader_help // 6 | #param tile_n // 7 | #param use_subgroup // 8 | #param vec_size -// 9 | -// 10 | #use .getByOffset .setByOffset -// 11 | -// 12 | // im2col access for src: [N, H_i, W_i, C_i / vec_size] -// 13 | // Conceptual Matrix Shape: N * (H_o * W_o) x (K_h * K_w * C_i / vec_size) -// 14 | fn load_src(batch : u32, m : u32, k_packed_idx : u32) -> src_value_t { +// 9 | // Mirrors ActivationKind; static_asserts in im2col_matmul.cc enforce these values. +// 10 | // 0=None, 1=Relu, 2=Sigmoid, 3=Clip, 4=HardSigmoid, 5=LeakyRelu, 6=Tanh. +// 11 | // Keep branches synchronized with IsActivationSupported(). +// 12 | #param activation_kind +// 13 | +// 14 | #use .getByOffset .setByOffset +// 15 | +// 16 | // im2col access for src: [N, H_i, W_i, C_i / vec_size] +// 17 | // Conceptual Matrix Shape: N * (H_o * W_o) x (K_h * K_w * C_i / vec_size) +// 18 | fn load_src(batch : u32, m : u32, k_packed_idx : u32) -> src_value_t { ss << "fn load_src(batch : u32, m : u32, k_packed_idx : u32) -> src_value_t {\n"; -// 15 | if (batch >= uniforms.batch || m >= uniforms.im2col_m || k_packed_idx * vec_size >= uniforms.im2col_k) { +// 19 | if (batch >= uniforms.batch || m >= uniforms.im2col_m || k_packed_idx * vec_size >= uniforms.im2col_k) { ss << " if (batch >= uniforms.batch || m >= uniforms.im2col_m || k_packed_idx * "; ss << __param_vec_size; ss << " >= uniforms.im2col_k) {\n"; -// 16 | return src_value_t(); +// 20 | return src_value_t(); ss << " return src_value_t();\n"; -// 17 | } +// 21 | } ss << " }\n"; -// 18 | +// 22 | ss << "\n"; -// 19 | let channel_i_vec = uniforms.channel_i / vec_size; +// 23 | let channel_i_vec = uniforms.channel_i / vec_size; ss << " let channel_i_vec = uniforms.channel_i / "; ss << __param_vec_size; ss << ";\n"; -// 20 | +// 24 | ss << "\n"; -// 21 | // 1. Decompose M index (H_o * W_o) into (h_idx, w_idx) -// 22 | let h_idx = m / uniforms.output_w; // Output H index (H_o) +// 25 | // 1. Decompose M index (H_o * W_o) into (h_idx, w_idx) +// 26 | let h_idx = m / uniforms.output_w; // Output H index (H_o) ss << " let h_idx = m / uniforms.output_w;\n"; -// 23 | let w_idx = m % uniforms.output_w; // Output W index (W_o) +// 27 | let w_idx = m % uniforms.output_w; // Output W index (W_o) ss << " let w_idx = m % uniforms.output_w;\n"; -// 24 | +// 28 | ss << "\n"; -// 25 | // 2. Decompose K index into (k_h, k_w, c_i_vec_idx) -// 26 | let c_i_vec_idx = k_packed_idx % channel_i_vec; +// 29 | // 2. Decompose K index into (k_h, k_w, c_i_vec_idx) +// 30 | let c_i_vec_idx = k_packed_idx % channel_i_vec; ss << " let c_i_vec_idx = k_packed_idx % channel_i_vec;\n"; -// 27 | let k_h_w_idx = k_packed_idx / channel_i_vec; +// 31 | let k_h_w_idx = k_packed_idx / channel_i_vec; ss << " let k_h_w_idx = k_packed_idx / channel_i_vec;\n"; -// 28 | let k_h = k_h_w_idx / uniforms.kernel_w; // Kernel Row +// 32 | let k_h = k_h_w_idx / uniforms.kernel_w; // Kernel Row ss << " let k_h = k_h_w_idx / uniforms.kernel_w;\n"; -// 29 | let k_w = k_h_w_idx % uniforms.kernel_w; // Kernel Column +// 33 | let k_w = k_h_w_idx % uniforms.kernel_w; // Kernel Column ss << " let k_w = k_h_w_idx % uniforms.kernel_w;\n"; -// 30 | +// 34 | ss << "\n"; -// 31 | // 3. Calculate the coordinate in the padded input tensor -// 32 | let src_h_coord_padded = h_idx * uniforms.strides.x + k_h * uniforms.dilations.x; +// 35 | // 3. Calculate the coordinate in the padded input tensor +// 36 | let src_h_coord_padded = h_idx * uniforms.strides.x + k_h * uniforms.dilations.x; ss << " let src_h_coord_padded = h_idx * uniforms.strides.x + k_h * uniforms.dilations.x;\n"; -// 33 | let src_w_coord_padded = w_idx * uniforms.strides.y + k_w * uniforms.dilations.y; +// 37 | let src_w_coord_padded = w_idx * uniforms.strides.y + k_w * uniforms.dilations.y; ss << " let src_w_coord_padded = w_idx * uniforms.strides.y + k_w * uniforms.dilations.y;\n"; -// 34 | +// 38 | ss << "\n"; -// 35 | // 4. Calculate the coordinate in the original input tensor -// 36 | let src_h_coord : i32 = i32(src_h_coord_padded) - i32(uniforms.pads.x); +// 39 | // 4. Calculate the coordinate in the original input tensor +// 40 | let src_h_coord : i32 = i32(src_h_coord_padded) - i32(uniforms.pads.x); ss << " let src_h_coord : i32 = i32(src_h_coord_padded) - i32(uniforms.pads.x);\n"; -// 37 | let src_w_coord : i32 = i32(src_w_coord_padded) - i32(uniforms.pads.y); +// 41 | let src_w_coord : i32 = i32(src_w_coord_padded) - i32(uniforms.pads.y); ss << " let src_w_coord : i32 = i32(src_w_coord_padded) - i32(uniforms.pads.y);\n"; -// 38 | +// 42 | ss << "\n"; -// 39 | // 5. Check for padding/out-of-bounds -// 40 | if (src_h_coord < 0 || src_h_coord >= i32(uniforms.src_h) || +// 43 | // 5. Check for padding/out-of-bounds +// 44 | if (src_h_coord < 0 || src_h_coord >= i32(uniforms.src_h) || ss << " if (src_h_coord < 0 || src_h_coord >= i32(uniforms.src_h) ||\n"; -// 41 | src_w_coord < 0 || src_w_coord >= i32(uniforms.src_w)) { +// 45 | src_w_coord < 0 || src_w_coord >= i32(uniforms.src_w)) { ss << " src_w_coord < 0 || src_w_coord >= i32(uniforms.src_w)) {\n"; -// 42 | return src_value_t(); +// 46 | return src_value_t(); ss << " return src_value_t();\n"; -// 43 | } +// 47 | } ss << " }\n"; -// 44 | +// 48 | ss << "\n"; -// 45 | // 6. Calculate final NHWC index -// 46 | let src_idx = batch * uniforms.src_h * uniforms.src_w * channel_i_vec + +// 49 | // 6. Calculate final NHWC index +// 50 | let src_idx = batch * uniforms.src_h * uniforms.src_w * channel_i_vec + ss << " let src_idx = batch * uniforms.src_h * uniforms.src_w * channel_i_vec +\n"; -// 47 | u32(src_h_coord) * uniforms.src_w * channel_i_vec + +// 51 | u32(src_h_coord) * uniforms.src_w * channel_i_vec + ss << " u32(src_h_coord) * uniforms.src_w * channel_i_vec +\n"; -// 48 | u32(src_w_coord) * channel_i_vec + +// 52 | u32(src_w_coord) * channel_i_vec + ss << " u32(src_w_coord) * channel_i_vec +\n"; -// 49 | c_i_vec_idx; +// 53 | c_i_vec_idx; ss << " c_i_vec_idx;\n"; -// 50 | return src.getByOffset(src_idx); +// 54 | return src.getByOffset(src_idx); ss << " return "; ss << __var_src.GetByOffset("src_idx"); ss << ";\n"; -// 51 | } +// 55 | } ss << "}\n"; -// 52 | +// 56 | ss << "\n"; -// 53 | // weight shape: [Co, K_h, K_w, C_i / vec_size] (CoHWCi) -// 54 | fn load_weight(n : u32, k_packed_idx : u32) -> weight_value_t { +// 57 | // weight shape: [Co, K_h, K_w, C_i / vec_size] (CoHWCi) +// 58 | fn load_weight(n : u32, k_packed_idx : u32) -> weight_value_t { ss << "fn load_weight(n : u32, k_packed_idx : u32) -> weight_value_t {\n"; -// 55 | if (n < uniforms.im2col_n && k_packed_idx < uniforms.im2col_k / vec_size) { +// 59 | if (n < uniforms.im2col_n && k_packed_idx < uniforms.im2col_k / vec_size) { ss << " if (n < uniforms.im2col_n && k_packed_idx < uniforms.im2col_k / "; ss << __param_vec_size; ss << ") {\n"; -// 56 | let weight_idx = n * uniforms.im2col_k / vec_size + +// 60 | let weight_idx = n * uniforms.im2col_k / vec_size + ss << " let weight_idx = n * uniforms.im2col_k / "; ss << __param_vec_size; ss << " +\n"; -// 57 | k_packed_idx; +// 61 | k_packed_idx; ss << " k_packed_idx;\n"; -// 58 | return weight.getByOffset(weight_idx); +// 62 | return weight.getByOffset(weight_idx); ss << " return "; ss << __var_weight.GetByOffset("weight_idx"); ss << ";\n"; -// 59 | } +// 63 | } ss << " }\n"; -// 60 | return weight_value_t(); +// 64 | return weight_value_t(); ss << " return weight_value_t();\n"; -// 61 | } +// 65 | } ss << "}\n"; -// 62 | +// 66 | ss << "\n"; -// 63 | fn load_bias(n : u32) -> output_element_t { +// 67 | fn load_bias(n : u32) -> output_element_t { ss << "fn load_bias(n : u32) -> output_element_t {\n"; -// 64 | #if has_bias +// 68 | #if has_bias if (__param_has_bias) { -// 65 | if (n < uniforms.im2col_n) { +// 69 | if (n < uniforms.im2col_n) { ss << " if (n < uniforms.im2col_n) {\n"; -// 66 | return output_element_t(bias[n]); +// 70 | return output_element_t(bias[n]); ss << " return output_element_t(bias[n]);\n"; -// 67 | } +// 71 | } ss << " }\n"; -// 68 | #endif +// 72 | #endif } -// 69 | return output_element_t(); +// 73 | return output_element_t(); ss << " return output_element_t();\n"; -// 70 | } +// 74 | } ss << "}\n"; -// 71 | +// 75 | ss << "\n"; -// 72 | // output shape: [N, H_o, W_o, C_o] (NHWC) -// 73 | fn write_output(batch : u32, m : u32, n : u32, value : output_element_t) { +// 76 | // output shape: [N, H_o, W_o, C_o] (NHWC) +// 77 | fn write_output(batch : u32, m : u32, n : u32, value : output_element_t) { ss << "fn write_output(batch : u32, m : u32, n : u32, value : output_element_t) {\n"; -// 74 | if (batch < uniforms.batch && m < uniforms.im2col_m && n < uniforms.im2col_n) { +// 78 | if (batch < uniforms.batch && m < uniforms.im2col_m && n < uniforms.im2col_n) { ss << " if (batch < uniforms.batch && m < uniforms.im2col_m && n < uniforms.im2col_n) {\n"; -// 75 | let output_idx = batch * uniforms.im2col_m * uniforms.im2col_n + +// 79 | let output_idx = batch * uniforms.im2col_m * uniforms.im2col_n + ss << " let output_idx = batch * uniforms.im2col_m * uniforms.im2col_n +\n"; -// 76 | m * uniforms.im2col_n + +// 80 | m * uniforms.im2col_n + ss << " m * uniforms.im2col_n +\n"; -// 77 | n; +// 81 | n; ss << " n;\n"; -// 78 | output.setByOffset(output_idx, value); +// 82 | output.setByOffset(output_idx, value); ss << " "; ss << __var_output.SetByOffset("output_idx", "value"); ss << ";\n"; -// 79 | } +// 83 | } ss << " }\n"; -// 80 | } +// 84 | } ss << "}\n"; -// 81 | +// 85 | ss << "\n"; -// 82 | const TILE_M_SIZE : u32 = tile_m; +// 86 | const TILE_M_SIZE : u32 = tile_m; ss << "const TILE_M_SIZE : u32 = "; ss << __param_tile_m; ss << ";\n"; -// 83 | const TILE_N_SIZE : u32 = tile_n; +// 87 | const TILE_N_SIZE : u32 = tile_n; ss << "const TILE_N_SIZE : u32 = "; ss << __param_tile_n; ss << ";\n"; -// 84 | // In dimension K, the tile consists of 16 scalars, requiring `16 / vec_size` vector loads. +// 88 | // In dimension K, the tile consists of 16 scalars, requiring `16 / vec_size` vector loads. ss << "\n"; -// 85 | const TILE_K_VEC_SIZE : u32 = 16 / vec_size; +// 89 | const TILE_K_VEC_SIZE : u32 = 16 / vec_size; ss << "const TILE_K_VEC_SIZE : u32 = 16 / "; ss << __param_vec_size; ss << ";\n"; -// 86 | // In dimensions M and N, since a workgroup has 64 threads, it advances by `64 / TILE_K_VEC_SIZE`. +// 90 | // In dimensions M and N, since a workgroup has 64 threads, it advances by `64 / TILE_K_VEC_SIZE`. ss << "\n"; -// 87 | const ADVANCE_DIM = 64 / TILE_K_VEC_SIZE; +// 91 | const ADVANCE_DIM = 64 / TILE_K_VEC_SIZE; ss << "const ADVANCE_DIM = 64 / TILE_K_VEC_SIZE;\n"; -// 88 | +// 92 | ss << "\n"; -// 89 | var src_tile : array, TILE_K_VEC_SIZE>; +// 93 | var src_tile : array, TILE_K_VEC_SIZE>; ss << "var src_tile : array, TILE_K_VEC_SIZE>;\n"; -// 90 | var weight_tile : array, TILE_K_VEC_SIZE>; +// 94 | var weight_tile : array, TILE_K_VEC_SIZE>; ss << "var weight_tile : array, TILE_K_VEC_SIZE>;\n"; -// 91 | +// 95 | ss << "\n"; -// 92 | $MAIN { +// 96 | $MAIN { MainFunctionStart(); ss << "\n"; -// 93 | let batch = workgroup_idx / (uniforms.M_tiles * uniforms.N_tiles); +// 97 | let batch = workgroup_idx / (uniforms.M_tiles * uniforms.N_tiles); ss << " let batch = workgroup_idx / (uniforms.M_tiles * uniforms.N_tiles);\n"; -// 94 | let m_global_base = ((workgroup_idx / uniforms.N_tiles) % uniforms.M_tiles) * TILE_M_SIZE; +// 98 | let m_global_base = ((workgroup_idx / uniforms.N_tiles) % uniforms.M_tiles) * TILE_M_SIZE; ss << " let m_global_base = ((workgroup_idx / uniforms.N_tiles) % uniforms.M_tiles) * TILE_M_SIZE;\n"; -// 95 | let n_global_base = (workgroup_idx % uniforms.N_tiles) * TILE_N_SIZE; +// 99 | let n_global_base = (workgroup_idx % uniforms.N_tiles) * TILE_N_SIZE; ss << " let n_global_base = (workgroup_idx % uniforms.N_tiles) * TILE_N_SIZE;\n"; -// 96 | +// 100 | ss << "\n"; -// 97 | var results : array; +// 101 | var results : array; ss << " var results : array;\n"; -// 98 | for (var k_idx = 0u; k_idx < uniforms.K_tiles; k_idx++) { +// 102 | for (var k_idx = 0u; k_idx < uniforms.K_tiles; k_idx++) { ss << " for (var k_idx = 0u; k_idx < uniforms.K_tiles; k_idx++) {\n"; -// 99 | for (var src_m = 0u; src_m < TILE_M_SIZE; src_m += ADVANCE_DIM) { +// 103 | for (var src_m = 0u; src_m < TILE_M_SIZE; src_m += ADVANCE_DIM) { ss << " for (var src_m = 0u; src_m < TILE_M_SIZE; src_m += ADVANCE_DIM) {\n"; -// 100 | // Loads a 64 vec of src into the workgroup memory. +// 104 | // Loads a 64 vec of src into the workgroup memory. ss << "\n"; -// 101 | let load_src_m = src_m + local_idx / TILE_K_VEC_SIZE; +// 105 | let load_src_m = src_m + local_idx / TILE_K_VEC_SIZE; ss << " let load_src_m = src_m + local_idx / TILE_K_VEC_SIZE;\n"; -// 102 | let load_src_k = local_idx % TILE_K_VEC_SIZE; +// 106 | let load_src_k = local_idx % TILE_K_VEC_SIZE; ss << " let load_src_k = local_idx % TILE_K_VEC_SIZE;\n"; -// 103 | +// 107 | ss << "\n"; -// 104 | src_tile[load_src_k][load_src_m] = load_src(batch, +// 108 | src_tile[load_src_k][load_src_m] = load_src(batch, ss << " src_tile[load_src_k][load_src_m] = load_src(batch,\n"; -// 105 | m_global_base + load_src_m, +// 109 | m_global_base + load_src_m, ss << " m_global_base + load_src_m,\n"; -// 106 | k_idx * TILE_K_VEC_SIZE + load_src_k); +// 110 | k_idx * TILE_K_VEC_SIZE + load_src_k); ss << " k_idx * TILE_K_VEC_SIZE + load_src_k);\n"; -// 107 | } +// 111 | } ss << " }\n"; -// 108 | +// 112 | ss << "\n"; -// 109 | for (var weight_n = 0u; weight_n < TILE_N_SIZE; weight_n += ADVANCE_DIM) { +// 113 | for (var weight_n = 0u; weight_n < TILE_N_SIZE; weight_n += ADVANCE_DIM) { ss << " for (var weight_n = 0u; weight_n < TILE_N_SIZE; weight_n += ADVANCE_DIM) {\n"; -// 110 | // Loads a 64 vec of weight into the workgroup memory. +// 114 | // Loads a 64 vec of weight into the workgroup memory. ss << "\n"; -// 111 | let load_weight_n = weight_n + local_idx / TILE_K_VEC_SIZE; +// 115 | let load_weight_n = weight_n + local_idx / TILE_K_VEC_SIZE; ss << " let load_weight_n = weight_n + local_idx / TILE_K_VEC_SIZE;\n"; -// 112 | let load_weight_k = local_idx % TILE_K_VEC_SIZE; +// 116 | let load_weight_k = local_idx % TILE_K_VEC_SIZE; ss << " let load_weight_k = local_idx % TILE_K_VEC_SIZE;\n"; -// 113 | +// 117 | ss << "\n"; -// 114 | weight_tile[load_weight_k][load_weight_n] = load_weight(n_global_base + load_weight_n, +// 118 | weight_tile[load_weight_k][load_weight_n] = load_weight(n_global_base + load_weight_n, ss << " weight_tile[load_weight_k][load_weight_n] = load_weight(n_global_base + load_weight_n,\n"; -// 115 | k_idx * TILE_K_VEC_SIZE + load_weight_k); +// 119 | k_idx * TILE_K_VEC_SIZE + load_weight_k); ss << " k_idx * TILE_K_VEC_SIZE + load_weight_k);\n"; -// 116 | } +// 120 | } ss << " }\n"; -// 117 | workgroupBarrier(); +// 121 | workgroupBarrier(); ss << " workgroupBarrier();\n"; -// 118 | +// 122 | ss << "\n"; -// 119 | for (var inner_k_idx = 0u; inner_k_idx < TILE_K_VEC_SIZE; inner_k_idx++) { +// 123 | for (var inner_k_idx = 0u; inner_k_idx < TILE_K_VEC_SIZE; inner_k_idx++) { ss << " for (var inner_k_idx = 0u; inner_k_idx < TILE_K_VEC_SIZE; inner_k_idx++) {\n"; -// 120 | let weight_data = weight_tile[inner_k_idx][local_idx]; +// 124 | let weight_data = weight_tile[inner_k_idx][local_idx]; ss << " let weight_data = weight_tile[inner_k_idx][local_idx];\n"; -// 121 | #if use_subgroup +// 125 | #if use_subgroup if (__param_use_subgroup) { -// 122 | let src_data = src_tile[inner_k_idx][sg_id]; +// 126 | let src_data = src_tile[inner_k_idx][sg_id]; ss << " let src_data = src_tile[inner_k_idx][sg_id];\n"; -// 123 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { +// 127 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { ss << " for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) {\n"; -// 124 | results[m_idx] += output_element_t(dot(weight_data, subgroupShuffle(src_data, m_idx))); +// 128 | results[m_idx] += output_element_t(dot(weight_data, subgroupShuffle(src_data, m_idx))); ss << " results[m_idx] += output_element_t(dot(weight_data, subgroupShuffle(src_data, m_idx)));\n"; -// 125 | } +// 129 | } ss << " }\n"; -// 126 | #else +// 130 | #else } else { -// 127 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { +// 131 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { ss << " for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) {\n"; -// 128 | #if vec_size == 1 +// 132 | #if vec_size == 1 if (__param_vec_size == 1) { -// 129 | results[m_idx] += output_element_t(weight_data * src_tile[inner_k_idx][m_idx]); +// 133 | results[m_idx] += output_element_t(weight_data * src_tile[inner_k_idx][m_idx]); ss << " results[m_idx] += output_element_t(weight_data * src_tile[inner_k_idx][m_idx]);\n"; -// 130 | #else +// 134 | #else } else { -// 131 | results[m_idx] += output_element_t(dot(weight_data, src_tile[inner_k_idx][m_idx])); +// 135 | results[m_idx] += output_element_t(dot(weight_data, src_tile[inner_k_idx][m_idx])); ss << " results[m_idx] += output_element_t(dot(weight_data, src_tile[inner_k_idx][m_idx]));\n"; -// 132 | #endif +// 136 | #endif } -// 133 | } +// 137 | } ss << " }\n"; -// 134 | #endif +// 138 | #endif } -// 135 | } +// 139 | } ss << " }\n"; -// 136 | workgroupBarrier(); +// 140 | workgroupBarrier(); ss << " workgroupBarrier();\n"; -// 137 | } +// 141 | } ss << " }\n"; -// 138 | +// 142 | ss << "\n"; -// 139 | let m_base = m_global_base; +// 143 | let m_base = m_global_base; ss << " let m_base = m_global_base;\n"; -// 140 | let n_base = n_global_base + local_idx; +// 144 | let n_base = n_global_base + local_idx; ss << " let n_base = n_global_base + local_idx;\n"; -// 141 | +// 145 | ss << "\n"; -// 142 | let bias = load_bias(n_base); +// 146 | let bias = load_bias(n_base); ss << " let bias = load_bias(n_base);\n"; -// 143 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { +// 147 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { ss << " for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) {\n"; -// 144 | var output_data = results[m_idx] + bias; +// 148 | var output_data = results[m_idx] + bias; ss << " var output_data = results[m_idx] + bias;\n"; -// 145 | write_output(batch, m_base + m_idx, n_base, output_data); +// 149 | #if activation_kind == 1 +if (__param_activation_kind == 1) { +// 150 | output_data = max(output_data, output_element_t(0)); +ss << " output_data = max(output_data, output_element_t(0));\n"; +// 151 | #elif activation_kind == 2 +} else if (__param_activation_kind == 2) { +// 152 | output_data = output_element_t(1) / (output_element_t(1) + exp(-output_data)); +ss << " output_data = output_element_t(1) / (output_element_t(1) + exp(-output_data));\n"; +// 153 | #elif activation_kind == 3 +} else if (__param_activation_kind == 3) { +// 154 | output_data = clamp(output_data, output_element_t(uniforms.activation_param_0), output_element_t(uniforms.activation_param_1)); +ss << " output_data = clamp(output_data, output_element_t(uniforms.activation_param_0), output_element_t(uniforms.activation_param_1));\n"; +// 155 | #elif activation_kind == 4 +} else if (__param_activation_kind == 4) { +// 156 | output_data = clamp(output_element_t(uniforms.activation_param_0) * output_data + output_element_t(uniforms.activation_param_1), output_element_t(0), output_element_t(1)); +ss << " output_data = clamp(output_element_t(uniforms.activation_param_0) * output_data + output_element_t(uniforms.activation_param_1), output_element_t(0), output_element_t(1));\n"; +// 157 | #elif activation_kind == 5 +} else if (__param_activation_kind == 5) { +// 158 | output_data = select(output_element_t(uniforms.activation_param_0) * output_data, output_data, output_data >= output_element_t(0)); +ss << " output_data = select(output_element_t(uniforms.activation_param_0) * output_data, output_data, output_data >= output_element_t(0));\n"; +// 159 | #elif activation_kind == 6 +} else if (__param_activation_kind == 6) { +// 160 | output_data = tanh(output_data); +ss << " output_data = tanh(output_data);\n"; +// 161 | #endif +} +// 162 | write_output(batch, m_base + m_idx, n_base, output_data); ss << " write_output(batch, m_base + m_idx, n_base, output_data);\n"; -// 146 | } +// 163 | } ss << " }\n"; -// 147 | } // MAIN +// 164 | } // MAIN MainFunctionEnd(); ss << "\n"; -// 148 | +// 165 | return Status::OK(); diff --git a/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/index.h b/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/index.h index bcd58dfb1f3b7..e481534e62d5d 100644 --- a/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/index.h +++ b/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/index.h @@ -70,6 +70,7 @@ Status ApplyTemplate<"math/subgroup_matrix_matmul_pad_b.wgsl.template">(ShaderHe template <> struct TemplateParameter<"nn/im2col_matmul.wgsl.template"> { using type = struct { + int param_activation_kind; int param_has_bias; int param_tile_m; int param_tile_n; diff --git a/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/index_impl.h b/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/index_impl.h index 9af04b9036f81..6a543f0dfaa10 100644 --- a/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/index_impl.h +++ b/tools/python/wgsl_template/test/in_tree_golden/static-cpp-literal/index_impl.h @@ -37,7 +37,7 @@ std::string pass_as_string(T&& v) { #include "wgsl_template_gen/generated/math/subgroup_matrix_gemm_8x16x16.h" // d068d46b84a7c8561439328fe3b6b45dd6fe0c4a7db05dc2072c761794d2bd9b #include "wgsl_template_gen/generated/math/subgroup_matrix_matmul_8x16x16.h" // a4a6be72122e57aa68971f574f292db28f589fdf1a845e0dd67429c008a8d678 #include "wgsl_template_gen/generated/math/subgroup_matrix_matmul_pad_b.h" // 8f9b5bcb94ae91edc78567b06c4bd77584898deab7783a84d400d0d8f139dc73 -#include "wgsl_template_gen/generated/nn/im2col_matmul.h" // d8d68023c1442e7366ed4a28cb4dd402ba4a6da8629fa4b926f4042b7ba6d70c +#include "wgsl_template_gen/generated/nn/im2col_matmul.h" // 67949855e1b8b8c3f21aae3b5cd466612759a5d96b1a154e5edff75ee8f5fab4 #include "wgsl_template_gen/generated/tensor/oihw_to_ohwi.h" // 35487692058e08b027768dcb1ab30ef93772da9a4a664702c34351a74d2568dc #include "wgsl_template_gen/generated/tensor/pad.h" // 43a2d8f5014de2ce571c703d842f2457d7c1762575254bc42ccee437a181cd3f diff --git a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/nn/im2col_matmul.h b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/nn/im2col_matmul.h index adb8d914a6c00..049004235d90d 100644 --- a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/nn/im2col_matmul.h +++ b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/nn/im2col_matmul.h @@ -10,6 +10,7 @@ Status ApplyTemplate<"nn/im2col_matmul.wgsl.template">(ShaderHelper& shader_help [[maybe_unused]] auto& ss = shader_helper.AdditionalImplementation(); // Extract parameters + auto& __param_activation_kind = params.param_activation_kind; auto& __param_has_bias = params.param_has_bias; auto& __param_tile_m = params.param_tile_m; auto& __param_tile_n = params.param_tile_n; @@ -29,294 +30,324 @@ Status ApplyTemplate<"nn/im2col_matmul.wgsl.template">(ShaderHelper& shader_help // 6 | #param tile_n // 7 | #param use_subgroup // 8 | #param vec_size -// 9 | -// 10 | #use .getByOffset .setByOffset -// 11 | -// 12 | // im2col access for src: [N, H_i, W_i, C_i / vec_size] -// 13 | // Conceptual Matrix Shape: N * (H_o * W_o) x (K_h * K_w * C_i / vec_size) -// 14 | fn load_src(batch : u32, m : u32, k_packed_idx : u32) -> src_value_t { +// 9 | // Mirrors ActivationKind; static_asserts in im2col_matmul.cc enforce these values. +// 10 | // 0=None, 1=Relu, 2=Sigmoid, 3=Clip, 4=HardSigmoid, 5=LeakyRelu, 6=Tanh. +// 11 | // Keep branches synchronized with IsActivationSupported(). +// 12 | #param activation_kind +// 13 | +// 14 | #use .getByOffset .setByOffset +// 15 | +// 16 | // im2col access for src: [N, H_i, W_i, C_i / vec_size] +// 17 | // Conceptual Matrix Shape: N * (H_o * W_o) x (K_h * K_w * C_i / vec_size) +// 18 | fn load_src(batch : u32, m : u32, k_packed_idx : u32) -> src_value_t { ss << __str_223; -// 15 | if (batch >= uniforms.batch || m >= uniforms.im2col_m || k_packed_idx * vec_size >= uniforms.im2col_k) { +// 19 | if (batch >= uniforms.batch || m >= uniforms.im2col_m || k_packed_idx * vec_size >= uniforms.im2col_k) { ss << __str_224; ss << __param_vec_size; ss << __str_225; -// 16 | return src_value_t(); +// 20 | return src_value_t(); ss << __str_226; -// 17 | } +// 21 | } ss << __str_218; -// 18 | +// 22 | ss << __str_12; -// 19 | let channel_i_vec = uniforms.channel_i / vec_size; +// 23 | let channel_i_vec = uniforms.channel_i / vec_size; ss << __str_227; ss << __param_vec_size; ss << __str_189; -// 20 | +// 24 | ss << __str_12; -// 21 | // 1. Decompose M index (H_o * W_o) into (h_idx, w_idx) -// 22 | let h_idx = m / uniforms.output_w; // Output H index (H_o) +// 25 | // 1. Decompose M index (H_o * W_o) into (h_idx, w_idx) +// 26 | let h_idx = m / uniforms.output_w; // Output H index (H_o) ss << __str_228; -// 23 | let w_idx = m % uniforms.output_w; // Output W index (W_o) +// 27 | let w_idx = m % uniforms.output_w; // Output W index (W_o) ss << __str_229; -// 24 | +// 28 | ss << __str_12; -// 25 | // 2. Decompose K index into (k_h, k_w, c_i_vec_idx) -// 26 | let c_i_vec_idx = k_packed_idx % channel_i_vec; +// 29 | // 2. Decompose K index into (k_h, k_w, c_i_vec_idx) +// 30 | let c_i_vec_idx = k_packed_idx % channel_i_vec; ss << __str_230; -// 27 | let k_h_w_idx = k_packed_idx / channel_i_vec; +// 31 | let k_h_w_idx = k_packed_idx / channel_i_vec; ss << __str_231; -// 28 | let k_h = k_h_w_idx / uniforms.kernel_w; // Kernel Row +// 32 | let k_h = k_h_w_idx / uniforms.kernel_w; // Kernel Row ss << __str_232; -// 29 | let k_w = k_h_w_idx % uniforms.kernel_w; // Kernel Column +// 33 | let k_w = k_h_w_idx % uniforms.kernel_w; // Kernel Column ss << __str_233; -// 30 | +// 34 | ss << __str_12; -// 31 | // 3. Calculate the coordinate in the padded input tensor -// 32 | let src_h_coord_padded = h_idx * uniforms.strides.x + k_h * uniforms.dilations.x; +// 35 | // 3. Calculate the coordinate in the padded input tensor +// 36 | let src_h_coord_padded = h_idx * uniforms.strides.x + k_h * uniforms.dilations.x; ss << __str_234; -// 33 | let src_w_coord_padded = w_idx * uniforms.strides.y + k_w * uniforms.dilations.y; +// 37 | let src_w_coord_padded = w_idx * uniforms.strides.y + k_w * uniforms.dilations.y; ss << __str_235; -// 34 | +// 38 | ss << __str_12; -// 35 | // 4. Calculate the coordinate in the original input tensor -// 36 | let src_h_coord : i32 = i32(src_h_coord_padded) - i32(uniforms.pads.x); +// 39 | // 4. Calculate the coordinate in the original input tensor +// 40 | let src_h_coord : i32 = i32(src_h_coord_padded) - i32(uniforms.pads.x); ss << __str_236; -// 37 | let src_w_coord : i32 = i32(src_w_coord_padded) - i32(uniforms.pads.y); +// 41 | let src_w_coord : i32 = i32(src_w_coord_padded) - i32(uniforms.pads.y); ss << __str_237; -// 38 | +// 42 | ss << __str_12; -// 39 | // 5. Check for padding/out-of-bounds -// 40 | if (src_h_coord < 0 || src_h_coord >= i32(uniforms.src_h) || +// 43 | // 5. Check for padding/out-of-bounds +// 44 | if (src_h_coord < 0 || src_h_coord >= i32(uniforms.src_h) || ss << __str_238; -// 41 | src_w_coord < 0 || src_w_coord >= i32(uniforms.src_w)) { +// 45 | src_w_coord < 0 || src_w_coord >= i32(uniforms.src_w)) { ss << __str_239; -// 42 | return src_value_t(); +// 46 | return src_value_t(); ss << __str_226; -// 43 | } +// 47 | } ss << __str_218; -// 44 | +// 48 | ss << __str_12; -// 45 | // 6. Calculate final NHWC index -// 46 | let src_idx = batch * uniforms.src_h * uniforms.src_w * channel_i_vec + +// 49 | // 6. Calculate final NHWC index +// 50 | let src_idx = batch * uniforms.src_h * uniforms.src_w * channel_i_vec + ss << __str_240; -// 47 | u32(src_h_coord) * uniforms.src_w * channel_i_vec + +// 51 | u32(src_h_coord) * uniforms.src_w * channel_i_vec + ss << __str_241; -// 48 | u32(src_w_coord) * channel_i_vec + +// 52 | u32(src_w_coord) * channel_i_vec + ss << __str_242; -// 49 | c_i_vec_idx; +// 53 | c_i_vec_idx; ss << __str_243; -// 50 | return src.getByOffset(src_idx); +// 54 | return src.getByOffset(src_idx); ss << __str_244; ss << __var_src.GetByOffset(__str_219); ss << __str_189; -// 51 | } +// 55 | } ss << __str_245; -// 52 | +// 56 | ss << __str_12; -// 53 | // weight shape: [Co, K_h, K_w, C_i / vec_size] (CoHWCi) -// 54 | fn load_weight(n : u32, k_packed_idx : u32) -> weight_value_t { +// 57 | // weight shape: [Co, K_h, K_w, C_i / vec_size] (CoHWCi) +// 58 | fn load_weight(n : u32, k_packed_idx : u32) -> weight_value_t { ss << __str_246; -// 55 | if (n < uniforms.im2col_n && k_packed_idx < uniforms.im2col_k / vec_size) { +// 59 | if (n < uniforms.im2col_n && k_packed_idx < uniforms.im2col_k / vec_size) { ss << __str_247; ss << __param_vec_size; ss << __str_248; -// 56 | let weight_idx = n * uniforms.im2col_k / vec_size + +// 60 | let weight_idx = n * uniforms.im2col_k / vec_size + ss << __str_249; ss << __param_vec_size; ss << __str_250; -// 57 | k_packed_idx; +// 61 | k_packed_idx; ss << __str_251; -// 58 | return weight.getByOffset(weight_idx); +// 62 | return weight.getByOffset(weight_idx); ss << __str_252; ss << __var_weight.GetByOffset(__str_220); ss << __str_189; -// 59 | } +// 63 | } ss << __str_218; -// 60 | return weight_value_t(); +// 64 | return weight_value_t(); ss << __str_253; -// 61 | } +// 65 | } ss << __str_245; -// 62 | +// 66 | ss << __str_12; -// 63 | fn load_bias(n : u32) -> output_element_t { +// 67 | fn load_bias(n : u32) -> output_element_t { ss << __str_254; -// 64 | #if has_bias +// 68 | #if has_bias if (__param_has_bias) { -// 65 | if (n < uniforms.im2col_n) { +// 69 | if (n < uniforms.im2col_n) { ss << __str_255; -// 66 | return output_element_t(bias[n]); +// 70 | return output_element_t(bias[n]); ss << __str_256; -// 67 | } +// 71 | } ss << __str_218; -// 68 | #endif +// 72 | #endif } -// 69 | return output_element_t(); +// 73 | return output_element_t(); ss << __str_257; -// 70 | } +// 74 | } ss << __str_245; -// 71 | +// 75 | ss << __str_12; -// 72 | // output shape: [N, H_o, W_o, C_o] (NHWC) -// 73 | fn write_output(batch : u32, m : u32, n : u32, value : output_element_t) { +// 76 | // output shape: [N, H_o, W_o, C_o] (NHWC) +// 77 | fn write_output(batch : u32, m : u32, n : u32, value : output_element_t) { ss << __str_258; -// 74 | if (batch < uniforms.batch && m < uniforms.im2col_m && n < uniforms.im2col_n) { +// 78 | if (batch < uniforms.batch && m < uniforms.im2col_m && n < uniforms.im2col_n) { ss << __str_259; -// 75 | let output_idx = batch * uniforms.im2col_m * uniforms.im2col_n + +// 79 | let output_idx = batch * uniforms.im2col_m * uniforms.im2col_n + ss << __str_260; -// 76 | m * uniforms.im2col_n + +// 80 | m * uniforms.im2col_n + ss << __str_261; -// 77 | n; +// 81 | n; ss << __str_262; -// 78 | output.setByOffset(output_idx, value); +// 82 | output.setByOffset(output_idx, value); ss << __str_263; ss << __var_output.SetByOffset(__str_221, __str_222); ss << __str_189; -// 79 | } +// 83 | } ss << __str_218; -// 80 | } +// 84 | } ss << __str_245; -// 81 | +// 85 | ss << __str_12; -// 82 | const TILE_M_SIZE : u32 = tile_m; +// 86 | const TILE_M_SIZE : u32 = tile_m; ss << __str_264; ss << __param_tile_m; ss << __str_189; -// 83 | const TILE_N_SIZE : u32 = tile_n; +// 87 | const TILE_N_SIZE : u32 = tile_n; ss << __str_265; ss << __param_tile_n; ss << __str_189; -// 84 | // In dimension K, the tile consists of 16 scalars, requiring `16 / vec_size` vector loads. +// 88 | // In dimension K, the tile consists of 16 scalars, requiring `16 / vec_size` vector loads. ss << __str_12; -// 85 | const TILE_K_VEC_SIZE : u32 = 16 / vec_size; +// 89 | const TILE_K_VEC_SIZE : u32 = 16 / vec_size; ss << __str_266; ss << __param_vec_size; ss << __str_189; -// 86 | // In dimensions M and N, since a workgroup has 64 threads, it advances by `64 / TILE_K_VEC_SIZE`. +// 90 | // In dimensions M and N, since a workgroup has 64 threads, it advances by `64 / TILE_K_VEC_SIZE`. ss << __str_12; -// 87 | const ADVANCE_DIM = 64 / TILE_K_VEC_SIZE; +// 91 | const ADVANCE_DIM = 64 / TILE_K_VEC_SIZE; ss << __str_267; -// 88 | +// 92 | ss << __str_12; -// 89 | var src_tile : array, TILE_K_VEC_SIZE>; +// 93 | var src_tile : array, TILE_K_VEC_SIZE>; ss << __str_268; -// 90 | var weight_tile : array, TILE_K_VEC_SIZE>; +// 94 | var weight_tile : array, TILE_K_VEC_SIZE>; ss << __str_269; -// 91 | +// 95 | ss << __str_12; -// 92 | $MAIN { +// 96 | $MAIN { MainFunctionStart(); ss << __str_12; -// 93 | let batch = workgroup_idx / (uniforms.M_tiles * uniforms.N_tiles); +// 97 | let batch = workgroup_idx / (uniforms.M_tiles * uniforms.N_tiles); ss << __str_270; -// 94 | let m_global_base = ((workgroup_idx / uniforms.N_tiles) % uniforms.M_tiles) * TILE_M_SIZE; +// 98 | let m_global_base = ((workgroup_idx / uniforms.N_tiles) % uniforms.M_tiles) * TILE_M_SIZE; ss << __str_271; -// 95 | let n_global_base = (workgroup_idx % uniforms.N_tiles) * TILE_N_SIZE; +// 99 | let n_global_base = (workgroup_idx % uniforms.N_tiles) * TILE_N_SIZE; ss << __str_272; -// 96 | +// 100 | ss << __str_12; -// 97 | var results : array; +// 101 | var results : array; ss << __str_273; -// 98 | for (var k_idx = 0u; k_idx < uniforms.K_tiles; k_idx++) { +// 102 | for (var k_idx = 0u; k_idx < uniforms.K_tiles; k_idx++) { ss << __str_274; -// 99 | for (var src_m = 0u; src_m < TILE_M_SIZE; src_m += ADVANCE_DIM) { +// 103 | for (var src_m = 0u; src_m < TILE_M_SIZE; src_m += ADVANCE_DIM) { ss << __str_275; -// 100 | // Loads a 64 vec of src into the workgroup memory. +// 104 | // Loads a 64 vec of src into the workgroup memory. ss << __str_12; -// 101 | let load_src_m = src_m + local_idx / TILE_K_VEC_SIZE; +// 105 | let load_src_m = src_m + local_idx / TILE_K_VEC_SIZE; ss << __str_276; -// 102 | let load_src_k = local_idx % TILE_K_VEC_SIZE; +// 106 | let load_src_k = local_idx % TILE_K_VEC_SIZE; ss << __str_277; -// 103 | +// 107 | ss << __str_12; -// 104 | src_tile[load_src_k][load_src_m] = load_src(batch, +// 108 | src_tile[load_src_k][load_src_m] = load_src(batch, ss << __str_278; -// 105 | m_global_base + load_src_m, +// 109 | m_global_base + load_src_m, ss << __str_279; -// 106 | k_idx * TILE_K_VEC_SIZE + load_src_k); +// 110 | k_idx * TILE_K_VEC_SIZE + load_src_k); ss << __str_280; -// 107 | } +// 111 | } ss << __str_135; -// 108 | +// 112 | ss << __str_12; -// 109 | for (var weight_n = 0u; weight_n < TILE_N_SIZE; weight_n += ADVANCE_DIM) { +// 113 | for (var weight_n = 0u; weight_n < TILE_N_SIZE; weight_n += ADVANCE_DIM) { ss << __str_281; -// 110 | // Loads a 64 vec of weight into the workgroup memory. +// 114 | // Loads a 64 vec of weight into the workgroup memory. ss << __str_12; -// 111 | let load_weight_n = weight_n + local_idx / TILE_K_VEC_SIZE; +// 115 | let load_weight_n = weight_n + local_idx / TILE_K_VEC_SIZE; ss << __str_282; -// 112 | let load_weight_k = local_idx % TILE_K_VEC_SIZE; +// 116 | let load_weight_k = local_idx % TILE_K_VEC_SIZE; ss << __str_283; -// 113 | +// 117 | ss << __str_12; -// 114 | weight_tile[load_weight_k][load_weight_n] = load_weight(n_global_base + load_weight_n, +// 118 | weight_tile[load_weight_k][load_weight_n] = load_weight(n_global_base + load_weight_n, ss << __str_284; -// 115 | k_idx * TILE_K_VEC_SIZE + load_weight_k); +// 119 | k_idx * TILE_K_VEC_SIZE + load_weight_k); ss << __str_285; -// 116 | } +// 120 | } ss << __str_135; -// 117 | workgroupBarrier(); +// 121 | workgroupBarrier(); ss << __str_168; -// 118 | +// 122 | ss << __str_12; -// 119 | for (var inner_k_idx = 0u; inner_k_idx < TILE_K_VEC_SIZE; inner_k_idx++) { +// 123 | for (var inner_k_idx = 0u; inner_k_idx < TILE_K_VEC_SIZE; inner_k_idx++) { ss << __str_286; -// 120 | let weight_data = weight_tile[inner_k_idx][local_idx]; +// 124 | let weight_data = weight_tile[inner_k_idx][local_idx]; ss << __str_287; -// 121 | #if use_subgroup +// 125 | #if use_subgroup if (__param_use_subgroup) { -// 122 | let src_data = src_tile[inner_k_idx][sg_id]; +// 126 | let src_data = src_tile[inner_k_idx][sg_id]; ss << __str_288; -// 123 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { +// 127 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { ss << __str_289; -// 124 | results[m_idx] += output_element_t(dot(weight_data, subgroupShuffle(src_data, m_idx))); +// 128 | results[m_idx] += output_element_t(dot(weight_data, subgroupShuffle(src_data, m_idx))); ss << __str_290; -// 125 | } +// 129 | } ss << __str_291; -// 126 | #else +// 130 | #else } else { -// 127 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { +// 131 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { ss << __str_289; -// 128 | #if vec_size == 1 +// 132 | #if vec_size == 1 if (__param_vec_size == 1) { -// 129 | results[m_idx] += output_element_t(weight_data * src_tile[inner_k_idx][m_idx]); +// 133 | results[m_idx] += output_element_t(weight_data * src_tile[inner_k_idx][m_idx]); ss << __str_292; -// 130 | #else +// 134 | #else } else { -// 131 | results[m_idx] += output_element_t(dot(weight_data, src_tile[inner_k_idx][m_idx])); +// 135 | results[m_idx] += output_element_t(dot(weight_data, src_tile[inner_k_idx][m_idx])); ss << __str_293; -// 132 | #endif +// 136 | #endif } -// 133 | } +// 137 | } ss << __str_291; -// 134 | #endif +// 138 | #endif } -// 135 | } +// 139 | } ss << __str_135; -// 136 | workgroupBarrier(); +// 140 | workgroupBarrier(); ss << __str_168; -// 137 | } +// 141 | } ss << __str_218; -// 138 | +// 142 | ss << __str_12; -// 139 | let m_base = m_global_base; +// 143 | let m_base = m_global_base; ss << __str_294; -// 140 | let n_base = n_global_base + local_idx; +// 144 | let n_base = n_global_base + local_idx; ss << __str_295; -// 141 | +// 145 | ss << __str_12; -// 142 | let bias = load_bias(n_base); +// 146 | let bias = load_bias(n_base); ss << __str_296; -// 143 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { +// 147 | for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) { ss << __str_297; -// 144 | var output_data = results[m_idx] + bias; +// 148 | var output_data = results[m_idx] + bias; ss << __str_298; -// 145 | write_output(batch, m_base + m_idx, n_base, output_data); +// 149 | #if activation_kind == 1 +if (__param_activation_kind == 1) { +// 150 | output_data = max(output_data, output_element_t(0)); ss << __str_299; -// 146 | } +// 151 | #elif activation_kind == 2 +} else if (__param_activation_kind == 2) { +// 152 | output_data = output_element_t(1) / (output_element_t(1) + exp(-output_data)); +ss << __str_300; +// 153 | #elif activation_kind == 3 +} else if (__param_activation_kind == 3) { +// 154 | output_data = clamp(output_data, output_element_t(uniforms.activation_param_0), output_element_t(uniforms.activation_param_1)); +ss << __str_301; +// 155 | #elif activation_kind == 4 +} else if (__param_activation_kind == 4) { +// 156 | output_data = clamp(output_element_t(uniforms.activation_param_0) * output_data + output_element_t(uniforms.activation_param_1), output_element_t(0), output_element_t(1)); +ss << __str_302; +// 157 | #elif activation_kind == 5 +} else if (__param_activation_kind == 5) { +// 158 | output_data = select(output_element_t(uniforms.activation_param_0) * output_data, output_data, output_data >= output_element_t(0)); +ss << __str_303; +// 159 | #elif activation_kind == 6 +} else if (__param_activation_kind == 6) { +// 160 | output_data = tanh(output_data); +ss << __str_304; +// 161 | #endif +} +// 162 | write_output(batch, m_base + m_idx, n_base, output_data); +ss << __str_305; +// 163 | } ss << __str_218; -// 147 | } // MAIN +// 164 | } // MAIN MainFunctionEnd(); ss << __str_12; -// 148 | +// 165 | return Status::OK(); diff --git a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/tensor/oihw_to_ohwi.h b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/tensor/oihw_to_ohwi.h index 6e9dcf25b6297..9e9483c0948e9 100644 --- a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/tensor/oihw_to_ohwi.h +++ b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/tensor/oihw_to_ohwi.h @@ -19,40 +19,40 @@ Status ApplyTemplate<"tensor/oihw_to_ohwi.wgsl.template">(ShaderHelper& shader_h // 4 | #use .getByOffset .setByOffset // 5 | // 6 | fn load_src(co : u32, ci : u32, h_w : u32) -> src_element_t { -ss << __str_301; +ss << __str_307; // 7 | if (co < uniforms.O && ci < uniforms.I && h_w < uniforms.H * uniforms.W) { -ss << __str_302; +ss << __str_308; // 8 | let offset = co * uniforms.I * uniforms.H * uniforms.W + -ss << __str_303; +ss << __str_309; // 9 | ci * uniforms.H * uniforms.W + -ss << __str_304; +ss << __str_310; // 10 | h_w; -ss << __str_305; +ss << __str_311; // 11 | return src.getByOffset(offset); ss << __str_252; -ss << __var_src.GetByOffset(__str_300); +ss << __var_src.GetByOffset(__str_306); ss << __str_189; // 12 | } ss << __str_218; // 13 | return src_element_t(); -ss << __str_306; +ss << __str_312; // 14 | } ss << __str_245; // 15 | ss << __str_12; // 16 | fn write_output(co : u32, h_w : u32, ci : u32, value : output_element_t) { -ss << __str_307; +ss << __str_313; // 17 | if (co < uniforms.O && ci < uniforms.I && h_w < uniforms.H * uniforms.W) { -ss << __str_302; -// 18 | let offset = co * uniforms.H * uniforms.W * uniforms.I + ss << __str_308; +// 18 | let offset = co * uniforms.H * uniforms.W * uniforms.I + +ss << __str_314; // 19 | h_w * uniforms.I + -ss << __str_309; +ss << __str_315; // 20 | ci; -ss << __str_310; +ss << __str_316; // 21 | output.setByOffset(offset, value); ss << __str_263; -ss << __var_output.SetByOffset(__str_300, __str_222); +ss << __var_output.SetByOffset(__str_306, __str_222); ss << __str_189; // 22 | } ss << __str_218; @@ -61,44 +61,44 @@ ss << __str_245; // 24 | ss << __str_12; // 25 | var data_cache : array, 4>; -ss << __str_311; +ss << __str_317; // 26 | ss << __str_12; // 27 | $MAIN { MainFunctionStart(); ss << __str_12; // 28 | let group_co : u32 = workgroup_idx / uniforms.Ci_tiles; -ss << __str_312; +ss << __str_318; // 29 | let group_ci : u32 = (workgroup_idx % uniforms.Ci_tiles) * 64; -ss << __str_313; +ss << __str_319; // 30 | ss << __str_12; // 31 | if (group_co >= uniforms.O || group_ci >= uniforms.I) { -ss << __str_314; +ss << __str_320; // 32 | return; -ss << __str_315; +ss << __str_321; // 33 | } ss << __str_218; // 34 | ss << __str_12; // 35 | for (var h_w_idx = 0u; h_w_idx < uniforms.H_W_tiles; h_w_idx++) { -ss << __str_316; +ss << __str_322; // 36 | // load ss << __str_12; // 37 | for (var ci_idx = 0u; ci_idx < 64u; ci_idx += 16u) { -ss << __str_317; +ss << __str_323; // 38 | let load_ci_idx = ci_idx + local_idx / 4; -ss << __str_318; +ss << __str_324; // 39 | let load_h_w_idx = local_idx % 4; -ss << __str_319; +ss << __str_325; // 40 | ss << __str_12; // 41 | data_cache[load_h_w_idx][load_ci_idx] = load_src(group_co, -ss << __str_320; +ss << __str_326; // 42 | group_ci + load_ci_idx, -ss << __str_321; +ss << __str_327; // 43 | h_w_idx * 4 + load_h_w_idx); -ss << __str_322; +ss << __str_328; // 44 | } ss << __str_135; // 45 | workgroupBarrier(); @@ -107,11 +107,11 @@ ss << __str_168; ss << __str_12; // 47 | // store // 48 | for (var local_h_w_idx = 0u; local_h_w_idx < 4u; local_h_w_idx++) { -ss << __str_323; +ss << __str_329; // 49 | let output_data = data_cache[local_h_w_idx][local_idx]; -ss << __str_324; +ss << __str_330; // 50 | write_output(group_co, h_w_idx * 4 + local_h_w_idx, group_ci + local_idx, output_data); -ss << __str_325; +ss << __str_331; // 51 | } ss << __str_135; // 52 | workgroupBarrier(); diff --git a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/tensor/pad.h b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/tensor/pad.h index be2636fb78615..fb66997aa36d4 100644 --- a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/tensor/pad.h +++ b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/generated/tensor/pad.h @@ -40,106 +40,106 @@ ss << __str_189; // 16 | ss << __str_12; // 17 | let constant_value = -ss << __str_332; +ss << __str_338; // 18 | #if is_float16 if (__param_is_float16) { // 19 | bitcast>(uniforms.constant_value)[0]; -ss << __str_333; +ss << __str_339; // 20 | #else } else { // 21 | bitcast(uniforms.constant_value); -ss << __str_334; +ss << __str_340; // 22 | #endif } // 23 | // 24 | #if dim_value_zero if (__param_dim_value_zero) { // 25 | output[global_idx] = constant_value; -ss << __str_335; +ss << __str_341; // 26 | #else } else { // 27 | let output_indices = output.offsetToIndices(global_idx); -ss << __str_336; +ss << __str_342; ss << __var_output.OffsetToIndices(__str_210); ss << __str_189; // 28 | var input_index = u32(0); -ss << __str_337; +ss << __str_343; // 29 | var use_pad_value = false; -ss << __str_338; +ss << __str_344; // 30 | var in_coord = i32(0); -ss << __str_339; +ss << __str_345; // 31 | ss << __str_12; // 32 | for (var dim = 0; dim < output.rank && !use_pad_value; dim++) { -ss << __str_340; +ss << __str_346; ss << __var_output.Rank(); -ss << __str_341; +ss << __str_347; // 33 | let output_index = i32(getElementAt(output_indices, dim, output.rank)); -ss << __str_342; -ss << GetElementAt(__str_326, __str_327, __var_output.Rank()); +ss << __str_348; +ss << GetElementAt(__str_332, __str_333, __var_output.Rank()); ss << __str_3; // 34 | let lower_pads = getElementAt(uniforms.lower_pads, dim, output.rank); -ss << __str_343; -ss << GetElementAt(__str_328, __str_327, __var_output.Rank()); +ss << __str_349; +ss << GetElementAt(__str_334, __str_333, __var_output.Rank()); ss << __str_189; // 35 | let data_shape = i32(getElementAt(uniforms.data_shape, dim, output.rank)); -ss << __str_344; -ss << GetElementAt(__str_329, __str_327, __var_output.Rank()); +ss << __str_350; +ss << GetElementAt(__str_335, __str_333, __var_output.Rank()); ss << __str_3; // 36 | #if pad_mode == PAD_MODE_CONSTANT if (__param_pad_mode == 0) { // 37 | if (output_index < lower_pads || output_index >= data_shape + lower_pads) { -ss << __str_345; +ss << __str_351; // 38 | use_pad_value = true; -ss << __str_346; +ss << __str_352; // 39 | #elif pad_mode == PAD_MODE_EDGE } else if (__param_pad_mode == 2) { // 40 | if (output_index < lower_pads) { -ss << __str_347; +ss << __str_353; // 41 | in_coord = 0; -ss << __str_348; +ss << __str_354; // 42 | } else if (output_index >= data_shape + lower_pads) { -ss << __str_349; +ss << __str_355; // 43 | in_coord = data_shape - 1; -ss << __str_350; +ss << __str_356; // 44 | #elif pad_mode == PAD_MODE_REFLECT } else if (__param_pad_mode == 1) { // 45 | if (output_index < lower_pads || output_index >= data_shape + lower_pads) { -ss << __str_345; -// 46 | in_coord = output_index - lower_pads; ss << __str_351; +// 46 | in_coord = output_index - lower_pads; +ss << __str_357; // 47 | if (in_coord < 0) { -ss << __str_352; +ss << __str_358; // 48 | in_coord = -in_coord; -ss << __str_353; +ss << __str_359; // 49 | } ss << __str_291; // 50 | let _2n_1 = 2 * (data_shape - 1); -ss << __str_354; +ss << __str_360; // 51 | in_coord = in_coord % _2n_1; -ss << __str_355; +ss << __str_361; // 52 | if (in_coord >= data_shape) { -ss << __str_356; +ss << __str_362; // 53 | in_coord = _2n_1 - in_coord; -ss << __str_357; +ss << __str_363; // 54 | } ss << __str_291; // 55 | #else // PAD_MODE_WRAP } else { // 56 | if (output_index < lower_pads) { -ss << __str_347; +ss << __str_353; // 57 | in_coord = data_shape + output_index - lower_pads; -ss << __str_358; +ss << __str_364; // 58 | } else if (output_index >= data_shape + lower_pads) { -ss << __str_349; +ss << __str_355; // 59 | in_coord = output_index - data_shape - lower_pads; -ss << __str_359; +ss << __str_365; // 60 | #endif // pad_mode } // 61 | } else { -ss << __str_360; +ss << __str_366; // 62 | in_coord = output_index - lower_pads; -ss << __str_361; +ss << __str_367; // 63 | } ss << __str_135; // 64 | @@ -147,31 +147,31 @@ ss << __str_12; // 65 | #if pad_mode == PAD_MODE_WRAP if (__param_pad_mode == 3) { // 66 | in_coord = ((in_coord % data_shape) + data_shape) % data_shape; -ss << __str_362; +ss << __str_368; // 67 | #endif } // 68 | // 69 | input_index += select(u32(in_coord) -ss << __str_363; +ss << __str_369; // 70 | #if output.rank > 1 if (__var_output.Rank() > 1) { // 71 | * getElementAt(uniforms.data_stride, dim, output.rank - 1) -ss << __str_364; -ss << GetElementAt(__str_330, __str_327, __var_output.Rank() - 1); +ss << __str_370; +ss << GetElementAt(__str_336, __str_333, __var_output.Rank() - 1); ss << __str_12; // 72 | #endif } // 73 | , u32(in_coord), dim == output.rank - 1); -ss << __str_365; +ss << __str_371; ss << __var_output.Rank(); -ss << __str_366; +ss << __str_372; // 74 | } ss << __str_218; // 75 | ss << __str_12; // 76 | output.setByOffset(global_idx, select(data[input_index], constant_value, use_pad_value)); ss << __str_212; -ss << __var_output.SetByOffset(__str_210, __str_331); +ss << __var_output.SetByOffset(__str_210, __str_337); ss << __str_189; // 77 | #endif } diff --git a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/index.h b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/index.h index bcd58dfb1f3b7..e481534e62d5d 100644 --- a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/index.h +++ b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/index.h @@ -70,6 +70,7 @@ Status ApplyTemplate<"math/subgroup_matrix_matmul_pad_b.wgsl.template">(ShaderHe template <> struct TemplateParameter<"nn/im2col_matmul.wgsl.template"> { using type = struct { + int param_activation_kind; int param_has_bias; int param_tile_m; int param_tile_n; diff --git a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/index_impl.h b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/index_impl.h index bdbaa23455fad..19a08e5e4ced0 100644 --- a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/index_impl.h +++ b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/index_impl.h @@ -31,16 +31,16 @@ std::string pass_as_string(T&& v) { } } // namespace wgsl_detail -#include "wgsl_template_gen/string_table.h" // 37a79ce6e8f994a91bdf7a3732dd33f99193e7b52a201169dd87b8bfc33d2a97 +#include "wgsl_template_gen/string_table.h" // 5782445befe9ac59257ba45b084e58baff234680d2a2b48c2054ac1a003f8e94 // Include template implementations #include "wgsl_template_gen/generated/math/subgroup_matrix_gemm_8x16x16.h" // 5165922e266c4c9fd7625fdccd350ca4d8f58dafd313446ec4a0b5961f14f812 #include "wgsl_template_gen/generated/math/subgroup_matrix_matmul_8x16x16.h" // 4ace42086f6a7d277f2c26c2a01cdee1d53812fe8ea12752df1086f8730d9520 #include "wgsl_template_gen/generated/math/subgroup_matrix_matmul_pad_b.h" // 6a2d3ef81124f2e2bc7af04cb10d365e9aaf7a266b73f03417a5ad0842fdb18c -#include "wgsl_template_gen/generated/nn/im2col_matmul.h" // c203f647a0956b4e80985a2dfb7bc74c8585962e4408218db4b2c2fb01907390 -#include "wgsl_template_gen/generated/tensor/oihw_to_ohwi.h" // dc36783b9884b4d25bc9137d601f4e149872c00fc8e29afa75ad29aacb8e0f2f -#include "wgsl_template_gen/generated/tensor/pad.h" // ae25f15ae953ab0376097b563f246721d1ff613eca9e46560f56c54cdc182da7 +#include "wgsl_template_gen/generated/nn/im2col_matmul.h" // 39489c0dc124ae5427d09a6ee0d9249e6664e0cc1c6776c7a1889b45a83a79cc +#include "wgsl_template_gen/generated/tensor/oihw_to_ohwi.h" // 6eb9410a321b8d186df94b477dab032a5aaa67759b94d33570a07219603e738b +#include "wgsl_template_gen/generated/tensor/pad.h" // bc3f3a1dd04bbd7807494efb0bce20a5747aad85cfacc5bbc3eec412cf01d2eb #pragma pop_macro("MainFunctionStart") #pragma pop_macro("MainFunctionEnd") \ No newline at end of file diff --git a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/string_table.h b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/string_table.h index 51c8e1e2f3e5e..89565ff6edf47 100644 --- a/tools/python/wgsl_template/test/in_tree_golden/static-cpp/string_table.h +++ b/tools/python/wgsl_template/test/in_tree_golden/static-cpp/string_table.h @@ -305,71 +305,77 @@ constexpr const char* __str_295 = " let n_base = n_global_base + local_idx;\n"; constexpr const char* __str_296 = " let bias = load_bias(n_base);\n"; constexpr const char* __str_297 = " for (var m_idx = 0u; m_idx < TILE_M_SIZE; m_idx++) {\n"; constexpr const char* __str_298 = " var output_data = results[m_idx] + bias;\n"; -constexpr const char* __str_299 = " write_output(batch, m_base + m_idx, n_base, output_data);\n"; -constexpr const char* __str_300 = "offset"; -constexpr const char* __str_301 = "fn load_src(co : u32, ci : u32, h_w : u32) -> src_element_t {\n"; -constexpr const char* __str_302 = " if (co < uniforms.O && ci < uniforms.I && h_w < uniforms.H * uniforms.W) {\n"; -constexpr const char* __str_303 = " let offset = co * uniforms.I * uniforms.H * uniforms.W +\n"; -constexpr const char* __str_304 = " ci * uniforms.H * uniforms.W +\n"; -constexpr const char* __str_305 = " h_w;\n"; -constexpr const char* __str_306 = " return src_element_t();\n"; -constexpr const char* __str_307 = "fn write_output(co : u32, h_w : u32, ci : u32, value : output_element_t) {\n"; -constexpr const char* __str_308 = " let offset = co * uniforms.H * uniforms.W * uniforms.I +\n"; -constexpr const char* __str_309 = " h_w * uniforms.I +\n"; -constexpr const char* __str_310 = " ci;\n"; -constexpr const char* __str_311 = "var data_cache : array, 4>;\n"; -constexpr const char* __str_312 = " let group_co : u32 = workgroup_idx / uniforms.Ci_tiles;\n"; -constexpr const char* __str_313 = " let group_ci : u32 = (workgroup_idx % uniforms.Ci_tiles) * 64;\n"; -constexpr const char* __str_314 = " if (group_co >= uniforms.O || group_ci >= uniforms.I) {\n"; -constexpr const char* __str_315 = " return;\n"; -constexpr const char* __str_316 = " for (var h_w_idx = 0u; h_w_idx < uniforms.H_W_tiles; h_w_idx++) {\n"; -constexpr const char* __str_317 = " for (var ci_idx = 0u; ci_idx < 64u; ci_idx += 16u) {\n"; -constexpr const char* __str_318 = " let load_ci_idx = ci_idx + local_idx / 4;\n"; -constexpr const char* __str_319 = " let load_h_w_idx = local_idx % 4;\n"; -constexpr const char* __str_320 = " data_cache[load_h_w_idx][load_ci_idx] = load_src(group_co,\n"; -constexpr const char* __str_321 = " group_ci + load_ci_idx,\n"; -constexpr const char* __str_322 = " h_w_idx * 4 + load_h_w_idx);\n"; -constexpr const char* __str_323 = " for (var local_h_w_idx = 0u; local_h_w_idx < 4u; local_h_w_idx++) {\n"; -constexpr const char* __str_324 = " let output_data = data_cache[local_h_w_idx][local_idx];\n"; -constexpr const char* __str_325 = " write_output(group_co, h_w_idx * 4 + local_h_w_idx, group_ci + local_idx, output_data);\n"; -constexpr const char* __str_326 = "output_indices"; -constexpr const char* __str_327 = "dim"; -constexpr const char* __str_328 = "uniforms.lower_pads"; -constexpr const char* __str_329 = "uniforms.data_shape"; -constexpr const char* __str_330 = "uniforms.data_stride"; -constexpr const char* __str_331 = "select(data[input_index], constant_value, use_pad_value)"; -constexpr const char* __str_332 = " let constant_value =\n"; -constexpr const char* __str_333 = " bitcast>(uniforms.constant_value)[0];\n"; -constexpr const char* __str_334 = " bitcast(uniforms.constant_value);\n"; -constexpr const char* __str_335 = " output[global_idx] = constant_value;\n"; -constexpr const char* __str_336 = " let output_indices = "; -constexpr const char* __str_337 = " var input_index = u32(0);\n"; -constexpr const char* __str_338 = " var use_pad_value = false;\n"; -constexpr const char* __str_339 = " var in_coord = i32(0);\n"; -constexpr const char* __str_340 = " for (var dim = 0; dim < "; -constexpr const char* __str_341 = " && !use_pad_value; dim++) {\n"; -constexpr const char* __str_342 = " let output_index = i32("; -constexpr const char* __str_343 = " let lower_pads = "; -constexpr const char* __str_344 = " let data_shape = i32("; -constexpr const char* __str_345 = " if (output_index < lower_pads || output_index >= data_shape + lower_pads) {\n"; -constexpr const char* __str_346 = " use_pad_value = true;\n"; -constexpr const char* __str_347 = " if (output_index < lower_pads) {\n"; -constexpr const char* __str_348 = " in_coord = 0;\n"; -constexpr const char* __str_349 = " } else if (output_index >= data_shape + lower_pads) {\n"; -constexpr const char* __str_350 = " in_coord = data_shape - 1;\n"; -constexpr const char* __str_351 = " in_coord = output_index - lower_pads;\n"; -constexpr const char* __str_352 = " if (in_coord < 0) {\n"; -constexpr const char* __str_353 = " in_coord = -in_coord;\n"; -constexpr const char* __str_354 = " let _2n_1 = 2 * (data_shape - 1);\n"; -constexpr const char* __str_355 = " in_coord = in_coord % _2n_1;\n"; -constexpr const char* __str_356 = " if (in_coord >= data_shape) {\n"; -constexpr const char* __str_357 = " in_coord = _2n_1 - in_coord;\n"; -constexpr const char* __str_358 = " in_coord = data_shape + output_index - lower_pads;\n"; -constexpr const char* __str_359 = " in_coord = output_index - data_shape - lower_pads;\n"; -constexpr const char* __str_360 = " } else {\n"; -constexpr const char* __str_361 = " in_coord = output_index - lower_pads;\n"; -constexpr const char* __str_362 = " in_coord = ((in_coord % data_shape) + data_shape) % data_shape;\n"; -constexpr const char* __str_363 = " input_index += select(u32(in_coord)\n"; -constexpr const char* __str_364 = " * "; -constexpr const char* __str_365 = " , u32(in_coord), dim == "; -constexpr const char* __str_366 = " - 1);\n"; +constexpr const char* __str_299 = " output_data = max(output_data, output_element_t(0));\n"; +constexpr const char* __str_300 = " output_data = output_element_t(1) / (output_element_t(1) + exp(-output_data));\n"; +constexpr const char* __str_301 = " output_data = clamp(output_data, output_element_t(uniforms.activation_param_0), output_element_t(uniforms.activation_param_1));\n"; +constexpr const char* __str_302 = " output_data = clamp(output_element_t(uniforms.activation_param_0) * output_data + output_element_t(uniforms.activation_param_1), output_element_t(0), output_element_t(1));\n"; +constexpr const char* __str_303 = " output_data = select(output_element_t(uniforms.activation_param_0) * output_data, output_data, output_data >= output_element_t(0));\n"; +constexpr const char* __str_304 = " output_data = tanh(output_data);\n"; +constexpr const char* __str_305 = " write_output(batch, m_base + m_idx, n_base, output_data);\n"; +constexpr const char* __str_306 = "offset"; +constexpr const char* __str_307 = "fn load_src(co : u32, ci : u32, h_w : u32) -> src_element_t {\n"; +constexpr const char* __str_308 = " if (co < uniforms.O && ci < uniforms.I && h_w < uniforms.H * uniforms.W) {\n"; +constexpr const char* __str_309 = " let offset = co * uniforms.I * uniforms.H * uniforms.W +\n"; +constexpr const char* __str_310 = " ci * uniforms.H * uniforms.W +\n"; +constexpr const char* __str_311 = " h_w;\n"; +constexpr const char* __str_312 = " return src_element_t();\n"; +constexpr const char* __str_313 = "fn write_output(co : u32, h_w : u32, ci : u32, value : output_element_t) {\n"; +constexpr const char* __str_314 = " let offset = co * uniforms.H * uniforms.W * uniforms.I +\n"; +constexpr const char* __str_315 = " h_w * uniforms.I +\n"; +constexpr const char* __str_316 = " ci;\n"; +constexpr const char* __str_317 = "var data_cache : array, 4>;\n"; +constexpr const char* __str_318 = " let group_co : u32 = workgroup_idx / uniforms.Ci_tiles;\n"; +constexpr const char* __str_319 = " let group_ci : u32 = (workgroup_idx % uniforms.Ci_tiles) * 64;\n"; +constexpr const char* __str_320 = " if (group_co >= uniforms.O || group_ci >= uniforms.I) {\n"; +constexpr const char* __str_321 = " return;\n"; +constexpr const char* __str_322 = " for (var h_w_idx = 0u; h_w_idx < uniforms.H_W_tiles; h_w_idx++) {\n"; +constexpr const char* __str_323 = " for (var ci_idx = 0u; ci_idx < 64u; ci_idx += 16u) {\n"; +constexpr const char* __str_324 = " let load_ci_idx = ci_idx + local_idx / 4;\n"; +constexpr const char* __str_325 = " let load_h_w_idx = local_idx % 4;\n"; +constexpr const char* __str_326 = " data_cache[load_h_w_idx][load_ci_idx] = load_src(group_co,\n"; +constexpr const char* __str_327 = " group_ci + load_ci_idx,\n"; +constexpr const char* __str_328 = " h_w_idx * 4 + load_h_w_idx);\n"; +constexpr const char* __str_329 = " for (var local_h_w_idx = 0u; local_h_w_idx < 4u; local_h_w_idx++) {\n"; +constexpr const char* __str_330 = " let output_data = data_cache[local_h_w_idx][local_idx];\n"; +constexpr const char* __str_331 = " write_output(group_co, h_w_idx * 4 + local_h_w_idx, group_ci + local_idx, output_data);\n"; +constexpr const char* __str_332 = "output_indices"; +constexpr const char* __str_333 = "dim"; +constexpr const char* __str_334 = "uniforms.lower_pads"; +constexpr const char* __str_335 = "uniforms.data_shape"; +constexpr const char* __str_336 = "uniforms.data_stride"; +constexpr const char* __str_337 = "select(data[input_index], constant_value, use_pad_value)"; +constexpr const char* __str_338 = " let constant_value =\n"; +constexpr const char* __str_339 = " bitcast>(uniforms.constant_value)[0];\n"; +constexpr const char* __str_340 = " bitcast(uniforms.constant_value);\n"; +constexpr const char* __str_341 = " output[global_idx] = constant_value;\n"; +constexpr const char* __str_342 = " let output_indices = "; +constexpr const char* __str_343 = " var input_index = u32(0);\n"; +constexpr const char* __str_344 = " var use_pad_value = false;\n"; +constexpr const char* __str_345 = " var in_coord = i32(0);\n"; +constexpr const char* __str_346 = " for (var dim = 0; dim < "; +constexpr const char* __str_347 = " && !use_pad_value; dim++) {\n"; +constexpr const char* __str_348 = " let output_index = i32("; +constexpr const char* __str_349 = " let lower_pads = "; +constexpr const char* __str_350 = " let data_shape = i32("; +constexpr const char* __str_351 = " if (output_index < lower_pads || output_index >= data_shape + lower_pads) {\n"; +constexpr const char* __str_352 = " use_pad_value = true;\n"; +constexpr const char* __str_353 = " if (output_index < lower_pads) {\n"; +constexpr const char* __str_354 = " in_coord = 0;\n"; +constexpr const char* __str_355 = " } else if (output_index >= data_shape + lower_pads) {\n"; +constexpr const char* __str_356 = " in_coord = data_shape - 1;\n"; +constexpr const char* __str_357 = " in_coord = output_index - lower_pads;\n"; +constexpr const char* __str_358 = " if (in_coord < 0) {\n"; +constexpr const char* __str_359 = " in_coord = -in_coord;\n"; +constexpr const char* __str_360 = " let _2n_1 = 2 * (data_shape - 1);\n"; +constexpr const char* __str_361 = " in_coord = in_coord % _2n_1;\n"; +constexpr const char* __str_362 = " if (in_coord >= data_shape) {\n"; +constexpr const char* __str_363 = " in_coord = _2n_1 - in_coord;\n"; +constexpr const char* __str_364 = " in_coord = data_shape + output_index - lower_pads;\n"; +constexpr const char* __str_365 = " in_coord = output_index - data_shape - lower_pads;\n"; +constexpr const char* __str_366 = " } else {\n"; +constexpr const char* __str_367 = " in_coord = output_index - lower_pads;\n"; +constexpr const char* __str_368 = " in_coord = ((in_coord % data_shape) + data_shape) % data_shape;\n"; +constexpr const char* __str_369 = " input_index += select(u32(in_coord)\n"; +constexpr const char* __str_370 = " * "; +constexpr const char* __str_371 = " , u32(in_coord), dim == "; +constexpr const char* __str_372 = " - 1);\n";