You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The WebAssembly relaxed-SIMD proposal is a finished proposal integrated into the Wasm 3.0 draft (WG approval 2024-07-10). It is implemented in the major engines we target: V8, SpiderMonkey, JavaScriptCore, and Wasmtime. Relaxed SIMD adds a small set of 128-bit SIMD instructions whose observable behavior may differ across engines — trading strict determinism for direct mapping to native SIMD (FMA, pshufb/vqrdmulh/vdot, hardware min/max). These are the same instructions cross-platform intrinsics like Vector128.MultiplyAddEstimate and Vector128.ConvertToInt32Native were designed to expose, and they're exactly the instructions our own numerics/ML/codec paths need for cross-platform speedups on wasm.
We already surface System.Runtime.Intrinsics.Wasm.PackedSimd (approved in #53730, refined in #88092). This proposal adds a sibling System.Runtime.Intrinsics.Wasm.RelaxedSimd class following the same shape: IsSupported gate + static methods that map 1:1 to spec instructions. Callers opt in explicitly by naming the class, mirroring how X86 splits Sse2 vs Ssse3 and Wasm splits PackedSimd vs relaxed. Cross-platform code should still prefer Vector128.* helpers where they exist — this class is the low-level surface for PackedSimd-style consumers.
Prior art:
System.Runtime.Intrinsics.Wasm.PackedSimd — sibling class for deterministic wasm SIMD.
All eight browsers/runtimes finalized support in 2023–2024; the spec is stable.
Workarounds today:
Callers who need FMA on wasm currently write a * b + c and hope the JIT contracts it (Mono LLVM AOT sometimes does; interp/JIT do not). No equivalent path exists for relaxed_swizzle, relaxed_dot, relaxed_laneselect, or relaxed_q15mulr — those regress to scalar loops.
Vector128.MultiplyAddEstimate on wasm currently lowers to separate mul+add. This proposal is a prerequisite for wiring MultiplyAddEstimate to f32x4.relaxed_madd on wasm.
Related: This proposal has no known duplicates. #127665 (Wasm RyuJIT SIMD + Intrinsics tracking issue) references relaxed-SIMD as future work.
API Proposal
namespaceSystem.Runtime.Intrinsics.Wasm;[CLSCompliant(false)]publicabstractclassRelaxedSimd{publicstaticboolIsSupported{get;}// Swizzle — i8x16.relaxed_swizzle. Out-of-range indices produce an// implementation-defined value (PackedSimd.Swizzle zeroes them).publicstaticVector128<sbyte>Swizzle(Vector128<sbyte>vector,Vector128<sbyte>indices);publicstaticVector128<byte>Swizzle(Vector128<byte>vector,Vector128<byte>indices);// Float-to-int truncation — i32x4.relaxed_trunc_f{32x4,64x2}_{s,u}[_zero].// NaN and out-of-range inputs may produce any of the results the wasm// relaxed-trunc semantics permit — closest existing .NET contract is// Vector128.ConvertToInt32Native ("platform specific behavior on overflow"),// with the additional caveat that on wasm the specific "engine choice" is// per-embedding rather than per-ISA. Callers wanting portable behavior// should use PackedSimd.ConvertToInt32Saturate.publicstaticVector128<int>ConvertToInt32(Vector128<float>value);publicstaticVector128<uint>ConvertToUInt32(Vector128<float>value);publicstaticVector128<int>ConvertToInt32(Vector128<double>value);publicstaticVector128<uint>ConvertToUInt32(Vector128<double>value);// Multiply-add estimates — f{32x4,64x2}.relaxed_{madd,nmadd}.// Implementation may or may not fuse; matches Vector128.MultiplyAddEstimate.publicstaticVector128<float>MultiplyAddEstimate(Vector128<float>a,Vector128<float>b,Vector128<float>c);publicstaticVector128<double>MultiplyAddEstimate(Vector128<double>a,Vector128<double>b,Vector128<double>c);publicstaticVector128<float>MultiplyAddNegatedEstimate(Vector128<float>a,Vector128<float>b,Vector128<float>c);publicstaticVector128<double>MultiplyAddNegatedEstimate(Vector128<double>a,Vector128<double>b,Vector128<double>c);// Lane-select — i{8,16,32,64}x{16,8,4,2}.relaxed_laneselect.// Behavior of non-high mask bits is implementation-defined// (PackedSimd.BitwiseSelect examines every bit).publicstaticVector128<sbyte>LaneSelect(Vector128<sbyte>left,Vector128<sbyte>right,Vector128<sbyte>mask);publicstaticVector128<byte>LaneSelect(Vector128<byte>left,Vector128<byte>right,Vector128<byte>mask);publicstaticVector128<short>LaneSelect(Vector128<short>left,Vector128<short>right,Vector128<short>mask);publicstaticVector128<ushort>LaneSelect(Vector128<ushort>left,Vector128<ushort>right,Vector128<ushort>mask);publicstaticVector128<int>LaneSelect(Vector128<int>left,Vector128<int>right,Vector128<int>mask);publicstaticVector128<uint>LaneSelect(Vector128<uint>left,Vector128<uint>right,Vector128<uint>mask);publicstaticVector128<long>LaneSelect(Vector128<long>left,Vector128<long>right,Vector128<long>mask);publicstaticVector128<ulong>LaneSelect(Vector128<ulong>left,Vector128<ulong>right,Vector128<ulong>mask);// Min/Max — f{32x4,64x2}.relaxed_{min,max}.// NaN and signed-zero handling is implementation-defined// (PackedSimd.Min/Max are IEEE-conformant).publicstaticVector128<float>Min(Vector128<float>left,Vector128<float>right);publicstaticVector128<float>Max(Vector128<float>left,Vector128<float>right);publicstaticVector128<double>Min(Vector128<double>left,Vector128<double>right);publicstaticVector128<double>Max(Vector128<double>left,Vector128<double>right);// Q15 fixed-point multiply — i16x8.relaxed_q15mulr_s.// Saturation on 0x8000 * 0x8000 is implementation-defined// (PackedSimd.MultiplyRoundedSaturateQ15 always saturates).publicstaticVector128<short>MultiplyRoundedQ15(Vector128<short>left,Vector128<short>right);// Extended integer dot products — i16x8.relaxed_dot_i8x16_i7x16_s,// i32x4.relaxed_dot_i8x16_i7x16_add_s.// Per the finished spec: `a` is signed, `b` is unsigned 7-bit. When any lane// of `b` has the high bit set, that lane's product is implementation-defined// (may be interpreted as signed or unsigned). The sum reduction is also// implementation-defined-saturating (may or may not saturate on overflow).publicstaticVector128<short>DotProduct(Vector128<sbyte>left,Vector128<byte>right);publicstaticVector128<int>DotProductAdd(Vector128<sbyte>left,Vector128<byte>right,Vector128<int>accumulator);}
Prototype: lewing@7f99067dc5e (branch wasm-relaxed-simd-api on lewing/runtime; earlier revision at bfebb64b7042e0f1095a4d87ff42a3504f62e122)
API Usage
Cross-platform saturating FMA in a hot vector loop (mirrors Vector128.MultiplyAddEstimate shape):
staticVector128<float>Blend(Vector128<float>a,Vector128<float>b,Vector128<float>t){if(RelaxedSimd.IsSupported)returnRelaxedSimd.MultiplyAddEstimate(a,t,RelaxedSimd.MultiplyAddEstimate(-b,t,b));// Cross-platform helper already exists; on wasm it currently lowers to// separate mul+add. Wiring it to RelaxedSimd is the follow-up.returnVector128.MultiplyAddEstimate(a,t,Vector128.MultiplyAddEstimate(-b,t,b));}
Byte permutation with a computed table (relaxed swizzle enables Ssse3.Shuffle-shaped code paths on wasm):
staticVector128<byte>Pshufb(Vector128<byte>data,Vector128<byte>indices){if(RelaxedSimd.IsSupported)returnRelaxedSimd.Swizzle(data,indices);// may leave OOR lanes implementation-definedif(PackedSimd.IsSupported)returnPackedSimd.Swizzle(data,indices);// OOR lanes are zeroedreturnVector128.Shuffle(data,indices);}
Name the class Wasm.RelaxedSimd vs prefixing every method with Relaxed on PackedSimd. Chose a separate class so IsSupported gates the whole set (relaxed-SIMD requires a distinct engine feature flag), matching X86.Sse2 vs X86.Ssse3 and giving trimming a clean cut point. Not merging into PackedSimd also avoids expanding an already-large surface.
Reuse Vector128.MultiplyAddEstimate etc. rather than adding a class. Cross-platform helpers should stay the recommended API, but users of PackedSimd also need direct access (e.g., to reason about codegen, or when compiling with WasmEnableRelaxedSimd=false). Same argument the design has already accepted for PackedSimd living alongside Vector128.
Swizzle vs RelaxedSwizzle. PackedSimd already uses Swizzle; disambiguation happens at the class name. Same for Min/Max.
MultiplyRoundedQ15 vs MultiplyRoundedSaturateQ15. PackedSimd uses the latter (deterministic saturation on overflow); this class deliberately omits Saturate because saturation on 0x8000 * 0x8000 is the exact bit spec leaves implementation-defined. The name reflects the contract.
Open design questions worth reviewer input:
ConvertToInt32 vs ConvertToInt32Native. Cross-platform Vector128.ConvertToInt32Native uses the Native suffix to mark implementation-defined OOR/NaN behavior — exactly what i32x4.relaxed_trunc_f32x4_s provides. Renaming to ConvertToInt32Native / ConvertToUInt32Native would make it obvious that this class supplies the wasm implementation of the cross-platform "Native" contract. Tentative: rename to ConvertToInt32Native / ConvertToUInt32Native.
DotProduct operand types.The spec defines the right operand as 7-bit signed; passing an 8-bit value is implementation-defined. Per the finished spec pseudocode, operand a is signed and operand b is unsigned-7-bit — the current prototype had these reversed. The signatures above (sbyte left, byte right) match the spec. Reviewers should confirm the name DotProduct / DotProductAdd conveys enough about the implementation-defined summation behavior; adding a Saturate suffix would be misleading (may or may not saturate).
ConvertToInt32(Vector128<double>). The wasm instruction is i32x4.relaxed_trunc_f64x2_s_zero — it produces four int lanes with the upper two zeroed. Cross-platform helpers spell this as Narrow. Consider NarrowingConvertToInt32 for the double overloads (matches PackedSimd's ConvertNarrowingSaturate naming), or leave as an overload and document. Tentative: keep overloaded ConvertToInt32 and document.
Non-deterministic results across engines. By design. Users opting into RelaxedSimd.* accept implementation-defined behavior; documented per-method.
Overload resolution ambiguity. None expected: all methods are on a new class in a namespace already reserved for Wasm.* intrinsics.
TFM compatibility.System.Runtime.Intrinsics.Wasm types are net5.0+; RelaxedSimd is a new type in that namespace with no netstandard/netfx surface. No compatibility risk.
Binary/source breaking: none — this is pure addition.
Usage in dotnet/runtime
The *Native and *Estimate suffixes on cross-platform Vector128 helpers are already documented as "platform-specific" / "may or may not fuse". Lowering them to relaxed-SIMD on wasm does not stretch that contract — it satisfies it (compare: ConvertToInt32Native maps to cvttps2dq on x64 and fcvtzs on arm64, both platform-defined for OOR/NaN; i32x4.relaxed_trunc_f32x4_s is the wasm equivalent). Callers who need portable behavior already use Vector128.ConvertToInt32Saturate / PackedSimd.* instead.
Adoption in Vector128.* wasm lowerings
The most compelling in-tree consumers are Vector128.ShuffleNative and the Vector128.*Native/*Estimate helpers, whose current wasm lowerings emit portable-but-slower fallbacks even though their documented contracts already allow platform-specific behavior. Concrete examples:
Cross-platform helper
Current wasm lowering
Relaxed target
Vector128.ShuffleNative(byte)
falls through to PackedSimd.Swizzle (Vector128.cs:3534-3536) — deterministic zero-OOR
All the target intrinsics (INTRINS_WASM_RELAXED_*) are already declared and used by the RelaxedSimd.* class-path lowering added in this prototype; the follow-up is teaching the generic Vector128.* intrinsic recognizer to prefer them on wasm when the engine reports relaxed-SIMD support. RyuJIT wasm work (tracked in #127665) will get the same lowering table.
For ShuffleNative specifically: adopting RelaxedSimd.Swizzle on wasm is a behavior change for callers that today implicitly rely on PackedSimd.Swizzle's zero-OOR fallback. That's within ShuffleNative's documented contract, but is worth a compatibility scan of internal call sites before the companion PR lands.
Adoption strategy
Nothing is updated in this prototype commit — following the PackedSimd precedent (#53730 shipped without in-tree adoption; #88092 refined based on real use). Companion PRs wiring the lowerings above are straightforward and can land alongside if reviewers prefer.
Note
This API proposal was drafted with the assistance of GitHub Copilot.
Background and motivation
The WebAssembly relaxed-SIMD proposal is a finished proposal integrated into the Wasm 3.0 draft (WG approval 2024-07-10). It is implemented in the major engines we target: V8, SpiderMonkey, JavaScriptCore, and Wasmtime. Relaxed SIMD adds a small set of 128-bit SIMD instructions whose observable behavior may differ across engines — trading strict determinism for direct mapping to native SIMD (FMA,
pshufb/vqrdmulh/vdot, hardwaremin/max). These are the same instructions cross-platform intrinsics likeVector128.MultiplyAddEstimateandVector128.ConvertToInt32Nativewere designed to expose, and they're exactly the instructions our own numerics/ML/codec paths need for cross-platform speedups on wasm.We already surface
System.Runtime.Intrinsics.Wasm.PackedSimd(approved in #53730, refined in #88092). This proposal adds a siblingSystem.Runtime.Intrinsics.Wasm.RelaxedSimdclass following the same shape:IsSupportedgate + static methods that map 1:1 to spec instructions. Callers opt in explicitly by naming the class, mirroring how X86 splitsSse2vsSsse3and Wasm splitsPackedSimdvs relaxed. Cross-platform code should still preferVector128.*helpers where they exist — this class is the low-level surface forPackedSimd-style consumers.Prior art:
System.Runtime.Intrinsics.Wasm.PackedSimd— sibling class for deterministic wasm SIMD.Vector128.MultiplyAddEstimate,Vector128.ConvertToInt32Native— cross-platform helpers whose semantics deliberately match relaxed-SIMD instructions.Workarounds today:
a * b + cand hope the JIT contracts it (Mono LLVM AOT sometimes does; interp/JIT do not). No equivalent path exists forrelaxed_swizzle,relaxed_dot,relaxed_laneselect, orrelaxed_q15mulr— those regress to scalar loops.Vector128.MultiplyAddEstimateon wasm currently lowers to separatemul+add. This proposal is a prerequisite for wiringMultiplyAddEstimatetof32x4.relaxed_maddon wasm.Related: This proposal has no known duplicates. #127665 (Wasm RyuJIT SIMD + Intrinsics tracking issue) references relaxed-SIMD as future work.
API Proposal
Prototype: lewing@7f99067dc5e (branch
wasm-relaxed-simd-apionlewing/runtime; earlier revision atbfebb64b7042e0f1095a4d87ff42a3504f62e122)API Usage
Cross-platform saturating FMA in a hot vector loop (mirrors
Vector128.MultiplyAddEstimateshape):Byte permutation with a computed table (relaxed swizzle enables
Ssse3.Shuffle-shaped code paths on wasm):INT8 dot-product (ML inference / audio codecs):
Alternative Designs
Name the class
Wasm.RelaxedSimdvs prefixing every method withRelaxedonPackedSimd. Chose a separate class soIsSupportedgates the whole set (relaxed-SIMD requires a distinct engine feature flag), matchingX86.Sse2vsX86.Ssse3and giving trimming a clean cut point. Not merging intoPackedSimdalso avoids expanding an already-large surface.Reuse
Vector128.MultiplyAddEstimateetc. rather than adding a class. Cross-platform helpers should stay the recommended API, but users ofPackedSimdalso need direct access (e.g., to reason about codegen, or when compiling withWasmEnableRelaxedSimd=false). Same argument the design has already accepted forPackedSimdliving alongsideVector128.SwizzlevsRelaxedSwizzle. PackedSimd already usesSwizzle; disambiguation happens at the class name. Same forMin/Max.MultiplyRoundedQ15vsMultiplyRoundedSaturateQ15. PackedSimd uses the latter (deterministic saturation on overflow); this class deliberately omitsSaturatebecause saturation on0x8000 * 0x8000is the exact bit spec leaves implementation-defined. The name reflects the contract.Open design questions worth reviewer input:
ConvertToInt32vsConvertToInt32Native. Cross-platformVector128.ConvertToInt32Nativeuses theNativesuffix to mark implementation-defined OOR/NaN behavior — exactly whati32x4.relaxed_trunc_f32x4_sprovides. Renaming toConvertToInt32Native/ConvertToUInt32Nativewould make it obvious that this class supplies the wasm implementation of the cross-platform "Native" contract. Tentative: rename toConvertToInt32Native/ConvertToUInt32Native.DotProductoperand types.The spec defines the right operand as 7-bit signed; passing an 8-bit value is implementation-defined.Per the finished spec pseudocode, operandais signed and operandbis unsigned-7-bit — the current prototype had these reversed. The signatures above (sbyte left, byte right) match the spec. Reviewers should confirm the nameDotProduct/DotProductAddconveys enough about the implementation-defined summation behavior; adding aSaturatesuffix would be misleading (may or may not saturate).ConvertToInt32(Vector128<double>). The wasm instruction isi32x4.relaxed_trunc_f64x2_s_zero— it produces four int lanes with the upper two zeroed. Cross-platform helpers spell this asNarrow. ConsiderNarrowingConvertToInt32for thedoubleoverloads (matches PackedSimd'sConvertNarrowingSaturatenaming), or leave as an overload and document. Tentative: keep overloadedConvertToInt32and document.Min/MaxvsMinNative/MaxNative. Same argument as WIP: repo consolidation scouting kick-off - make clr build locally on Windows #1 —Vector128.MinNative/MaxNativealready document platform-specific NaN/±0 handling, which matchesf32x4/f64x2.relaxed_min/max. Reviewers may prefer the class use theNativesuffix for symmetry with WIP: repo consolidation scouting kick-off - make clr build locally on Windows #1 (and to reinforce that this is whereVector128.MinNativeon wasm gets its implementation). Tentative: keepMin/Maxand rely on class-name disambiguation, matchingPackedSimd.Min/Max.Risks
RelaxedSimd.*accept implementation-defined behavior; documented per-method.Wasm.*intrinsics.System.Runtime.Intrinsics.Wasmtypes are net5.0+;RelaxedSimdis a new type in that namespace with no netstandard/netfx surface. No compatibility risk.Usage in dotnet/runtime
The
*Nativeand*Estimatesuffixes on cross-platformVector128helpers are already documented as "platform-specific" / "may or may not fuse". Lowering them to relaxed-SIMD on wasm does not stretch that contract — it satisfies it (compare:ConvertToInt32Nativemaps tocvttps2dqon x64 andfcvtzson arm64, both platform-defined for OOR/NaN;i32x4.relaxed_trunc_f32x4_sis the wasm equivalent). Callers who need portable behavior already useVector128.ConvertToInt32Saturate/PackedSimd.*instead.Adoption in
Vector128.*wasm loweringsThe most compelling in-tree consumers are
Vector128.ShuffleNativeand theVector128.*Native/*Estimatehelpers, whose current wasm lowerings emit portable-but-slower fallbacks even though their documented contracts already allow platform-specific behavior. Concrete examples:Vector128.ShuffleNative(byte)PackedSimd.Swizzle(Vector128.cs:3534-3536) — deterministic zero-OORi8x16.relaxed_swizzle— engine-defined OOR, matchesShuffleNative's contractVector128.MultiplyAddEstimate(float|double)FMUL+FADD(two ops) insrc/mono/mono/mini/simd-intrinsics.c:2208-2244f32x4/f64x2.relaxed_maddVector128.MultiplyAddNegatedEstimaterelaxed_nmaddVector128.ConvertToInt32Native(float)/ConvertToUInt32Native(float)i32x4.relaxed_trunc_f32x4_{s,u}Vector128.MinNative/MaxNative(float|double)OP_XBINOP FMIN/FMAXf32x4/f64x2.relaxed_min/maxAll the target intrinsics (
INTRINS_WASM_RELAXED_*) are already declared and used by theRelaxedSimd.*class-path lowering added in this prototype; the follow-up is teaching the genericVector128.*intrinsic recognizer to prefer them on wasm when the engine reports relaxed-SIMD support. RyuJIT wasm work (tracked in #127665) will get the same lowering table.For
ShuffleNativespecifically: adoptingRelaxedSimd.Swizzleon wasm is a behavior change for callers that today implicitly rely onPackedSimd.Swizzle's zero-OOR fallback. That's withinShuffleNative's documented contract, but is worth a compatibility scan of internal call sites before the companion PR lands.Adoption strategy
Nothing is updated in this prototype commit — following the
PackedSimdprecedent (#53730 shipped without in-tree adoption; #88092 refined based on real use). Companion PRs wiring the lowerings above are straightforward and can land alongside if reviewers prefer.Note
This API proposal was drafted with the assistance of GitHub Copilot.