# 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. """Reusable pipeline state and mbarrier helpers for SM100 kernels. These classes emit TIR via @T.inline. Decorate with @T.meta_class so that instances are automatically treated as meta values inside @T.prim_func. """ from tvm.script import tirx as T @T.meta_class class PipelineState: """Tracks stage and phase for a software-pipelined ring buffer. This class does not know anything about full/empty barriers. Use it when the kernel manually waits/signals barriers, or when the stage/phase drives a ring not wrapped in a ``Pipeline``. Parameters ---------- depth : int Number of stages in the ring. phase : int, optional Initial phase. Omit when initialization should happen later. """ def __init__(self, depth: int, phase=None): self.stage = T.local_scalar("int32") self.phase = T.local_scalar("int32") self.depth = depth if phase is not None: self.init(phase) @T.inline def init(self, phase): self.stage = 0 self.phase = phase @T.inline def advance(self): if self.depth > 1: self.stage = self.stage + 1 if self.stage == self.depth: self.stage = 0 self.phase = self.phase ^ 1 else: self.phase = self.phase ^ 1 @T.meta_class class MBarrier: """Mbarrier wrapper with regular ``mbarrier.arrive``. Parameters ---------- pool : SMEMPool Shared memory pool allocator. depth : int Number of barrier slots (one per pipeline stage). phase_offset : int XORed into the phase bit on every ``wait`` / ``arrive``. leader : Expr, optional Boolean predicate selecting the single thread that runs ``mbarrier.init``. Defaults to ``T.cuda.thread_rank() == 0`` -- thread 0 of the enclosing CTA, which always picks exactly one thread regardless of which scope_id vars the caller declared. Override only when you want a different CTA-local thread to do the init. Note: the default deliberately avoids ``T.warp_id()`` / ``T.lane_id()``. Those introduce deferred ``cta->warp`` / ``warp->thread`` ScopeIdDefs that the verifier cannot pin down unless the kernel header declares the full warp/lane chain (e.g. a single-CTA DSMEM kernel that only declares ``thread_id``). It also avoids the synccheck false-deadlock on kernels that declare a second warp-scope id. The generated CUDA is equivalent. """ def __init__(self, pool, depth, phase_offset=0, leader=None): self.buf = pool.alloc((depth,), "uint64", align=8) self.depth = depth self.phase_offset = phase_offset self.leader = leader if leader is not None else (T.cuda.thread_rank() == 0) @T.inline def init(self, count): if self.leader: for i in T.unroll(self.depth): T.ptx.mbarrier.init(self.buf.ptr_to([i]), count) @T.inline def wait(self, stage, phase): # Blocks: ``mbarrier.try_wait`` loops internally until the phase flips, # so this returns only once the barrier has completed. T.ptx.mbarrier.try_wait(self.buf.ptr_to([stage]), phase ^ self.phase_offset) @T.inline def arrive(self, stage, cta_id=None, pred=None, count=None): # Default: local-CTA arrive — emits the simple # ``mbarrier.arrive.shared.b64`` form. To arrive on a remote # CTA's mbarrier in a cluster kernel, callers must pass # ``cta_id=`` explicitly (e.g. ``bar.arrive(stage, cta_id=0)``) # or use ``MBarrier.remote_view(rank).arrive(stage)``. Defaulting # the cross-CTA path was both surprising (``bar.arrive(stage)`` # silently ``mapa`` ed across the cluster) and a per-call cost # of ~3 PTX ops on every single-CTA kernel. # # ``count`` (cross-CTA path only) emits the explicit arrival-count # operand, i.e. ``mbarrier.arrive.shared::cluster.b64 _, [addr], count``. # When ``None`` the implicit count-of-1 form is emitted. Passing # ``count=1`` is semantically identical but spells the count explicitly. if cta_id is None: T.ptx.mbarrier.arrive(self.buf.ptr_to([stage])) else: actual_pred = True if pred is None else pred T.ptx.mbarrier.arrive( self.buf.ptr_to([stage]), cta_id=cta_id, pred=actual_pred, count=count ) def ptr_to(self, idx): return self.buf.ptr_to(idx) def remote_view(self, rank): """Create a view of this barrier mapped to another CTA's shared memory. Arrive-only: the returned view is built with ``object.__new__`` and never copies ``self.leader``, so calling ``.init()`` on it would fail. Use it solely to ``arrive`` on a remote CTA's mbarrier. """ from tvm.ir import PointerType, PrimType from tvm.tirx import Var as TIRVar expr = T.reinterpret("handle", T.ptx.map_shared_rank(self.buf.ptr_to([0]), rank)) ptr = TIRVar("remote_mbar_ptr", PointerType(PrimType("uint64"))) T.Bind(expr, var=ptr) buf = T.decl_buffer([self.depth], "uint64", data=ptr, scope="shared") remote = object.__new__(type(self)) remote.buf = buf remote.depth = self.depth remote.phase_offset = self.phase_offset return remote class TMABar(MBarrier): """Barrier signaled by TMA (mbarrier.arrive.expect_tx). When ``tx_count`` is None, falls back to a remote mbarrier.arrive (matching MBarrier.arrive defaults). """ @T.inline def arrive(self, stage, tx_count=None, cta_id=None, pred=None): # NOTE: this arrive() kwarg set intentionally differs from # MBarrier.arrive (hardware necessity, LSP-incompatible by design). # ``tx_count``: TMA byte count for ``mbarrier.arrive.expect_tx``. # ``cta_id`` / ``pred``: forwarded to the underlying # ``mbarrier.arrive`` (cluster path) when set; otherwise the # arrive is local-CTA only. See ``MBarrier.arrive`` for the # full default-local rationale. if tx_count is not None: T.ptx.mbarrier.arrive.expect_tx(self.buf.ptr_to([stage]), tx_count) elif cta_id is None: T.ptx.mbarrier.arrive(self.buf.ptr_to([stage])) else: actual_pred = True if pred is None else pred T.ptx.mbarrier.arrive(self.buf.ptr_to([stage]), cta_id=cta_id, pred=actual_pred) class TCGen05Bar(MBarrier): """Barrier signaled by ``tcgen05`` commit. The caller is responsible for ensuring only one thread issues the commit, e.g. by wrapping the call in ``if T.ptx.elect_sync():``. """ @T.inline def arrive(self, stage, cta_group=1, cta_mask=None): # NOTE: this arrive() kwarg set intentionally differs from # MBarrier.arrive (hardware necessity, LSP-incompatible by design). if cta_mask is None and cta_group == 1: T.ptx.tcgen05.commit(self.buf.ptr_to([stage])) else: T.ptx.tcgen05.commit(self.buf.ptr_to([stage]), cta_group=cta_group, cta_mask=cta_mask) # Barrier-type tags accepted by Pipeline's ``full=`` / ``empty=`` arguments. _BAR_KINDS = {"tma": TMABar, "tcgen05": TCGen05Bar, "mbar": MBarrier} @T.meta_class class Pipeline: """A full/empty mbarrier pair for a software-pipelined data flow. Pass barrier-type tags and ``Pipeline`` constructs and ``init``\\ s the barriers itself. Tags: ``"tma"`` (TMABar), ``"tcgen05"`` (TCGen05Bar), ``"mbar"`` (MBarrier). The barrier type and arrival count of each event stay explicit at the call site -- e.g. ``Pipeline(pool, n, full="tma", empty="tcgen05", init_empty=NUM_CONSUMER)``. Both signals are required: a ``Pipeline`` is a *pair*. For a one-way event (a pure "X happened" signal with no slot to recycle) use a bare barrier (``TMABar``/``TCGen05Bar``/``MBarrier``) directly -- it has no empty side. Parameters ---------- pool : SMEMPool Shared memory pool allocator. stages : int Number of pipeline stages (barrier slots). full, empty : str Barrier-type tag for the full / empty signal (see above). init_full, init_empty : int Expected arrival count for the full / empty barrier. empty_phase_offset : int XORed into the empty barrier's phase bit on every wait / arrive. leader : Expr, optional Propagated to both barriers; defaults to thread 0 of the CTA. """ def __init__( self, pool, stages, *, full, empty, init_full=1, init_empty=1, empty_phase_offset=0, leader=None, ): self.stages = stages self.full = _BAR_KINDS[full](pool, stages, leader=leader) self.full.init(init_full) self.empty = _BAR_KINDS[empty](pool, stages, phase_offset=empty_phase_offset, leader=leader) self.empty.init(init_empty)