深色模式
复现论文与做实验
为什么要先复现
复现是进入研究最快的方式:你会真正理解方法的细节,会拿到一套能跑的代码和实验环境,还经常会发现论文没写的问题,这些问题往往就是你的研究起点。
选哪篇来复现
进组后的第一篇通常是组内论文,三种复现方式(运行官方代码、自己写仿真器、搭真实系统)见复现组内论文。
选其他论文时,优先选满足这些条件的:
- 和你的方向最相关,最好是组内论文或组内论文的对比对象;
- 有官方开源代码,且仓库近一两年有人维护;
- 实验需要的硬件你能拿到(先看论文用了几张什么卡);
- 依赖的模型和数据集能下载到。
复现检查清单
开始前
- 读完论文第三遍,列出要复现的实验(通常是主实验的一张表或一张图)。
- 读代码仓库的 README、Issues,看别人复现时遇到过什么问题。
- 记下论文用的硬件、软件版本、模型、数据集、超参数。
过程中
- 用独立的 Python 环境或 Docker 容器,记录所有依赖的版本(
uv pip freeze > requirements.lock或保存 Dockerfile),见 Python 环境。 - 固定随机种子。
- 先用小规模配置跑通整个流程,再跑完整实验。
- 每次运行都记录:代码的 Git commit、配置、开始时间、结果文件位置。
- 遇到报错和解决方法立刻记下来。
结束后
- 把你的结果和论文的数字放在同一张表里对比。
- 差距能解释清楚,就算复现成功。同样的硬件和设置下,准确率等指标的差距通常在几个百分点以内;硬件不同时比较加速比、相对提升,而不是绝对延迟。差距大要分析原因:硬件不同?版本不同?超参数没写全?
- 写复现报告:环境、步骤、结果对比、差距分析、发现的问题和改进点。
从 Jupyter 到实验脚本
Jupyter Notebook 适合探索和画图,但正式实验要写成脚本:脚本能放进 tmux 长时间运行、能用不同参数批量运行、能被 Git 准确记录。建议的项目结构:
text
my-experiment/
├── README.md # 怎么安装、怎么运行
├── pyproject.toml # 依赖(uv 管理)
├── configs/ # 实验配置
│ └── baseline.yaml
├── src/ # 核心代码
├── scripts/
│ ├── run.py # 运行一次实验
│ └── plot.py # 从结果生成图
├── results/ # 原始结果(不提交到 Git)
├── figures/ # 生成的图
└── notebooks/ # 探索用的 notebook一个实验脚本模板
python
# scripts/run.py
import argparse
import json
import random
import subprocess
import time
from pathlib import Path
import numpy as np
import torch
def set_seed(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
def git_commit():
try:
return subprocess.check_output(["git", "rev-parse", "--short", "HEAD"], text=True).strip()
except Exception:
return "unknown"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--method", default="baseline")
parser.add_argument("--batch-size", type=int, default=8)
parser.add_argument("--repeats", type=int, default=3)
parser.add_argument("--seed", type=int, default=0)
parser.add_argument("--out", default="results")
args = parser.parse_args()
set_seed(args.seed)
run_dir = Path(args.out) / f"{args.method}_bs{args.batch_size}_{time.strftime('%Y%m%d-%H%M%S')}"
run_dir.mkdir(parents=True)
# 保存这次运行的全部配置,之后能准确知道每个结果是怎么来的
meta = vars(args) | {"commit": git_commit(), "gpu": torch.cuda.get_device_name(0) if torch.cuda.is_available() else "cpu"}
(run_dir / "config.json").write_text(json.dumps(meta, indent=2, ensure_ascii=False))
records = []
for r in range(args.repeats):
start = time.perf_counter()
# ===== 在这里运行你要测量的东西 =====
time.sleep(0.1)
# ==================================
records.append({"repeat": r, "latency_s": time.perf_counter() - start})
# 保存原始数据,画图和统计都从这里读
with open(run_dir / "records.jsonl", "w") as f:
for rec in records:
f.write(json.dumps(rec) + "\n")
print(f"结果已保存到 {run_dir}")
if __name__ == "__main__":
main()运行:python scripts/run.py --method baseline --batch-size 16。批量跑多组参数时写一个 shell 循环,放在 tmux 里运行。
做实验的好习惯
代码与配置
- 所有代码都放进 Git,每次正式实验前先 commit。
- 超参数写在配置文件(YAML)或命令行参数里,不要改代码里的常量来调参。
- 模型权重、数据集、实验输出不要放进 Git,在
.gitignore里排除。
记录与结果
- 用 TensorBoard 或 Weights & Biases 记录训练曲线。
- 原始结果(每次请求的延迟、每步的耗时)都保存成 CSV 或 JSON,画图和统计用脚本从原始数据生成。
- 不要手工修改任何实验数字。
- 画图脚本也放进 Git,论文里每张图都能一键重新生成。
系统实验的特殊要求
- 预热:第一次运行包含编译、加载、缓存冷启动,测性能前先跑几轮预热。
- 多次测量:每个配置至少跑 3 次,报告平均值和波动(标准差或误差棒)。
- 看尾延迟:除了平均延迟,还要看 P90、P99。用户感受到的卡顿往往来自尾延迟。
- GPU 计时要同步:PyTorch 的 GPU 操作是异步的,计时前后要调用
torch.cuda.synchronize()。 - 控制干扰:测性能时确认 GPU 上没有别人的任务(
nvidia-smi),记录 GPU 型号和驱动版本。 - 公平对比:baseline 和你的方法用同样的硬件、模型、负载,并且 baseline 要调到它的最佳配置。
统计与画图
从原始数据算统计量:
python
import json
import numpy as np
lat = np.array([json.loads(line)["latency_s"] for line in open("results/xxx/records.jsonl")])
print(f"平均 {lat.mean() * 1000:.1f} ms,标准差 {lat.std() * 1000:.1f} ms")
print(f"P50 {np.percentile(lat, 50) * 1000:.1f} ms,P99 {np.percentile(lat, 99) * 1000:.1f} ms")画论文用的图(矢量 PDF、字体够大、带误差棒):
python
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams.update({"font.size": 12, "pdf.fonttype": 42}) # fonttype 42 避免投稿时的字体问题
rates = [1, 2, 4, 8, 16]
baseline = np.array([[120, 125, 118], [130, 128, 135], [160, 170, 165], [260, 280, 270], [900, 950, 920]])
ours = np.array([[100, 102, 98], [105, 108, 104], [118, 120, 121], [150, 158, 155], [320, 340, 330]])
fig, ax = plt.subplots(figsize=(4.5, 3))
for name, data, marker in [("Baseline", baseline, "o"), ("Ours", ours, "s")]:
ax.errorbar(rates, data.mean(axis=1), yerr=data.std(axis=1), marker=marker, capsize=3, label=name)
ax.set_xlabel("Request rate (req/s)")
ax.set_ylabel("P99 TTFT (ms)")
ax.set_xscale("log", base=2)
ax.grid(alpha=0.3)
ax.legend()
fig.tight_layout()
fig.savefig("figures/ttft_vs_rate.pdf")图中的数字是示例。论文里的图通常一栏宽约 3.5 英寸,坐标轴标签要写单位。
负载与数据集
系统实验需要真实的请求负载。常用的公开数据:
| 名称 | 内容 |
|---|---|
| ShareGPT | 真实对话,常用于构造 LLM 请求的输入输出长度 |
| Azure LLM 推理 trace | Azure 上 LLM 服务的请求到达时间和 token 长度 |
| BurstGPT | 真实 LLM 服务的请求 trace,包含突发流量 |
| Alibaba 集群 trace | 阿里巴巴集群的任务与资源使用数据,常用于调度研究 |
| Google 集群 trace | Google Borg 集群数据 |
扩散模型方向常用 MS-COCO 的文本描述作为提示词,视频生成常用 VBench 的提示词集和评测。
消融实验与敏感性分析
一个方法通常由好几个设计组成。消融实验逐个去掉某个设计,证明每个设计都有用。敏感性分析改变关键参数(负载强度、模型大小、缓存阈值等),说明方法在不同条件下都有效、有什么适用边界。这两类实验是审稿人一定会看的。