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

159 lines
5.9 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 → fallback
===============
The ``fallback`` variant is the **priority-0 catch-all**: a scalar, single-thread
copy that runs only when every faster variant (:doc:`gmem_smem`, :doc:`reg`,
:doc:`ldstmatrix`) has declined. It always works — for any valid copy at any scope
— and is intentionally slow, so it emits a ``UserWarning`` when chosen. Source:
``python/tvm/backend/cuda/operator/tile_primitive/copy/fallback.py``.
What it accepts
---------------
Any valid copy. The only gate is ``_is_valid_copy`` (layouts present, equal dtype,
equal non-unit extents); there is no scope, pair, or divisibility restriction. It is
registered at ``priority=0`` so it is the last candidate the dispatcher tries:
.. list-table::
:header-rows: 1
:widths: 22 78
* - Property
- Requirement
* - priority
- ``0`` — only reached after all priority-10 variants ``fail`` / decline
* - target / scope
- any (``thread`` / ``warp`` / ``warpgroup`` / ``cta``)
* - memory pair
- any (global / shared / local, either direction)
* - dtype / shape
- ``_is_valid_copy`` only — equal dtype and equal non-unit extents
A typical reason the fast variants all decline: the region's element count does not
divide the thread count, so the ``[outer, threads, vec]`` split has no solution
(``gmem_smem`` declines), and neither side is a register layout (``reg`` /
``ldstmatrix`` decline). Example: a warp (32 threads) copying a ``4×6 = 24``-element
tile (``24 ∤ 32``).
Demonstration program
----------------------
A warp copies a ``4×6`` ``float32`` tile global → shared and back. ``24`` is not
divisible by ``32``, so this falls through to ``fallback`` (from
``test_fallback.py``):
.. code-block:: python
shape, dtype = (4, 6), "float32" # 24 elements, 32 threads -> 24 ∤ 32
s_layout = TileLayout(S[shape])
full = (slice(0, 4), slice(0, 6))
@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[full], A[full]) # fallback
T.cuda.cta_sync()
Tx.warp.copy(B[full], A_smem[full]) # fallback
Algorithm
---------
**1. Elect one thread.** For a multi-thread scope the copy is done entirely by the
**first thread of the scope** — its ``laneid`` base, plus the warp offset within a
warpgroup / cta (radix-32 composition of the per-axis offsets); every other thread
skips it. For ``thread`` scope there is only one thread, so no guard is emitted:
.. code-block:: python
if scope_kind == "thread":
def impl():
_copy_body(dst, src) # the single thread copies everything
else:
first_tid = int(sctx.intra["laneid"][1]) # first thread of the scope:
if scope_kind == "warpgroup":
first_tid += 32 * int(sctx.intra["wid_in_wg"][1]) # + warp offset within the wg
elif scope_kind == "cta":
first_tid += 32 * int(sctx.intra["warpid"][1]) # + warp offset within the cta
def impl():
tid = _axis_decl(tid_axis_name, sctx)
if tid == first_tid: # only the scope's first thread copies
_copy_body(dst, src)
**2. Scalar nested loop over the region.** ``_copy_body`` iterates the non-unit
extents with ``T.grid`` and copies one element per step — no vectorization, no
partition:
.. code-block:: python
with T.grid(*copy_extents) as lvs: # copy_extents = non-unit dst extents
dst[_dst_coord(lvs)] = src[_src_coord(lvs)]
Generated TIRx IR
-----------------
``LowerTIRx`` produces the guarded scalar grid (global → shared shown):
.. code-block:: python
if tid == 0: # first_tid (lane 0 for this warp scope)
for v_3, v_4 in T.grid(4, 6):
A_smem[v_3, v_4] = A[v_3, v_4]
Generated CUDA
--------------
.. code-block:: c++
__shared__ alignas(64) float A_smem_ptr[24];
if (((int)threadIdx.x) == 0) { // lane 0 does the whole copy
for (int v_3 = 0; v_3 < 4; ++v_3)
for (int v_4 = 0; v_4 < 6; ++v_4)
A_smem_ptr[(v_3 * 6) + v_4] = A_ptr[(v_3 * 6) + v_4];
}
Lane 0 copies all 24 elements one at a time; the other 31 lanes do nothing. (At
lowering the dispatch also prints ``UserWarning: copy/fallback (scalar
single-thread) picked … all faster variants rejected``.)
How inputs change the algorithm
-------------------------------
The **scope** decides who runs the loop:
.. list-table::
:header-rows: 1
:widths: 30 70
* - scope
- who copies
* - ``thread``
- the single thread, no ``if`` guard
* - ``warp`` / ``warpgroup`` / ``cta``
- only ``first_tid`` (the scope's first thread = lane base + warp offset); all
other threads skip
The **shape** sets the ``T.grid`` bounds (the non-unit extents); the loop body is
always one scalar element copy, regardless of dtype. There is no vectorization, so
performance does not depend on dtype width — this variant exists for correctness,
not speed.