Skip to content

perf: add onednn binary primitives for add/multiply achieving parity with torchsharp - #732

Merged
ooples merged 3 commits into
masterfrom
perf/onednn-binary-primitives
Jan 20, 2026
Merged

ooples merged 3 commits into
masterfrom
perf/onednn-binary-primitives

Conversation

@ooples

@ooples ooples commented Jan 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add oneDNN binary primitive support (dnnl_binary_add, dnnl_binary_mul)
  • Implement TryAdd and TryMultiply methods in OneDnnProvider with caching
  • Add parallel Sum implementation with lock for combining partial results
  • Optimize FloatOperations to use oneDNN for large arrays (>=50K elements)
  • Update CpuParallelSettings ParallelThreshold to 50000
  • Add raw TensorPrimitives benchmarks for baseline comparison

Benchmark Results

Operation AiDotNet TorchSharp Status
TensorAdd (1M) 19.76μs 20.01μs PARITY/WIN
TensorMultiply (1M) 20.41μs 19.22μs PARITY
TensorAdd (100K) 9.99μs 9.18μs PARITY
TensorMultiply (100K) 10.90μs 8.47μs Close
TensorSum (1M) 23.54μs 31.57μs WIN 34%
TensorMean (1M) 25.80μs 38.93μs WIN 51%

Test plan

  • Build succeeds on net10.0 and net471
  • Tensor tests pass
  • Benchmarks show parity or better performance vs TorchSharp

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings January 20, 2026 00:41
@coderabbitai

coderabbitai Bot commented Jan 20, 2026 •

Copy link
Copy Markdown
Contributor

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

  • New Features

    • Added in-place Sigmoid and ReLU variants and OneDNN-backed caching for element-wise and binary operations.
  • Performance

    • Lowered CPU parallelism threshold and added OneDNN fast-paths for add, multiply and sum to boost throughput on large arrays.
    • Improved parallel chunking logic for safer, more efficient multi-threading.
  • Tests

    • Added raw-array microbenchmarks to compare direct tensor-primitive performance.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds raw-array TensorPrimitives benchmarks; introduces in-place Sigmoid/ReLU APIs on CpuEngine with optional oneDNN unsafe fast paths; lowers parallel threshold and rewrites chunked parallel execution; implements eltwise and binary primitive caches and in-place execution in OneDnnProvider; FloatOperations now prefers oneDNN for large Add/Multiply and parallel Sum.

Changes

Cohort / File(s) Summary
Benchmark Infrastructure
AiDotNetBenchmarkTests/TorchSharpCpuComparisonBenchmarks.cs
Adds _rawArraysA, _rawArraysB, _rawDestination and two benchmarks (RawTensorPrimitives_Add(int), RawTensorPrimitives_Sum()) operating on raw float[] spans via TensorPrimitives.
CPU Engine In-place Activations
src/AiDotNet.Tensors/Engines/CpuEngine.cs
Adds SigmoidInPlace<T> and ReLUInPlace<T> (unsafe variants when not NETFRAMEWORK, plus NETFRAMEWORK guarded delegates) that attempt oneDNN/unsafe fast paths for float tensors and fall back to existing in-place implementations.
Parallelism Helpers
src/AiDotNet.Tensors/Helpers/CpuParallelSettings.cs
Lowers ParallelThreshold (65536→50000), adds null-check for action, refactors ParallelForChunks chunking, degree-of-parallelism gating, and early sequential exits.
OneDNN Caching & Primitives
src/AiDotNet.Tensors/Helpers/OneDnnProvider.cs
Adds _eltwiseCache and _binaryCache with size limits, new types (EltwiseKey, BinaryKey, CachedEltwise, CachedBinary), P/Invoke bindings for eltwise/binary primitives, creation/eviction logic, and in-place eltwise/binary execution helpers (TryEltwiseInPlace/TryBinary and TryReLU/TrySigmoid/TryAdd/TryMultiply).
Numeric Operations Integration
src/AiDotNet.Tensors/NumericOperations/FloatOperations.cs
Add and Multiply now attempt oneDNN for large arrays (length >= ParallelThreshold) with unsafe pointer paths and fallback to TensorPrimitives; Sum gains NET8+ parallel reduction for large arrays; comments/thresholds updated.
BLAS provider guards
src/AiDotNet.Tensors/Helpers/BlasProvider.cs
Consolidates MKL.NET GEMM into a NoInlining helper TryMklNetSpanDgemm to avoid JIT-time MKL.NET type loading; duplicate removed.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller
    participant CpuEngine as CpuEngine
    participant OneDnn as OneDnnProvider
    participant FloatOps as FloatOperations
    participant TensorPrims as TensorPrimitives

    rect rgba(100,150,200,0.5)
    Note over Caller,CpuEngine: In-place activation flow
    Caller->>CpuEngine: SigmoidInPlace(tensor)
    CpuEngine->>OneDnn: TrySigmoid(span)
    OneDnn->>OneDnn: Lookup/Create eltwise primitive & mem
    OneDnn->>OneDnn: Execute on stream (in-place)
    alt OneDNN success
        OneDnn-->>CpuEngine: true
    else fallback
        CpuEngine->>CpuEngine: Generic in-place sigmoid
    end
    CpuEngine-->>Caller: done
    end

    rect rgba(150,200,100,0.5)
    Note over Caller,FloatOps: Binary add/multiply flow
    Caller->>FloatOps: Add(aSpan, bSpan)
    FloatOps->>OneDnn: TryBinary/Add(aSpan,bSpan,dstSpan)
    OneDnn->>OneDnn: Lookup/Create binary primitive & mem
    alt OneDNN success (large arrays)
        OneDnn-->>FloatOps: true (dst written)
    else fallback
        FloatOps->>TensorPrims: TensorPrimitives.Add(aSpan,bSpan,dstSpan)
        TensorPrims-->>FloatOps: result
    end
    FloatOps-->>Caller: result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

