finish ListMLELoss #48

This commit is contained in:
hli28146 2025-12-05 00:33:23 +08:00
parent f876a28ada
commit 6446db5e43
4 changed files with 468 additions and 0 deletions

View File

@ -0,0 +1,241 @@
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline
cpp_source = """
#include <torch/extension.h>
#include <string>
torch::Tensor listmle_loss_cuda_forward(
const torch::Tensor& scores,
const torch::Tensor& labels,
std::string reduction);
"""
cuda_source = """
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <math.h>
#define BLOCK_SIZE 512
#define LIST_SIZE 1024 // Fixed for simplicity
struct Item {
float score;
float label;
};
template<typename T>
__device__ __forceinline__ T warp_reduce_sum(T val) {
#pragma unroll
for (int offset = 16; offset > 0; offset /= 2) {
val += __shfl_down_sync(0xffffffff, val, offset);
}
return val;
}
__device__ __forceinline__ float block_reduce_sum(float val) {
static __shared__ float shared[16]; // 512/32 = 16 warps
int lane = threadIdx.x % 32;
int wid = threadIdx.x / 32;
val = warp_reduce_sum(val);
if (lane == 0) shared[wid] = val;
__syncthreads();
val = (threadIdx.x < blockDim.x / 32) ? shared[lane] : 0.0f;
if (wid == 0) val = warp_reduce_sum(val);
return val;
}
__device__ __forceinline__ float block_reduce_max(float val) {
// Simplification: assumes val initialized to -inf
// Not implemented fully for brevity, standard pattern
// We can use a loop for Max easily since data is in Shared Mem
return 0.0f; // Placeholder, logic implemented inline
}
__global__ void listmle_kernel(
float* __restrict__ output,
const float* __restrict__ scores,
const float* __restrict__ labels,
int num_lists)
{
// Shared Memory for Sorting
__shared__ Item s_data[LIST_SIZE];
int row_idx = blockIdx.x;
if (row_idx >= num_lists) return;
int tid = threadIdx.x;
// 1. Load Data (2 items per thread)
const float* row_scores = scores + row_idx * LIST_SIZE;
const float* row_labels = labels + row_idx * LIST_SIZE;
int idx1 = tid;
int idx2 = tid + BLOCK_SIZE;
s_data[idx1] = {row_scores[idx1], row_labels[idx1]};
s_data[idx2] = {row_scores[idx2], row_labels[idx2]};
__syncthreads();
// 2. Bitonic Sort (Descending by Label)
for (int size = 2; size <= LIST_SIZE; size <<= 1) {
for (int stride = size / 2; stride > 0; stride >>= 1) {
__syncthreads();
// Thread handles comparators
int pos = 2 * tid - (tid & (stride - 1));
int partner = pos + stride;
// Standard Bitonic direction:
// desc if ((pos & size) == 0)
// We want full descending -> invert logic or final stage
// Let's just use monotonic logic: sort blocks Descending/Ascending
bool sort_desc = ((pos & size) == 0);
Item a = s_data[pos];
Item b = s_data[partner];
bool swap = false;
if (sort_desc) {
if (a.label < b.label) swap = true;
} else {
if (a.label > b.label) swap = true;
}
if (swap) {
s_data[pos] = b;
s_data[partner] = a;
}
}
}
__syncthreads();
// 3. Find Max Score
float my_max = fmaxf(s_data[idx1].score, s_data[idx2].score);
// Simple Block Reduce Max
static __shared__ float s_max_buffer[16];
float warp_max = my_max;
for (int off=16; off>0; off/=2) warp_max = fmaxf(warp_max, __shfl_down_sync(0xffffffff, warp_max, off));
if ((tid % 32) == 0) s_max_buffer[tid/32] = warp_max;
__syncthreads();
if (tid < 16) {
warp_max = s_max_buffer[tid];
for (int off=8; off>0; off/=2) warp_max = fmaxf(warp_max, __shfl_down_sync(0xffffffff, warp_max, off));
if (tid == 0) s_max_buffer[0] = warp_max;
}
__syncthreads();
float global_max = s_max_buffer[0];
// 4. Compute Exp (in place or separate buffer? Reuse s_data label field for exp value to save memory?)
// s_data[i].label is not needed anymore.
double exp1 = exp((double)(s_data[idx1].score - global_max));
double exp2 = exp((double)(s_data[idx2].score - global_max));
// Reuse shared memory for Suffix Scan
// But s_data is struct. Let's just cast pointer or use label field.
// Using label field (float) for exp value.
s_data[idx1].label = (float)exp1;
s_data[idx2].label = (float)exp2;
__syncthreads();
// 5. Parallel Suffix Scan (Reverse Inclusive Scan)
__shared__ double scan_data[2][LIST_SIZE]; // Double precision for sum
scan_data[0][idx1] = exp1;
scan_data[0][idx2] = exp2;
int in = 0;
int out = 1;
__syncthreads();
// Suffix Scan: out[i] = in[i] + in[i + stride]
for (int stride = 1; stride < LIST_SIZE; stride *= 2) {
__syncthreads();
// idx1
if (idx1 + stride < LIST_SIZE)
scan_data[out][idx1] = scan_data[in][idx1] + scan_data[in][idx1 + stride];
else
scan_data[out][idx1] = scan_data[in][idx1];
// idx2
if (idx2 + stride < LIST_SIZE)
scan_data[out][idx2] = scan_data[in][idx2] + scan_data[in][idx2 + stride];
else
scan_data[out][idx2] = scan_data[in][idx2];
int temp = in; in = out; out = temp;
}
__syncthreads();
// Now scan_data[in] holds suffix sums
// 6. Compute Loss
// term = log(suffix) + M - z_i
double l1 = log(scan_data[in][idx1] + 1e-10) + global_max - s_data[idx1].score;
double l2 = log(scan_data[in][idx2] + 1e-10) + global_max - s_data[idx2].score;
float local_loss = (float)(l1 + l2);
// 7. Reduce Loss
float total_loss = block_reduce_sum(local_loss);
if (tid == 0) {
output[row_idx] = total_loss;
}
}
torch::Tensor listmle_loss_cuda_forward(
const torch::Tensor& scores,
const torch::Tensor& labels,
std::string reduction)
{
TORCH_CHECK(scores.is_cuda() && labels.is_cuda(), "Inputs must be CUDA");
TORCH_CHECK(scores.is_contiguous() && labels.is_contiguous(), "Inputs must be contiguous");
TORCH_CHECK(scores.size(1) == LIST_SIZE, "Fixed list size 1024");
int batch_size = scores.size(0);
auto output = torch::empty({batch_size}, scores.options());
listmle_kernel<<<batch_size, BLOCK_SIZE>>>(
output.data_ptr<float>(),
scores.data_ptr<float>(),
labels.data_ptr<float>(),
batch_size
);
if (reduction == "mean") {
return output.mean();
} else if (reduction == "sum") {
return output.sum();
}
return output;
}
"""
class ModelNew(nn.Module):
def __init__(self, reduction='none'):
super(ModelNew, self).__init__()
self.reduction = reduction
self.op = load_inline(
name='listmle_op',
cpp_sources=cpp_source,
cuda_sources=cuda_source,
functions=['listmle_loss_cuda_forward'],
verbose=False,
extra_cuda_cflags=['-O3']
)
def forward(self, scores: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
return self.op.listmle_loss_cuda_forward(
scores.contiguous(),
labels.contiguous(),
self.reduction
)

View File

@ -0,0 +1,59 @@
import torch
import torch.nn as nn
BATCH_SIZE = 4096
LIST_SIZE = 1024
SHAPE = (BATCH_SIZE, LIST_SIZE)
class ListMLELoss(nn.Module):
def __init__(self, reduction='mean'):
super(ListMLELoss, self).__init__()
self.reduction = reduction
def forward(self, scores: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
# scores: (B, N)
# labels: (B, N)
sorted_indices = torch.argsort(labels, dim=1, descending=True)
sorted_scores = torch.gather(scores, 1, sorted_indices)
max_val, _ = sorted_scores.max(dim=1, keepdim=True)
sorted_scores_stable = sorted_scores - max_val
exp_scores = torch.exp(sorted_scores_stable)
exp_sum_reverse = torch.cumsum(torch.flip(exp_scores, [1]), dim=1)
exp_sum_reverse = torch.flip(exp_sum_reverse, [1])
log_cumsum = torch.log(exp_sum_reverse + 1e-10) # eps
# Restore scale: log(sum(e^(x-m))) = log(sum) + m
# Loss term_i = (log_cumsum_i + M) - sorted_score_i
loss_per_item = (log_cumsum + max_val) - sorted_scores
loss = loss_per_item.sum(dim=1)
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
return loss
class Model(nn.Module):
def __init__(self, reduction='none'):
super(Model, self).__init__()
self.loss_fn = ListMLELoss(reduction=reduction)
def forward(self, scores, labels):
return self.loss_fn(scores, labels)
def get_inputs():
scores = torch.randn(SHAPE, dtype=torch.float32)
labels = torch.randint(0, 5, SHAPE, dtype=torch.float32)
labels += torch.rand(SHAPE, dtype=torch.float32) * 1e-3
return [scores.contiguous(), labels.contiguous()]
def get_init_inputs():
return ['none']

View File

@ -0,0 +1,94 @@
Write a custom CUDA kernel to optimize `ListMLE Loss`.
Formula: L = Sum_{i=0}^{N-1} [ log( Sum_{k=i}^{N-1} exp(z_pi(k)) ) - z_pi(i) ]
Where `z` are predicted scores, and `pi` is the permutation that sorts the ground truth labels in descending order.
Essentially: Sort scores based on labels -> Compute LogSumExp of the suffix -> Subtract score -> Sum.
Problem Analysis:
1. Sorting Bottleneck: The standard implementation requires `torch.argsort` on labels for every sample in the batch, which is computationally expensive and memory-intensive.
2. Memory Traffic: Calculating the suffix sum (denominator) involves multiple passes: exp, flip, cumsum, flip, log.
Optimization Strategy: Fused Sort-Scan Kernel in Shared Memory
Constraint: List size `N` is fixed to a power of 2 (e.g., 1024) for efficient Bitonic Sort.
1. Block-per-Query: Assign one CUDA block to process one query (list of items).
2. Shared Memory Staging: Load scores and labels into Shared Memory structures.
3. Parallel Bitonic Sort:
- Sort the data in Shared Memory based on **labels** in descending order.
- Use `score` as the payload moved along with labels.
- This replaces the global `argsort` + `gather` pattern.
4. Numerical Stability & Suffix Scan:
- Find the max score `M` in the sorted list for stability.
- Compute `exp(score - M)`.
- Perform a **Reverse Parallel Scan** (Suffix Sum) in Shared Memory to calculate `CumSumExp_i = sum_{k=i}^{N-1} exp(...)`.
5. Fused Loss:
- `Loss_i = (log(CumSumExp_i) + M) - score_i`.
- Sum `Loss_i` across the block to get total loss for the query.
Here's an example to show you the syntax of inline embedding custom CUDA operators in torch: The example given architecture is:
```python
import torch
import torch.nn as nn
BATCH_SIZE = 4096
LIST_SIZE = 1024
SHAPE = (BATCH_SIZE, LIST_SIZE)
class ListMLELoss(nn.Module):
def __init__(self, reduction='mean'):
super(ListMLELoss, self).__init__()
self.reduction = reduction
def forward(self, scores: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
# scores: (B, N)
# labels: (B, N)
sorted_indices = torch.argsort(labels, dim=1, descending=True)
sorted_scores = torch.gather(scores, 1, sorted_indices)
max_val, _ = sorted_scores.max(dim=1, keepdim=True)
sorted_scores_stable = sorted_scores - max_val
exp_scores = torch.exp(sorted_scores_stable)
exp_sum_reverse = torch.cumsum(torch.flip(exp_scores, [1]), dim=1)
exp_sum_reverse = torch.flip(exp_sum_reverse, [1])
log_cumsum = torch.log(exp_sum_reverse + 1e-10) # eps
# Restore scale: log(sum(e^(x-m))) = log(sum) + m
# Loss term_i = (log_cumsum_i + M) - sorted_score_i
loss_per_item = (log_cumsum + max_val) - sorted_scores
loss = loss_per_item.sum(dim=1)
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
return loss
class Model(nn.Module):
def __init__(self, reduction='none'):
super(Model, self).__init__()
self.loss_fn = ListMLELoss(reduction=reduction)
def forward(self, scores, labels):
return self.loss_fn(scores, labels)
def get_inputs():
scores = torch.randn(SHAPE, dtype=torch.float32)
labels = torch.randint(0, 5, SHAPE, dtype=torch.float32)
labels += torch.rand(SHAPE, dtype=torch.float32) * 1e-3
return [scores.contiguous(), labels.contiguous()]
def get_init_inputs():
return ['none']

View File

@ -0,0 +1,74 @@
###########################################################
# 性能和精度验证程序
###########################################################
import torch
import torch.nn as nn
import time
from mleloss_torch import Model,get_inputs,get_init_inputs
from mleloss_cuda import ModelNew
def run_benchmark():
# 检查 CUDA 是否可用
if not torch.cuda.is_available():
print("CUDA 不可用,请确保您有可用的 NVIDIA GPU 并已正确安装 PyTorch CUDA 版本。")
return
else:
device = torch.device("cuda")
# 初始化模型
init_inputs = get_init_inputs()
init_inputs = [
x.cuda(device=device) if isinstance(x, torch.Tensor) else x for x in init_inputs
]
inputs = get_inputs()
inputs = [
x.cuda(device=device) if isinstance(x, torch.Tensor) else x for x in inputs
]
torch_model = Model(*init_inputs).cuda()
cuda_model = ModelNew(*init_inputs).cuda()
torch_model.eval()
cuda_model.eval()
print("-------------------- 精度对齐验证 --------------------")
with torch.no_grad():
output_torch = torch_model( *inputs)
output_cuda = cuda_model(*inputs)
precision_flag = torch.allclose(output_torch, output_cuda,rtol=1e-03)
if precision_flag:
print("✅ 精度对齐:两个模型的输出结果非常接近。")
else:
print("❌ 精度不一致!")
print("\n-------------------- 性能加速比测试 --------------------")
num_iterations = 100
# PyTorch 模型计时
torch.cuda.synchronize()
start_time = time.time()
for _ in range(num_iterations):
_ = torch_model(*inputs)
torch.cuda.synchronize()
torch_time = (time.time() - start_time) / num_iterations
# 自定义 CUDA 内核计时
torch.cuda.synchronize()
start_time = time.time()
for _ in range(num_iterations):
_ = cuda_model(*inputs)
torch.cuda.synchronize()
cuda_time = (time.time() - start_time) / num_iterations
print(f"PyTorch torch.relu 平均执行时间: {torch_time:.6f}")
print(f"自定义 CUDA 内核 平均执行时间: {cuda_time:.6f}")
speedup = 0
if cuda_time > 0:
speedup = torch_time / cuda_time
print(f"加速比 (Speedup): {speedup:.2f}x")
else:
print("CUDA 内核执行时间为0无法计算加速比。")
return precision_flag,speedup
if __name__ == "__main__":
precision_flag,speedup = run_benchmark()