reasoning
Let me analyze this task. I need to implement a custom CUDA kernel for Bayesian linear regression with a conjugate Gaussian prior.
The reference implementation does:
1. Compute `Lambda_post = prior_precision * I + noise_precision * X^T X`
2. Cholesky decomposition: `L = cholesky(Lambda_post)`
3. Compute `mu_post = Lambda_post^{-1} * noise_precision * X^T y` using `cholesky_solve`
4. Predictive mean: `pred_mean = X_test @ mu_post`
5. Predictive variance: `pred_var = sum(X_test @ Sigma_post * X_test, dim=-1) + 1/noise_precision`
The shapes are:
- X: (B, N, D) = (16, 256, 16)
- y: (B, N, 1) = (16, 256, 1)
- X_test: (B, M, D) = (16, 64, 16)
- Lambda_post: (B, D, D) = (16, 16, 16)
- L: (B, D, D) = (16, 16, 16)
- mu_post: (B, D, 1) = (16, 16, 1)
- pred_mean: (B, M, 1) = (16, 64, 1)
- Sigma_post: (B, D, D) = (16, 16, 16)
- pred_var: (B, M) = (16, 64)
The key operations are:
1. Batched matrix multiplication: X^T @ X (DxD matrices)
2. Cholesky decomposition (small matrices, D=16)
3. Cholesky solve
4. More matrix multiplications
Since D=16 is small, we could potentially optimize this well with custom kernels.
I could fuse multiple operations together for efficiency. The Cholesky decomposition and related operations on these small 16x16 matrices could be handled efficiently with custom kernels, possibly leveraging shared memory and cooperative thread groups. For larger dimensions, cuBLAS would be more appropriate, but here I have an opportunity to optimize specifically for this size. I'll start by building a working implementation that uses batched operations, then look for optimization opportunities through kernel fusion.
The main computational flow involves computing the posterior precision matrix through batched matrix multiplication, decomposing it via Cholesky, solving for the posterior mean, then computing the predictive distribution. I can use `torch.linalg.cholesky` for the decomposition and `torch.cholesky_solve` for the linear system, with the inverse computed through `torch.cholesky_inverse`. I could create a fused kernel to combine some of these operations and reduce memory transfers between them. The computational bottleneck centers on three main operations: computing X^T @ X as a (D, D) matrix, performing Cholesky decomposition and solve on the small D×D system, and calculating the predictive variance. Since D=16 is quite small, a custom kernel approach might be viable—I'd launch B×D×D threads with each thread block handling one batch element, using shared memory for the matrix operations and computing the Cholesky decomposition collaboratively within the block.
A more realistic path forward would start with PyTorch's optimized batched operations as a baseline, then selectively fuse expensive operations like the variance computation. The X^T @ X computation itself could benefit from a custom kernel that pre-scales X and accumulates results directly in shared memory rather than through multiple kernel launches. s = self.noise_precision * X^T @ y
mu_post = torch.cholesky_solve(Xty_scaled, L)
pred_mean = X_test @ mu_post
Sigma_post = torch.cholesky_inverse(L)
# Compute variance with einsum for efficiency
tmp = torch.einsum('bmd,bdk,bmk->bm', X_test, Sigma_post, X_test)
pred_var = tmp + 1.0 / self.noise_precision
return pred_mean, pred_var
I can streamline this by computing X^T @ y directly as a single operation, then scale it by noise_precision before solving. The variance calculation uses einsum to efficiently compute the quadratic form across all batches at once, avoiding explicit matrix multiplications.
call → submit_kernel
callsubmit_kernel
{
"kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\n# Compile custom CUDA kernels at module load time\ncuda_source = r\"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <vector>\n\n// Constants for our problem\nconstexpr int BLOCK_SIZE = 256;\nconstexpr int D = 16; // Input dimension\n\n// Fused kernel: X^T @ X with scaling and adding diagonal\n// Each block handles one batch element, computes DxD matrix\n__global__ void compute_lambda_post_kernel(\n const float* __restrict__ X, // (B, N, D)\n float* __restrict__ Lambda, // (B, D, D)\n int B, int N,\n float prior_prec, float noise_prec\n) {\n int b = blockIdx.x;\n int tid = threadIdx.x;\n \n // Shared memory for X tile and accumulation\n __shared__ float X_tile[256][16]; // Store X[b, :, :] transposed\n __shared__ float Lambda_shared[16][16];\n \n // Initialize Lambda_shared to zero\n for (int i = tid; i < D * D; i += blockDim.x) {\n int row = i / D;\n int col = i % D;\n Lambda_shared[row][col] = 0.0f;\n }\n __syncthreads();\n \n // Load X[b, :, :] into shared memory (transposed for coalesced access)\n const float* X_b = X + b * N * D;\n \n // Each thread processes multiple rows\n for (int n = tid; n < N; n += blockDim.x) {\n for (int d = 0; d < D; d++) {\n X_tile[n][d] = X_b[n * D + d];\n }\n }\n __syncthreads();\n \n // Compute X^T @ X using all threads\n // Each thread computes part of the result\n for (int row = 0; row < D; row++) {\n for (int col = row; col < D; col++) {\n float sum = 0.0f;\n for (int n = tid; n < N; n += blockDim.x) {\n sum += X_tile[n][row] * X_tile[n][col];\n }\n // Block-wide reduction\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n sum += __shfl_down_sync(0xffffffff, sum, offset);\n }\n if (tid == 0) {\n Lambda_shared[row][col] = sum * noise_prec;\n Lambda_shared[col][row] = sum * noise_prec; // Symmetric\n }\n __syncthreads();\n }\n }\n \n // Add diagonal prior\n if (tid < D) {\n Lambda_shared[tid][tid] += prior_prec;\n }\n __syncthreads();\n \n // Write result to global memory\n float* Lambda_b = Lambda + b * D * D;\n for (int i = tid; i < D * D; i += blockDim.x) {\n int row = i / D;\n int col = i % D;\n Lambda_b[row * D + col] = Lambda_shared[row][col];\n }\n}\n\n// Fused kernel: compute X^T @ y\n__global__ void compute_xt_y_kernel(\n const float* __restrict__ X, // (B, N, D)\n const float* __restrict__ y, // (B, N, 1)\n float* __restrict__ Xty, // (B, D, 1)\n int B, int N, float noise_prec\n) {\n int b = blockIdx.x;\n int d = threadIdx.x;\n \n if (d >= D) return;\n \n const float* X_b = X + b * N * D;\n const float* y_b = y + b * N;\n \n float sum = 0.0f;\n for (int n = 0; n < N; n++) {\n sum += X_b[n * D + d] * y_b[n];\n }\n \n Xty[b * D + d] = sum * noise_prec;\n}\n\n// Fused kernel: compute predictive variance\n// pred_var = sum_d sum_k (X_test * Sigma_post * X_test^T) + 1/noise_precision\n__global__ void compute_pred_var_kernel(\n const float* __restrict__ X_test, // (B, M, D)\n const float* __restrict__ Sigma_post, // (B, D, D)\n float* __restrict__ pred_var, // (B, M)\n int B, int M, float inv_noise_prec\n) {\n int b = blockIdx.y;\n int m = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M) return;\n \n const float* X_test_m = X_test + b * M * D + m * D;\n const float* Sigma = Sigma_post + b * D * D;\n \n float var = 0.0f;\n for (int d1 = 0; d1 < D; d1++) {\n float sum = 0.0f;\n for (int d2 = 0; d2 < D; d2++) {\n sum += Sigma[d1 * D + d2] * X_test_m[d2];\n }\n var += X_test_m[d1] * sum;\n }\n \n pred_var[b * M + m] = var + inv_noise_prec;\n}\n\n// Fused kernel for X_test @ mu_post (predictive mean)\n__global__ void compute_pred_mean_kernel(\n const float* __restrict__ X_test, // (B, M, D)\n const float* __restrict__ mu_post, // (B, D, 1)\n float* __restrict__ pred_mean, // (B, M, 1)\n int B, int M\n) {\n int b = blockIdx.y;\n int m = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M) return;\n \n const float* X_test_m = X_test + b * M * D + m * D;\n const float* mu = mu_post + b * D;\n \n float sum = 0.0f;\n for (int d = 0; d < D; d++) {\n sum += X_test_m[d] * mu[d];\n }\n \n pred_mean[b * M + m] = sum;\n}\n\n// Host functions\ntorch::Tensor compute_lambda_post_cuda(torch::Tensor X, float prior_prec, float noise_prec) {\n int B = X.size(0);\n int N = X.size(1);\n // int D = X.size(2); // Known to be 16\n \n auto Lambda = torch::empty({B, D, D}, X.options());\n \n compute_lambda_post_kernel<<<B, BLOCK_SIZE>>>(\n X.data_ptr<float>(),\n Lambda.data_ptr<float>(),\n B, N, prior_prec, noise_prec\n );\n \n return Lambda;\n}\n\ntorch::Tensor compute_xt_y_cuda(torch::Tensor X, torch::Tensor y, float noise_prec) {\n int B = X.size(0);\n int N = X.size(1);\n \n auto Xty = torch::empty({B, D, 1}, X.options());\n \n compute_xt_y_kernel<<<B, 32>>>(\n X.data_ptr<float>(),\n y.data_ptr<float>(),\n Xty.data_ptr<float>(),\n B, N, noise_prec\n );\n \n return Xty;\n}\n\ntorch::Tensor compute_pred_mean_cuda(torch::Tensor X_test, torch::Tensor mu_post) {\n int B = X_test.size(0);\n int M = X_test.size(1);\n \n auto pred_mean = torch::empty({B, M, 1}, X_test.options());\n \n int threads = 64;\n int blocks_m = (M + threads - 1) / threads;\n dim3 grid(blocks_m, B);\n \n compute_pred_mean_kernel<<<grid, threads>>>(\n X_test.data_ptr<float>(),\n mu_post.data_ptr<float>(),\n pred_mean.data_ptr<float>(),\n B, M\n );\n \n return pred_mean;\n}\n\ntorch::Tensor compute_pred_var_cuda(torch::Tensor X_test, torch::Tensor Sigma_post, float inv_noise_prec) {\n int B = X_test.size(0);\n int M = X_test.size(1);\n \n auto pred_var = torch::empty({B, M}, X_test.options());\n \n int threads = 64;\n int blocks_m = (M + threads - 1) / threads;\n dim3 grid(blocks_m, B);\n \n compute_pred_var_kernel<<<grid, threads>>>(\n X_test.data_ptr<float>(),\n Sigma_post.data_ptr<float>(),\n pred_var.data_ptr<float>(),\n B, M, inv_noise_prec\n );\n \n return pred_var;\n}\n\n// Fused kernel: X_test @ Sigma_post, stores result for variance computation\n// Then compute variance directly\n__global__ void compute_pred_var_fused_kernel(\n const float* __restrict__ X_test, // (B, M, D)\n const float* __restrict__ Sigma_post, // (B, D, D)\n float* __restrict__ pred_var, // (B, M)\n int B, int M, float inv_noise_prec\n) {\n // Each block handles one (b, m) pair\n int b = blockIdx.y;\n int m = blockIdx.x;\n \n __shared__ float X_shared[16]; // One row of X_test\n __shared__ float S_shared[16][16]; // Sigma_post\n __shared__ float XS_shared[16]; // X_test @ Sigma_post result for row m\n \n // Collaborative load of X_test[b, m, :]\n for (int d = threadIdx.x; d < D; d += blockDim.x) {\n X_shared[d] = X_test[b * M * D + m * D + d];\n }\n \n // Collaborative load of Sigma_post[b, :, :]\n const float* Sigma = Sigma_post + b * D * D;\n for (int i = threadIdx.x; i < D * D; i += blockDim.x) {\n int row = i / D;\n int col = i % D;\n S_shared[row][col] = Sigma[i];\n }\n __syncthreads();\n \n // Compute X_test[m, :] @ Sigma_post\n // Each thread computes one element\n float local_xs[4] = {0.0f, 0.0f, 0.0f, 0.0f};\n for (int d = threadIdx.x; d < D; d += blockDim.x) {\n float sum = 0.0f;\n for (int k = 0; k < D; k++) {\n sum += X_shared[k] * S_shared[k][d];\n }\n if (threadIdx.x < D) {\n XS_shared[d] = sum;\n }\n }\n __syncthreads();\n \n // Compute sum over d of X_shared[d] * XS_shared[d]\n // This is (X @ Sigma @ X^T) for row m\n float dot = 0.0f;\n for (int d = threadIdx.x; d < D; d += blockDim.x) {\n dot += X_shared[d] * XS_shared[d];\n }\n \n // Reduce across threads\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n dot += __shfl_down_sync(0xffffffff, dot, offset);\n }\n \n if (threadIdx.x == 0) {\n pred_var[b * M + m] = dot + inv_noise_prec;\n }\n}\n\ntorch::Tensor compute_pred_var_fused_cuda(torch::Tensor X_test, torch::Tensor Sigma_post, float inv_noise_prec) {\n int B = X_test.size(0);\n int M = X_test.size(1);\n \n auto pred_var = torch::empty({B, M}, X_test.options());\n \n dim3 grid(M, B); // One block per (b, m) pair\n int threads = 32; // Enough threads for D=16\n \n compute_pred_var_fused_kernel<<<grid, threads>>>(\n X_test.data_ptr<float>(),\n Sigma_post.data_ptr<float>(),\n pred_var.data_ptr<float>(),\n B, M, inv_noise_prec\n );\n \n return pred_var;\n}\n\n// Combined kernel that does X^T @ X and X^T @ y together to reuse X in shared memory\n__global__ void compute_xtx_and_xty_kernel(\n const float* __restrict__ X, // (B, N, D)\n const float* __restrict__ y, // (B, N, 1)\n float* __restrict__ Lambda, // (B, D, D)\n float* __restrict__ Xty, // (B, D, 1)\n int B, int N,\n float prior_prec, float noise_prec\n) {\n int b = blockIdx.x;\n int tid = threadIdx.x;\n \n __shared__ float X_shared[256][16]; // X transposed in shared memory\n __shared__ float y_shared[256]; // y values\n __shared__ float Lambda_s[16][16];\n __shared__ float Xty_s[16];\n \n const float* X_b = X + b * N * D;\n const float* y_b = y + b * N;\n \n // Initialize outputs\n for (int i = tid; i < D * D; i += blockDim.x) {\n int row = i / D;\n int col = i % D;\n Lambda_s[row][col] = 0.0f;\n }\n if (tid < D) {\n Xty_s[tid] = 0.0f;\n }\n __syncthreads();\n \n // Load X and y into shared memory\n for (int n = tid; n < N; n += blockDim.x) {\n y_shared[n] = y_b[n];\n for (int d = 0; d < D; d++) {\n X_shared[n][d] = X_b[n * D + d];\n }\n }\n __syncthreads();\n \n // Compute X^T @ X and X^T @ y\n for (int row = 0; row < D; row++) {\n // Compute X^T @ y for this row\n float xty_sum = 0.0f;\n for (int n = tid; n < N; n += blockDim.x) {\n xty_sum += X_shared[n][row] * y_shared[n];\n }\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n xty_sum += __shfl_down_sync(0xffffffff, xty_sum, offset);\n }\n if (tid == 0) {\n Xty_s[row] = xty_sum * noise_prec;\n }\n \n // Compute X^T @ X for columns >= row (upper triangle)\n for (int col = row; col < D; col++) {\n float xtx_sum = 0.0f;\n for (int n = tid; n < N; n += blockDim.x) {\n xtx_sum += X_shared[n][row] * X_shared[n][col];\n }\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n xtx_sum += __shfl_down_sync(0xffffffff, xtx_sum, offset);\n }\n if (tid == 0) {\n float val = xtx_sum * noise_prec;\n Lambda_s[row][col] = val;\n Lambda_s[col][row] = val;\n }\n }\n __syncthreads();\n }\n \n // Add diagonal prior\n if (tid < D) {\n Lambda_s[tid][tid] += prior_prec;\n }\n __syncthreads();\n \n // Write Lambda to global memory\n float* Lambda_b = Lambda + b * D * D;\n for (int i = tid; i < D * D; i += blockDim.x) {\n int row = i / D;\n int col = i % D;\n Lambda_b[row * D + col] = Lambda_s[row][col];\n }\n \n // Write Xty to global memory\n float* Xty_b = Xty + b * D;\n if (tid < D) {\n Xty_b[tid] = Xty_s[tid];\n }\n}\n\nstd::tuple<torch::Tensor, torch::Tensor> compute_xtx_and_xty_cuda(\n torch::Tensor X, torch::Tensor y, float prior_prec, float noise_prec\n) {\n int B = X.size(0);\n int N = X.size(1);\n \n auto Lambda = torch::empty({B, D, D}, X.options());\n auto Xty = torch::empty({B, D, 1}, X.options());\n \n compute_xtx_and_xty_kernel<<<B, BLOCK_SIZE>>>(\n X.data_ptr<float>(),\n y.data_ptr<float>(),\n Lambda.data_ptr<float>(),\n Xty.data_ptr<float>(),\n B, N, prior_prec, noise_prec\n );\n \n return std::make_tuple(Lambda, Xty);\n}\n\"\"\"\n\ncpp_source = r\"\"\"\nstd::tuple<torch::Tensor, torch::Tensor> compute_xtx_and_xty_cuda(torch::Tensor X, torch::Tensor y, float prior_prec, float noise_prec);\ntorch::Tensor compute_pred_mean_cuda(torch::Tensor X_test, torch::Tensor mu_post);\ntorch::Tensor compute_pred_var_fused_cuda(torch::Tensor X_test, torch::Tensor Sigma_post, float inv_noise_prec);\n\"\"\"\n\n# Load the extension\nbayesian_ops = load_inline(\n name='bayesian_linear_cuda',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['compute_xtx_and_xty_cuda', 'compute_pred_mean_cuda', 'compute_pred_var_fused_cuda'],\n verbose=False,\n extra_cuda_cflags=['-O3', '--use_fast_math']\n)\n\n\nclass ModelNew(nn.Module):\n \"\"\"\n Bayesian linear regression with a conjugate Gaussian prior - optimized CUDA version.\n Uses custom kernels for the key operations:\n 1. Fused X^T@X and X^T@y computation (shares X in shared memory)\n 2. Fused predictive variance computation\n \"\"\"\n\n def __init__(self, input_dim, prior_precision=1.0, noise_precision=1.0):\n super().__init__()\n self.input_dim = input_dim\n self.prior_precision = prior_precision\n self.noise_precision = noise_precision\n\n def forward(\n self, X: torch.Tensor, y: torch.Tensor, X_test: torch.Tensor\n ) -> tuple:\n \"\"\"\n Args:\n X: (B, N, D) - training features\n y: (B, N, 1) - training targets\n X_test: (B, M, D) - test features\n Returns:\n (pred_mean, pred_var):\n pred_mean: (B, M, 1)\n pred_var: (B, M)\n \"\"\"\n D = X.shape[-1]\n \n # Ensure contiguous float32 tensors\n X = X.contiguous().float()\n y = y.contiguous().float()\n X_test = X_test.contiguous().float()\n \n # Fused kernel: compute X^T@X (Lambda_post scaled) and X^T@y (scaled)\n # Returns Lambda_post and Xty both scaled appropriately\n Lambda_post, rhs = bayesian_ops.compute_xtx_and_xty_cuda(\n X, y.squeeze(-1), self.prior_precision, self.noise_precision\n )\n rhs = rhs.unsqueeze(-1) # (B, D, 1)\n \n # Cholesky decomposition and solve (use PyTorch optimized routines)\n L = torch.linalg.cholesky(Lambda_post)\n mu_post = torch.cholesky_solve(rhs, L)\n \n # Predictive mean using custom kernel\n pred_mean = bayesian_ops.compute_pred_mean_cuda(X_test, mu_post.squeeze(-1))\n pred_mean = pred_mean.unsqueeze(-1) # (B, M, 1)\n \n # Predictive variance using fused kernel\n Sigma_post = torch.cholesky_inverse(L)\n pred_var = bayesian_ops.compute_pred_var_fused_cuda(\n X_test, Sigma_post, 1.0 / self.noise_precision\n )\n \n return pred_mean, pred_var"
}