reasoning
There's a correctness issue. Let me think about what might be wrong:
1. The Cholesky decomposition expects a symmetric positive definite matrix but Lambda_post is symmetric by construction (X^T @ X is symmetric).
2. The cholesky_solve and forward/backward substitution should be correct, but let me double-check.
3. The predictive variance computation: I'm trying to compute ||L^{-1} @ x||^2, but my implementation might have issues.
Let me rewrite this more carefully, using a cleaner approach. The key insight is:
- pred_var = x @ Sigma @ x^T + 1/noise_precision
- where Sigma = (L @ L^T)^{-1} = L^{-T} @ L^{-1}
- So pred_var = ||L^{-1} @ x||^2 + 1/noise_precision
But computing L^{-1} is tricky. Let me instead use cholesky_solve to solve L @ L^T @ z = x, then pred_var = z @ x^T... wait, that's not quite right either.
Actually, let me think again:
- Sigma = (Lambda_post)^{-1} = (L @ L^T)^{-1}
- pred_var = x @ Sigma @ x^T = x @ (L @ L^T)^{-1} @ x^T
- Let y = L^{-1} @ x^T, then y^T @ y = ||y||^2 = ||L^{-1} @ x^T||^2 = pred_var
So I need to solve L @ y = x^T (lower triangular), then pred_var = ||y||^2.
Let me also check my L^{-1} computation. I was computing L @ L_inv = I, which is forward substitution.
Actually, I think the issue is in how I'm using s_inv. Let me trace through:
- s_inv[row][col] stores L_inv at (row, col)
- L is lower triangular, so L_inv should also be lower triangular
- L @ L_inv = I means L[i,:] @ L_inv[:,j] = I[i,j]
For column j of L_inv:
- L[0,0] * L_inv[0,j] = I[0,j], so L_inv[0,j] = I[0,j] / L[0,0]
- L[1,0] * L_inv[0,j] + L[1,1] * L_inv[1,j] = I[1,j]
- etc.
If j=0: I[0,0]=1, I[1,0]=0, etc.
So L_inv[0,0] = 1/L[0,0]
L_inv[1,0] = -L[1,0]/L[0,0] * 1/L[1,1] = -L[1,0]/(L[0,0]*L[1,1])
Wait, let me redo this more carefully.
For L_inv such that L @ L_inv = I:
For each column j, we have L @ (column j of L_inv) = e_j (j-th column of I)
So for j=0:
L[0,0] * L_inv[0,0] = 1 => L_inv[0,0] = 1/L[0,0]
L[1,0] * L_inv[0,0] + L[1,1] * L_inv[1,0] = 0 => L_inv[1,0] = -L[1,0] * L_inv[0,0] / L[1,1]
For j=1:
L[0,0] * L_inv[0,1] = 0 => L_inv[0,1] = 0
L[1,0] * L_inv[0,1] + L[1,1] * L_inv[1,1] = 1 => L_inv[1,1] = 1/L[1,1]
L[2,0] * L_inv[0,1] + L[2,1] * L_inv[1,1] + L[2,2] * L_inv[2,1] = 0
...
So L_inv is also lower triangular, and only L_inv[j,j] on the diagonal depends on 1/L[j,j], while off-diagonal elements are computed from forward substitution.
Looking at my code:
```cpp
for (int col = 0; col < D; col++) {
for (int i = 0; i < D; i++) {
if (i == col) {
float sum = 1.0f; // This should be I[i,col] = 1 for i==col
for (int j = 0; j < i; j++) {
sum -= s_L[i][j] * s_inv[j][col];
}
s_inv[i][col] = sum / s_L[i][i];
} else if (i > col) {
float sum = 0.0f; // This should be I[i,col] = 0 for i>col
for (int j = 0; j < i; j++) {
sum -= s_L[i][j] * s_inv[j][col];
}
s_inv[i][col] = sum / s_L[i][i];
}
// else: i < col, s_inv[i][col] stays 0 (upper triangular part)
}
}
```
Wait, this looks wrong. The forward substitution should be:
```
s_inv[i][col] = (I[i,col] - sum_{j < i} L[i,j] * L_inv[j,col]) / L[i,i]
```
For i == col: I[i,col] = I[i,i] = 1
For i > col: I[i,col] = 0
For i < col: We're in the upper triangle, but L_inv should be lower triangular, so this stays 0.
Actually wait, I think there's a bug. For column col:
- When i == col, the RHS is I[i,col] = I[col,col] = 1
- When i > col, the RHS is I[i,col] = 0
- When i < col, L_inv[i,col] = 0 because L_inv is lower triangular
But my loop has i < D, so for i < col, we skip setting anything, which keeps s_inv[i][col] = 0 from initialization.
Actually, the issue might be that L_inv is NOT lower triangular! Let me reconsider.
If L is lower triangular, then L^{-1} is also lower triangular. This is a standard result.
So L_inv should be lower triangular. Let me trace through for D=2:
L = [[a, 0], [b, c]]
L_inv = [[x, y], [z, w]]
L @ L_inv = [[a*x, a*y], [b*x + c*z, b*y + c*w]] = I = [[1, 0], [0, 1]]
So:
a*x = 1 => x = 1/a
a*y = 0 => y = 0
b*x + c*z = 0 => z = -b*x/c = -b/(a*c)
b*y + c*w = 1 => w = 1/c
So L_inv = [[1/a, 0], [-b/(ac), 1/c]], which is indeed lower triangular.
My code should compute this correctly... let me check again.
For col=0:
- i=0, col=0: sum = 1.0, no j < 0, so s_inv[0][0] = 1/L[0][0] = 1/a ✓
- i=1, col=0: i > col, sum = 0, j=0: sum -= L[1][0] * s_inv[0][0] = -b * 1/a = -b/a
s_inv[1][0] = sum / L[1][1] = (-b/a) / c = -b/(ac) ✓
For col=1:
- i=0, col=1: i < col, skip => s_inv[0][1] = 0 ✓
- i=1, col=1: i == col, sum = 1.0, j=0: sum -= L[1][0] * s_inv[0][1] = -b * 0 = 0
s_inv[1][1] = 1 / L[1][1] = 1/c ✓
So my L_inv computation looks correct.
Now for the variance computation:
pred_var = ||L^{-1} @ x||^2
Where x is a row of X_test. Let me compute L_inv @ x^T:
L_inv[i,:] @ x^T = sum_j L_inv[i,j] * x[j] = sum_{j=0}^{i} L_inv[i,j] * x[j] (since L_inv is lower triangular)
In my code:
```cpp
for (int row = 0; row < D; row++) {
float sum = 0.0f;
for (int col = row; col < D; col++) { // L_inv is lower triangular
sum += s_inv[col][row] * s_LinvX[col];
}
result[row] = sum;
}
```
Wait, this is wrong! I'm accessing s_inv[col][row], but the loop should be:
- For row i of L_inv, we need sum over col j from 0 to i: L_inv[i,j] * x[j]
- But I'm looping col from row to D, which gives elements L_inv[col,row] = L_inv^T[row,col]
Actually, s_inv[col][row] = L_inv[col][row], not L_inv^T[row,col].
Let me think again. s_inv[row][col] = L_inv[row][col].
So for L_inv @ x:
result[i] = sum_j L_inv[i,j] * x[j]
Since L_inv is lower triangular, L_inv[i,j] = 0 for j > i.
So result[i] = sum_{j=0}^{i} L_inv[i,j] * x[j] = sum_{j=0}^{i} s_inv[i][j] * x[j]
My code does:
```cpp
for (int col = row; col < D; col++) { // L_inv is lower triangular
sum += s_inv[col][row] * s_LinvX[col];
}
```
This is summing s_inv[col][row] * x[col] for col from row to D.
But s_inv[col][row] is L_inv[col][row], which is accessing the upper triangular part (col >= row means we're above the diagonal).
This is completely wrong! I should be accessing:
```cpp
for (int col = 0; col <= row; col++) { // Lower triangular
sum += s_inv[row][col] * s_LinvX[col];
}
```
Let me fix this.
Actually wait, there's another issue. I need to be careful about the matrix layout. Let me think about what I'm storing:
s_inv[row][col] stores the element at row 'row' and column 'col' of L_inv.
L_inv is lower triangular, so L_inv[row][col] = 0 for col > row.
So L_inv @ x gives:
result[i] = sum_{j=0}^{D-1} L_inv[i][j] * x[j] = sum_{j=0}^{i} L_inv[i][j] * x[j]
In terms of s_inv:
result[i] = sum_{j=0}^{i} s_inv[i][j] * x[j]
My buggy code accesses s_inv[col][row] which is L_inv[col][row]. For col >= row, this is the upper triangle or diagonal, not lower triangle.
The fix is:
```cpp
for (int j = 0; j <= row; j++) {
sum += s_inv[row][j] * s_LinvX[j];
}
```
Let me also check other parts of the code for similar issues.
Also, I notice that I'm computing cholesky_solve using forward and backward substitution, which should be correct. Let me verify:
Given L @ L^T @ mu = rhs:
1. Solve L @ z = rhs (forward substitution)
2. Solve L^T @ mu = z (backward substitution)
For step 1:
z[i] = (rhs[i] - sum_{j<i} L[i,j] * z[j]) / L[i,i]
For step 2:
mu[i] = (z[i] - sum_{j>i} L[j,i] * mu[j]) / L[i,i] // L^T[i,j] = L[j,i]
Actually, I need to double-check the backward substitution. For L^T @ mu = z where L^T is upper triangular:
L^T[i,:] @ mu = z[i]
sum_j L^T[i,j] * mu[j] = z[i]
Since L^T is upper triangular, L^T[i,j] = 0 for j < i.
So sum_{j=i}^{D-1} L^T[i,j] * mu[j] = z[i]
L^T[i,i] * mu[i] + sum_{j>i} L^T[i,j] * mu[j] = z[i]
L^T[i,i] = L[i,i]
L^T[i,j] = L[j,i] for j > i
So:
mu[i] = (z[i] - sum_{j>i} L[j,i] * mu[j]) / L[i,i]
This should be done from i = D-1 down to 0.
In my code:
```cpp
for (int i = D - 1; i >= 0; i--) {
float sum = s_z[i];
for (int j = i + 1; j < D; j++) {
sum -= s_L[j][i] * mb[j]; // L[j][i] = L^T[i][j]
}
mb[i] = sum / s_L[i][i];
}
```
This looks correct.
Let me fix the variance computation and also double-check other parts.
Actually, I think there might also be issues with my choice of using atomics for accumulation. The atomics can cause race conditions and aren't necessary since each thread processes different elements.
Let me rewrite the code more carefully.
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\nconstexpr int D = 16;\nconstexpr int THREADS_PER_BLOCK = 256;\n\n// Fused kernel for computing XtX and adding prior\n// Each block handles one batch\n__global__ void compute_lambda_post_kernel(\n const float* __restrict__ X,\n float* __restrict__ Lambda_post,\n int N, int B, float prior_prec, float noise_prec\n) {\n int b = blockIdx.x;\n int tx = threadIdx.x;\n \n // X is (B, N, D), Lambda_post is (B, D, D)\n const float* Xb = X + b * N * D;\n float* Lb = Lambda_post + b * D * D;\n \n // Shared memory for partial sums\n __shared__ float s_XtX[D][D + 1]; // +1 to avoid bank conflicts\n \n // Initialize XtX to zero\n if (tx < D * D) {\n s_XtX[tx / D][tx % D] = 0.0f;\n }\n __syncthreads();\n \n // Each thread accumulates part of XtX\n // Process rows of X in chunks\n for (int iter = 0; iter < (N + 255) / 256; iter++) {\n int base_row = iter * 256;\n int valid_rows = min(256, N - base_row);\n \n // Each thread computes one or more entries of XtX\n for (int idx = tx; idx < D * D; idx += blockDim.x) {\n int i = idx / D;\n int j = idx % D;\n float sum = 0.0f;\n for (int k = 0; k < valid_rows; k++) {\n sum += Xb[(base_row + k) * D + i] * Xb[(base_row + k) * D + j];\n }\n atomicAdd(&s_XtX[i][j], sum);\n }\n __syncthreads();\n }\n \n // Store result with prior\n for (int idx = tx; idx < D * D; idx += blockDim.x) {\n int i = idx / D;\n int j = idx % D;\n float val = noise_prec * s_XtX[i][j];\n if (i == j) {\n val += prior_prec;\n }\n Lb[i * D + j] = val;\n }\n}\n\n// Cholesky decomposition for small matrices (D=16)\n__global__ void cholesky_kernel(\n const float* __restrict__ A,\n float* __restrict__ L_out,\n int B\n) {\n int b = blockIdx.x;\n const float* Ab = A + b * D * D;\n float* Lb = L_out + b * D * D;\n \n __shared__ float s_A[D][D + 1];\n __shared__ float s_L[D][D + 1];\n \n // Load A into shared memory\n if (threadIdx.x < D * D) {\n int row = threadIdx.x / D;\n int col = threadIdx.x % D;\n s_A[row][col] = Ab[row * D + col];\n s_L[row][col] = 0.0f;\n }\n __syncthreads();\n \n // Cholesky decomposition (single thread for correctness)\n if (threadIdx.x == 0) {\n for (int i = 0; i < D; i++) {\n // L[i][i] = sqrt(A[i][i] - sum(L[i][k]^2))\n float diag = s_A[i][i];\n for (int k = 0; k < i; k++) {\n diag -= s_L[i][k] * s_L[i][k];\n }\n s_L[i][i] = sqrtf(fmaxf(diag, 1e-10f));\n \n // L[j][i] = (A[j][i] - sum(L[j][k]*L[i][k])) / L[i][i]\n for (int j = i + 1; j < D; j++) {\n float val = s_A[j][i];\n for (int k = 0; k < i; k++) {\n val -= s_L[j][k] * s_L[i][k];\n }\n s_L[j][i] = val / s_L[i][i];\n }\n }\n }\n __syncthreads();\n \n // Store L (copy entire matrix including upper triangle zeros)\n if (threadIdx.x < D * D) {\n int row = threadIdx.x / D;\n int col = threadIdx.x % D;\n Lb[row * D + col] = s_L[row][col];\n }\n}\n\n// Fused kernel: compute XtY and solve with Cholesky\n__global__ void solve_mu_post_kernel(\n const float* __restrict__ X,\n const float* __restrict__ y,\n const float* __restrict__ L,\n float* __restrict__ mu_post,\n int N, int B, float noise_prec\n) {\n int b = blockIdx.x;\n int tx = threadIdx.x;\n \n const float* Xb = X + b * N * D;\n const float* yb = y + b * N;\n const float* Lb = L + b * D * D;\n float* mb = mu_post + b * D;\n \n __shared__ float s_XtY[D];\n __shared__ float s_L[D][D + 1];\n __shared__ float s_rhs[D];\n __shared__ float s_z[D];\n __shared__ float s_mu[D];\n \n // Initialize\n if (tx < D) s_XtY[tx] = 0.0f;\n __syncthreads();\n \n // Compute XtY = X^T * y by accumulation\n for (int iter = 0; iter < (N + 255) / 256; iter++) {\n int base_row = iter * 256;\n int valid_rows = min(256, N - base_row);\n \n if (tx < D) {\n float sum = 0.0f;\n for (int k = 0; k < valid_rows; k++) {\n sum += Xb[(base_row + k) * D + tx] * yb[base_row + k];\n }\n atomicAdd(&s_XtY[tx], sum);\n }\n __syncthreads();\n }\n \n // Load L and compute rhs\n if (tx < D * D) {\n int row = tx / D;\n int col = tx % D;\n s_L[row][col] = Lb[row * D + col];\n }\n if (tx < D) {\n s_rhs[tx] = noise_prec * s_XtY[tx];\n }\n __syncthreads();\n \n // Solve L * L^T * mu = rhs\n // Step 1: L * z = rhs (forward substitution)\n if (tx == 0) {\n for (int i = 0; i < D; i++) {\n float sum = s_rhs[i];\n for (int j = 0; j < i; j++) {\n sum -= s_L[i][j] * s_z[j];\n }\n s_z[i] = sum / s_L[i][i];\n }\n \n // Step 2: L^T * mu = z (backward substitution)\n for (int i = D - 1; i >= 0; i--) {\n float sum = s_z[i];\n for (int j = i + 1; j < D; j++) {\n sum -= s_L[j][i] * s_mu[j];\n }\n s_mu[i] = sum / s_L[i][i];\n }\n }\n __syncthreads();\n \n // Store mu_post\n if (tx < D) {\n mb[tx] = s_mu[tx];\n }\n}\n\n// Predictive mean: pred_mean = X_test @ mu_post\n__global__ void predict_mean_kernel(\n const float* __restrict__ X_test,\n const float* __restrict__ mu_post,\n float* __restrict__ pred_mean,\n int M, int B\n) {\n int b = blockIdx.x;\n int tx = threadIdx.x;\n \n const float* Xtb = X_test + b * M * D;\n const float* mb = mu_post + b * D;\n float* pb = pred_mean + b * M;\n \n __shared__ float s_mu[D];\n \n // Load mu_post into shared memory\n if (tx < D) {\n s_mu[tx] = mb[tx];\n }\n __syncthreads();\n \n // Each thread computes one or more predictions\n for (int i = tx; i < M; i += blockDim.x) {\n float sum = 0.0f;\n for (int j = 0; j < D; j++) {\n sum += Xtb[i * D + j] * s_mu[j];\n }\n pb[i] = sum;\n }\n}\n\n// Predictive variance using L^{-1}\n__global__ void predict_var_kernel(\n const float* __restrict__ X_test,\n const float* __restrict__ L,\n float* __restrict__ pred_var,\n int M, int B, float noise_prec\n) {\n int b = blockIdx.x;\n int tx = threadIdx.x;\n \n const float* Xtb = X_test + b * M * D;\n const float* Lb = L + b * D * D;\n float* vb = pred_var + b * M;\n \n __shared__ float s_L[D][D + 1];\n __shared__ float s_Linv[D][D + 1];\n \n // Load L\n if (tx < D * D) {\n int row = tx / D;\n int col = tx % D;\n s_L[row][col] = Lb[row * D + col];\n s_Linv[row][col] = 0.0f;\n }\n __syncthreads();\n \n // Compute L^{-1} by forward substitution: L * L_inv = I\n // For each column col of L_inv, solve L * (L_inv[:,col]) = e_col\n if (tx == 0) {\n for (int col = 0; col < D; col++) {\n for (int i = 0; i < D; i++) {\n // We need L_inv[i,col], but L_inv is lower triangular so only compute for i >= col\n if (i >= col) {\n float rhs = (i == col) ? 1.0f : 0.0f;\n float sum = rhs;\n for (int j = 0; j < i; j++) {\n sum -= s_L[i][j] * s_Linv[j][col];\n }\n s_Linv[i][col] = sum / s_L[i][i];\n }\n // For i < col, s_Linv[i][col] stays 0 (upper triangular part)\n }\n }\n }\n __syncthreads();\n \n // pred_var[i] = x_i @ Sigma @ x_i^T = ||L^{-1} @ x_i^T||^2\n // where x_i is the i-th row of X_test\n for (int i = tx; i < M; i += blockDim.x) {\n float result[D];\n \n // Compute L_inv @ x^T\n // result[row] = sum_{col=0}^{row} s_Linv[row][col] * x[col]\n for (int row = 0; row < D; row++) {\n float sum = 0.0f;\n for (int col = 0; col <= row; col++) { // L_inv is lower triangular\n sum += s_Linv[row][col] * Xtb[i * D + col];\n }\n result[row] = sum;\n }\n \n // Compute squared norm\n float var = 0.0f;\n for (int j = 0; j < D; j++) {\n var += result[j] * result[j];\n }\n vb[i] = var + 1.0f / noise_prec;\n }\n}\n\n// Host function\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 = X.size(2);\n int M = X_test.size(1);\n \n auto options = X.options();\n \n // Allocate intermediate tensors\n auto Lambda_post = torch::empty({B, D, D}, options);\n auto L = torch::empty({B, D, D}, options);\n auto mu_post = torch::empty({B, D}, options);\n auto pred_mean = torch::empty({B, M}, options);\n auto pred_var = torch::empty({B, M}, options);\n \n // Launch kernels\n int blocks = B;\n \n // Compute Lambda_post = prior_precision * I + noise_precision * X^T @ X\n compute_lambda_post_kernel<<<blocks, THREADS_PER_BLOCK>>>(\n X.data_ptr<float>(),\n Lambda_post.data_ptr<float>(),\n N, B, prior_precision, noise_precision\n );\n \n // Cholesky decomposition\n cholesky_kernel<<<blocks, THREADS_PER_BLOCK>>>(\n Lambda_post.data_ptr<float>(),\n L.data_ptr<float>(),\n B\n );\n \n // Solve for mu_post\n solve_mu_post_kernel<<<blocks, THREADS_PER_BLOCK>>>(\n X.data_ptr<float>(),\n y.data_ptr<float>(),\n L.data_ptr<float>(),\n mu_post.data_ptr<float>(),\n N, B, noise_precision\n );\n \n // Predictive mean\n predict_mean_kernel<<<blocks, THREADS_PER_BLOCK>>>(\n X_test.data_ptr<float>(),\n mu_post.data_ptr<float>(),\n pred_mean.data_ptr<float>(),\n M, B\n );\n \n // Predictive variance\n predict_var_kernel<<<blocks, THREADS_PER_BLOCK>>>(\n X_test.data_ptr<float>(),\n L.data_ptr<float>(),\n pred_var.data_ptr<float>(),\n M, B, noise_precision\n );\n \n // Reshape outputs to match expected shapes\n return std::make_tuple(\n pred_mean.unsqueeze(-1), // (B, M, 1)\n pred_var // (B, M)\n );\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_cuda_v2',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['bayesian_linear_forward'],\n extra_cuda_cflags=['-O3', '--use_fast_math'],\n verbose=True\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 \n def forward(self, X: torch.Tensor, y: torch.Tensor, X_test: torch.Tensor):\n pred_mean, pred_var = bayesian_extension.bayesian_linear_forward(\n X.contiguous(),\n y.contiguous(),\n X_test.contiguous(),\n self.prior_precision,\n self.noise_precision\n )\n return pred_mean, pred_var"
}