Batched-1D GEMM APIs#
GLASS provides batched GEMM primitives that run inside a single 1D thread
block. They exist because the cuBLASDx-backed glass::nvidia::gemm_batched
requires a 2D launch (dim3(TC, BATCH)) — it gives each batch element a
threadIdx.y slot. Kernels that were launched 1D (dim3(TC*BATCH, 1, 1),
because every other block-level helper uses threadIdx.x) cannot use it
without rewriting their launch geometry.
The batched-1D APIs solve this for the small shapes — max(M,N,K) ≲ 8 — where
cuBLASDx’s tile-load overhead dominates and SIMT wins anyway.
Why a SIMT implementation#
cuBLASDx’s collective operations (cublasdx::copy, GEMM().execute()) read
threadIdx.x directly and assume the whole block participates; there is no
built-in way to tell cuBLASDx “use only threads [b*TC, (b+1)*TC) for this
batch element”. A clean 1D-launch batched GEMM therefore cannot use cuBLASDx
unchanged.
For the entire small-shape dynamics workload (M,N,K ≤ ~8), cuBLASDx is not
faster than SIMT anyway, so these APIs are pure SIMT: a single 1D block of
TC*BATCH threads is partitioned into BATCH groups of TC threads, and
each group computes one independent GEMM. They need no shared memory and
no DEFINE_NVIDIA_* macro — they are fully templated on T.
The two APIs#
gemm_batched_1d#
BATCH independent GEMMs of the same shape, each with its own A/B/C pointer
(passed as pointer arrays):
__global__ void k(float* const* A, float* const* B, float* const* C) {
// No DEFINE macro needed — fully templated on T.
glass::nvidia::gemm_batched_1d<float, 4, 4, 4, /*BATCH=*/8, /*TC=*/32>(
1.f, A, B, 0.f, C);
}
k<<<1, dim3(32 * 8, 1, 1)>>>(dA_ptrs, dB_ptrs, dC_ptrs); // no smem
Threads required: gemm_batched_1d_threads<T,M,N,K,BATCH,TC>() == TC * BATCH.
gemm_strided_batched_1d#
The shared-A variant: a single A matrix broadcast across BATCH packed
(B, C) pairs. The caller passes one A pointer plus base B/C pointers; GLASS
indexes B[b * B_STRIDE] and C[b * C_STRIDE] internally — no pointer
arrays to set up:
__global__ void k_shared(float* A_shared, float* B_base, float* C_base) {
// tightly packed: B_STRIDE = N*K, C_STRIDE = M*K (the defaults)
glass::nvidia::gemm_strided_batched_1d<float, 4, 4, 4, 8, 32>(
1.f, A_shared, B_base, 0.f, C_base);
}
This is the GRiD end-effector-pose-gradient case, where &s_Xhom[16*parent]
is the shared A for every batch element. B_STRIDE and C_STRIDE are
template parameters (defaults: N*K and M*K for tight packing).
Note
A related primitive, glass::gemm_batched_indexed
(src/base/L3/gemm_batched_indexed.cuh), gathers per-batch operands
through an index array at irregular offsets. It lives in glass:: (not
glass::nvidia::) and uses a different model — a block stride over the
flattened output elements with atomic/transpose flags — not the TC-group
partitioning of the two 1D-partition APIs above. It is documented with the
other L3 ops, not here.
When to use which#
API |
Use when |
|---|---|
|
Each batch element has its own independent A, B, and C pointer. |
|
One shared A applied to many B/C blocks at a regular stride (cleanest codegen — no pointer arrays). |
Both run pure SIMT, need no shared memory, and are best for small shapes
(max(M,N,K) ≲ 8) where cuBLASDx’s tile-load overhead dominates. They respect
the TRAILING_SYNC template parameter (see Trailing Synchronization (TRAILING_SYNC)).
Note
For large batched shapes (M,N,K ≥ 16) where cuBLASDx would win, use the
2D-launch glass::nvidia::gemm_batched<...,BATCH,TC> instead and pay the
dim3(TC, BATCH) launch geometry. The batched-1D path deliberately does
not attempt to wrap cuBLASDx.
See bench/bench_gemm_batched_1d.cu for a working example and the
microbenchmark that feeds the autotune table.