Skip to content

Tensor<T> slice pinning starts at the parent array instead of the slice #134691

Description

@luisquintanilla

Description

A contiguous Tensor<long> slice returns different values through its pinning APIs than through its indexer or tensor span. With System.Numerics.Tensors 10.0.9, GetPinnableReference() and GetPinnedHandle() start at the parent array rather than the beginning of the slice.

Scenario

I'm prototyping a .NET ONNX text-embedding pipeline. I want to use tensors through preprocessing, inference, and postprocessing, including passing contiguous slices to native inference without another data copy.

The shape is correct, but the pointer addresses the wrong part of the data. That means a native consumer can receive different inputs without a shape error. The repro below removes ONNX and the rest of my application. It only needs System.Numerics.Tensors.

Parent [3,2]       Logical slice [2,2], starting at [1,0]
[91,92]           excluded
[11,12]           [11,12]  <- expected pointer start
[21,22]           [21,22]

Observed pointer starts at 91, not 11.

Configuration

  • Windows x64
  • .NET SDK 10.0.401
  • Runtime .NET 10.0.12
  • NuGet System.Numerics.Tensors 10.0.9
  • JIT, net10.0; NativeAOT explicitly disabled

Reproduction Steps

You can find the runnable file-based app and captured output in this gist. The complete code is also included below.

Save the following as TensorSlicePinning.cs in a new directory outside a repository. File-based apps inherit project settings from parent directories.

#:package System.Numerics.Tensors@10.0.9
#:property TargetFramework=net10.0
#:property AllowUnsafeBlocks=true
#:property PublishAot=false

using System.Numerics.Tensors;

long[] values = [91, 92, 11, 12, 21, 22];
long[] expected = [11, 12, 21, 22];
var parent = Tensor.Create(values, [3, 2]);
var slice = parent.Slice([1, 0]);
Console.WriteLine($"Runtime: {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}");
Console.WriteLine($"Shape: [{string.Join(",", slice.Lengths.ToArray())}]; strides: [{string.Join(",", slice.Strides.ToArray())}]");
Console.WriteLine($"Expected slice: [{string.Join(",", expected)}]");
Console.WriteLine($"Indexer first: {slice[0, 0]}");
Console.WriteLine($"Tensor span: [{string.Join(",", slice.AsTensorSpan().GetSpan([0, 0], 4).ToArray())}]");
Console.WriteLine($"Flattened: [{string.Join(",", slice.ToArray())}]");
Console.WriteLine($"Parent GetPinnableReference: {parent.GetPinnableReference()} (expected 91)");
long referenceFirst = slice.GetPinnableReference();
Console.WriteLine($"Slice GetPinnableReference: {referenceFirst} (expected 11)");
unsafe
{
    using var handle = slice.GetPinnedHandle();
    long[] pinned = new ReadOnlySpan<long>(handle.Pointer, 4).ToArray();
    Console.WriteLine($"Slice GetPinnedHandle: [{string.Join(",", pinned)}] (expected [{string.Join(",", expected)}])");
    bool matches = slice[0, 0] == 11 && slice.ToArray().SequenceEqual(expected)
        && slice.AsTensorSpan().GetSpan([0, 0], 4).SequenceEqual(expected)
        && referenceFirst == 11 && pinned.SequenceEqual(expected);
    Console.WriteLine(matches ? "PASS" : "FAIL: pinning does not start at the logical slice offset.");
    return matches ? 0 : 1;
}
dotnet run --file .\TensorSlicePinning.cs

Actual behavior

Compilation succeeds. Running the app produces the following output and exits with code 1:

Runtime: .NET 10.0.12
Shape: [2,2]; strides: [2,1]
Expected slice: [11,12,21,22]
Indexer first: 11
Tensor span: [11,12,21,22]
Flattened: [11,12,21,22]
Parent GetPinnableReference: 91 (expected 91)
Slice GetPinnableReference: 91 (expected 11)
Slice GetPinnedHandle: [91,92,11,12] (expected [11,12,21,22])
FAIL: pinning does not start at the logical slice offset.

Expected behavior

The reference and pointer should start at 11, the first element of the slice. Reading four elements should return [11,12,21,22], matching the indexer and tensor span. The app should exit with code 0.

Observation Expected Actual
Slice indexer, first element 11 11
Tensor-span and enumerated values [11,12,21,22] [11,12,21,22]
Slice GetPinnableReference() 11 91
Four values through slice GetPinnedHandle() [11,12,21,22] [91,92,11,12]
Process exit code 0 1

Other information

All pointer reads happen while the MemoryHandle is alive and stay within the allocated six-element array.

In the 10.0.9 source, Slice records _start, but GetPinnableReference() appears to return the array's first element without applying that offset. GetPinnedHandle() uses that reference. This looks related to the behavior above.

I've reproduced this on the configuration listed above. I haven't checked other versions or platforms.

Activity

  1. dotnet-policy-service commented on Sep 26, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/area-system-numerics-tensors
    See info in area-owners.md if you want to be subscribed.

  2. luisquintanilla commented on Sep 26, 2026

    @luisquintanilla
    Author

    I confirmed a downstream impact while validating the proposed ONNX Runtime fix in PR #25972, discussed in onnxruntime#25460.

    I'm building a .NET embeddings pipeline and exploring direct interoperability between System.Numerics.Tensors and ONNX Runtime. The proposed ORT bridge uses the public pinning API for dense tensors. This issue causes it to pass the correct shape but the wrong values to native inference when the tensor is a contiguous slice with a nonzero offset.

    For example, a [2,2] slice starting at element 2 of [91,92,11,12,21,22] produces native Identity output [91,92,11,12] instead of [11,12,21,22]. A Float32 slice exhibits the same problem.

    To isolate the cause, I built the original Tensors project at 5eaa18a, the runtime component revision corresponding to the 10.0.9 package. The unmodified source reproduces both failures. I then applied a local one-line candidate correction that includes _start when GetPinnableReference() returns the first-element reference.

    With the same ORT PR binary, native runtime, and inputs, the result changed from 15/17 to 17/17 correct native-inference outputs. Both contiguous-slice failures disappeared. The dense input still aliases the original slice, so this wasn't resolved by introducing a copy. The pin-handle ownership and disposal code remained unchanged.

    The experiment used ORT PR commit 3553884, released CPU native ORT 1.23.2, and .NET 10.0.12 on Windows x64. An additional 40 direct pinning probes passed with the candidate, covering roots, offset and nested slices, empty tensors, and pinned/unpinned storage.

    This is a local candidate, not an accepted or released runtime fix. It confirms that this issue blocks correct direct contiguous-slice interoperability in the proposed ORT bridge, rather than ONNX Runtime inference generally. Stable Memory binding remains a working alternative and passed every control case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions