Files
wehub-resource-sync 26446540fa
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

207 lines
8.0 KiB
ReStructuredText
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
.. 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.
copy → reg
==========
The ``reg`` variant lowers a synchronous ``copy`` where **exactly one side is a
register** (``local``) buffer and the other is ``shared*`` or ``global``. Unlike
:doc:`gmem_smem`, the partition is **not synthesized** — it is *induced* by the
register operand's layout: that layout's thread-axis iters already say which thread
owns which logical coordinate, so the dispatch drops those axes, leaves each thread
its private bundle of elements, and copies them in a vectorized serial loop. Source:
``python/tvm/backend/cuda/operator/tile_primitive/copy/reg.py``.
What it accepts
---------------
.. code-block:: python
def _is_reg_copy(op_call, sctx):
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 exec_scope {sctx.scope_kind}"
for check in (
lambda: _all_threads_active(sctx),
lambda: _is_valid_copy(op_call, sctx),
lambda: _scope_allowed(op_call, sctx, allowed_pairs=_REG_PAIRS),
lambda: _r_side_layout_valid(op_call, sctx), # the register operand
lambda: _s_side_slice_ok(op_call), # the other operand
):
ok, msg = check()
if not ok:
return False, msg
return True, None
.. list-table::
:header-rows: 1
:widths: 22 78
* - Property
- Requirement
* - target / scope
- ``cuda``; ``thread`` / ``warp`` / ``warpgroup`` / ``cta`` with all threads
active
* - memory pair
- ``_REG_PAIRS`` = ``(local, shared*)`` / ``(shared*, local)`` /
``(local, global)`` / ``(global, local)`` — exactly one side is ``local``
* - register layout
- ``_r_side_layout_valid``: the ``local`` operand is a non-swizzle
``TileLayout`` whose thread-axis iters have **stride 1**, a register-level
subscope no wider than the exec scope, and a **zero sliced thread offset**
(the region doesn't split a thread axis)
* - other side
- ``_s_side_slice_ok``: the ``shared*`` / ``global`` operand slices cleanly to
its region
Demonstration program
----------------------
A warp round-trips a ``32×8`` ``float32`` tile shared → register → shared, with the
register layout ``S[(32,8):(1@laneid, 1)]``**lane ``i`` owns row ``i``** (8
contiguous elements). From ``test_reg.py``:
.. code-block:: python
from tvm.tirx.layout import S, TileLayout, laneid
shape, dtype = (32, 8), "float32"
r_layout = TileLayout(S[shape : (1 @ laneid, 1)]) # lane i -> row i, 8 regs
s_layout = TileLayout(S[shape])
fs = (slice(0, 32), slice(0, 8))
@T.prim_func
def kernel(B_ptr: T.handle):
B = T.match_buffer(B_ptr, shape, dtype)
T.device_entry(); T.cta_id([1]); T.lane_id([32]); tid = T.thread_id([32])
A_smem = T.alloc_buffer(shape, dtype, scope="shared", layout=s_layout)
for kk in range(8): A_smem[tid, kk] = T.cast(tid * 100 + kk + 1, dtype)
T.cuda.cta_sync()
R = T.alloc_buffer(shape, dtype, scope="local", layout=r_layout)
Tx.warp.copy(R[fs], A_smem[fs]) # shared -> register (this dispatch)
# ... clear A_smem, cta_sync ...
Tx.warp.copy(A_smem[fs], R[fs]) # register -> shared (this dispatch)
# ... cta_sync; B[tid, kk] = A_smem[tid, kk] ...
Algorithm
---------
**1. Inherit the partition from R.** The register layout's thread axis (``laneid``)
states that lane ``i`` owns row ``i``; the dispatch aligns the other (shared) side
to that order, then **drops the thread iters** — what remains is each thread's
private memory bundle: here ``8`` contiguous elements per lane.
**2. Linearize and choose the vector width.** The per-thread elements are
flattened into ``(extent, stride)`` atoms; the vector width is chosen widest-first
(``128 → … → 8`` bits) so the contiguous tail divides it and the outer atom
strides + base offsets are aligned. Crucially the **thread-axis strides are
excluded** from this alignment check (they live in *partition-coordinate* space —
which thread owns which element — and never appear in a single thread's physical
address). For ``8`` contiguous ``float32`` that is ``vec = 4``, so ``outer = 2``.
**3. Per-thread base offset + serial loop.** The shared-side base offset is built
from thread-axis placeholders (substituted with the real ``T.lane_id()`` etc.),
and the register side is a flat per-thread ``local`` buffer. The emit is a serial
loop (not ``T.unroll`` — same flooding rationale as :doc:`gmem_smem`):
.. code-block:: python
r_local = r_buf.local(*per_thread_r_shape) # flat per-thread registers
for f in range(total_outer):
ds, dr = _outer_const_offsets(outer, f) # shared / reg deltas
s_ptr = _ptr_off(s_buf.ptr_to(s_zero_indices), _s_iter_off(f, ds, s_off))
r_ptr = _ptr_off(r_local.ptr_to([0]), r_off_base + dr)
if r_is_src:
copy_op(s_ptr, r_ptr) # register -> shared/global
else:
copy_op(r_ptr, s_ptr) # shared/global -> register
Generated TIRx IR
-----------------
``LowerTIRx`` turns the shared → register copy into a per-thread loop over the
8-element register bundle (trimmed):
.. code-block:: python
r_local = T.decl_buffer((8,), data=R.data, scope="local") # 8 regs / lane
for f in range(2): # outer = 8 / vec 4
s_ptr = pointer_offset(A_smem, ...) # this lane's row
r_ptr = pointer_offset(r_local, dr)
T.cuda.copy_bytes(r_ptr, s_ptr, 16) # 16 B = vec 4 × 4 B
(The register → shared copy is the mirror: ``copy_bytes(s_ptr, r_ptr, 16)``.)
Generated CUDA
--------------
.. code-block:: c++
alignas(64) float r_local_ptr[8]; // 8 registers, private to the lane
for (int f = 0; f < 2; ++f) {
void* r_ptr = tvm_builtin_pointer_offset(&r_local_ptr[0], dr);
void* s_ptr = tvm_builtin_pointer_offset(&A_smem_ptr[0], /* lane row + f*4 */);
tvm_builtin_copy_128b(r_ptr, s_ptr); // shared -> register
}
// ... register -> shared mirror writes A_smem back ...
Each lane copies its own 8 elements as 2 × 128-bit transfers; no cross-lane
addressing appears because the thread partition was resolved away at lowering.
How inputs change the algorithm
-------------------------------
The register layout's **per-thread element count** (the non-thread extents — here
``k``) and the **dtype** set the register count, vector width, and round count:
.. list-table::
:header-rows: 1
:widths: 18 18 22 22 20
* - dtype
- ``k``
- regs / lane
- ``vec``
- ``outer = k / vec``
* - ``float32``
- 8
- 8
- 4
- 2
* - ``float32``
- 16
- 16
- 4
- 4
* - ``float16``
- 8
- 8
- 8
- 1
* - ``float16``
- 16
- 16
- 8
- 2
The copy is always a 128-bit transfer (``copy_bytes = 16``) when the contiguous
tail allows. The **scope** sets the thread axis (``warp````laneid``, ``cta``
``tx``, …) the register layout must use; a different R layout (e.g. a strided or
multi-row ownership) changes which elements each lane holds and therefore the atom
list and ``outer``.