(6) Tech-Renaissance全景图:七大模块与一条样本的完整旅程

——“一个人用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/下。下面是每个模块的职责与关键组件。

模块源码目录公开头文件核心职责
Coresrc/core/include/renaissance/core/基础类型、日志、随机数、全局配置、异常体系
Datasrc/data/include/renaissance/data/数据加载、图像解码、预处理增强、H2D双缓冲传输
Tensorsrc/tensor/include/renaissance/tensor/主机端Tensor与分布式张量描述符DTensor
Graphsrc/graph/include/renaissance/graph/BluePrint DSL、ArchPlan、Compiler、MemoryPlan、ComputationGraph、CUDA Graph捕获
Algosrc/algo/include/renaissance/algo/损失函数、优化器、学习率调度器
Tasksrc/task/include/renaissance/task/训练/推理任务门面与三阶段生命周期
Backendsrc/backend/include/renaissance/backend/算子注册、设备上下文、内存池、GraphExecutor

Core是最底层的基础设施。include/renaissance/core/types.h定义了框架所有的基础类型:Shape(统一NHWC四维)、DTypePhaseStreamKindRegion等。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搬运;DTensorDistributedTensor)则是整个框架最重要的抽象之一——它是一个纯描述符,只保存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负责固定间隔的样本,实现零竞争并发——依次执行PadRandomCropRandomRotationRandomScaleRandomErasing等增强。随后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_DATAI_B_DATA。与此同时,GPU正在计算上一批数据。传输与计算重叠,CPU不会因为等待GPU而空转,GPU也不会因为等待数据而”挨饿”。

所有进入GPU的图像张量都是NHWC布局、256字节对齐的。这在types.hdistributed_tensor.h中由utils::align_up_256DTensor的stride计算共同保证,使得cuDNN能够以最优模式访问数据。

4.4 编译:从BluePrint到可执行CUDA Graph

task.compile()内部,DeepLearningTask::on_prepare()驱动Graph模块完成一次完整的编译流水线:

  1. ArchPlan::from_blueprint()把BluePrint展开成ArchLayer序列;
  2. ArchPlan::build()执行10步标准化与融合变换,例如把cbr()标记为Conv+BN+ReLU融合单元;
  3. Compiler::compile()五阶段编译:对6个变体做形状推导、计算最大槽位、生成6个MemoryPlan、构建训练/推理ComputationGraph、对shape-only变体做图共享;
  4. GraphAtlas::build()建立6变体×33子图的O(1)索引;
  5. pre_capture()把每个子图捕获成CapturedGraph,最终实例化为cudaGraphExec_t
  6. 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_COMMUPDATE_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_AUPDATE_BN_INF_PARAMS),五个流是轨道,三个阶段是行车时刻表,一条样本的旅程就是一趟完整的列车运行。你会发现,所有后面的专题文章——MemoryPlan、DTensor、CUDA Graph、多流架构、FusedNormalization、CBR融合、NCCL分布式——都能在这两张图上找到位置。

下一篇,我们将把镜头拉近到最基础的”砖块”:张量。讲讲Tech-Renaissance为什么坚持NHWC布局、为什么要求256字节对齐、以及DTensor这个”纯描述符”设计到底意味着什么。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

ICP备案号:京ICP备2025133467号-1