Merge pull request 'finish cosfaceloss #21' (#317) from hli28146/GPUCodeForces:h21 into main

This commit is contained in:
Kuohais 2025-12-04 14:54:04 +08:00
commit 7ef720c8bf
4 changed files with 445 additions and 0 deletions

View File

@ -0,0 +1,202 @@
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 cosface_cuda_forward(
const torch::Tensor& cosine,
const torch::Tensor& label,
float s,
float m,
std::string reduction);
"""
cuda_source = """
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <math.h>
#include <cfloat>
#define BLOCK_SIZE 256
#define WARP_SIZE 32
// --- Reductions Helpers ---
template<typename T>
__device__ __forceinline__ T warp_reduce_max(T val) {
#pragma unroll
for (int offset = WARP_SIZE / 2; offset > 0; offset /= 2) {
val = max(val, __shfl_down_sync(0xffffffff, val, offset));
}
return val;
}
template<typename T>
__device__ __forceinline__ T warp_reduce_sum(T val) {
#pragma unroll
for (int offset = WARP_SIZE / 2; offset > 0; offset /= 2) {
val += __shfl_down_sync(0xffffffff, val, offset);
}
return val;
}
__device__ __forceinline__ float block_reduce_max(float val) {
static __shared__ float shared[32];
int lane = threadIdx.x % WARP_SIZE;
int wid = threadIdx.x / WARP_SIZE;
val = warp_reduce_max(val);
if (lane == 0) shared[wid] = val;
__syncthreads();
val = (threadIdx.x < blockDim.x / WARP_SIZE) ? shared[lane] : -FLT_MAX;
if (wid == 0) val = warp_reduce_max(val);
return val;
}
__device__ __forceinline__ float block_reduce_sum(float val) {
static __shared__ float shared[32];
int lane = threadIdx.x % WARP_SIZE;
int wid = threadIdx.x / WARP_SIZE;
val = warp_reduce_sum(val);
if (lane == 0) shared[wid] = val;
__syncthreads();
val = (threadIdx.x < blockDim.x / WARP_SIZE) ? shared[lane] : 0.0f;
if (wid == 0) val = warp_reduce_sum(val);
return val;
}
// --- Fused CosFace Kernel ---
__global__ void cosface_kernel(
float* __restrict__ output,
const float* __restrict__ cosine,
const int64_t* __restrict__ label,
int num_classes,
float s,
float m)
{
// One block per sample (row)
int row = blockIdx.x;
int tid = threadIdx.x;
const float* row_cosine = cosine + row * num_classes;
int64_t target_idx = label[row];
// 1. Pass 1: Find Max (of Modified Logits)
float local_max = -FLT_MAX;
// Grid-Stride Loop
for (int i = tid; i < num_classes; i += blockDim.x) {
float val = row_cosine[i];
// Apply CosFace Margin Logic On-The-Fly
if (i == target_idx) {
val = val - m;
}
// Scale
val = val * s;
local_max = fmaxf(local_max, val);
}
float global_max = block_reduce_max(local_max);
// Broadcast max to all threads in block
__shared__ float s_max;
if (tid == 0) s_max = global_max;
__syncthreads();
global_max = s_max;
// 2. Pass 2: Sum Exp
float local_sum = 0.0f;
float target_logit = 0.0f; // Cache target logit for final loss
for (int i = tid; i < num_classes; i += blockDim.x) {
float val = row_cosine[i];
if (i == target_idx) {
val = val - m;
}
val = val * s;
// Save target logit if this thread handles it
if (i == target_idx) {
target_logit = val;
}
local_sum += __expf(val - global_max);
}
float global_sum = block_reduce_sum(local_sum);
// 3. Final Calculation (Thread 0)
// We need target_logit available to Thread 0.
// Optimization: Since we don't store target_logit in Shared Memory,
// let Thread 0 re-calculate it. It's just one memory read, cheaper than shared mem sync.
if (tid == 0) {
float t_val = row_cosine[target_idx];
t_val = (t_val - m) * s; // Target logit
// LogSumExp = global_max + log(global_sum)
// LogSoftmax(target) = target_logit - LogSumExp
// NLL Loss = -LogSoftmax(target) = LogSumExp - target_logit
float loss = (global_max + __logf(global_sum)) - t_val;
output[row] = loss;
}
}
torch::Tensor cosface_cuda_forward(
const torch::Tensor& cosine,
const torch::Tensor& label,
float s,
float m,
std::string reduction)
{
TORCH_CHECK(cosine.is_cuda() && label.is_cuda(), "Inputs must be CUDA");
TORCH_CHECK(cosine.is_contiguous(), "Cosine must be contiguous");
TORCH_CHECK(cosine.dim() == 2, "Cosine must be 2D (Batch, Class)");
TORCH_CHECK(label.dim() == 1, "Label must be 1D (Batch)");
TORCH_CHECK(cosine.size(0) == label.size(0), "Batch size mismatch");
int batch_size = cosine.size(0);
int num_classes = cosine.size(1);
auto output = torch::empty({batch_size}, cosine.options());
// One Block per Sample
cosface_kernel<<<batch_size, BLOCK_SIZE>>>(
output.data_ptr<float>(),
cosine.data_ptr<float>(),
label.data_ptr<int64_t>(),
num_classes,
s,
m
);
if (reduction == "mean") {
return output.mean();
} else if (reduction == "sum") {
return output.sum();
}
return output;
}
"""
class ModelNew(nn.Module):
def __init__(self, s=64.0, m=0.35, reduction='none'):
super(ModelNew, self).__init__()
self.s = s
self.m = m
self.reduction = reduction
self.op = load_inline(
name='cosface_loss_op',
cpp_sources=cpp_source,
cuda_sources=cuda_source,
functions=['cosface_cuda_forward'],
verbose=False,
extra_cuda_cflags=['-O3']
)
def forward(self, cosine: torch.Tensor, label: torch.Tensor) -> torch.Tensor:
return self.op.cosface_cuda_forward(cosine.contiguous(), label.contiguous(), self.s, self.m, self.reduction)

View File

@ -0,0 +1,65 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
# 人脸识别场景Batch Size 较小,但分类数 (Classes/Identities) 极大
BATCH_SIZE = 512
NUM_CLASSES = 10000
SHAPE = (BATCH_SIZE, NUM_CLASSES)
# CosFace 超参数
SCALE_S = 64.0
MARGIN_M = 0.35
class CosFaceLoss(nn.Module):
"""
Standard PyTorch implementation of CosFace Loss.
"""
def __init__(self, s=64.0, m=0.35, reduction='mean'):
super(CosFaceLoss, self).__init__()
self.s = s
self.m = m
self.reduction = reduction
def forward(self, cosine: torch.Tensor, label: torch.Tensor) -> torch.Tensor:
# cosine: (N, C) - Normalized Features @ Normalized Weights
# label: (N)
# 1. 创建 One-hot 掩码
one_hot = torch.zeros_like(cosine)
one_hot.scatter_(1, label.view(-1, 1), 1.0)
# 2. Apply Margin: cos_theta - m (only for target class)
logits = cosine - one_hot * self.m
# 3. Scale: s * logits
logits = logits * self.s
# 4. CrossEntropy
loss = F.cross_entropy(logits, label, reduction='none')
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
return loss
class Model(nn.Module):
def __init__(self, s=64.0, m=0.35, reduction='none'):
super(Model, self).__init__()
self.loss_fn = CosFaceLoss(s=s, m=m, reduction=reduction)
def forward(self, cosine, label):
return self.loss_fn(cosine, label)
def get_inputs():
# 模拟归一化后的 Cosine Similarity (-1 ~ 1)
cosine = torch.randn(SHAPE, dtype=torch.float32)
# 归一化到 [-1, 1] 模拟真实余弦值
cosine = torch.clamp(cosine, -1.0, 1.0)
label = torch.randint(0, NUM_CLASSES, (BATCH_SIZE,), dtype=torch.long)
return [cosine.contiguous(), label.contiguous()]
def get_init_inputs():
return [SCALE_S, MARGIN_M, 'none']

104
S1/hli28146_#21/prompt.txt Normal file
View File

@ -0,0 +1,104 @@
Write a custom CUDA kernel to optimize `CosFace Loss` (Large Margin Cosine Loss).
Formula: Loss = -log( exp(s * (cos_theta_yi - m)) / Sum(exp(s * cos_theta_j_modified)) )
Where:
- `cos_theta` is the cosine similarity matrix (Batch, Classes).
- `yi` is the ground truth class index for the sample.
- `m` is the additive cosine margin.
- `s` is the scaling factor.
- For the target class `j == yi`, the logit is `s * (cos_theta - m)`.
- For other classes `j != yi`, the logit is `s * cos_theta`.
Problem Analysis:
1. Memory Overhead: A standard implementation uses `torch.scatter` or `one_hot` multiplication to subtract `m` only from the target indices. This allocates auxiliary tensors equal to the size of the logits (N, C), wasting memory bandwidth.
2. Operator Chaining: The sequence `scatter/sub` -> `scale` -> `CrossEntropy (LogSoftmax -> NLL)` involves multiple kernel launches and redundant global memory reads/writes.
Optimization Strategy: Fused Logits-Modification and CrossEntropy
The strategy is to fuse the margin application, scaling, softmax normalization, and loss calculation into a single pass.
1. One-Block-per-Row: Each CUDA block processes one sample (one row of the cosine matrix) to calculate its loss.
2. On-the-Fly Logic:
- Load cosine values from global memory.
- Check if the current column index matches the target label `y_i`.
- If match: val = s * (val - m).
- If not match: val = s * val.
This eliminates the need for one-hot masks or scatter operations.
3. Online Softmax (LogSumExp):
- Use the 2-pass reduction algorithm (or 1-pass online algorithm) within the block to compute `Max` and `SumExp` of the *modified* logits.
- This ensures numerical stability without materializing the full modified logits tensor.
4. Fused NLL Loss:
- Calculate `log_prob = target_logit - (max_val + log(sum_exp))`.
- Output `-log_prob`.
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
import torch.nn.functional as F
# 人脸识别场景Batch Size 较小,但分类数 (Classes/Identities) 极大
BATCH_SIZE = 512
NUM_CLASSES = 10000
SHAPE = (BATCH_SIZE, NUM_CLASSES)
# CosFace 超参数
SCALE_S = 64.0
MARGIN_M = 0.35
class CosFaceLoss(nn.Module):
"""
Standard PyTorch implementation of CosFace Loss.
"""
def __init__(self, s=64.0, m=0.35, reduction='mean'):
super(CosFaceLoss, self).__init__()
self.s = s
self.m = m
self.reduction = reduction
def forward(self, cosine: torch.Tensor, label: torch.Tensor) -> torch.Tensor:
# cosine: (N, C) - Normalized Features @ Normalized Weights
# label: (N)
# 1. 创建 One-hot 掩码
one_hot = torch.zeros_like(cosine)
one_hot.scatter_(1, label.view(-1, 1), 1.0)
# 2. Apply Margin: cos_theta - m (only for target class)
logits = cosine - one_hot * self.m
# 3. Scale: s * logits
logits = logits * self.s
# 4. CrossEntropy
loss = F.cross_entropy(logits, label, reduction='none')
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
return loss
class Model(nn.Module):
def __init__(self, s=64.0, m=0.35, reduction='none'):
super(Model, self).__init__()
self.loss_fn = CosFaceLoss(s=s, m=m, reduction=reduction)
def forward(self, cosine, label):
return self.loss_fn(cosine, label)
def get_inputs():
# 模拟归一化后的 Cosine Similarity (-1 ~ 1)
cosine = torch.randn(SHAPE, dtype=torch.float32)
# 归一化到 [-1, 1] 模拟真实余弦值
cosine = torch.clamp(cosine, -1.0, 1.0)
label = torch.randint(0, NUM_CLASSES, (BATCH_SIZE,), dtype=torch.long)
return [cosine.contiguous(), label.contiguous()]
def get_init_inputs():
return [SCALE_S, MARGIN_M, 'none']

View File

@ -0,0 +1,74 @@
###########################################################
# 性能和精度验证程序
###########################################################
import torch
import torch.nn as nn
import time
from cosfaceloss_torch import Model,get_inputs,get_init_inputs
from cosfaceloss_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()