cvt#

cvt performs a numeric conversion selected by the opcode. A dtype view only reinterprets bits; it is used here to expose the native carrier layout, not to perform the conversion itself. Destination and source widths, operand group lengths, rounding, saturation and packing come from the PTX form.

Scalar conversion uses tensor suffix (1,) on both sides. Packed cvt.rn.f16x2.f32 has three operands: one b32 destination, a float32 source for its high halfword, and a float32 source for its low halfword. All three have suffix (1,). The pair is not an inferred vectorization of a scalar cast.

Example: eight float32 values to four packed half pairs#

from tvm.script import tirx as T


@T.function
def narrow_pairs(A: T.Tensor((4, 2), "float32"), Out: T.Tensor((4, 1), "uint32")):
    T.device_entry(launch=T.cuda.LaunchConfig(grid=1, block=1))
    a = T.alloc_local((4, 2), "float32", layout="default")
    d = T.alloc_local((4, 1), "uint32", layout="default")
    T.cuda.tile.apply(T.ptx["ld.global.v2.f32"], a, A)
    T.cuda.tile.apply(T.ptx["cvt.rn.f16x2.f32"], d, a[:, 1:2], a[:, 0:1])
    T.cuda.tile.apply(T.ptx["st.global.b32"], Out, d)

For each input row [x0,x1], the output word contains converted x0 in its low half and x1 in its high half. Reversing the two source operands would exchange the output elements. The expansion emits four packed cvt instructions, one per row.

import numpy as np
import tvm

x = np.linspace(-2, 2, 8, dtype="float32").reshape(4, 2)
compiled = tvm.tirx.compile(narrow_pairs, target="cuda", pipeline="tirx")
out = tvm.runtime.tensor(np.zeros((4, 1), dtype="uint32"), tvm.cuda())
compiled(tvm.runtime.tensor(x, tvm.cuda()), out)
np.testing.assert_array_equal(out.numpy().view("float16"), x.astype("float16"))

The inverse operation is two scalar cvt.f32.f16 instructions per packed word; there is no corresponding cvt.f32x2.f16x2 spelling in this API. Expose the packed source with a uint16 view, add a singleton register-group axis per half, and apply the scalar opcode. For a composite conversion expression, including explicit intermediate rounding, use ewise: caller-defined scalar compositions.

Register ownership must agree on both sides even when their storage widths differ. A wrong carrier width, missing group axis, or overlapping destination is rejected. There is no implicit staging through shared memory.

Implementation: python/tvm/backend/cuda/tile/patterns/register.py.