Arm64 SVE: Support scalable constant vectors and masks#127520
Conversation
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
4d39083 to
4754486
Compare
Adds support to GenTreeVecCon and GenTreeMskCon for constants with unknown sizes. Instead of having a blob of data, the constant is represented as being one of either: a repeated value, an sequence with start and step values, or a value in the first lane and the rest zeroed. To handle this the base type is also required. As this new structure is slightly bigger than a simd16, the simd_t typedef is pushed up to simd32 sized. For vector constants, a vector is scalable because if it is of TYP_SIMD. For mask constants, the type is always TYP_MASK. However on Arm64, masks are only used by SVE. Therefore to tell if a mask is scalable then JitUseScalableVectorT is checked. The IsAllBitsSet() on mask constants is updated to include a base type. A mask that is all set for TYP_LONG will not be all set for TYP_BYTE, and instead will be 100010001000... Given two scalable constants it may not be possible to add them together to produce a third scalable constant. Instead they will remain as two vectors in the IR. To show this implementation is workable, scalable support is added for: Sve.CreateTrueMask*() Sve.CreateFalseMask*() Vector.Create() Vector.CreateScalar() Vector.CreateScalarUnsafe() Vector.CreateSequence() Fixes dotnet#125057
4754486 to
7fac1f9
Compare
|
Taking this out of draft now. Because of the very limited support for scalable SVE, this is currently very hard to test. I've been working off the top of @snickolls-arm's WIP branch with all his code in, which allows me to to call handwritten tests. In current HEAD, there are too many errors before getting to my code. There's still a lot of work to do on top of this. Eg, I need to get generic ops working, plus all the other Vector APIs which create constants. But, I didn't want this PR to grow too big. The important part is this serves as a base for further constant work. @dotnet/arm64-contrib @jakobbotsch @tannergooding |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR adds Arm64 SVE “scalable VectorT” support across the JIT, including new encodings for scalable vector/mask constants and updates to value numbering, folding, lowering, LSRA, and codegen to recognize and emit SVE-friendly patterns.
Changes:
- Introduce new scalable constant representations (
simdscalable_t,simdmaskscalable_t) and plumb them throughGenTreeconstant nodes and hashing. - Extend value numbering and folding to create/consume scalable SIMD constants on Arm64.
- Implement Arm64 SVE
VectorTintrinsics import and codegen pathways (create/broadcast/sequence), plus mask handling updates.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/jit/valuenum.h | Adds VN support for scalable SIMD constants on Arm64 |
| src/coreclr/jit/valuenum.cpp | Creates/broadcasts scalable SIMD VN constants and dumps them |
| src/coreclr/jit/simd.h | Defines new scalable vector/mask constant encodings and helper APIs |
| src/coreclr/jit/simd.cpp | Implements scalable vector/mask helpers and conversions |
| src/coreclr/jit/lsraarm64.cpp | Reserves temps for scalable vector constants that can’t be directly encoded |
| src/coreclr/jit/lowerarmarch.cpp | Updates mask lowering + VectorT intrinsic handling |
| src/coreclr/jit/hwintrinsiclistarm64sve.h | Enables VectorT intrinsics for SVE |
| src/coreclr/jit/hwintrinsiccodegenarm64.cpp | Emits SVE instructions for VectorT intrinsics |
| src/coreclr/jit/hwintrinsicarm64.cpp | Imports VectorT intrinsics and updates true/false mask creation |
| src/coreclr/jit/hwintrinsic.h | Marks VectorT_* as special cases for scalar/broadcast creation |
| src/coreclr/jit/gentree.h | Extends vector/mask constants to support scalable encodings |
| src/coreclr/jit/gentree.cpp | Adds scalable constant construction, hashing, folding, and printing |
| src/coreclr/jit/emitarm64.h | Repositions signed-immediate helpers used by new SVE paths |
| src/coreclr/jit/compiler.hpp | Extends bitmask helpers for >64-register targets |
| src/coreclr/jit/compiler.h | Adds new compiler helpers for scalable vector/mask constants |
| src/coreclr/jit/codegenarm64.cpp | Adds emission for scalable vector/mask constants |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/coreclr/jit/gentree.cpp:1
- The
printElementlambda ignores itselementparameter due to shadowing (e.g.,uint8_t element = ...), and the sequence/scalar element computation is incorrect (index + index*stepand scalar zeroing based on the value rather than the element index). This will print wrong values for scalable constants (including printing scalar vectors as all zeros when the scalar is non-zero). Rename the parameter (e.g.,elementIndex), avoid shadowing, and computevalue = index + step * elementIndex(and scalar should returnindexonly forelementIndex == 0).
src/coreclr/jit/gentree.cpp:1 simdscalable_t::operator==treats all “zero” encodings as equal (viaIsZero()canonicalization), but this hash includes kind/baseType/step bits unconditionally. That breaks the hash/equality contract for cases like{kind=Sequence, index=0, step=0}vs{kind=Repeated, index=0, step=0}which compare equal but will hash differently, potentially degrading or breaking CSE/lookup logic based ongtHashValue. Canonicalize “zero” before hashing (e.g., ifsimdVal.IsZero()then hash only a canonical representation, similar to the VN-map hash code path).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/coreclr/jit/gentree.cpp:20504
- TryEvaluateUnaryInPlace has a control-flow path that doesn't return a value: the
defaultcase callsunreached()but then falls through without returning. Even ifunreached()is treated as noreturn in some builds, making the return explicit avoids UB/warnings and keeps the function contract clear.
| if (varTypeIsIntegral(simdBaseType)) | ||
| { | ||
| scalableVecCon->gtSimdScalableVal.gtSimdScalableIndex = | ||
| static_cast<uint64_t>(op1->AsIntConCommon()->IntegralValue()); | ||
| } |
| if (varTypeIsIntegral(simdBaseType)) | ||
| { | ||
| uint16_t cnsVal = static_cast<uint16_t>(op1->AsIntConCommon()->IntegralValue()); | ||
| vecCon->gtSimdVal.u16[0] = cnsVal; | ||
| break; | ||
| scalableVecCon->gtSimdScalableVal.gtSimdScalableIndex = | ||
| static_cast<uint64_t>(op1->AsIntConCommon()->IntegralValue()); | ||
| } |
| if (varTypeIsIntegral(simdBaseType)) | ||
| { | ||
| scalableVecCon->gtSimdScalableVal.gtSimdScalableIndex = | ||
| static_cast<uint64_t>(op1->AsIntConCommon()->IntegralValue()); | ||
| } |
| scalableVecCon->gtSimdScalableVal.gtSimdScalableIndex = | ||
| static_cast<uint64_t>(op1->AsIntConCommon()->IntegralValue()); | ||
| scalableVecCon->gtSimdScalableVal.gtSimdScalableStep = | ||
| static_cast<uint64_t>(op2->AsIntConCommon()->IntegralValue()); |
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "a7f17a70a3191b5f04900e39a3248e7148afebb7",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "c473d80e67a0e090fa4a3e67fbc5bc9d122b172a",
"last_reviewed_commit": "a7f17a70a3191b5f04900e39a3248e7148afebb7",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "c473d80e67a0e090fa4a3e67fbc5bc9d122b172a",
"last_recorded_worker_run_id": "29679155775",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "a7f17a70a3191b5f04900e39a3248e7148afebb7",
"review_id": 4730529285
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: The PR adds vector-length-agnostic constant support for Arm64 SVE (Fixes #125057). Previously scalable SIMD constants and predicate masks could not be represented, because a fixed-size data blob presumes a known vector width. This blocks constant folding and efficient codegen for Sve.CreateTrue/FalseMask* and Vector.Create/CreateScalar/CreateScalarUnsafe/CreateSequence on scalable types. The motivation is real and well-scoped.
Approach: Instead of a data blob, scalable constants are modeled abstractly via a new simdscalable_t (baseType + kind Repeated/Sequence/Scalar + 64-bit index/step unions) and simdmaskscalable_t (baseType + 0/1 index). The simd_t typedef is widened to simd32_t on Arm64 (guarded by static_assert(sizeof(simd_t) >= sizeof(simdscalable_t))) so existing storage can hold the new struct. Value numbering gains a simdmaskvalue_t wrapper plus new VN maps and specializations, codegen (genSetRegToConst TYP_SIMD) emits SVE dup/index/fmov/ins with a data-section fallback, and LSRA reserves internal registers when the constant isn't immediate-encodable. Crucially, scalable mask behavior is gated behind the DEBUG-only JitConfig.JitUseScalableVectorT(), which meaningfully limits release-codegen risk; scalable vector constants (TYP_SIMD) are wired in more broadly. The EvaluateUnaryInPlace → TryEvaluateUnaryInPlace (bool) refactor correctly threads fold-failure early-outs through all callers, since scalable constants cannot always be folded. The abstraction is clean and the layering is sensible.
Summary:
Detailed Findings
✅ Try* fold-failure threading is correct. EvaluateUnaryInPlace → TryEvaluateUnaryInPlace and gtFoldExprConvertVecCnsToMask returning GenTree* (original tree on failure) are handled at all call sites (morph.cpp x4, gtFoldExprHWIntrinsic) with proper early-outs; the caller only consumes the result when conversion succeeds.
✅ genFindLowestBit/genMaxOneBit predicate-register handling (compiler.hpp). These are changed from asserting the high mask is RBM_NONE to actually handling high registers under HAS_MORE_THAN_64_REGISTERS. This is a genuine behavioral change enabling predicate-register masks and appears correct and necessary; flagging it as a focus area for the human reviewer since it affects register-mask math beyond the scalable-constant paths.
✅ emitarm64.h isValidSimm hardening. The relocation/shift now guards against undefined-behavior shift overflow when bits == ssize_t bit-width — a real improvement.
💡 Zero-canonicalization equivalence (simd.h/simd.cpp). simdscalable_t::operator== and the VN hash canonicalize any IsZero() value equal regardless of baseType/kind, and a Sequence with index 0 / step 0 is treated as zero. This equivalence (zero-step sequence == repeated-zero) is semantically sound, but it is load-bearing for VN correctness and deduplication; worth a maintainer's confirmation that no consumer distinguishes those representations by baseType.
💡 FP sequence path. gtNewSimdCreateSequenceNode folds integral sequences to a scalable Sequence constant but keeps the FP path as an explicit indices*step+broadcast expression (codegen asserts integral for the sequence form). This appears intentional; noting it so the reviewer confirms the FP path is covered by existing tests.
💡 Test coverage. Per the repo's JIT guidance, new regression tests are not expected for mechanical/infra refactors, and differential testing covers codegen — so this is not a blocker. Given the breadth of new scalable codegen, confirming green SVE CI (and Antigen/Fuzzlyn differential runs) before merge is the primary validation.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 270.4 AIC · ⌖ 11.1 AIC · ⊞ 10K
Adds support to GenTreeVecCon and GenTreeMskCon for constants with unknown sizes. Instead of having a blob of data, the constant is represented as being one of either: a repeated value, an sequence with start and step values, or a value in the first lane and the rest zeroed. To handle this the base type is also required.
As this new structure is slightly bigger than a simd16, the simd_t typedef is pushed up to simd32 sized.
For vector constants, a vector is scalable because if it is of TYP_SIMD.
For mask constants, the type is always TYP_MASK. However on Arm64, masks are only used by SVE. Therefore to tell if a mask is scalable then JitUseScalableVectorT is checked.
The IsAllBitsSet() on mask constants is updated to include a base type. A mask that is all set for TYP_LONG will not be all set for TYP_BYTE, and instead will be 100010001000...
Given two scalable constants it may not be possible to add them together to produce a third scalable constant. Instead they will remain as two vectors in the IR.
To show this implementation is workable, scalable support is added for:
Fixes #125057