diffusion

Poem

🐰 I cloned arrays, I cached with care,

OneDNN hums in the chilly air,
In-place hops and parallel springs,
Raw spans race while the benchmark sings,
A rabbit cheers for faster things!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main objective: adding oneDNN binary primitives for add/multiply operations to achieve performance parity with TorchSharp. It is concise, specific, and highlights the primary change.
Description check ✅ Passed The description comprehensively explains the changes including oneDNN binary primitive support, TryAdd/TryMultiply implementation, parallel Sum, threshold updates, and benchmarks. It is directly related to the changeset and provides meaningful context.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf/onednn-binary-primitives

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the feature Feature work item label Jan 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds oneDNN binary primitive support for element-wise addition and multiplication operations to achieve performance parity with TorchSharp. The implementation includes caching of oneDNN primitives, parallel Sum implementation, and optimization of FloatOperations to use oneDNN for large arrays (>=50K elements).

Changes:

  • Added oneDNN binary primitives (TryAdd, TryMultiply) with primitive caching to OneDnnProvider
  • Implemented parallel Sum with lock-based reduction for large arrays in FloatOperations
  • Lowered ParallelThreshold from 65536 to 50000 to enable parallelism for 100K+ element arrays
  • Added raw TensorPrimitives benchmarks for baseline performance comparison

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/AiDotNet.Tensors/NumericOperations/FloatOperations.cs Integrated oneDNN for Add/Multiply operations on large arrays, added parallel Sum implementation, updated ParallelThreshold to 50000
src/AiDotNet.Tensors/Helpers/OneDnnProvider.cs Added binary primitive support (TryAdd, TryMultiply), eltwise operations (TryReLU, TrySigmoid), and caching infrastructure for both primitive types
src/AiDotNet.Tensors/Helpers/CpuParallelSettings.cs Updated ParallelThreshold to 50000, refactored ParallelForChunks with improved chunking logic and null validation
src/AiDotNet.Tensors/Engines/CpuEngine.cs Added oneDNN fast-path for SigmoidInPlace and ReLUInPlace operations with conditional compilation for non-NETFRAMEWORK targets
AiDotNetBenchmarkTests/TorchSharpCpuComparisonBenchmarks.cs Added raw TensorPrimitives benchmarks for direct performance comparison with AiDotNet and TorchSharp implementations

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/AiDotNet.Tensors/Helpers/OneDnnProvider.cs Outdated
Comment thread src/AiDotNet.Tensors/Helpers/OneDnnProvider.cs Outdated
Comment thread src/AiDotNet.Tensors/NumericOperations/FloatOperations.cs Outdated
Comment thread AiDotNetBenchmarkTests/TorchSharpCpuComparisonBenchmarks.cs Outdated
Comment thread src/AiDotNet.Tensors/NumericOperations/FloatOperations.cs
Comment thread src/AiDotNet.Tensors/NumericOperations/FloatOperations.cs
Comment thread src/AiDotNet.Tensors/NumericOperations/FloatOperations.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/AiDotNet.Tensors/Helpers/CpuParallelSettings.cs (1)

62-86: Validate minChunkSize to avoid divide-by-zero.
Public callers can pass minChunkSize <= 0, which will break the new chunk math. Guard early.

🛠️ Proposed fix
     if (action is null)
         throw new ArgumentNullException(nameof(action));
+
+    if (minChunkSize <= 0)
+        throw new ArgumentOutOfRangeException(nameof(minChunkSize), "minChunkSize must be > 0.");
🤖 Fix all issues with AI agents
In `@src/AiDotNet.Tensors/NumericOperations/FloatOperations.cs`:
- Around line 929-947: The OneDnn unsafe path in Add (and similarly in Multiply)
uses fixed pointers into ReadOnlySpan/Span without validating that x.Length ==
y.Length == destination.Length, risking memory corruption; before entering the
NET8_0 block and before calling OneDnnProvider.TryAdd/TryMultiply, validate the
three span lengths (x, y, destination) and throw a clear
ArgumentException/ArgumentOutOfRangeException on mismatch (same validation
TensorPrimitives would have done), then proceed to the
OneDnnProvider.TryAdd/TryMultiply and fallback to TensorPrimitives.Add/Multiply
as written.
🧹 Nitpick comments (1)
src/AiDotNet.Tensors/Engines/CpuEngine.cs (1)

