Wasm RelaxedSimd intrinsics experiment#130050
Conversation
Adds System.Runtime.Intrinsics.Wasm.RelaxedSimd as a sibling class
to PackedSimd, exposing the 19 opcodes from the WebAssembly Relaxed
SIMD proposal (Phase 5; shipped in Chrome 114+, Firefox 120+, V8,
and Node 22+).
Class shape mirrors the AVX10 precedent (sibling, not subclass) for
two reasons:
1. Method-name overlap with PackedSimd on Swizzle/Min/Max has
intentionally different semantics (relaxed = implementation-
defined for out-of-domain inputs, deterministic on PackedSimd).
Inheritance would silently shadow the deterministic operation.
2. The relaxed-SIMD proposal is its own feature flag at the engine
level, so a distinct IsSupported makes the gate explicit.
Method naming follows the PackedSimd idiom (MultiplyAdd not Fma,
ConvertToInt32 not TruncateToInt32, DotProduct to disambiguate from
PackedSimd.Dot which is the standard signed-i16 path).
The relaxed dot-product overloads take Vector128<byte> by
Vector128<sbyte> directly, encoding the spec's i7 constraint at
the type-system level.
This commit only adds the managed API surface (RelaxedSimd.cs,
RelaxedSimd.PlatformNotSupported.cs, ref source, tests). Runtime
wiring (Mono SIMD intrinsic recognizer, LLVM/AOT codegen,
WasmEnableRelaxedSimd MSBuild property, IsSupported runtime
detection) follows in stacked commits.
API proposal draft: docs/ at ~/.copilot/session-state/.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Match Vector128.MultiplyAddEstimate's naming for the 'may or may not be true FMA' semantic. The Wasm relaxed_madd / relaxed_nmadd ops are exactly that: the runtime may or may not emit a true fused multiply add depending on the host. FusedMultiplyAdd remains reserved for guaranteed-fusion (Avx512F, AdvSimd.Fma, etc.). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduces the build-time switch for the Wasm Relaxed SIMD extension (API surface added in the previous commit). When the property is set to true, the build: - threads -mrelaxed-simd to emcc (BrowserWasmApp.targets) - threads --enable-relaxed-simd to wasm-opt (WasmApp.Common.targets) - threads mattr=+relaxed-simd to the AOT compiler (likewise) - sets WASM_ENABLE_RELAXED_SIMD=1 in the emscripten env - includes ILLink.Substitutions.WasmRelaxedSimd.xml so that RelaxedSimd.get_IsSupported is stubbed to true by the trimmer When the property is false (default), the parallel ILLink.Substitutions.NoWasmRelaxedSimd.xml stubs IsSupported to false. The test infra in eng/testing/tests.wasm.targets mirrors the substitution wiring for the BuildAOTTestsOnHelix=true path that sidesteps BrowserWasmApp.targets. Validation that WasmEnableRelaxedSimd=true requires WasmEnableSIMD=true is performed at build time (in the _WasmCommonPrepareForWasmBuildNative target) with a clear error message. Default is false everywhere. The browser-wasm flavor will likely want to flip to true once the runtime wiring (Mono SIMD intrinsic recognizer + LLVM intrinsic table + interpreter handlers) lands in subsequent commits. Validated locally with both ./build.sh mono+libs -os browser -c Release (default off) and the same with /p:WasmEnableSIMD=true /p:WasmEnableRelaxedSimd=true. Both succeed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 13 INTRINS_* entries for the WebAssembly relaxed-SIMD opcodes, mapping to LLVM's Intrinsic::wasm_relaxed_* function declarations: - relaxed swizzle (i8x16) - relaxed truncating float-to-int (f32x4 and f64x2->i32x4) - relaxed multiply-add / negated multiply-add (f32x4, f64x2) - relaxed lane select (i8x16, i16x8, i32x4, i64x2) - relaxed min / max (f32x4, f64x2) - relaxed q15mulr signed - relaxed dot i8x16 i7x16 signed - relaxed dot i8x16 i7x16 add signed Non-overloaded ops use INTRINS(...) (single fixed signature in LLVM); overloaded ones (madd, nmadd, laneselect, min, max) use INTRINS_OVR_TAG(...) with element-width tag bitmasks so the existing overloaded-intrinsic registration path picks the correct concrete type at call time. This commit is no-op on its own: nothing references INTRINS_WASM_ RELAXED_* yet. The Mono SIMD intrinsic recognizer wiring in the next commit will dispatch RelaxedSimd.* methods to these IDs. Build verified: ./build.sh mono+libs -os browser -c Release passes with mono-aot-cross successfully linked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pieces 2 + 3 of the relaxed-SIMD support: declares the
MONO_CPU_WASM_RELAXED_SIMD CPU feature, recognizes the
'+relaxed-simd' mattr passed by the AOT compiler, advertises the
feature to the LLVM CPU-features query, and dispatches the
RelaxedSimd.* managed methods to the LLVM intrinsics added in the
previous commit.
Specifics:
- mini.h: add MONO_CPU_WASM_RELAXED_SIMD = 1 << 3.
- aot-compiler.c: parse 'relaxed-simd' (with optional +/- prefix)
in mattr arguments.
- mini-llvm.c flags_map: advertise the feature so the runtime
reports it when the AOT'd code is loaded.
- simd-methods.h: register the four new method names not previously
used elsewhere — DotProductAdd, LaneSelect,
MultiplyAddNegatedEstimate, MultiplyRoundedQ15. (DotProduct,
MultiplyAddEstimate, Swizzle, Min, Max, ConvertToInt32,
ConvertToUInt32 reuse existing entries.)
- simd-intrinsics.c: new relaxedsimd_methods[] table mapping each
method to its (opcode, intrinsic-id) pair; register the group
under 'RelaxedSimd'/MONO_CPU_WASM_RELAXED_SIMD; add an
emit-wasm-supported-intrinsics block to dispatch the type-
discriminated ConvertToInt32/UInt32 overloads (float vs double).
- mini-llvm.c: hoist case OP_XOP_X_X_X_X out of the TARGET_ARM64
block (DotProductAdd is the first non-arm consumer); keep the
INTRINS_AARCH64_SHA1{C,M,P} bits under TARGET_ARM64.
End-to-end validation on browser-wasm AOT (Chromium 143,
WasmEnableSIMD=true, WasmEnableRelaxedSimd=true):
System.Runtime.Intrinsics.Tests 13023/13023 passing including the
six new RelaxedSimdTests (DotProduct, DotProductAdd,
MultiplyAddEstimate, LaneSelect, Swizzle, IsSupportedReflects).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…library variant
Mirrors the existing PackedSimd library structure: the SDK ships three static
mono-wasm interp-simd library variants and the app build picks one based on its
SIMD selection (the existing WasmEnableSIMD switch plus the new WasmEnableRelaxedSimd):
libmono-wasm-nosimd.a (default) WasmEnableSIMD=false
libmono-wasm-simd.a -msimd128 WasmEnableSIMD=true (default when SIMD on)
libmono-wasm-relaxed-simd.a -msimd128 -mrelaxed-simd WasmEnableSIMD=true, WasmEnableRelaxedSimd=true
This avoids forcing a runtime-build-time choice (the previous
MonoWasmEnableRelaxedSimd flag, now removed) and keeps WasmEnableRelaxedSimd a
pure per-app property paralleling the rest of the relaxed-simd wiring (AOT mattr,
emcc -mrelaxed-simd, wasm-opt --enable-relaxed-simd, ILLink IsSupported stub).
How the .def routes the relaxed-simd entries:
* New INTERP_WASM_RELAXED_SIMD_INTRINSIC_V_{V,VV,VVV} macro family is set up in
the prelude of interp-simd-intrins.def. With HOST_WASM_RELAXED_SIMD defined it
aliases the regular INTERP_WASM_SIMD_INTRINSIC_V_{V,VV,VVV} macros, so the
relaxed-simd library generates real emscripten intrinsic wrappers and tables
with the real Wasm opcode values (0x100-0x113). Without the define the macros
route the table entries to per-arity stub functions
(_mono_interp_simd_relaxed_unsupported_{1,2,3}) and clear the Wasm-opcode slot,
so libmono-wasm-simd.a still exports all the same symbols with stable indices
and the jiterpreter falls back to the C helper (which asserts but should be
unreachable because RelaxedSimd.IsSupported reports false).
* transform-simd.c (in mono-ee-interp) compiles the relaxed entries
unconditionally through the same INTRINS_COMMON path that PackedSimd uses, so
the lookup table and MintSIMDOpsPP/PPP/PPPP enums in mintops.h have identical
layouts across both library flavors. A single mono-ee-interp.a links cleanly
against either variant.
* genmintops.py learns the new RELAXED macro names so the TypeScript SimdIntrinsic
enums and SimdInfo[] stay in sync.
* emit_sri_relaxedsimd in transform-simd.c keys RelaxedSimd.IsSupported and the
intrinsic dispatch off mono_interp_relaxed_simd_supported, an extern int
exported by both library variants (1 in relaxed, 0 in regular).
* Method-name collisions with PackedSimd (Swizzle, Min, Max, MultiplyAddEstimate,
ConvertToInt32, ConvertToUInt32) are sidestepped by prefixing the .def entry
names with 'Relaxed'; emit_sri_relaxedsimd prepends 'Relaxed' before lookup.
The public API surface keeps the unprefixed names.
* installer manifest learns the new libmono-wasm-relaxed-simd.a entry.
Jiterpreter wiring is fully data-driven and needs no TypeScript changes: the
existing emit_simd_3/emit_simd_4 fast path calls mono_jiterp_get_simd_opcode
which returns the relaxed-simd Wasm opcode value, and appendSimd encodes it
through appendULeb so the 2-byte LEB128 form for opcodes >= 0x80 is handled
correctly.
Validated on browser-wasm Release with Chromium 143:
* AOT, WasmEnableRelaxedSimd off: System.Runtime.Intrinsics.Tests
RelaxedSimdTests = 1 passed + 5 ConditionalFact skipped.
* AOT, WasmEnableRelaxedSimd on: full System.Runtime.Intrinsics suite
13023/13023 pass.
* Interp-only, both off and on: 1 passed + 5 skipped (interp path uses the
SDK's pre-built dotnet.native.wasm, which currently always links
libmono-wasm-simd.a; AOT is required to exercise the relaxed-simd helpers
end-to-end. Shipping a second dotnet.native.wasm variant for interp-only
apps is a future enhancement.)
| [CLSCompliant(false)] | ||
| public abstract partial class RelaxedSimd | ||
| { | ||
| public static bool IsSupported { get { throw null; } } | ||
| public static Vector128<sbyte> Swizzle(Vector128<sbyte> vector, Vector128<sbyte> indices) { throw null; } | ||
| public static Vector128<byte> Swizzle(Vector128<byte> vector, Vector128<byte> indices) { throw null; } | ||
| public static Vector128<int> ConvertToInt32(Vector128<float> value) { throw null; } | ||
| public static Vector128<uint> ConvertToUInt32(Vector128<float> value) { throw null; } | ||
| public static Vector128<int> ConvertToInt32(Vector128<double> value) { throw null; } | ||
| public static Vector128<uint> ConvertToUInt32(Vector128<double> value) { throw null; } | ||
| public static Vector128<float> MultiplyAddEstimate(Vector128<float> a, Vector128<float> b, Vector128<float> c) { throw null; } | ||
| public static Vector128<double> MultiplyAddEstimate(Vector128<double> a, Vector128<double> b, Vector128<double> c) { throw null; } | ||
| public static Vector128<float> MultiplyAddNegatedEstimate(Vector128<float> a, Vector128<float> b, Vector128<float> c) { throw null; } | ||
| public static Vector128<double> MultiplyAddNegatedEstimate(Vector128<double> a, Vector128<double> b, Vector128<double> c) { throw null; } | ||
| public static Vector128<sbyte> LaneSelect(Vector128<sbyte> left, Vector128<sbyte> right, Vector128<sbyte> mask) { throw null; } | ||
| public static Vector128<byte> LaneSelect(Vector128<byte> left, Vector128<byte> right, Vector128<byte> mask) { throw null; } | ||
| public static Vector128<short> LaneSelect(Vector128<short> left, Vector128<short> right, Vector128<short> mask) { throw null; } | ||
| public static Vector128<ushort> LaneSelect(Vector128<ushort> left, Vector128<ushort> right, Vector128<ushort> mask) { throw null; } | ||
| public static Vector128<int> LaneSelect(Vector128<int> left, Vector128<int> right, Vector128<int> mask) { throw null; } | ||
| public static Vector128<uint> LaneSelect(Vector128<uint> left, Vector128<uint> right, Vector128<uint> mask) { throw null; } | ||
| public static Vector128<long> LaneSelect(Vector128<long> left, Vector128<long> right, Vector128<long> mask) { throw null; } | ||
| public static Vector128<ulong> LaneSelect(Vector128<ulong> left, Vector128<ulong> right, Vector128<ulong> mask) { throw null; } | ||
| public static Vector128<float> Min(Vector128<float> left, Vector128<float> right) { throw null; } | ||
| public static Vector128<float> Max(Vector128<float> left, Vector128<float> right) { throw null; } | ||
| public static Vector128<double> Min(Vector128<double> left, Vector128<double> right) { throw null; } | ||
| public static Vector128<double> Max(Vector128<double> left, Vector128<double> right) { throw null; } | ||
| public static Vector128<short> MultiplyRoundedQ15(Vector128<short> left, Vector128<short> right) { throw null; } | ||
| public static Vector128<short> DotProduct(Vector128<byte> left, Vector128<sbyte> right) { throw null; } | ||
| public static Vector128<int> DotProductAdd(Vector128<byte> left, Vector128<sbyte> right, Vector128<int> accumulator) { throw null; } | ||
| } |
…n nosimd lib; wire WasiApp.targets Two CI regressions exposed on PR dotnet#130050: 1. browser.proj was still passing -DENABLE_WASM_RELAXED_SIMD=0 to the dotnet.native cmake configure based on a removed MonoWasmEnableRelaxedSimd property. Nothing under src/mono/browser/runtime/CMakeLists.txt consumes that variable, so CMake's 'Manually-specified variables were not used' diagnostic was elevated to a build error across all browser-wasm Build legs. Drop the dead line. 2. WASI's prebuilt dotnet.wasm always links libmono-wasm-nosimd.a (the runtime cmake never receives WasmEnableSIMD=true), but the new mono_interp_relaxed_simd_supported extern is referenced unconditionally from libmono-ee-interp.a under HOST_BROWSER || HOST_WASI. Definitions only lived in libmono-wasm-{simd,relaxed-simd}.a, leaving the WASI link with an undefined symbol. Provide a default '= 0' definition in interp-nosimd.c so every SIMD-library variant exports the symbol. Also mirror the browser app-build library selection into WasiApp.targets so WASI apps can opt into the relaxed-simd library variant when WasmEnableRelaxedSimd=true, matching the BrowserWasmApp.targets behavior. Validated locally: * browser-wasm Release mono+libs build clean. * wasi-wasm Release mono+libs build clean. * AOT smoke: System.Runtime.Intrinsics.Tests with WasmEnableSIMD=true and WasmEnableRelaxedSimd=true: 13023/13023 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The PR's third SIMD library variant (libmono-wasm-relaxed-simd.a) ships in the
runtime pack but is picked up by the broad '*.a' glob in {Browser,Wasi}App.targets
that scoops every static lib into _WasmNativeFileForLinking. The selection logic
sets _WasmSIMDLibToExclude to a semicolon-separated list of bare filenames and
then does:
<_WasmNativeFileForLinking
Remove="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)$(_WasmSIMDLibToExclude)" />
MSBuild splits the semicolons into items but only prefixes the FIRST one with the
runtime-pack directory; subsequent items become bare filenames that don't match
the full-path entries in _WasmNativeFileForLinking, so the second exclusion is
silently dropped and libmono-wasm-relaxed-simd.a leaks into every test app's link
line. The post-link wasm-opt then fails because it isn't invoked with
--enable-relaxed-simd:
[wasm-validator error in function _mono_interp_simd_wasm_i32x4_relaxed_trunc_f32x4]
unexpected false: all used features should be allowed, on
(i32x4.relaxed_trunc_f32x4_s ...)
[--enable-relaxed-simd]
Fatal: error validating input
Fix: embed the runtime-pack directory prefix into every value of
_WasmSIMDLibToExclude so the Remove attribute receives fully-qualified paths
that actually match the items added by the glob. Applied symmetrically to
BrowserWasmApp.targets and WasiApp.targets.
Validated locally on browser-wasm:
* Default (WasmEnableRelaxedSimd unset): System.Runtime.Intrinsics.Tests
RelaxedSimdTests = 1 passed + 5 ConditionalFact-skipped (was failing
wasm-opt validation pre-fix), full suite 13018 passed + 5 skipped.
* WasmEnableRelaxedSimd=true: full suite 13023/13023 pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| [CLSCompliant(false)] | ||
| public abstract partial class RelaxedSimd | ||
| { | ||
| public static bool IsSupported { get { throw null; } } | ||
| public static Vector128<sbyte> Swizzle(Vector128<sbyte> vector, Vector128<sbyte> indices) { throw null; } | ||
| public static Vector128<byte> Swizzle(Vector128<byte> vector, Vector128<byte> indices) { throw null; } |
|
@tannergooding relaxedsimd is standardized now, there is no rush but it would be nice to figure out the api details |
|
We should just need to get an API proposal up and it will be relatively straightforward to get in/approved. The surface in here looks generally correct at a glance, I'd just need to go through the full proposal against the finalized spec before marking it |
|
API proposal opened at #130223 (tracks the surface added in this PR). Marking this PR as dependent on the API-review outcome there. Note This comment was created with the assistance of GitHub Copilot. |
- Correct DotProduct/DotProductAdd operand types per the finished WebAssembly relaxed-simd spec pseudocode: operand `a` is signed and operand `b` is unsigned-7-bit, not the reversed (byte, sbyte) shape the prototype had. Updated the impl, PlatformNotSupported, ref, and tests. - Fix RelaxedSimd IsSupported reflection test: assert MethodInfo is non-null before invoking, so a lookup miss yields a diagnostic instead of NRE. - Replace float.Epsilon-based ULP-style tolerance in the MultiplyAddEstimate test with an explicit 1e-5 relative tolerance. float.Epsilon is the smallest subnormal (~1.4e-45), not the unit roundoff, so the previous check was effectively exact-equality. - Document the reflection-reach stack-overflow invariant in the Mono interp emit_sri_relaxedsimd path so future editors preserve the IsSupported gate contract, and note the general follow-up covering all wasm intrinsic classes. Addresses copilot-pull-request-reviewer feedback on dotnet#130050 and tracks the API-review corrections captured in dotnet#130223. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Pushed 7f99067 addressing the Copilot review feedback plus a spec-derived correction to
API-review issue #130223 updated with the corrected surface and prototype pointer. Note This comment was created with the assistance of GitHub Copilot. |
Experimental implementation of API proposed in #130223