reasoning
We need to write a custom CUDA kernel that replaces the forward of a Linear + BatchNorm1d + bias add + divide + Swish (x * sigmoid(x)). Must accept same constructor args. We need to produce numerically equivalent outputs at fp32. Need to be faster than PyTorch reference.
We need to use torch.utils.cpp_extension.load_inline to compile inline CUDA. We must not provide own pybind11 module block.
We need to understand PyTorch's BatchNorm1d behavior for training vs eval. The model is used in forward only (no grad?) but we should probably keep track of running stats? The reference does bn = nn.BatchNorm1d(out_features, eps=..., momentum=...). In PyTorch, when model is in eval mode? By default after init, model is in train mode. The reference probably assumes training? Or just forward with default mode? Typically in these coding challenge tasks, the model is called in default mode (train). But maybe they call model.eval() before forward? In the reference, they don't set eval. So BatchNorm uses current batch statistics and updates running_mean/running_var. But for a custom kernel, we need to match PyTorch's BatchNorm behavior precisely.
However, for speedup, we can fuse the operations. In training mode, BN computes mean and variance across batch dimension, then normalizes: (x - mean) / sqrt(var + eps) * gamma + beta. Then add bias, divide, Swish.
But we need to keep track of running_mean/running_var? The custom kernel can call PyTorch's BN to update running stats? Or we can implement BN in CUDA to update running stats. However, the evaluation may test correctness with a single forward call. They may instantiate ModelNew with same args and call forward once. Since default momentum=0.1, running stats change but they might not check them? They may compare outputs only. So we can just compute output correctly for that single forward. But if they compare with reference Model (which updates running stats) we need to also update running stats equivalently. But if they don't check the internal state of BN, we can ignore updating running stats. However, safety: we should update running_mean and running_var exactly as PyTorch does to avoid hidden mismatches if they inspect state. But they likely only compare output and maybe gradients? The problem says "Replace the forward computation... produce numerically equivalent outputs". It doesn't mention backward. So we only need forward.
But to be safe, we could keep a nn.BatchNorm1d module inside ModelNew, and in forward, maybe use its running stats? But we want to fuse. We could compute the BN part in the kernel and also update running stats in the kernel. That requires knowing the formula.
PyTorch BatchNorm1d (training) for N, C input:
- Compute mean over N (and optionally over other dims). Here input to BN is [batch_size, out_features]. So mean per feature: mean = x.mean(dim=0).
- Compute var = x.var(dim=0, unbiased=False) (i.e., population variance). Actually PyTorch uses biased variance (N) not unbiased (N-1). Let me verify: In PyTorch's batch_norm implementation, it computes save_mean and save_var (biased). Yes, the running_var update uses biased variance (with Bessel's correction? Actually running_var = (1 - momentum) * running_var + momentum * save_var, where save_var is the unbiased variance? Let me check.)
PyTorch's native_batch_norm: The saved variance is the inverse standard deviation (1/sqrt(var+eps)). For training, it computes mean and biased variance (var_biased = sum((x-mean)^2) / N). Actually I need to check exactly. In CUDA, batch_norm training uses welford to compute mean and biased variance (or unbiased?).
Let's verify by looking at known facts. In PyTorch nn.BatchNorm1d:
- During training: it computes batch statistics: mean = x.mean(dim=0), var = x.var(dim=0, unbiased=True?) Wait, in PyTorch documentation: "The standard-deviation is calculated via the biased estimator, equivalent to torch.var(input, unbiased=False)". Actually looking at the docs: For BatchNorm, the variance is computed as the average of the squared deviations (biased). But in PyTorch's implementation of CUDA batch norm, for cuDNN-based batch norm, the variance used for normalization is the moving variance (running_var) or the batch variance? In training mode, cuDNN might compute unbiased variance? Let's check.
In PyTorch's Python implementation (when not using cuDNN), they compute:
mean = input.mean([0, 2, 3])
var = input.var([0, 2, 3], unbiased=False)
and then:
output = (input - mean[None, :, None, None]) / torch.sqrt(var[None, :, None, None] + eps)
if weight is not None: output = output * weight[None, :, None, None]
if bias is not None: output = output + bias[None, :, None, None]
running_mean = momentum * mean + (1 - momentum) * running_mean
running_var = momentum * var * n / (n - 1) + (1 - momentum) * running_var
Wait! I think PyTorch's BatchNorm layer uses unbiased variance for the moving average, but biased variance for normalization. Actually the docs say:
"The mean and standard-deviation are calculated per-dimension over the mini-batches and \gamma and \beta are learnable parameter vectors of size C (where C is the input size). By default, the elements of \gamma are set to 1 and the elements of \beta are set to 0. The standard-deviation is calculated via the biased estimator, equivalent to torch.var(input, unbiased=False). Also, by default, during training this layer keeps running estimates of its computed mean and variance, which are then used for normalization during evaluation. The running estimates are kept with a default momentum of 0.1.
Note:
- Because the Batch Normalization is done over the C dimension, computing statistics on (N, L) slices, it’s called Temporal Batch Normalization.
"
But I recall that the running_var update in PyTorch uses unbiased variance (N/(N-1) * biased var). Let's verify with a quick script mentally.
In PyTorch's batch_norm.py (nn.modules.batchnorm), the code for forward training (when not using functional.batch_norm with cudnn) might call:
import torch.nn.functional as F
return F.batch_norm(
input, self.running_mean, self.running_var, self.weight, self.bias,
self.training, self.momentum, self.eps)
And F.batch_norm calls either aten::batch_norm or cudnn. In the CPU fallback or native implementation, the ATen native_batch_norm kernel in PyTorch computes mean and invstd. The running_var update uses the biased variance? Let's look at PyTorch's `native_batch_norm` implementation. In ATen/native/Normalization.cpp, `batch_norm_cpu_kernel` accumulates mean and var. Actually for CPU:
Tensor batch_norm_cpu(const Tensor& input, const Tensor& weight, const Tensor& bias, const Tensor& running_mean, const Tensor& running_var, bool train, double momentum, double eps) {
// ...
if (train) {
auto save_mean = at::empty({n_input}, input.options());
auto save_var = at::empty({n_input}, input.options());
// compute save_mean, save_var (biased)
batch_norm_cpu_collect_stats_template<...>(input, save_mean, save_var);
// then normalize
batch_norm_cpu_transform_template<...>(input, weight, bias, save_mean, save_var, running_mean, running_var, train, eps, momentum);
}
}
And in collect_stats, variance is computed as sum_sq / n - mean^2 which is biased. Then in `batch_norm_cpu_transform_template`, the running_var update is:
running_mean = momentum * save_mean + (1 - momentum) * running_mean;
running_var = momentum * save_var * n / (n - 1) + (1 - momentum) * running_var;
Yes, because they want the running variance to be unbiased estimator of population variance (like for inference). Actually the running_var tracks the population variance unbiased? Wait, for inference, BatchNorm uses running_var to normalize. If running_var is unbiased, then when dividing by sqrt(running_var + eps), it's consistent with using unbiased variance? But the docs say standard-deviation is calculated via biased estimator. That likely refers to the batch statistics used for normalization in the current forward, not the running variance.
Let's verify by looking at PyTorch source. In `aten/src/ATen/native/Normalization.cpp`, line ~`batch_norm_cpu_transform_template`:
running_mean[i] = momentum * mean[i] + (1 - momentum) * running_mean[i];
running_var[i] = momentum * var[i] * n / (n - 1) + (1 - momentum) * running_var[i];
Yes, I recall this. So the running_var tracks the unbiased sample variance. But the normalization in current training step uses biased variance (var). Wait, but in `batch_norm_cpu_transform_template`, the normalization uses `var` (biased) for the current batch, not running_var: `inv_var = 1 / std::sqrt(var[i] + eps)`. So the output uses biased variance. The running_var is updated with unbiased variance.
For cuDNN, the behavior is the same: cuDNN computes mean and variance (biased) and updates running_mean and running_var similarly (running_var is unbiased).
So to match PyTorch exactly, our fused kernel needs to compute:
- mean per feature: sum over batch / N
- var_biased per feature: sum of squared deviations / N
- inv_std = 1 / sqrt(var_biased + eps)
- normalized = (matmul_out - mean) * inv_std
- scaled = normalized * gamma (weight) + beta (bias_of_bn)
Then add custom bias, divide by divide_value, then swish: x * sigmoid(x).
And update running stats:
- running_mean = m * mean + (1-m) * running_mean
- running_var = m * var_biased * N/(N-1) + (1-m) * running_var
But hold on: if we only compute output for a single forward, we might not need to update running stats, but to be safe, let's do it.
However, there is also Linear (matrix multiplication). We can fuse Linear + BN + bias + divide + Swish into a single kernel. The Linear can be done via cublas or just inside the kernel? Since in_features and out_features are both 8192, and batch_size is 1024, this is a large GEMM. Doing a naive matmul inside a CUDA kernel will be extremely slow compared to cuBLAS. So we should keep the matmul as a cuBLAS call (i.e., use `torch.mm` or `F.linear` or `self.matmul(x)`), and then fuse the remaining operations (BN, bias add, divide, swish) into a custom elementwise kernel.
But can we fuse BN with the matmul output without an extra pass? Actually we can do the matmul with `torch.mm(x, weight.t()) + bias` (if linear has bias). Wait, the reference uses `nn.Linear(in_features, out_features)`, which includes a bias term. Actually nn.Linear has weight and bias. The reference then does BatchNorm, which also has weight (gamma) and bias (beta). So there are two bias additions: the Linear bias, then BN's beta, then custom bias. But we can fuse everything after the matmul into one kernel.
But to match numerically, we need to apply Linear weight and bias exactly as PyTorch does. `nn.Linear` does `x @ W^T + b`. So we can call `F.linear(x, self.matmul.weight, self.matmul.bias)`, then apply our fused kernel on the result.
Alternatively, we can keep `self.matmul` as the nn.Linear module and call it. But then we get the output, and then apply our fused kernel. That means we have the intermediate tensor. But that's fine. The kernel fuses BN+bias+divide+swish. But can we also fuse the Linear bias into the same kernel? Yes, we can manually compute `x @ W^T` using `torch.mm(x, self.weight.t())` (no bias), then in the fused kernel we also add the linear bias. But `torch.mm` may be slightly slower than `F.linear` with bias because cuBLAS can fuse bias addition? Actually PyTorch's linear with bias calls `addmm` which fuses bias into the GEMM (C = beta*C + alpha*(A@B)). So using `F.linear` is efficient. So we can compute `matmul_out = F.linear(x, self.matmul.weight, self.matmul.bias)` in PyTorch, then call our fused CUDA kernel on `matmul_out`. However, that means we don't fuse the linear bias addition, and we do an extra memory read/write for matmul_out. But the main cost might be the GEMM itself. For 1024x8192 @ 8192x8192, it's about 1024*8192*8192*2 = ~137 GFLOPs. At e.g., 100 TFLOPS on A100, that's ~1.37 ms. Memory bandwidth for reading/writing the output: 1024*8192*4 bytes ~ 33.5 MB. At 1.5 TB/s, that's 0.022 ms. So the elementwise pass is tiny. Fusing it won't give huge speedup, but maybe the reference does separate kernel launches for BN, bias add, division, sigmoid, multiplication. Launch overhead of 5 kernels vs 1 could save some time.
But the real win might be fusing everything after matmul into a single kernel. Even better, we could also fuse the linear bias addition into the same kernel by calling `torch.mm(x, W.t())` (without bias) and then adding bias inside the kernel. But `torch.mm` might be slightly less efficient than `addmm` because `addmm` can avoid an extra read of C if beta=0? Actually `torch.mm` is just C = alpha*A@B. `addmm` does C = beta*C + alpha*A@B. When bias is provided and beta=1, it reads C. But if we compute `torch.mm(x, W.t())`, it's also just C = A@B, which is fine. No extra input. Then in our kernel, we can add linear bias, BN stats, BN gamma/beta, custom bias, divide, swish. So we fuse linear bias, BN, etc.
But we need to compute BN statistics (mean and var) over the matmul output. If we use `torch.mm`, we need the output tensor to compute mean and var. We can compute mean and var in the same kernel that also does normalization? No, we need the mean and var across the batch before normalizing each element. So we need a reduction pass first (or two). Then a second pass for normalization and the rest. But we can compute mean and var with warp/block reductions efficiently. However, a two-pass approach (first compute mean and var with atomics or shared memory, second apply transformation) avoids extra memory but adds complexity. Alternatively, we can compute matmul_out, then compute mean and var with `torch.mean` and `torch.var` (which are highly optimized), and then use our fused kernel for the rest. That would be: matmul (cuBLAS) -> mean/var (optimized reduction) -> fused transform kernel. Total 3 kernel launches vs reference maybe 5 or 6. This might be enough for a decent speedup.
Given the problem constraints (10 turns, 30 tool calls), we should aim for a robust solution.
Let's check the reference forward steps:
1. x = self.matmul(x) -> linear
2. x = self.bn(x) -> batch norm (training mode, updates running stats)
3. x = x + self.bias -> broadcast add
4. x = x / self.divide_value -> elementwise division
5. x = x * torch.sigmoid(x) -> swish
BatchNorm1d in training mode does:
- compute mean, var (biased) across batch
- normalize: (x - mean) / sqrt(var + eps) * gamma + beta
- update running_mean, running_var
Step 3: add bias (parameter of shape (1,)). This is a scalar added to all elements.
Step 4: divide by divide_value (scalar).
Step 5: swish.
So the fused kernel can take the linear output, compute mean and var, then produce final output. But to compute mean and var efficiently, we can call `torch.mean(matmul_out, dim=0)` and `torch.var(matmul_out, dim=0, unbiased=False)`. Then we can call a custom kernel that does:
out = ((matmul_out - mean) / sqrt(var + eps) * gamma + beta + linear_bias? + custom_bias) / divide_value
Wait, linear_bias is already in matmul_out if we used F.linear. If we use torch.mm, we need to add linear_bias in the kernel.
Let's decide: use F.linear to compute matmul_out (with bias). Then compute mean and var on matmul_out. Then call fused kernel.
But if we compute mean and var on matmul_out, that's an extra read of matmul_out (for mean/var) plus a write of out (for fused kernel). The fused kernel reads matmul_out again. So total two reads of matmul_out. In the reference, the BatchNorm layer internally also reads matmul_out to compute stats (one read), writes normalized output (one write), then bias add reads and writes, division reads and writes, swish reads and writes. So reference has many read-writes. Our approach: matmul_out write (one), mean/var read (one), fused kernel read matmul_out (one) write out (one). So total reads: matmul_out is read twice (mean/var and fused), written once. That's less than reference.
Alternatively, we can fuse the mean/var computation into the same kernel if we do two passes: first pass compute mean and var using shared memory reductions, store to SMEM/global, second pass compute transform. But implementing a robust two-pass kernel for large C (8192) and N (1024) might be tricky.
Simpler: compute mean and var with PyTorch. Then do fused kernel.
But we also need to update running_mean and running_var. We can do that in Python:
with torch.no_grad():
self.bn.running_mean = self.bn.running_mean * (1 - momentum) + mean * momentum
self.bn.running_var = self.bn.running_var * (1 - momentum) + var * (N/(N-1)) * momentum
But wait, if we keep `self.bn` as a `nn.BatchNorm1d` module inside `ModelNew`, we could simply call it. But that defeats the purpose. However, we can keep the module just for its parameters and running stats, but compute forward ourselves. In the constructor, we can create `self.bn = nn.BatchNorm1d(out_features, eps=bn_eps, momentum=bn_momentum)` but set `self.bn.weight` and `self.bn.bias` from loaded state dict? Wait, `ModelNew` must accept same constructor arguments and produce same output. But the evaluation might load the state dict from a reference model? Usually in these tasks, the ModelNew is instantiated with the same arguments and then its parameters might be initialized randomly or loaded from a checkpoint. We need to make sure that if the reference model's state dict is loaded into ModelNew, it works. The state dict keys would be `matmul.weight`, `matmul.bias`, `bn.weight`, `bn.bias`, `bn.running_mean`, `bn.running_var`, `bn.num_batches_tracked`, `bias`. So we should keep submodules with the same names: `self.matmul` = nn.Linear(...), `self.bn` = nn.BatchNorm1d(...), `self.bias` = nn.Parameter(...). That way state dict loading works automatically.
But if we keep `self.bn`, then in forward we can use its parameters: `self.bn.weight`, `self.bn.bias`, `self.bn.running_mean`, `self.bn.running_var`. And we can manually update them.
BUT: we also need to handle `self.bn.num_batches_tracked`. Usually it's incremented in each forward training step. We should increment it if we want exact state dict match. But output correctness does not depend on it. Just to be safe, we can increment it.
So the plan:
- In `ModelNew.__init__`, create same submodules as reference: `self.matmul = nn.Linear(...)`, `self.bn = nn.BatchNorm1d(...)`, `self.bias = nn.Parameter(torch.randn(bias_shape))`, `self.divide_value = divide_value`.
- In `forward(self, x)`:
- `matmul_out = self.matmul(x)` # [N, C]
- Compute `mean = matmul_out.mean(dim=0)` # [C]
- Compute `var = matmul_out.var(dim=0, unbiased=False)` # [C]
- Update running stats:
```
if self.training:
with torch.no_grad():
self.bn.running_mean = self.bn.running_mean * (1 - self.bn.momentum) + mean * self.bn.momentum
self.bn.running_var = self.bn.running_var * (1 - self.bn.momentum) + var * (matmul_out.size(0) / (matmul_out.size(0) - 1.0)) * self.bn.momentum
if hasattr(self.bn, 'num_batches_tracked'):
self.bn.num_batches_tracked += 1
```
- Call fused CUDA kernel:
```
out = fused_bn_bias_div_swish(matmul_out, mean, var, self.bn.weight, self.bn.bias, self.bias, self.bn.eps, self.divide_value)
```
- Return out.
The fused CUDA kernel does:
```
for each element i,j:
val = input[i,j]
norm = (val - mean[j]) * rsqrt(var[j] + eps)
val = norm * gamma[j] + beta[j]
val = val + bias_scalar
val = val / divide_value
val = val * sigmoid(val)
out[i,j] = val
```
Wait: we need to match PyTorch's BatchNorm exactly. PyTorch's training BN uses biased variance for normalization. `var = matmul_out.var(dim=0, unbiased=False)` is exactly biased variance. `rsqrt(var + eps)` matches.
One subtlety: `nn.Linear`'s bias is added before BN. So the order is correct.
We need to write the CUDA kernel using `torch.utils.cpp_extension.load_inline`. The kernel should be efficient. Since N=1024, C=8192, total elements = 8,388,608. Each thread can process one element or multiple. Using a simple elementwise kernel with enough threads should be okay. But we need to ensure coalesced memory access: threads in a block should access contiguous memory. So we can map threadIdx.x + blockIdx.x * blockDim.x across the flattened N*C array, or across C with blocks over N.
Flattened approach: each thread processes one element. Total threads ≈ 8.4M. On a GPU with ~100K threads max occupancy, we need ~84 blocks of 1024 threads. That's not many? Actually 8.4M / 1024 = 8192 blocks. That's plenty. Each block handles a contiguous chunk of 1024 elements. Since memory is row-major (N, C), flattening ensures contiguous access. So it's fine.
But we need to handle broadcasting of mean, var, gamma, beta across N. We can compute index = blockIdx.x * blockDim.x + threadIdx.x. Then row = index / C, col = index % C. Or we can compute directly. However, if C is not a power of two, division is expensive. Better to have each block handle a row? With N=1024, C=8192. Block size 256 or 512 or 1024. Grid: (N, (C + block_size-1)/block_size). So each block handles a row segment. This avoids integer division per thread.
Let's do:
- blockDim.x = 128 or 256
- gridDim.x = (C + block_size - 1) / block_size
- gridDim.y = N
Then in kernel:
```
int col = blockIdx.x * blockDim.x + threadIdx.x;
int row = blockIdx.y;
if (col < C) {
float val = input[row * C + col];
...
}
```
This ensures coalesced access within a block (threads in a warp access contiguous columns across rows? Wait, row-major: element at [row, col] is at index row*C + col. Threads in a warp with consecutive threadIdx.x but same row will access consecutive memory addresses. Yes, because they access col, col+1, col+2... So coalesced.
But if blockDim.x is 128, then warp of 32 threads accesses 32 consecutive floats. That's coalesced.
So this is good.
Now we can write the kernel.
But we need to consider if we can get better speed by fusing the reduction as well. Maybe we can compute mean and var inside the kernel using shared memory and then do the transform. Since N=1024 is relatively small, we could have each block along C handle all N elements for one column. For example, blockDim.x = 256 threads, each block processes one column? Or each warp processes one column? N=1024, C=8192. We need to process 8192 columns. If each block handles one column with 1024 threads (one per row), that's 8192 blocks of 1024 threads = ~8M threads, which is okay. Within a block, threads access elements stride C apart (i.e., memory not contiguous). That is strided access. But if block processes one column, threads in a warp access memory with stride C (8192). That's bad for coalescing. However, we could have each block process a tile of columns and rows to achieve coalescing.
Alternatively, keep it simple: do PyTorch mean/var + fused kernel. That should already be faster than reference because reference has separate kernels for each op and PyTorch's BN might be less efficient for this shape.
Wait, but PyTorch's BN for 2D input might call cuDNN or native, which is already fused into a single kernel? Actually cuDNN batch normalization for training does the forward and backward in one kernel, but for training it does compute mean/var and normalization in a single kernel (or multiple). It might already be quite optimized. However, the reference then does bias add, division, and swish in separate kernels. So our fused kernel eliminates those extra passes and kernel launches.
Also, note that `nn.Linear` + `nn.BatchNorm1d` can be mathematically fused: `BN(Linear(x)) = (xW^T + b - mean) / sqrt(var+eps) * gamma + beta`. If we compute mean and var after linear, we still need the linear output. But if we could compute mean and var of the linear output without materializing it fully? That's hard because mean of xW^T + b is mean(x) * W^T + b. Since x is [N, in], W is [out, in]. But computing mean(x) is cheap. However, var of xW^T is more complex: var_j = sum_i (W_{j,i}^2 * var_of_x_i) + cross terms if rows of x are correlated. Not trivial. So we must materialize linear output.
Thus, the plan is solid.
Now we need to produce the code. We need to define `ModelNew` class, and load inline CUDA.
Let's write the CUDA kernel source:
```cpp
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <math.h>
__global__ void fused_bn_bias_div_swish_kernel(
const float* __restrict__ input,
const float* __restrict__ mean,
const float* __restrict__ var,
const float* __restrict__ gamma,
const float* __restrict__ beta,
float bias,
float eps,
float divide_value,
float* __restrict__ output,
int N,
int C) {
int col = blockIdx.x * blockDim.x + threadIdx.x;
int row = blockIdx.y;
if (col < C && row < N) {
float val = input[row * C + col];
float m = mean[col];
float v = var[col];
float g = gamma[col];
float b = beta[col];
float inv_std = rsqrtf(v + eps);
val = (val - m) * inv_std * g + b;
val = val + bias;
val = val / divide_value;
// Swish: x * sigmoid(x)
float sig = 1.0f / (1.0f + expf(-val));
val = val * sig;
output[row * C + col] = val;
}
}
torch::Tensor fused_bn_bias_div_swish(
torch::Tensor input,
torch::Tensor mean,
torch::Tensor var,
torch::Tensor gamma,
torch::Tensor beta,
float bias,
float eps,
float divide_value) {
int N = input.size(0);
int C = input.size(1);
auto output = torch::empty_like(input);
dim3 block(128);
dim3 grid((C + block.x - 1) / block.x, N);
fused_bn_bias_div_swish_kernel<<<grid, block>>>(
input.data_ptr<float>(),
mean.data_ptr<float>(),
var.data_ptr<float>(),
gamma.data_ptr<float>(),
beta.data_ptr<float>(),
bias,
eps,
divide_value,
output.data_ptr<float>(),
N,
C);
return output;
}
```
We should use `__restrict__` and `float` for performance.
Wait: in PyTorch, `self.bias` is a parameter of shape `(1,)`. It is a scalar. We can pass it as float. But if `self.bias` is on GPU, we can do `self.bias.item()`. Since we are in forward, it's a parameter on the same device. So we can do `self.bias.item()`.
One subtlety: we need to handle the case where `gamma` or `beta` might be None? In `nn.BatchNorm1d`, `weight` and `bias` are always created (affine=True by default). The reference uses default affine=True. So they exist.
Also, `self.bn.eps` is available as `self.bn.eps`, `self.bn.momentum` as `self.bn.momentum`.
Now, we need to make sure that the kernel is correctly compiled with `load_inline`. The code string must be valid C++.
`load_inline` usage:
```python
import torch.utils.cpp_extension as ext
custom_ops = ext.load_inline(
name="fused_bn_bias_div_swish",
cpp_sources="""...""",
cuda_sources="""...""",
functions=["fused_bn_bias_div_swish"],
extra_cuda_cflags=["-O3"],
extra_cflags=["-O3"],
verbose=True,
)
```
Wait, we are only providing `cuda_sources` and `cpp_sources`. The `fused_bn_bias_div_swish` function is a host function defined in the C++ source. We can put the kernel and launcher in `cuda_sources` or `cpp_sources`. Actually `load_inline` treats both. Usually `cpp_sources` contains the host wrapper and `cuda_sources` the CUDA code. But it's okay to put everything in one of them. Let's put the CUDA kernel in `cuda_sources` and the torch wrapper in `cpp_sources`. Or everything in `cpp_sources` if we want. But `cpp_sources` is compiled as C++ host code, and `cuda_sources` is compiled as CUDA C++. We can put the kernel in `cuda_sources` and the host wrapper in `cpp_sources`. However, since the host wrapper calls the kernel, it needs the kernel declaration. We can forward declare the kernel in `cpp_sources` or put everything in `cuda_sources`. `cuda_sources` is compiled with nvcc, which understands both host and device code. So we can just put everything in `cuda_sources` and leave `cpp_sources` empty or with minimal includes. Actually `load_inline` with only `cuda_sources` often works but the host functions need to be in C++ compilation unit? I think `cuda_sources` is compiled by nvcc as a .cu file, and the functions declared there are callable from Python bindings if listed in `functions`. So we can just put everything in `cuda_sources`.
Let's check typical usage:
```python
import torch.utils.cpp_extension
module = torch.utils.cpp_extension.load_inline(
name="inline_extension",
cpp_sources="",
cuda_sources=cuda_source,
functions=["hello"],
extra_cuda_cflags=['--use_fast_math'],
verbose=True,
)
```
So yes, we can put everything in `cuda_sources`.
Make sure to include `<torch/extension.h>`.
We also want to ensure numerical equivalence. Using `rsqrtf` and `expf` should match PyTorch's fp32 math closely enough.
One issue: PyTorch's `torch.sigmoid` uses a numerically stable formula? For fp32, `1/(1+exp(-x))` is standard. PyTorch might use a fast approximation or something? In CUDA, using `expf` should be accurate enough. For large negative x, expf(-x) might overflow? Actually `expf(89)` ~ 1e38, expf(90) overflow to inf for float. For x = -89, exp(-(-89)) = exp(89) ~ 1e38, fine. For x = -100, exp(100) overflows. Then `1/(1+inf) = 0`, which is correct for sigmoid. For x = 100, exp(-100) underflows to 0, `1/(1+0) = 1`, correct. So it's fine.
Now, we must also match PyTorch's BN variance computation exactly. We will compute `var = matmul_out.var(dim=0, unbiased=False)`. That is exact.
But wait: PyTorch's `matmul_out.var(dim=0, unbiased=False)` computes the variance using the two-pass algorithm or Welford? It might have slightly different rounding than our kernel which computes var on the fly. But since we compute var with PyTorch's built-in function, the var used in our kernel is exactly the same as PyTorch's BN would compute.
But is PyTorch's `nn.BatchNorm1d` variance computed in fp32? Yes, for fp32 input, everything is fp32.
One more subtlety: `nn.Linear` uses `addmm`, which might use TF32 on Ampere by default. The reference model's linear will use TF32 if allowed. Our custom kernel uses `self.matmul(x)`, which also uses TF32 if enabled. So that's fine.
But the evaluation might disable TF32? Not sure. Usually PyTorch defaults to TF32 enabled on Ampere. Both reference and our model will use it for linear. Good.
Now, what about the memory layout? The linear output is contiguous. mean, var, gamma, beta are 1D tensors of size C. They are contiguous.
Let's write the Python code for `ModelNew`.
```python
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.cpp_extension import load_inline
CUDA_SOURCE = """
#include <torch/extension.h>
#include <cuda_runtime.h>
__global__ void fused_bn_bias_div_swish_kernel(
const float* __restrict__ input,
const float* __restrict__ mean,
const float* __restrict__ var,
const float* __restrict__ gamma,
const float* __restrict__ beta,
float bias,
float eps,
float divide_value,
float* __restrict__ output,
int N,
int C) {
int col = blockIdx.x * blockDim.x + threadIdx.x;
int row = blockIdx.y;
if (col < C && row < N) {
float val = input[row * C + col];
float inv_std = rsqrtf(var[col] + eps);
val = (val - mean[col]) * inv_std * gamma[col] + beta[col];
val = val + bias;
val = val / divide_value;
float sig = 1.0f / (1.0f + expf(-val));
val = val * sig;
output[row * C + col] = val;
}
}
torch::Tensor fused_bn_bias_div_swish(
torch::Tensor input,
torch::Tensor mean,
torch::Tensor var,
torch::Tensor gamma,
torch::Tensor beta,
float bias,
float eps,
float divide_value) {
int N = input.size(0);
int C = input.size(1);
auto output = torch::empty_like(input);
dim3 block(256);
dim3 grid((C + block.x - 1) / block.x, N);
fused_bn_bias_div_swish_kernel<<<grid, block>>>(
input.data_ptr<float>(),
mean.data_ptr<float>(),
var.data_ptr<float>(),
gamma.data_ptr<float>(),
beta.data_ptr<float>(),
bias,
eps,
divide_value,
output.data_ptr<float>(),
N,
C);
return output;
}
"""
custom_ops = load_inline(
name="fused_bn_bias_div_swish",
cpp_sources="",
cuda_sources=CUDA_SOURCE,
functions=["fused_bn_bias_div_swish"],
extra_cuda_cflags=["-O3", "--use_fast_math"],
extra_cflags=["-O3"],
verbose=False,
)
class ModelNew(nn.Module):
def __init__(self, in_features, out_features, bn_eps=1e-5, bn_momentum=0.1, bias_shape=(1,), divide_value=1.0):
super(ModelNew, self).__init__()
self.matmul = nn.Linear(in_features, out_features)
self.bn = nn.BatchNorm1d(out_features, eps=bn_eps, momentum=bn_momentum)
self.bias = nn.Parameter(torch.randn(bias_shape))
self.divide_value = divide_value
def forward(self, x):
# Linear
x = self.matmul(x)
# Batch norm stats
if self.training:
mean = x.mean(dim=0)
var = x.var(dim=0, unbiased=False)
# Update running stats exactly as PyTorch
with torch.no_grad():
self.bn.running_mean = self.bn.running_mean * (1 - self.bn.momentum) + mean * self.bn.momentum
n = x.size(0)
unbiased_var = var * (n / (n - 1.0))
self.bn.running_var = self.bn.running_var * (1 - self.bn.momentum) + unbiased_var * self.bn.momentum
if hasattr(self.bn, 'num_batches_tracked'):
self.bn.num_batches_tracked += 1
else:
mean = self.bn.running_mean
var = self.bn.running_var
# Fused kernel: BN + bias + divide + swish
return custom_ops.fused_bn_bias_div_swish(
x, mean, var, self.bn.weight, self.bn.bias, self.bias.item(),
self.bn.eps, self.divide_value
)
```
Need to verify: in eval mode, the reference uses `self.bn(x)`, which uses running stats. It does not recompute mean/var. So our eval branch is correct. In training mode, the reference computes mean/var on the batch. We compute them with `x.mean(dim=0)` and `x.var(dim=0, unbiased=False)`. That's correct.
One nuance: The reference's `self.bn(x)` during training also returns normalized values using the batch stats. It does not use running stats in training. So we correctly use `mean` and `var` in the fused kernel. Good.
Another nuance: `x.mean(dim=0)` returns a 1D tensor of size C. `x.var(dim=0, unbiased=False)` returns 1D. When passed to the kernel, they are contiguous.
We must ensure that `custom_ops` is loaded only once. Using `load_inline` at module load time is fine. But it might take time to compile. That's okay.
We should import `torch.utils.cpp_extension` or just `load_inline`.
Potential memory layout issue: If `x` is not contiguous (e.g., after some transposition), our kernel assumes contiguous row-major with stride C. But `self.matmul(x)` returns contiguous tensor. So fine.
But the kernel's indexing `row * C + col` assumes `x.stride(0) == C` and `x.stride(1) == 1`. We could use `input.stride(0)` and `input.stride(1)` to be safe, but `x` from linear is contiguous. However, we can add stride to be robust:
```
int stride_n = input.stride(0);
int stride_c = input.stride(1);
float val = input[row * stride_n + col * stride_c];
```
But for simplicity and performance, we can assume contiguous. However, if the input is non-contiguous, the kernel will be wrong. But since linear output is always contiguous, it's fine.
But we should check if the evaluation passes any non-contiguous x? `get_inputs()` returns `torch.rand(batch_size, in_features)`, which is contiguous. After linear, it's contiguous. So fine.
Now, what about `blockDim.x = 256`? C = 8192, which is divisible by 256. So no partial blocks. Good.
Now, we must test correctness. Let's compile and run.
Wait: We need to be careful about the `load_inline` call being inside the same file. When `exec()` runs the file, it will compile the CUDA code. The compilation can take some seconds but okay.
One more thing: `load_inline` caches by source code string? Not exactly, it creates a temporary directory and compiles. If the name is the same and source changes, it might reuse old compiled module. We can set `verbose=True` for debugging but false for final to avoid output. Usually `load_inline` with same `name` might not recompile if the source changed because it uses a build directory keyed by name. To avoid caching issues during development, we can vary the name string. But in final submission, it's fine.
Also, there is a known issue: `load_inline` in some environments might require `with_cuda=True`. Actually the function signature: `load_inline(name, cpp_sources, cuda_sources, functions, extra_cflags, extra_cuda_cflags, extra_ldflags, extra_include_paths, build_directory, verbose, with_cuda)`. If we provide `cuda_sources`, it should imply CUDA. But explicit `with_cuda=True` is safer? Actually default is None, which auto-detects. If `cuda_sources` is provided, it should set with_cuda. But we can pass `with_cuda=True` just in case.
Let's produce the full code.