Add DeepSeek Engram contrib ops (EngramGate, NGramHashMapping, ShortConv) - #32268
Add DeepSeek Engram contrib ops (EngramGate, NGramHashMapping, ShortConv)#32268kunal-vaishnavi with Copilot wants to merge 10 commits into
Conversation
Co-authored-by: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com>
Co-authored-by: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com>
Co-authored-by: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com>
Co-authored-by: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com>
Co-authored-by: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com>
Co-authored-by: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com>
Co-authored-by: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com>
Co-authored-by: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com>
Co-authored-by: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Adds three DeepSeek Engram contrib operators across CPU, CUDA, and WebGPU.
Changes:
- Defines and documents EngramGate, NGramHashMapping, and ShortConv schemas.
- Implements and registers kernels for all three execution providers.
- Adds shared helpers and initial operator tests.
Reviewed changes
Copilot reviewed 35 out of 35 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
onnxruntime/test/contrib_ops/engram_ops_test.cc |
Adds operator tests. |
onnxruntime/core/graph/contrib_ops/ms_opset.h |
Registers schemas. |
onnxruntime/core/graph/contrib_ops/bert_defs.cc |
Defines schemas and inference. |
onnxruntime/contrib_ops/webgpu/webgpu_contrib_kernels.cc |
Registers WebGPU kernels. |
onnxruntime/contrib_ops/webgpu/bert/short_conv.h |
Declares WebGPU ShortConv. |
onnxruntime/contrib_ops/webgpu/bert/short_conv.cc |
Implements WebGPU ShortConv. |
onnxruntime/contrib_ops/webgpu/bert/ngram_hash_mapping.h |
Declares WebGPU hash mapping. |
onnxruntime/contrib_ops/webgpu/bert/ngram_hash_mapping.cc |
Implements WebGPU hash mapping. |
onnxruntime/contrib_ops/webgpu/bert/kernel_helper.h |
Adds shared WGSL helpers. |
onnxruntime/contrib_ops/webgpu/bert/engram_gate.h |
Declares WebGPU EngramGate. |
onnxruntime/contrib_ops/webgpu/bert/engram_gate.cc |
Implements WebGPU EngramGate. |
onnxruntime/contrib_ops/cuda/cuda_contrib_kernels.cc |
Registers CUDA kernels. |
onnxruntime/contrib_ops/cuda/bert/short_conv.h |
Declares CUDA ShortConv. |
onnxruntime/contrib_ops/cuda/bert/short_conv.cc |
Validates and launches ShortConv. |
onnxruntime/contrib_ops/cuda/bert/short_conv_impl.h |
Declares ShortConv launcher. |
onnxruntime/contrib_ops/cuda/bert/short_conv_impl.cu |
Implements ShortConv kernel. |
onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping.h |
Declares CUDA hash mapping. |
onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping.cc |
Validates and launches hashing. |
onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping_impl.h |
Declares hash launcher. |
onnxruntime/contrib_ops/cuda/bert/ngram_hash_mapping_impl.cu |
Implements hash kernel. |
onnxruntime/contrib_ops/cuda/bert/kernel_helper.cuh |
Adds shared CUDA helpers. |
onnxruntime/contrib_ops/cuda/bert/engram_gate.h |
Declares CUDA EngramGate. |
onnxruntime/contrib_ops/cuda/bert/engram_gate.cc |
Validates and launches EngramGate. |
onnxruntime/contrib_ops/cuda/bert/engram_gate_impl.h |
Declares EngramGate launcher. |
onnxruntime/contrib_ops/cuda/bert/engram_gate_impl.cu |
Implements EngramGate kernel. |
onnxruntime/contrib_ops/cpu/cpu_contrib_kernels.cc |
Registers CPU kernels. |
onnxruntime/contrib_ops/cpu/bert/short_conv.h |
Declares CPU ShortConv. |
onnxruntime/contrib_ops/cpu/bert/short_conv.cc |
Implements CPU ShortConv. |
onnxruntime/contrib_ops/cpu/bert/ngram_hash_mapping.h |
Declares CPU hash mapping. |
onnxruntime/contrib_ops/cpu/bert/ngram_hash_mapping.cc |
Implements CPU hash mapping. |
onnxruntime/contrib_ops/cpu/bert/kernel_helper.h |
Adds shared CPU helpers. |
onnxruntime/contrib_ops/cpu/bert/engram_gate.h |
Declares CPU EngramGate. |
onnxruntime/contrib_ops/cpu/bert/engram_gate.cc |
Implements CPU EngramGate. |
docs/OperatorKernels.md |
Documents provider type support. |
docs/ContribOperators.md |
Documents public operator schemas. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| dot += normed_key * normed_query; | ||
| } | ||
| dot /= std::sqrt(static_cast<float>(hidden_size)); | ||
| const float gate_arg = std::copysign(std::sqrt(std::max(std::abs(dot), 1.0e-6f)), dot); |
There was a problem hiding this comment.
Fixed. std::copysign is gone; the CPU kernel now calls a new shared helper kernel_helper::EngramGateArg() (in onnxruntime/contrib_ops/cpu/bert/kernel_helper.h) which returns exactly 0.0f when dot == 0.0f and otherwise sign(dot) * sqrt(max(abs(dot), 1e-6)). The CUDA kernel got the equivalent helper, and WebGPU now has a matching engram_gate_arg() WGSL function, so all three EPs share one definition.
Added a zero-dot regression test, EngramOpsTest.EngramGateZeroDotProduct, which uses an orthogonal key/query pair so the dot product is exactly zero and asserts the gate is exactly 0.5.
| const float key_inv_rms = rsqrtf(key_sum_sq / static_cast<float>(hidden_size) + epsilon); | ||
| const float query_inv_rms = rsqrtf(query_sum_sq / static_cast<float>(hidden_size) + epsilon); | ||
| const float dot = dot_numerator * key_inv_rms * query_inv_rms / sqrtf(static_cast<float>(hidden_size)); | ||
| const float gate_arg = copysignf(sqrtf(fmaxf(fabsf(dot), 1.0e-6f)), dot); |
There was a problem hiding this comment.
Fixed. copysignf is replaced by the shared kernel_helper::EngramGateArg() device helper in onnxruntime/contrib_ops/cuda/bert/kernel_helper.cuh, which returns 0.0f for a zero dot product. The CPU helper and the new WGSL engram_gate_arg() use the same definition, so all EPs agree. Covered by the new EngramOpsTest.EngramGateZeroDotProduct test.
| for (int64_t d = 0; d < hidden_size; ++d) { | ||
| float key = key_bias == nullptr ? 0.0f : to_float<T>(key_bias[g * hidden_size + d]); | ||
| for (int64_t e = 0; e < embedding_size; ++e) { | ||
| key += to_float<T>(embedding_row[e]) * | ||
| to_float<T>(key_weight[(g * embedding_size + e) * hidden_size + d]); |
There was a problem hiding this comment.
Fixed. The kernel is now one block per (token, g) row instead of one thread per output channel:
- Threads in the block stride over
dto compute the key projection, accumulating partialkey_sum_sq,query_sum_sqanddot_numerator. - Those three partials are combined with a new shared
kernel_helper::BlockSum()block reduction (added tokernel_helper.cuhso ShortConv can reuse it). - The gate is then computed once and applied in a second strided loop over the output channels, which is where the value projection is computed.
Per-row work drops from hidden_size² × embedding_size to 2 × hidden_size × embedding_size. Launch config is min(rows, 65535) blocks with kThreads threads and kThreads * sizeof(float) bytes of dynamic shared memory.
| << " for (var e = 0u; e < uniforms.embedding_size; e++) {\n" | ||
| << " key += f32(" << embeddings.GetByOffset("embedding_base + e") << ") * f32(" | ||
| << key_weight.GetByOffset("(g * uniforms.embedding_size + e) * uniforms.hidden_size + d") << ");\n" | ||
| << " }\n" |
There was a problem hiding this comment.
Fixed by splitting the shader into two programs:
EngramGateScalarProgram— one workgroup per(token, g)row. Invocations stride overd, accumulatekey_sum_sq/query_sum_sq/dot_numeratorintovar<workgroup>arrays, do a tree reduction, and invocation 0 writes the single f32 gate for the row into an intermediate{rows}tensor.EngramGateProgram— unchanged one-invocation-per-output-element dispatch, but it now only computes the value projection and multiplies by the pre-computed gate read from the intermediate tensor.
The key projection is therefore executed once per row rather than hidden_size times. Input binding order is preserved in both programs (all AddInput() calls, including the optional bias, precede AddOutput() and match the shader order).
| float sum_sq = 0.0f; | ||
| for (int64_t i = 0; i < hidden_size; ++i) { | ||
| const float value = static_cast<float>(input_data[row_base + i]); | ||
| sum_sq += value * value; | ||
| } |
There was a problem hiding this comment.
Fixed. ShortConv<T>::Compute now runs a cheap first TryParallelFor over the batch_size * sequence_length * hc_mult rows that fills a std::vector<float> inv_rms with one inverse-RMS value per row. The convolution loop reads inv_rms[source_row] instead of re-reducing the hidden dimension, and norm_scale is hoisted out of the tap loop as well.
Total work drops from O(total × kernel_size × hidden_size) to O(rows × hidden_size) for the normalization plus O(total × kernel_size) for the convolution, and the extra buffer is only rows floats (hidden_size times smaller than the input). The parallel-for cost estimate for the convolution loop was updated to kernel_size accordingly.
| float sum_sq = 0.0f; | ||
| for (int64_t i = 0; i < hidden_size; ++i) { | ||
| const float value = to_float<T>(input[row_base + i]); | ||
| sum_sq += value * value; | ||
| } |
There was a problem hiding this comment.
Fixed with a two-kernel launch:
ShortConvInvRmsKernel— one block per(batch, t, g)row, threads stride over the hidden dimension and the partials are combined with the sharedkernel_helper::BlockSum()reduction. It writes onefloatinverse-RMS per row.ShortConvKernel— unchanged grid-stride loop over output elements, but it now readsinv_rms[source_row]per tap instead of re-reducing, and hoists thenorm_scaleload out of the tap loop.
The scratch buffer is allocated in ShortConv<T>::ComputeInternal via GetScratchBuffer<float>(rows, context->GetComputeStream()), so it is only batch_size * sequence_length * hc_mult floats.
| << " var sum_sq = 0.0;\n" | ||
| << " for (var i = 0u; i < uniforms.hidden_size; i++) {\n" | ||
| << " let v = f32(" << input.GetByOffset("row_base + i") << ");\n" | ||
| << " sum_sq += v * v;\n" | ||
| << " }\n" |
There was a problem hiding this comment.
Fixed. Added a ShortConvInvRmsProgram first pass: one workgroup per (batch, sequence, hc_mult) row, invocations stride over the hidden dimension into a var<workgroup> partials array, tree-reduce, and invocation 0 writes the row's inverse RMS into an intermediate f32 {rows} tensor.
ShortConvProgram now takes that tensor as an extra input and the tap loop simply reads inv_rms[source_row]; norm_scale is also hoisted out of the loop. The epsilon uniform moved to the new program since the conv shader no longer needs it.
|
|
||
| } // namespace | ||
|
|
||
| TEST(EngramOpsTest, NGramHashMappingInt64) { |
There was a problem hiding this comment.
Fixed. The body was extracted into a templated RunNGramHashMappingTest<T>() and there are now two cases: EngramOpsTest.NGramHashMappingInt64 and EngramOpsTest.NGramHashMappingInt32, so the int32 CPU/CUDA paths and the int32-only WebGPU kernel are exercised.
| std::vector<T> ToTensorType(const std::vector<float>& data) { | ||
| if constexpr (std::is_same_v<T, MLFloat16>) { | ||
| return ToFloat16(data); | ||
| } else { | ||
| return data; | ||
| } |
There was a problem hiding this comment.
Fixed. ToTensorType<T> now has a BFloat16 branch calling ToBFloat16(data), matching linear_attention_gates_op_test.cc:37-45.
Because the BFloat16 kernels are CUDA-only, I added a RunOnSupportedProviders<T>() helper: for BFloat16 it runs OpTester against DefaultCudaExecutionProvider() only (and returns false so the caller does GTEST_SKIP() when CUDA is unavailable), and for other types it keeps the default test.Run() path. Added EngramOpsTest.ShortConvBFloat16 and EngramOpsTest.EngramGateBFloat16 with a 2e-2 tolerance.
Co-authored-by: kunal-vaishnavi <115581922+kunal-vaishnavi@users.noreply.github.com>
Description
Adds three
com.microsoftcontrib ops for DeepSeek Engram, each with CPU, CUDA, and WebGPU kernels:NGramHashMappingint32/int64)EngramGatefloat/float16/bfloat16)ShortConvfloat/float16/bfloat16)Layout
engram_gate.*,ngram_hash_mapping.*,short_conv.*), following the existingcontrib_ops/<ep>/bert/convention.kernel_helperfile rather than being duplicated in each kernel:contrib_ops/cpu/bert/kernel_helper.h—SigmoidFloat,SiluFloat,PositiveMod,WrappedMultiplycontrib_ops/cuda/bert/kernel_helper.cuh— same as device functions, plus the sharedkThreads/GridSizelaunch configcontrib_ops/webgpu/bert/kernel_helper.h— WGSL snippets (stable_sigmoid,silu,positive_mod)Type coverage
float+MLFloat16forEngramGate/ShortConv;int32/int64forNGramHashMapping.float/MLFloat16/BFloat16.WebGpuSupportedFloatTypes();NGramHashMappingisint32-only.Notes for review
WrappedMultiplycomputes the hash mix through the unsigned counterpart ofTso overflow wraps instead of being UB — this is load-bearing for matching reference hash ids, not a style choice.BuildKernelCreateInfotable.docs/ContribOperators.md/docs/OperatorKernels.mdwere updated by hand to matchgen_docoutput; the Windows CI--gen_doc validaterun is the authoritative check here.Motivation and Context
Engram currently decomposes into long chains of primitive ONNX ops (shifts, hashes, mod, RMSNorm, depthwise conv, sigmoid gating), which is both slow and awkward to export. These three ops collapse those patterns into single kernels so Engram-based models can run efficiently across CPU, CUDA, and WebGPU.