turn 01006.5s turn · 34.3s LLM · 972.3s tools · tokens in=1,346 · out=2,960 · total=4,306 · FINAL
reasoning
Let me analyze this task carefully. I need to implement a custom CUDA kernel for the HMM forward algorithm.
The reference implementation:
1. Normalizes log_init, log_trans, log_emit using logsumexp
2. For each time step, computes log_alpha by:
- Initial: log_alpha = log_pi + emit_0
- Recursion: log_alpha = logsumexp(log_alpha.unsqueeze(-1) + log_A.unsqueeze(0), dim=1) + emit_t
3. Returns logsumexp(log_alpha, dim=-1)
Let me design a CUDA kernel for this:
1. First, I need to normalize the log parameters (log_init, log_trans, log_emit)
2. Then, iterate through time steps, computing log_alpha for each batch element
3. Finally, compute log_lik = logsumexp(log_alpha)
The key operations:
- logsumexp normalization (can be done with parallel reduction)
- The forward recursion: for each (b, j), compute logsumexp over i of (log_alpha[b,i] + log_A[i,j])
For batch_size=64, seq_len=128, num_states=16:
- The inner loop has num_states iterations per output element
- We have batch_size * num_states output elements per time step
Let me think about the parallelization strategy:
1. Normalization kernels:
- log_init: reduce over num_states
- log_trans: reduce over rows (num_states, num_states)
- log_emit: reduce over rows (num_states, num_obs)
2. Forward algorithm:
- For each time step t:
- Compute log_alpha_new[b, j] = logsumexp_i(log_alpha[b, i] + log_A[i, j])
- This can be parallelized over (b, j) pairs
- Each thread computes one logsumexp reduction over num_states elements
I'll structure the kernel to handle normalization first, then process each time step by parallelizing the log_alpha updates across batch elements and states. The logsumexp reductions within each update will run sequentially in each thread since num_states is relatively small.
For logsumexp computation, I'll use the standard technique: find the max value, subtract it from all elements, sum the exponentials, then add the log of that sum back to max. This prevents numerical overflow.
The kernel signature takes the normalized parameters and observation data, then iterates through each time step computing log_alpha values. For time step 0, I initialize with log_pi plus the emission probabilities. For subsequent steps, I compute the transition probabilities by combining the previous alpha values with the transition matrix.
When transitioning from t-1 to t, I need to compute logsumexp over all states from the previous time step for each current state j. Using shared memory to cache log_A reduces global memory accesses during this computation. After computing the transition component, I add the emission probability for the current observation.
One thing to check - the original code transposes log_B when extracting emission probabilities, so the indexing needs to match that convention. Each thread handles one (b, j) pair, computing logsumexp across all i values, then adding the corresponding emission probability. ) = log_B[j, obs]
So I need to load log_B[j, :] for each state j, which means for the observation at time t, emit_t[b, j] = log_B[j, obs[b, t]].
The main computation involves three stages: normalizing the parameters, initializing log_alpha at time 0 with log_pi and the emissions, then iterating through time steps to update log_alpha via logsumexp transitions and emissions, and finally computing logsumexp across the final log_alpha values.
For a more efficient CUDA implementation, I should normalize the parameters upfront on the host side rather than repeating it each time. Then I can launch a kernel for the forward pass itself.
The kernel needs to handle three phases: initialization at t=0 where each thread computes log_alpha[b,j] as log_pi[j] plus the emission probability for the first observation; recursion for subsequent timesteps where each thread performs logsumexp over the transition matrix; and final reduction. The emission lookup is log_B[j, observations[b, t]], so I need to index correctly based on state and observation. Then for each subsequent timestep, I'm computing the forward pass by taking the logsumexp of log_alpha[b, i] + log_A[i, j] across all states i, which gives me the new log_alpha[b, j], and adding the emission probability for that observation. Finally, I compute the log-likelihood by taking logsumexp of the final log_alpha values across all states. Each thread handles one state j, iterating through all previous states i to compute the logsumexp efficiently using max subtraction for numerical stability.
logsumexp = max_val + logf(sum_exp);
log_alpha[batch_idx * num_states + j] = logsumexp + emit;
The implementation starts by normalizing the log parameters through a helper device function that computes the log of the sum of exponentials across a given dimension. For log_A, this means normalizing each row so the transition probabilities from any state sum to 1 in log-space.
After normalizing the emission matrix similarly across observations, I extract the observations tensor and set up batch and sequence dimensions. The forward pass then computes log-likelihoods by iterating through time steps, maintaining log-space probabilities that avoid numerical underflow.
For each batch element, I initialize the forward probabilities using the initial state distribution and emission probabilities at the first timestep. Then I recursively update these probabilities through the sequence, incorporating transition and emission probabilities at each step while keeping everything in log-space for stability.
Once I've processed all timesteps, I compute the final log-likelihood for each batch element by taking the log-sum-exp of the terminal forward probabilities. The kernel architecture is set up with blocks covering all batch-state combinations, and after launching it, I extract the log-likelihoods from the device buffer back to the host.
For the normalization step, I'm considering using PyTorch's built-in logsumexp operations since they're already optimized, then only offloading the heavy forward pass computation to CUDA. This avoids redundant normalization work and lets me focus acceleration where it matters most.
The core kernel handles the forward algorithm by first initializing log_alpha for each batch element and state at time 0, then iterating through the remaining time steps to propagate probabilities through the HMM chain. Each thread computes the transition and emission contributions for its assigned state across all batches.
In the recursion phase, I compute the logsumexp over all previous states to transition into the current state, which prevents numerical underflow in log-space. After adding the emission probability for the current observation, I update the log_alpha value and advance to the next time step.
Once the forward pass completes through all timesteps, I compute the final log-likelihood by taking logsumexp across all states for each batch element. Then I set up the CUDA extension module with the necessary includes and compile it with the appropriate flags.
The ModelNew class wraps this kernel, storing the HMM parameters and normalization method. During forward pass, it normalizes the log probabilities, prepares observations, launches the kernel with the normalized parameters and observation data, and returns the computed log-likelihoods.
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 CUDA extension at module load time\nhmm_cuda = load_inline(\n name='hmm_forward_cuda',\n cpp_sources='''\nextern \"C\" void hmm_forward_cuda_impl(\n const float* log_pi,\n const float* log_A,\n const float* log_B,\n const long* observations,\n float* log_lik_out,\n int batch_size, int seq_len, int num_states);\n''',\n cuda_sources='''\n#include <cuda_fp16.h>\n#include <cfloat>\n\n__device__ float logsumexp_device(const float* arr, int n) {\n float max_val = -FLT_MAX;\n for (int i = 0; i < n; i++) {\n max_val = max(max_val, arr[i]);\n }\n if (max_val == -FLT_MAX) return -FLT_MAX;\n float sum_exp = 0.0f;\n for (int i = 0; i < n; i++) {\n sum_exp += expf(arr[i] - max_val);\n }\n return max_val + logf(sum_exp);\n}\n\nextern \"C\" __global__ void hmm_forward_kernel(\n const float* __restrict__ log_pi,\n const float* __restrict__ log_A,\n const float* __restrict__ log_B,\n const long* __restrict__ observations,\n float* __restrict__ log_alpha,\n int batch_size, int seq_len, int num_states) {\n \n // Each thread handles one (batch, state) pair\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n int total = batch_size * num_states;\n \n if (idx >= total) return;\n \n int b = idx / num_states;\n int j = idx % num_states;\n \n // Time 0: log_alpha[b, j] = log_pi[j] + log_B[j, obs[b, 0]]\n long obs0 = observations[b * seq_len + 0];\n log_alpha[idx] = log_pi[j] + log_B[j * num_states + (int)obs0];\n \n __syncthreads();\n \n // Iterate through time steps\n for (int t = 1; t < seq_len; t++) {\n // Compute logsumexp_i(log_alpha[b, i] + log_A[i, j])\n float buffer[64]; // max num_states = 64\n for (int i = 0; i < num_states; i++) {\n buffer[i] = log_alpha[b * num_states + i] + log_A[i * num_states + j];\n }\n float logsumexp = logsumexp_device(buffer, num_states);\n \n // Add emission\n long obs_t = observations[b * seq_len + t];\n log_alpha[idx] = logsumexp + log_B[j * num_states + (int)obs_t];\n \n __syncthreads();\n }\n}\n\nextern \"C\" __global__ void reduce_logsumexp_kernel(\n const float* __restrict__ log_alpha,\n float* __restrict__ log_lik_out,\n int batch_size, int num_states) {\n \n // Each thread handles one batch element\n int b = blockIdx.x * blockDim.x + threadIdx.x;\n if (b >= batch_size) return;\n \n float max_val = -FLT_MAX;\n for (int j = 0; j < num_states; j++) {\n max_val = max(max_val, log_alpha[b * num_states + j]);\n }\n \n if (max_val == -FLT_MAX) {\n log_lik_out[b] = -FLT_MAX;\n return;\n }\n \n float sum_exp = 0.0f;\n for (int j = 0; j < num_states; j++) {\n sum_exp += expf(log_alpha[b * num_states + j] - max_val);\n }\n log_lik_out[b] = max_val + logf(sum_exp);\n}\n\nvoid hmm_forward_cuda_impl(\n const float* log_pi,\n const float* log_A,\n const float* log_B,\n const long* observations,\n float* log_lik_out,\n int batch_size, int seq_len, int num_states) {\n \n // Allocate temporary buffer for log_alpha\n float* log_alpha;\n cudaMalloc(&log_alpha, batch_size * num_states * sizeof(float));\n \n // Launch forward kernel\n int total = batch_size * num_states;\n int blockSize = 256;\n int numBlocks = (total + blockSize - 1) / blockSize;\n \n hmm_forward_kernel<<<numBlocks, blockSize>>>(\n log_pi, log_A, log_B, observations, log_alpha,\n batch_size, seq_len, num_states);\n \n // Launch reduction kernel\n int reduceBlockSize = 256;\n int reduceNumBlocks = (batch_size + reduceBlockSize - 1) / reduceBlockSize;\n \n reduce_logsumexp_kernel<<<reduceNumBlocks, reduceBlockSize>>>(\n log_alpha, log_lik_out, batch_size, num_states);\n \n cudaFree(log_alpha);\n}\n''',\n functions=['hmm_forward_cuda_impl'],\n extra_cuda_cflags=['-O3', '-use_fast_math'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, num_states, num_obs):\n super().__init__()\n self.num_states = num_states\n self.num_obs = num_obs\n \n self.log_init = nn.Parameter(torch.zeros(num_states))\n self.log_trans = nn.Parameter(torch.zeros(num_states, num_states))\n self.log_emit = nn.Parameter(torch.zeros(num_states, num_obs))\n \n def _normalize_log(self, x, dim=-1):\n return x - torch.logsumexp(x, dim=dim, keepdim=True)\n \n def forward(self, observations: torch.Tensor) -> torch.Tensor:\n B, T = observations.shape\n \n # Normalize parameters (done in PyTorch - already optimized)\n log_pi = self._normalize_log(self.log_init)\n log_A = self._normalize_log(self.log_trans, dim=-1)\n log_B = self._normalize_log(self.log_emit, dim=-1)\n \n # Ensure contiguous and on same device\n log_pi = log_pi.contiguous()\n log_A = log_A.contiguous()\n log_B = log_B.contiguous()\n observations = observations.contiguous()\n \n # Allocate output\n log_lik = torch.empty(B, dtype=torch.float32, device=observations.device)\n \n # Call CUDA kernel\n hmm_cuda.hmm_forward_cuda_impl(\n log_pi.data_ptr(),\n log_A.data_ptr(),\n log_B.data_ptr(),\n observations.data_ptr(),\n log_lik.data_ptr(),\n B, T, self.num_states\n )\n \n return log_lik"
}
result ← submit_kernel (fail)
failsubmit_kernel
submit_kernel FAILED: kernel did not compile.
/scratch/abaner/KernelBench/runs/pop_l123_st_gpt/popcorn/FW-GLM-5-1/level_2_problem_50_cache/k_9a0a0347c29b/hmm_forward_cuda/hmm_forward_cuda.so: undefined symbol: hmm_forward_cuda_impl