Intro-ops/notebooks/04_softmax_kernel.ipynb

261 lines
7.8 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 04 — Softmax 算子\n",
"\n",
"## 学习目标\n",
"\n",
"1. 理解数值稳定性:为什么需要减 max\n",
"2. 理解 online softmax 算法(一遍扫描 vs 三趟扫描)\n",
"3. 理解 TileLang 中为什么用 `exp2`/`log2` 替代 `exp`/`log`\n",
"4. 完成 NVIDIA + TileLang 两个 kernel 的 TODO\n",
"\n",
"Softmax 是四个算子中最复杂的——既需要线程间通信(同步归约),又有数值稳定性陷阱。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 概念导入Softmax 的数值稳定性\n",
"\n",
"### 朴素公式\n",
"\n",
"$$\\text{softmax}(x_i) = \\frac{e^{x_i}}{\\sum_j e^{x_j}}$$\n",
"\n",
"### 问题\n",
"\n",
"$e^{88.7} \\approx 1.6 \\times 10^{38}$,接近 FP32 上限。如果 $x_i = 100$,则 $e^{100}$ 溢出为 infinf/inf = NaN。\n",
"\n",
"### 解决:减最大值\n",
"\n",
"$$\\text{softmax}(x_i) = \\frac{e^{x_i - \\max(x)}}{\\sum_j e^{x_j - \\max(x)}}$$\n",
"\n",
"分子分母同除 $e^{\\max(x)}$,数学结果不变,但 $e^{x_i - \\max(x)}$ 最大为 $e^0 = 1$,永不溢出。\n",
"\n",
"详细图示见 [docs/diagrams/softmax-pipeline.md](../docs/diagrams/softmax-pipeline.md)。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## PyTorch 参考实现"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"\n",
"src = torch.randn(32, 128, device=\"cuda\", dtype=torch.float32)\n",
"expected = torch.softmax(src, dim=1)\n",
"\n",
"print(f\"输入 shape: {src.shape}\")\n",
"print(f\"输出 shape: {expected.shape}\")\n",
"print(f\"每行和: {expected.sum(dim=1)[:5]}\") # 应该全为 1.0\n",
"\n",
"# 演示溢出问题\n",
"big = torch.tensor([100.0, 200.0, 300.0], device=\"cuda\")\n",
"naive = torch.exp(big) / torch.exp(big).sum()\n",
"print(f\"\\n朴素 softmax([100, 200, 300]): {naive}\") # NaN!\n",
"\n",
"stable = torch.softmax(big, dim=0)\n",
"print(f\"稳定 softmax([100, 200, 300]): {stable}\") # [0, 0, 1]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## NVIDIA CUDA Kernel三趟扫描\n",
"\n",
"打开 `ops/softmax/nvidia/kernel.cuh`。基础版本使用三趟扫描:\n",
"\n",
"1. Pass 1: 求行最大值 `max_val`\n",
"2. Pass 2: 写 `exp(x - max)` 到 out同时累加 sum\n",
"3. Pass 3: 除 sum 归一化"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"KERNEL_SKELETON = \"\"\"\n",
"__global__ void softmax_rowwise_kernel(\n",
" float *out, const float *in, int64_t rows, int64_t cols) {\n",
"\n",
" // TODO: implement a numerically stable row-wise softmax kernel.\n",
" //\n",
" // Suggested steps:\n",
" // 1. One block per row.\n",
" // 2. Pass 1: find row max via shared memory reduction.\n",
" // 3. Pass 2: exp(x - max) → out, accumulate sum via reduction.\n",
" // 4. Pass 3: out[i] /= sum.\n",
"}\n",
"\"\"\"\n",
"print(KERNEL_SKELETON)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 为什么是三趟?\n",
"\n",
"因为 softmax 有两个需要全局信息才能计算的步骤:\n",
"- 求 max 需要全局信息(所有元素的最大值)\n",
"- 求 sum 需要全局信息(所有 exp(x-max) 的和)\n",
"\n",
"这两个全局信息都通过 shared memory 归约获得,每一步都需要 block 内同步。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 验证 NVIDIA 版本\n",
"import subprocess, sys\n",
"result = subprocess.run([\n",
" sys.executable, \"-m\", \"pytest\",\n",
" \"tests/op_tests/test_softmax.py\", \"-v\",\n",
" \"--backend\", \"nvidia\"\n",
"], capture_output=True, text=True)\n",
"print(result.stdout)\n",
"if result.returncode == 0:\n",
" print(\"✓ NVIDIA softmax kernel 测试全部通过!\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## TileLang KernelOnline Softmax\n",
"\n",
"打开 `ops/softmax/tilelang/kernel.py`。TileLang 版本使用 **online softmax**——只需两趟扫描。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"TILELANG_SKELETON = \"\"\"\n",
"@tilelang.jit\n",
"def softmax_kernel(src, BLOCK_N: int, BLOCK_M: int):\n",
" log2_e = 1.44269504 # log2(e) for exp/log2 conversion\n",
" N, M = T.const(\"N, M\")\n",
" dtype = T.float32\n",
" src: T.Tensor((N, M), dtype)\n",
" out = T.empty((N, M), dtype)\n",
"\n",
" # TODO: implement a tiled row-wise softmax with online algorithm.\n",
" #\n",
" # Two-pass approach:\n",
" # Pass 1: scroll through column tiles, update running log-sum-exp.\n",
" # Pass 2: scroll again, normalize each tile with final lse.\n",
"\n",
" return out\n",
"\"\"\"\n",
"print(TILELANG_SKELETON)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 为什么用 `exp2`/`log2`\n",
"\n",
"GPU 硬件对 2 的幂运算 (`2^x`) 有专门的快速指令,比自然指数 (`e^x`) 快。\n",
"\n",
"转换公式:$\\exp(x) = 2^{x \\cdot \\log_2(e)}$\n",
"\n",
"其中 $\\log_2(e) \\approx 1.44269504$(代码中的 `log2_e` 常量)。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 验证 TileLang 版本\n",
"result = subprocess.run([\n",
" sys.executable, \"-m\", \"pytest\",\n",
" \"tests/op_tests/test_softmax.py\", \"-v\",\n",
" \"--backend\", \"tilelang\"\n",
"], capture_output=True, text=True)\n",
"print(result.stdout)\n",
"if result.returncode == 0:\n",
" print(\"✓ TileLang softmax kernel 测试全部通过!\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 对比验证"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from operator_runtime import softmax\n",
"\n",
"src = torch.randn(32, 128, device=\"cuda\", dtype=torch.float32)\n",
"my_out = softmax(src, dim=1, backend=\"nvidia\")\n",
"torch_out = torch.softmax(src, dim=1)\n",
"\n",
"print(f\"我的 kernel 行和: {my_out.sum(dim=1)[:5]}\")\n",
"print(f\"PyTorch 行和: {torch_out.sum(dim=1)[:5]}\")\n",
"print(f\"最大误差: {(my_out - torch_out).abs().max().item():.2e}\")\n",
"print(f\"一致: {torch.allclose(my_out, torch_out, atol=1e-5, rtol=1e-5)}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 检查清单\n",
"\n",
"- [ ] 理解数值稳定性问题:为什么需要减 max\n",
"- [ ] 理解 online softmax 算法(一遍扫描 vs 三趟扫描)\n",
"- [ ] 理解 log-sum-exp 的滚动更新逻辑\n",
"- [ ] 理解 TileLang 中为什么用 `exp2`/`log2` 替代 `exp`/`log`\n",
"- [ ] NVIDIA `kernel.cuh` TODO 完成\n",
"- [ ] TileLang `kernel.py` TODO 完成\n",
"- [ ] 两种后端测试全部通过\n",
"- [ ] 性能与 PyTorch 参考实现对比\n",
"- [ ] 尝试 warp-level 优化"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}