chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
@@ -0,0 +1,38 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""CUDA elementwise dispatch.
Split by storage scope to mirror ``cuda/copy/``:
reg.py — operands all in ``local`` (registers) → induced partition
smem.py — operands all in ``shared*`` → synthesized partition
Each op in ``ops.ALL_OPS`` is registered under both variants. Per-op packed
PTX/CUDA intrinsics live in ``vec_emit/`` (``binary_f32x2`` / ``cast_vec2``
/ ``fma_f32x2``) and are attached to the relevant ``OpSpec.vec_impls``.
"""
from .register import *
# Suppress submodule-attribute leakage. Without an explicit ``__all__`` here,
# ``from tvm.backend.cuda.operator.tile_primitive.elementwise import *`` (run by
# tile_primitive/__init__.py) re-exports the implicit submodule attributes
# (``ops``, ``reg``, ``smem``, ``vec_emit``) — and ``ops`` in particular
# shadows the top-level ``tile_primitive/ops.py`` (BinaryReduce / UnaryReduce
# / ...) when downstream code does ``from tile_primitive import ops``.
__all__: list[str] = []
@@ -0,0 +1,412 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Shared layout / vec-selection / emit helpers for ``reg.py`` and ``smem.py``.
Borrows directly from ``cuda/copy/reg.py`` (induced partition) and
``cuda/copy/_common.py`` (synthesized partition), extended to N operands.
The dispatch split mirrors copy:
reg.py — all operands in ``local`` → partition induced by anchor's layout
smem.py — all operands in ``shared*`` → partition synthesized from ``sctx.intra``
"""
from __future__ import annotations
import functools
import operator
from tvm.arith.analyzer import Analyzer
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.tirx import BufferRegion
from tvm.tirx.layout import Axis, Iter, TileLayout
from ..common import get_indices, get_st_extent
# Re-use copy's primitives (PR-640) — same algorithm, same scope_id machinery.
from ..copy._common import _TID_AXIS_FOR_SCOPE, _extract_tile, _thread_cnt
from ..copy.reg import _all_threads_active, _axis_decl, _compute_perm_r
# -----------------------------------------------------------------------------
# Plan helpers
# -----------------------------------------------------------------------------
def buffer_regions(plan) -> list[BufferRegion]:
"""All BufferRegion args (dst + buffer-region srcs), in plan order."""
out: list[BufferRegion] = [plan.dst]
for s in plan.srcs:
if s.buf_region is not None:
out.append(s.buf_region)
return out
def dtype_name(dtype) -> str:
dtype_obj = getattr(dtype, "dtype", None)
if dtype_obj is not None:
return str(dtype_obj)
return str(dtype)
def dtype_bits(dtype) -> int:
return DataType(dtype_name(dtype)).bits
def compute_dtype_of(plan) -> str:
"""Widest dtype in bits across dst + buffer/scalar srcs (dst breaks ties)."""
candidates = [dtype_name(plan.dst.buffer.dtype)]
for s in plan.srcs:
if s.buf_region is not None:
candidates.append(dtype_name(s.buf_region.buffer.dtype))
elif s.scalar is not None:
candidates.append(scalar_dtype(s.scalar))
widest = candidates[0]
widest_bits = DataType(widest).bits
for d in candidates[1:]:
b = DataType(d).bits
if b > widest_bits:
widest, widest_bits = d, b
return widest
def scalar_dtype(scalar) -> str:
dtype = getattr(scalar, "dtype", None)
if dtype is not None:
return str(dtype)
ty = getattr(scalar, "ty", None)
if ty is None and hasattr(scalar, "expr_ty"):
ty = scalar.expr_ty()
dtype = getattr(ty, "dtype", None)
if dtype is None:
raise AttributeError(f"{type(scalar).__name__} has no dtype-bearing PrimType")
return str(dtype)
def n_elements(buf_region: BufferRegion) -> int:
_, ext = get_st_extent(buf_region)
return functools.reduce(operator.mul, ext, 1)
# -----------------------------------------------------------------------------
# Anchor selection (reg.py)
# -----------------------------------------------------------------------------
def pick_anchor(plan) -> BufferRegion:
"""Anchor is always ``plan.dst`` — every operand must have a layout
(enforced by predicate); dst's layout drives iteration. No choice to make.
"""
return plan.dst
# -----------------------------------------------------------------------------
# Broadcast support (NumPy-style right-aligned, anchor = result shape)
# -----------------------------------------------------------------------------
def _tensor_shape_of(region) -> tuple[int, ...]:
"""Per-dim region extent (post-slice tensor shape, NOT layout shape).
Accepts either ``[(start, end), ...]`` pairs (as built locally from a
``BufferRegion``) or the ``BufferRegion.region`` sequence of ``Range``
objects directly. ``Range.extent`` is already simplified by the
front-end, so we avoid computing ``end - start`` on raw Expr (which
yields an un-simplified ``Sub`` and breaks ``int(...)``).
"""
out = []
a = Analyzer()
for r in region:
if hasattr(r, "extent"):
ext = r.extent
else:
start, end = r
ext = a.simplify(end - start)
out.append(int(ext))
return tuple(out)
def shape_broadcast_compat(op_shape, anchor_shape) -> tuple[bool, str | None]:
"""NumPy-style: right-align op against anchor; per-dim extent must equal
anchor's or be 1. anchor is the result shape; op broadcasts TO anchor.
"""
pad = len(anchor_shape) - len(op_shape)
if pad < 0:
return False, f"op rank {len(op_shape)} > anchor rank {len(anchor_shape)}"
for d in range(len(op_shape)):
e_op = int(op_shape[d])
e_a = int(anchor_shape[pad + d])
if e_op != e_a and e_op != 1:
return False, f"dim {d}: op extent {e_op} vs anchor {e_a} (need equal or 1)"
return True, None
def _broadcast_lift(op_layout, op_tensor_shape, anchor_tensor_shape):
"""Lift ``op_layout`` to ``anchor_tensor_shape`` by inserting stride-0
iters for padded leading dims and replacing extent-1 buckets with a
single stride-0 iter of anchor's extent. Offset and replica list are
preserved untouched.
The lift preserves the physical-address function: new iters have
stride 0 so they contribute ``coord * 0 = 0`` to the address
regardless of which virtual index is supplied, and dropped extent-1
iters contributed ``0 * stride = 0`` already.
"""
pad = len(anchor_tensor_shape) - len(op_tensor_shape)
assert pad >= 0, "shape_broadcast_compat should have rejected this"
try:
grouped, seps = op_layout.group(list(op_tensor_shape))
except Exception as e: # pylint: disable=broad-except
raise ValueError(
f"op layout {op_layout} not groupable by tensor shape {op_tensor_shape}: {e}"
) from e
new_shard: list = []
# (1) Padded leading dims — one stride-0 iter each.
for d in range(pad):
new_shard.append(Iter(int(anchor_tensor_shape[d]), 0, Axis.get("m")))
# (2) Aligned dims.
for d_op in range(len(op_tensor_shape)):
e_op = int(op_tensor_shape[d_op])
e_a = int(anchor_tensor_shape[pad + d_op])
bucket = list(grouped.shard[seps[d_op] : seps[d_op + 1]])
if e_op == e_a:
new_shard.extend(bucket)
elif e_op == 1:
new_shard.append(Iter(e_a, 0, Axis.get("m")))
else:
raise ValueError(
f"dim {d_op}: op extent {e_op} vs anchor {e_a}"
" (shape_broadcast_compat should have rejected)"
)
return TileLayout.from_iters(new_shard, grouped.replica, grouped.offset)
# -----------------------------------------------------------------------------
# Shared preprocess: slice each operand by its region, broadcast-lift to
# anchor's tensor shape. Output: every operand has a layout whose logical
# shape equals ``anchor_tensor_shape``. Reg / smem diverge from here.
# -----------------------------------------------------------------------------
def preprocess_operand(op_br, anchor_tshape):
"""Slice ``op_br``'s buffer layout by region (region offset absorbed into
layout.offset), then broadcast-lift to ``anchor_tshape`` if shapes differ.
Raises ``ValueError`` if the lift is not broadcast-compatible (caller
should have verified via ``shape_broadcast_compat`` in the predicate).
"""
op_layout = op_br.buffer.layout
op_shape = op_br.buffer.shape
op_region = [(r.min, r.min + r.extent) for r in op_br.region]
sliced = op_layout.slice(list(op_shape), op_region).canonicalize()
sliced = _extract_tile(sliced, op_region)
op_tshape = _tensor_shape_of(op_br.region)
if op_tshape != tuple(anchor_tshape):
sliced = _broadcast_lift(sliced, op_tshape, anchor_tshape)
return sliced
def preprocess_operands(plan):
"""Shared entry for reg.py and smem.py: returns
``(anchor_tensor_shape, {op_br: sliced_lifted_layout})``.
Every output layout has logical shape == ``anchor_tensor_shape``.
Broadcast iters carry stride 0 with the default mem axis. Reg's induced
partition (`align_operands_to_anchor`) and smem's synthesized partition
both build on this output.
"""
anchor_tshape = _tensor_shape_of(plan.dst.region)
out: dict = {}
for br in buffer_regions(plan):
out[br] = preprocess_operand(br, anchor_tshape)
return anchor_tshape, out
# -----------------------------------------------------------------------------
# Multi-operand layout alignment for reg.py (induced)
# -----------------------------------------------------------------------------
def _align_layouts_no_post_canon(r_layout, r_shape, r_region, s_layout, s_shape, s_region):
"""Variant of copy ``reg.py:align_layouts_raw`` that omits the final
``canonicalize()`` on ``r_p``.
Copy's version returns ``r_p = r.permute_dims(perm).canonicalize()`` —
that post-permute canonicalize can fuse adjacent iters (e.g. wgmma
layout's 5 iters collapse to 2), but ``s_seps`` is built from
``perm`` of length ``len(r.shard) pre-canon``. The two lengths then
disagree and ``s_p.shard[s_seps[k]:s_seps[k+1]]`` indexes into the
wrong sub-range.
Dropping the final canonicalize keeps ``r_p.shard`` and ``s_seps`` in
1-to-1 correspondence. Copy's tests don't hit this because R is
typically 1D and doesn't fuse further after permute.
"""
r = r_layout.slice(list(r_shape), r_region).canonicalize()
s = s_layout.slice(list(s_shape), s_region).canonicalize()
s = _extract_tile(s, s_region)
# Broadcast lift: when op's post-slice tensor shape != anchor's, expand
# s via stride-0 iters so group() below can partition along anchor's
# iter structure. Legality must be enforced upstream by the predicate.
r_tshape = _tensor_shape_of(r_region)
s_tshape = _tensor_shape_of(s_region)
if s_tshape != r_tshape:
s = _broadcast_lift(s, s_tshape, r_tshape)
perm = _compute_perm_r(r)
r_shape_for_group = [int(it.extent) for it in r.shard]
s_grp, seps = s.group(r_shape_for_group)
s_p = s_grp.permute_by_groups(list(seps), perm)
r_p = r.permute_dims(perm) # NO post-canonicalize
sizes = [seps[i + 1] - seps[i] for i in range(len(seps) - 1)]
s_seps = [0]
for p in perm:
s_seps.append(s_seps[-1] + sizes[p])
return r_p, s_p, s_seps
def align_operands_to_anchor(anchor_br, layout_others_br):
"""Align every layout-bearing non-anchor operand to ``anchor_br``.
Returns ``(anchor_p, per_op_aligned)`` where ``per_op_aligned[op_br] =
(op_p, op_seps)``. Trivial-layout operands are NOT included here —
caller indexes them directly via their region. Scalar srcs likewise
live outside this map.
Caller must enter ``with sctx.target:`` so ``canonicalize()`` runs the
scope-aware fusers (e.g. laneid+wid_in_wg → tid_in_wg).
Uses ``_align_layouts_no_post_canon`` (not copy's ``align_layouts_raw``
directly) so ``anchor_p.shard`` length matches ``op_seps`` groupings.
"""
anchor_layout = anchor_br.buffer.layout
anchor_shape = anchor_br.buffer.shape
anchor_region = [(r.min, r.min + r.extent) for r in anchor_br.region]
per_op_aligned: dict = {}
anchor_p = None
if not layout_others_br:
# Just slice + permute anchor alone (no post-canon — keep iters
# 1-to-1 with how they'd appear with srcs).
r = anchor_layout.slice(list(anchor_shape), anchor_region).canonicalize()
perm = _compute_perm_r(r)
anchor_p = r.permute_dims(perm)
return anchor_p, per_op_aligned
for op_br in layout_others_br:
op_layout = op_br.buffer.layout
op_shape = op_br.buffer.shape
op_region = [(r.min, r.min + r.extent) for r in op_br.region]
r_p, op_p, op_seps = _align_layouts_no_post_canon(
anchor_layout,
anchor_shape,
anchor_region,
op_layout,
op_shape,
op_region,
)
if anchor_p is None:
anchor_p = r_p
per_op_aligned[op_br] = (op_p, op_seps)
return anchor_p, per_op_aligned
# -----------------------------------------------------------------------------
# vec_chunk selection
# -----------------------------------------------------------------------------
def pick_vec_chunk(spec, op_call, sctx, plan, max_layout_vec_len: int):
"""Pick widest ``(vec_chunk, vec_impl)`` such that:
- ``vec_impl.vec_len`` divides ``max_layout_vec_len`` AND ``vec_impl.applies(...)``
- Or no vec_impl matches → scalar fallback ``(max_layout_vec_len, None)``
``spec.vec_impls`` is assumed pre-sorted widest-first.
"""
if max_layout_vec_len <= 0:
return 1, None
for impl in getattr(spec, "vec_impls", []):
if impl.vec_len > max_layout_vec_len:
continue
if max_layout_vec_len % impl.vec_len != 0:
continue
ok, _ = impl.applies(op_call, sctx, plan)
if ok:
return impl.vec_len, impl
return max_layout_vec_len, None
# -----------------------------------------------------------------------------
# Emit-time helpers
# -----------------------------------------------------------------------------
def _broadcast_indices(dst_indices, dst_start, dst_extent, op_start, op_ext):
"""NumPy-style right-aligned broadcast: derive op's per-dim indices from
dst's. For matching extents copies dst's coord (rebased); for op_ext[d]
== 1 returns the constant start (the only valid index for that dim).
"""
pad = len(dst_extent) - len(op_ext)
return [
(dst_indices[i + pad] - dst_start[i + pad]) + op_start[i]
if int(op_ext[i]) != 1
else op_start[i]
for i in range(len(op_ext))
]
def fetch_src_value(src, fused, dst_indices, dst_start, dst_extent):
"""Per-element load Expr for one src. Handles buffer / scalar / broadcast srcs."""
if src.is_scalar:
return src.scalar
region = src.buf_region
src_st, src_ext = get_st_extent(region)
if src.index_fn is not None:
idx = src.index_fn(dst_indices, dst_start, dst_extent, src_st, src_ext)
elif tuple(int(e) for e in src_ext) != tuple(int(e) for e in dst_extent):
# Broadcast — derive src indices from dst's via right-aligned compat.
idx = _broadcast_indices(dst_indices, dst_start, dst_extent, src_st, src_ext)
else:
idx = get_indices(fused, src_st, src_ext)
return region.buffer[tuple(idx)]
def emit_scope_sync(scope_kind: str):
"""Returns an ``@T.inline`` sync helper matched to the exec scope."""
@T.inline
def sync():
if scope_kind == "cta":
T.cuda.cta_sync()
elif scope_kind == "warpgroup":
T.cuda.warpgroup_sync(8)
elif scope_kind == "warp":
T.cuda.warp_sync()
return sync
__all__ = [
"_TID_AXIS_FOR_SCOPE",
"_all_threads_active",
"_axis_decl",
"_broadcast_indices",
"_tensor_shape_of",
"_thread_cnt",
"align_operands_to_anchor",
"buffer_regions",
"compute_dtype_of",
"dtype_bits",
"dtype_name",
"emit_scope_sync",
"fetch_src_value",
"n_elements",
"pick_anchor",
"pick_vec_chunk",
"preprocess_operand",
"preprocess_operands",
"shape_broadcast_compat",
]
@@ -0,0 +1,121 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Per-op data model + ALL_OPS registry.
``OpSpec`` describes one elementwise op. ``VecImpl`` describes one packed-PTX
or CUDA-intrinsic emit available for that op (e.g. ``add_f32x2``); a list of
these (widest-first) lets ``reg.py``/``smem.py`` pick the widest matching
both the layout and the op's available intrinsics, like copy picks
``copy_{128,64,32,16,8}b`` based on bit-width and tail contiguity.
"""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from tvm.ir.expr import Expr
from tvm.tirx import BufferRegion, TilePrimitiveCall
@dataclass
class SrcSpec:
"""One operand of an elementwise op.
Either a ``BufferRegion`` (per-element load) or a scalar ``Expr``.
``index_fn``, if given, derives per-element indices for broadcasting srcs:
``index_fn(dst_indices, dst_start, dst_extent, src_start, src_extent) -> list[Expr]``
Default is the standard ``get_indices`` over the src's own region.
"""
buf_region: BufferRegion | None = None
scalar: Expr | None = None
index_fn: Callable | None = None
@property
def is_scalar(self) -> bool:
return self.scalar is not None
@property
def buffer(self):
return self.buf_region.buffer if self.buf_region is not None else None
@dataclass
class Plan:
"""Parsed elementwise op ready for a schedule to consume."""
dst: BufferRegion
srcs: list[SrcSpec]
extras: dict[str, Any] = field(default_factory=dict)
@dataclass
class VecImpl:
"""One packed-vector implementation registered for an op.
Mirrors the ``copy_{Nb}`` menu in copy: each entry says "I can process
``vec_len`` consecutive elements per call". The schedule picks the widest
one whose ``vec_len`` divides the layout's contig tail AND whose
``applies()`` returns ``True``.
"""
vec_len: int # elements per packed call
applies: Callable[[TilePrimitiveCall, Any, Plan], tuple[bool, str | None]]
# emit(dst_ptr, src_ptrs, extras) -> Stmt
# dst_ptr: typed ptr to ``vec_len`` consecutive dst elements
# src_ptrs[i]: typed ptr to ``vec_len`` consecutive src[i] elements,
# OR a scalar Expr if src[i].is_scalar.
# Runs in Python at @T.prim_func build time -- branching on src kind is a
# normal Python ``if``, not a TVMScript shape limitation. This is what
# collapses the old 4x2 shape-explosion in schema.py's factories.
emit: Callable
@dataclass
class OpSpec:
"""Metadata for an elementwise op."""
name: str
# parse(op_call) -> (Plan, msg|None); msg explains why parse failed.
parse: Callable[[TilePrimitiveCall], tuple[Plan | None, str | None]]
# Scalar compute used by the fallback emit path (wrapped in Tx.vectorized).
# compute_scalar(src_vals_at_one_idx, extras, dst_dtype) -> Expr
compute_scalar: Callable[[list, dict, str], Any]
# Optional dtype check on plan.extras (e.g. unary bias/scale dtype agreement).
check_extras: Callable | None = None
# Widest-first vec impls. Schedule picks first matching layout+applies.
vec_impls: list[VecImpl] = field(default_factory=list)
def _build_all_ops() -> dict[str, OpSpec]:
"""Aggregate per-family op specs. Deferred imports avoid cycles
(vec_emit/* imports VecImpl from this module)."""
from .binary import BINARY_OPS
from .cast import CAST_OPS
from .fma import FMA_OPS
from .unary import UNARY_OPS
return {**UNARY_OPS, **BINARY_OPS, **CAST_OPS, **FMA_OPS}
ALL_OPS: dict[str, OpSpec] = _build_all_ops()
__all__ = ["ALL_OPS", "OpSpec", "Plan", "SrcSpec", "VecImpl"]
@@ -0,0 +1,139 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Binary elementwise ops: add / sub / mul / fdiv / maximum.
Includes constant-lhs commute logic. Broadcasting (extent=1 dims) is
handled at the layout level in dispatch's ``_broadcast_lift``, not here —
parser just records each src as-is.
``add``/``sub``/``mul`` attach a ``VecImpl`` for sm_100+ packed f32x2;
``fdiv``/``maximum`` have no packed PTX (scalar fallback only — ``max`` lowers
to a single ``FMNMX``/``max.f32``, which is exact, so there is no rounding/ftz
variant to pack).
"""
from __future__ import annotations
import functools
import operator
from typing import Any
from tvm.script import tirx as Tx
from tvm.tirx import BufferRegion, TilePrimitiveCall
from ..vec_emit.binary_f32x2 import BINARY_F32X2_IMPLS
from . import OpSpec, Plan, SrcSpec
_COMMUTATIVE = frozenset({"add", "mul", "maximum"})
def _parse_binary_for(op_name: str):
"""Build a ``parse(op_call) -> (Plan, msg)`` for a specific binary op."""
def parse(op: TilePrimitiveCall) -> tuple[Plan | None, str | None]:
_dst: BufferRegion = op.args[0]
_src1 = op.args[1]
_src2 = op.args[2]
s1_scalar = not isinstance(_src1, BufferRegion)
s2_scalar = not isinstance(_src2, BufferRegion)
if s1_scalar and s2_scalar:
return None, "both inputs are constants"
# Move constant to rhs (commute if allowed; else reject).
if s1_scalar:
if op_name not in _COMMUTATIVE:
return None, f"non-commutative op {op_name} cannot have constant lhs"
_src1, _src2 = _src2, _src1
s2_scalar = True
# If rhs is a smaller buffer (broadcast), swap if commutative so the
# bigger one is in src1 — keeps src1 == dst convention.
if not s2_scalar:
s1_n = functools.reduce(operator.mul, [r.extent for r in _src1.region], 1)
s2_n = functools.reduce(operator.mul, [r.extent for r in _src2.region], 1)
if s1_n < s2_n:
if op_name not in _COMMUTATIVE:
return None, f"non-commutative op {op_name} cannot swap to broadcast"
_src1, _src2 = _src2, _src1
srcs: list[SrcSpec] = [SrcSpec(buf_region=_src1)]
if s2_scalar:
srcs.append(SrcSpec(scalar=_src2))
else:
srcs.append(SrcSpec(buf_region=_src2))
extras: dict[str, Any] = {}
rm = op.config.get("rounding_mode", None)
if rm is not None:
extras["rounding_mode"] = rm
return Plan(dst=_dst, srcs=srcs, extras=extras), None
return parse
def _compute_add(src_vals, extras, dt):
return src_vals[0] + src_vals[1]
def _compute_sub(src_vals, extras, dt):
return src_vals[0] - src_vals[1]
def _compute_mul(src_vals, extras, dt):
return src_vals[0] * src_vals[1]
def _compute_fdiv(src_vals, extras, dt):
return src_vals[0] / src_vals[1]
def _compute_maximum(src_vals, extras, dt):
return Tx.max(src_vals[0], src_vals[1])
BINARY_OPS: dict[str, OpSpec] = {
"add": OpSpec(
"add",
_parse_binary_for("add"),
_compute_add,
vec_impls=[BINARY_F32X2_IMPLS["add"]],
),
"sub": OpSpec(
"sub",
_parse_binary_for("sub"),
_compute_sub,
vec_impls=[BINARY_F32X2_IMPLS["sub"]],
),
"mul": OpSpec(
"mul",
_parse_binary_for("mul"),
_compute_mul,
vec_impls=[BINARY_F32X2_IMPLS["mul"]],
),
"fdiv": OpSpec(
"fdiv",
_parse_binary_for("fdiv"),
_compute_fdiv,
),
"maximum": OpSpec(
"maximum",
_parse_binary_for("maximum"),
_compute_maximum,
),
}
@@ -0,0 +1,45 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Cast op: ``Tx.cast(dst, src)``. Outer ``Tx.cast(..., dst.dtype)`` in the
schedule handles the scalar conversion; the vec-impl packs pairs via
CUDA intrinsics like ``__float22half2_rn``."""
from __future__ import annotations
from tvm.tirx import BufferRegion, TilePrimitiveCall
from ..vec_emit.cast_vec2 import CAST_VEC2_IMPL
from . import OpSpec, Plan, SrcSpec
def _parse_cast(op: TilePrimitiveCall) -> tuple[Plan | None, str | None]:
_dst: BufferRegion = op.args[0]
_src = op.args[1]
if not isinstance(_src, BufferRegion):
return None, "cast src must be a buffer region"
return Plan(dst=_dst, srcs=[SrcSpec(buf_region=_src)], extras={}), None
def _compute_cast(src_vals, extras, dt):
# Schedule wraps with Tx.cast(..., dst.dtype) — just pass through.
return src_vals[0]
CAST_OPS: dict[str, OpSpec] = {
"cast": OpSpec("cast", _parse_cast, _compute_cast, vec_impls=[CAST_VEC2_IMPL]),
}
@@ -0,0 +1,50 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""FMA op: ``Tx.fma(dst, a, b, c)`` → ``dst = a*b + c``.
Attaches ``fma_f32x2`` VecImpl for sm_100+ f32; falls back to scalar
``a*b + c`` otherwise.
"""
from __future__ import annotations
from tvm.tirx import BufferRegion, TilePrimitiveCall
from ..vec_emit.fma_f32x2 import FMA_F32X2_IMPL
from . import OpSpec, Plan, SrcSpec
def _parse_fma(op: TilePrimitiveCall) -> tuple[Plan | None, str | None]:
_dst: BufferRegion = op.args[0]
args = op.args[1:4]
srcs: list[SrcSpec] = []
for a in args:
if isinstance(a, BufferRegion):
srcs.append(SrcSpec(buf_region=a))
else:
srcs.append(SrcSpec(scalar=a))
return Plan(dst=_dst, srcs=srcs, extras={}), None
def _compute_fma(src_vals, extras, dt):
return src_vals[0] * src_vals[1] + src_vals[2]
FMA_OPS: dict[str, OpSpec] = {
"fma": OpSpec("fma", _parse_fma, _compute_fma, vec_impls=[FMA_F32X2_IMPL]),
}
@@ -0,0 +1,121 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Unary elementwise ops: zero / fill / reciprocal / sqrt / exp / exp2 / silu.
All carry the same ``T.<unary>(dst, src[, bias, scale])`` shape (bias / scale
optional; ``silu`` ignores bias/scale to preserve legacy behavior).
"""
from __future__ import annotations
from typing import Any
from tvm.ir import is_prim_expr
from tvm.script import tirx as T
from tvm.tirx import BufferRegion, TilePrimitiveCall
from tvm.tirx.expr import FloatImm
from .._common import scalar_dtype
from . import OpSpec, Plan, SrcSpec
def _parse_unary(op: TilePrimitiveCall) -> tuple[Plan | None, str | None]:
"""T.<unary>(dst, src[, bias, scale]) → Plan."""
_dst: BufferRegion = op.args[0]
_src = op.args[1]
_bias = op.args[2] if len(op.args) > 2 else None
_scale = op.args[3] if len(op.args) > 2 else None
srcs: list[SrcSpec] = []
if isinstance(_src, BufferRegion):
srcs.append(SrcSpec(buf_region=_src))
elif is_prim_expr(_src):
srcs.append(SrcSpec(scalar=_src))
else:
return None, f"unsupported src type {type(_src).__name__}"
extras: dict[str, Any] = {
"scale": _scale,
"bias_const": _bias if isinstance(_bias, FloatImm) else None,
}
if isinstance(_bias, BufferRegion):
srcs.append(SrcSpec(buf_region=_bias))
extras["has_bias_buf"] = True
else:
extras["has_bias_buf"] = False
return Plan(dst=_dst, srcs=srcs, extras=extras), None
def _check_unary_extras(extras: dict, compute_dtype: str) -> tuple[bool, str | None]:
scale = extras.get("scale")
if scale is not None and scalar_dtype(scale) != compute_dtype:
return False, f"scale dtype {scalar_dtype(scale)} != compute dtype {compute_dtype}"
bias_const = extras.get("bias_const")
if bias_const is not None and scalar_dtype(bias_const) != compute_dtype:
return (
False,
f"bias_const dtype {scalar_dtype(bias_const)} != compute dtype {compute_dtype}",
)
return True, None
def _with_bias_scale(raw_op):
"""Wrap ``raw_op`` (e.g. ``T.exp``) into a compute that applies bias/scale first."""
def compute(src_vals, extras, dt):
x = src_vals[0]
scale = extras.get("scale")
if scale is not None:
x = x * scale
if extras.get("has_bias_buf"):
x = x + src_vals[1]
elif extras.get("bias_const") is not None:
x = x + extras["bias_const"]
return raw_op(x)
return compute
def _compute_zero(src_vals, extras, dt):
return 0.0
def _compute_fill(src_vals, extras, dt):
return src_vals[0]
def _compute_reciprocal(src_vals, extras, dt):
x = src_vals[0]
return T.FloatImm(x.ty, 1.0) / x
def _compute_silu(src_vals, extras, dt):
# Legacy: silu doesn't apply bias/scale.
x = src_vals[0]
return x / (T.FloatImm(x.ty, 1.0) + T.exp(T.FloatImm(x.ty, 0.0) - x))
UNARY_OPS: dict[str, OpSpec] = {
"zero": OpSpec("zero", _parse_unary, _compute_zero, _check_unary_extras),
"fill": OpSpec("fill", _parse_unary, _compute_fill, _check_unary_extras),
"reciprocal": OpSpec("reciprocal", _parse_unary, _compute_reciprocal, _check_unary_extras),
"sqrt": OpSpec("sqrt", _parse_unary, _with_bias_scale(T.sqrt), _check_unary_extras),
"exp": OpSpec("exp", _parse_unary, _with_bias_scale(T.exp), _check_unary_extras),
"exp2": OpSpec("exp2", _parse_unary, _with_bias_scale(T.exp2), _check_unary_extras),
"silu": OpSpec("silu", _parse_unary, _compute_silu, _check_unary_extras),
}
@@ -0,0 +1,429 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Elementwise dispatch when all operands live in ``local`` (registers).
Mirrors ``cuda/copy/reg.py``: the partition is *induced* by the layout that
carries thread-axis info (the "anchor" operand). The region slice is absorbed
into the sliced layout up front via ``align_operands_to_anchor`` — emit
operates on a flat 1D per-thread view and indexes it with a scalar offset, so
codegen never sees multi-dim ``get_indices`` inside ``T.vectorized``.
Two paths inside emit:
* induced (anchor exists) — atom-based, exactly mirrors copy reg.py
* trivial (no anchor) — flat full region, every thread runs the full
loop on its private storage
"""
from __future__ import annotations
import functools
import operator
from tvm.arith import Analyzer
from tvm.script import tirx as T
from tvm.tirx import PrimFunc, TilePrimitiveCall
from tvm.tirx.layout import TileLayout
from tvm.tirx.operator.tile_primitive import DispatchContext
from tvm.tirx.operator.tile_primitive.dispatcher import fail
from ..common import get_st_extent
from ..copy._common import _carve_tail, _verify_s_tail_contig
from ..layout_utils import get_sublayout_from_region, layout_signature
from ._common import (
_TID_AXIS_FOR_SCOPE,
_all_threads_active,
_tensor_shape_of,
_thread_cnt,
align_operands_to_anchor,
buffer_regions,
compute_dtype_of,
pick_anchor,
shape_broadcast_compat,
)
# -----------------------------------------------------------------------------
# Predicate
# -----------------------------------------------------------------------------
def _validate_anchor_layout(anchor_br) -> tuple[bool, str | None]:
layout = anchor_br.buffer.layout
if layout.is_swizzle():
return False, "anchor layout is swizzle"
if not isinstance(layout, TileLayout):
return False, f"anchor layout is {type(layout).__name__}, not TileLayout"
return True, None
def _validate_scope_level_anchor(anchor_br, sctx: DispatchContext) -> tuple[bool, str | None]:
"""For warp/warpgroup/cta scope, require dst to be scope-level: after
canonicalizing with the target its thread axes are the scope's intra-thread
axis (laneid/tid_in_wg/tx) and, sorted by stride, tile a complete ``T:1``
chain over all ``T`` threads of the scope. Rejects thread-local ``.local()``
views; thread scope is exempt.
"""
scope = sctx.scope_kind
if scope == "thread":
return True, None
expected_axis = _TID_AXIS_FOR_SCOPE.get(scope)
if expected_axis is None:
return True, None
expected_cnt = _thread_cnt(sctx)
# Canonicalize the sliced anchor with the target so warp/lane axes fuse.
st, ext = get_st_extent(anchor_br)
sliced = get_sublayout_from_region(anchor_br.buffer.layout, anchor_br.buffer.shape, st, ext)
with sctx.target:
canon = sliced.canonicalize()
shard = getattr(canon, "shard", None)
if shard is None:
return False, f"{scope}-scope op operand layout is not a TileLayout after slicing"
thread_iters = [it for it in shard if it.axis.is_thread()]
if not thread_iters:
return (
False,
f"{scope}-scope op needs a {scope}-level operand whose layout carries "
f"thread axes ({expected_axis} composing to {expected_cnt}:1); got a "
f"thread-local view with no thread axes — pass the {scope}-level tensor, "
f"not its `.local()` (per-thread) view",
)
bad = sorted({it.axis.name for it in thread_iters if it.axis.name != expected_axis})
if bad:
return (
False,
f"{scope}-scope op operand carries thread axes {bad}; after "
f"canonicalization a {scope}-level layout must use only {expected_axis!r}",
)
# Sorted by stride the thread iters must tile a complete chain 1, e0,
# e0*e1, ... up to the scope thread count — i.e. cover all T threads with
# no gap or overlap (extents alone would miss gaps/overlaps).
running = 1
for it in sorted(thread_iters, key=lambda i: int(i.stride)):
stride, extent = int(it.stride), int(it.extent)
if stride != running:
return (
False,
f"{scope}-scope op operand thread axes do not tile a complete "
f"{expected_cnt}:1 (sorted by stride: expected {running}, got {stride})",
)
running *= extent
if running != expected_cnt:
return (
False,
f"{scope}-scope op operand thread axes span {running} threads, not the "
f"full {expected_cnt} of the {scope}",
)
return True, None
def _check_layout_operands_agree(plan, sctx) -> tuple[bool, str | None]:
"""Replica sigs must match across non-trivial-layout operands.
``align_operands_to_anchor`` normalizes thread + local parts via
permute/group, but the replica part isn't touched by alignment — if
operands disagree there, alignment can't fix it and emit will be wrong.
Thread / local mismatches that alignment can't resolve will raise
cleanly at align time, so we don't pre-check them.
"""
# All operands have a layout (predicate already enforced this); just
# iterate them all.
layout_brs = list(buffer_regions(plan))
if len(layout_brs) < 2:
return True, None
analyzer = Analyzer()
replica_sigs = []
for br in layout_brs:
st, ext = get_st_extent(br)
with sctx.target:
sliced = get_sublayout_from_region(br.buffer.layout, br.buffer.shape, st, ext)
canon = sliced.canonicalize()
sig = layout_signature(canon)
if sig is None:
return False, "layout has no signature (not a TileLayout?)"
# layout_signature returns (thread_sig, local_sig, replica_sig)
replica_sigs.append(sig[2])
for s in replica_sigs[1:]:
# Compare replica entries (axis_key, extent, stride) element-wise.
if len(s) != len(replica_sigs[0]):
return False, "replica sig mismatch (different number of replica iters)"
for (k_a, e_a, st_a), (k_b, e_b, st_b) in zip(replica_sigs[0], s):
if k_a != k_b:
return False, "replica sig mismatch (axis key)"
if not analyzer.can_prove_equal(e_a, e_b):
return False, "replica sig mismatch (extent)"
if not analyzer.can_prove_equal(st_a, st_b):
return False, "replica sig mismatch (stride)"
return True, None
def is_reg_ewise(spec):
"""Predicate factory: dispatch accepted iff all operands in ``local`` scope."""
def check(op_call: TilePrimitiveCall, sctx: DispatchContext) -> tuple[bool, str | None]:
if not sctx.is_target("cuda"):
return False, "non-cuda target"
if sctx.scope_kind not in ("thread", "warp", "warpgroup", "cta"):
return False, f"unsupported scope {sctx.scope_kind}"
ok, reason = _all_threads_active(sctx)
if not ok:
return False, reason
plan, msg = spec.parse(op_call)
if msg is not None or plan is None:
return False, msg
for br in buffer_regions(plan):
if br.buffer.scope() != "local":
return False, f"operand scope {br.buffer.scope()} != local"
if br.buffer.layout is None:
return False, f"operand {br} has no layout"
if spec.check_extras is not None:
ok2, reason2 = spec.check_extras(plan.extras, compute_dtype_of(plan))
if not ok2:
return False, reason2
anchor = pick_anchor(plan)
ok3, reason3 = _validate_anchor_layout(anchor)
if not ok3:
return False, reason3
ok_scope, reason_scope = _validate_scope_level_anchor(anchor, sctx)
if not ok_scope:
return False, reason_scope
# Shape compat (NumPy-style broadcast): anchor's tensor shape is the
# result shape; every operand must broadcast TO anchor.
anchor_tshape = _tensor_shape_of(anchor.region)
for br in buffer_regions(plan):
if br is anchor:
continue
op_tshape = _tensor_shape_of(br.region)
ok_b, reason_b = shape_broadcast_compat(op_tshape, anchor_tshape)
if not ok_b:
return False, f"shape incompat: {reason_b}"
ok4, reason4 = _check_layout_operands_agree(plan, sctx)
if not ok4:
return False, reason4
return True, None
return check
# -----------------------------------------------------------------------------
# Shared helpers
# -----------------------------------------------------------------------------
def _prod(it) -> int:
return functools.reduce(operator.mul, it, 1)
# -----------------------------------------------------------------------------
# Main entry
# -----------------------------------------------------------------------------
def emit_reg(op_call: TilePrimitiveCall, spec, sctx: DispatchContext) -> PrimFunc:
plan, msg = spec.parse(op_call)
if msg is not None or plan is None:
fail(msg or "parse failed")
return _emit_induced(plan, spec, sctx, op_call, pick_anchor(plan))
# -----------------------------------------------------------------------------
# Induced path — anchor with non-trivial layout drives partition
# -----------------------------------------------------------------------------
def _strip_thread(layout):
"""Return a new TileLayout with thread iters removed from shard."""
mem_iters = [it for it in layout.shard if not it.axis.is_thread()]
return TileLayout.from_iters(mem_iters, list(layout.replica), dict(layout.offset))
def _pick_vec_and_carve(spec, op_call, sctx, plan, per_op_mem_layouts):
"""Pick ``(vec_len, vec_impl, carved_layouts)``.
Enumerate ``spec.vec_impls`` widest-first. For each candidate ``vec_len``:
1. Try ``_carve_tail`` on each operand's mem-only layout (may split a
boundary iter so the tail product equals ``vec_len``).
2. Verify the carved tail is physically contiguous (stride-1 chain whose
product equals ``vec_len``).
3. Call ``impl.applies(op_call, sctx, plan)``.
First candidate that passes all three on EVERY operand wins. Otherwise
fall back to scalar: ``vec_len=1``, ``vec_impl=None``, original (uncarved)
layouts.
"""
impls = sorted(getattr(spec, "vec_impls", []), key=lambda i: -i.vec_len)
for impl in impls:
cand = impl.vec_len
carved_try = {}
all_ok = True
for op_br, layout in per_op_mem_layouts.items():
new_iters = _carve_tail(list(layout.shard), cand)
if new_iters is None:
all_ok = False
break
new_layout = TileLayout.from_iters(new_iters, list(layout.replica), dict(layout.offset))
if not _verify_s_tail_contig(new_layout, cand):
all_ok = False
break
carved_try[op_br] = new_layout
if not all_ok:
continue
ok, _ = impl.applies(op_call, sctx, plan)
if not ok:
continue
return cand, impl, carved_try
# Scalar fallback — use uncarved mem layouts as-is.
return 1, None, dict(per_op_mem_layouts)
def _emit_induced(plan, spec, sctx, op_call, anchor_br) -> PrimFunc:
# Every buffer-region operand has a layout (enforced by predicate);
# trivial / identity layouts are fine — the algorithm is robust to
# layouts with no thread axes (strip is no-op, placeholders empty).
layout_others = [br for br in buffer_regions(plan) if br is not anchor_br]
# Step 1: slice + permute (region offset absorbed into op_p.offset; per-iter
# strides reflect post-slice physical addressing). No (st, ext) leaks out.
with sctx.target:
anchor_p, per_op_aligned = align_operands_to_anchor(anchor_br, layout_others)
# Step 2: post-align thread-equality check. ``align`` is supposed to
# normalize the thread part; we verify it did. (Replica was pre-checked
# in the predicate.)
def _thread_iters(layout):
c = layout.canonicalize()
return [(it.axis, int(it.extent), int(it.stride)) for it in c.shard if it.axis.is_thread()]
anchor_thread = _thread_iters(anchor_p)
for op_br, (op_p, _) in per_op_aligned.items():
if _thread_iters(op_p) != anchor_thread:
fail("thread part mismatch between anchor and operand after alignment")
# Step 3: drop thread iters; from here on operands have mem-only layouts.
per_op_mem = {anchor_br: _strip_thread(anchor_p)}
for op_br, (op_p, _) in per_op_aligned.items():
per_op_mem[op_br] = _strip_thread(op_p)
# Step 4: enumerate spec.vec_impls widest-first; try carve tail for each
# operand. First candidate that all operands can carve + impl.applies wins.
# Otherwise scalar fallback (vec=1, no inner loop).
vec_len, vec_impl, per_op_carved = _pick_vec_and_carve(spec, op_call, sctx, plan, per_op_mem)
# Step 5: totals + emit. per_thread_total = ∏ extents of (carved) mem
# layout. All operands have the same per_thread_total (alignment invariant).
per_thread_total = _prod(int(it.extent) for it in per_op_carved[anchor_br].shard)
outer_total = per_thread_total // vec_len if vec_len > 0 else per_thread_total
if vec_impl is not None:
result = _emit_induced_packed(
plan,
vec_impl,
vec_len,
outer_total,
per_thread_total,
per_op_carved,
anchor_br,
)
else:
result = _emit_induced_scalar(
plan,
spec,
outer_total,
per_thread_total,
per_op_carved,
anchor_br,
)
return result
def _make_views_meta(per_op_carved, per_thread_total):
"""Build the per-operand 1D buffer view dict.
Each view aliases the operand's physical storage as a 1D shape of
``per_thread_total`` elements, with layout = the operand's carved mem-only
TileLayout. Scalar indexing into the view goes through this layout's
iter strides at codegen time.
"""
return {
op_br: T.decl_buffer(
(per_thread_total,),
op_br.buffer.dtype,
op_br.buffer.data,
scope="local",
layout=per_op_carved[op_br],
)
for op_br in per_op_carved
}
# -----------------------------------------------------------------------------
# Emit — packed (one PTX/CUDA call per outer chunk; no T.vectorized inside)
# -----------------------------------------------------------------------------
def _emit_induced_packed(
plan, vec_impl, vec_len, outer_total, per_thread_total, per_op_carved, anchor_br
) -> PrimFunc:
extras = plan.extras
srcs = plan.srcs
dst_br = plan.dst
@T.prim_func(check_well_formed=False)
def impl():
views = T.meta_var(_make_views_meta(per_op_carved, per_thread_total))
# Serial loop (not T.unroll): T.unroll materializes each per-iter
# ``dst_lane_indices`` / ``src_args`` buffer as a fresh int[1]
# declaration, multiplying by outer_total. ptxas unrolls the
# static-bound loop without that scratch explosion.
for f in range(outer_total):
# Pass logical 1D coord; each buffer's own layout maps it to
# physical at access time (handles wgmma, broadcast, etc.).
dst_lane_indices = [[f * vec_len + k] for k in range(vec_len)]
src_args = T.meta_var(
[
src.scalar
if src.is_scalar
else (
views[src.buf_region],
[[f * vec_len + k] for k in range(vec_len)],
)
for src in srcs
]
)
T.evaluate(vec_impl.emit(views[dst_br], dst_lane_indices, src_args, extras))
return impl
# -----------------------------------------------------------------------------
# Emit — scalar fallback (vec_len = 1; one element per outer iter; no
# T.vectorized inside, so no codegen vec-packing of multi-dim indices).
# -----------------------------------------------------------------------------
def _emit_induced_scalar(
plan, spec, outer_total, per_thread_total, per_op_carved, anchor_br
) -> PrimFunc:
extras = plan.extras
srcs = plan.srcs
dst_br = plan.dst
dst_dtype = dst_br.buffer.dtype
compute = spec.compute_scalar
@T.prim_func(check_well_formed=False)
def impl():
views = T.meta_var(_make_views_meta(per_op_carved, per_thread_total))
# Serial loop (not T.unroll) — see _emit_induced_packed for why.
for f in range(outer_total):
# Logical 1D coord = f (vec_len = 1 in scalar path); each
# buffer's layout maps to physical at access time.
src_vals = T.meta_var(
[src.scalar if src.is_scalar else views[src.buf_region][f] for src in srcs]
)
views[dst_br][f] = T.cast(compute(src_vals, extras, dst_dtype), dst_dtype)
return impl
@@ -0,0 +1,61 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Register each op in ``ALL_OPS`` for both dispatch variants (``reg``, ``smem``).
Mirrors copy PR-640's two-variant model: scope-pair drives the dispatch
selection, the underlying algorithm (induced vs synthesized) follows.
"""
from tvm.tirx import PrimFunc, TilePrimitiveCall
from tvm.tirx.operator.tile_primitive import DispatchContext, predicate, register_dispatch
from .ops import ALL_OPS
from .reg import emit_reg, is_reg_ewise
from .smem import emit_smem, is_smem_ewise
def _register_reg(spec) -> None:
@register_dispatch(
spec.name,
"cuda",
variant="reg",
priority=10,
when=[predicate(f"{spec.name}_reg", is_reg_ewise(spec))],
)
def _dispatch(op: TilePrimitiveCall, sctx: DispatchContext, _spec=spec) -> PrimFunc:
return emit_reg(op, _spec, sctx)
def _register_smem(spec) -> None:
@register_dispatch(
spec.name,
"cuda",
variant="smem",
priority=10,
when=[predicate(f"{spec.name}_smem", is_smem_ewise(spec))],
)
def _dispatch(op: TilePrimitiveCall, sctx: DispatchContext, _spec=spec) -> PrimFunc:
return emit_smem(op, _spec, sctx)
for _spec in ALL_OPS.values():
_register_reg(_spec)
_register_smem(_spec)
__all__: list[str] = []
@@ -0,0 +1,264 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Elementwise dispatch when all operands live in ``shared*``.
Mirrors ``cuda/copy/gmem_smem.py``: no operand carries a per-thread partition,
so partition is *synthesized* from ``sctx.intra`` (``thread_cnt = ∏ intra``).
Each thread takes ``ceildiv(total, vec_chunk * thread_cnt)`` strided chunks.
The shared buffers are indexed multi-dim via ``get_indices(fused, dst_st,
dst_ext)`` and the buffer's own layout resolves to physical addresses at
codegen time. Packed-vec emit requires the innermost dim to have stride 1
(non-swizzle slice) so lanes are physically contiguous; checked in
``_max_layout_vec``.
"""
from __future__ import annotations
from tvm.script import tirx as T
from tvm.tirx import PrimFunc, TilePrimitiveCall
from tvm.tirx.operator.tile_primitive import DispatchContext
from tvm.tirx.operator.tile_primitive.dispatcher import fail
from ..common import get_indices, get_st_extent, get_thread_cnt
from ._common import (
_TID_AXIS_FOR_SCOPE,
_all_threads_active,
_axis_decl,
_broadcast_indices,
_tensor_shape_of,
buffer_regions,
compute_dtype_of,
dtype_bits,
emit_scope_sync,
fetch_src_value,
n_elements,
pick_vec_chunk,
shape_broadcast_compat,
)
# -----------------------------------------------------------------------------
# Predicate
# -----------------------------------------------------------------------------
def is_smem_ewise(spec):
"""Predicate factory: dispatch accepted iff all operands in ``shared*``."""
def check(op_call: TilePrimitiveCall, sctx: DispatchContext) -> tuple[bool, str | None]:
if not sctx.is_target("cuda"):
return False, "non-cuda target"
if sctx.scope_kind not in ("thread", "warp", "warpgroup", "cta"):
return False, f"unsupported scope {sctx.scope_kind}"
ok, reason = _all_threads_active(sctx)
if not ok:
return False, reason
plan, msg = spec.parse(op_call)
if msg is not None or plan is None:
return False, msg
for br in buffer_regions(plan):
if not br.buffer.scope().startswith("shared"):
return False, f"operand scope {br.buffer.scope()} != shared*"
if br.buffer.layout is None:
return False, "shared operand has no layout"
if spec.check_extras is not None:
ok2, reason2 = spec.check_extras(plan.extras, compute_dtype_of(plan))
if not ok2:
return False, reason2
# NumPy-style right-aligned broadcast: anchor = plan.dst; every src
# must be shape-compatible with anchor (extent matches or is 1).
anchor_tshape = _tensor_shape_of(plan.dst.region)
for s in plan.srcs:
if s.buf_region is None:
continue
src_tshape = _tensor_shape_of(s.buf_region.region)
ok_b, reason_b = shape_broadcast_compat(src_tshape, anchor_tshape)
if not ok_b:
return False, f"shape incompat: {reason_b}"
return True, None
return check
# -----------------------------------------------------------------------------
# vec_chunk selection
# -----------------------------------------------------------------------------
def _max_layout_vec(plan, total: int, thread_cnt: int) -> int:
"""Widest vec_chunk dividing all operands' innermost extents AND
``total / thread_cnt``, within dtype-bit candidates ``{128,64,32,16,8}``."""
max_bits = dtype_bits(plan.dst.buffer.dtype)
for s in plan.srcs:
if s.buf_region is not None:
max_bits = max(max_bits, dtype_bits(s.buf_region.buffer.dtype))
per_thread = total // thread_cnt if thread_cnt > 0 else total
if total % thread_cnt != 0:
return 1
inners = [int(plan.dst.region[-1].extent)]
for s in plan.srcs:
if s.buf_region is None or s.index_fn is not None:
continue
inners.append(int(s.buf_region.region[-1].extent))
for cand_bits in (128, 64, 32, 16, 8):
n = cand_bits // max_bits
if n <= 0:
continue
if per_thread % n != 0:
continue
if all(i % n == 0 for i in inners):
return n
return 1
# -----------------------------------------------------------------------------
# Main entry
# -----------------------------------------------------------------------------
def emit_smem(op_call: TilePrimitiveCall, spec, sctx: DispatchContext) -> PrimFunc:
plan, msg = spec.parse(op_call)
if msg is not None or plan is None:
fail(msg or "parse failed")
# Use cuda/common.py:get_thread_cnt rather than copy/_common.py:_thread_cnt
# — the latter computes ``∏ sctx.intra`` which silently returns 0 for
# sub-warp counts at cta scope (warpid extent rounds down to 0). The
# former reads launch_params["threadIdx.x"].dom.extent and is correct
# for all scopes.
thread_cnt = get_thread_cnt(sctx)
if thread_cnt is None:
fail(f"unsupported scope {sctx.scope_kind} for smem emit")
thread_cnt = int(thread_cnt)
if thread_cnt <= 0:
fail(f"non-positive thread_cnt {thread_cnt}")
assert "threadIdx.y" not in sctx.launch_params and "threadIdx.z" not in sctx.launch_params, (
"smem emit currently assumes 1D threadIdx"
)
total = n_elements(plan.dst)
vec_max = _max_layout_vec(plan, total, thread_cnt)
vec_chunk, vec_impl = pick_vec_chunk(spec, op_call, sctx, plan, vec_max)
if vec_impl is not None:
return _emit_packed(plan, vec_impl, vec_chunk, total, thread_cnt, sctx)
return _emit_scalar(plan, spec, vec_chunk, total, thread_cnt, sctx)
def _tid_expr(sctx: DispatchContext):
"""Per-scope tid expr. ``thread`` scope returns 0; collective scopes use
``_axis_decl`` (T.lane_id / T.thread_id_in_wg / threadIdx.x)."""
if sctx.scope_kind == "thread":
return 0
axis_name = _TID_AXIS_FOR_SCOPE[sctx.scope_kind]
return _axis_decl(axis_name, sctx)
# -----------------------------------------------------------------------------
# Per-lane src index helper (handles broadcast via right-aligned compat)
# -----------------------------------------------------------------------------
def _src_lane_indices(src_br, dst_lane_indices, dst_st, dst_ext, vec_chunk, fused0):
"""Return the per-lane multi-dim index list for ``src_br``.
If src's region shape matches dst's, fall through to ``get_indices``
(same as the legacy non-broadcast path). Otherwise derive each lane's
src index from the corresponding dst lane index via right-aligned
broadcast compat.
"""
src_st, src_ext = get_st_extent(src_br)
if tuple(int(e) for e in src_ext) == tuple(int(e) for e in dst_ext):
return [get_indices(fused0 + k, src_st, src_ext) for k in range(vec_chunk)]
return [
_broadcast_indices(dst_lane_indices[k], dst_st, dst_ext, src_st, src_ext)
for k in range(vec_chunk)
]
# -----------------------------------------------------------------------------
# Emit — packed
# -----------------------------------------------------------------------------
def _emit_packed(plan, vec_impl, vec_chunk, total, thread_cnt, sctx) -> PrimFunc:
extras = plan.extras
srcs = plan.srcs
dst_buf = plan.dst.buffer
dst_st, dst_ext = get_st_extent(plan.dst)
sync = emit_scope_sync(sctx.scope_kind)
n_outer = (total + vec_chunk * thread_cnt - 1) // (vec_chunk * thread_cnt)
@T.prim_func(check_well_formed=False)
def impl():
tid = _tid_expr(sctx)
for s in T.serial(0, n_outer):
# First lane's fused index for this thread, this chunk.
fused0 = T.meta_var(s * vec_chunk * thread_cnt + tid * vec_chunk)
# Predicate the call (skip the trailing partial chunk).
if fused0 + vec_chunk <= total:
dst_lane_indices = T.meta_var(
[get_indices(fused0 + k, dst_st, dst_ext) for k in range(vec_chunk)]
)
src_args = T.meta_var(
[
srcs[i].scalar
if srcs[i].is_scalar
else (
srcs[i].buf_region.buffer,
_src_lane_indices(
srcs[i].buf_region,
dst_lane_indices,
dst_st,
dst_ext,
vec_chunk,
fused0,
),
)
for i in range(len(srcs))
]
)
T.evaluate(vec_impl.emit(dst_buf, dst_lane_indices, src_args, extras))
sync()
return impl
# -----------------------------------------------------------------------------
# Emit — scalar fallback
# -----------------------------------------------------------------------------
def _emit_scalar(plan, spec, vec_chunk, total, thread_cnt, sctx) -> PrimFunc:
extras = plan.extras
srcs = plan.srcs
dst_buf = plan.dst.buffer
dst_st, dst_ext = get_st_extent(plan.dst)
dst_dtype = dst_buf.dtype
compute = spec.compute_scalar
sync = emit_scope_sync(sctx.scope_kind)
n_outer = (total + vec_chunk * thread_cnt - 1) // (vec_chunk * thread_cnt)
@T.prim_func(check_well_formed=False)
def impl():
tid = _tid_expr(sctx)
for s in T.serial(0, n_outer):
for vec in T.vectorized(vec_chunk):
fused = T.meta_var(s * vec_chunk * thread_cnt + tid * vec_chunk + vec)
if fused < total:
dst_idx = T.meta_var(get_indices(fused, dst_st, dst_ext))
src_vals = T.meta_var(
[fetch_src_value(src, fused, dst_idx, dst_st, dst_ext) for src in srcs]
)
dst_buf[tuple(dst_idx)] = T.cast(
compute(src_vals, extras, dst_dtype), dst_dtype
)
sync()
return impl
@@ -0,0 +1,40 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Packed-vector emit functions for elementwise ops.
Each module here exposes one or more ``VecImpl`` instances that an ``OpSpec``
(in ``ops/``) attaches to its ``vec_impls`` list. ``reg.py``/``smem.py`` then
pick the widest matching one at dispatch time, mirroring how copy picks
``copy_{Nb}`` from a menu.
VecImpl emit contract:
emit(dst_buf, dst_lane_indices, src_args, extras) -> Expr
* dst_buf: Buffer
* dst_lane_indices: list[list[Expr]] of length ``vec_len``; each entry is the
multi-dim indices for one lane (precomputed by schedule).
* src_args[i]: one of
- Expr (scalar src — broadcast across all lanes)
- tuple (Buffer, list[list[Expr]] of length ``vec_len``) — buffer src
with per-lane indices
* extras: dict (rounding_mode, etc.)
Returns the PTX/CUDA call result; the schedule wraps in ``T.evaluate`` at
the call site. All Python-side shape branching (scalar vs buffer src) happens
in this emit function -- collapses the old 4x2 schema.py factory explosion.
"""
@@ -0,0 +1,97 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Packed f32x2 VecImpls for binary add/sub/mul on sm_100+.
PTX op family: ``{add,sub,mul}.<rm>.ftz.f32x2``. Each call processes 2 f32s
per operand. The old ``_make_binary_packed_f32x2_factory`` (240+ lines, 8
``@T.prim_func`` shape combos per op) collapses to one ``emit`` per op
because operand-shape branching is now Python-level (outside any
``@T.prim_func``).
"""
from __future__ import annotations
from tvm.ir.expr import Expr
from tvm.script import tirx as T
from .._common import dtype_name, scalar_dtype
from ..ops import VecImpl
def _lane(arg, k):
"""Read lane ``k`` of one operand argument.
arg is either a scalar Expr (broadcast) or ``(Buffer, lane_indices)``.
"""
if isinstance(arg, tuple):
buf, lane_indices = arg
return buf[tuple(lane_indices[k])]
return arg
def _f32x2_applies(op_name):
"""Predicate: f32 dst+srcs, sm_100+, no broadcasting srcs, two srcs."""
def applies(op_call, sctx, plan):
from ...common import sm_version_ok
if dtype_name(plan.dst.buffer.dtype) != "float32":
return False, "dst dtype not f32"
if not sm_version_ok(op_call, sctx, min_version=100)[0]:
return False, "sm version < 100"
if len(plan.srcs) != 2:
return False, "binary requires 2 srcs"
for s in plan.srcs:
if s.is_scalar:
if scalar_dtype(s.scalar) != "float32":
return False, "scalar src dtype not f32"
else:
if dtype_name(s.buf_region.buffer.dtype) != "float32":
return False, "buffer src dtype not f32"
if s.index_fn is not None:
return False, "broadcasting src not supported by f32x2 packed"
return True, None
return applies
def _emit_binary_f32x2_for(op_name):
op_func = getattr(T.ptx, f"{op_name}_f32x2")
def emit(dst_buf, dst_lane_indices, src_args, extras) -> Expr:
a_arg, b_arg = src_args
rm = extras.get("rounding_mode", "rz")
return op_func(
T.address_of(dst_buf[tuple(dst_lane_indices[0])]),
T.cuda.make_float2(_lane(a_arg, 0), _lane(a_arg, 1)),
T.cuda.make_float2(_lane(b_arg, 0), _lane(b_arg, 1)),
rounding=rm,
ftz=True,
)
return emit
BINARY_F32X2_IMPLS: dict[str, VecImpl] = {
name: VecImpl(
vec_len=2,
applies=_f32x2_applies(name),
emit=_emit_binary_f32x2_for(name),
)
for name in ("add", "sub", "mul")
}
@@ -0,0 +1,92 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Packed vec_len=2 cast via CUDA pair intrinsics.
Each supported (src_dtype, dst_dtype) pair has a CUDA builtin that converts a
packed-2 source to a packed-2 destination in one instruction
(e.g. ``__float22half2_rn``). The intrinsic takes pointers to the first
element of each packed pair on either side.
"""
from __future__ import annotations
from tvm.ir.expr import Expr
from tvm.script import tirx as T
from .._common import dtype_name
from ..ops import VecImpl
_VEC2_CAST_INTRINSICS = {
("float32", "float16"): "__float22half2_rn",
("float16", "float32"): "__half22float2",
("bfloat16", "float32"): "__bfloat1622float2",
("float32", "bfloat16"): "__float22bfloat162_rn",
}
_DTYPE_X2_NAME = {"float32": "float2", "float16": "half2", "bfloat16": "nv_bfloat162"}
def _intrinsic_name(src_dtype, dst_dtype):
return f"tvm_builtin_cast_{src_dtype}x2_{dst_dtype}x2"
def _intrinsic_source(src_dtype, dst_dtype):
intrinsic = _VEC2_CAST_INTRINSICS[(src_dtype, dst_dtype)]
return (
f"\n__forceinline__ __device__ void {_intrinsic_name(src_dtype, dst_dtype)}"
f"(void* dst, void* src) {{\n"
f" (({_DTYPE_X2_NAME[dst_dtype]}*)dst)[0] = "
f"{intrinsic}((({_DTYPE_X2_NAME[src_dtype]}*)src)[0]);\n"
"}\n"
)
def _cast_vec2_applies(op_call, sctx, plan):
if len(plan.srcs) != 1 or plan.srcs[0].is_scalar:
return False, "cast requires 1 buffer src"
src = plan.srcs[0]
if src.index_fn is not None:
return False, "broadcasting src not supported by cast vec2"
src_dtype = dtype_name(src.buf_region.buffer.dtype)
dst_dtype = dtype_name(plan.dst.buffer.dtype)
if (src_dtype, dst_dtype) not in _VEC2_CAST_INTRINSICS:
return False, f"no vec2 intrinsic for {src_dtype}->{dst_dtype}"
return True, None
def _emit_cast_vec2(dst_buf, dst_lane_indices, src_args, extras) -> Expr:
src_arg = src_args[0]
# cast_vec2 requires buffer src (guarded by applies()).
assert isinstance(src_arg, tuple), "cast vec2 src must be a buffer"
src_buf, src_lane_indices = src_arg
src_dtype = dtype_name(src_buf.dtype)
dst_dtype = dtype_name(dst_buf.dtype)
func_name = _intrinsic_name(src_dtype, dst_dtype)
source_code = _intrinsic_source(src_dtype, dst_dtype)
return T.cuda.func_call(
func_name,
T.address_of(dst_buf[tuple(dst_lane_indices[0])]),
T.address_of(src_buf[tuple(src_lane_indices[0])]),
source_code=source_code,
)
CAST_VEC2_IMPL = VecImpl(
vec_len=2,
applies=_cast_vec2_applies,
emit=_emit_cast_vec2,
)
@@ -0,0 +1,79 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Packed f32x2 VecImpl for FMA on sm_100+.
PTX: ``fma.<rm>.ftz.f32x2 d, a, b, c`` — 2 f32 FMAs per call. Same Python-side
shape collapse as binary_f32x2.
"""
from __future__ import annotations
from tvm.ir.expr import Expr
from tvm.script import tirx as T
from .._common import dtype_name, scalar_dtype
from ..ops import VecImpl
from .binary_f32x2 import _lane
def _fma_f32x2_applies(op_call, sctx, plan):
from ...common import sm_version_ok
if dtype_name(plan.dst.buffer.dtype) != "float32":
return False, "dst dtype not f32"
if not sm_version_ok(op_call, sctx, min_version=100)[0]:
return False, "sm version < 100"
if len(plan.srcs) != 3:
return False, "fma requires 3 srcs"
a, b, c = plan.srcs
if a.is_scalar:
return False, "fma 'a' must be a buffer (no scalar-a packed FMA)"
if dtype_name(a.buf_region.buffer.dtype) != "float32":
return False, "src a dtype not f32"
if a.index_fn is not None:
return False, "broadcasting src a not supported"
for s in (b, c):
if s.is_scalar:
if scalar_dtype(s.scalar) != "float32":
return False, "scalar b/c dtype not f32"
else:
if dtype_name(s.buf_region.buffer.dtype) != "float32":
return False, "buffer b/c dtype not f32"
if s.index_fn is not None:
return False, "broadcasting src b/c not supported"
return True, None
def _emit_fma_f32x2(dst_buf, dst_lane_indices, src_args, extras) -> Expr:
a_arg, b_arg, c_arg = src_args
rm = extras.get("rounding_mode", "rz")
return T.ptx.fma_f32x2(
T.address_of(dst_buf[tuple(dst_lane_indices[0])]),
T.cuda.make_float2(_lane(a_arg, 0), _lane(a_arg, 1)),
T.cuda.make_float2(_lane(b_arg, 0), _lane(b_arg, 1)),
T.cuda.make_float2(_lane(c_arg, 0), _lane(c_arg, 1)),
rounding=rm,
ftz=True,
)
FMA_F32X2_IMPL = VecImpl(
vec_len=2,
applies=_fma_f32x2_applies,
emit=_emit_fma_f32x2,
)