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

210 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 → gmem_smem
================
The ``gmem_smem`` variant lowers a synchronous ``copy`` between **global and
shared** memory (either direction) when **neither side is a register**. Because
neither operand carries a thread partition, the dispatch *synthesizes* one from the
execution scope: it splits the region into ``[outer, threads, vec]`` and emits a
serial loop of vectorized loads/stores. Source:
``python/tvm/backend/cuda/operator/tile_primitive/copy/gmem_smem.py``.
What it accepts
---------------
The predicate ``_is_gmem_smem`` gates the variant:
.. code-block:: python
def _is_gmem_smem(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), # full scope, no narrowing
lambda: _is_valid_copy(op_call, sctx), # layouts, equal dtype/extents
lambda: _scope_allowed(op_call, sctx, allowed_pairs=_GMEM_SMEM_PAIRS),
lambda: _divides_thread_cnt(op_call, sctx),
):
ok, msg = check()
if not ok:
return False, msg
return True, None
So the accepted input is:
.. list-table::
:header-rows: 1
:widths: 22 78
* - Property
- Requirement
* - target
- ``cuda``
* - scope
- ``thread`` / ``warp`` / ``warpgroup`` / ``cta``, and **all threads active**
(``_all_threads_active````laneid`` spans 32, etc., none narrowed by an
enclosing ``if``)
* - memory pair
- ``(global, shared*)`` or ``(shared*, global)````_GMEM_SMEM_PAIRS``;
neither side is ``local``
* - dtype / shape
- both operands have a layout, equal dtype, equal non-unit extents
(``_is_valid_copy````validate_copy_op``)
* - divisibility
- the region's element count is divisible by the thread count
(``_divides_thread_cnt``) — otherwise the ``[outer, threads, vec]`` split has
no integer solution and the variant declines
Demonstration program
----------------------
A warp (32 threads) copies a ``32×32`` ``float32`` tile global → shared and back
(the round trip from ``test_gmem_smem.py``):
.. code-block:: python
from tvm.script import tirx as T
from tvm.script.tirx import tile as Tx
from tvm.tirx.layout import S, TileLayout
shape, dtype = (32, 32), "float32"
s_layout = TileLayout(S[shape])
fs = (slice(0, 32), slice(0, 32))
@T.prim_func
def kernel(A_ptr: T.handle, B_ptr: T.handle):
A = T.match_buffer(A_ptr, shape, dtype)
B = T.match_buffer(B_ptr, shape, dtype)
T.device_entry()
T.cta_id([1]); T.lane_id([32]); T.thread_id([32])
A_smem = T.alloc_buffer(shape, dtype, scope="shared", layout=s_layout)
Tx.warp.copy(A_smem[fs], A[fs]) # global -> shared (this dispatch)
T.cuda.cta_sync()
Tx.warp.copy(B[fs], A_smem[fs]) # shared -> global (this dispatch)
Algorithm
---------
**1. Synthesize the partition.** With 32 threads and ``32×32 = 1024`` elements,
the dispatch builds a 3-D iteration ``[outer, threads, vec]`` via
``align_layouts_gs``: it slices both layouts to the region, makes the **global**
side drive the canonical (stride-descending) order, then carves a contiguous
``vec`` tail and a ``threads`` chunk off it and re-groups the shared side to match.
**2. Choose the vector width, widest first.** It tries element counts for
``{128, 64, 32, 16, 8}`` bits and accepts the widest where (a) the contiguous tail
divides it and (b) every **non-vec iter stride** (on both sides, the thread iter
included) and both base offsets is a multiple of it, so the per-thread, per-round
vector pointer is naturally aligned. (Only the innermost ``vec`` iter is excluded
from that check.) For ``float32`` that is ``vec = 4`` (``4 × 4 B = 16 B = 128 bit``),
giving ``outer = 1024 / (32 × 4) = 8``.
**3. Emit a serial loop** (`gmem_smem.py`) — deliberately a Python ``for`` (so
ptxas unrolls it), *not* ``T.unroll``:
.. code-block:: python
for f in range(total_outer):
s_lin = s_p.apply(f, tid, v0, shape=apply_shape)["m"] # shared element offset
g_lin = g_p.apply(f, tid, v0, shape=apply_shape)["m"] # global element offset
s_off = _s_off(f, s_lin) # apply swizzle if any
s_ptr = _ptr_off(s_buf.ptr_to(s_zero), s_off)
g_ptr = _ptr_off(g_buf.ptr_to(g_zero), g_lin)
if g_is_src:
copy_op(s_ptr, g_ptr) # global -> shared
else:
copy_op(g_ptr, s_ptr) # shared -> global
Each ``(f, tid, 0)`` coordinate is flattened by ``layout.apply`` against
``[outer, threads, vec]``, so the emit never needs to know how the partition split
the iters; ``copy_op`` is ``T.cuda.copy_{vec_bits}b`` (here ``copy_128b``).
Generated TIRx IR
-----------------
Running ``LowerTIRx`` on the program above turns each ``Tx.warp.copy`` into the
synthesized loop (global → shared shown, trimmed):
.. code-block:: python
tid: T.let = threadIdx_x % 32
A_smem = T.alloc_shared((1024,))
for f in range(8): # outer = 8
s_lin = f * 128 + tid * 4 # 32 threads × vec 4 = 128 / round
g_lin = f * 128 + tid * 4
s_ptr = pointer_offset(A_smem, s_lin)
g_ptr = pointer_offset(A_1, g_lin) # A_1 = A.view(1024)
T.cuda.copy_bytes(s_ptr, g_ptr, 16) # 16 B = vec 4 × 4 B
Generated CUDA
--------------
.. code-block:: c++
extern "C" __global__ void __launch_bounds__(32)
kernel_kernel(float* __restrict__ A_ptr, float* __restrict__ B_ptr) {
int tid = ((int)threadIdx.x);
__shared__ alignas(64) float A_smem_ptr[1024];
for (int f = 0; f < 8; ++f) {
int s_off = (f * 128) + (tid * 4);
void* s_ptr = tvm_builtin_pointer_offset(&A_smem_ptr[0], s_off);
void* g_ptr = tvm_builtin_pointer_offset(&A_ptr[0], s_off);
tvm_builtin_copy_128b(s_ptr, g_ptr); // 128-bit vector load+store
}
// ... __syncthreads(); then the shared -> global loop into B_ptr ...
}
Thread ``tid`` handles elements ``[f·128 + tid·4 .. +4)`` each round; across 8
rounds and 32 lanes that covers all 1024 elements, each as one 128-bit transfer.
How inputs change the algorithm
-------------------------------
The element **dtype** sets the vector width (widest 128-bit transfer that stays
aligned), which sets the round count. For the same ``32×32`` tile and 32 threads:
.. list-table::
:header-rows: 1
:widths: 20 20 20 40
* - dtype
- ``vec``
- ``copy_bytes``
- ``outer = 1024 / (32 · vec)``
* - ``float32``
- 4
- 16 (``copy_128b``)
- 8
* - ``float16``
- 8
- 16 (``copy_128b``)
- 4
* - ``uint8``
- 16
- 16 (``copy_128b``)
- 2
The **scope** sets which axis names the thread id (``warp````laneid``,
``cta````tx``, …) and the thread count, hence the partition. A **swizzled**
shared layout caps ``vec`` to one swizzle chunk and routes ``s_off`` through the
swizzle (a recognized swizzle becomes a few register adds per round; otherwise
``swizzle.apply`` per round).