Don't throw from TensorPrimitives.Clamp when min is greater than max - #130703
Merged
tannergooding merged 5 commits intoJul 15, 2026
Merged
Conversation
The span-based Clamp overloads dispatch to the ClampOperator* structs whose scalar Invoke previously called T.Clamp, which throws ArgumentException when min > max, while the Vector128/256/512 Invoke paths call Vector*.Clamp, which follow HLSL and compute Min(Max(x, min), max) without validation. This made behavior position-dependent: an unordered min[i] > max[i] pair threw when it landed in the scalar remainder but was silently clamped in the vectorized region. The Clamp(x, T min, T max) overload additionally validated min <= max up front and threw before dispatch. Align the scalar path with the vector path by computing Min(Max(x, min), max) in the scalar Invoke and removing the up-front validation, so Clamp succeeds for unordered bounds regardless of element position, matching Vector.Clamp. Remove the now-unused ThrowArgument_MinGreaterThanMax helper and its resource string, and update the doc comments. Add regression tests that exercise both the scalar and vectorized paths across the affected overloads and types, including NaN and signed-zero parity between the two paths for the floating-point types. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 3 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @dotnet/area-system-numerics |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR changes TensorPrimitives.Clamp to avoid throwing when min > max by aligning the scalar fallback behavior with the vectorized Vector128/256/512.Clamp semantics (compute Min(Max(x, min), max)), and updates documentation/tests accordingly.
Changes:
- Remove the scalar
min > maxvalidation and switch scalarClampOperator*implementations toT.Min(T.Max(x, min), max). - Delete the now-unused throw helper and resource string for the removed
ArgumentException. - Add generic tests to ensure scalar vs vectorized paths match (including floating-point special values like NaN and signed zero).
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.Clamp.cs | Removes min > max throwing and makes scalar operator semantics match vector clamp; updates XML docs. |
| src/libraries/System.Numerics.Tensors/src/System/ThrowHelper.cs | Removes unused ThrowArgument_MinGreaterThanMax. |
| src/libraries/System.Numerics.Tensors/src/Resources/Strings.resx | Removes unused Argument_MinGreaterThanMax resource string. |
| src/libraries/System.Numerics.Tensors/tests/TensorPrimitives.Generic.cs | Adds regression tests validating consistent clamp semantics across scalar/vectorized paths and special values. |
Copilot's findings
- Files reviewed: 4/4 changed files
- Comments generated: 1
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The scalar ClampOperator Invoke previously computed Min(Max(x, min), max) for every T, which silently dropped the min > max validation even for types with no vector acceleration. Gate the bypass on Vector128<T>.IsSupported so vector-accelerated types match the non-throwing Vector.Clamp path while all other types defer to T.Clamp and keep throwing when min > max. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… comments Restore an ArgumentException note on the Clamp overloads describing that unordered bounds throw for a non-vectorizable T, and reword two test comments that misstated the vector-width and Half code paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
TensorPrimitives.Clamp vectorizes Half via the Half-as-Int16 path, so its scalar fallback must also compute Min(Max(x, min), max) instead of deferring to T.Clamp; otherwise Half would throw on unordered bounds only for short spans (below the reinterpret threshold) or when hardware acceleration is unavailable. Also add length 129 to cover the vectorized-plus-scalar-remainder mix in a single call. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
eiriktsarpalis
approved these changes
Jul 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The span-based
TensorPrimitives.Clampoverloads dispatch to theClampOperator*structs whose scalarInvokecallsT.Clamp, which throwsArgumentExceptionwhenmin > max, while theVector128/256/512Invokepaths callVector*.Clamp, which follow HLSL and computeMin(Max(x, min), max)without validation.This made behavior position-dependent for the span-min/span-max overloads: an unordered
min[i] > max[i]pair threw when it landed in the scalar remainder but was silently clamped when it landed in the vectorized region. TheClamp(ReadOnlySpan<T> x, T min, T max, ...)overload additionally validatedmin <= maxup front and threw before dispatch.Align the scalar path with the vector path by computing
Min(Max(x, min), max)in the scalarInvokeand removing the up-front validation, soClampsucceeds for unordered bounds regardless of element position, matchingVector.Clamp.T.Min(T.Max(x, min), max)is exactlyT.Clampminus the throw, and (via IEEEMin/Max) agrees with the vector path onNaNand signed zeros by construction.Also removes the now-unused
ThrowArgument_MinGreaterThanMaxhelper and its resource string, and updates the doc comments.Tests: adds
Clamp_UnorderedBoundsMatchScalarSemanticsandClamp_SpecialValuesMatchScalarSemanticstoGenericNumberTensorPrimitivesTests<T>, exercising both the scalar (sub-vector lengths) and vectorized (length 128) paths across all affected overloads/operand orderings and types, includingNaNand signed-zero parity between the two paths for the floating-point types.Note
This is a documented-behavior change: the previously documented
ArgumentExceptiononmin > maxis removed, so it warrants breaking-change consideration.Note
This PR was authored with GitHub Copilot.