reasoning
We need to replace the forward computation of a PyTorch model that computes cumulative product along a specified dimension.
The dimensions are batch_size = 32768 and input_shape=(32768,), so the tensor is [32768, 32768]. dim=1.
Computing cumprod on a tensor of shape [32768, 32768] in fp32. The reference is `torch.cumprod(x, dim=1)`. We need a custom CUDA kernel that does this faster.
We need to consider the performance of PyTorch's `cumprod` along dimension 1. In PyTorch, `cumprod` on CUDA likely uses a scan algorithm, possibly not super optimized. Since the tensor is huge (32768 x 32768 = 1,073,741,824 elements, ~4 GB in fp32), memory bandwidth and compute are both significant.
The kernel is a 1D exclusive/inclusive scan per row. For each row, we need to compute the cumulative product. The product of 32k numbers in fp32 can overflow/underflow easily, but input is random in [0,1], so values quickly approach zero. However, the reference does this in fp32, so we just need to match.
Key algorithmic idea: Since the row length is 32768, we can do a Blelloch (work-efficient) scan or a warp scan. With row length 32768 = 2^15, maybe we can tile into blocks of e.g. 1024 threads.
Each block processes one row or multiple rows? 32768 rows and 32768 cols. We have 32768 rows.
If we assign one block per row: 32768 blocks, each block has 1024 threads. Then each thread processes 32 elements (since 32768/1024 = 32). That's reasonable. We can do an intra-block scan using shared memory, but each block would need to handle a prefix of length 32768, which might require multiple block passes if each block handles a subsegment, and then a reduction across blocks for the row. However, since row length = 32768 which is moderate, we could also have each block process a chunk and then do a two-level scan: block-level scan within the block, then a second kernel to propagate the block prefixes, or a single kernel using thread block clusters or cooperation? Actually, we can assign multiple blocks per row (e.g., 32 blocks per row, 1024 threads each -> 32768 threads per row? That would be too many). Better: assign one block per row with enough threads to cover the row. Since max threads per block is 1024, we can have each thread process 32 items. We can perform an intra-thread sequential scan on its 32 items, then do a scan across threads (e.g., warp shuffle or shared memory) to get the prefix for each thread, then apply it.
Let's design a kernel:
For each row r (0 <= r < rows):
tid = threadIdx.x
lane = tid % 32
warp = tid / 32
num_warps = blockDim.x / 32 = 32 (since blockDim.x = 1024)
Elements per thread: items_per_thread = cols / blockDim.x = 32768 / 1024 = 32
Each thread loads its 32 elements into a local array (e.g., float reg[32]).
Compute inclusive scan within the thread: local_prod[i] = product of elements 0..i in the thread's chunk.
Actually, we want thread-local inclusive scan: for i in 0..31: thread_prefix[i] = thread_prefix[i-1] * x_i.
Then warp_prefix = the last element of each thread's local scan (i.e., thread's total product).
Now perform a warp-level inclusive scan of warp_prefix using shfl or smem. Let's use shared memory of size num_warps (32). Actually we can do a block-level scan of the warp aggregates.
We have 32 warps, each warp has 32 threads. Wait blockDim.x = 1024 = 32 warps * 32 threads.
Each thread has 32 elements, so total elements per warp = 32 threads * 32 = 1024 elements. So each warp handles 1024 elements. There are 32 warps, handling 32*1024 = 32768 elements total. Good.
Now we need to propagate the prefix across warps. Let's compute warp_scan_inclusive of warp_prefix. Use a shared memory array of size 32 (one per warp).
Within a warp, all threads write the warp_prefix to smem[warp_id]? Actually each warp can have lane 0 write to smem[warp_id] after computing the warp_prefix for each thread? Wait warp_prefix per thread is the product of all 32 elements in that thread. But within a warp, we need to scan across threads. Actually, we can do warp-level inclusive scan of thread_prefix (the product of all elements in each thread). Each thread in the warp has a value `thread_sum` (the product of its 32 elements). We can compute an inclusive scan of `thread_sum` within the warp using shfl up. Then each thread can compute its offset as the inclusive scan result minus its own contribution (i.e., prefix before this thread). But for multiplication, inclusive scan of `thread_sum`:
prefix[tid] = product_{j=0}^{tid} thread_sum[j]
Then each thread's starting multiplier for its local elements is prefix[tid] / thread_sum[tid] (or if we do exclusive scan, it's product of previous threads). Better: compute exclusive scan of `thread_sum` within the warp, store as `warp_exclusive`. Then each thread multiplies its local elements by `warp_exclusive * (thread's own prefix of previous elements within its 32)`.
Then we need to combine across warps: each warp has a total product = product of all `thread_sum` in that warp (which is prefix of last thread). We can do a block-level exclusive scan of `warp_total` across warps using shared memory. Then add the block-level prefix to each thread.
Let's outline more concretely:
Grid: (rows, 1, 1). Each block processes one row.
BlockDim.x = 1024.
Items per thread = cols / 1024 = 32.
```
__global__ void cumprod_kernel(const float* input, float* output, int rows, int cols) {
int row = blockIdx.x;
if (row >= rows) return;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
const int items_per_thread = 32; // cols / blockDim.x
const int num_warps = blockDim.x >> 5; // 32
// load 32 elements per thread
float local[32];
const float* row_in = input + row * cols;
float* row_out = output + row * cols;
#pragma unroll
for (int i = 0; i < items_per_thread; ++i) {
int idx = tid * items_per_thread + i;
local[i] = row_in[idx];
}
// Step 1: intra-thread inclusive scan
float thread_scan[32];
float acc = 1.0f;
#pragma unroll
for (int i = 0; i < items_per_thread; ++i) {
acc *= local[i];
thread_scan[i] = acc;
}
float thread_total = acc; // product of this thread's 32 elements
// Step 2: warp-level exclusive scan of thread_total
// Compute inclusive scan using shfl, then derive exclusive.
float inclusive = thread_total;
#pragma unroll
for (int offset = 1; offset < 32; offset <<= 1) {
float other = __shfl_up_sync(0xFFFFFFFF, inclusive, offset);
if (lane >= offset) inclusive *= other;
}
// inclusive now has product of thread_total for threads 0..lane in this warp.
float warp_exclusive = inclusive / thread_total; // product of previous threads in warp
// For lane==0, thread_total may be 0? Wait if thread_total=0, division by zero.
// We should handle zero carefully. But input is random uniform [0,1]. Probability of exactly 0 is zero.
// However, due to floating point rounding, thread_total could be zero if any element is zero or underflow.
// Division by zero is a problem. Safer: compute exclusive scan directly.
```
We need to handle zero robustly for multiplication exclusive scan via division. Better to compute exclusive scan without division.
Alternative for warp-level exclusive scan:
```
float lane_val = thread_total;
// inclusive scan
for offset...
// exclusive: shift inclusive by one.
float warp_exclusive;
if (lane == 0) warp_exclusive = 1.0f;
else {
// get inclusive of previous lane
warp_exclusive = __shfl_sync(0xFFFFFFFF, inclusive, lane - 1);
}
```
Yes, that avoids division.
Similarly for block-level exclusive scan of warp_total: we can compute `warp_total` for each warp (the product of all thread_total in that warp = the inclusive scan result of lane 31). Then do a block-level exclusive scan of warp_total in shared memory. Since there are at most 32 warps, we can have lane 0 of each warp write warp_total to `smem[warp_id]`, then one warp (e.g., warp 0) does the exclusive scan in shared memory, and then lane 0 of each warp reads back the exclusive prefix.
Let's detail:
```
__shared__ float warp_smem[32]; // one per warp
__shared__ float block_smem[32];
```
Actually we can use one shared memory array of size 32.
After Step 2 (warp inclusive scan), we need `warp_total`, the product of all threads in this warp. That's the inclusive result from lane 31. So lane 31 can broadcast it:
```
float warp_total = __shfl_sync(0xFFFFFFFF, inclusive, 31);
```
Each warp's lane 0 writes `warp_total` to `warp_smem[warp_id]`.
Then after `__syncthreads()`, warp 0 performs an exclusive scan on `warp_smem` (size num_warps = 32).
```
if (tid < num_warps) {
float val = warp_smem[tid];
// exclusive scan in a small array
// can do simple sequential
// but tid only up to 31
// Let lane = tid
float running = 1.0f;
// actually we need to compute exclusive scan for each tid
// easier: have tid 0 compute prefix array in tmp[32]
}
```
Simpler: have thread 0 compute it:
```
__shared__ float warp_prefix[32];
if (tid == 0) {
float running = 1.0f;
for (int i = 0; i < num_warps; ++i) {
float t = warp_smem[i];
warp_prefix[i] = running;
running *= t;
}
}
__syncthreads();
```
Then each thread reads `block_prefix = warp_prefix[warp_id];`. This is the product of all warps before this warp.
Then the true multiplier for this thread is:
`mult = block_prefix * warp_exclusive`
Then for each element i in local array:
`output[idx] = mult * thread_scan_before_i`
where `thread_scan_before_i` is the inclusive scan up to i within the thread. Wait, we need the cumulative product of all elements before the current element.
For element i in this thread:
prefix = block_prefix * warp_exclusive * (product of local[0..i-1])
where product of local[0..i-1] is `thread_exclusive_i`. We can compute this as:
if i==0: thread_exclusive = 1.0f
else: thread_exclusive = thread_scan[i-1]
Then output = prefix * local[i]? Wait, the cumulative product up to and including element i is prefix * local[i] if prefix is product of all elements BEFORE this element. Actually:
cumprod(element) = (product of all previous elements) * element.
inclusive scan of element i is prefix * thread_scan[i]?
Let's define:
`before = block_prefix * warp_exclusive * thread_exclusive_i`
Then `out = before * local[i]` = prefix of all previous elements times current element = inclusive product.
But note `thread_scan[i]` is the inclusive product of local[0..i], i.e., `thread_exclusive_i * local[i]`. So `out = block_prefix * warp_exclusive * thread_scan[i]`.
Yes! Because `thread_scan[i]` already includes `local[i]`. So the output for element idx = tid*items_per_thread + i is:
`out = block_prefix * warp_exclusive * thread_scan[i]`.
Wait, check:
- For i=0, thread_scan[0] = local[0]. `out = block_prefix * warp_exclusive * local[0]`. `block_prefix * warp_exclusive` is product of all elements in all previous warps and all previous threads in this warp. That is correct.
- For i=31, `out = block_prefix * warp_exclusive * thread_scan[31]`. `thread_scan[31]` = thread_total. So `out` = product of all elements up to the end of this thread. Correct.
So we can compute:
```
float mult = block_prefix * warp_exclusive;
#pragma unroll
for (int i = 0; i < items_per_thread; ++i) {
int idx = tid * items_per_thread + i;
row_out[idx] = mult * thread_scan[i];
}
```
That seems correct.
But we need to handle the case where cols is not a multiple of blockDim.x * items_per_thread? Here 32768 / 1024 = 32 exactly. So it's fine.
Potential issues:
- `__shfl_up_sync` and `__shfl_sync` require compute capability >= 3.0. Usually fine.
- Shared memory usage: small (32 floats = 128 bytes). Register usage: local array of 32 floats. That's 32 registers for the data + maybe 32 for scan. So ~64 registers plus some temporaries. That's okay. 1024 threads * 64 registers = 65536 registers. Each SM has 65536 registers. So we might hit register limit. Actually each SM on modern GPU has 65536 registers (for consumer/enterprise). For 1024 threads, max registers per thread is 64. If we use >64 registers, occupancy drops. We should try to reduce register pressure.
The `local` and `thread_scan` arrays can be the same array to save registers:
```
float local[32];
// load into local
// then do in-place inclusive scan
float acc = 1.0f;
#pragma unroll
for (int i = 0; i < 32; ++i) {
acc *= local[i];
local[i] = acc;
}
float thread_total = acc;
```
Then output uses `local[i]` as the inclusive scan.
That's still 32 floats in registers. Each float is a register. 32 registers. Plus `acc`, `thread_total`, `mult`, some temporaries for shfl, loop counters. Total maybe ~40 registers. Under 64. Good.
Now performance: The kernel reads 4 GB and writes 4 GB. The product itself is one mul per element. Memory bandwidth bound. The theoretical memory bandwidth of the GPU (likely A100 or H100) is 1.5-2 TB/s or 3 TB/s. For 8 GB total traffic, at 2 TB/s it's 4 ms. PyTorch's `cumprod` might be doing multiple passes or using atomics or not optimizing for this exact shape. Our kernel does one pass per element,reading and writing once. So it could be close to bandwidth bound.
But there is an issue: because of the sequential dependence within each thread, the instruction pipeline might have some stalls, but since each thread processes 32 elements sequentially, the latency of multiplication is small. We can also unroll the loop.
We need to consider that blockDim.x = 1024, gridDim.x = 32768. That's 32768 blocks. On an A100 with 108 SMs, that's about 300 blocks per SM. Launch overhead is fine.
Potential further optimization: We can process multiple rows per block to improve occupancy? Actually 32768 blocks is plenty. Occupancy might be limited by registers. 1024 threads per block with 40 registers -> 40960 registers per block. SM might schedule one block at a time if it has 65536 registers. Occupancy = 1024 / 2048 = 50% (if max threads per SM is 2048). That's fine.
Alternatively, we could reduce block size to 512 threads, each handling 64 elements. Then register usage per thread still ~32 for the array + a few. Total registers per block ~17000. Two blocks can fit per SM, giving 100% occupancy (512*2=1024 threads). Might be better. But the memory access pattern: each thread reads 64 elements strided by blockDim.x. With blockDim 512, the warp reads contiguous segments of 64*32 = 2048 elements? Let's see: thread 0 reads indices 0, 512, 1024, ...? No, we load `idx = tid * items_per_thread + i`. If items_per_thread = 64, then thread 0 reads 0..63, thread 1 reads 64..127, etc. That's coalesced: consecutive threads read consecutive memory. Actually thread 0 reads 0, thread 1 reads 64? Wait, if items_per_thread = 64, tid=0 reads 0..63, tid=1 reads 64..127. Within a warp (32 threads), they read indices 0..63, 64..127, ..., 1984..2047. This is perfectly coalesced: memory requests per warp are contiguous 2048 floats = 8192 bytes. That's two 4KB sectors or something. Actually memory transaction size is 32 bytes, so 8192/32 = 256 transactions. With 32 threads, that's 8 transactions per thread? Wait each thread reads 64 floats = 256 bytes. A warp of 32 threads reads 8192 bytes, which is 256 cache lines of 32 bytes? Actually L2 cache line is 32 bytes. So 256 transactions? That seems a lot. But modern GPUs coalesce into 128-byte transactions. 8192 bytes / 128 = 64 transactions per warp. That's fine.
But the issue is that each thread's 64 loads are sequential and can be pipelined. The memory throughput should be near peak.
With blockDim 1024, each warp handles 32*32 = 1024 elements. The memory pattern is still coalesced. However, 1024 threads per block means 32 warps. With 50% occupancy, maybe not an issue. But if register count is high, occupancy drops.
Let's estimate register count precisely. The array `float local[32]` will be stored in registers if the compiler decides so. 32 registers. `acc` = 1. `thread_total` = 1. `warp_exclusive` = 1. `mult` = 1. `row` = 1. `row_in` = 1. `row_out` = 1. `tid`, `lane`, `warp_id` might be in registers or immediate. `idx` in loop unrolled may be computed immediate. So maybe ~40 registers. 40 * 1024 = 40960 < 65536. So one block per SM (1024 threads). Max threads per SM is 2048 (A100), so occupancy = 50%. Good enough.
If we drop to blockDim 512, items_per_thread = 64. Array `local[64]` uses 64 registers. Then plus other registers maybe 72. 72 * 512 = 36864. Two blocks per SM => 1024 threads, occupancy 50% again. Actually max threads 2048, so could fit 4 blocks of 512 if registers allow? 36864 * 4 = 147456 > 65536, so no, only 1 block. Wait 65536 / 36864 = 1 block. So occupancy is 512/2048 = 25%. That's worse! So 1024 threads with 32 array size is better for occupancy.
Could we use `float local[32]` but process two rows per block? That would double the register usage if we do two arrays, but we can process sequentially: one row then another. But grid would be halved. Not necessary.
Another thought: 32768 rows, each block one row. We could assign one warp per row and have each thread in the warp process 1024 elements? That would be 32 threads * 1024 = 32768. Then we'd have 32768 warps, = 1024 blocks of 32 warps? Wait, if one warp per row, we need 32768 warps total. With 32 warps per block (1024 threads), that's 1024 blocks. Each block handles 32 rows. This might be great because each row is processed by a warp, and we can use warp shuffles for the scan across the 32 threads (each handling 1024 elements). The intra-thread scan would be over 1024 elements, which is a lot but fine. Register usage: each thread has `float local[1024]`? That's impossible. We can't hold 1024 floats in registers. We would need to stream: each thread processes 1024 elements in a loop of 32 chunks of 32, storing partial results in registers or shared memory.
Alternatively, assign one thread per row: 32768 threads total, each thread iterates over 32768 elements and does a sequential scan. That is super slow because memory is not coalesced (threads in a warp read different rows, so contiguous memory across threads is not contiguous). Actually if each thread reads its row sequentially, warps issue strided loads (thread 0 reads col 0 of row 0, thread 1 reads col 0 of row 1, etc). That is coalesced across rows! Yes: for a fixed column c, threads in a warp read `input[row*cols + c]` for different row. If rows are consecutive, these addresses are separated by `cols` elements. `cols = 32768`, so offset between threads is 32768*4 = 131072 bytes. That's way larger than a cache line. Memory coalescing would be terrible: each warp reads 32 floats from 32 different cache lines. So bandwidth would be abysmal.
Therefore, processing a row with a full block is the right approach for coalescing. Since the tensor is row-major, processing contiguous columns within a row with contiguous threads is best.
Could we use shared memory to stage the row and do a tree-based scan? For 32768 floats = 128 KB per row. Shared memory per SM is ~164 KB (A100). We could load an entire row into shared memory? 128 KB is within limit. Then 1024 threads could operate on it. But shared memory bank conflicts might be an issue if not careful. However, 128 KB per block means only 1 block per SM (since 164 KB). Occupancy would be low (1024 threads / 2048 = 50% again). But using shared memory might reduce register usage. However, scanning in shared memory with strided access could have bank conflicts.
But the simple register-based approach is likely fine and close to optimal.
Let's look at potential bottlenecks. The kernel does:
- Global read (4 GB)
- 32 multiplies per thread (1.07B multiplies total)
- Shuffle instructions
- Shared memory read/write (small)
- Global write (4 GB)
So the arithmetic intensity is very low: 1 op per 8 bytes (read+write). Memory bandwidth bound. As long as we achieve near peak bandwidth, we are good. The reference `torch.cumprod` likely also is memory bandwidth bound, but maybe it does extra passes or has some overhead that makes it slower.
One possible issue: Because the scan is sequential within each thread, the latency of the multiply chain might limit instruction throughput. But we have ILP because each thread does 32 independent multiplies? Actually they are dependent: `acc = acc * local[i]`. That's a dependency chain of 32 multiplies. FP32 multiply latency is ~4 cycles. With 32 sequential dependent muls, that's ~128 cycles per thread. But since we have 1024 threads per SM (or 2048 if occupancy allows), we can hide latency via thread switching. However, 50% occupancy gives 1024 threads per SM, which is enough to hide ~4 cycle latency? Usually need ~6-8 warps per SM to hide latency. 1024 threads = 32 warps. That's plenty. The instruction throughput for FP32 mul is high (64 per cycle per SM). With 32 warps * 32 threads = 1024 threads, each doing 32 muls, total muls per block = 32768. For 32768 blocks, total muls = 1.07B. At throughput of 64 per cycle per SM, on 108 SMs, that's ~6912 per cycle total. At 1.4 GHz, that's ~9.7e12 per second. So 1.07B muls take ~0.1 ms. Negligible. The memory time dominates.
Wait, but the dependency chain per thread means each thread can only issue one mul every 4 cycles. But there are 32 warps per block. With 32 warps, the warp scheduler can pick another warp each cycle. So effective throughput per block is 1 mul per cycle from each warp? Actually each warp has 32 threads, so each warp can issue 32 muls per instruction. But because of dependency, the next instruction for that warp cannot issue for 4 cycles. So each warp issues every 4 cycles. With 32 warps, that's 32/4 = 8 warps ready per cycle, each issuing 32 muls = 256 muls per cycle per SM. That's well below throughput. So the multiply latency is not a bottleneck.
But what about the shuffle instructions? They also have some latency.
Overall, the kernel should be close to bandwidth bound.
One more thought: We can vectorize loads and stores using `float4` to reduce instruction count and improve bandwidth. Since each thread loads 32 floats = 8 `float4` vectors. That is great. Similarly stores can be `float4`. This would reduce the number of load/store instructions by 4x, which often improves bandwidth.
Let's incorporate `float4`:
```
const float4* row_in4 = reinterpret_cast<const float4*>(row_in);
float4* row_out4 = reinterpret_cast<float4*>(row_out);
```
Since `cols` = 32768 is divisible by 4, and each thread's items_per_thread = 32 is divisible by 4? Actually items_per_thread = 32 floats = 8 float4s. Each thread loads 8 float4s. Thread 0 loads float4 at indices 0, 8, 16, ..., 56? Wait, if we treat the row as array of float4, total float4 count = 32768/4 = 8192. With 1024 threads, each thread handles 8192/1024 = 8 float4s. Thread tid loads float4 at `tid*8 + j` for j=0..7. That is coalesced: consecutive threads load consecutive float4s.
Let's align properly. We need to ensure pointer alignment: `row_in` is aligned to 4 bytes (float). For `float4`, we need 16-byte alignment. Since `cols` = 32768, which is divisible by 4, and the tensor is contiguous, each row starts at `base + row * cols`. `base` is aligned to at least 256 bytes for CUDA allocations. `row * cols * 4` is multiple of 16? `cols*4 = 131072`, which is divisible by 16. So each row is 16-byte aligned. Good.
So we can use `float4`.
Local array would be `float4 local4[8];` but we need to do per-element multiplication. We can load as `float4`, then extract `x, y, z, w` into a `float local[32]`. Or we can keep as `float4` and do per-component multiply. But the scan requires sequential processing of each float. We could load as `float4`, then in the unrolled loop process `.x`, `.y`, `.z`, `.w`. The compiler can schedule this.
Alternatively, we can load 8 `float4`s, store them in a `float local[32]`, but that might increase register usage due to unpacking. Actually `float4` loaded into a `float4` register is 4 floats. If we then store into `float local[32]`, that's 32 registers. Instead, we could keep them in `float4 local4[8]`, which is also 8*4 = 32 floats, but organized as 8 vector registers. Then the scan loop would be:
```
float acc = 1.0f;
float out_local[32]; // can reuse local array
int idx4 = 0;
#pragma unroll
for (int j = 0; j < 8; ++j) {
float4 v = local4[j];
acc *= v.x; out_local[idx4++] = acc;
acc *= v.y; out_local[idx4++] = acc;
acc *= v.z; out_local[idx4++] = acc;
acc *= v.w; out_local[idx4++] = acc;
}
float thread_total = acc;
```
This uses `local4[8]` (8 registers of type float4, which are 4 float registers each, but the compiler might allocate them as vectors) and `out_local[32]` (32 registers). That's 40 registers, maybe more. But we can reuse `local4` to store the output? We could overwrite `v` component-wise, but we need the original values for the scan. Actually we can compute inclusive scan and write back to a float array, but that float array is the same as `local` in previous design. If we use `float local[32]`, we can load from float4 and store into local. The compiler may optimize and not keep both. Let's not overcomplicate. We can keep `float local[32]` and load with pointer casts.
```
const float* in_ptr = row_in + tid * items_per_thread;
#pragma unroll
for (int i = 0; i < items_per_thread; ++i) {
local[i] = in_ptr[i];
}
```
This is 32 loads per thread. The compiler might vectorize them automatically if it sees consecutive loads. But explicit `float4` is safer. Let's try explicit `float4`:
```
const float4* in4 = reinterpret_cast<const float4*>(row_in + tid * items_per_thread);
#pragma unroll
for (int i = 0; i < items_per_thread / 4; ++i) {
float4 v = in4[i];
local[i*4+0] = v.x;
local[i*4+1] = v.y;
local[i*4+2] = v.z;
local[i*4+3] = v.w;
}
```
This uses `in4` pointer and `v` temporary. The compiler might keep `local` array. Register count remains ~32 for local + a few. This is fine.
Similarly for stores:
```
float4* out4 = reinterpret_cast<float4*>(row_out + tid * items_per_thread);
#pragma unroll
for (int i = 0; i < items_per_thread / 4; ++i) {
float4 v;
v.x = mult * local[i*4+0];
v.y = mult * local[i*4+1];
v.z = mult * local[i*4+2];
v.w = mult * local[i*4+3];
out4[i] = v;
}
```
This is good.
Can we reduce the register count further to get higher occupancy? If we use 24 registers per thread, we could have 2 blocks per SM (2048 threads) -> 100% occupancy. But is that necessary? Memory bandwidth bound kernels often benefit from higher occupancy to saturate memory throughput. 50% occupancy (1024 threads) on A100 might still be enough to reach near-peak bandwidth if memory latency is hidden well. A100 has L2 cache, and coalesced loads are efficient. 32 warps per SM might already saturate the memory pipeline. However, 100% occupancy might help if we have latency from shuffle and shared memory syncs. But the kernel is simple.
Let's see if we can get 2 blocks per SM with shared memory being 128 bytes. The limiting factor is registers. 65536 / 2 = 32768 registers per block. For 1024 threads, that's 32 registers per thread. We are currently using ~40. Could we compress to 32?
If we reduce items_per_thread to 16 (blockDim = 2048 threads), we can't have 2048 threads per block because max is 1024. If blockDim = 1024, items_per_thread = 32. Must have 32 floats per thread. To keep 32 registers, we need to store the array in shared memory. For example, we can load data into shared memory, do the scan in shared memory, and write back. But shared memory for 1024 threads * 32 floats = 32768 floats = 128 KB, which exceeds shared memory per SM (if we have 2 blocks, 256 KB). Not possible. But we could tile: each block processes the row in tiles of e.g. 1024 elements using shared memory, then do block-level scan and propagate. That would be more complicated and might have extra passes. Not worth it for this problem.
Alternatively, blockDim = 512, items_per_thread = 64. Array in registers = 64 registers. That's worse.
What about blockDim = 256, items_per_thread = 128. Array = 128 registers. Worse.
So blockDim = 1024, items_per_thread = 32 seems the sweet spot.
Another optimization: Instead of using `float local[32]`, we can keep the data in registers as we load and scan, and reuse the registers for output. We don't need to keep the original values after scanning. So the compiler might only allocate 32 registers for the array plus a few. We can help the compiler by not using a separate array for output. Let's name it `reg` and overwrite it.
```
float reg[32];
const float* in_ptr = row_in + tid * 32;
#pragma unroll
for (int i = 0; i < 32; ++i) reg[i] = in_ptr[i];
float acc = 1.0f;
#pragma unroll
for (int i = 0; i < 32; ++i) {
acc *= reg[i];
reg[i] = acc;
}
```
Then later store from `reg`. That's 32 registers for `reg`, plus `acc` (maybe reused), plus other scalars. Likely under 40.
We can reduce further by vectorizing the scan loop such that we process 4 elements at a time? But they are dependent, so we can't.
Another optimization: use half2 or something? No, it's fp32.
Now let's think about correctness: we need to match PyTorch's `cumprod` exactly. Since multiplication is associative in floating point but not exactly, the order of operations matters. Our kernel processes elements in order within each thread, and across threads in order. So it should match the sequential order of PyTorch's `cumprod` if PyTorch also processes in the same order. However, PyTorch's implementation may use a tree-based scan that changes the order of multiplications and thus the rounding. The tolerance for fp32 is `atol=rtol=1e-4`. Since floating point error for a scan of 32k numbers can be significant (products become very small), the relative tolerance matters. If our kernel produces different rounding, the error might accumulate and exceed tolerance. Wait, tolerance is 1e-4 relative and absolute. For numbers near 1e-30, absolute tolerance 1e-4 is huge, so it's fine. For numbers near 1, relative tolerance 1e-4 is strict: error must be less than 0.01%. Since we have ~32k multiplications, rounding differences per operation could accumulate. But the product of random numbers in [0,1] very quickly goes down to ~1e-1000 (underflow to 0). Actually 32768 random numbers in [0,1] have product ~exp(32768 * mean(log(U[0,1]))) which is extremely small. Most elements will be 0 in fp32 after a few hundred multiplications, because the smallest positive normal is ~1e-38, subnormal down to ~1e-45. So after ~100 multiplies of numbers < 0.5, the result underflows to 0. So for later elements, both reference and kernel will have 0. The difference between 0 and 0 is exact.
But for early elements, say first 10-20, the product is non-zero and the rounding might differ. Since each element involves at most 32 multiplies within a thread, and then multiplying by a prefix from previous threads/warps. The prefix from previous threads involves multiplications in a different order (warp-level scan, block-level scan). This could change the result compared to a pure sequential scan. However, after the first few threads, the prefix might be 0. Let's analyze the order:
Our kernel does:
- Within each thread: sequential scan of its 32 elements. This matches sequential order for that chunk.
- Across threads within a warp: the prefix is computed as a warp-level scan using shuffles. The multiplications happen in a tree-like way (shfl_up). Specifically, `inclusive` is computed by combining values in a binary tree pattern. For example, lane 1 gets `val1 * val0`, lane 2 gets `val2 * (val1 * val0)` or `val2 * val1 * val0` depending on shfl pattern. Actually shfl_up with offset 1,2,4,... builds a prefix where lane i gets product of lanes 0..i. The exact rounding may differ from a sequential product because the order of multiplications is different due to the parallel reduction tree. For example, lane 3: after offset 1: `v3 * v2`. After offset 2: `v3*v2 * v1*v0`. The product is `((v3*v2)*(v1*v0))`. In a sequential scan, lane 3 would be `(((v0*v1)*v2)*v3)`. The order differs, so rounding differs.
Since the product of many floats quickly approaches 0, the rounding difference for early elements might be noticeable. But tolerance is 1e-4. Let's test the worst case: the values are random [0,1]. For the first few elements, say element 64 (end of second warp), the product is `P = product of first 64 random numbers`. The expected value is (0.5)^64 = 5.4e-20. The relative error from different multiplication order might be on the order of machine epsilon (1e-7) times number of operations (64), so ~6e-6. That's well within 1e-4.
But wait, for element 1024, product is (0.5)^1024 ~ 1e-308, underflow to 0 in fp32 (which min normal ~1e-38, min subnormal ~1e-45). Actually random [0,1] has mean ~0.5. After ~150 elements, product ~1e-45. After ~200, definitely 0. So for all elements beyond ~200, both reference and kernel will produce 0. The kernel's order difference only matters for the first ~200 elements. That's few enough that the rounding difference is negligible.
Wait, PyTorch's `cumprod` on CUDA likely also uses a parallel scan algorithm (maybe tree-based) and doesn't do a purely sequential scan per row. On GPU, its internal implementation might be similar to ours or use a different algorithm, but it also doesn't guarantee exact sequential order. In fact, I think PyTorch's `cumprod` on CUDA uses some sort of parallel scan as well. So it's possible that our output might match exactly or closely.
However, the safest way to guarantee exact match is to ensure we multiply in the exact same order as the reference. Does the reference do sequential order? Let's check PyTorch source or typical behavior. `torch.cumprod` on CPU does sequential. On GPU, it likely uses `cub::DeviceScan` or a custom scan. `cub::DeviceScan` implements an inclusive scan using a single-pass decoupled look-back algorithm. The look-back aggregates block prefixes using atomic or flag-based synchronization. Within each block, it likely does a sequential scan per thread, then block-level reduction, then look-back. The order within a block's tile is sequential per thread, but block prefix is aggregated from other blocks via a binary tree of blocks, so the order might differ. Actually the block prefix is the product of all previous blocks' totals. If each block processes a tile of e.g. 1024 elements, the prefix for block k is product of block totals of blocks 0..k-1. The product of block totals is done sequentially as they become available, but each block total is the product of its elements computed sequentially. So overall, the order may be: block 0 elements in order, block 1 elements in order, etc. But the product of block totals might be accumulated as `prefix_k = prefix_{k-1} * block_total_{k-1}`. That is sequential across blocks. So the overall order is the same as sequential order! Let's verify: For tile scan, each thread sequentially scans its elements. Then a warp scan of thread aggregates. Then block scan of warp aggregates. Then the block aggregate is written to a global prefix array. For look-back, subsequent blocks read previous block aggregates and multiply them. The multiplication order is sequential across blocks. So the overall order is:
- Within thread: sequential.
- Within warp: sequential (thread 0, thread 1, ...).
- Within block: sequential (warp 0, warp 1, ...).
- Across blocks: sequential (block 0, block 1, ...).
So CUB's device scan preserves the sequential multiplication order in terms of the region mapping. Specifically, for inclusive scan, `y[i] = x[0] * x[1] * ... * x[i]` exactly, but due to parallel execution, the actual floating point operations are performed in groups. Wait, does CUB preserve exact sequential associativity? Let's think.
In CUB's block scan, the thread aggregates are combined in a tree. For example, to compute the prefix for thread 3, it might compute `(thread0_total * thread1_total) * (thread2_total * thread3_total)`? Actually in a typical Blelloch scan, the prefix is not computed in exact sequential order. But CUB might use a different algorithm. However, for the look-back scan, each block computes its tile prefix as `aggregate_prev * scan_within_tile`. The `aggregate_prev` is the product of all previous tile totals, computed sequentially by reading the previous tiles' totals. But `aggregate_prev` itself is computed by the previous block as `aggregate_prev = aggregate_prev_prev * tile_total_prev`. So it's a chain. The tile_total_prev is the product of elements in that tile, but the multiplication order within the tile might be tree-based. However, tile_total is just a single value: product of all elements in the tile. Since multiplication is commutative (but not associative), different internal orders for the tile_total may produce slightly different results. But `cumprod` requires the entire prefix array, not just the final total. The values within the tile are also scanned. The within-tile scan algorithm may not match the exact sequential order of multiplications.
Thus, it's not guaranteed that PyTorch's `cumprod` matches CPU sequential order on GPU. The tolerance of 1e-4 suggests that small differences are acceptable.
To maximize our chance of passing correctness, we should try to replicate the exact ordering of PyTorch's GPU implementation. But that's unknown. However, since tolerance is loose (1e-4 for fp32), our kernel will likely pass.
One subtlety: if PyTorch's `cumprod` uses a naive sequential loop per element on GPU (which is unlikely due to performance), then our parallel scan might differ more. But it's okay.
Actually, we can verify by running correctness check.
Let's write the kernel.
**Kernel design v1:**
```
__global__ void cumprod_kernel(const float* __restrict__ input,
float* __restrict__ output,
int rows, int cols) {
int row = blockIdx.x;
if (row >= rows) return;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
const int items_per_thread = 32;
const int num_warps = 32;
const float* row_in = input + row * (long long)cols;
float* row_out = output + row * (long long)cols;
float reg[items_per_thread];
// load
int offset = tid * items_per_thread;
const float4* in4 = reinterpret_cast<const float4*>(row_in + offset);
#pragma unroll
for (int i = 0; i < items_per_thread / 4; ++i) {
float4 v = in4[i];
reg[i*4+0] = v.x;
reg[i*4+1] = v.y;
reg[i*4+2] = v.z;
reg[i*4+3] = v.w;
}
// intra-thread inclusive scan
float acc = 1.0f;
#pragma unroll
for (int i = 0; i < items_per_thread; ++i) {
acc *= reg[i];
reg[i] = acc;
}
float thread_total = acc;
// warp-level exclusive scan of thread_total
float inclusive = thread_total;
#pragma unroll
for (int offset = 1; offset < 32; offset <<= 1) {
float other = __shfl_up_sync(0xFFFFFFFF, inclusive, offset);
if (lane >= offset) inclusive *= other;
}
float warp_exclusive;
if (lane == 0) {
warp_exclusive = 1.0f;
} else {
warp_exclusive = __shfl_sync(0xFFFFFFFF, inclusive, lane - 1);
}
// block-level exclusive scan of warp totals
__shared__ float warp_smem[32];
float warp_total = __shfl_sync(0xFFFFFFFF, inclusive, 31);
if (lane == 0) {
warp_smem[warp_id] = warp_total;
}
__syncthreads();
__shared__ float block_prefix_smem[32];
if (tid == 0) {
float running = 1.0f;
#pragma unroll
for (int i = 0; i < num_warps; ++i) {
block_prefix_smem[i] = running;
running *= warp_smem[i];
}
}
__syncthreads();
float block_prefix = block_prefix_smem[warp_id];
float mult = block_prefix * warp_exclusive;
// store
float4* out4 = reinterpret_cast<float4*>(row_out + offset);
#pragma unroll
for (int i = 0; i < items_per_thread / 4; ++i) {
float4 v;
v.x = mult * reg[i*4+0];
v.y = mult * reg[i*4+1];
v.z = mult * reg[i*4+2];
v.w = mult * reg[i*4+3];
out4[i] = v;
}
}
```
We need to pass `int64_t` for large offsets because `row * cols` fits in 32-bit (32768*32768 = 1<<30, which is 1073741824 < 2^31). However, `int` is fine but safer with `int64_t`.
Wait `row * cols` is 1,073,741,824. It fits in signed 32-bit (max 2,147,483,648). So `int` is okay. But pointer arithmetic with `int` might be promoted. Using `int64_t` is safer for generic.
Now, compile and check correctness.
Potential issues:
- `float4` alignment: We need `__align__(16)`? `float4` is automatically 16-byte aligned. Casting a `float*` to `float4*` is okay if the pointer is 16-byte aligned. As argued, row start is aligned. `offset = tid * 32` is multiple of 32 floats = 128 bytes, so aligned. Good.
- We assume `cols % (blockDim.x * 4) == 0`. Here 32768 % (1024*4) = 32768 % 4096 = 0. Good.
- We assume `blockDim.x = 1024`. We can hardcode or pass. Better to pass as template or just compute items_per_thread at runtime as `cols / blockDim.x`. Since it's constant, the compiler can optimize if we pass as a compile-time constant via template. But runtime division of `32768 / 1024 = 32` is cheap and can be constant-folded if blockDim.x is known at compile time? It's not known at compile time. But we can define `items_per_thread = cols / blockDim.x` inside the kernel. For our case, we can template the kernel with `items_per_thread` and launch with block size 1024.
Better to template:
```
template <int ITEMS_PER_THREAD>
__global__ void cumprod_kernel(...)
```
But `load_inline` with templates is fine; we just instantiate in the host wrapper. Actually with `load_inline`, we write the CUDA source as a string. We can template the kernel and call it from the host function.
Host wrapper:
```
torch::Tensor cumprod_cuda(torch::Tensor x, int dim) {
// assuming dim == 1 and x is 2D
int rows = x.size(0);
int cols = x.size(1);
auto out = torch::empty_like(x);
const float* in_ptr = x.data_ptr<float>();
float* out_ptr = out.data_ptr<float>();
dim3 grid(rows);
dim3 block(1024);
cumprod_kernel<32><<<grid, block>>>(in_ptr, out_ptr, rows, cols);
return out;
}
```
We need to check if dim is 1 and x is contiguous. But the reference only uses dim=1 and a 2D tensor. We can assume that or handle generically. The prompt says accept the same constructor arguments (dim), and forward signature. We can just handle the specific case or support general. Let's support general 2D with dim=1. If dim=0, we could transpose and call or write a separate kernel. But the reference only uses dim=1. It might be safer to handle both, but since input is always dim=1 and 2D, we can just assert or handle it simply.
Actually, we should handle arbitrary positive dims by flattening? `cumprod` can be applied to any shape, but the benchmark will only use the provided config. It's okay to just handle the 2D case. However, if we assert, it's safer to avoid silent errors.
Let's see the correctness harness: they instantiate `ModelNew(dim)` and call `forward(x)`. The inputs are generated by `get_inputs()`. So it's always `dim=1` and `x` shape `(32768, 32768)`. So we can write a simple host wrapper that checks `dim == 1` and `x.dim() == 2`. If not, fallback to `torch.cumprod`. But the instructions say not to use try/except fallbacks to reference. A simple if statement using `torch.cumprod` for unsupported cases is a fallback and might be flagged? The static check looks for patterns like `try/except` or fallback to reference. But a conditional fallback for unsupported dims might be okay? The instructions say "Do not use try/except fallbacks to the reference...". A direct fallback might be okay but to be safe, we can just assert.
Better: we can support generic dim by doing the logic: if dim is the last dimension and tensor is contiguous, we can launch our kernel. In Python wrapper, we can get stride and handle it. For simplicity, we can just write:
```
if (dim == x.dim() - 1 && x.is_contiguous()) {
// launch kernel
} else {
return torch::cumprod(x, dim);
}
```
This is a fallback but not a try/except. It's a legitimate handling of unsupported cases. I think it's fine. But the static check might detect `torch::cumprod` in the code and flag it as fallback. The prompt says "Do not use try/except fallbacks to the reference, patch timing or RNG functions, or otherwise route around the evaluation". I think a fallback for unsupported dims is okay as long as it's not triggered in the benchmark. But if the static check is too aggressive, it might fail. To be safe, I will not include any fallback to `torch.cumprod`. I will just assert.
Actually, we can handle dim 0 as well by transposing or launching a kernel for dim 0, but it's unnecessary. Let's just assert.
Wait, what if the evaluation uses different dims? The benchmark description says the reference uses dim=1. Likely the evaluation uses the same. The prompt says "Replace the forward computation of the PyTorch model below with a custom CUDA kernel." It doesn't say we need to support arbitrary shapes. But the `ModelNew` class must accept the same constructor arguments and implement forward with same signature. It might be tested with the same inputs. So it's fine.
But let's consider generically: we can write a kernel that works for any 2D tensor and dim=1. If dim is not the last dimension, we could transpose the tensor (which is a view) and then launch. But transposing might make memory non-contiguous along the scan dim. Our kernel requires contiguous along dim 1. For dim=0, the elements are strided by cols, not 1, so our kernel would need to read strided. We could write a strided kernel. But again, not needed.
So let's write a kernel that takes strides.
Take `x` as 2D. For dim=1, `stride[1] = 1`, `stride[0] = cols`. Our kernel uses `row * cols + col`. That's correct.
For dim=0, `stride[0] = 1`, `stride[1] = rows`. But not needed.
So host wrapper: assume dim == 1, x is contiguous.
Now, compile and test.
I need to write the `ModelNew` class in Python.
Structure of the file:
```python
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline
cuda_source = r'''
#include <torch/extension.h>
#include <cuda_runtime.h>
template <int ITEMS_PER_THREAD>
__global__ void cumprod_kernel(const float* __restrict__ input,
float* __restrict__ output,
int rows, int cols) {
int row = blockIdx.x;
if (row >= rows) return;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
const int num_warps = blockDim.x >> 5;
const float* row_in = input + static_cast<long long>(row) * cols;
float* row_out = output + static_cast<long long>(row) * cols;
float reg[ITEMS_PER_THREAD];
// load vectorized
int offset = tid * ITEMS_PER_THREAD;
const float4* in4 = reinterpret_cast<const float4*>(row_in + offset);
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD / 4; ++i) {
float4 v = in4[i];
reg[i*4+0] = v.x;
reg[i*4+1] = v.y;
reg[i*4+2] = v.z;
reg[i*4+3] = v.w;
}
// intra-thread inclusive scan
float acc = 1.0f;
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD; ++i) {
acc *= reg[i];
reg[i] = acc;
}
float thread_total = acc;
// warp-level inclusive scan of thread_total
float inclusive = thread_total;
#pragma unroll
for (int o = 1; o < 32; o <<= 1) {
float other = __shfl_up_sync(0xFFFFFFFF, inclusive, o);
if (lane >= o) inclusive *= other;
}
// warp-level exclusive prefix
float warp_exclusive;
if (lane == 0) {
warp_exclusive = 1.0f;
} else {
warp_exclusive = __shfl_sync(0xFFFFFFFF, inclusive, lane - 1);
}
// block-level prefix
__shared__ float warp_total_smem[32];
__shared__ float block_prefix_smem[32];
float warp_total = __shfl_sync(0xFFFFFFFF, inclusive, 31);
if (lane == 0) {
warp_total_smem[warp_id] = warp_total;
}
__syncthreads();
if (tid == 0) {
float running = 1.0f;
#pragma unroll
for (int i = 0; i < num_warps; ++i) {
block_prefix_smem[i] = running;
running *= warp_total_smem[i];
}
}
__syncthreads();
float block_prefix = block_prefix_smem[warp_id];
float mult = block_prefix * warp_exclusive;
// store vectorized
float4* out4 = reinterpret_cast<float4*>(row_out + offset);
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD / 4; ++i) {
float4 v;
v.x = mult * reg[i*4+0];
v.y = mult * reg[i*4+1];
v.z = mult * reg[i*4+2];
v.w = mult * reg[i*4+3];
out4[i] = v;
}
}
torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim) {
// Expect dim == 1 and contiguous 2D float tensor
int rows = x.size(0);
int cols = x.size(1);
TORCH_CHECK(x.dim() == 2, "Only 2D tensors supported");
TORCH_CHECK(dim == 1, "Only dim=1 supported");
TORCH_CHECK(x.is_contiguous(), "Only contiguous tensors supported");
TORCH_CHECK(x.scalar_type() == torch::kFloat32, "Only float32 supported");
auto out = torch::empty_like(x);
const float* in_ptr = x.data_ptr<float>();
float* out_ptr = out.data_ptr<float>();
const int threads = 1024;
const int items_per_thread = cols / threads; // 32 for 32768 cols
dim3 grid(rows);
dim3 block(threads);
// Dispatch based on items_per_thread
if (items_per_thread == 32) {
cumprod_kernel<32><<<grid, block>>>(in_ptr, out_ptr, rows, cols);
} else if (items_per_thread == 16) {
cumprod_kernel<16><<<grid, block>>>(in_ptr, out_ptr, rows, cols);
} else {
// generic fallback - shouldn't happen for benchmark
// use a non-templated kernel or simple loop
// For safety, just launch with dynamic loop
// But we can also just use PyTorch fallback if needed. Let's implement a simple generic version.
// Actually easier: call a non-template kernel with items_per_thread passed as arg.
}
return out;
}
'''
# But we need a generic fallback if cols isn't exactly 1024*32.
# We can implement a simple non-templated kernel that loops.
# But for this problem, we can just assert items_per_thread is integer.
```
Wait, we need to compile the code. `load_inline` takes `cuda_sources` and `functions`. We can include both templated and non-templated versions. Since the benchmark always uses cols=32768 with threads=1024, `items_per_thread=32`. But we might as well support generic block size and items_per_thread.
Alternative: define `items_per_thread` as a runtime variable and loop inside the kernel. The compiler may or may not unroll. Since the benchmark shape is fixed, we can just hardcode `items_per_thread=32` and `block_size=1024`. That simplifies the code and avoids template dispatch.
However, if the benchmark uses different shapes (e.g., they might test other sizes), hardcoding might fail. The prompt defines specific input dimensions: `batch_size = 32768`, `input_shape = (32768,)`, `dim = 1`. So we can rely on these. But sometimes evaluation also tests other sizes. The instructions say: "Define input dimensions and parameters" but the correctness harness might call `get_inputs()` and also other sizes? Usually they test exactly those shapes. But to be robust, we can support dynamic sizes.
Let's write a generic kernel that works for any `cols` that is a multiple of `blockDim.x * 4`? Actually we can write a kernel that loops over elements per thread dynamically:
```
int items_per_thread = cols / blockDim.x;
float* reg = new float[items_per_thread]; // not allowed dynamic alloc on device
```
We can't allocate variable-length arrays in CUDA registers easily. But if `items_per_thread` is known at compile time via template, we can dispatch for common values. Or we can use shared memory to hold all elements and avoid large registers. Or we can process in a small fixed chunk (e.g., 8 floats per iteration) to keep register count low.
Better approach: process the row in tiles. For each tile of `blockDim.x * tile_size` elements? Actually each thread can process a fixed chunk per iteration, say 32 elements, and accumulate a running prefix across chunks.
Let's think of a generic kernel:
```
__global__ void cumprod_kernel(const float* input, float* output, int rows, int cols) {
int row = blockIdx.x;
if (row >= rows) return;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
int num_warps = blockDim.x >> 5;
const float* row_in = input + (long long)row * cols;
float* row_out = output + (long long)row * cols;
// number of elements each thread is responsible for
int items_per_thread = cols / blockDim.x;
// we process them in chunks of e.g. 32 to keep register usage low
const int REG_SIZE = 32;
float reg[REG_SIZE];
float thread_acc = 1.0f; // product of all fully processed chunks
for (int chunk = 0; chunk < items_per_thread / REG_SIZE; ++chunk) {
int base = tid * items_per_thread + chunk * REG_SIZE;
// load
const float4* in4 = reinterpret_cast<const float4*>(row_in + base);
#pragma unroll
for (int i = 0; i < REG_SIZE / 4; ++i) {
float4 v = in4[i];
reg[i*4+0] = v.x;
reg[i*4+1] = v.y;
reg[i*4+2] = v.z;
reg[i*4+3] = v.w;
}
// inclusive scan within this chunk, but multiplied by thread_acc
float acc = thread_acc;
#pragma unroll
for (int i = 0; i < REG_SIZE; ++i) {
acc *= reg[i];
reg[i] = acc;
}
// compute chunk_total = acc
float chunk_total = acc;
// Now we need to do the same warp/block scan as before, but for each chunk.
// However, the warp/block prefix depends on previous chunks of this thread and previous threads' previous chunks.
// We can maintain thread_total as the product of all previous chunks. At each chunk boundary, we compute the prefix from block-level scan.
// But doing block-level scan per chunk is expensive due to sync. However, since all threads process chunks in lockstep, we can do it.
}
}
```
This gets complex. Since the shape is fixed, hardcoding is easier and more performant. Let's hardcode `block_size=1024` and `items_per_thread=32`. But we should also handle arbitrary `items_per_thread` up to some limit by using dynamic shared memory or reducing register pressure. Actually, we can choose `block_size` at runtime to be a power of two dividing `cols`. For `cols=32768`, we can choose `block_size = 1024`. For other `cols` (e.g., 4096), `block_size = 1024`, `items_per_thread = 4`. We can compile-time dispatch for `items_per_thread` values: 1,2,4,8,16,32,64,128. However, 128 would use 128 registers, exceeding limit. So up to 32 or 64.
Actually, we can write a kernel that doesn't require a large register array: each thread processes its elements sequentially from global memory, storing partial results back to global memory, then do the cross-thread prefix adjustment in shared memory. Wait, we can do a two-pass approach:
Pass 1: Each thread computes the product of its assigned elements locally and writes them to a temporary buffer. Then we do a scan of the thread products. Then Pass 2: Each thread re-reads its elements and multiplies by the prefix.
Specifically:
- Each thread reads `items_per_thread` elements, computes `thread_total = product of these elements`.
- Writes the elements to global temp? No, we can write them to global output as is, but we need to apply prefix later. Actually we can write them into an intermediate buffer (or the output buffer directly if we read them again in pass 2).
- Then we compute `warp_exclusive` and `block_prefix` for `thread_total` exactly as before.
- Then each thread re-reads its elements (from input) and computes the inclusive scan within the thread, multiplies by `mult = block_prefix * warp_exclusive`, and writes to output.
This requires two reads of input per element but zero register array. Actually we can read once into registers, compute `thread_total`, then write to output or keep in a small register array? Wait, if `items_per_thread` is 32, we can keep them in registers for the second pass. But if we want to support larger `items_per_thread`, we might not have registers. So we re-read from global memory in pass 2.
But re-reading doubles memory traffic (8 GB read + 4 GB write = 12 GB). That's 50% more memory traffic. Since we are bandwidth bound, this might be slower than keeping registers. But for generality, it's fine. However, for the given size, we can keep registers.
Given the contest nature, they might test exactly the specified shape. Let's just optimize for that shape while adding a basic generic fallback for other shapes that might be slower but correct.
Idea: if `cols == 32768` and `dim == 1` (and `x.is_contiguous()` and `x.dtype == torch.float32`), launch the fast kernel. Otherwise, call `torch.cumprod(x, dim=dim)`. This fallback is safe for other shapes.
But static check might flag `torch.cumprod` as fallback? The instruction: "Do not use try/except fallbacks to the reference...". A direct `if/else` fallback is not try/except. It might be okay. But to be absolutely safe, we can avoid any PyTorch fallback and implement a generic kernel that works for any shape by using a simpler scheme: a single thread per row doing a sequential scan. This will be slow but correct for any shape. Then we branch: if `cols == 32768 && dim == 1`, launch fast kernel; else launch simple kernel. The simple kernel does not call `torch.cumprod`. That's fully custom.
Simple kernel:
```
__global__ void cumprod_simple_kernel(const float* input, float* output, int rows, int cols) {
int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= rows) return;
const float* in_ptr = input + (long long)row * cols;
float* out_ptr = output + (long long)row * cols;
float acc = 1.0f;
for (int c = 0; c < cols; ++c) {
acc *= in_ptr[c];
out_ptr[c] = acc;
}
}
```
This launches one thread per row. Coalescing is terrible for reads (each warp reads strided by cols). But for shapes not benchmarked, it's okay. However, if the evaluation tests our kernel on large shapes with `dim=0`, this would be terrible but not timed? Actually they time it. But if they only test the provided shape, we want the fast path.
I'll do: static check might look for `torch.cumprod` string in the source? It's possible. Let's avoid it entirely and implement both fast and simple generic kernels. The host wrapper checks dimensions and chooses the appropriate kernel.
So generic wrapper:
```
torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim) {
TORCH_CHECK(x.dim() == 2, "Only 2D supported");
TORCH_CHECK(dim == 1 || dim == -1, "Only last dim supported");
int rows = x.size(0);
int cols = x.size(1);
auto out = torch::empty_like(x);
const float* in_ptr = x.data_ptr<float>();
float* out_ptr = out.data_ptr<float>();
if (cols == 32768 && x.is_contiguous() && x.scalar_type() == torch::kFloat32 && (dim == 1 || dim == -1)) {
dim3 grid(rows);
dim3 block(1024);
cumprod_fast_kernel<32><<<grid, block>>>(in_ptr, out_ptr, rows, cols);
} else {
dim3 block_simple(256);
dim3 grid_simple((rows + block_simple.x - 1) / block_simple.x);
cumprod_simple_kernel<<<grid_simple, block_simple>>>(in_ptr, out_ptr, rows, cols);
}
return out;
}
```
This avoids PyTorch fallback entirely. Good.
Let's refine the fast kernel. I want to make sure `num_warps` is compile-time constant if possible. `blockDim.x` is runtime but for our launch it's 1024. The loop `for (int i = 0; i < num_warps; ++i)` where `num_warps = blockDim.x >> 5` is loop of 32 iterations. The compiler can unroll it since `blockDim.x` is not known. But we can make `num_warps = 32` by hardcoding for the fast kernel. We can template on `BLOCK_SIZE` and `ITEMS_PER_THREAD` and derive `NUM_WARPS = BLOCK_SIZE / 32`.
```
template <int BLOCK_SIZE, int ITEMS_PER_THREAD>
__global__ void cumprod_fast_kernel(...)
```
But `load_inline` doesn't easily handle custom template instantiation from Python. Actually we can define the template in the CUDA source and just call it from the host wrapper since the host wrapper is also in the same source. That's fine.
Let's do:
```
template <int BLOCK_SIZE, int ITEMS_PER_THREAD>
__global__ void cumprod_fast_kernel(const float* __restrict__ input, float* __restrict__ output, int rows, int cols) {
static_assert(ITEMS_PER_THREAD % 4 == 0, "...");
static_assert(BLOCK_SIZE == 1024, "..."); // not necessary but okay
int row = blockIdx.x;
if (row >= rows) return;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
const int NUM_WARPS = BLOCK_SIZE / 32;
...
}
```
But for the simple case, we don't need templates. Since we only call one instantiation, we can just hardcode constants and not template. That's easiest.
```
__global__ void cumprod_fast_kernel(const float* __restrict__ input, float* __restrict__ output, int rows, int cols) {
const int ITEMS_PER_THREAD = 32;
const int BLOCK_SIZE = 1024;
const int NUM_WARPS = BLOCK_SIZE / 32;
int row = blockIdx.x;
if (row >= rows) return;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
// as before
}
```
And launch with `dim3 block(BLOCK_SIZE);`. Since it's hardcoded, we can just assert `cols == 32768` in host wrapper.
Actually, let's also make the kernel work for any `cols` that equals `1024 * k` where `k` is something we can unroll. But with hardcoded `ITEMS_PER_THREAD = 32`, it only works for `cols = 32768`. For other sizes, we use the simple kernel. That's acceptable.
One more thought: `__shfl_sync` and `__shfl_up_sync` require CC >= 3.0. Modern PyTorch builds for CC >= 5.0 or 7.0, so fine.
Now, register usage might be an issue because we use `float reg[32]`. Let's check: with `const int ITEMS_PER_THREAD = 32;`, the array size is compile-time constant. The compiler allocates 32 scalar registers. Plus `acc`, `thread_total`, `inclusive`, `warp_exclusive`, `warp_total`, `block_prefix`, `mult`, some temps => maybe 42 registers. 42 * 1024 = 43008 < 65536. So one block per SM. Occupancy 50%. Maybe okay.
If we could get it under 32 registers, we could have 2 blocks per SM. Let's see if we can reduce to 24 registers. We could process 16 elements per thread with block size 1024, which would require half the array. But then items_per_thread = 16 means cols = 16384. Not our case. We could use shared memory for half the array: load 16 floats into registers, process them, then load next 16. But that doubles the loads. Hmm.
Actually, maybe we can process 32 elements without storing all of them: we can load 8 float4, process each component as we load, but we need the scan results to store later. We could compute the scan, write to shared memory, then read back to store. But writing to shared memory has conflicts.
Alternative: compute and store directly using two passes:
1. Load 8 float4, compute `thread_total`. Save the loaded values? We can reload in pass 2. But that doubles global reads.
Given that memory bandwidth is the bottleneck, doubling reads would make it slower. But maybe PyTorch's implementation is even slower. Let's estimate.
Reference: PyTorch cumprod on CUDA likely does one pass with CUB or a custom kernel, reading and writing once. So its memory traffic is 8 GB. Our fast kernel is also 8 GB. If we do double reads, it's 12 GB. That would be 1.5x memory traffic, which on a bandwidth-bound kernel means 1.5x slower memory time. However, maybe PyTorch's kernel is not bandwidth bound because it does extra passes or uses atomics or has poor occupancy. The CUB-based scan might have multiple passes (load, scan, write, plus look-back scratchpad). Actually CUB device scan uses two passes: one to scan tiles, another to look-back and output. It writes temporary aggregates to global memory, then reads them. So total memory traffic is input + output + aggregate traffic. Our kernel does input + output only. So we have an advantage.
Thus, we should keep single pass.
What about using `__half2` or `float2`? `float2` loads might not be as efficient as `float4`. `float4` gives 128-bit loads. Store similarly.
Are there any other optimizations? We could use `__ldg` for loads (read-only cache). Since we only load once, `__ldg` might help.
```
const float4* in4 = reinterpret_cast<const float4*>(row_in + offset);
float4 v = __ldg(in4 + i);
```
But `__ldg` is for scalar pointers. We can cast to `const float4*` and use it. Actually `__ldg` has overloads for `float4` via `__ldg(const float4*)`? I think there is `__ldg(const float4*)` in newer CUDA. But we can just rely on normal loads; the compiler uses `ld.global.v4.f32` which is fine.
Another idea: use warp-level `__reduce_sync`? Not helpful.
We could also process two rows per block: interleave the loads. But 32768 rows is enough.
Actually, we might be able to launch multiple blocks per row to increase parallelism if cols is huge. But cols=32768 is only 128KB per row. With 1024 threads per block, each block already saturates memory bandwidth for that row? Not exactly.
Let's consider using a persistent kernel where each block processes multiple rows to amortize block-level overhead (shared memory sync, etc.). For example, each block processes 2 rows. Then we need 16384 blocks. That might help occupancy? We already have 32768 blocks. Occupancy is limited by registers per SM, not block count. 32768 blocks is plenty to keep all SMs busy. But each block dispatch has overhead. For small rows (128KB), maybe not huge. But we can add a loop over rows per block:
```
__global__ void cumprod_fast_kernel(...) {
for (int row = blockIdx.x; row < rows; row += gridDim.x) {
// process row
}
}
```
This would allow us to launch fewer blocks and have each block process multiple rows. But does it help? If we launch 32768 blocks on 108 SMs, that's ~300 blocks per SM. Each block processes 1 row. The block launch overhead is small compared to the work (1024 threads * 32 loads = heavy). But if we launch, say, 1000 blocks, each block processes ~32 rows. Then inside the block, after finishing one row, it proceeds to the next. The advantage is reduced grid launch overhead and maybe better cache locality (L2). However, with 300 blocks per SM already, launch overhead is amortized. Also, processing a row fully before moving to the next means good temporal locality for L1? Not really, because each row is 128KB, bigger than L1 (128KB shared). Actually L1 is per SM. L2 is shared. Each row is loaded from global memory; L2 might cache it. If multiple blocks read the same row? No. If a block processes multiple rows sequentially, it might evict L2. Not a big issue.
I think a straightforward 1 block per row is fine.
One important thing: we need to ensure `cols / blockDim.x` is integer. We can add a check in host code.
Now, let's write the Python file.
```python
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline
CUDA_SOURCE = r'''
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <c10/cuda/CUDAException.h>
// Fast kernel for cols == 32768, block_size == 1024, items_per_thread == 32
__global__ void cumprod_fast_kernel(const float* __restrict__ input,
float* __restrict__ output,
int rows, int cols) {
const int ITEMS_PER_THREAD = 32;
const int BLOCK_SIZE = 1024;
const int NUM_WARPS = BLOCK_SIZE / 32;
int row = blockIdx.x;
if (row >= rows) return;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
const float* row_in = input + static_cast<long long>(row) * cols;
float* row_out = output + static_cast<long long>(row) * cols;
float reg[ITEMS_PER_THREAD];
// Vectorized load
int offset = tid * ITEMS_PER_THREAD;
const float4* in4 = reinterpret_cast<const float4*>(row_in + offset);
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD / 4; ++i) {
float4 v = in4[i];
reg[i*4+0] = v.x;
reg[i*4+1] = v.y;
reg[i*4+2] = v.z;
reg[i*4+3] = v.w;
}
// Intra-thread inclusive scan
float acc = 1.0f;
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD; ++i) {
acc *= reg[i];
reg[i] = acc;
}
float thread_total = acc;
// Warp-level inclusive scan of thread_total
float inclusive = thread_total;
#pragma unroll
for (int o = 1; o < 32; o <<= 1) {
float other = __shfl_up_sync(0xFFFFFFFF, inclusive, o);
if (lane >= o) inclusive *= other;
}
// Derive warp-exclusive prefix
float warp_exclusive;
if (lane == 0) {
warp_exclusive = 1.0f;
} else {
warp_exclusive = __shfl_sync(0xFFFFFFFF, inclusive, lane - 1);
}
// Block-level exclusive scan of warp totals
__shared__ float warp_total_smem[NUM_WARPS];
__shared__ float block_prefix_smem[NUM_WARPS];
float warp_total = __shfl_sync(0xFFFFFFFF, inclusive, 31);
if (lane == 0) {
warp_total_smem[warp_id] = warp_total;
}
__syncthreads();
if (tid == 0) {
float running = 1.0f;
#pragma unroll
for (int i = 0; i < NUM_WARPS; ++i) {
block_prefix_smem[i] = running;
running *= warp_total_smem[i];
}
}
__syncthreads();
float block_prefix = block_prefix_smem[warp_id];
float mult = block_prefix * warp_exclusive;
// Vectorized store
float4* out4 = reinterpret_cast<float4*>(row_out + offset);
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD / 4; ++i) {
float4 v;
v.x = mult * reg[i*4+0];
v.y = mult * reg[i*4+1];
v.z = mult * reg[i*4+2];
v.w = mult * reg[i*4+3];
out4[i] = v;
}
}
// Simple fallback kernel for other shapes (one thread per row)
__global__ void cumprod_simple_kernel(const float* __restrict__ input,
float* __restrict__ output,
int rows, int cols) {
int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= rows) return;
const float* in_ptr = input + static_cast<long long>(row) * cols;
float* out_ptr = output + static_cast<long long>(row) * cols;
float acc = 1.0f;
for (int c = 0; c < cols; ++c) {
acc *= in_ptr[c];
out_ptr[c] = acc;
}
}
torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim) {
TORCH_CHECK(x.dim() == 2, "cumprod_cuda: only 2D tensors supported");
int rows = x.size(0);
int cols = x.size(1);
auto out = torch::empty_like(x);
const float* in_ptr = x.data_ptr<float>();
float* out_ptr = out.data_ptr<float>();
if (cols == 32768 && x.is_contiguous() && x.scalar_type() == torch::kFloat32 && (dim == 1 || dim == -1)) {
dim3 grid(rows);
dim3 block(1024);
cumprod_fast_kernel<<<grid, block>>>(in_ptr, out_ptr, rows, cols);
C10_CUDA_KERNEL_LAUNCH_CHECK();
} else {
dim3 block(256);
dim3 grid((rows + block.x - 1) / block.x);
cumprod_simple_kernel<<<grid, block>>>(in_ptr, out_ptr, rows, cols);
C10_CUDA_KERNEL_LAUNCH_CHECK();
}
return out;
}
'''
cpp_sources = 'torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim);'
# Actually load_inline takes cpp_sources and cuda_sources.
# The host wrapper is declared in cpp_sources.
# But we need to provide the implementation. We can put everything in cuda_sources and provide a thin wrapper in cpp_sources? Actually load_inline allows both.
# According to docs:
# cpp_sources: string with C++ source
# cuda_sources: string with CUDA source
# functions: list of function names to bind.
#
# We can put the CUDA kernel and the host function in cuda_sources, and then in cpp_sources just declare it or wrap it. Actually we need to define the host function in C++ code so pybind can see it. If we define it in cuda_sources, it's still compiled as CUDA code. pybind binds to the symbol.
# Let's put everything in cuda_sources and leave cpp_sources empty or with a declaration? Usually load_inline expects cpp_sources to contain the pybind-friendly wrappers, but it says it auto-generates pybind. We just need to list function names.
# I think putting the full C++/CUDA code in cuda_sources is fine. Or we can put everything in cpp_sources? But kernels must be in .cu file (cuda_sources). The host function can also be in cuda_sources; nvcc compiles it.
# Let's do:
cpp_sources = '' # empty
# Then functions = ['cumprod_cuda'].
# The pybind wrapper will call cumprod_cuda.
# Wait, the function signature must be accessible from C++. Since we put it in CUDA source, it's compiled by nvcc as a C++ function with CUDA. That's fine.
# Let's construct the load_inline call.
_ext = load_inline(
name='cumprod_ext',
cpp_sources='',
cuda_sources=CUDA_SOURCE,
functions=['cumprod_cuda'],
extra_cuda_cflags=['-O3', '--use_fast_math'],
verbose=False
)
class ModelNew(nn.Module):
def __init__(self, dim):
super().__init__()
self.dim = dim
def forward(self, x):
return _ext.cumprod_cuda(x, self.dim)
```
Wait, `load_inline` may need `cpp_sources` to contain at least something if we define the function there. Actually, the `cuda_sources` are compiled into a separate object and linked. The function `cumprod_cuda` defined in `cuda_sources` will have external linkage. Then pybind auto-generated code will declare it as `extern torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim);` and call it. So `cpp_sources` can be empty.
But sometimes `load_inline` expects `cpp_sources` to be non-empty? I'm not sure. Usually `cpp_sources` is where you put the non-CUDA C++ code. `cuda_sources` is for .cu code. Both are fine.
However, there is a subtlety: if we include `<torch/extension.h>` in `cuda_sources`, it's okay. If we also include it in a separate `cpp_sources`, it would be included automatically? Not needed.
Let's look at `extra_cuda_cflags`. We can add `-O3`.
One more thing: `C10_CUDA_KERNEL_LAUNCH_CHECK()` requires `#include <c10/cuda/CUDAException.h>`. I included it.
Now, let's verify correctness. I need to call `compile_kernel` and then `run_correctness`.
Wait, but the GPU specs might help. Let's get them first. `get_gpu_specs`. Actually, it's optional. I can directly compile.
I'll write the kernel code string fully in `kernel_code`.
Let me write it carefully.
```python
kernel_code = r'''
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline
CUDA_SOURCE = """
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <c10/cuda/CUDAException.h>
__global__ void cumprod_fast_kernel(const float* __restrict__ input,
float* __restrict__ output,
int rows, int cols) {
const int ITEMS_PER_THREAD = 32;
const int BLOCK_SIZE = 1024;
const int NUM_WARPS = BLOCK_SIZE / 32;
int row = blockIdx.x;
if (row >= rows) return;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
const float* row_in = input + static_cast<long long>(row) * cols;
float* row_out = output + static_cast<long long>(row) * cols;
float reg[ITEMS_PER_THREAD];
int offset = tid * ITEMS_PER_THREAD;
const float4* in4 = reinterpret_cast<const float4*>(row_in + offset);
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD / 4; ++i) {
float4 v = in4[i];
reg[i*4+0] = v.x;
reg[i*4+1] = v.y;
reg[i*4+2] = v.z;
reg[i*4+3] = v.w;
}
float acc = 1.0f;
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD; ++i) {
acc *= reg[i];
reg[i] = acc;
}
float thread_total = acc;
float inclusive = thread_total;
#pragma unroll
for (int o = 1; o < 32; o <<= 1) {
float other = __shfl_up_sync(0xFFFFFFFF, inclusive, o);
if (lane >= o) inclusive *= other;
}
float warp_exclusive;
if (lane == 0) {
warp_exclusive = 1.0f;
} else {
warp_exclusive = __shfl_sync(0xFFFFFFFF, inclusive, lane - 1);
}
__shared__ float warp_total_smem[NUM_WARPS];
__shared__ float block_prefix_smem[NUM_WARPS];
float warp_total = __shfl_sync(0xFFFFFFFF, inclusive, 31);
if (lane == 0) {
warp_total_smem[warp_id] = warp_total;
}
__syncthreads();
if (tid == 0) {
float running = 1.0f;
#pragma unroll
for (int i = 0; i < NUM_WARPS; ++i) {
block_prefix_smem[i] = running;
running *= warp_total_smem[i];
}
}
__syncthreads();
float block_prefix = block_prefix_smem[warp_id];
float mult = block_prefix * warp_exclusive;
float4* out4 = reinterpret_cast<float4*>(row_out + offset);
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD / 4; ++i) {
float4 v;
v.x = mult * reg[i*4+0];
v.y = mult * reg[i*4+1];
v.z = mult * reg[i*4+2];
v.w = mult * reg[i*4+3];
out4[i] = v;
}
}
__global__ void cumprod_simple_kernel(const float* __restrict__ input,
float* __restrict__ output,
int rows, int cols) {
int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= rows) return;
const float* in_ptr = input + static_cast<long long>(row) * cols;
float* out_ptr = output + static_cast<long long>(row) * cols;
float acc = 1.0f;
for (int c = 0; c < cols; ++c) {
acc *= in_ptr[c];
out_ptr[c] = acc;
}
}
torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim) {
TORCH_CHECK(x.dim() == 2, "cumprod_cuda: only 2D tensors supported");
int rows = x.size(0);
int cols = x.size(1);
auto out = torch::empty_like(x);
const float* in_ptr = x.data_ptr<float>();
float* out_ptr = out.data_ptr<float>();
if (cols == 32768 && x.is_contiguous() && x.scalar_type() == torch::kFloat32 && (dim == 1 || dim == -1)) {
dim3 grid(rows);
dim3 block(1024);
cumprod_fast_kernel<<<grid, block>>>(in_ptr, out_ptr, rows, cols);
C10_CUDA_KERNEL_LAUNCH_CHECK();
} else {
int threads = 256;
dim3 block(threads);
dim3 grid((rows + threads - 1) / threads);
cumprod_simple_kernel<<<grid, block>>>(in_ptr, out_ptr, rows, cols);
C10_CUDA_KERNEL_LAUNCH_CHECK();
}
return out;
}
"""
_ext = load_inline(
name="cumprod_ext",
cpp_sources="",
cuda_sources=CUDA_SOURCE,
functions=["cumprod_cuda"],
extra_cuda_cflags=["-O3", "--use_fast_math"],
verbose=False,
)
class ModelNew(nn.Module):
def __init__(self, dim):
super().__init__()
self.dim = dim
def forward(self, x):
return _ext.cumprod_cuda(x, self.dim)
'''
```
Wait, but the `cpp_sources` is empty and `cuda_sources` contains the host function. Is that okay? In PyTorch's `load_inline`, if `cpp_sources` is provided, it's compiled as C++ code. `cuda_sources` is compiled as CUDA code. The host function defined in `cuda_sources` will be in the CUDA object file. The generated pybind code will be in the C++ object file and will link against the CUDA object file. As long as the function has external linkage, it should work. `torch::Tensor cumprod_cuda(...)` has external linkage.
However, there might be an issue: the generated pybind module includes `<torch/extension.h>` and other headers. It will generate a call to `cumprod_cuda` with a forward declaration. This works.
But I recall that in `load_inline`, the `functions` list is used to generate wrappers that call the listed functions. It assumes they are declared in `cpp_sources` or `cuda_sources`. So it's fine if they are in `cuda_sources`.
One thing: `cpp_sources=""` might cause issues with some versions of PyTorch? Usually it's okay to pass empty string. But to be safe, we can put a trivial declaration in `cpp_sources` and the implementation in `cuda_sources`. Or put everything in `cpp_sources` and mark kernel with `__global__`? No, kernels must be in `.cu` source. Actually you can put `__global__` in `.cpp` files if compiled with nvcc, but `load_inline` compiles `cpp_sources` with the C++ compiler, not nvcc. So kernels must be in `cuda_sources`.
So:
```python
cpp_sources = 'torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim);'
```
And `cuda_sources` contains the definition. This works because the generated pybind code will include this declaration (or generate its own). Actually the generated code does `extern ...` for each function. If we provide a declaration in `cpp_sources`, it's fine. If not, the generated code still emits its own `extern` declaration before using it, based on the signature inferred from the call? Wait, `load_inline` might generate wrappers that call the function by name, but they need a declaration. I'm not 100% sure. Let's check the source of `torch.utils.cpp_extension.load_inline`.
In PyTorch, `load_inline` uses `CppExtension` or `CUDAExtension` internally. It generates a small wrapper file. Actually, for each function in `functions`, it doesn't automatically declare them. The user is expected to declare them in `cpp_sources` and define them in `cpp_sources` or `cuda_sources`. But if the function is defined in `cuda_sources`, the generated pybind wrapper might be in a `.cpp` file that needs a declaration. It might not have one unless we provide it.
Actually, looking at PyTorch docs/examples: `load_inline` is used like:
```python
source = """
at::Tensor sin_add(at::Tensor x, at::Tensor y) {
return x.sin() + y.sin();
}
"""
module = load_inline(name='inline_extension', cpp_sources=[source], functions=['sin_add'])
```
Here `cpp_sources` contains both declaration and definition. The generated pybind code calls `sin_add`. Since `sin_add` is defined in the same translation unit (the `cpp_sources` are compiled together with generated code?), they are all combined into one `.cpp` file and compiled. Wait, `load_inline` creates a temporary directory, writes `cpp_sources` to a `.cpp` file, writes `cuda_sources` to a `.cu` file, and compiles them together. The generated pybind module is usually in a separate `.cpp` file. So if the function is defined in `.cu`, the generated `.cpp` file needs an `extern` declaration. If we don't provide one, the generated file might try to call it without declaration, which is a compilation error in C++ (implicit declaration not allowed in C++). But wait, the generated file is compiled as C++. Does it include any header we provide? No.
Actually, `load_inline` might generate something like:
```cpp
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("sin_add", &sin_add, "...");
}
```
This requires `sin_add` to be declared before this point. If `sin_add` is defined in the `.cu` file, the `.cpp` file with pybind won't see it. In many PyTorch versions, `load_inline` concatenates `cpp_sources` to a `.cpp` file and compiles it. The pybind code is auto-generated as part of that `.cpp` file, after the user-provided `cpp_sources`. So if we put the declaration in `cpp_sources`, it will be visible to the pybind code. The definition in `cuda_sources` will be linked later.
So we should put `torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim);` in `cpp_sources`. Or we can put the full host wrapper in `cpp_sources` and call the kernels declared in `cuda_sources`. But kernels cannot be declared in C++ code (they need `__global__`, which is CUDA). So we can declare the kernels in `cuda_sources` and call them from a host function defined in `cpp_sources`. Or we can define the host function in `cuda_sources` and just declare it in `cpp_sources`. Both work. Let's do declaration in `cpp_sources` and definition in `cuda_sources`.
Wait, the pybind code will call `cumprod_cuda`. If we put only the declaration in `cpp_sources`, the generated pybind code will compile against that declaration, and the linker will find the definition in the `.cu` object file. That should work.
Let's do:
```python
cpp_sources = """
torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim);
"""
```
But wait, we also need to include `<torch/extension.h>` in `cpp_sources` for `torch::Tensor` type. The generated file likely includes it already, but it's safer to include it. In `cuda_sources`, we also need it.
Actually, the generated pybind wrapper includes `torch/extension.h`. But the declaration might need it too if it's parsed before the includes. In PyTorch's `load_inline`, the generated code likely includes necessary headers at the top of the generated `.cpp` file, then appends user `cpp_sources`, then writes pybind module. So our declaration in `cpp_sources` will appear after the includes. Good.
Let's check with a known example. Actually, I can recall that in PyTorch's CUDA tutorial:
```python
cuda_source = """
#include <torch/extension.h>
__global__ void... { ... }
torch::Tensor forward(torch::Tensor input) {
...
}
"""
cpp_source = """
#include <torch/extension.h>
torch::Tensor forward(torch::Tensor input);
"""
```
This works.
However, some versions of `load_inline` might just generate a `.cpp` file with:
```cpp
#include <torch/extension.h>
// user cpp_sources here
// pybind code here
```
So as long as our `cpp_sources` doesn't conflict, it's fine. Let's include the declaration.
Also, `load_inline` param is `cpp_sources` which can be a string or list of strings. Same for `cuda_sources`.
Now, I'll prepare the final Python string. I need to be careful with quotes and escaping. Since the code includes triple quotes inside a string, I need to handle it. The easiest way is to define `kernel_code` as a raw triple-quoted string `r'''...'''` and inside it use double quotes for the CUDA source. The CUDA source itself contains `#include <...>` and `#pragma`. No triple quotes inside. So `r'''...'''` works.
Let's draft the final code:
```python
kernel_code = r'''
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline
cpp_sources = """
#include <torch/extension.h>
torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim);
"""
cuda_sources = """
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <c10/cuda/CUDAException.h>
__global__ void cumprod_fast_kernel(const float* __restrict__ input,
float* __restrict__ output,
int rows, int cols) {
const int ITEMS_PER_THREAD = 32;
const int BLOCK_SIZE = 1024;
const int NUM_WARPS = BLOCK_SIZE / 32;
int row = blockIdx.x;
if (row >= rows) return;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
const float* row_in = input + static_cast<long long>(row) * cols;
float* row_out = output + static_cast<long long>(row) * cols;
float reg[ITEMS_PER_THREAD];
int offset = tid * ITEMS_PER_THREAD;
const float4* in4 = reinterpret_cast<const float4*>(row_in + offset);
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD / 4; ++i) {
float4 v = in4[i];
reg[i*4+0] = v.x;
reg[i*4+1] = v.y;
reg[i*4+2] = v.z;
reg[i*4+3] = v.w;
}
float acc = 1.0f;
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD; ++i) {
acc *= reg[i];
reg[i] = acc;
}
float thread_total = acc;
float inclusive = thread_total;
#pragma unroll
for (int o = 1; o < 32; o <<= 1) {
float other = __shfl_up_sync(0xFFFFFFFF, inclusive, o);
if (lane >= o) inclusive *= other;
}
float warp_exclusive;
if (lane == 0) {
warp_exclusive = 1.0f;
} else {
warp_exclusive = __shfl_sync(0xFFFFFFFF, inclusive, lane - 1);
}
__shared__ float warp_total_smem[NUM_WARPS];
__shared__ float block_prefix_smem[NUM_WARPS];
float warp_total = __shfl_sync(0xFFFFFFFF, inclusive, 31);
if (lane == 0) {
warp_total_smem[warp_id] = warp_total;
}
__syncthreads();
if (tid == 0) {
float running = 1.0f;
#pragma unroll
for (int i = 0; i < NUM_WARPS; ++i) {
block_prefix_smem[i] = running;
running *= warp_total_smem[i];
}
}
__syncthreads();
float block_prefix = block_prefix_smem[warp_id];
float mult = block_prefix * warp_exclusive;
float4* out4 = reinterpret_cast<float4*>(row_out + offset);
#pragma unroll
for (int i = 0; i < ITEMS_PER_THREAD / 4; ++i) {
float4 v;
v.x = mult * reg[i*4+0];
v.y = mult * reg[i*4+1];
v.z = mult * reg[i*4+2];
v.w = mult * reg[i*4+3];
out4[i] = v;
}
}
__global__ void cumprod_simple_kernel(const float* __restrict__ input,
float* __restrict__ output,
int rows, int cols) {
int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= rows) return;
const float* in_ptr = input + static_cast<long long>(row) * cols;
float* out_ptr = output + static_cast<long long>(row) * cols;
float acc = 1.0f;
for (int c = 0; c < cols; ++c) {
acc *= in_ptr[c];
out_ptr[c] = acc;
}
}
torch::Tensor cumprod_cuda(torch::Tensor x, int64_t dim) {
TORCH_CHECK(x.dim() == 2, "cumprod_cuda: only 2D tensors supported");
int rows = x.size(0);
int cols = x.size(1);
auto out = torch::empty_like(x);
const float* in_ptr = x.data_ptr<float>();
float* out_ptr = out.data_ptr<float>();
if (cols == 32768 && x.is_contiguous() && x.scalar_type() == torch::kFloat32 && (dim == 1 || dim == -1)) {
dim3 grid(rows);
dim3 block(1024);
cumprod_fast_kernel<<<grid, block>>>(in_ptr, out_ptr, rows, cols);
C10_CUDA_KERNEL_LAUNCH_CHECK();
} else {
int threads = 256;
dim3 block(threads);
dim3 grid((rows + threads - 1) / threads);
cumprod_simple_kernel<<<grid, block>>>(in_ptr, out_ptr, rows, cols);
C10_CUDA_KERNEL_LAUNCH_CHECK();
}
return out;
}
"""
_ext = load_inline(
name="cumprod_ext",
cpp_sources=cpp_sources,
cuda_sources=cuda_sources,
functions=["cumprod_cuda"],
extra_cuda_cflags=["-O3", "--use_fast_math"],
verbose=False,
)
class ModelNew(nn.Module):
def __init__(self, dim):
super().__init__()
self.dim = dim
def forward(self, x):
return _ext.cumprod_cuda(x, self.dim)
'''
```
This looks good.
One concern: inside the CUDA source, `__shfl_sync(0xFFFFFFFF, inclusive, lane - 1)` when `lane == 0` is guarded. But in the else branch, lane >= 1, so `lane - 1` is valid. Good.
Another concern: `__shfl_sync(0xFFFFFFFF, inclusive, 31)` when `lane` is any thread. That's fine.
Now compile.