4118-4142: Consider extracting the oneDNN fast-path boilerplate.

The memory-marshaling and fixed-pointer pattern is duplicated between SigmoidInPlace and ReLUInPlace. If more unary activations are added (e.g., Tanh, LeakyReLU), this will grow.

A helper could reduce duplication:

♻️ Optional helper to reduce boilerplate
// Helper method (private)
private unsafe bool TryOneDnnUnary<T>(Tensor<T> tensor, Func<float*, int, bool> op)
{
    if (typeof(T) != typeof(float) || !OneDnnProvider.IsAvailable)
        return false;

    var floatMem = (Memory<float>)(object)tensor.Data;
    if (MemoryMarshal.TryGetArray((ReadOnlyMemory<float>)floatMem, out var segment) &&
        segment.Array is not null && segment.Offset == 0)
    {
        fixed (float* ptr = segment.Array)
        {
            return op(ptr, tensor.Length);
        }
    }
    return false;
}

// Usage in SigmoidInPlace:
if (TryOneDnnUnary(tensor, OneDnnProvider.TrySigmoid))
    return;

// Usage in ReLUInPlace:
if (TryOneDnnUnary(tensor, OneDnnProvider.TryReLU))
    return;

Comment thread src/AiDotNet.Tensors/NumericOperations/FloatOperations.cs

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
15 Security Hotspots
3.6% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

ooples and others added 3 commits January 20, 2026 07:45
…with torchsharp

- Add oneDNN binary primitive support (dnnl_binary_add, dnnl_binary_mul)
- Implement TryAdd and TryMultiply methods in OneDnnProvider with caching
- Add parallel Sum implementation with lock for combining partial results
- Optimize FloatOperations to use oneDNN for large arrays (>=50K elements)
- Add CpuParallelSettings for configurable parallel threshold
- Add raw TensorPrimitives benchmarks for baseline comparison

Benchmark results:
- TensorAdd (1M): 19.76μs vs TorchSharp 20.01μs - PARITY/WIN
- TensorMultiply (1M): 20.41μs vs TorchSharp 19.22μs - PARITY
- TensorAdd (100K): 9.99μs vs TorchSharp 9.18μs - PARITY
- TensorMultiply (100K): 10.90μs vs TorchSharp 8.47μs - Close
- TensorSum (1M): 23.54μs vs TorchSharp 31.57μs - WIN 34%
- TensorMean (1M): 25.80μs vs TorchSharp 38.93μs - WIN 51%

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix parallel Sum lock contention by using array of partial sums instead of
  shared variable with lock. Each thread writes to its own slot, eliminating
  contention, and sums are combined sequentially after parallel work completes.
- Add documentation noting floating-point non-determinism in parallel Sum
- Fix cache eviction race condition in OneDnnProvider by adding entry first,
  then evicting if over limit. Uses GetOrAdd for atomic insertion and handles
  duplicate creation by disposing the duplicate.
- Add span length validation to Add and Multiply methods before calling unsafe
  oneDNN operations to prevent memory corruption with mismatched span lengths.
- Fix benchmark in-place modification by using separate destination array
  instead of writing back to source array which modified test data.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add [MethodImpl(MethodImplOptions.NoInlining)] attribute to MKL.NET
helper methods to prevent JIT from trying to load MKL.NET assembly
types when the containing method is compiled. This fixes
FileNotFoundException on platforms where MKL.NET is not available.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@ooples
ooples force-pushed the perf/onednn-binary-primitives branch from 700468c to b5a29c4 Compare January 20, 2026 12:57
@coderabbitai coderabbitai Bot added the diffusion Diffusion pipelines and schedulers label Jan 20, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/AiDotNet.Tensors/Helpers/OneDnnProvider.cs (1)

114-134: Fix incorrect format tag constants.

The format tag constants DnnlFormatTagNCHW and DnnlFormatTagOIHW are set to 11, but they should be 5. In oneDNN, both NCHW (4D activations) and OIHW (4D weights) map to the dnnl_abcd format tag, which has the numeric value 5.

The binary constants (DnnlBinaryAdd = 0x1fff0, DnnlBinaryMul = 0x1fff1) and argument indices (SRC=1, DST=17, WEIGHTS=33, SRC_1=2, SCRATCHPAD=80) are correct and match oneDNN's dnnl_types.h.

@ooples
ooples merged commit 52372bf into master Jan 20, 2026
37 of 38 checks passed
@ooples
ooples deleted the perf/onednn-binary-primitives branch January 20, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

diffusion Diffusion pipelines and schedulers feature Feature work item

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants