159 lines
5.9 KiB
ReStructuredText
159 lines
5.9 KiB
ReStructuredText
.. 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.
|