reasoning
The problem has:
- D = 16 (input_dim)
- N = 256 (num_train)
- M = 64 (num_test)
- B = 16 (batch_size)
The main computation involves:
1. Computing X^T @ X: (D, D) = (16, 16) - small matrix
2. Cholesky decomposition of (D, D) matrix
3. Computing X^T @ y: (D, 1)
4. Cholesky solve for posterior mean
5. Cholesky inverse for posterior covariance
6. Predictive mean: X_test @ mu_post
7. Predictive variance computation
Since D=16 is very small, most of the matrix operations are on tiny matrices. The main parallelism comes from the batch dimension B=16.
Let me think about the approach:
- For small matrices like 16x16, cuBLAS is very efficient
- cuSOLVER has batched Cholesky, but for 16x16 matrices, a custom implementation might be faster
- The batch dimension (16) means we have limited parallelism if we process one batch element per thread block
Actually, for such small matrices, the best approach might be:
1. Use cuBLAS batched GEMM for X^T @ X and X^T @ y
2. Implement a custom batched Cholesky decomposition optimized for small matrices
3. Use custom kernels for the solve and inverse operations
Let me start with a basic implementation using CUDA kernels that handle each batch element.
Actually, let me think more carefully. The dimensions are:
- X: (B, N, D) = (16, 256, 16)
- y: (B, N, 1) = (16, 256, 1)
- X_test: (B, M, D) = (16, 64, 16)
The computation:
1. X^T @ X: (B, D, D) = (16, 16, 16) - each batch element produces a 16x16 matrix
2. Cholesky of 16x16 matrix
3. X^T @ y: (B, D, 1) = (16, 16, 1)
4. Cholesky solve
5. X_test @ mu_post: (B, M, 1) = (16, 64, 1)
6. Cholesky inverse
7. Predictive variance: (B, M) = (16, 64)
For small matrices, the most efficient approach is often to:
1. Use shared memory and have multiple threads work on each matrix
2. Or use a single thread per small matrix (since 16x16 is tiny)
Given B=16 and D=16, one approach is:
- Launch B thread blocks
- Each block has D*D = 256 threads
- Each block handles one batch element
Or we could do:
- Launch B * some_factor thread blocks
- Each block has a smaller number of threads
Let me start with a straightforward implementation using cuBLAS for the matrix multiplications and custom kernels for the Cholesky-related operations.
Actually, for simplicity and to ensure correctness first, let me start with custom CUDA kernels that implement all operations, using shared memory for the small matrices.
call → compile_kernel
callcompile_kernel
{
"kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <cmath>\n\n// Constants for the problem\nconstexpr int D = 16;\n\n// Naive Cholesky decomposition for small matrices\n__device__ void cholesky_decomp(const float* A, float* L, int n) {\n for (int i = 0; i < n; i++) {\n for (int j = 0; j <= i; j++) {\n float sum = 0.0f;\n for (int k = 0; k < j; k++) {\n sum += L[i * n + k] * L[j * n + k];\n }\n if (i == j) {\n float val = A[i * n + i] - sum;\n L[i * n + j] = sqrtf(fmaxf(val, 1e-10f));\n } else {\n L[i * n + j] = (A[i * n + j] - sum) / L[j * n + j];\n }\n }\n }\n}\n\n// Forward substitution: solve L @ x = b (L is lower triangular)\n__device__ void forward_solve(const float* L, const float* b, float* x, int n) {\n for (int i = 0; i < n; i++) {\n float sum = 0.0f;\n for (int j = 0; j < i; j++) {\n sum += L[i * n + j] * x[j];\n }\n x[i] = (b[i] - sum) / L[i * n + i];\n }\n}\n\n// Backward substitution: solve L^T @ x = b (L is lower triangular)\n__device__ void backward_solve(const float* L, const float* b, float* x, int n) {\n for (int i = n - 1; i >= 0; i--) {\n float sum = 0.0f;\n for (int j = i + 1; j < n; j++) {\n sum += L[j * n + i] * x[j];\n }\n x[i] = (b[i] - sum) / L[i * n + i];\n }\n}\n\n// Cholesky solve: solve A @ x = b where A = L @ L^T\n__device__ void cholesky_solve(const float* L, const float* b, float* x, int n) {\n float temp[D];\n forward_solve(L, b, temp, n);\n backward_solve(L, temp, x, n);\n}\n\n// Cholesky inverse: compute A^{-1} where A = L @ L^T\n__device__ void cholesky_inverse(const float* L, float* A_inv, int n) {\n // Solve for each column of the identity\n for (int col = 0; col < n; col++) {\n float e[D] = {0};\n e[col] = 1.0f;\n \n float temp[D], result[D];\n forward_solve(L, e, temp, n);\n backward_solve(L, temp, result, n);\n \n for (int row = 0; row < n; row++) {\n A_inv[row * n + col] = result[row];\n }\n }\n}\n\n// Main kernel: one block per batch element\n__global__ void bayesian_linear_kernel(\n const float* __restrict__ X, // (B, N, D)\n const float* __restrict__ y, // (B, N, 1)\n const float* __restrict__ X_test, // (B, M, D)\n float* __restrict__ pred_mean, // (B, M, 1)\n float* __restrict__ pred_var, // (B, M)\n int B, int N, int M, int D_dim,\n float prior_precision, float noise_precision\n) {\n int batch_idx = blockIdx.x;\n if (batch_idx >= B) return;\n \n int tid = threadIdx.x;\n \n // Shared memory for matrices\n __shared__ float XtX[D * D]; // D x D\n __shared__ float L_mat[D * D]; // D x D (Cholesky factor)\n __shared__ float Xty[D]; // D x 1\n __shared__ float mu_post[D]; // D x 1\n __shared__ float Sigma_post[D * D]; // D x D\n __shared__ float temp_vec[D]; // temporary\n \n // Initialize XtX with prior: alpha * I\n if (tid < D * D) {\n int row = tid / D;\n int col = tid % D;\n XtX[tid] = (row == col) ? prior_precision : 0.0f;\n }\n \n // Compute X^T @ X and add beta * X^T @ X\n // Each thread computes partial sum for one element of XtX\n // XtX[i,j] += beta * sum over n of X[n,i] * X[n,j]\n \n // First, add noise_precision * X^T @ X\n for (int n = 0; n < N; n++) {\n float x_i = X[batch_idx * N * D + n * D + tid % D];\n for (int j_offset = 0; j_offset < D; j_offset += 16) {\n int j = (tid / D) + j_offset;\n if (j < D) {\n float x_j = X[batch_idx * N * D + n * D + j];\n atomicAdd(&XtX[tid % D * D + j], noise_precision * x_i * x_j);\n }\n }\n }\n \n __syncthreads();\n \n // Thread 0 performs Cholesky decomposition\n if (tid == 0) {\n cholesky_decomp(XtX, L_mat, D);\n }\n __syncthreads();\n \n // Compute X^T @ y: each thread computes one element\n if (tid < D) {\n float sum = 0.0f;\n for (int n = 0; n < N; n++) {\n sum += X[batch_idx * N * D + n * D + tid] * y[batch_idx * N + n];\n }\n Xty[tid] = noise_precision * sum;\n }\n __syncthreads();\n \n // Thread 0 solves for posterior mean\n if (tid == 0) {\n cholesky_solve(L_mat, Xty, mu_post, D);\n }\n __syncthreads();\n \n // Compute pred_mean: X_test @ mu_post\n // Each thread computes one output element\n for (int m = tid; m < M; m += blockDim.x) {\n float sum = 0.0f;\n for (int d = 0; d < D; d++) {\n sum += X_test[batch_idx * M * D + m * D + d] * mu_post[d];\n }\n pred_mean[batch_idx * M + m] = sum;\n }\n \n // Thread 0 computes Cholesky inverse\n if (tid == 0) {\n cholesky_inverse(L_mat, Sigma_post, D);\n }\n __syncthreads();\n \n // Compute pred_var: sum_d (X_test[m,d] * Sigma_post[d,:] * X_test[m,:]) + 1/noise_precision\n for (int m = tid; m < M; m += blockDim.x) {\n float var = 0.0f;\n for (int i = 0; i < D; i++) {\n float sum = 0.0f;\n for (int j = 0; j < D; j++) {\n sum += Sigma_post[i * D + j] * X_test[batch_idx * M * D + m * D + j];\n }\n var += X_test[batch_idx * M * D + m * D + i] * sum;\n }\n pred_var[batch_idx * M + m] = var + 1.0f / noise_precision;\n }\n}\n\nstd::tuple<torch::Tensor, torch::Tensor> bayesian_linear_forward(\n torch::Tensor X,\n torch::Tensor y,\n torch::Tensor X_test,\n float prior_precision,\n float noise_precision\n) {\n int B = X.size(0);\n int N = X.size(1);\n int D_dim = X.size(2);\n int M = X_test.size(1);\n \n auto pred_mean = torch::empty({B, M, 1}, X.options());\n auto pred_var = torch::empty({B, M}, X.options());\n \n int threads = 256; // Need at least D threads for Xty computation\n int blocks = B;\n \n bayesian_linear_kernel<<<blocks, threads>>>(\n X.data_ptr<float>(),\n y.data_ptr<float>(),\n X_test.data_ptr<float>(),\n pred_mean.data_ptr<float>(),\n pred_var.data_ptr<float>(),\n B, N, M, D_dim,\n prior_precision, noise_precision\n );\n \n return std::make_tuple(pred_mean, pred_var);\n}\n\"\"\"\n\ncpp_source = \"\"\"\nstd::tuple<torch::Tensor, torch::Tensor> bayesian_linear_forward(\n torch::Tensor X,\n torch::Tensor y,\n torch::Tensor X_test,\n float prior_precision,\n float noise_precision\n);\n\"\"\"\n\nbayesian_extension = load_inline(\n name='bayesian_linear',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['bayesian_linear_forward'],\n extra_cuda_cflags=['-O3'],\n verbose=True\n)\n\n\nclass ModelNew(nn.Module):\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 self.extension = bayesian_extension\n\n def forward(self, X: torch.Tensor, y: torch.Tensor, X_test: torch.Tensor) -> tuple:\n pred_mean, pred_var = self.extension.bayesian_linear_forward(\n X, y, X_test, self.prior_precision, self.noise_precision\n )\n return pred_mean, pred_var"
}