——“一个人用AI如何写出比PyTorch更快的自研深度学习框架”系列文章之六
本文给大家一张Tech-Renaissance的全局地图。
为什么需要这样一张地图?因为任何一个深度学习框架,本质上都是一个横跨数学抽象、系统软件与硬件微架构的复杂系统。没有地图,你很容易陷进某一个算子、某一条CUDA kernel的细节里,忘了它为什么存在;也很容易把”训练一次迭代”简单理解为”forward + backward”,而忽略了数据从磁盘到指标输出之间究竟发生了多少件事。本文的目标很简单:先告诉你Tech-Renaissance的七大模块各自是干什么的、彼此如何依赖;再跟着一条训练样本,把它从DTS文件被读取、预处理、H2D传输、前向传播、反向传播、梯度通信、优化器更新,直到指标输出的完整旅程走一遍。这样,后面每一篇专题文章你都能在这张地图上找到自己的位置。
一、深度学习框架为什么需要”模块地图”
一个现代深度学习框架,如果把它剖开来看,至少要同时解决以下几类问题:
- 张量与内存:怎么表示多维数据、怎么管理设备内存、怎么保证不同硬件上的布局一致;
- 算子与自动微分:怎么实现前向算子、怎么自动推导反向梯度;
- 计算图与编译:怎么把模型描述转换成可执行计划、怎么做内存规划和图优化;
- 数据管线:怎么从磁盘高效读取样本、怎么做预处理增强、怎么喂给GPU;
- 训练算法:损失函数、优化器、学习率调度器怎么配置、怎么与图执行结合;
- 运行时与硬件抽象:怎么调用CUDA/cuDNN/NCCL、怎么管理流和同步;
- 任务生命周期:怎么把以上所有东西组织成一个完整的训练或推理任务。
这些问题彼此耦合,但解决的问题类别不同。如果把它们全部揉进同一个大文件里,框架很快会变得不可维护;如果分得太碎,模块之间又会因为接口不匹配而相互拖累。因此,几乎所有主流框架在演进过程中都会逐渐形成某种模块化边界。PyTorch有ATen(张量/算子)、TorchScript/FX(图)、Autograd(微分)、c10d(分布式)、DataLoader(数据)等分层;TensorFlow也有Runtime、Graph、XLA、Kernel、Distributed等概念。只不过这些框架经历了十几年的迭代与合并,很多边界已经不那么”干净”,模块之间为了兼容历史接口而存在大量胶水代码。
Tech-Renaissance作为从零开始设计的框架,有机会把模块边界画得更清晰一些。它的目标不是”再做一个PyTorch”,而是围绕”静态图编译 + CUDA Graph全捕获 + 运行时零动态内存”这一核心假设,把职责切到最合适的地方。结果是七个模块:Core、Data、Tensor、Graph、Algo、Task、Backend。
二、七大模块:一张清晰的职责表
Tech-Renaissance的源码目录与模块一一对应:src/core/、src/data/、src/tensor/、src/graph/、src/algo/、src/task/、src/backend/。公开头文件则统一放在include/renaissance/下。下面是每个模块的职责与关键组件。
| 模块 | 源码目录 | 公开头文件 | 核心职责 |
|---|---|---|---|
| Core | src/core/ | include/renaissance/core/ | 基础类型、日志、随机数、全局配置、异常体系 |
| Data | src/data/ | include/renaissance/data/ | 数据加载、图像解码、预处理增强、H2D双缓冲传输 |
| Tensor | src/tensor/ | include/renaissance/tensor/ | 主机端Tensor与分布式张量描述符DTensor |
| Graph | src/graph/ | include/renaissance/graph/ | BluePrint DSL、ArchPlan、Compiler、MemoryPlan、ComputationGraph、CUDA Graph捕获 |
| Algo | src/algo/ | include/renaissance/algo/ | 损失函数、优化器、学习率调度器 |
| Task | src/task/ | include/renaissance/task/ | 训练/推理任务门面与三阶段生命周期 |
| Backend | src/backend/ | include/renaissance/backend/ | 算子注册、设备上下文、内存池、GraphExecutor |
Core是最底层的基础设施。include/renaissance/core/types.h定义了框架所有的基础类型:Shape(统一NHWC四维)、DType、Phase、StreamKind、Region等。Region把整个显存划分为68 个语义分区(枚举槽位共 69 个,含一个边界哨兵)——权重区、梯度区、一阶/二阶动量区、EMA区、AMP FP16权重区、输入缓冲区、特征图区、临时张量区、结果区等等,这是后续MemoryPlan静态显存规划的根基。Core还提供基于Philox4x32-10的可复现随机数生成器,以及全局配置单例GlobalRegistry。
Data负责”把数据从磁盘搬到GPU输入缓冲区”。它定义了DataLoader抽象基类,为MNIST、CIFAR、ImageNet分别提供DTS格式和RAW格式的加载器;Preprocessor负责配置多线程预处理流水线、数据增强变换和FusedNormalization——后者将ToTensor、随机水平翻转、Normalize、随机擦除等多个原本独立的预处理步骤,融合为单次内存遍历,消除中间结果的反复读写开销;TransferStation维护A/B双缓冲,把CPU侧已经处理好的batch通过锁页内存异步拷贝到GPU的I_A_DATA / I_B_DATA区。
Tensor提供两种张量抽象。主机端Tensor是一个紧凑的NHWC数据容器,用于H2D/D2H搬运;DTensor(DistributedTensor)则是整个框架最重要的抽象之一——它是一个纯描述符,只保存shape / dtype / region / offset / stride / slot_bytes,不持有任何实际内存。所有DTensor的offset由MemoryPlan在编译期确定,同一张布局图纸可以在多张GPU上共享执行。
Graph是静态图编译的核心。BluePrint提供声明式DSL,让用户用seq(fc(1024), relu(), ...)这种方式描述模型;ArchPlan把BluePrint展开成标准化的ArchLayer序列,并执行10步标准化与融合变换(如将cbr()标记为Conv+BN+ReLU融合单元);Compiler执行五阶段编译管线——形状推导、最大槽位计算、MemoryPlan生成、ComputationGraph构建、图共享去重;MemoryPlan基于68 个命名 Region(枚举槽位共 69 个,含边界哨兵)做静态显存布局;ComputationGraph是”零形状信息”的纯拓扑容器,33个GraphId桶各存一个节点序列;GraphAtlas管理6个变体×33个子图的O(1)索引映射;CapturedGraph负责把子图捕获成CUDA Graph或CPU回退队列。
Algo是训练算法的配置层。它提供CrossEntropyLoss(支持label smoothing)、SGD / SGD-Momentum / LARS / Adam / AdamW优化器,以及PolynomialLR、CosineAnnealingLR、StepLR等多种学习率调度器。这里有一个值得注意的设计:优化器构造函数不接受学习率,学习率由Scheduler唯一管理,从而把”优化算法”和”学习率策略”干净地分开。这些配置在编译期被嵌入计算图,运行期不再有Python级别的调度开销。
Task是用户-facing的门面。TaskBase强制三阶段状态机;SimpleTask面向手动构图的测试与研究场景;DeepLearningTask封装了完整的训练循环,包括验证、EMA、早停、指标收集和模型保存。
Backend是执行引擎。DeviceContext管理每个rank的CUDA流(TRANS、COMP_1、COMP_2、COMP_3、UPDATE)、cuDNN/cuBLAS句柄、workspace和NCCL通信器;MemoryArena负责全局显存/内存池的一次性分配;OpRegistry做算子分发;GraphExecutor则在运行期按照预捕获的图集调度每个训练步骤。
七个模块的依赖是从上到下的单向链:
Task → Graph → Tensor → Core<br>Task → Algo → Core<br>Task → Data → Core<br>Task → Backend → Core
Task是总调度者,它把Graph(编译好的图)、Algo(算法配置)、Data(数据管线)、Backend(执行引擎)串在一起。Core是所有模块的共同底座。
三、三阶段状态机:从”规划”到”锁定”再到”执行”
在主流动态图框架里,用户随时可以改变模型结构、插入新张量、调用.backward();运行时则按需分配内存。Tech-Renaissance既然选择了静态图编译,就必须在运行前把所有东西确定下来。为此,框架在include/renaissance/core/types.h里定义了一个非常明确的状态机:
enum class Phase : uint8_t { PLANNING, MEMORY_LOCKED, COMPILED };
这三个阶段定义了框架从”配置”到”冻结”再到”运行”的完整生命周期,阶段之间只能单向推进,不可逆。
在PLANNING阶段,用户配置一切:模型结构(BluePrint)、损失函数、优化器、调度器、训练epoch数、验证频率、早停阈值等。框架内部会调用alloc()在MemoryPlan中为每个参数、缓冲区、临时张量预留槽位。此阶段可以调用的API是受限的:只能配置,不能执行。任何试图在PLANNING阶段之外修改配置的操作都会触发异常。
当用户调用finalize_memory()后,进入MEMORY_LOCKED阶段。所有张量的offset和大小都被固定,不能再动态分配。随后compile()被调用,框架执行完整的编译流水线:ArchPlan::from_blueprint()将BluePrint展开为层序列→ArchPlan::build()执行10步标准化与融合变换→Compiler::compile()五阶段编译→MemoryPlan::finalize()锁定所有DTensor的偏移→GraphAtlas::build()填充6变体×33槽位的索引表→CUDA Graph捕获开始。
捕获完成后,框架进入COMPILED阶段。此时所有图都已固化,run()方法被调用时,框架只是在一个循环中按预定义的执行顺序通过cudaGraphLaunch提交子图。运行期不再有任何CUDA kernel的单个launch、不再有任何显存的动态分配、不再有任何Python级别的调度。
这个状态机不是装饰性的。它从根本上防止了”运行期偷偷malloc一块显存”或”临时插入一张新图”这类会破坏CUDA Graph地址稳定性的行为。对Tech-Renaissance来说,编译期确定一切不是口号,而是由状态机强制执行的设计约束。
四、一条样本的完整旅程
有了模块地图和状态机,我们就可以跟着一条MNIST样本走一遍。示例来自tests/example/mlp_mnist.cpp:
#include "renaissance.h"
using namespace tr;
int main() {
GLOBAL_SETTING
.use_gpu("0")
.amp(true)
.manual_seed(123)
.global_batch_size(200)
.input_resolution(28);
PREPROCESSOR_SETTING
.dataset("mnist", std::string(TR_PROJECT_ROOT) + "/data/mnist")
.download(true)
.preprocess_workers(8)
.normalization(NormMode::MNIST)
.train_transforms(
Pad(2), RandomCrop(28), RandomRotation(20.0f, 0),
RandomScale(0.8f, 1.2f), RandomErasing(0.5f))
.commit();
BluePrint mlp = seq(
fc(1024, true), relu(),
fc(512, true), relu(),
fc(256, true), relu(),
fc(10, true)
);
DeepLearningTask task;
task.model(mlp)
.loss(CrossEntropyLoss().label_smoothing(0.1f))
.total_epochs(100)
.optimizer(AdamW().weight_decay(1e-4f))
.scheduler(CosineAnnealingLR().base_lr(0.001f).warmup(5));
task.compile();
auto result = task.run();
}
这几十行代码的背后,是一条样本从磁盘到指标输出的完整旅程。
4.1 配置阶段:一切在PLANNING阶段完成
GLOBAL_SETTING进入GlobalRegistry,把GPU选择、AMP开关、随机种子、全局batch size、输入分辨率等全局参数固定下来。PREPROCESSOR_SETTING进入Preprocessor,配置数据集路径、worker数量、归一化模式和训练增强变换。BluePrint进入Graph模块,把模型描述成一棵Layer树。DeepLearningTask的链式调用则把模型、损失、优化器、调度器绑定在一起。
这些全部发生在PLANNING阶段。此时还没有真正分配GPU显存,所有张量只是MemoryPlan里的待分配槽位。
4.2 数据读取与预处理:Data模块
当task.run()开始执行epoch循环时,Data模块开始工作。DataLoader::get_next_sample()从DTS文件(或RAW文件)中读取一个原始样本。Preprocessor的持久worker池按照预先分配的静态顺序领取样本——每个Worker负责固定间隔的样本,实现零竞争并发——依次执行Pad、RandomCrop、RandomRotation、RandomScale、RandomErasing等增强。随后FusedNormalization被自动注入,把uint8→float/FP16、归一化、随机水平翻转、随机擦除等多个步骤融合成单次内存遍历。
注意,所有这些随机操作都使用Core模块的Philox计数器式RNG。由于Philox是无状态的、可跳转到任意offset的算法,即使开200多个预处理线程,只要seed固定,样本增强结果仍然字节级可复现。
4.3 H2D传输:TransferStation与A/B双缓冲
预处理好的样本被写入TransferStation的A/B双缓冲主机侧槽位。TransferStation维护两块锁页内存(pinned memory),当一块被CPU worker填满后,框架会触发一次异步H2D拷贝,把数据搬到GPU输入区I_A_DATA或I_B_DATA。与此同时,GPU正在计算上一批数据。传输与计算重叠,CPU不会因为等待GPU而空转,GPU也不会因为等待数据而”挨饿”。
所有进入GPU的图像张量都是NHWC布局、256字节对齐的。这在types.h和distributed_tensor.h中由utils::align_up_256和DTensor的stride计算共同保证,使得cuDNN能够以最优模式访问数据。
4.4 编译:从BluePrint到可执行CUDA Graph
在task.compile()内部,DeepLearningTask::on_prepare()驱动Graph模块完成一次完整的编译流水线:
ArchPlan::from_blueprint()把BluePrint展开成ArchLayer序列;ArchPlan::build()执行10步标准化与融合变换,例如把cbr()标记为Conv+BN+ReLU融合单元;Compiler::compile()五阶段编译:对6个变体做形状推导、计算最大槽位、生成6个MemoryPlan、构建训练/推理ComputationGraph、对shape-only变体做图共享;GraphAtlas::build()建立6变体×33子图的O(1)索引;pre_capture()把每个子图捕获成CapturedGraph,最终实例化为cudaGraphExec_t;GraphExecutor拿到捕获结果,在运行期按GraphId调度执行。
这一步完成后,所有张量的offset、所有kernel的序列、所有流之间的依赖都已经确定。运行期不再需要cudaMalloc,也不再需要动态图调度。
4.5 训练步骤:GraphExecutor的精确调度
一条样本真正参与计算,是在GraphExecutor::run_train_step()中。下面是src/backend/graph_executor.cpp里的核心调度逻辑:
void GraphExecutor::run_train_step() {
update_lr_scalar();
GraphId xfer_gid = ab_toggle_ ? GraphId::TRANSFER_A : GraphId::TRANSFER_B;
GraphId first_fwd_gid = ab_toggle_ ? GraphId::FIRST_LAYER_FWD_A : GraphId::FIRST_LAYER_FWD_B;
launch(xfer_gid); // 1. H2D传输
launch(GraphId::ZERO_GRAD); // 2. 梯度区清零
launch(first_fwd_gid); // 3. 首层前向
GraphId next_xfer = ab_toggle_ ? GraphId::TRANSFER_B : GraphId::TRANSFER_A;
launch_dual(next_xfer, GraphId::DEEP_FWD_BWD); // 4. 下一batch传输 + 深层前向反向 并行
sync_all();
if (!skip_first_bwd_) {
launch(GraphId::FIRST_LAYER_BWD_A); // 5. 首层反向
}
sync_all();
launch(GraphId::CAST_DEEP_GRAD_FP16_TO_FP32); // 6. AMP梯度类型转换
launch(GraphId::DEEP_COMM); // 7. 深层梯度AllReduce
sync_all();
launch(GraphId::CAST_FIRST_GRAD_FP16_TO_FP32);
if (!skip_first_bwd_) {
launch(GraphId::FIRST_COMM); // 8. 首层梯度AllReduce
}
launch(GraphId::NAN_CHECK_AND_GRAD_SCALING); // 9. NaN检查 + 梯度缩放
launch(GraphId::STATS_COMM); // 10. BN统计量同步
launch(GraphId::UPDATE_STATS); // 11. BN统计量推进
if (!check_nan_flag()) {
launch(GraphId::OPTIMIZER); // 12. 优化器更新
launch(GraphId::EMA_UPDATE); // 13. EMA更新
}
launch_dual(GraphId::CAST_MAIN_FP32_TO_FP16,
GraphId::CAST_EMA_FP32_TO_FP16); // 14. 权重FP32→FP16
sync_all();
toggle_ab(); // 15. 切换A/B缓冲
}
这段代码把一次训练迭代的所有关键阶段都串了起来:
- H2D传输(
TRANSFER_A/B)把下一batch的数据从CPU搬到GPU,在TRANS流上执行; - ZERO_GRAD一次性把梯度区清零——得益于MemoryPlan的语义分区设计,梯度区是连续的一块显存,清零操作只需一次kernel调用,而非逐参数、逐层地分散清零;
- 首层前向(
FIRST_LAYER_FWD_*)与深层前反向(DEEP_FWD_BWD)分离,是因为首层卷积往往需要与数据转换、梯度通信做特殊调度; - 双图并行(
launch_dual)让下一batch的传输与当前batch的深层计算同时跑在不同流上,实现传输与计算的重叠; - CAST阶段把FP16梯度转成FP32,以便优化器在FP32主权重上更新;
- COMM阶段通过NCCL做AllReduce,同步多卡梯度,在UPDATE流上执行;
- NAN_CHECK_AND_GRAD_SCALING做AMP的NaN检测与梯度缩放;
- STATS_COMM与UPDATE_STATS负责BN统计量的跨卡同步与本地推进;
- OPTIMIZER阶段根据配置调度SGD / Adam / AdamW / LARS等融合更新算子;
- 最后CAST_MAIN_FP32_TO_FP16把更新后的FP32权重拷回FP16计算权重,并切换A/B缓冲。
整个过程中,五个CUDA流(TRANS、COMP_1、COMP_2、COMP_3、UPDATE)各自跑在不同的硬件队列上,它们之间的同步关系在编译期就已经通过事件(event)插入到图中。CPU只负责按顺序launch已经捕获好的CUDA Graph;真正的GPU工作由cudaGraphLaunch一次性提交。运行期不再有任何动态的事件等待或流同步——因为连”什么时候等谁”这件事,都已经被编译期决定了。这正是Tech-Renaissance能在训练吞吐上明显领先的关键之一。
4.6 指标输出:从R_RESULT到屏幕
损失函数(SOFTMAX_CE_AMP_FWD/BWD)的输出被写到R_RESULT区,每个batch的loss、top1正确数、top5正确数再累加到R_RESULT_ACCUMULATED。一个epoch结束时,DeepLearningTask从rank 0取回累积结果,除以样本数,得到epoch loss和准确率,最后打印到屏幕。
五、旅程中的几个关键设计印记
走完这条路径,你应该能感受到几个贯穿始终的设计选择:
- DTensor是描述符,不是容器:它不持有内存,只描述
(region, offset, shape, stride)。这让同一张MemoryPlan可以在多卡间共享,也让跨变体的图复用成为可能。 - 68-Region显存布局(含哨兵共 69 个槽位):所有显存按语义分区静态规划,运行时不再
cudaMalloc。这消除了分配延迟和碎片,也为CUDA Graph全捕获提供了地址稳定性。 - 多流并发:TRANS / COMP_1 / COMP_2 / COMP_3 / UPDATE五条非阻塞流分工明确,传输、计算、通信、更新尽可能重叠。
- CUDA Graph全捕获:从H2D传输到优化器更新,关键环节都被捕获成可重放的图,CPU派发开销被压到极低。
- 可复现性:Philox RNG + 静态图 + 确定性引擎选择,让同一程序在同一平台上多次运行可以得到字节级一致的结果。
这些不是孤立的技术点,而是同一条设计主线上的不同侧面:在编译期尽可能多地确定一切,在运行期只做最少的事。
六、小结
本文没有深入任何单一模块的实现细节,而是先用七大模块给你一张Tech-Renaissance的”行政区划图”,再用一条样本的完整旅程给你一条”交通路线图”。你可以把七大模块看作七条线路,33个GraphId是站点(从TRANSFER_A到UPDATE_BN_INF_PARAMS),五个流是轨道,三个阶段是行车时刻表,一条样本的旅程就是一趟完整的列车运行。你会发现,所有后面的专题文章——MemoryPlan、DTensor、CUDA Graph、多流架构、FusedNormalization、CBR融合、NCCL分布式——都能在这两张图上找到位置。
下一篇,我们将把镜头拉近到最基础的”砖块”:张量。讲讲Tech-Renaissance为什么坚持NHWC布局、为什么要求256字节对齐、以及DTensor这个”纯描述符”设计到底意味着什么。
