finish hillloss #52

This commit is contained in:
hli28146 2025-12-08 10:56:04 +08:00
parent f876a28ada
commit 23f48ac7ce
4 changed files with 379 additions and 0 deletions

View File

@ -0,0 +1,150 @@
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 hill_loss_cuda_forward(
const torch::Tensor& logits,
const torch::Tensor& targets,
float lambda_val,
std::string reduction);
"""
cuda_source = """
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <math.h>
#define BLOCK_SIZE 256
struct __align__(16) Float4 {
float x, y, z, w;
};
// Sigmoid
__device__ __forceinline__ float fast_sigmoid(float x) {
return 1.0f / (1.0f + __expf(-x));
}
// Softplus (stable -log(sigmoid(x)))
__device__ __forceinline__ float fast_softplus_neg(float x) {
// return log(1 + exp(-x))
// if -x is large, exp(-x) -> inf.
// if x < -80, result is -x.
// if x > 20, result is 0.
// Standard Softplus: log(1 + exp(x))
// We want log(1 + exp(-x)) = Softplus(-x)
float neg_x = -x;
if (neg_x > 20.0f) return neg_x;
return logf(1.0f + __expf(neg_x));
}
__device__ __forceinline__ float compute_hill_point(
float logit, float target, float lambda_val)
{
if (target > 0.5f) {
// Positive: BCE
// -log(p) = -log(sigmoid(logit)) = softplus(-logit)
return fast_softplus_neg(logit);
} else {
// Negative: Hill
// (lambda - p) * p^2
float p = fast_sigmoid(logit);
return (lambda_val - p) * p * p;
}
}
__global__ void hill_loss_kernel(
float* __restrict__ output,
const float* __restrict__ logits,
const float* __restrict__ targets,
const int n_elements,
const float lambda_val)
{
const int idx = blockIdx.x * blockDim.x + threadIdx.x;
const int vec_n = n_elements / 4;
int stride = blockDim.x * gridDim.x;
for (int i = idx; i < vec_n; i += stride) {
Float4 l_vec = reinterpret_cast<const Float4*>(logits)[i];
Float4 t_vec = reinterpret_cast<const Float4*>(targets)[i];
Float4 out_vec;
out_vec.x = compute_hill_point(l_vec.x, t_vec.x, lambda_val);
out_vec.y = compute_hill_point(l_vec.y, t_vec.y, lambda_val);
out_vec.z = compute_hill_point(l_vec.z, t_vec.z, lambda_val);
out_vec.w = compute_hill_point(l_vec.w, t_vec.w, lambda_val);
reinterpret_cast<Float4*>(output)[i] = out_vec;
}
int tail_start = vec_n * 4;
int scalar_idx = blockIdx.x * blockDim.x + threadIdx.x;
if (scalar_idx < (n_elements - tail_start)) {
int real_idx = tail_start + scalar_idx;
output[real_idx] = compute_hill_point(
logits[real_idx], targets[real_idx], lambda_val);
}
}
torch::Tensor hill_loss_cuda_forward(
const torch::Tensor& logits,
const torch::Tensor& targets,
float lambda_val,
std::string reduction)
{
TORCH_CHECK(logits.is_cuda() && targets.is_cuda(), "Inputs must be CUDA");
TORCH_CHECK(logits.is_contiguous() && targets.is_contiguous(), "Inputs must be contiguous");
TORCH_CHECK(logits.numel() == targets.numel(), "Shapes must match");
const int n = logits.numel();
auto output = torch::empty_like(logits);
const int vec_n = n / 4;
const int grid_size = (vec_n + BLOCK_SIZE - 1) / BLOCK_SIZE;
int final_grid = (grid_size < 1) ? 1 : grid_size;
if (final_grid > 65535) final_grid = 65535;
hill_loss_kernel<<<final_grid, BLOCK_SIZE>>>(
output.data_ptr<float>(),
logits.data_ptr<float>(),
targets.data_ptr<float>(),
n,
lambda_val
);
if (reduction == "mean") {
return output.mean();
} else if (reduction == "sum") {
return output.sum();
}
return output;
}
"""
class ModelNew(nn.Module):
def __init__(self, lambda_val=1.5, reduction='none'):
super(ModelNew, self).__init__()
self.lambda_val = lambda_val
self.reduction = reduction
self.op = load_inline(
name='hill_loss_op',
cpp_sources=cpp_source,
cuda_sources=cuda_source,
functions=['hill_loss_cuda_forward'],
verbose=False,
extra_cuda_cflags=['-O3']
)
def forward(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
return self.op.hill_loss_cuda_forward(
logits.contiguous(),
targets.contiguous(),
self.lambda_val,
self.reduction
)

View File

@ -0,0 +1,62 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
BATCH_SIZE = 128
NUM_CLASSES = 80
SHAPE = (BATCH_SIZE, NUM_CLASSES, 64, 64)
LAMBDA = 1.5
REDUCTION = 'none'
class HillLoss(nn.Module):
"""
Hill Loss for Multi-Label Learning with Missing Labels.
https://arxiv.org/pdf/2112.07368
Negatives: (lambda - p) * p^2
Positives: BCE
"""
def __init__(self, lambda_val=1.5, reduction='mean'):
super(HillLoss, self).__init__()
self.lambda_val = lambda_val
self.reduction = reduction
def forward(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
# logits: arbitrary shape
# targets: same shape, 0 or 1
probs = torch.sigmoid(logits)
# Positive Loss
pos_loss = F.softplus(-logits)
# Negative Loss (Hill Loss)
neg_loss = (self.lambda_val - probs) * (probs ** 2)
# Combine
# loss = y * pos + (1-y) * neg
loss = targets * pos_loss + (1 - targets) * neg_loss
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
return loss
class Model(nn.Module):
def __init__(self, lambda_val=1.5, reduction='none'):
super(Model, self).__init__()
self.loss_fn = HillLoss(lambda_val=lambda_val, reduction=reduction)
def forward(self, logits, targets):
return self.loss_fn(logits, targets)
def get_inputs():
logits = torch.randn(SHAPE, dtype=torch.float32)
# 稀疏标签,大部分为 0
targets = (torch.rand(SHAPE) > 0.9).float()
return [logits.contiguous(), targets.contiguous()]
def get_init_inputs():
return [LAMBDA, REDUCTION]

View File

@ -0,0 +1,93 @@
Write a custom CUDA kernel to optimize `Hill Loss` (from "Simple and Robust Loss Design for Multi-Label Learning with Missing Labels").
Formula:
Loss = y * L_pos + (1 - y) * L_neg
L_pos = -log(p) (Standard BCE for positives)
L_neg = (lambda - p) * p^2 (Hill Loss for negatives)
Where p = sigmoid(logit).
Lambda is a hyperparameter (default 1.5).
Problem Analysis:
1. Memory Efficiency: Standard implementation requires calculating sigmoid, creating masks for positive/negative samples, computing different loss branches, and merging. This generates intermediate tensors and multiple memory passes.
2. Element-wise Fusion: The operation is strictly element-wise and computationally lightweight.
Optimization Strategy: Fused Element-wise Kernel with Vectorization
1. Flattened Input: Treat input tensors of any shape `(N, C, ...)` as a 1D array.
2. Vectorized Loads (float4): Use `float4` to load 4 logits and 4 targets at a time.
3. In-Register Logic:
- Compute `p = sigmoid(logit)`.
- Branchless or If-Else logic based on `target`:
- If `target == 1`: `loss = -log(p)` (using stable `log_sigmoid` equivalent).
- If `target == 0`: `loss = (lambda - p) * p * p`.
- Store result directly.
4. Numerical Stability: Ensure `log(p)` handles edge cases safely, or use `log_sigmoid(logit)` for positives.
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 = 128
NUM_CLASSES = 80
SHAPE = (BATCH_SIZE, NUM_CLASSES, 64, 64)
LAMBDA = 1.5
REDUCTION = 'none'
class HillLoss(nn.Module):
"""
Hill Loss for Multi-Label Learning with Missing Labels.
https://arxiv.org/pdf/2112.07368
Negatives: (lambda - p) * p^2
Positives: BCE
"""
def __init__(self, lambda_val=1.5, reduction='mean'):
super(HillLoss, self).__init__()
self.lambda_val = lambda_val
self.reduction = reduction
def forward(self, logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
# logits: arbitrary shape
# targets: same shape, 0 or 1
probs = torch.sigmoid(logits)
# Positive Loss
pos_loss = F.softplus(-logits)
# Negative Loss (Hill Loss)
neg_loss = (self.lambda_val - probs) * (probs ** 2)
# Combine
# loss = y * pos + (1-y) * neg
loss = targets * pos_loss + (1 - targets) * neg_loss
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
return loss
class Model(nn.Module):
def __init__(self, lambda_val=1.5, reduction='none'):
super(Model, self).__init__()
self.loss_fn = HillLoss(lambda_val=lambda_val, reduction=reduction)
def forward(self, logits, targets):
return self.loss_fn(logits, targets)
def get_inputs():
logits = torch.randn(SHAPE, dtype=torch.float32)
# 稀疏标签,大部分为 0
targets = (torch.rand(SHAPE) > 0.9).float()
return [logits.contiguous(), targets.contiguous()]
def get_init_inputs():
return [LAMBDA, REDUCTION]

View File

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