# CTI 生成式推荐广告排序推理优化方案

> 基于 baseline 代码分析、HSTU / GRAB 论文研究、官方提交规范

---

## 0. 前置修复：接口对齐（必须首先完成）

### 0.1 发现的问题

官方 `任务提交接口说明.md` 定义了评测系统调用接口。baseline 代码与规范存在三处致命不匹配：

| 接口 | 官方要求 | Baseline 实际 | 后果 |
|------|----------|---------------|------|
| 数据集类名 | `CTRTestSeqDataset` | `CTRUserDataset` | 评测 `from infer import CTRTestSeqDataset` 失败 |
| 构造参数 | `test_logids_ordered`, `item_dict`, `user_seq`, `max_feasign_per_slot`, `max_ctx_len` | `item_dict`, `user_seq`, `max_feasign_per_slot`, `pred_logids` | 参数名和数量不匹配 |
| `load_model` | `load_model(ckpt_path: Path) -> (model, device)` | `load_model(device='cuda:0', ckpt_path=None)` | 评测调用时 Path 会被错误赋给 `device` 参数 |

评测系统会用 `python -c "from infer import CTRTestSeqDataset, load_model; ..."` 来加载你的代码。**这三处不改，任何优化都白费。**

### 0.2 修复步骤

**修复 1：重命名类并调整构造参数**

将 `CTRUserDataset` 改为 `CTRTestSeqDataset`，参数名改为 `test_logids_ordered`，增加 `max_ctx_len` 占位：

```python
class CTRTestSeqDataset(Dataset):
    """按用户组织的 CTR 测试数据集（对齐评测接口）"""

    def __init__(self, test_logids_ordered, item_dict, user_seq=None,
                 max_feasign_per_slot=None, max_ctx_len=None):
        super().__init__()
        self.item_dict = item_dict
        self.user_seq = user_seq if user_seq else {}
        self.max_feasign_per_slot = max_feasign_per_slot
        self.max_ctx_len = max_ctx_len
        self.pred_logids = set(test_logids_ordered) if test_logids_ordered else set()
        # ... 其余逻辑不变
```

**修复 2：修正 `load_model` 签名**

```python
def load_model(ckpt_path, device='cuda:0'):
    """加载模型。签名对齐评测接口：第一个参数必须是 ckpt_path。"""
    # ... 其余逻辑不变
```

### 0.3 提交验证标准

修改后必须在 AI Studio 提交一次，确认能跑通（得分 > 0），再开始做优化。这是所有优化的前提。

---

## 1. 优化起点（Baseline 数据）

| 指标 | 当前值 | 说明 |
|------|--------|------|
| 推理耗时 | 229.18s | **只计 `model(batch)` 的逐 batch 累加时间** |
| AUC | 0.759 | 阈值 ≥ 0.65 |
| PCOC | 1.110 | 阈值 [0.85, 1.15] |
| 综合得分 | 25.85 | `score_latency * 70 + score_model * 30` |

技术栈：
- Python 3.10.10 + PyTorch 2.6.0 + CUDA 12.4
- 模型：RepEncoder（28 slot / 512 维 embedding）→ 8 层 Transformer（8 头 / 512 维）→ MoE FFN（8 个 expert，Top-2 gating）
- 数据：已缓存为 9 个 shard 分片（shard_0000.pt ~ shard_0008.pt），共 2039 batch / 7774 条预测

---

## 2. 约束条件（全部来自官方规则）

| 约束 | 来源 | 说明 |
|------|------|------|
| 不能改"组网" | 赛题说明 | 模型结构、层数、头数、维度不可改 |
| 不能改参数（权重值） | 赛题说明 | 量化/稀疏化/剪枝**明确允许** |
| 不能用测试集训练 | 赛题说明 | 仅推理，不做任何训练 |
| 推理时限 ≤ 300s | 提交规范 §4.1 | 超时总分直接 0。**只计 `model(batch)` 时间，数据加载和模型加载不计** |
| build_env.sh 时限 ≤ 720s | 提交规范 §3 | 超时或非 0 退出码直接失败 |
| AUC ≥ 0.65 且 PCOC ∈ [0.85, 1.15] | 提交规范 §4.2 | 任一不满足，总分直接 0 |
| 压缩包内不能有 `dataset/` 和 `ckpt.pt` | 提交规范 §2.2 | 评测系统自行提供 |
| 压缩包后缀必须是 `.zip`/`.tar.gz`/`.tar` | 提交规范 §1 | 其他格式不识别 |
| 解压后文件必须直接在根目录 | 提交规范 §1 | 不能多一层包裹文件夹 |

---

## 3. 优化方案总览（修订版）

```
第〇步：接口对齐（不优化，先跑通）
  └── 确认能提交得分 > 0
       │
第①步：FP16 量化 ←── 明确允许，收益最大
  └── 预期：229s → ~120s
       │
第②步：Flash Attention ←── 数学等价，不改组网
  └── 预期：120s → ~90s
       │
第③步：torch.compile ←── 编译器优化，不改组网
  └── 预期：90s → ~65s
       │
第④步：数据流优化 ←── 减少 CPU→GPU 传输开销
  └── 预期：65s → ~55s
       │
第⑤步：MoE 优化 ←── "剪枝"明确允许
  └── 预期：55s → ~50s
       │
第⑥步：INT8 量化（可选） ←── 收益大但风险高
  └── 只有在①②③⑤之后仍不够时才尝试
```

每个方案完成后在 AI Studio 提交验证，确认分数提升后再进入下一步。

---

## 4. 方案一：FP16 量化

### 4.1 合规性

✅ 比赛规则明确写有"量化除外"，属于允许范围。

### 4.2 原理

当前 baseline 使用 FP32。FP16 将模型参数和激活值减半，GPU 的张量核心对 FP16 有原生加速。注意 `Embedding` 层保留 FP32（索引是整数，embedding 查表时再转换）。

### 4.3 实现

修改 `load_model` 函数中的模型加载部分：

```python
def load_model(ckpt_path, device='cuda:0'):
    # ... 模型初始化不变 ...

    dev = torch.device(device if torch.cuda.is_available() else "cpu")

    if ckpt_path is None:
        ckpt_path = Path(__file__).parent / 'ckpt.pt'
    else:
        ckpt_path = Path(ckpt_path)

    if ckpt_path.exists():
        ckpt = torch.load(ckpt_path, map_location='cpu', weights_only=False)
        model.load_state_dict(ckpt['model_state_dict'])

        # === FP16 优化：将模型转为半精度 ===
        model = model.half()
        # Embedding 层保留 FP32（索引是 int，不需要转）
        model.rep_encoder.emb = model.rep_encoder.emb.to(torch.float32)

        print(f"[INFO] Loaded checkpoint from {ckpt_path} (epoch={ckpt.get('epoch', '?')})")
    else:
        print(f"[WARNING] Checkpoint {ckpt_path} not found, using random weights")

    model.to(dev)
    model.eval()
    print(f"[INFO] Model ready. Device: {dev}")
    return model, dev
```

`move_batch_to_device` 中同步处理输入数据精度：

```python
def move_batch_to_device(batch, device):
    if isinstance(batch, dict):
        return {k: move_batch_to_device(v, device) for k, v in batch.items()}
    elif isinstance(batch, (list, tuple)):
        return [move_batch_to_device(x, device) for x in batch]
    elif torch.is_tensor(batch):
        x = batch.to(device)
        # 浮点 tensor → FP16，整数 tensor 保持不变
        if x.dtype == torch.float32:
            x = x.half()
        return x
    else:
        return batch
```

同时修改 `main()` 中缓存 batch 的逻辑，在落地磁盘前就转 FP16，避免推理时逐 batch 转换：

```python
for batch in test_loader:
    # 预转为 FP16
    batch = move_batch_to_device(batch, torch.device('cpu'))
    all_batches.append(batch)
```

### 4.4 风险

- 极低。现代 NVIDIA GPU（V100/A100/H100 等）的 FP16 吞吐量是 FP32 的 2-8 倍
- Embedding 保留 FP32 是因为 embedding 查表操作用 int 索引，不受浮点精度影响

### 4.5 预期收益

| 指标 | 变化 |
|------|------|
| 推理时间 | 229s → ~115-150s |
| AUC | 几乎不变（差异 < 0.0001） |

---

## 5. 方案二：Flash Attention

### 5.1 合规性

✅ Flash Attention 是**数学等价**的注意力计算。它用分块（tiling）算法计算 `softmax(QK^T/√d)V`，**输出结果与标准 attention 相同**（在浮点误差范围内）。不改变模型结构、不改变参数——只改变了内存访问模式和计算顺序。这属于编译器级优化，不是组网修改。

### 5.2 原理

Baseline 的 `scaled_dot_product` 先计算完整的 `QK^T` 矩阵（O(L²) 显存），再做 softmax，再乘 V。Flash Attention 将 Q、K、V 分块加载到 SRAM，逐块计算 softmax 并累积结果，**避免完整 QK^T 驻留 HBM**。

Baseline 用了 Sequence Packing（同一用户的多个 impression 拼成一条长序列），序列越长收益越大。

### 5.3 实现

PyTorch 2.0+ 提供了 `F.scaled_dot_product_attention`，自动选择最优后端（Flash Attention / Memory Efficient Attention / 标准实现）：

```python
import torch.nn.functional as F

def scaled_dot_product(q, k, v, extension):
    """使用 PyTorch SDPA 后端（自动启用 Flash Attention）"""
    d = q.size(-1)

    if extension is not None and "mask" in extension:
        mask = extension["mask"]
        # mask 形状: [1, 1, S, S] 或 [B, 1, S, S]
        # 转换为 float mask，确保 device 和 dtype 一致
        attn_mask = mask.to(device=q.device, dtype=q.dtype)
    else:
        attn_mask = None

    return F.scaled_dot_product_attention(
        q, k, v,
        attn_mask=attn_mask,
        dropout_p=0.0,
        is_causal=False,
    )
```

验证是否启用了 Flash Attention：

```python
import torch
# 在模型加载后运行，确认后端可用
print("Flash SDP:", torch.backends.cuda.flash_sdp_enabled())
print("Mem Efficient SDP:", torch.backends.cuda.mem_efficient_sdp_enabled())
print("Math SDP:", torch.backends.cuda.math_sdp_enabled())
```

### 5.4 与 Baseline 输出的兼容性

Baseline 中 mask 是 `torch.bool` 类型，原代码用 `scores.masked_fill(mask == 0, float("-inf"))` 处理。`F.scaled_dot_product_attention` 接受 bool mask 时自动做等价处理。数学等价，不会影响 AUC/PCOC。

### 5.5 预期收益

| 序列长度 | 加速比 |
|----------|--------|
| < 512 | 1.1x - 1.3x |
| 512 - 2048 | 1.5x - 2x |
| > 2048 | 2x - 4x |

---

## 6. 方案三：torch.compile

### 6.1 合规性

✅ `torch.compile` 是纯粹的**编译器优化**（JIT 编译 + 算子融合）。它不改变模型结构、不改变权重值、不改变任何数学运算——只是把多个小 kernel 合并成一个大 kernel 以减少 GPU kernel launch 开销。

### 6.2 实现

在 `load_model` 中，`model.eval()` 之后添加一行：

```python
model = torch.compile(model, mode="reduce-overhead")
```

`mode` 选择：
| 模式 | 行为 | 适合场景 |
|------|------|----------|
| `"default"` | 平衡编译速度与运行时 | 不确定时先用这个 |
| `"reduce-overhead"` | 更激进的融合，减少 kernel launch | **推理场景首选** |
| `"max-autotune"` | 自动调优 Triton kernel（首次编译慢） | 最终提交时切换到此 |

建议先用 `"reduce-overhead"` 快速验证，最终提交时换成 `"max-autotune"`。

### 6.3 已知限制

- 首次运行会触发 JIT 编译（0.5-2 分钟），在评测环境可能计入推理时间
- 解决：在 `build_env.sh` 中用一个小输入做一次预热编译：

```bash
#!/bin/bash
set -e
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

# 预热 torch inductor，避免推理时编译
python -c "
import torch
@torch.compile(mode='max-autotune')
def warmup(x):
    return x * 2
x = torch.randn(100, 100, device='cuda')
warmup(x)
print('Inductor cache ready')
"

echo "build env success"
```

### 6.4 预期收益

| 模式 | 加速比 |
|------|--------|
| reduce-overhead | 1.2x - 1.4x |
| max-autotune | 1.3x - 1.5x（首次多花 1-2 分钟） |

---

## 7. 方案四：数据流优化

### 7.1 重要澄清：评测的计时逻辑

从提交规范 §4.1：
> 只统计模型前向时间（逐 batch 累加 `model(batch)` 耗时），数据加载、模型加载不计入。

所以 **DataLoader 层面的优化（num_workers、pin_memory）不影响评分**。但这不代表数据流优化没用——以下两点仍然有效：

1. **减少 `move_batch_to_device` 开销**：此函数在 `model(batch)` 之前调用，但如果它在推理循环里逐 batch 执行，仍会阻塞。解决方案：提前把所有 batch 搬到 GPU。
2. **减少 FP32→FP16 转换开销**：在缓存阶段就转好，推理循环里无需重复转换。

### 7.2 实现

在 `main()` 的数据加载部分，缓存时就完成设备搬运和类型转换：

```python
# 原代码
all_batches = [batch for batch in test_loader]

# 优化：缓存时直接搬到 GPU + 转 FP16
dev = torch.device('cuda:0')
all_batches = []
for batch in test_loader:
    batch = move_batch_to_device(batch, dev)
    all_batches.append(batch)
```

这样推理循环中不需要再调用 `move_batch_to_device`：

```python
with torch.no_grad():
    for batch in tqdm(all_batches, desc="Inference"):
        # batch 已在 GPU 上，无需 move_batch_to_device
        pred_mask = batch["pred_mask"].bool()
        t_start = time.time()
        logits, moe_loss = model(batch)
        logits = logits.squeeze(-1)
        probs = torch.sigmoid(logits)
        time_sum += time.time() - t_start
        # ...
```

### 7.3 潜在问题

全部 batch 都在 GPU 上会占用大量显存。如果数据总量超过可用显存，会 OOM。安全做法：先测试单个 shard 的显存占用，若不够则分批预加载。

如果显存不够，采用双缓冲策略（当前 batch 在 GPU 推理，下一个 batch 异步上传）。

### 7.4 预期收益

有限（因为数据加载本来就不计分），但能消除循环内的不必要开销。

---

## 8. 方案五：MoE 推理优化

### 8.1 合规性

✅ "剪枝"是规则明确允许的三种优化之一（量化/稀疏/剪枝）。Expert 剪枝属于模型剪枝的子类。

### 8.2 当前状态

```python
class SMoE(nn.Module):
    def __init__(self, d_model, dim_ff, num_experts=8, k=2):
        # 每层 8 个 FFN expert，每个 token 激活 2 个
```

8 层 × 8 expert = 64 个 FFN 模块。推理时大多数 token 只会路由到少数 expert。

### 8.3 优化方向

**a) Expert 负载统计 + 合并**

对少量数据做一次前向，统计各 expert 的被激活次数：

```python
expert_hits = torch.zeros(8, 8)  # [layers, experts]
model.eval()
with torch.no_grad():
    for batch in sample_batches[:10]:  # 只用 10 个 batch 采样
        batch = move_batch_to_device(batch, dev)
        for layer_idx, moe in enumerate(model.seq_encoder.moe):
            topk_idx, _, _ = moe.gate(x)  # x 需要从 forward 中间取
            for e in range(8):
                expert_hits[layer_idx, e] += (topk_idx == e).sum()
```

如果某个 expert 激活次数 < 1%，可将其权重合并到最相似的 expert（通过权重余弦相似度找最近邻），或直接移除（需验证 AUC 不掉）。

**b) 替换为静态 FFN（激进方案）**

如果发现某些层的 expert 负载极度不均衡（例如 95% token 走同一个 expert），可直接把该层的 SMoE 替换为单个 Expert：

```python
# 仅当某层 expert 负载极不均衡时
# 找到最常用的 expert
best = expert_hits[layer_idx].argmax().item()
# 替换 SMoE 为单个 Expert
model.seq_encoder.moe[layer_idx] = model.seq_encoder.moe[layer_idx].experts[best]
```

此方案**必须提交验证 AUC/PCOC 不跌破阈值**。

### 8.4 风险

- 负载统计需要从 Transformer forward 中间提取 gate 输出，需要修改 forward 添加 hook
- Expert 合并可能影响 AUC 0.001-0.005

### 8.5 预期收益

| 操作 | 加速比 |
|------|--------|
| 移除 1-2 个死 expert | 1.05x - 1.15x |
| 替换 2-3 层为单 FFN | 1.2x - 1.4x |

---

## 9. 方案六：INT8 量化（可选）

### 9.1 合规性

✅ "量化除外"，明确允许。

### 9.2 适用条件

只有在前 5 步做完后仍需进一步加速时才尝试。INT8 量化需要校准数据（可用测试集的一小部分做 calibration），有精度损失风险。

```python
import torch.quantization as quant

# 仅对 Transformer encoder 做 INT8 量化（Embedding 层跳过）
model.seq_encoder.qconfig = quant.get_default_qconfig('qnnpack')
model.seq_encoder = quant.quantize_dynamic(
    model.seq_encoder,
    {nn.Linear},  # 仅量化 Linear 层
    dtype=torch.qint8,
)
```

### 9.3 风险

- AUC 可能下降 0.005-0.02，需提交后验证
- 如果评测环境无 GPU（CPU-only），QNNPACK 才有用；GPU 下需用 TensorRT 做 INT8

---

## 10. CUDA Graph（为什么不做）

原方案中列了 CUDA Graph，审查后**移除**，理由如下：

1. CUDA Graph 要求**所有输入形状完全相同**。Baseline 数据经过 Sequence Packing，不同 batch 的序列长度差异很大，不满足这一前提
2. 每个 batch 的 `user_offsets` 长度不同，导致 mask 形状也不同
3. 若要强行使用，需要对 batch 做 padding 对齐，反而引入额外开销

**结论：不适用于本场景，放弃此方案。**

---

## 11. 部署要点

### 11.1 压缩包结构

```
submit.zip
├── infer.py              # 主推理脚本（实现所有必需接口）
├── requirements.txt      # Python 依赖列表
└── build_env.sh          # 环境构建脚本（可选但推荐）
```

提交前务必验证：
- [ ] 包内**没有** `dataset/` 目录
- [ ] 包内**没有** `ckpt.pt` 文件
- [ ] 包内**没有**多余的顶层文件夹
- [ ] 后缀是 `.zip`（或 `.tar.gz`）

### 11.2 build_env.sh 设计

```bash
#!/bin/bash
set -e
# 安装依赖（评测系统自动使用阿里云镜像）
pip install -r requirements.txt

# 预热 torch compile（如果方案三启用）
python -c "
import torch
@torch.compile(mode='max-autotune')
def _warmup(x): return x * 2
_warmup(torch.randn(100, 100, device='cuda'))
print('Inductor ready')
" 2>/dev/null || echo 'torch.compile not available, skipping'

echo "build env success"
```

### 11.3 requirements.txt（最小化）

```
torch==2.6.0
triton==3.2.0
numpy==2.2.6
scikit-learn==1.7.2
tqdm==4.67.3
```

- 去掉所有 `nvidia-*` 包（评测环境已预装 CUDA）
- 版本号精确锁定，避免安装时依赖冲突

---

## 12. 风险评估与底线策略

### 各方案对模型质量的影响

| 方案 | AUC 影响 | PCOC 影响 | 0 分风险 |
|------|----------|-----------|----------|
| 接口修复 | 无 | 无 | **必须做，否则直接 0 分** |
| FP16 | < 0.0001 | < 0.0001 | 极低 |
| Flash Attention | < 0.0001 | 无 | 极低 |
| torch.compile | 无 | 无 | 低（首次编译可能超时） |
| GPU 预加载 | 无 | 无 | 低（OOM 风险） |
| MoE 剪枝 | 0.001 - 0.005 | 微小 | 中（需提交验证） |
| INT8 | 0.005 - 0.02 | 可能偏移 | 高（可能跌破阈值） |

### 安全策略（必须遵守）

每完成一个优化：
1. 在 AI Studio 提交，拿到新得分
2. 记录本次得分变化
3. 如果 AUC < 0.65 或 PCOC 不在 [0.85, 1.15]，立即回退该优化
4. 如果得分上升，保留并进入下一步

### 保底策略

- 至少完成接口修复 + FP16，能稳定拿到 > 25 分
- 如果 Flash Attention / torch.compile 跑不通，回退不影响得分
- **在截止日期前一天（6 月 25 日）停止实验**，提交当前最优版本

---

## 13. 实施检查清单

### 第〇周（立即）
- [ ] 修复接口：`CTRTestSeqDataset`、`load_model(ckpt_path, ...)` 
- [ ] 在 AI Studio 提交一次，确认得分 > 0（验证接口正确）

### 第一轮
- [ ] FP16 量化：`model.half()` + embedding 保留 FP32
- [ ] 数据预加载到 GPU + 预转 FP16
- [ ] 提交验证

### 第二轮
- [ ] Flash Attention：替换 `scaled_dot_product`
- [ ] `torch.compile(mode="reduce-overhead")`
- [ ] `build_env.sh` 写预热逻辑
- [ ] 提交验证

### 第三轮（时间允许）
- [ ] MoE expert 负载分析 + 合并
- [ ] `torch.compile` 切换为 `"max-autotune"`
- [ ] INT8 量化评估（如果得分仍不满意）

---

## 14. 预期效果（修订）

| 阶段 | 预期推理时间 | 预期得分 | 主要贡献 |
|------|-------------|----------|----------|
| Baseline | 229s | 25.85 | — |
| 接口修复 | 229s | 25.85 | 确保能跑 |
| + FP16 + GPU 预加载 | ~120s | ~50 | 量化为主要贡献 |
| + Flash Attention | ~90s | ~60 | 长序列受益 |
| + torch.compile | ~65s | ~70 | 算子融合 |
| + MoE 优化 | ~50s | ~78 | 剪枝 |
| 极限（+INT8） | ~30s | ~87 | 有精度风险 |

---

## 附录 A：官方规则原文引用

来自赛题说明页面：
> 【由于是推理性能优化，组网不可进行策略性改动，不可对测试集进行训练】

来自 baseline notebook 第三单元：
> 选手不可对组网和相关参数进行修改。【违规为0分】
> **量化稀疏剪枝除外**

来自 `任务提交接口说明.md` §4-5：
> - 延迟阈值：300 秒。只统计模型前向时间（逐 batch 累加 model(batch) 耗时）
> - AUC ∈ [0.65, 1.0]、PCOC ∈ [0.85, 1.15]，任一不满足总分直接置 0
> - 压缩包自带 dataset/ 或 ckpt.pt → 评测失败
> - build_env.sh 超时 720 秒或返回码非 0 → 评测失败

## 附录 B：相关资源

| 资源 | 链接 |
|------|------|
| GRAB 论文 | arXiv 2602.01865 |
| HSTU 论文 | arXiv 2402.17152 (ICML 2024) |
| 官方 Baseline 项目 | AI Studio project/10186630 |
| 比赛主页 | aistudio.baidu.com/competition/detail/1461 |
| 提交结果页 | aistudio.baidu.com/competition/detail/1461/0/submit-result |
