From ee6cbb3c3cdc889830c8ca0b66bfba1f10a649a0 Mon Sep 17 00:00:00 2001 From: CuiLingyunCrispy Date: Sun, 6 Sep 2026 23:14:03 +0800 Subject: [PATCH 1/4] feat: add custom pipeline layer partition layout --- README.md | 309 +++++++++--------- docs/pipeline_layout_guide.md | 134 ++++++++ example/gpt2/checkpoint_loader.cc | 16 +- example/gpt2/main.cc | 9 +- example/llama3/checkpoint_loader.cc | 18 +- example/llama3/main.cc | 13 +- .../nn/modules/transformer/transformer.h | 4 +- .../modules/transformer/transformer_config.h | 1 - infini_train/include/nn/parallel/global.h | 12 +- .../include/nn/parallel/pp/pipeline_layout.h | 52 +++ .../nn/parallel/pp/pipeline_parallel.h | 15 +- .../src/nn/modules/transformer/transformer.cc | 14 +- .../modules/transformer/transformer_config.cc | 10 - infini_train/src/nn/parallel/global.cc | 9 +- .../src/nn/parallel/pp/pipeline_parallel.cc | 46 +-- .../src/nn/parallel/pp/pipeline_schedule.cc | 9 +- tests/distributed/CMakeLists.txt | 5 + tests/distributed/test_pipeline_layout.cc | 117 +++++++ 18 files changed, 547 insertions(+), 246 deletions(-) create mode 100644 docs/pipeline_layout_guide.md create mode 100644 infini_train/include/nn/parallel/pp/pipeline_layout.h create mode 100644 tests/distributed/test_pipeline_layout.cc diff --git a/README.md b/README.md index abd8070b2..4b2b9073e 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Build Options: > Both options are optional and can be disabled for CPU-only builds. -## ✨ InfiniTrain Overview +## ✨ InfiniTrain Overview ### ✔ Support Matrix @@ -96,160 +96,160 @@ For example, the `llama3` example produces a binary named `llama3`. To view available runtime options: -```bash -./build/llama3 --help +```bash +./build/llama3 --help +``` + +### Getting Started + +#### Prepare Datasets and Weights + +Run the asset preparation script from the repository root. Prepared files are +written to `data/` by default. + +```bash +# MNIST dataset +./scripts/assets/prepare-infinitrain-assets.sh mnist + +# GPT-2 124M weights, tokenizer, and tokenized TinyShakespeare data +./scripts/assets/prepare-infinitrain-assets.sh gpt2 + +# LLaMA 3.2 1B weights and tokenized TinyShakespeare data +HF_TOKEN=hf_xxx ./scripts/assets/prepare-infinitrain-assets.sh llama3 +``` + +Preparing LLaMA requires access to the gated +`meta-llama/Llama-3.2-1B` repository. Accept its license on Hugging Face and +provide `HF_TOKEN`, or authenticate with `hf auth login`, before running the +command. The complete LLaMA preparation requires approximately 8.5 GB of free +disk space, including the downloaded checkpoint and converted FP32 weights. + +Use `DATA_DIR` to write the assets elsewhere, or prepare all supported assets +in one invocation: + +```bash +DATA_DIR=/path/to/data \ +HF_TOKEN=hf_xxx \ +./scripts/assets/prepare-infinitrain-assets.sh all +``` + +#### Model Examples + +The generated files can be passed directly to the corresponding executables: + +##### MNIST + +```bash +./build/mnist \ + --device cpu \ + --dataset data/mnist +``` + +##### GPT-2 124M + +```bash +./build/gpt2 \ + --device cuda \ + --input_bin data/gpt2/tiny_shakespeare_train.bin \ + --input_val_bin data/gpt2/tiny_shakespeare_val.bin \ + --tokenizer_bin data/gpt2/gpt2_tokenizer.bin \ + --llmc_filepath data/gpt2/gpt2_124M.bin \ + --num_iteration 10 +``` + +##### LLaMA 3.2 1B + +```bash +./build/llama3 \ + --device cuda \ + --input_bin data/llama3/tiny_shakespeare_train.bin \ + --input_val_bin data/llama3/tiny_shakespeare_val.bin \ + --llmc_filepath data/llama3/llama3.2_1B_fp32.bin \ + --num_iteration 10 ``` -### Getting Started - -#### Prepare Datasets and Weights - -Run the asset preparation script from the repository root. Prepared files are -written to `data/` by default. - -```bash -# MNIST dataset -./scripts/assets/prepare-infinitrain-assets.sh mnist - -# GPT-2 124M weights, tokenizer, and tokenized TinyShakespeare data -./scripts/assets/prepare-infinitrain-assets.sh gpt2 - -# LLaMA 3.2 1B weights and tokenized TinyShakespeare data -HF_TOKEN=hf_xxx ./scripts/assets/prepare-infinitrain-assets.sh llama3 -``` - -Preparing LLaMA requires access to the gated -`meta-llama/Llama-3.2-1B` repository. Accept its license on Hugging Face and -provide `HF_TOKEN`, or authenticate with `hf auth login`, before running the -command. The complete LLaMA preparation requires approximately 8.5 GB of free -disk space, including the downloaded checkpoint and converted FP32 weights. - -Use `DATA_DIR` to write the assets elsewhere, or prepare all supported assets -in one invocation: - -```bash -DATA_DIR=/path/to/data \ -HF_TOKEN=hf_xxx \ -./scripts/assets/prepare-infinitrain-assets.sh all -``` - -#### Model Examples - -The generated files can be passed directly to the corresponding executables: - -##### MNIST - -```bash -./build/mnist \ - --device cpu \ - --dataset data/mnist -``` - -##### GPT-2 124M - -```bash -./build/gpt2 \ - --device cuda \ - --input_bin data/gpt2/tiny_shakespeare_train.bin \ - --input_val_bin data/gpt2/tiny_shakespeare_val.bin \ - --tokenizer_bin data/gpt2/gpt2_tokenizer.bin \ - --llmc_filepath data/gpt2/gpt2_124M.bin \ - --num_iteration 10 -``` - -##### LLaMA 3.2 1B - -```bash -./build/llama3 \ - --device cuda \ - --input_bin data/llama3/tiny_shakespeare_train.bin \ - --input_val_bin data/llama3/tiny_shakespeare_val.bin \ - --llmc_filepath data/llama3/llama3.2_1B_fp32.bin \ - --num_iteration 10 -``` - -### Launch Modes - -GPT-2 and LLaMA training support both thread-based and process-based launches. -The examples below use LLaMA, but the same launch modes also apply to GPT-2. - -#### Direct Launch - -Running a model executable directly uses one process and one device by default. -Set `--nthread_per_process` to use multiple execution threads and devices in the -same process: - -```bash -./build/llama3 \ - --device cuda \ - --input_bin data/llama3/tiny_shakespeare_train.bin \ - --llmc_filepath data/llama3/llama3.2_1B_fp32.bin \ - --nthread_per_process 8 \ - --num_iteration 10 -``` - -#### Single-node Multi-process Launch - -Use `infini_run` to start multiple training processes on one node. Each process -uses one execution thread by default: - -```bash -./build/infini_run \ - --nnodes=1 \ - --nproc_per_node=8 \ - ./build/llama3 \ - --device cuda \ - --input_bin data/llama3/tiny_shakespeare_train.bin \ - --llmc_filepath data/llama3/llama3.2_1B_fp32.bin \ - --num_iteration 10 -``` - -#### Multi-node Multi-process Launch - -Run the following command on every node with the same rendezvous settings and -a distinct `node_rank`: - -```bash -./build/infini_run \ - --nnodes=2 \ - --nproc_per_node=4 \ - --node_rank=[rank_id] \ - --rdzv_endpoint=[master_addr]:29500 \ - --rdzv_id=[job_id] \ - ./build/llama3 \ - --device cuda \ - --input_bin data/llama3/tiny_shakespeare_train.bin \ - --llmc_filepath data/llama3/llama3.2_1B_fp32.bin \ - --num_iteration 10 \ - --tensor_parallel 2 \ - --pipeline_parallel 2 \ - --sequence_parallel -``` - -`--nproc_per_node` and `--nthread_per_process` can be combined. The total -training world size is: - -```text -world_size = nnodes × nproc_per_node × nthread_per_process -``` +### Launch Modes + +GPT-2 and LLaMA training support both thread-based and process-based launches. +The examples below use LLaMA, but the same launch modes also apply to GPT-2. + +#### Direct Launch + +Running a model executable directly uses one process and one device by default. +Set `--nthread_per_process` to use multiple execution threads and devices in the +same process: + +```bash +./build/llama3 \ + --device cuda \ + --input_bin data/llama3/tiny_shakespeare_train.bin \ + --llmc_filepath data/llama3/llama3.2_1B_fp32.bin \ + --nthread_per_process 8 \ + --num_iteration 10 +``` + +#### Single-node Multi-process Launch + +Use `infini_run` to start multiple training processes on one node. Each process +uses one execution thread by default: + +```bash +./build/infini_run \ + --nnodes=1 \ + --nproc_per_node=8 \ + ./build/llama3 \ + --device cuda \ + --input_bin data/llama3/tiny_shakespeare_train.bin \ + --llmc_filepath data/llama3/llama3.2_1B_fp32.bin \ + --num_iteration 10 +``` + +#### Multi-node Multi-process Launch + +Run the following command on every node with the same rendezvous settings and +a distinct `node_rank`: + +```bash +./build/infini_run \ + --nnodes=2 \ + --nproc_per_node=4 \ + --node_rank=[rank_id] \ + --rdzv_endpoint=[master_addr]:29500 \ + --rdzv_id=[job_id] \ + ./build/llama3 \ + --device cuda \ + --input_bin data/llama3/tiny_shakespeare_train.bin \ + --llmc_filepath data/llama3/llama3.2_1B_fp32.bin \ + --num_iteration 10 \ + --tensor_parallel 2 \ + --pipeline_parallel 2 \ + --sequence_parallel +``` + +`--nproc_per_node` and `--nthread_per_process` can be combined. The total +training world size is: + +```text +world_size = nnodes × nproc_per_node × nthread_per_process +``` ### Parallelism Strategies -#### Distributed Data Parallelism (DDP) - -For a direct launch with TP and PP disabled, the following starts eight -data-parallel workers in one process: - -```bash ---nthread_per_process 8 # 8-way DDP when TP=1 and PP=1 -``` - -For all launch modes, the data-parallel size is derived from the total world -size after accounting for tensor and pipeline parallelism: - -```text -data_parallel_size = world_size / (tensor_parallel × pipeline_parallel) -``` +#### Distributed Data Parallelism (DDP) + +For a direct launch with TP and PP disabled, the following starts eight +data-parallel workers in one process: + +```bash +--nthread_per_process 8 # 8-way DDP when TP=1 and PP=1 +``` + +For all launch modes, the data-parallel size is derived from the total world +size after accounting for tensor and pipeline parallelism: + +```text +data_parallel_size = world_size / (tensor_parallel × pipeline_parallel) +``` #### Tensor Parallelism (TP) @@ -263,8 +263,15 @@ data_parallel_size = world_size / (tensor_parallel × pipeline_parallel) ```bash --pipeline_parallel 8 # 8 pipeline stages --virtual_pipeline_parallel 4 # Virtual pipeline for better load balancing +--pipeline_layer_partition 4,8,6,6 # Optional custom per-stage layer counts ``` +`--pipeline_layer_partition` lets you assign a non-uniform but contiguous number +of transformer layers to each stage (e.g. `4,8,6,6` for 4 stages / 24 layers). +The sum of entries must equal the model layer count. When omitted, the default +uniform partition (compatible with vPP) is used. See +[`docs/pipeline_layout_guide.md`](docs/pipeline_layout_guide.md) for details. + #### Combining Parallelism Strategies Multiple parallelism strategies (DDP, TP, SP, PP) can be freely combined to scale training across devices and nodes. @@ -316,4 +323,4 @@ Multiple parallelism strategies (DDP, TP, SP, PP) can be freely combined to scal optimizations. Integrated a CTest + GTest based testing infrastructure to strengthen the - framework's automated test workflow. + framework's automated test workflow. diff --git a/docs/pipeline_layout_guide.md b/docs/pipeline_layout_guide.md new file mode 100644 index 000000000..ebc48a3f2 --- /dev/null +++ b/docs/pipeline_layout_guide.md @@ -0,0 +1,134 @@ +# Pipeline 并行自定义布局使用说明 + +本文档描述 InfiniTrain 新增的 Pipeline 自定义布局能力:通过 `--pipeline_layer_partition` +显式指定每个 Pipeline Stage 的 Transformer 层数,并让模型构建、Pipeline 调度与参数加载统一 +使用同一份 `PipelineLayout`,避免层归属逻辑在多处重复实现。 + +## 快速开始 + +```bash +./build/infini_run \ + --nproc_per_node=4 \ + ./build/gpt2 \ + --device cuda \ + --input_bin data/train.bin \ + --llmc_filepath data/gpt2_124M.bin \ + --pipeline_parallel 4 \ + --pipeline_layer_partition 4,8,6,6 +``` + +上述配置把 24 个 Transformer 层依次划分为: + +```text +stage 0: embedding + layers 0-3 +stage 1: layers 4-11 +stage 2: layers 12-17 +stage 3: layers 18-23 + final_norm + lm_head +``` + +## 参数配置 + +| 参数 | 默认值 | 说明 | +| --- | --- | --- | +| `--pipeline_parallel` | `1` | Pipeline Stage 数量 | +| `--virtual_pipeline_parallel` | `1` | 每个 Stage 的 virtual chunk 数量(vPP) | +| `--pipeline_layer_partition` | `""` | 逗号分隔的各 Stage 层数列表,例如 `4,8,6,6` | + +GPT2 与 LLaMA3 示例入口均支持 `--pipeline_layer_partition`,解析后写入全局环境 +`GlobalEnv`,模型构建、`PipelineParallel` 包装、调度器与 checkpoint 加载都从同一布局查询层归属。 + +## 布局语法 + +- 语法为逗号分隔的正整数列表,例如 `4,8,6,6`。 +- 列表长度必须等于 `--pipeline_parallel` 的 Stage 数量。 +- 各 Stage 层数之和必须等于模型总层数(GPT2-124M 为 12 层,需先选定层数与 Stage 数匹配的模型)。 +- 布局是「连续划分」:`stage i` 拥有编号从 `sum(前 i 项)` 到 `sum(前 i+1 项)` 的连续 Transformer 层。 +- Embedding 固定归属第一个 Stage,Final Norm + LM Head 固定归属最后一个 Stage(当前版本不开放单独配置)。 + +## 默认行为 + +不传 `--pipeline_layer_partition`(或传空串)时,保持原有自动均匀划分: + +```text +layers_per_chunk = total_layers / (num_stages * vpp_size) +remainder = total_layers % (num_stages * vpp_size) +``` + +余数按 global chunk 顺序依次多分配一层,vPP 下各 Stage 按 +`global_chunk = local_chunk * num_stages + stage` 交错持有多个层范围。因此默认均匀布局与 +vPP 完全兼容;自定义布局当前要求 `--virtual_pipeline_parallel 1`(二者不兼容,会报错)。 + +## 输入输出示例 + +启动时若 `--pipeline_parallel > 1`,Stage 0 会打印最终布局: + +```text +PipelineLayout: num_stages=4, total_layers=24, vpp=1 + stage 0: [0, 4) + embedding + stage 1: [4, 12) + stage 2: [12, 18) + stage 3: [18, 24) + final_norm + lm_head +``` + +其中 `[start, end)` 表示本 Stage 持有 `start`(含)到 `end`(不含)的 Transformer 层区间; +每个 Stage 在 vPP 下可能打印多个区间。 + +## 错误排查 + +以下非法配置会在启动阶段直接 `LOG(FATAL)` 终止,并给出可定位的报错信息: + +| 场景 | 触发条件 | 报错关键字 | +| --- | --- | --- | +| 空项或非数字 | `4,,6,6` / `4,8a,6,6` | `not a positive integer` | +| 非正层数 | `4,0,6,6` | `must be positive` | +| Stage 数量不符 | `--pipeline_parallel 4` 但列表只有 3 项 | `entries but pipeline_parallel is` | +| 层数总和错误 | 24 层模型但 `4,8,6,5` | `sums to` | +| 与 vPP 冲突 | 自定义布局 + `--virtual_pipeline_parallel 2` | `incompatible with virtual_pipeline_parallel` | + +若报「模型构建与参数加载层归属不一致」,通常是因为某个调用点仍在使用旧的均匀划分:请确认 +模型构建(`TransformerModel`)、`PipelineParallel` 包装、两个 checkpoint loader 都改为查询 +`PipelineLayout` / `StageInfo`,且 `GetPipelineLayerPartition()` 已正确传入 `InitAllEnv`。 + +## 测试 + +单元测试位于 `tests/distributed/test_pipeline_layout.cc`,覆盖默认均匀划分、自定义 `4,8,6,6`、 +Embedding/FinalNorm/LMHead 归属、`StageOfLayer`/`OwnsLayer`、vPP 交错、chunk↔stage 映射以及 +各类非法配置的死亡断言。 + +```bash +cmake -S . -B build -DBUILD_TEST=ON +cmake --build build -j +ctest --test-dir build -R 'test_pipeline_layout' --output-on-failure +``` + +端到端验证需至少 2 个 Pipeline Stage:用相同初始权重分别以单卡/默认布局与自定义布局跑若干 +训练迭代,比较前向结果、loss 与梯度在允许误差内一致(fp32 1e-05,bf16 1e-02),并确认训练 +过程无通信死锁。 + +## API 摘要 + +```cpp +namespace infini_train::nn::parallel { + +std::vector ParsePipelineLayerPartition(const std::string &str); + +class PipelineLayout { +public: + static PipelineLayout Create(int total_layers, int num_stages, int vpp_size, + const std::vector &partition = {}); + StageInfo GetStageInfo(int stage_id) const; + int StageOfLayer(int layer_id) const; + bool OwnsLayer(int stage_id, int layer_id) const; + static int StageOfChunk(int global_chunk_id, int num_stages); + static int LocalChunkIndexOfChunk(int global_chunk_id, int num_stages); + std::string Describe() const; +}; + +} // namespace infini_train::nn::parallel +``` + +- `StageInfo` 包含 `is_first_stage`、`is_last_stage` 与 `layer_ranges_per_chunk` + (每个 chunk 一个 `(start, end)` 区间)。 +- `Create` 是统一布局入口:空 `partition` 走默认均匀划分,否则按显式层数构建并做完整校验。 +- `GetStageInfo` 供模型构建 / `PipelineParallel` / checkpoint loader 使用; + `StageOfChunk` / `LocalChunkIndexOfChunk` 供调度器统一计算 chunk 归属。 diff --git a/example/gpt2/checkpoint_loader.cc b/example/gpt2/checkpoint_loader.cc index 95e54730b..bd24cb1a4 100644 --- a/example/gpt2/checkpoint_loader.cc +++ b/example/gpt2/checkpoint_loader.cc @@ -18,6 +18,7 @@ #include "infini_train/include/nn/modules/transformer/mlp.h" #include "infini_train/include/nn/modules/transformer/transformer.h" #include "infini_train/include/nn/parallel/global.h" +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" #include "infini_train/include/nn/parallel/pp/pipeline_parallel.h" #include "infini_train/include/nn/parallel/tensor_parallel.h" #include "infini_train/include/tensor.h" @@ -99,15 +100,16 @@ std::shared_ptr LoadFromLLMC(const std::string &filepath) CHECK_EQ(n_embd % n_head, 0) << "n_embd must be divisible by n_head."; CHECK_EQ(n_head % tp_size, 0) << "n_head must be divisible by TP world size."; - // ========== pp_size:num_stages; vpp_size: num_chunks_per_stage ========== + // Unified pipeline layout: which layers / special modules this rank owns. int pp_size = nn::parallel::global::GetPipelineParallelSize(); - int vpp_size = nn::parallel::global::GetVirtualPipelineParallelSize(); - auto pp_rank = nn::parallel::pp_rank; - auto [is_first_stage, is_last_stage, layer_ranges_per_chunk] - = nn::parallel::PipelineParallel::GetStageInfo(n_layer, pp_size, pp_rank, vpp_size); - // ========== layer to chunk ========== + auto layout = nn::parallel::PipelineLayout::Create( + static_cast(n_layer), pp_size, nn::parallel::global::GetVirtualPipelineParallelSize(), + nn::parallel::global::GetPipelineLayerPartition()); + const auto stage_info = layout.GetStageInfo(nn::parallel::pp_rank); + const bool is_first_stage = stage_info.is_first_stage; + const bool is_last_stage = stage_info.is_last_stage; std::vector owned_layers(n_layer, false); - for (const auto &[start, end] : layer_ranges_per_chunk) { + for (const auto &[start, end] : stage_info.layer_ranges_per_chunk) { for (int i = start; i < end; ++i) { owned_layers[i] = true; } } diff --git a/example/gpt2/main.cc b/example/gpt2/main.cc index 2551880e7..465a60c9f 100644 --- a/example/gpt2/main.cc +++ b/example/gpt2/main.cc @@ -24,6 +24,7 @@ #include "infini_train/include/nn/parallel/ddp/distributed_optimizer.h" #include "infini_train/include/nn/parallel/global.h" #include "infini_train/include/nn/parallel/parallel_functional.h" +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" #include "infini_train/include/nn/parallel/pp/pipeline_parallel.h" #include "infini_train/include/nn/parallel/rank.h" #include "infini_train/include/nn/parallel/reduce_op_type.h" @@ -85,6 +86,8 @@ DEFINE_uint32(tensor_parallel, 1, "Tensor Parallel world size"); DEFINE_bool(sequence_parallel, false, "Whether to enable Sequence Parallel"); DEFINE_uint32(pipeline_parallel, 1, "Pipeline Parallel world size, specified the number of PP stages."); DEFINE_uint32(virtual_pipeline_parallel, 1, "Number of chunks in PP stage."); +DEFINE_string(pipeline_layer_partition, "", + "comma-separated per-stage layer counts for a custom pipeline layout, e.g. 4,8,6,6"); // precision DEFINE_string(dtype, "float32", "precision used in training (float32/bfloat16)"); @@ -287,7 +290,7 @@ void Train(const nn::parallel::Rank &rank) { {FLAGS_batch_size, FLAGS_sequence_length / sp_world_size, model_config.n_embd}}; model = std::make_shared(model, pp_world_size, num_micro_batches, shapes, - pp_rank, device, model_config.GetChunkSize()); + pp_rank, device, gpt2_model->stage_info()); if (ddp_world_size > 1) { auto ddp_config = DistributedDataParallelConfig{.zero_stage = FLAGS_zero_stage}; auto *mutable_chunks = dynamic_cast(model.get())->mutable_chunks(); @@ -570,8 +573,10 @@ int main(int argc, char *argv[]) { google::InitGoogleLogging(argv[0]); auto precision_config = utils::PrecisionCheckConfig::Parse(FLAGS_precision_check); + auto pipeline_layer_partition = nn::parallel::ParsePipelineLayerPartition(FLAGS_pipeline_layer_partition); nn::parallel::global::InitAllEnv(FLAGS_nthread_per_process, FLAGS_tensor_parallel, FLAGS_sequence_parallel, - FLAGS_pipeline_parallel, FLAGS_virtual_pipeline_parallel); + FLAGS_pipeline_parallel, FLAGS_virtual_pipeline_parallel, + pipeline_layer_partition); utils::PrecisionCheckEnv::Instance().Init(precision_config); LOG(INFO) << nn::parallel::global::ProcessGroupOverview(); diff --git a/example/llama3/checkpoint_loader.cc b/example/llama3/checkpoint_loader.cc index f3590af6e..0dfb8c36b 100644 --- a/example/llama3/checkpoint_loader.cc +++ b/example/llama3/checkpoint_loader.cc @@ -17,6 +17,8 @@ #include "infini_train/include/nn/modules/transformer/mlp.h" #include "infini_train/include/nn/modules/transformer/transformer.h" #include "infini_train/include/nn/parallel/global.h" +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" +#include "infini_train/include/nn/parallel/pp/pipeline_parallel.h" #include "infini_train/include/nn/parallel/tensor_parallel.h" #include "infini_train/include/tensor.h" @@ -84,13 +86,15 @@ std::shared_ptr LoadFromLLMC(const std::string &filepath) llama3::SanitizeLLaMA3Config(llama3_config); auto llama3 = std::make_shared(llama3_config); - // ========== pp_size:num_stages; vpp_size: num_chunks_per_stage ========== - int pp_size = nn::parallel::global::GetPipelineParallelSize(); - int vpp_size = nn::parallel::global::GetVirtualPipelineParallelSize(); - auto pp_rank = nn::parallel::pp_rank; - auto [is_first_stage, is_last_stage, layer_ranges_per_chunk] - = nn::parallel::PipelineParallel::GetStageInfo(n_layer, pp_size, pp_rank, vpp_size); - // ========== layer to chunk ========== + // Unified pipeline layout: which layers / special modules this rank owns. + auto layout = nn::parallel::PipelineLayout::Create( + static_cast(n_layer), nn::parallel::global::GetPipelineParallelSize(), + nn::parallel::global::GetVirtualPipelineParallelSize(), + nn::parallel::global::GetPipelineLayerPartition()); + const auto stage_info = layout.GetStageInfo(nn::parallel::pp_rank); + const bool is_first_stage = stage_info.is_first_stage; + const bool is_last_stage = stage_info.is_last_stage; + const auto &layer_ranges_per_chunk = stage_info.layer_ranges_per_chunk; std::vector owned_layers(n_layer, false); for (const auto &[start, end] : layer_ranges_per_chunk) { for (int i = start; i < end; ++i) { owned_layers[i] = true; } diff --git a/example/llama3/main.cc b/example/llama3/main.cc index ccfca86a2..0c5f370ad 100644 --- a/example/llama3/main.cc +++ b/example/llama3/main.cc @@ -23,6 +23,7 @@ #include "infini_train/include/nn/parallel/ddp/distributed_optimizer.h" #include "infini_train/include/nn/parallel/global.h" #include "infini_train/include/nn/parallel/parallel_functional.h" +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" #include "infini_train/include/nn/parallel/pp/pipeline_parallel.h" #include "infini_train/include/nn/parallel/process_group.h" #include "infini_train/include/nn/parallel/rank.h" @@ -84,6 +85,8 @@ DEFINE_uint32(tensor_parallel, 1, "Tensor Parallel world size"); DEFINE_bool(sequence_parallel, false, "Whether to enable Sequence Parallel"); DEFINE_uint32(pipeline_parallel, 1, "Pipeline Parallel world size, specified the number of PP stages."); DEFINE_uint32(virtual_pipeline_parallel, 1, "Number of chunks in PP stage."); +DEFINE_string(pipeline_layer_partition, "", + "comma-separated per-stage layer counts for a custom pipeline layout, e.g. 4,8,6,6"); // precision DEFINE_string(dtype, "float32", "precision used in training (float32/bfloat16)"); DEFINE_uint32(save_interval, 0, "save checkpoint every N steps; 0 disables saving"); @@ -221,6 +224,10 @@ void Train(const nn::parallel::Rank &rank) { utils::PrecisionChecker::BuildNameMap(model.get()); + // Cache the transformer stage info before wrapping with LoRA / PipelineParallel. + auto llama_model = std::dynamic_pointer_cast(model); + CHECK(llama_model) << "LLaMA3 example expects a TransformerModel."; + // Apply LoRA using GetLoRAModel (in-place injection) bool lora_enabled = FLAGS_lora_rank > 0; if (lora_enabled) { @@ -260,7 +267,7 @@ void Train(const nn::parallel::Rank &rank) { {FLAGS_batch_size, FLAGS_sequence_length / sp_world_size, model_config.n_embd}}; model = std::make_shared(model, pp_world_size, num_micro_batches, shapes, - pp_rank, device, model_config.GetChunkSize()); + pp_rank, device, llama_model->stage_info()); if (ddp_world_size > 1) { auto ddp_config = DistributedDataParallelConfig{.zero_stage = FLAGS_zero_stage}; auto *mutable_chunks = dynamic_cast(model.get())->mutable_chunks(); @@ -549,8 +556,10 @@ int main(int argc, char *argv[]) { google::InitGoogleLogging(argv[0]); auto precision_config = utils::PrecisionCheckConfig::Parse(FLAGS_precision_check); + auto pipeline_layer_partition = nn::parallel::ParsePipelineLayerPartition(FLAGS_pipeline_layer_partition); nn::parallel::global::InitAllEnv(FLAGS_nthread_per_process, FLAGS_tensor_parallel, FLAGS_sequence_parallel, - FLAGS_pipeline_parallel, FLAGS_virtual_pipeline_parallel); + FLAGS_pipeline_parallel, FLAGS_virtual_pipeline_parallel, + pipeline_layer_partition); utils::PrecisionCheckEnv::Instance().Init(precision_config); LOG(INFO) << nn::parallel::global::ProcessGroupOverview(); diff --git a/infini_train/include/nn/modules/transformer/transformer.h b/infini_train/include/nn/modules/transformer/transformer.h index 0471c32fe..f1b78c456 100644 --- a/infini_train/include/nn/modules/transformer/transformer.h +++ b/infini_train/include/nn/modules/transformer/transformer.h @@ -4,7 +4,7 @@ #include "infini_train/include/nn/modules/module.h" #include "infini_train/include/nn/modules/transformer/transformer_config.h" -#include "infini_train/include/nn/parallel/pp/pipeline_parallel.h" +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" namespace infini_train::nn { class TransformerLayer : public CloneableModule { @@ -77,9 +77,11 @@ class TransformerModel : public CloneableModule { Forward(const std::vector> &x) override; const TransformerConfig &Config() const { return config_; } + const infini_train::nn::parallel::StageInfo &stage_info() const { return stage_info_; } private: const TransformerConfig config_; + const infini_train::nn::parallel::PipelineLayout layout_; const infini_train::nn::parallel::StageInfo stage_info_; }; diff --git a/infini_train/include/nn/modules/transformer/transformer_config.h b/infini_train/include/nn/modules/transformer/transformer_config.h index a646ab14e..531dd8c31 100644 --- a/infini_train/include/nn/modules/transformer/transformer_config.h +++ b/infini_train/include/nn/modules/transformer/transformer_config.h @@ -93,6 +93,5 @@ struct TransformerConfig { int64_t max_gen_batch_size = 4; // max batch size during inference bool UseGQA() const; - int GetChunkSize() const; }; } // namespace infini_train::nn diff --git a/infini_train/include/nn/parallel/global.h b/infini_train/include/nn/parallel/global.h index 38694a91d..fe92f3a8d 100644 --- a/infini_train/include/nn/parallel/global.h +++ b/infini_train/include/nn/parallel/global.h @@ -29,7 +29,8 @@ class GlobalEnv { static GlobalEnv &Instance(); void Init(int threads_per_process, int tensor_parallel_size, bool sequence_parallel_enabled, - int pipeline_parallel_size, int virtual_pipeline_parallel_size); + int pipeline_parallel_size, int virtual_pipeline_parallel_size, + const std::vector &pipeline_layer_partition = {}); int nnodes() const; @@ -55,6 +56,8 @@ class GlobalEnv { int virtual_pipeline_parallel_size() const; + const std::vector &pipeline_layer_partition() const; + Layout layout() const; private: @@ -80,6 +83,7 @@ class GlobalEnv { int pipeline_parallel_size_ = 1; int virtual_pipeline_parallel_size_ = 1; + std::vector pipeline_layer_partition_; mutable std::mutex mutex_; bool initialized_ = false; @@ -88,9 +92,10 @@ class GlobalEnv { }; inline void InitAllEnv(int nthread_per_process, int tensor_parallel_size, bool sequence_parallel_enabled, - int pipeline_parallel_size, int virtual_pipeline_parallel) { + int pipeline_parallel_size, int virtual_pipeline_parallel, + const std::vector &pipeline_layer_partition = {}) { GlobalEnv::Instance().Init(nthread_per_process, tensor_parallel_size, sequence_parallel_enabled, - pipeline_parallel_size, virtual_pipeline_parallel); + pipeline_parallel_size, virtual_pipeline_parallel, pipeline_layer_partition); } inline int GetNnodes() { return GlobalEnv::Instance().nnodes(); } inline int GetWorldSize() { return GlobalEnv::Instance().world_size(); } @@ -106,6 +111,7 @@ inline bool GetSequenceParallelEnabled() { return GlobalEnv::Instance().sequence inline int GetDataParallelSize() { return GlobalEnv::Instance().data_parallel_size(); } inline int GetPipelineParallelSize() { return GlobalEnv::Instance().pipeline_parallel_size(); } inline int GetVirtualPipelineParallelSize() { return GlobalEnv::Instance().virtual_pipeline_parallel_size(); } +inline const std::vector &GetPipelineLayerPartition() { return GlobalEnv::Instance().pipeline_layer_partition(); } // ========================= // Layout Helper Functions diff --git a/infini_train/include/nn/parallel/pp/pipeline_layout.h b/infini_train/include/nn/parallel/pp/pipeline_layout.h new file mode 100644 index 000000000..3fabe0e10 --- /dev/null +++ b/infini_train/include/nn/parallel/pp/pipeline_layout.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +namespace infini_train::nn::parallel { + +// Describes which layers (and special modules) belong to a single pipeline stage. +struct StageInfo { + bool is_first_stage = false; // this stage owns the Embedding (first-stage module) + bool is_last_stage = false; // this stage owns the Final Norm and LM Head + // Layer index ranges assigned to this stage, one per (virtual) chunk: + // (inclusive_start_layer, exclusive_end_layer). + std::vector> layer_ranges_per_chunk; +}; + +// Unified source of truth for the pipeline layer partition. Model construction, +// pipeline stage wrapping and parameter loading all query the same layout so the +// layer-ownership logic is not duplicated across modules. +class PipelineLayout { +public: + // Build a layout. `partition` holds the number of transformer layers per stage + // (e.g. {4, 8, 6, 6}); when empty, fall back to the default uniform partition. + static PipelineLayout Create(int total_layers, int num_stages, int vpp_size, + const std::vector &partition = {}); + + StageInfo GetStageInfo(int stage_id) const; + int StageOfLayer(int layer_id) const; + bool OwnsLayer(int stage_id, int layer_id) const; + + // Round-robin chunk -> stage / local-chunk mapping used by the pipeline scheduler. + static int StageOfChunk(int global_chunk_id, int num_stages); + static int LocalChunkIndexOfChunk(int global_chunk_id, int num_stages); + + std::string Describe() const; + +private: + int num_stages_ = 1; + int total_layers_ = 0; + int vpp_size_ = 1; + int first_stage_idx_ = 0; // stage owning the Embedding + int last_stage_idx_ = 0; // stage owning the Final Norm + LM Head + std::vector>> stage_layer_ranges_; + std::vector layer_to_stage_; +}; + +// Parse a comma-separated per-stage layer count string ("4,8,6,6"). Returns an empty +// vector when `str` is empty, meaning "use the default uniform partition". +std::vector ParsePipelineLayerPartition(const std::string &str); + +} // namespace infini_train::nn::parallel diff --git a/infini_train/include/nn/parallel/pp/pipeline_parallel.h b/infini_train/include/nn/parallel/pp/pipeline_parallel.h index 25939bdc2..0f6e9fdbd 100644 --- a/infini_train/include/nn/parallel/pp/pipeline_parallel.h +++ b/infini_train/include/nn/parallel/pp/pipeline_parallel.h @@ -5,6 +5,7 @@ #include #include "infini_train/include/nn/modules/module.h" +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" namespace infini_train { class Tensor; @@ -18,26 +19,16 @@ class PipelineSchedule; extern thread_local int pp_rank; -struct StageInfo { - bool is_first_stage; - bool is_last_stage; - - // Layer index ranges for chunks assigned to this pipeline stage. - // Each element is a pair: (inclusive_start_layer, exclusive_end_layer) - std::vector> layer_ranges_per_chunk; -}; - class PipelineParallel : public Module { public: PipelineParallel(const std::shared_ptr module, int num_stages, int num_micro_batches, - const std::vector> &recv_shape, int rank, Device device, int vpp); + const std::vector> &recv_shape, int rank, Device device, + const StageInfo &stage_info); float TrainStep(const std::vector> &input, const std::vector> &target, const std::shared_ptr &optimizer, const std::shared_ptr &loss_fn, DataType dtype) override; - static StageInfo GetStageInfo(int total_layers, int pp_size, int pp_rank, int chunks_per_stage = 1); - std::vector> *mutable_chunks(); private: diff --git a/infini_train/src/nn/modules/transformer/transformer.cc b/infini_train/src/nn/modules/transformer/transformer.cc index 99a739d2d..aabbc32c3 100644 --- a/infini_train/src/nn/modules/transformer/transformer.cc +++ b/infini_train/src/nn/modules/transformer/transformer.cc @@ -18,6 +18,8 @@ #include "infini_train/include/nn/modules/transformer/moe/moe_layer.h" #include "infini_train/include/nn/modules/transformer/utils.h" #include "infini_train/include/nn/parallel/global.h" +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" +#include "infini_train/include/nn/parallel/pp/pipeline_parallel.h" #include "infini_train/include/nn/parallel/tensor_parallel.h" #include "infini_train/include/nn/parallel/utils.h" #include "infini_train/include/tensor.h" @@ -206,9 +208,15 @@ std::vector> TransformerLastStage::Forward(const std::ve TransformerModel::TransformerModel(const TransformerConfig config) : CloneableModule(kType), config_(config), - stage_info_(nn::parallel::PipelineParallel::GetStageInfo( - config_.n_layer, nn::parallel::global::GetPipelineParallelSize(), nn::parallel::pp_rank, - nn::parallel::global::GetVirtualPipelineParallelSize())) { + layout_(nn::parallel::PipelineLayout::Create( + static_cast(config_.n_layer), nn::parallel::global::GetPipelineParallelSize(), + nn::parallel::global::GetVirtualPipelineParallelSize(), + nn::parallel::global::GetPipelineLayerPartition())), + stage_info_(layout_.GetStageInfo(nn::parallel::pp_rank)) { + if (nn::parallel::global::GetPipelineParallelSize() > 1 && nn::parallel::pp_rank == 0) { + LOG(INFO) << layout_.Describe(); + } + auto tp_world_size = nn::parallel::global::GetTensorParallelSize(); // NOTE(zbl): VocabParallelEmbedding requires vocab_size % tp_size == 0 diff --git a/infini_train/src/nn/modules/transformer/transformer_config.cc b/infini_train/src/nn/modules/transformer/transformer_config.cc index b8947d4b6..09d55baa6 100644 --- a/infini_train/src/nn/modules/transformer/transformer_config.cc +++ b/infini_train/src/nn/modules/transformer/transformer_config.cc @@ -1,15 +1,5 @@ #include "infini_train/include/nn/modules/transformer/transformer_config.h" -#include "infini_train/include/nn/parallel/global.h" -#include "infini_train/include/nn/parallel/pp/pipeline_parallel.h" - namespace infini_train::nn { bool TransformerConfig::UseGQA() const { return n_kv_head < n_head; } - -int TransformerConfig::GetChunkSize() const { - auto stage_info = parallel::PipelineParallel::GetStageInfo(n_layer, parallel::global::GetPipelineParallelSize(), - parallel::pp_rank, - parallel::global::GetVirtualPipelineParallelSize()); - return stage_info.layer_ranges_per_chunk.size(); -} } // namespace infini_train::nn diff --git a/infini_train/src/nn/parallel/global.cc b/infini_train/src/nn/parallel/global.cc index 655b4bceb..7b2f98745 100644 --- a/infini_train/src/nn/parallel/global.cc +++ b/infini_train/src/nn/parallel/global.cc @@ -87,7 +87,8 @@ GlobalEnv &GlobalEnv::Instance() { } void GlobalEnv::Init(int nthread_per_process, int tensor_parallel_size, bool sequence_parallel_enabled, - int pipeline_parallel_size, int virtual_pipeline_parallel_size) { + int pipeline_parallel_size, int virtual_pipeline_parallel_size, + const std::vector &pipeline_layer_partition) { std::lock_guard lock(mutex_); CHECK(!initialized_) << "Repeated initialization of GlobalEnv!"; @@ -112,6 +113,7 @@ void GlobalEnv::Init(int nthread_per_process, int tensor_parallel_size, bool seq sequence_parallel_enabled_ = sequence_parallel_enabled; pipeline_parallel_size_ = pipeline_parallel_size; virtual_pipeline_parallel_size_ = virtual_pipeline_parallel_size; + pipeline_layer_partition_ = pipeline_layer_partition; data_parallel_size_ = world_size_ / tensor_parallel_size_ / pipeline_parallel_size_; layout_.sizes[DP] = data_parallel_size_; @@ -182,6 +184,11 @@ int GlobalEnv::virtual_pipeline_parallel_size() const { return virtual_pipeline_parallel_size_; } +const std::vector &GlobalEnv::pipeline_layer_partition() const { + CHECK(initialized_) << "GlobalEnv is not initialized!"; + return pipeline_layer_partition_; +} + Layout GlobalEnv::layout() const { CHECK(initialized_) << "GlobalEnv is not initialized!"; return layout_; diff --git a/infini_train/src/nn/parallel/pp/pipeline_parallel.cc b/infini_train/src/nn/parallel/pp/pipeline_parallel.cc index c0369cdeb..ea15bb133 100644 --- a/infini_train/src/nn/parallel/pp/pipeline_parallel.cc +++ b/infini_train/src/nn/parallel/pp/pipeline_parallel.cc @@ -39,60 +39,22 @@ float PipelineParallel::TrainStep(const std::vector> &in return schedule_->Step(stage_input, stage_target, optimizer, loss_fn, dtype); } -StageInfo PipelineParallel::GetStageInfo(int total_layers, int pp_size, int rank, int chunks_per_stage) { - bool is_first_stage = (rank == 0); - bool is_last_stage = (rank == pp_size - 1); - - std::vector> layer_ranges_per_chunk; - - int layers_per_chunk = total_layers / (pp_size * chunks_per_stage); - int remainder = total_layers % (pp_size * chunks_per_stage); - - for (int local_chunk_idx = 0; local_chunk_idx < chunks_per_stage; ++local_chunk_idx) { - int global_chunk_idx = local_chunk_idx * pp_size + rank; - - if (global_chunk_idx * layers_per_chunk >= total_layers) { - break; - } - - int chunk_start = global_chunk_idx * layers_per_chunk; - int chunk_end = chunk_start + layers_per_chunk; - - if (global_chunk_idx < remainder) { - // Assign an additional layer to each of the first remainder chunks - chunk_start = global_chunk_idx * (layers_per_chunk + 1); - chunk_end = chunk_start + (layers_per_chunk + 1); - } else { - chunk_start = remainder * (layers_per_chunk + 1) + (global_chunk_idx - remainder) * layers_per_chunk; - chunk_end = chunk_start + layers_per_chunk; - } - - chunk_end = std::min(chunk_end, total_layers); - if (chunk_start < chunk_end) { - layer_ranges_per_chunk.push_back({chunk_start, chunk_end}); - } - } - - return {is_first_stage, is_last_stage, layer_ranges_per_chunk}; -} - PipelineParallel::PipelineParallel(const std::shared_ptr module, int num_stages, int num_micro_batches, const std::vector> &recv_shape, int pp_rank, Device device, - int chunk_size) + const StageInfo &stage_info) : num_stages_(num_stages), rank_(pp_rank) { modules_[kModuleName] = std::move(module); - int stage_id = pp_rank; - int stage_size = num_stages; + const int chunk_size = static_cast(stage_info.layer_ranges_per_chunk.size()); std::vector> chunks; for (int chunk_id = 0; chunk_id < chunk_size; ++chunk_id) { std::vector> chunk_parts; - if (chunk_id == 0 && stage_id == 0) { + if (chunk_id == 0 && stage_info.is_first_stage) { chunk_parts.push_back(module->mutable_module(kPPFirstStageName)); } chunk_parts.push_back(module->mutable_module(kPPChunkNamePrefix + std::to_string(chunk_id))); - if (chunk_id == chunk_size - 1 && stage_id == stage_size - 1) { + if (chunk_id == chunk_size - 1 && stage_info.is_last_stage) { chunk_parts.push_back(module->mutable_module(kPPLastStageName)); } chunks.push_back(std::make_shared(std::move(chunk_parts))); diff --git a/infini_train/src/nn/parallel/pp/pipeline_schedule.cc b/infini_train/src/nn/parallel/pp/pipeline_schedule.cc index b702a3016..38a47748f 100644 --- a/infini_train/src/nn/parallel/pp/pipeline_schedule.cc +++ b/infini_train/src/nn/parallel/pp/pipeline_schedule.cc @@ -13,6 +13,7 @@ #include "infini_train/include/nn/init.h" #include "infini_train/include/nn/modules/module.h" #include "infini_train/include/nn/parallel/global.h" +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" #include "infini_train/include/nn/parallel/pp/pipeline_stage.h" #include "infini_train/include/nn/parallel/pp/send_recv.h" #include "infini_train/include/optimizer.h" @@ -32,8 +33,8 @@ void PrintScheduleTable(const std::vector &sche LOG(INFO) << "-----|-----------|------------|--------------|-------------|-------"; for (const auto &task : schedule) { - int owning_stage = task.global_chunk_id % num_stages; - int local_chunk = task.global_chunk_id / num_stages; + int owning_stage = PipelineLayout::StageOfChunk(task.global_chunk_id, num_stages); + int local_chunk = PipelineLayout::LocalChunkIndexOfChunk(task.global_chunk_id, num_stages); std::string type_str = task.is_forward ? "Forward" : "Backward"; @@ -75,9 +76,9 @@ PipelineParallelScheduler::Task PipelineParallelScheduler::CreateTask(int step, task.step = step; task.microbatch_id = mb; task.global_chunk_id = global_chunk; - task.local_chunk_idx = global_chunk / num_stages; + task.local_chunk_idx = PipelineLayout::LocalChunkIndexOfChunk(global_chunk, num_stages); task.is_forward = is_forward; - task.stage_id = global_chunk % num_stages; + task.stage_id = PipelineLayout::StageOfChunk(global_chunk, num_stages); task.is_last_chunk = (global_chunk == total_chunks - 1); task.is_first_chunk = (global_chunk == 0); return task; diff --git a/tests/distributed/CMakeLists.txt b/tests/distributed/CMakeLists.txt index b8ed49700..d97f18163 100644 --- a/tests/distributed/CMakeLists.txt +++ b/tests/distributed/CMakeLists.txt @@ -7,6 +7,11 @@ infini_train_add_test(test_rank LABELS cpu ) +infini_train_add_test(test_pipeline_layout + SOURCES test_pipeline_layout.cc + LABELS cpu +) + add_test( NAME RankTest.MultiNodeSingleProcessIsParallel COMMAND ${CMAKE_COMMAND} -E env diff --git a/tests/distributed/test_pipeline_layout.cc b/tests/distributed/test_pipeline_layout.cc new file mode 100644 index 000000000..1d6f834b2 --- /dev/null +++ b/tests/distributed/test_pipeline_layout.cc @@ -0,0 +1,117 @@ +#include +#include +#include + +#include "gtest/gtest.h" + +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" + +namespace infini_train::nn::parallel { +namespace { + +TEST(PipelineLayoutTest, DefaultUniformPartition) { + auto layout = PipelineLayout::Create(24, 4, 1); + + auto s0 = layout.GetStageInfo(0); + EXPECT_TRUE(s0.is_first_stage); + EXPECT_FALSE(s0.is_last_stage); + ASSERT_EQ(s0.layer_ranges_per_chunk.size(), 1u); + EXPECT_EQ(s0.layer_ranges_per_chunk[0], std::make_pair(0, 6)); + + auto s3 = layout.GetStageInfo(3); + EXPECT_FALSE(s3.is_first_stage); + EXPECT_TRUE(s3.is_last_stage); + ASSERT_EQ(s3.layer_ranges_per_chunk.size(), 1u); + EXPECT_EQ(s3.layer_ranges_per_chunk[0], std::make_pair(18, 24)); +} + +TEST(PipelineLayoutTest, CustomPartition) { + auto layout = PipelineLayout::Create(24, 4, 1, {4, 8, 6, 6}); + + auto s0 = layout.GetStageInfo(0); + auto s1 = layout.GetStageInfo(1); + auto s2 = layout.GetStageInfo(2); + auto s3 = layout.GetStageInfo(3); + + EXPECT_TRUE(s0.is_first_stage); + EXPECT_FALSE(s0.is_last_stage); + EXPECT_FALSE(s3.is_first_stage); + EXPECT_TRUE(s3.is_last_stage); + + ASSERT_EQ(s0.layer_ranges_per_chunk.size(), 1u); + ASSERT_EQ(s1.layer_ranges_per_chunk.size(), 1u); + ASSERT_EQ(s2.layer_ranges_per_chunk.size(), 1u); + ASSERT_EQ(s3.layer_ranges_per_chunk.size(), 1u); + + EXPECT_EQ(s0.layer_ranges_per_chunk[0], std::make_pair(0, 4)); + EXPECT_EQ(s1.layer_ranges_per_chunk[0], std::make_pair(4, 12)); + EXPECT_EQ(s2.layer_ranges_per_chunk[0], std::make_pair(12, 18)); + EXPECT_EQ(s3.layer_ranges_per_chunk[0], std::make_pair(18, 24)); +} + +TEST(PipelineLayoutTest, StageOfLayerAndOwnsLayer) { + auto layout = PipelineLayout::Create(24, 4, 1, {4, 8, 6, 6}); + + EXPECT_EQ(layout.StageOfLayer(0), 0); + EXPECT_EQ(layout.StageOfLayer(5), 1); + EXPECT_EQ(layout.StageOfLayer(17), 2); + EXPECT_EQ(layout.StageOfLayer(23), 3); + + EXPECT_TRUE(layout.OwnsLayer(0, 3)); + EXPECT_FALSE(layout.OwnsLayer(0, 4)); +} + +TEST(PipelineLayoutTest, VirtualPipelineInterleaving) { + auto layout = PipelineLayout::Create(8, 2, 2); + + auto s0 = layout.GetStageInfo(0); + ASSERT_EQ(s0.layer_ranges_per_chunk.size(), 2u); + EXPECT_EQ(s0.layer_ranges_per_chunk[0], std::make_pair(0, 2)); + EXPECT_EQ(s0.layer_ranges_per_chunk[1], std::make_pair(4, 6)); + + auto s1 = layout.GetStageInfo(1); + ASSERT_EQ(s1.layer_ranges_per_chunk.size(), 2u); + EXPECT_EQ(s1.layer_ranges_per_chunk[0], std::make_pair(2, 4)); + EXPECT_EQ(s1.layer_ranges_per_chunk[1], std::make_pair(6, 8)); +} + +TEST(PipelineLayoutTest, ChunkMappingHelpers) { + EXPECT_EQ(PipelineLayout::StageOfChunk(0, 4), 0); + EXPECT_EQ(PipelineLayout::StageOfChunk(5, 4), 1); + EXPECT_EQ(PipelineLayout::LocalChunkIndexOfChunk(5, 4), 1); + EXPECT_EQ(PipelineLayout::LocalChunkIndexOfChunk(7, 4), 1); +} + +TEST(PipelineLayoutTest, DescribeNonEmpty) { + auto layout = PipelineLayout::Create(24, 4, 1, {4, 8, 6, 6}); + EXPECT_FALSE(layout.Describe().empty()); +} + +TEST(PipelineLayoutTest, ParsePartition) { + EXPECT_TRUE(ParsePipelineLayerPartition("").empty()); + const std::vector expected{4, 8, 6, 6}; + EXPECT_EQ(ParsePipelineLayerPartition("4,8,6,6"), expected); +} + +TEST(PipelineLayoutTest, RejectsPartitionSumMismatch) { + EXPECT_DEATH(PipelineLayout::Create(24, 4, 1, {4, 8, 6, 5}), "sums to"); +} + +TEST(PipelineLayoutTest, RejectsStageCountMismatch) { + EXPECT_DEATH(PipelineLayout::Create(24, 4, 1, {6, 6, 6}), "entries"); +} + +TEST(PipelineLayoutTest, RejectsNonPositiveEntry) { + EXPECT_DEATH(PipelineLayout::Create(24, 4, 1, {4, 0, 6, 14}), "must be positive"); +} + +TEST(PipelineLayoutTest, RejectsVirtualPipelineConflict) { + EXPECT_DEATH(PipelineLayout::Create(24, 4, 2, {4, 8, 6, 6}), "incompatible"); +} + +TEST(PipelineLayoutTest, RejectsInvalidToken) { + EXPECT_DEATH(ParsePipelineLayerPartition("4,8a,6,6"), "not a positive integer"); +} + +} // namespace +} // namespace infini_train::nn::parallel From d5735b9b1e0bc38d7cd8a33b7226ac453789ed92 Mon Sep 17 00:00:00 2001 From: CuiLingyunCrispy Date: Sun, 13 Sep 2026 13:57:28 +0800 Subject: [PATCH 2/4] feat: pipeline custom layout demo, suggest tests and guide --- .gitignore | 6 + CMakeLists.txt | 2 +- docs/pipeline_layout_demo.cc | 119 +++++++ docs/pipeline_layout_guide.md | 167 +++++++++- example/gpt2/main.cc | 57 +++- example/llama3/main.cc | 54 ++- .../nn/modules/transformer/transformer.h | 7 + .../include/nn/parallel/pp/pipeline_layout.h | 38 +++ .../nn/parallel/pp/pipeline_parallel.h | 6 + .../nn/parallel/pp/pipeline_schedule.h | 16 +- .../src/nn/modules/transformer/transformer.cc | 49 +++ .../src/nn/parallel/pp/pipeline_layout.cc | 308 ++++++++++++++++++ .../src/nn/parallel/pp/pipeline_parallel.cc | 63 ++++ .../src/nn/parallel/pp/pipeline_schedule.cc | 151 +++++++++ ...32\344\271\211\345\270\203\345\261\200.md" | 168 ++++++++++ ...45\270\203\345\261\200.md:Zone.Identifier" | Bin 0 -> 25 bytes ...11\351\242\230\346\226\207\346\241\243.md" | 72 ++++ ...46\226\207\346\241\243.md:Zone.Identifier" | Bin 0 -> 25 bytes tests/distributed/CMakeLists.txt | 5 + .../test_pipeline_layout_suggest.cc | 195 +++++++++++ 20 files changed, 1474 insertions(+), 9 deletions(-) create mode 100644 docs/pipeline_layout_demo.cc create mode 100644 infini_train/src/nn/parallel/pp/pipeline_layout.cc create mode 100644 "read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md" create mode 100644 "read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md:Zone.Identifier" create mode 100644 "read_notes/\351\241\271\347\233\256\351\200\211\351\242\230\346\226\207\346\241\243.md" create mode 100644 "read_notes/\351\241\271\347\233\256\351\200\211\351\242\230\346\226\207\346\241\243.md:Zone.Identifier" create mode 100644 tests/distributed/test_pipeline_layout_suggest.cc diff --git a/.gitignore b/.gitignore index 4ad6f92ff..7e8efd5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ build/ __pycache__/ /data/ +.aider* + +# local junk +.claude/ +*.msi +cclUniqueId_*.bin diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bd8069d4..3f7d72546 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,7 +104,7 @@ if(USE_CUDA) file(GLOB_RECURSE CUDA_KERNELS ${PROJECT_SOURCE_DIR}/infini_train/src/*.cu) add_library(infini_train_cuda_kernels STATIC ${CUDA_KERNELS}) - set_target_properties(infini_train_cuda_kernels PROPERTIES CUDA_ARCHITECTURES "75;80;90") + set_target_properties(infini_train_cuda_kernels PROPERTIES CUDA_ARCHITECTURES "75;80;90;120") target_link_libraries(infini_train_cuda_kernels PUBLIC diff --git a/docs/pipeline_layout_demo.cc b/docs/pipeline_layout_demo.cc new file mode 100644 index 000000000..950952851 --- /dev/null +++ b/docs/pipeline_layout_demo.cc @@ -0,0 +1,119 @@ +// Demonstration for excellent-standard #4: compare the default uniform pipeline +// layout against a custom (cost-balanced) layout on a load-imbalanced model, and +// show the resulting pipeline bubble, per-stage time and throughput. +// +// This is a pure-CPU analytical demo: it uses the same ComputePipelineLoadAnalysis / +// SuggestBalancedPartition functions the training binary uses, so it can run on any +// machine without a GPU. The real (measured) per-stage CUDA timings are produced by +// the gpt2 example on a multi-GPU machine (see the multi-GPU run command in +// docs/pipeline_layout_guide.md). +// +// Build & run (from the repo root, inside WSL): +// g++ -std=c++20 -I. -Ithird_party/glog/src \ +// docs/pipeline_layout_demo.cc infini_train/src/nn/parallel/pp/pipeline_layout.cc \ +// -Lbuild/third_party/glog -lglog -pthread -o build/pipeline_layout_demo +// ./build/pipeline_layout_demo + +#include +#include +#include + +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" + +namespace { +using infini_train::nn::parallel::ComputePipelineLoadAnalysis; +using infini_train::nn::parallel::PipelineLoadStats; +using infini_train::nn::parallel::SuggestBalancedPartition; + +// A load-imbalanced 12-layer model: the first 4 layers are "light" (cost 1) and the +// last 8 layers are "heavy" (cost 2). This models, e.g., attention-vs-MLP heavy blocks +// or a mixture-of-experts tail whose per-layer compute is no longer uniform. +std::vector ImbalancedCosts() { + std::vector costs(12, 2.0); + for (int i = 0; i < 4; ++i) { + costs[i] = 1.0; + } + return costs; +} + +std::string PartitionStr(const std::vector &p) { + std::string s; + for (size_t i = 0; i < p.size(); ++i) { + if (i) { + s += ","; + } + s += std::to_string(p[i]); + } + return s; +} + +// Steady-state throughput in micro-batches per unit time, from the GPipe make-span +// formula: T = bottleneck * (S - 1 + n), so throughput = n / T. +double Throughput(const PipelineLoadStats &s) { + return s.num_micro_batches / (s.bottleneck * (s.num_stages - 1 + s.num_micro_batches)); +} + +void PrintComparison(const char *title, const std::vector &uniform_partition, + const std::vector &custom_partition, const std::vector &costs, int n) { + const int total_layers = static_cast(costs.size()); + const int num_stages = static_cast(uniform_partition.size()); + + const PipelineLoadStats uniform = + ComputePipelineLoadAnalysis(total_layers, num_stages, uniform_partition, costs, n); + const PipelineLoadStats custom = + ComputePipelineLoadAnalysis(total_layers, num_stages, custom_partition, costs, n); + + const std::string u_part = PartitionStr(uniform_partition); + const std::string c_part = PartitionStr(custom_partition); + + std::printf("=== %s ===\n", title); + std::printf("per-layer costs: ["); + for (size_t i = 0; i < costs.size(); ++i) { + std::printf("%s%.0f", i ? "," : "", costs[i]); + } + std::printf("] (S=%d stages, n=%d micro-batches)\n\n", num_stages, n); + + const std::string u_col = "uniform (" + u_part + ")"; + const std::string c_col = "custom (" + c_part + ")"; + std::printf("%-22s | %-14s | %-14s\n", "metric", u_col.c_str(), c_col.c_str()); + std::printf("%-22s-+-%s-+-%s\n", "----------------------", "---------------", "---------------"); + + for (int s = 0; s < num_stages; ++s) { + char line[32]; + std::snprintf(line, sizeof(line), "stage %d time (load)", s); + std::printf("%-22s | %14.3f | %14.3f\n", line, uniform.stage_loads[s], custom.stage_loads[s]); + } + std::printf("%-22s | %14.3f | %14.3f\n", "bottleneck (max)", uniform.bottleneck, custom.bottleneck); + std::printf("%-22s | %14.3f | %14.3f\n", "average", uniform.average, custom.average); + std::printf("%-22s | %13.1f%% | %13.1f%%\n", "imbalance bubble", uniform.imbalance_bubble * 100.0, + custom.imbalance_bubble * 100.0); + std::printf("%-22s | %13.1f%% | %13.1f%%\n", "structural bubble", uniform.structural_bubble * 100.0, + custom.structural_bubble * 100.0); + std::printf("%-22s | %13.1f%% | %13.1f%%\n", "pipeline efficiency", uniform.efficiency * 100.0, + custom.efficiency * 100.0); + + const double t_uniform = Throughput(uniform); + const double t_custom = Throughput(custom); + std::printf("%-22s | %14.4f | %14.4f\n", "throughput (mb/t)", t_uniform, t_custom); + std::printf("%-22s | %14s | %13.2fx\n", "throughput speedup", "-", t_custom / t_uniform); + std::printf("\n"); +} +} // namespace + +int main() { + const int n = 8; // micro-batches per step + + // 1) Load-imbalanced model: default uniform {6,6} vs custom balanced {7,5}. + const std::vector costs = ImbalancedCosts(); + const std::vector uniform{6, 6}; + const std::vector custom = SuggestBalancedPartition(12, 2, costs); + PrintComparison("Load-imbalanced model (4 light + 8 heavy layers)", uniform, custom, costs, n); + + // 2) Uniform-cost model: both layouts are equivalent, showing the feature correctly + // reports zero imbalance bubble for the balanced default. + const std::vector unit_costs(12, 1.0); + const std::vector unit_custom = SuggestBalancedPartition(12, 2, unit_costs); + PrintComparison("Uniform-cost model (all layers equal)", uniform, unit_custom, unit_costs, n); + + return 0; +} diff --git a/docs/pipeline_layout_guide.md b/docs/pipeline_layout_guide.md index ebc48a3f2..64eb507ba 100644 --- a/docs/pipeline_layout_guide.md +++ b/docs/pipeline_layout_guide.md @@ -1,8 +1,9 @@ # Pipeline 并行自定义布局使用说明 本文档描述 InfiniTrain 新增的 Pipeline 自定义布局能力:通过 `--pipeline_layer_partition` -显式指定每个 Pipeline Stage 的 Transformer 层数,并让模型构建、Pipeline 调度与参数加载统一 -使用同一份 `PipelineLayout`,避免层归属逻辑在多处重复实现。 +显式指定每个 Pipeline Stage 的 Transformer 层数,或通过 `--pipeline_layer_costs` / +`--pipeline_auto_layout` 根据计算代价自动生成近似负载均衡的布局,并让模型构建、Pipeline 调度 +与参数加载统一使用同一份 `PipelineLayout`,避免层归属逻辑在多处重复实现。 ## 快速开始 @@ -33,6 +34,8 @@ stage 3: layers 18-23 + final_norm + lm_head | `--pipeline_parallel` | `1` | Pipeline Stage 数量 | | `--virtual_pipeline_parallel` | `1` | 每个 Stage 的 virtual chunk 数量(vPP) | | `--pipeline_layer_partition` | `""` | 逗号分隔的各 Stage 层数列表,例如 `4,8,6,6` | +| `--pipeline_layer_costs` | `""` | 逗号分隔的每层计算代价,用于自动生成负载均衡布局,例如 `1,2,1.5` | +| `--pipeline_auto_layout` | `false` | 按每层参数量自动生成负载均衡布局 | GPT2 与 LLaMA3 示例入口均支持 `--pipeline_layer_partition`,解析后写入全局环境 `GlobalEnv`,模型构建、`PipelineParallel` 包装、调度器与 checkpoint 加载都从同一布局查询层归属。 @@ -58,6 +61,129 @@ remainder = total_layers % (num_stages * vpp_size) `global_chunk = local_chunk * num_stages + stage` 交错持有多个层范围。因此默认均匀布局与 vPP 完全兼容;自定义布局当前要求 `--virtual_pipeline_parallel 1`(二者不兼容,会报错)。 +## 自动布局建议 + +除显式指定 `--pipeline_layer_partition` 外,还支持根据计算代价自动生成近似负载均衡的连续布局 +(即「线性划分」问题:DP 最小化各 Stage 最大总代价)。三种代价来源: + +1. **用户提供的计算代价**:`--pipeline_layer_costs "1,1,1,1,2,..."`,每个数对应一层的代价 + (参数量、实测耗时等均可)。列表长度即模型层数,结果会自动均衡各 Stage 总代价。 +2. **各层参数量**:`--pipeline_auto_layout`,根据 `TransformerConfig` 解析式计算每层参数量 + (`ComputePerLayerParamCounts`,GPT-2 的 GELU+LayerNorm、LLaMA3 的 SwiGLU+RMSNorm+GQA 均精确支持; + MoE 层暂不支持,需改用 `--pipeline_layer_costs`)。 +3. **Profiler 统计**:先用 `--freq_generate_txt` / PROFILE_MODE 跑一次得到每层 kernel 实测耗时,再 + 把每层耗时作为代价通过 `--pipeline_layer_costs` 传入,即可得到基于实测负载的布局建议。 + +```bash +# 代价不均衡(前 4 层轻、后 8 层重)时,PP=2 建议 7,5 而非均匀的 6,6 +./build/infini_run --nproc_per_node=2 \ + ./build/gpt2 --device cuda --model d12 --pipeline_parallel 2 \ + --pipeline_layer_costs 1,1,1,1,2,2,2,2,2,2,2,2 + +# 或按每层参数量自动建议(GPT-2 / LLaMA3 各层结构相同,结果等价于均匀布局) +./build/infini_run --nproc_per_node=2 \ + ./build/gpt2 --device cuda --model d12 --pipeline_parallel 2 --pipeline_auto_layout +``` + +三种方式彼此互斥,且都不能与 `--pipeline_layer_partition` 同时使用。建议结果会在启动阶段打印为 +`Auto-suggested pipeline layout ...: `;最终 `PipelineLayout` 仍按既有格式打印。核心算法见 +`SuggestBalancedPartition`,空代价(`{}`)即退化为按层数均匀划分。 + +## Pipeline 负载分析(bubble / 各 Stage 执行时间 / 吞吐) + +为证明「自定义布局能改善负载不均衡场景」,框架在运行结束时自动汇总一次 Pipeline 负载分析 +(`--pipeline_parallel > 1` 时):每个 PP rank 测量本 Stage 的前向 / 反向纯计算时间(CUDA 用 +event 计时、CPU 用 `steady_clock`),经 PP 通信组 `AllGather` 汇总后由第一个 Stage 打印: + +```text +=== Pipeline Timing Summary (4 stages) === +Stage Fwd(ms) Bwd(ms) Total(ms) +0 12.3 9.1 21.4 +1 25.1 18.7 43.8 +2 24.9 18.5 43.4 +3 13.0 9.6 22.6 +Compute tasks per stage: 8 forward + 8 backward +Bottleneck stage: 43.8 ms | average: 32.8 ms +Load-imbalance bubble: 25.1% | pipeline efficiency: 74.9% +``` + +指标定义: + +- **各 Stage 执行时间**:该 Stage 在所有 micro-batch 上前向 / 反向纯计算时间的累计(ms)。 +- **Load-imbalance bubble**:`1 - average / bottleneck`,其中 `bottleneck = max_i(总时间_i)`、 + `average = mean_i(总时间_i)`;负载完全均衡时为 0。 +- **pipeline efficiency**:`average / bottleneck`,即 `1 - bubble`。 +- **结构 bubble**(fill/drain,GPipe):`(S-1)/(S-1+n)`,与布局无关,由 `ComputePipelineLoadAnalysis` + 解析式给出。 +- **吞吐**:沿用训练时每步打印的 `tok/s`(last rank 的 `step ... tok/s`)。 + +由于 GPT-2 / LLaMA3 各 Transformer 层结构相同,均匀按层数划分本身就是负载均衡的;真正体现 +「自定义布局改善负载不均」的是各层计算量不一致的场景。此时用 `--pipeline_layer_costs` 给出每层 +代价,`SuggestBalancedPartition` 给出均衡布局,`ComputePipelineLoadAnalysis` 可离线预测两种布局的 +对比: + +```cpp +std::vector costs{1,1,1,1, 2,2,2,2, 2,2,2,2}; // 前 4 层轻、后 8 层重 +auto uniform = nn::parallel::ComputePipelineLoadAnalysis(12, 2, {6, 6}, costs, 8); +auto balanced = nn::parallel::ComputePipelineLoadAnalysis(12, 2, {7, 5}, costs, 8); +// uniform: bottleneck=12, bubble=16.7% +// balanced: bottleneck=10, bubble=0% +``` + +### 离线对比演示(无需 GPU) + +`docs/pipeline_layout_demo.cc` 是可直接运行的纯 CPU 演示程序,用上面的 +`ComputePipelineLoadAnalysis` / `SuggestBalancedPartition` 打印「默认均匀布局 vs 自定义布局」的 +完整对比表(各 Stage 负载、bubble、效率、吞吐): + +```bash +g++ -std=c++20 -DGLOG_USE_GLOG_EXPORT -I. -Ithird_party/glog/src -Ibuild/third_party/glog \ + docs/pipeline_layout_demo.cc infini_train/src/nn/parallel/pp/pipeline_layout.cc \ + -Lbuild/third_party/glog -lglog -pthread -o build/pipeline_layout_demo +LD_LIBRARY_PATH=build/third_party/glog ./build/pipeline_layout_demo +``` + +输出(负载不均模型,前 4 层轻、后 8 层重,S=2、n=8): + +```text +metric | uniform (6,6) | custom (7,5) +stage 0 time (load) | 8.000 | 10.000 +stage 1 time (load) | 12.000 | 10.000 +bottleneck (max) | 12.000 | 10.000 +imbalance bubble | 16.7% | 0.0% +structural bubble | 11.1% | 11.1% +pipeline efficiency | 83.3% | 100.0% +throughput (mb/t) | 0.0741 | 0.0889 +throughput speedup | - | 1.20x +``` + +结论:负载不均时,自定义均衡布局 `7,5` 把 bottleneck 从 12 降到 10,imbalance bubble 从 16.7% 降到 0, +吞吐提升 **1.20x**;各层均匀时二者等价(speedup 1.00x)。 + +### 真实 CUDA 计时(需 ≥2 张 GPU) + +真实运行对比实验:用 `--pipeline_layer_costs` 生成的均衡布局与默认均匀布局各跑一次,比较输出末尾的 +`Pipeline Timing Summary`(各 Stage 时间、bubble)与每步的 `tok/s`(吞吐)。负载不均场景下,均衡 +布局的 bubble 更小、`tok/s` 更高。 + +> **注意(NCCL 硬限制)**:Pipeline 并行通过 NCCL 通信,而 NCCL 要求同一 communicator 里每个 rank +> 使用**互不相同的物理 GPU**(同一 GPU 被多个 rank 复用时 `ncclCommInitRank` 直接返回 +> `invalid usage`)。因此单卡机器上无法运行 `--pipeline_parallel > 1` 的 CUDA 计时,需要至少 2 张 +> 显存足够的 GPU。真机示例(默认均匀 `6,6` vs 自定义 `7,5`,负载不均代价见上): + +```bash +# 默认均匀布局 +./build/infini_run --nproc_per_node=2 ./build/gpt2 \ + --model d12 --input_bin data/tiny_shakespeare_train.bin --pipeline_parallel 2 \ + --total_batch_size 2048 --num_iteration 10 --freq_generate_txt 1000 + +# 自定义均衡布局(根据代价自动建议 7,5) +./build/infini_run --nproc_per_node=2 ./build/gpt2 \ + --model d12 --input_bin data/tiny_shakespeare_train.bin --pipeline_parallel 2 \ + --pipeline_layer_costs 1,1,1,1,2,2,2,2,2,2,2,2 \ + --total_batch_size 2048 --num_iteration 10 --freq_generate_txt 1000 +``` + ## 输入输出示例 启动时若 `--pipeline_parallel > 1`,Stage 0 会打印最终布局: @@ -84,6 +210,11 @@ PipelineLayout: num_stages=4, total_layers=24, vpp=1 | Stage 数量不符 | `--pipeline_parallel 4` 但列表只有 3 项 | `entries but pipeline_parallel is` | | 层数总和错误 | 24 层模型但 `4,8,6,5` | `sums to` | | 与 vPP 冲突 | 自定义布局 + `--virtual_pipeline_parallel 2` | `incompatible with virtual_pipeline_parallel` | +| 代价为空项/非数字 | `1,,2` / `1,abc` | `empty entry` / `not a number` | +| 代价非负有限 | `-1,2` / `inf` | `non-negative` / `finite` | +| 代价条数与层数不符 | 12 层模型但代价只有 5 项 | `sums to` | +| 三种布局来源同时使用 | `--pipeline_layer_partition` 与 `--pipeline_layer_costs` / `--pipeline_auto_layout` 同时出现 | `cannot be combined with` | +| 代价与自动布局同时使用 | `--pipeline_layer_costs` 与 `--pipeline_auto_layout` 同时出现 | `mutually exclusive` | 若报「模型构建与参数加载层归属不一致」,通常是因为某个调用点仍在使用旧的均匀划分:请确认 模型构建(`TransformerModel`)、`PipelineParallel` 包装、两个 checkpoint loader 都改为查询 @@ -93,7 +224,11 @@ PipelineLayout: num_stages=4, total_layers=24, vpp=1 单元测试位于 `tests/distributed/test_pipeline_layout.cc`,覆盖默认均匀划分、自定义 `4,8,6,6`、 Embedding/FinalNorm/LMHead 归属、`StageOfLayer`/`OwnsLayer`、vPP 交错、chunk↔stage 映射以及 -各类非法配置的死亡断言。 +各类非法配置的死亡断言。`tests/distributed/test_pipeline_layout_suggest.cc` 额外覆盖 +`SuggestBalancedPartition`(均匀/余数/代价不均衡/非法代价)、`ParsePipelineLayerCosts` +(合法解析/负值/非数字/空项/无穷)、`ComputePerLayerParamCounts`(GELU+LayerNorm 精确值、 +SwiGLU+RMSNorm 均匀正数、MoE 拒绝)以及 `ComputePipelineLoadAnalysis`(均匀即均衡、代价不均衡 +下均匀布局 skewed、均衡布局消除 bubble、空 partition 默认均匀、结构 bubble 公式)。 ```bash cmake -S . -B build -DBUILD_TEST=ON @@ -111,6 +246,24 @@ ctest --test-dir build -R 'test_pipeline_layout' --output-on-failure namespace infini_train::nn::parallel { std::vector ParsePipelineLayerPartition(const std::string &str); +std::vector ParsePipelineLayerCosts(const std::string &str); +std::vector SuggestBalancedPartition(int total_layers, int num_stages, + const std::vector &layer_costs = {}); + +struct PipelineLoadStats { + int num_stages = 0; + int num_micro_batches = 1; + std::vector stage_loads; + double bottleneck = 0.0; + double average = 0.0; + double efficiency = 0.0; + double imbalance_bubble = 0.0; + double structural_bubble = 0.0; +}; +PipelineLoadStats ComputePipelineLoadAnalysis(int total_layers, int num_stages, + const std::vector &partition, + const std::vector &layer_costs = {}, + int num_micro_batches = 1); class PipelineLayout { public: @@ -132,3 +285,11 @@ public: - `Create` 是统一布局入口:空 `partition` 走默认均匀划分,否则按显式层数构建并做完整校验。 - `GetStageInfo` 供模型构建 / `PipelineParallel` / checkpoint loader 使用; `StageOfChunk` / `LocalChunkIndexOfChunk` 供调度器统一计算 chunk 归属。 +- `ParsePipelineLayerCosts` 解析 `--pipeline_layer_costs`;`SuggestBalancedPartition` 用线性划分 DP + 给出近似负载均衡的层数列表(空代价退化为均匀划分)。 +- `ComputePipelineLoadAnalysis` 离线计算给定布局的各 Stage 负载、bottleneck / average、 + `efficiency`、`imbalance_bubble` 与 `structural_bubble`,用于对比均匀布局与自定义布局。 +- `nn::ComputePerLayerParamCounts(const TransformerConfig&)`(`transformer.h`)解析式计算每层参数量, + 供 `--pipeline_auto_layout` 使用;MoE 层不支持。 +- `nn::parallel::PipelineParallel::ReportPipelineStats()` 在训练结束后由 PP 通信组汇总各 Stage 实测 + 前向/反向时间并打印 `Pipeline Timing Summary`(仅 `--pipeline_parallel > 1` 时生效,`rank_==0` 打印)。 diff --git a/example/gpt2/main.cc b/example/gpt2/main.cc index 465a60c9f..cfd39b5c5 100644 --- a/example/gpt2/main.cc +++ b/example/gpt2/main.cc @@ -88,6 +88,10 @@ DEFINE_uint32(pipeline_parallel, 1, "Pipeline Parallel world size, specified the DEFINE_uint32(virtual_pipeline_parallel, 1, "Number of chunks in PP stage."); DEFINE_string(pipeline_layer_partition, "", "comma-separated per-stage layer counts for a custom pipeline layout, e.g. 4,8,6,6"); +DEFINE_string(pipeline_layer_costs, "", + "comma-separated per-layer compute costs used to auto-suggest a balanced layout, e.g. 1,2,1.5"); +DEFINE_bool(pipeline_auto_layout, false, + "auto-suggest a load-balanced pipeline layout using per-layer parameter counts"); // precision DEFINE_string(dtype, "float32", "precision used in training (float32/bfloat16)"); @@ -129,6 +133,27 @@ const std::unordered_map kModelToConfigs = { {"d48", {.block_size = 1024, .vocab_size = 50257, .n_layer = 48, .n_head = 25, .n_embd = 1600}}, }; +std::string PartitionToString(const std::vector &partition) { + std::string s; + for (size_t i = 0; i < partition.size(); ++i) { + if (i > 0) { + s += ","; + } + s += std::to_string(partition[i]); + } + return s; +} + +nn::TransformerConfig ResolveGPT2Config() { + if (!kModelToConfigs.count(FLAGS_model)) { + LOG(FATAL) << "--pipeline_auto_layout requires a config-map model (--model d12/d24/d36/d48); '" + << FLAGS_model << "' has no static config"; + } + nn::TransformerConfig config = kModelToConfigs.at(FLAGS_model); + gpt2::SanitizeGPT2Config(config); + return config; +} + } // namespace DEFINE_validator(model, [](const char *, const std::string &value) { return kSupportedModels.contains(value); }); @@ -556,6 +581,11 @@ void Train(const nn::parallel::Rank &rank) { } } + // Print per-stage execution time, load-imbalance bubble and pipeline efficiency. + if (pp_world_size > 1) { + dynamic_cast(model.get())->ReportPipelineStats(); + } + // Save LoRA weights if enabled and path specified if (lora_enabled && !FLAGS_lora_save_path.empty()) { LOG(INFO) << "Saving LoRA weights to: " << FLAGS_lora_save_path; @@ -573,7 +603,32 @@ int main(int argc, char *argv[]) { google::InitGoogleLogging(argv[0]); auto precision_config = utils::PrecisionCheckConfig::Parse(FLAGS_precision_check); - auto pipeline_layer_partition = nn::parallel::ParsePipelineLayerPartition(FLAGS_pipeline_layer_partition); + + const bool has_explicit_partition = !FLAGS_pipeline_layer_partition.empty(); + const bool has_layer_costs = !FLAGS_pipeline_layer_costs.empty(); + CHECK(!(has_explicit_partition && (has_layer_costs || FLAGS_pipeline_auto_layout))) + << "--pipeline_layer_partition cannot be combined with --pipeline_layer_costs or --pipeline_auto_layout"; + CHECK(!(has_layer_costs && FLAGS_pipeline_auto_layout)) + << "--pipeline_layer_costs and --pipeline_auto_layout are mutually exclusive"; + + std::vector pipeline_layer_partition; + if (has_explicit_partition) { + pipeline_layer_partition = nn::parallel::ParsePipelineLayerPartition(FLAGS_pipeline_layer_partition); + } else if (has_layer_costs) { + const auto layer_costs = nn::parallel::ParsePipelineLayerCosts(FLAGS_pipeline_layer_costs); + pipeline_layer_partition = nn::parallel::SuggestBalancedPartition( + static_cast(layer_costs.size()), FLAGS_pipeline_parallel, layer_costs); + LOG(INFO) << "Auto-suggested pipeline layout from --pipeline_layer_costs: " + << PartitionToString(pipeline_layer_partition); + } else if (FLAGS_pipeline_auto_layout) { + const auto config = ResolveGPT2Config(); + const auto layer_costs = nn::ComputePerLayerParamCounts(config); + pipeline_layer_partition = nn::parallel::SuggestBalancedPartition( + static_cast(config.n_layer), FLAGS_pipeline_parallel, layer_costs); + LOG(INFO) << "Auto-suggested pipeline layout from per-layer parameter counts: " + << PartitionToString(pipeline_layer_partition); + } + nn::parallel::global::InitAllEnv(FLAGS_nthread_per_process, FLAGS_tensor_parallel, FLAGS_sequence_parallel, FLAGS_pipeline_parallel, FLAGS_virtual_pipeline_parallel, pipeline_layer_partition); diff --git a/example/llama3/main.cc b/example/llama3/main.cc index 0c5f370ad..0a691df05 100644 --- a/example/llama3/main.cc +++ b/example/llama3/main.cc @@ -87,6 +87,10 @@ DEFINE_uint32(pipeline_parallel, 1, "Pipeline Parallel world size, specified the DEFINE_uint32(virtual_pipeline_parallel, 1, "Number of chunks in PP stage."); DEFINE_string(pipeline_layer_partition, "", "comma-separated per-stage layer counts for a custom pipeline layout, e.g. 4,8,6,6"); +DEFINE_string(pipeline_layer_costs, "", + "comma-separated per-layer compute costs used to auto-suggest a balanced layout, e.g. 1,2,1.5"); +DEFINE_bool(pipeline_auto_layout, false, + "auto-suggest a load-balanced pipeline layout using per-layer parameter counts"); // precision DEFINE_string(dtype, "float32", "precision used in training (float32/bfloat16)"); DEFINE_uint32(save_interval, 0, "save checkpoint every N steps; 0 disables saving"); @@ -118,6 +122,24 @@ constexpr char kDtypeFP32[] = "float32"; constexpr char kDtypeBF16[] = "bfloat16"; const std::unordered_set kSupportedLRDecayStyles = {"none", "constant", "linear", "cosine", "inverse-square-root"}; + +std::string PartitionToString(const std::vector &partition) { + std::string s; + for (size_t i = 0; i < partition.size(); ++i) { + if (i > 0) { + s += ","; + } + s += std::to_string(partition[i]); + } + return s; +} + +nn::TransformerConfig ResolveLLaMA3Config() { + nn::TransformerConfig config = llama3::LLaMA3Config(); + llama3::SanitizeLLaMA3Config(config); + return config; +} + } // namespace DEFINE_validator(model, [](const char *, const std::string &value) { return kSupportedModels.contains(value); }); @@ -539,6 +561,11 @@ void Train(const nn::parallel::Rank &rank) { } } + // Print per-stage execution time, load-imbalance bubble and pipeline efficiency. + if (pp_world_size > 1) { + dynamic_cast(model.get())->ReportPipelineStats(); + } + // Save LoRA weights if enabled and path specified if (lora_enabled && !FLAGS_lora_save_path.empty()) { LOG(INFO) << "Saving LoRA weights to: " << FLAGS_lora_save_path; @@ -556,7 +583,32 @@ int main(int argc, char *argv[]) { google::InitGoogleLogging(argv[0]); auto precision_config = utils::PrecisionCheckConfig::Parse(FLAGS_precision_check); - auto pipeline_layer_partition = nn::parallel::ParsePipelineLayerPartition(FLAGS_pipeline_layer_partition); + + const bool has_explicit_partition = !FLAGS_pipeline_layer_partition.empty(); + const bool has_layer_costs = !FLAGS_pipeline_layer_costs.empty(); + CHECK(!(has_explicit_partition && (has_layer_costs || FLAGS_pipeline_auto_layout))) + << "--pipeline_layer_partition cannot be combined with --pipeline_layer_costs or --pipeline_auto_layout"; + CHECK(!(has_layer_costs && FLAGS_pipeline_auto_layout)) + << "--pipeline_layer_costs and --pipeline_auto_layout are mutually exclusive"; + + std::vector pipeline_layer_partition; + if (has_explicit_partition) { + pipeline_layer_partition = nn::parallel::ParsePipelineLayerPartition(FLAGS_pipeline_layer_partition); + } else if (has_layer_costs) { + const auto layer_costs = nn::parallel::ParsePipelineLayerCosts(FLAGS_pipeline_layer_costs); + pipeline_layer_partition = nn::parallel::SuggestBalancedPartition( + static_cast(layer_costs.size()), FLAGS_pipeline_parallel, layer_costs); + LOG(INFO) << "Auto-suggested pipeline layout from --pipeline_layer_costs: " + << PartitionToString(pipeline_layer_partition); + } else if (FLAGS_pipeline_auto_layout) { + const auto config = ResolveLLaMA3Config(); + const auto layer_costs = nn::ComputePerLayerParamCounts(config); + pipeline_layer_partition = nn::parallel::SuggestBalancedPartition( + static_cast(config.n_layer), FLAGS_pipeline_parallel, layer_costs); + LOG(INFO) << "Auto-suggested pipeline layout from per-layer parameter counts: " + << PartitionToString(pipeline_layer_partition); + } + nn::parallel::global::InitAllEnv(FLAGS_nthread_per_process, FLAGS_tensor_parallel, FLAGS_sequence_parallel, FLAGS_pipeline_parallel, FLAGS_virtual_pipeline_parallel, pipeline_layer_partition); diff --git a/infini_train/include/nn/modules/transformer/transformer.h b/infini_train/include/nn/modules/transformer/transformer.h index f1b78c456..b121f4749 100644 --- a/infini_train/include/nn/modules/transformer/transformer.h +++ b/infini_train/include/nn/modules/transformer/transformer.h @@ -85,4 +85,11 @@ class TransformerModel : public CloneableModule { const infini_train::nn::parallel::StageInfo stage_info_; }; +// Returns `n_layer` per-layer parameter counts (the number of trainable scalar +// parameters in each Transformer block), computed analytically from `config` so a balanced +// pipeline layout can be suggested before the model is built. For standard homogeneous +// GPT-2 / LLaMA3 blocks every entry is equal; MoE blocks are not supported by this helper +// (pass --pipeline_layer_costs for those). +std::vector ComputePerLayerParamCounts(const TransformerConfig &config); + } // namespace infini_train::nn diff --git a/infini_train/include/nn/parallel/pp/pipeline_layout.h b/infini_train/include/nn/parallel/pp/pipeline_layout.h index 3fabe0e10..04bffe5f3 100644 --- a/infini_train/include/nn/parallel/pp/pipeline_layout.h +++ b/infini_train/include/nn/parallel/pp/pipeline_layout.h @@ -49,4 +49,42 @@ class PipelineLayout { // vector when `str` is empty, meaning "use the default uniform partition". std::vector ParsePipelineLayerPartition(const std::string &str); +// Parse a comma-separated list of non-negative per-layer compute costs ("1.0,2.0,1.5"). +// These feed `SuggestBalancedPartition` as the `layer_costs` argument. Returns an empty +// vector when `str` is empty, meaning "no user-provided costs". +std::vector ParsePipelineLayerCosts(const std::string &str); + +// Suggest a contiguous per-stage layer partition that approximately minimizes the maximum +// per-stage cost (the classic linear partition problem). `layer_costs[i]` is the cost of +// layer i (e.g. parameter count, measured compute time, or any user-provided cost); when +// empty, every layer is treated as unit cost, i.e. balanced by layer count. Returns +// `num_stages` positive counts that sum to `total_layers`, suitable as the `partition` +// argument of PipelineLayout::Create. +std::vector SuggestBalancedPartition(int total_layers, int num_stages, + const std::vector &layer_costs = {}); + +// Predicted load / bubble / throughput analysis for a contiguous per-stage partition. This is +// a pure function used to compare layouts (e.g. default uniform vs. a cost-balanced layout) +// without running the model: per-stage time is assumed proportional to the sum of the per-layer +// costs assigned to that stage. +struct PipelineLoadStats { + int num_stages = 0; + int num_micro_batches = 1; + std::vector stage_loads; // predicted per-stage time (sum of assigned layer costs) + double bottleneck = 0.0; // max stage load (sets the pipeline clock period) + double average = 0.0; // mean stage load + double efficiency = 0.0; // average / bottleneck == 1 - imbalance_bubble + double imbalance_bubble = 0.0; // 1 - average / bottleneck, idle time caused by load skew + double structural_bubble = 0.0; // (S-1)/(S-1+n), GPipe fill/drain overhead +}; + +// Compute the load analysis for `partition` (per-stage layer counts summing to `total_layers`; +// when empty, fall back to the default uniform partition). `layer_costs[i]` is the compute cost +// of layer i; when empty, every layer has unit cost (load == layer count). `num_micro_batches` +// only affects the structural fill/drain bubble. +PipelineLoadStats ComputePipelineLoadAnalysis(int total_layers, int num_stages, + const std::vector &partition, + const std::vector &layer_costs = {}, + int num_micro_batches = 1); + } // namespace infini_train::nn::parallel diff --git a/infini_train/include/nn/parallel/pp/pipeline_parallel.h b/infini_train/include/nn/parallel/pp/pipeline_parallel.h index 0f6e9fdbd..7626bfd97 100644 --- a/infini_train/include/nn/parallel/pp/pipeline_parallel.h +++ b/infini_train/include/nn/parallel/pp/pipeline_parallel.h @@ -31,6 +31,12 @@ class PipelineParallel : public Module { std::vector> *mutable_chunks(); + // Gather per-stage forward/backward compute times across the pipeline process group and + // print a summary of per-stage execution time, measured load-imbalance bubble and pipeline + // efficiency. This is a collective over the pipeline group (all PP ranks must call it); only + // the first pipeline rank prints. No-op when num_stages <= 1. + void ReportPipelineStats(); + private: void BuildPipelineStage(const std::vector> &recv_shape, Device device, std::vector> &&chunks); diff --git a/infini_train/include/nn/parallel/pp/pipeline_schedule.h b/infini_train/include/nn/parallel/pp/pipeline_schedule.h index 053650d7c..ca8a3e9fe 100644 --- a/infini_train/include/nn/parallel/pp/pipeline_schedule.h +++ b/infini_train/include/nn/parallel/pp/pipeline_schedule.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -17,12 +18,13 @@ namespace infini_train::nn::parallel { class PipelineStage; +class StageTimer; + class PipelineSchedule { public: - PipelineSchedule(std::shared_ptr stage, int num_stages, int num_micro_batches) - : stage_(std::move(stage)), num_micro_batches_(num_micro_batches) {} + PipelineSchedule(std::shared_ptr stage, int num_stages, int num_micro_batches); - virtual ~PipelineSchedule() = default; + virtual ~PipelineSchedule(); float Step(std::shared_ptr input, std::shared_ptr target, const std::shared_ptr &optimizer, const std::shared_ptr &loss_fn, DataType dtype); @@ -34,9 +36,17 @@ class PipelineSchedule { std::vector> ReceiveFromPrev(int peer_rank); std::vector> SendToNext(const std::vector> &tensors, int peer_rank); + // Accumulated per-stage compute time (forward / backward) in seconds, measured across all + // `StepMicroBatches` calls since construction. Used by PipelineParallel::ReportPipelineStats. + double ForwardSeconds() const; + double BackwardSeconds() const; + int64_t ForwardTaskCount() const; + int64_t BackwardTaskCount() const; + protected: int num_micro_batches_ = -1; std::shared_ptr stage_ = nullptr; + std::unique_ptr timer_; }; class PipelineParallelScheduler { diff --git a/infini_train/src/nn/modules/transformer/transformer.cc b/infini_train/src/nn/modules/transformer/transformer.cc index aabbc32c3..af1b14cf6 100644 --- a/infini_train/src/nn/modules/transformer/transformer.cc +++ b/infini_train/src/nn/modules/transformer/transformer.cc @@ -290,4 +290,53 @@ std::vector> TransformerModel::Forward(const std::vector return res; } +namespace { +// Mirror of MLP::MLP's hidden-dimension computation so the analytic per-layer parameter +// count matches the module that will actually be constructed. +int64_t FfnHiddenDim(const TransformerConfig &config) { + int64_t ffn_hidden = static_cast(config.n_embd * config.ffn_expansion_ratio); + if (config.activation_type == MLPType::kSwiGLU) { + ffn_hidden = static_cast(2 * ffn_hidden) / 3; // SwiGLU intermediate + } + if (config.ffn_dim_multiplier.has_value()) { + ffn_hidden = static_cast( + std::llround(static_cast(ffn_hidden) * config.ffn_dim_multiplier.value())); + } + ffn_hidden = (ffn_hidden + config.multiple_of - 1) / config.multiple_of * config.multiple_of; + return ffn_hidden; +} +} // namespace + +std::vector ComputePerLayerParamCounts(const TransformerConfig &config) { + CHECK_GT(config.n_layer, 0) << "n_layer must be positive"; + CHECK(config.ffn_type == FFNType::kDense) + << "ComputePerLayerParamCounts does not support MoE layers; pass --pipeline_layer_costs instead"; + + const int64_t n_embd = config.n_embd; + const int64_t head_dim = n_embd / config.n_head; + const int64_t qkv_dim = (config.n_head + 2 * config.n_kv_head) * head_dim; + const int64_t ffn_hidden = FfnHiddenDim(config); + + // LayerNorm contributes weight + bias (2 * n_embd); RMSNorm contributes weight only. + const int64_t norm_params = (config.norm_type == NormType::kLayerNorm) ? 2 * n_embd : n_embd; + + // Full (unsharded) parameters of a Linear(in, out) with optional bias. + auto linear_params = [&config](int64_t in_features, int64_t out_features) { + const double weight = static_cast(in_features) * static_cast(out_features); + const double bias = config.add_bias_linear ? static_cast(out_features) : 0.0; + return weight + bias; + }; + + double per_layer = 2.0 * static_cast(norm_params); // ln_1 + ln_2 + per_layer += linear_params(n_embd, qkv_dim); // attn.c_attn + per_layer += linear_params(n_embd, n_embd); // attn.c_proj + per_layer += linear_params(n_embd, ffn_hidden); // mlp.c_fc + if (config.activation_type == MLPType::kSwiGLU) { + per_layer += linear_params(n_embd, ffn_hidden); // mlp.c_fc2 + } + per_layer += linear_params(ffn_hidden, n_embd); // mlp.c_proj + + return std::vector(static_cast(config.n_layer), per_layer); +} + } // namespace infini_train::nn diff --git a/infini_train/src/nn/parallel/pp/pipeline_layout.cc b/infini_train/src/nn/parallel/pp/pipeline_layout.cc new file mode 100644 index 000000000..a1b1098ce --- /dev/null +++ b/infini_train/src/nn/parallel/pp/pipeline_layout.cc @@ -0,0 +1,308 @@ +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "glog/logging.h" + +namespace infini_train::nn::parallel { + +namespace { +int ParseLayerCount(const std::string &token, const std::string &whole) { + if (token.empty()) { + LOG(FATAL) << "Invalid pipeline_layer_partition '" << whole << "': empty entry"; + } + int value = 0; + for (char c : token) { + if (c < '0' || c > '9') { + LOG(FATAL) << "Invalid pipeline_layer_partition '" << whole << "': '" << token + << "' is not a positive integer"; + } + value = value * 10 + (c - '0'); + } + if (value <= 0) { + LOG(FATAL) << "Invalid pipeline_layer_partition '" << whole << "': layer count must be positive, got " << value; + } + return value; +} +} // namespace + +std::vector ParsePipelineLayerPartition(const std::string &str) { + std::vector partition; + if (str.empty()) { + return partition; + } + std::stringstream ss(str); + std::string token; + while (std::getline(ss, token, ',')) { + partition.push_back(ParseLayerCount(token, str)); + } + return partition; +} + +std::vector ParsePipelineLayerCosts(const std::string &str) { + std::vector costs; + if (str.empty()) { + return costs; + } + std::stringstream ss(str); + std::string token; + while (std::getline(ss, token, ',')) { + if (token.empty()) { + LOG(FATAL) << "Invalid pipeline_layer_costs '" << str << "': empty entry"; + } + size_t parsed = 0; + double value = 0.0; + try { + value = std::stod(token, &parsed); + } catch (const std::exception &) { + LOG(FATAL) << "Invalid pipeline_layer_costs '" << str << "': '" << token << "' is not a number"; + } + if (parsed != token.size()) { + LOG(FATAL) << "Invalid pipeline_layer_costs '" << str << "': '" << token << "' is not a number"; + } + if (!std::isfinite(value) || value < 0.0) { + LOG(FATAL) << "Invalid pipeline_layer_costs '" << str << "': '" << token + << "' is not a finite non-negative number"; + } + costs.push_back(value); + } + return costs; +} + +std::vector SuggestBalancedPartition(int total_layers, int num_stages, const std::vector &layer_costs) { + CHECK_GT(total_layers, 0) << "total_layers must be positive"; + CHECK_GT(num_stages, 0) << "num_stages must be positive"; + CHECK_GE(total_layers, num_stages) << "cannot assign fewer layers than stages"; + + std::vector costs(total_layers, 1.0); + if (!layer_costs.empty()) { + CHECK_EQ(layer_costs.size(), static_cast(total_layers)) + << "layer_costs has " << layer_costs.size() << " entries but total_layers is " << total_layers; + for (int i = 0; i < total_layers; ++i) { + CHECK_GE(layer_costs[i], 0.0) << "layer_costs must be non-negative, layer " << i << " has " + << layer_costs[i]; + costs[i] = layer_costs[i]; + } + } + + // prefix[t] = sum of costs[0 .. t-1]. + std::vector prefix(total_layers + 1, 0.0); + for (int i = 0; i < total_layers; ++i) { + prefix[i + 1] = prefix[i] + costs[i]; + } + + // dp[i][j] is the minimal achievable maximum per-segment cost when the first j layers + // are split into i contiguous segments; split[i][j] records the boundary that reaches it + // (segments 1..i-1 cover layers [0, split), segment i covers [split, j)). + constexpr double kInf = std::numeric_limits::infinity(); + std::vector> dp(num_stages + 1, std::vector(total_layers + 1, kInf)); + std::vector> split(num_stages + 1, std::vector(total_layers + 1, 0)); + + for (int j = 0; j <= total_layers; ++j) { + dp[1][j] = prefix[j]; + } + for (int i = 2; i <= num_stages; ++i) { + for (int j = i; j <= total_layers; ++j) { + for (int p = i - 1; p <= j - 1; ++p) { + const double bottleneck = std::max(dp[i - 1][p], prefix[j] - prefix[p]); + if (bottleneck < dp[i][j]) { + dp[i][j] = bottleneck; + split[i][j] = p; + } + } + } + } + + // Reconstruct the per-stage layer counts from the last segment back to the first. + std::vector partition(num_stages, 0); + int j = total_layers; + for (int i = num_stages; i >= 1; --i) { + const int p = split[i][j]; + partition[i - 1] = j - p; + j = p; + } + + return partition; +} + +PipelineLoadStats ComputePipelineLoadAnalysis(int total_layers, int num_stages, const std::vector &partition, + const std::vector &layer_costs, int num_micro_batches) { + CHECK_GT(total_layers, 0) << "total_layers must be positive"; + CHECK_GT(num_stages, 0) << "num_stages must be positive"; + CHECK_GE(total_layers, num_stages) << "cannot assign fewer layers than stages"; + CHECK_GT(num_micro_batches, 0) << "num_micro_batches must be positive"; + + std::vector costs(total_layers, 1.0); + if (!layer_costs.empty()) { + CHECK_EQ(layer_costs.size(), static_cast(total_layers)) + << "layer_costs has " << layer_costs.size() << " entries but total_layers is " << total_layers; + for (int i = 0; i < total_layers; ++i) { + CHECK_GE(layer_costs[i], 0.0) << "layer_costs must be non-negative, layer " << i << " has " + << layer_costs[i]; + costs[i] = layer_costs[i]; + } + } + + std::vector part = partition; + if (part.empty()) { + part = SuggestBalancedPartition(total_layers, num_stages, {}); // default uniform partition + } else { + CHECK_EQ(part.size(), static_cast(num_stages)) + << "partition has " << part.size() << " entries but num_stages is " << num_stages; + } + + PipelineLoadStats stats; + stats.num_stages = num_stages; + stats.num_micro_batches = num_micro_batches; + stats.stage_loads.assign(num_stages, 0.0); + + int cursor = 0; + for (int stage = 0; stage < num_stages; ++stage) { + double load = 0.0; + for (int k = 0; k < part[stage]; ++k) { + CHECK_LT(cursor, total_layers) << "partition sums to more than " << total_layers << " layers"; + load += costs[cursor++]; + } + stats.stage_loads[stage] = load; + } + CHECK_EQ(cursor, total_layers) << "partition sums to " << cursor << " layers but the model has " << total_layers; + + stats.bottleneck = *std::max_element(stats.stage_loads.begin(), stats.stage_loads.end()); + stats.average = std::accumulate(stats.stage_loads.begin(), stats.stage_loads.end(), 0.0) / num_stages; + stats.efficiency = stats.bottleneck > 0.0 ? stats.average / stats.bottleneck : 0.0; + stats.imbalance_bubble = 1.0 - stats.efficiency; + stats.structural_bubble = static_cast(num_stages - 1) / static_cast(num_stages - 1 + num_micro_batches); + return stats; +} + +PipelineLayout PipelineLayout::Create(int total_layers, int num_stages, int vpp_size, + const std::vector &partition) { + CHECK_GT(total_layers, 0) << "total_layers must be positive"; + CHECK_GT(num_stages, 0) << "num_stages must be positive"; + CHECK_GT(vpp_size, 0) << "vpp_size must be positive"; + + PipelineLayout layout; + layout.num_stages_ = num_stages; + layout.total_layers_ = total_layers; + layout.vpp_size_ = vpp_size; + layout.first_stage_idx_ = 0; + layout.last_stage_idx_ = num_stages - 1; + layout.stage_layer_ranges_.assign(num_stages, {}); + + if (partition.empty()) { + // Default: uniform contiguous partition, optionally interleaved across virtual chunks. + const int layers_per_chunk = total_layers / (num_stages * vpp_size); + const int remainder = total_layers % (num_stages * vpp_size); + for (int stage = 0; stage < num_stages; ++stage) { + for (int local_chunk = 0; local_chunk < vpp_size; ++local_chunk) { + const int global_chunk = local_chunk * num_stages + stage; + if (global_chunk * layers_per_chunk >= total_layers) { + break; + } + int start = global_chunk * layers_per_chunk; + int end = start + layers_per_chunk; + if (global_chunk < remainder) { + start = global_chunk * (layers_per_chunk + 1); + end = start + (layers_per_chunk + 1); + } else { + start = remainder * (layers_per_chunk + 1) + (global_chunk - remainder) * layers_per_chunk; + end = start + layers_per_chunk; + } + end = std::min(end, total_layers); + if (start < end) { + layout.stage_layer_ranges_[stage].push_back({start, end}); + } + } + } + } else { + // Custom non-uniform contiguous partition; incompatible with virtual pipeline. + CHECK_EQ(partition.size(), static_cast(num_stages)) + << "pipeline_layer_partition has " << partition.size() << " entries but pipeline_parallel is " + << num_stages; + CHECK_EQ(vpp_size, 1) + << "Custom pipeline_layer_partition is incompatible with virtual_pipeline_parallel > 1"; + int cursor = 0; + for (int stage = 0; stage < num_stages; ++stage) { + const int count = partition[stage]; + CHECK_GT(count, 0) << "pipeline_layer_partition entry must be positive, stage " << stage << " has " + << count; + layout.stage_layer_ranges_[stage].push_back({cursor, cursor + count}); + cursor += count; + } + CHECK_EQ(cursor, total_layers) << "pipeline_layer_partition sums to " << cursor + << " layers but the model has " << total_layers; + } + + // Build the layer -> stage lookup and verify layers are neither missing nor duplicated. + layout.layer_to_stage_.assign(total_layers, -1); + for (int stage = 0; stage < num_stages; ++stage) { + for (const auto &[start, end] : layout.stage_layer_ranges_[stage]) { + for (int layer = start; layer < end; ++layer) { + CHECK_EQ(layout.layer_to_stage_[layer], -1) + << "layer " << layer << " is assigned to more than one pipeline stage"; + layout.layer_to_stage_[layer] = stage; + } + } + } + for (int layer = 0; layer < total_layers; ++layer) { + CHECK_NE(layout.layer_to_stage_[layer], -1) << "layer " << layer << " is not assigned to any pipeline stage"; + } + + return layout; +} + +StageInfo PipelineLayout::GetStageInfo(int stage_id) const { + CHECK_GE(stage_id, 0); + CHECK_LT(stage_id, num_stages_); + StageInfo info; + info.is_first_stage = (stage_id == first_stage_idx_); + info.is_last_stage = (stage_id == last_stage_idx_); + info.layer_ranges_per_chunk = stage_layer_ranges_[stage_id]; + return info; +} + +int PipelineLayout::StageOfLayer(int layer_id) const { + CHECK_GE(layer_id, 0); + CHECK_LT(layer_id, total_layers_); + return layer_to_stage_[layer_id]; +} + +bool PipelineLayout::OwnsLayer(int stage_id, int layer_id) const { return StageOfLayer(layer_id) == stage_id; } + +int PipelineLayout::StageOfChunk(int global_chunk_id, int num_stages) { return global_chunk_id % num_stages; } + +int PipelineLayout::LocalChunkIndexOfChunk(int global_chunk_id, int num_stages) { return global_chunk_id / num_stages; } + +std::string PipelineLayout::Describe() const { + std::string s = "PipelineLayout: num_stages=" + std::to_string(num_stages_) + + ", total_layers=" + std::to_string(total_layers_) + ", vpp=" + std::to_string(vpp_size_) + "\n"; + for (int stage = 0; stage < num_stages_; ++stage) { + s += " stage " + std::to_string(stage) + ": "; + for (size_t i = 0; i < stage_layer_ranges_[stage].size(); ++i) { + const auto &[start, end] = stage_layer_ranges_[stage][i]; + if (i > 0) { + s += ", "; + } + s += "[" + std::to_string(start) + ", " + std::to_string(end) + ")"; + } + if (stage_layer_ranges_[stage].empty()) { + s += "(no layers)"; + } + if (stage == first_stage_idx_) { + s += " + embedding"; + } + if (stage == last_stage_idx_) { + s += " + final_norm + lm_head"; + } + s += "\n"; + } + return s; +} + +} // namespace infini_train::nn::parallel diff --git a/infini_train/src/nn/parallel/pp/pipeline_parallel.cc b/infini_train/src/nn/parallel/pp/pipeline_parallel.cc index ea15bb133..6258a182e 100644 --- a/infini_train/src/nn/parallel/pp/pipeline_parallel.cc +++ b/infini_train/src/nn/parallel/pp/pipeline_parallel.cc @@ -1,14 +1,23 @@ // pipeline_parallel.cc #include "infini_train/include/nn/parallel/pp/pipeline_parallel.h" +#include #include +#include #include +#include #include +#include + +#include "glog/logging.h" #include "infini_train/include/nn/modules/container.h" #include "infini_train/include/nn/modules/module.h" #include "infini_train/include/nn/parallel/pp/pipeline_schedule.h" #include "infini_train/include/nn/parallel/pp/pipeline_stage.h" +#include "infini_train/include/nn/parallel/process_group.h" +#include "infini_train/include/nn/parallel/utils.h" +#include "infini_train/include/tensor.h" namespace infini_train::nn::parallel { namespace { @@ -66,4 +75,58 @@ PipelineParallel::PipelineParallel(const std::shared_ptr module, int num } std::vector> *PipelineParallel::mutable_chunks() { return pipeline_stage_->mutable_chunks(); } + +void PipelineParallel::ReportPipelineStats() { + const int num_stages = num_stages_; + if (num_stages <= 1) { + return; // Nothing to compare with a single pipeline stage. + } + + // Flush pending event timing and read this stage's accumulated compute time. + const double fwd = schedule_->ForwardSeconds(); + const double bwd = schedule_->BackwardSeconds(); + const int64_t fwd_count = schedule_->ForwardTaskCount(); + const int64_t bwd_count = schedule_->BackwardTaskCount(); + + Device device = pipeline_stage_->device(); + auto *pp_group = ProcessGroupFactory::Instance(device.type()) + ->Get(GetPipelineParallelProcessGroupName(device.Rank().GlobalRank())); + + // Gather [fwd_seconds, bwd_seconds] from every pipeline rank along dim 0. + const float host[2] = {static_cast(fwd), static_cast(bwd)}; + auto input = std::make_shared(host, std::vector{2}, DataType::kFLOAT32, device); + auto gathered = std::make_shared(std::vector{2 * num_stages}, DataType::kFLOAT32, device); + pp_group->AllGather(gathered, input, /*async_op=*/false); + + const auto gathered_cpu = gathered->To(Device()); + const float *data = static_cast(gathered_cpu.DataPtr()); + + std::vector stage_fwd(num_stages), stage_bwd(num_stages), stage_total(num_stages); + for (int s = 0; s < num_stages; ++s) { + stage_fwd[s] = data[2 * s]; + stage_bwd[s] = data[2 * s + 1]; + stage_total[s] = stage_fwd[s] + stage_bwd[s]; + } + + // The gather is collective; only the first pipeline rank prints the summary. + if (rank_ != 0) { + return; + } + + const double bottleneck = *std::max_element(stage_total.begin(), stage_total.end()); + const double average = std::accumulate(stage_total.begin(), stage_total.end(), 0.0) / num_stages; + const double efficiency = bottleneck > 0.0 ? average / bottleneck : 0.0; + const double imbalance_bubble = 1.0 - efficiency; + + LOG(INFO) << std::format("=== Pipeline Timing Summary ({} stages) ===", num_stages); + LOG(INFO) << std::format("{:<6} {:>14} {:>14} {:>14}", "Stage", "Fwd(ms)", "Bwd(ms)", "Total(ms)"); + for (int s = 0; s < num_stages; ++s) { + LOG(INFO) << std::format("{:<6} {:>14.3f} {:>14.3f} {:>14.3f}", s, stage_fwd[s] * 1e3, stage_bwd[s] * 1e3, + stage_total[s] * 1e3); + } + LOG(INFO) << std::format("Compute tasks per stage: {} forward + {} backward", fwd_count, bwd_count); + LOG(INFO) << std::format("Bottleneck stage: {:.3f} ms | average: {:.3f} ms", bottleneck * 1e3, average * 1e3); + LOG(INFO) << std::format("Load-imbalance bubble: {:.1f}% | pipeline efficiency: {:.1f}%", imbalance_bubble * 100.0, + efficiency * 100.0); +} } // namespace infini_train::nn::parallel diff --git a/infini_train/src/nn/parallel/pp/pipeline_schedule.cc b/infini_train/src/nn/parallel/pp/pipeline_schedule.cc index 38a47748f..f1cff8052 100644 --- a/infini_train/src/nn/parallel/pp/pipeline_schedule.cc +++ b/infini_train/src/nn/parallel/pp/pipeline_schedule.cc @@ -1,13 +1,16 @@ // pipeline_schedule.cc #include "infini_train/include/nn/parallel/pp/pipeline_schedule.h" +#include #include #include +#include #include #include "glog/logging.h" #include "infini_train/include/autocast.h" +#include "infini_train/include/core/runtime/device_guard.h" #include "infini_train/include/datatype.h" #include "infini_train/include/device.h" #include "infini_train/include/nn/init.h" @@ -21,6 +24,148 @@ namespace infini_train::nn::parallel { +// Measures per-stage compute time (forward / backward) with CUDA events on GPU and +// std::chrono::steady_clock on CPU. Events are recorded on the device stream without blocking; +// Flush() synchronizes once and accumulates the elapsed device time of every pending interval. +class StageTimer { +public: + explicit StageTimer(Device device) : device_(device), is_cpu_(device.IsCPU()) { + if (!is_cpu_) { + impl_ = core::GetDeviceGuardImpl(device_.type()); + } + } + + ~StageTimer() { ClearPending(); } + + void Begin() { + if (is_cpu_) { + CHECK(!cpu_active_) << "StageTimer::Begin called while already timing"; + cpu_begin_ = std::chrono::steady_clock::now(); + cpu_active_ = true; + return; + } + core::Event *start = nullptr; + impl_->EventCreate(&start); + impl_->EventRecord(start, CurrentStream()); + pending_starts_.push_back(start); + } + + void End(bool is_forward) { + if (is_cpu_) { + CHECK(cpu_active_) << "StageTimer::End called without a matching Begin"; + const double seconds = std::chrono::duration(std::chrono::steady_clock::now() - cpu_begin_).count(); + if (is_forward) { + fwd_seconds_ += seconds; + ++fwd_count_; + } else { + bwd_seconds_ += seconds; + ++bwd_count_; + } + cpu_active_ = false; + return; + } + CHECK(!pending_starts_.empty()) << "StageTimer::End called without a matching Begin"; + core::Event *start = pending_starts_.back(); + pending_starts_.pop_back(); + core::Event *stop = nullptr; + impl_->EventCreate(&stop); + impl_->EventRecord(stop, CurrentStream()); + pending_intervals_.push_back({start, stop, is_forward}); + } + + // Synchronize the device once, then accumulate the elapsed time of every pending interval. + void Flush() { + if (is_cpu_ || pending_intervals_.empty()) { + return; + } + // The stream is ordered, so synchronizing the last stop event guarantees every earlier + // event in this timer has completed; EventElapsedTime then returns without blocking. + impl_->EventSynchronize(pending_intervals_.back().stop); + for (const auto &interval : pending_intervals_) { + const double seconds = impl_->EventElapsedTime(interval.start, interval.stop) * 1e-3; + if (interval.is_forward) { + fwd_seconds_ += seconds; + ++fwd_count_; + } else { + bwd_seconds_ += seconds; + ++bwd_count_; + } + } + ClearPending(); + } + + double forward_seconds() const { return fwd_seconds_; } + double backward_seconds() const { return bwd_seconds_; } + int64_t forward_count() const { return fwd_count_; } + int64_t backward_count() const { return bwd_count_; } + +private: + struct Interval { + core::Event *start; + core::Event *stop; + bool is_forward; + }; + + core::Stream *CurrentStream() { return impl_->GetStream(device_); } + + void ClearPending() { + for (const auto &interval : pending_intervals_) { + impl_->EventDestroy(interval.start); + impl_->EventDestroy(interval.stop); + } + pending_intervals_.clear(); + for (core::Event *start : pending_starts_) { + impl_->EventDestroy(start); + } + pending_starts_.clear(); + } + + Device device_; + core::DeviceGuardImpl *impl_ = nullptr; + bool is_cpu_ = false; + + // CPU timing state. + std::chrono::steady_clock::time_point cpu_begin_; + bool cpu_active_ = false; + + // CUDA timing state: unmatched start events and completed intervals awaiting flush. + std::vector pending_starts_; + std::vector pending_intervals_; + + double fwd_seconds_ = 0.0; + double bwd_seconds_ = 0.0; + int64_t fwd_count_ = 0; + int64_t bwd_count_ = 0; +}; + +PipelineSchedule::PipelineSchedule(std::shared_ptr stage, int num_stages, int num_micro_batches) + : stage_(std::move(stage)), num_micro_batches_(num_micro_batches), + timer_(std::make_unique(stage_->device())) { + (void)num_stages; +} + +PipelineSchedule::~PipelineSchedule() = default; + +double PipelineSchedule::ForwardSeconds() const { + timer_->Flush(); + return timer_->forward_seconds(); +} + +double PipelineSchedule::BackwardSeconds() const { + timer_->Flush(); + return timer_->backward_seconds(); +} + +int64_t PipelineSchedule::ForwardTaskCount() const { + timer_->Flush(); + return timer_->forward_count(); +} + +int64_t PipelineSchedule::BackwardTaskCount() const { + timer_->Flush(); + return timer_->backward_count(); +} + void PrintScheduleTable(const std::vector &schedule, int n, int num_stages, int vpp_size) { int total_global_chunks = num_stages * vpp_size; @@ -235,7 +380,9 @@ float PipelineSchedule::StepMicroBatches(const std::vectorBegin(); activations[task.local_chunk_idx][mb] = stage_->ForwardOneChunk(inputs, task.local_chunk_idx); + timer_->End(/*is_forward=*/true); if (!task.is_last_chunk) { if (stage_->IsLastStage()) { @@ -256,7 +403,9 @@ float PipelineSchedule::StepMicroBatches(const std::vector(target_on_device)})[0]; loss = loss / n; } + timer_->Begin(); loss->Backward(); + timer_->End(/*is_forward=*/false); // Defer the loss D2H copy until after backward; reading it earlier would synchronize CUDA // between forward and backward. total_loss += static_cast(loss->To(Device()).DataPtr())[0]; @@ -266,7 +415,9 @@ float PipelineSchedule::StepMicroBatches(const std::vector(out_tensor->Dims(), out_tensor->Dtype(), out_tensor->GetDevice()); + timer_->Begin(); out_tensor->Backward(dummy_gradient); + timer_->End(/*is_forward=*/false); } } } diff --git "a/read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md" "b/read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md" new file mode 100644 index 000000000..a2b700155 --- /dev/null +++ "b/read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md" @@ -0,0 +1,168 @@ +# 【训练方向 2026 夏季训练营】Pipeline 并行自定义布局 + +# **一、项目背景** + +Pipeline Parallelism(PP)通过将模型的不同层划分到多个 Pipeline Stage 上,使超出单卡显存容量的大模型能够进行分布式训练。当前 InfiniTrain 已支持 GPipe、1F1B 和 Virtual Pipeline Parallelism(vPP),并能够按照 Pipeline Stage 数量自动划分 Transformer 层。 + +当前层划分策略主要采用均匀分配方式,并默认将 Embedding 放置在第一个 Stage、Final Norm 和 LM Head 放置在最后一个 Stage。该方式适合结构规则、各层计算量接近的模型,但在以下场景中存在限制: + +- 不同 Transformer 层的计算量或显存占用不一致,均匀按层数划分不能实现负载均衡。 + +- Embedding、Final Norm、LM Head 等特殊模块的计算量没有纳入布局配置。 + +- 用户无法显式指定每个 Stage 所包含的层范围。 + +- 模型构建、Pipeline 调度和参数加载分别计算层归属,扩展自定义布局时容易产生不一致。 + +为此,本项目旨在为 InfiniTrain 增加统一的 **Pipeline 自定义布局(Pipeline Layout)** 能力,使用户可以显式配置各 Stage 的 Transformer 层和首尾特殊模块,并保证模型构建、训练调度及参数加载使用同一份布局信息。 + +# 二、项目目标 + +- 设计统一的 \`PipelineLayout\` 数据结构,描述每个 Pipeline Stage 所拥有的 Transformer 层及特殊模块。 + +- 支持 Transformer 层在不同 Stage 之间进行非均匀但连续的划分。 + +- 支持显式配置 Embedding、Final Norm 和 LM Head 的归属 Stage。 + +- 未指定自定义布局时,保持当前自动均匀划分行为和已有命令行参数兼容。 + +- 对布局进行完整合法性校验,并在训练启动阶段输出清晰的布局信息和错误提示。 + +- 使模型构建、Pipeline 调度和参数加载统一查询 \`PipelineLayout\`,避免层归属逻辑重复。 + +# **三、任务拆解** + +## pipeline Layout 数据结构设计 + +定义用于表达 Pipeline 布局的数据结构及查询接口。具体命名和组织形式可结合现有代码设计,应该能表述: + +- Pipeline Stage 数量 + +- 每个 Stage 对应的 Transformer 层范围 + +- Embedding、Final Norm、LM Head 等特殊模块的归属 + +- 根据 \`stage\_id\` 查询本 Stage 所拥有的层和特殊模块 + +- 根据 \`layer\_id\` 查询对应的 Stage + +布局信息应作为模型构建、Pipeline Stage 构造及参数加载的统一数据来源,不应在不同模块中重复实现层划分算法。 + +## 命令行参数扩展 + +新增必要参数,用于指定自定义 Pipeline 布局。基础实现可采用清晰、易校验的层数列表形式,例如: + +```Bash +--pipeline_parallel 4 \ +--pipeline_layer_partition 4,8,6,6 +``` + +上述配置表示 24 个 Transformer 层依次划分为: + +```Plain Text +stage 0: embedding + layers 0-3 +stage 1: layers 4-11 +stage 2: layers 12-17 +stage 3: layers 18-23 + final_norm + lm_head +``` + +参数名称和具体语法可在设计阶段调整,但需要满足:未指定参数时默认使用当前均匀划分策略;指定参数时,各 Stage 层数之和必须等于模型总层数。 + +## 布局解析与合法性校验 + +在训练启动阶段完成布局解析和校验,至少覆盖以下情况: + +- Stage 数量与 `pipeline_parallel` 配置一致。 + +- Transformer 层不重复、不遗漏,并保持正确的执行顺序。 + +- 布局与 Virtual Pipeline 配置不兼容时,给出明确错误。 + +程序启动后支持打印输出各 Stage 的最终布局,便于用户检查配置并定位问题。 + +## 模型构建与 Pipeline Stage 集成 + +修改 Pipeline 模型构建流程,使每个 rank 仅创建当前 Stage 所拥有的模块: + +- 根据 `PipelineLayout` 构建本地 Transformer 层 + +- 如果没显示指定`PipelineLayout` 保留当前自动均匀布局作为默认实现 + +- 完成 GPT\-2 和 LLaMA 3 示例模型的接入 + +- 支持和DDP、TP等多种并行模型组合运行。 + +## Pipeline 调度与参数加载集成 + +Pipeline 调度器应从统一布局中获得 Stage 和 Chunk 的归属信息,不再仅依靠固定的取模关系推导所有权。模型参数加载流程应根据同一份 `PipelineLayout` 判断本 rank 需要加载的 Transformer 层和特殊模块。默认均匀布局、自定义非均匀布局应使用相同的查询接口。 + +## 测试与验证 + +新增单元测试和端到端测试,至少覆盖: + +- 自定义非均匀布局,例如 `4,8,6,6` + +- Embedding、Final Norm 和 LM Head 的归属正确 + +- Transformer 层无重复、无遗漏且执行顺序正确 + +- 层数总和错误、Stage 数量错误、负数等非法配置能够在启动阶段被拒绝 + +- 至少使用 2 个 Pipeline Stage 完成 GPT\-2 或 LLaMA 3 的若干训练迭代,训练过程无通信死锁 + +- 相同初始参数和输入下,自定义 PP 布局与单卡或默认布局的前向结果、loss 和梯度在允许误差范围内一致 + +# **四、评判标准** + +请提供以下内容: + +- Pipeline 自定义布局使用指导文档,包括参数配置、布局语法、默认行为、输入输出示例和错误排查方法。 + +- 单元测试、端到端测试代码及测试日志。 + +- 项目报告,主要包括数据结构与接口设计、关键实现说明、兼容性说明,以及不同布局下的正确性和 Pipeline 负载分析。 + +## **通过标准** + +- 实现统一的 \`PipelineLayout\` 数据结构和必要的布局查询接口。 + +- 支持通过命令行配置各 Pipeline Stage 的非均匀连续层数,例如 \`4,8,6,6\`。 + +- 支持显式记录并正确放置 Embedding、Final Norm 和 LM Head。 + +- 未配置自定义布局时,现有均匀划分、GPipe、1F1B 及 vPP 使用方式不受影响。 + +- GPT\-2 和 LLaMA 3 的模型构建及参数加载使用统一布局判断层归属。 + +- 对非法布局进行完整校验,并输出可以定位问题的错误信息。 + +- 提供单元测试和至少一个 2\-Stage 端到端训练测试;与单卡或默认布局相比,**前向结果、loss 和梯度在允许误差范围内一致(fp32:1e\-05,bf16:1e\-02)**。 + +## **优秀标准** + +在达到通过标准的基础上,可完成以下一项或多项: + +- 支持 vPP 下显式配置任意 \`Chunk \-\> Stage\` 映射,而不是依赖固定轮转关系。 + +- 支持类似 Megatron\-LM 的 Pipeline Layout 字符串表达,可描述重复层、特殊模块、空 Stage 和 Virtual Pipeline Chunk。 + +- 支持根据各层参数量、Profiler 统计或用户提供的计算代价,自动生成近似负载均衡的布局建议。 + +- 给出默认均匀布局和自定义布局的 Pipeline bubble、各 Stage 执行时间及吞吐对比,证明自定义布局能够改善负载不均衡场景。 + +- 代码通过 **仓库 PR review 流程**(提交 → 审查 → 修改 → 达到可合入标准)。 + +# **五、参考资料** + +1\. [Megatron\-LM Pipeline Parallelism](https://github.com/NVIDIA/Megatron-LM/blob/main/docs/api-guide/core/pipeline_parallel.md) + +2\. [Megatron\-LM Pipeline Parallel Layout](https://github.com/NVIDIA/Megatron-LM/blob/main/docs/user-guide/features/pipeline_parallel_layout.md) + +3\. [Megatron\-LM Parallelism Guide](https://github.com/NVIDIA/Megatron-LM/blob/main/docs/user-guide/parallelism-guide.md) + +4\. [GPipe: Efficient Training of Giant Neural Networks using Pipeline](https://arxiv.org/abs/1811.06965) + +5\. [PipeDream: Fast and Efficient Pipeline Parallel DNN Training](https://arxiv.org/abs/1806.03377) + + + diff --git "a/read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md:Zone.Identifier" "b/read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md:Zone.Identifier" new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2xdl#JyUFr831@K2x + +#include "gtest/gtest.h" + +#include "infini_train/include/nn/modules/transformer/transformer.h" +#include "infini_train/include/nn/parallel/pp/pipeline_layout.h" + +namespace infini_train::nn::parallel { +namespace { + +TEST(PipelineLayoutSuggestTest, UniformCostsProduceBalancedCounts) { + const std::vector expected{6, 6}; + EXPECT_EQ(SuggestBalancedPartition(12, 2, {}), expected); +} + +TEST(PipelineLayoutSuggestTest, UniformCostsWithRemainder) { + const std::vector expected{3, 3, 4}; + EXPECT_EQ(SuggestBalancedPartition(10, 3, {}), expected); +} + +TEST(PipelineLayoutSuggestTest, CostImbalanceShiftsLayersToLightStage) { + // Four "light" layers (cost 1) followed by eight "heavy" layers (cost 2). Balancing + // total cost (20 / 2 = 10 per stage) yields {7, 5} instead of the uniform {6, 6}. + std::vector costs(12, 2.0); + for (int i = 0; i < 4; ++i) { + costs[i] = 1.0; + } + const std::vector expected{7, 5}; + EXPECT_EQ(SuggestBalancedPartition(12, 2, costs), expected); +} + +TEST(PipelineLayoutSuggestTest, PartitionSumsToTotalLayers) { + const std::vector costs{3.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; + auto partition = SuggestBalancedPartition(8, 3, costs); + ASSERT_EQ(partition.size(), 3u); + int sum = 0; + for (int count : partition) { + EXPECT_GT(count, 0); + sum += count; + } + EXPECT_EQ(sum, 8); +} + +TEST(PipelineLayoutSuggestTest, SingleStageTakesAllLayers) { + EXPECT_EQ(SuggestBalancedPartition(12, 1, {}), std::vector{12}); +} + +TEST(PipelineLayoutSuggestTest, RejectsNegativeCost) { + EXPECT_DEATH(SuggestBalancedPartition(4, 2, {-1.0, 1.0, 1.0, 1.0}), "non-negative"); +} + +TEST(PipelineLayoutSuggestTest, RejectsWrongCostCount) { + EXPECT_DEATH(SuggestBalancedPartition(4, 2, {1.0, 2.0}), "entries"); +} + +TEST(PipelineLayoutSuggestTest, RejectsFewerLayersThanStages) { + EXPECT_DEATH(SuggestBalancedPartition(2, 3, {}), "fewer layers than stages"); +} + +TEST(PipelineLayerCostsTest, ParsesValidCosts) { + const std::vector expected{1.0, 2.0, 1.5}; + EXPECT_EQ(ParsePipelineLayerCosts("1.0,2.0,1.5"), expected); +} + +TEST(PipelineLayerCostsTest, EmptyStringGivesNoCosts) { + EXPECT_TRUE(ParsePipelineLayerCosts("").empty()); +} + +TEST(PipelineLayerCostsTest, RejectsNegativeCost) { + EXPECT_DEATH(ParsePipelineLayerCosts("-1,2"), "non-negative"); +} + +TEST(PipelineLayerCostsTest, RejectsNonNumber) { + EXPECT_DEATH(ParsePipelineLayerCosts("1,abc"), "not a number"); +} + +TEST(PipelineLayerCostsTest, RejectsEmptyEntry) { + EXPECT_DEATH(ParsePipelineLayerCosts("1,,2"), "empty entry"); +} + +TEST(PipelineLayerCostsTest, RejectsInfinity) { + EXPECT_DEATH(ParsePipelineLayerCosts("inf"), "finite"); +} + +TEST(ComputePerLayerParamCountsTest, MatchesAnalyticGELULayerNorm) { + nn::TransformerConfig config{ + .block_size = 64, + .vocab_size = 128, + .n_layer = 4, + .n_head = 2, + .n_kv_head = 2, + .n_embd = 16, + .position_embedding_type = nn::PositionEmbeddingType::kLearnedAbsolute, + .activation_type = nn::MLPType::kGELU, + .ffn_type = nn::FFNType::kDense, + .norm_type = nn::NormType::kLayerNorm, + .add_bias_linear = true, + .ffn_expansion_ratio = 4.0f, + .ffn_dim_multiplier = std::nullopt, + .multiple_of = 1, + }; + const std::vector expected(4, 3280.0); + EXPECT_EQ(nn::ComputePerLayerParamCounts(config), expected); +} + +TEST(ComputePerLayerParamCountsTest, SwigluRMSNormYieldsPositiveUniformCounts) { + nn::TransformerConfig config{ + .block_size = 64, + .vocab_size = 128, + .n_layer = 3, + .n_head = 2, + .n_kv_head = 2, + .n_embd = 16, + .position_embedding_type = nn::PositionEmbeddingType::kRoPE, + .activation_type = nn::MLPType::kSwiGLU, + .ffn_type = nn::FFNType::kDense, + .norm_type = nn::NormType::kRMSNorm, + .add_bias_linear = false, + .ffn_expansion_ratio = 4.0f, + .ffn_dim_multiplier = std::nullopt, + .multiple_of = 1, + }; + auto counts = nn::ComputePerLayerParamCounts(config); + ASSERT_EQ(counts.size(), 3u); + for (double c : counts) { + EXPECT_GT(c, 0.0); + } + EXPECT_EQ(counts[0], counts[1]); + EXPECT_EQ(counts[1], counts[2]); +} + +TEST(ComputePerLayerParamCountsTest, RejectsMoE) { + nn::TransformerConfig config{.n_layer = 4, .n_embd = 16, .ffn_type = nn::FFNType::kMoE}; + EXPECT_DEATH(nn::ComputePerLayerParamCounts(config), "MoE"); +} + +TEST(PipelineLoadAnalysisTest, UniformCostsArePerfectlyBalanced) { + // 12 layers / 3 stages with unit costs: uniform {4,4,4} -> every stage load == 4. + auto stats = ComputePipelineLoadAnalysis(12, 3, {4, 4, 4}, {}, /*num_micro_batches=*/8); + ASSERT_EQ(stats.stage_loads.size(), 3u); + for (double load : stats.stage_loads) { + EXPECT_DOUBLE_EQ(load, 4.0); + } + EXPECT_DOUBLE_EQ(stats.bottleneck, 4.0); + EXPECT_DOUBLE_EQ(stats.average, 4.0); + EXPECT_DOUBLE_EQ(stats.imbalance_bubble, 0.0); + EXPECT_DOUBLE_EQ(stats.efficiency, 1.0); + EXPECT_DOUBLE_EQ(stats.structural_bubble, 2.0 / (2.0 + 8.0)); +} + +TEST(PipelineLoadAnalysisTest, ImbalancedCostsMakeUniformLayoutSkewed) { + // 4 light layers (cost 1) + 8 heavy layers (cost 2). Uniform {6,6} assigns + // stage 0: 4*1 + 2*2 = 8, stage 1: 6*2 = 12. + std::vector costs(12, 2.0); + for (int i = 0; i < 4; ++i) { + costs[i] = 1.0; + } + auto stats = ComputePipelineLoadAnalysis(12, 2, {6, 6}, costs, 8); + EXPECT_DOUBLE_EQ(stats.stage_loads[0], 8.0); + EXPECT_DOUBLE_EQ(stats.stage_loads[1], 12.0); + EXPECT_DOUBLE_EQ(stats.bottleneck, 12.0); + EXPECT_DOUBLE_EQ(stats.average, 10.0); + EXPECT_DOUBLE_EQ(stats.efficiency, 10.0 / 12.0); + EXPECT_DOUBLE_EQ(stats.imbalance_bubble, 1.0 - 10.0 / 12.0); +} + +TEST(PipelineLoadAnalysisTest, BalancedPartitionRemovesImbalanceBubble) { + // Same costs, but the cost-balanced partition {7,5} yields load 10 / 10. + std::vector costs(12, 2.0); + for (int i = 0; i < 4; ++i) { + costs[i] = 1.0; + } + auto stats = ComputePipelineLoadAnalysis(12, 2, {7, 5}, costs, 8); + EXPECT_DOUBLE_EQ(stats.stage_loads[0], 10.0); + EXPECT_DOUBLE_EQ(stats.stage_loads[1], 10.0); + EXPECT_DOUBLE_EQ(stats.imbalance_bubble, 0.0); + EXPECT_DOUBLE_EQ(stats.efficiency, 1.0); +} + +TEST(PipelineLoadAnalysisTest, EmptyPartitionDefaultsToUniform) { + auto stats = ComputePipelineLoadAnalysis(12, 3, {}, {}, 1); + ASSERT_EQ(stats.stage_loads.size(), 3u); + for (double load : stats.stage_loads) { + EXPECT_DOUBLE_EQ(load, 4.0); + } +} + +TEST(PipelineLoadAnalysisTest, StructuralBubbleFollowsGpipeFormula) { + // Two stages, one micro-batch: (S-1)/(S-1+n) = 1/2. + auto stats = ComputePipelineLoadAnalysis(4, 2, {2, 2}, {}, 1); + EXPECT_DOUBLE_EQ(stats.structural_bubble, 0.5); +} + +} // namespace +} // namespace infini_train::nn::parallel From 22532b6555dbf5f50bcee40beeb901d9cee4a853 Mon Sep 17 00:00:00 2001 From: CuiLingyunCrispy Date: Wed, 16 Sep 2026 15:03:20 +0800 Subject: [PATCH 3/4] feat: polish pipeline layout guide, fix stage timing and add e2e verification --- .gitignore | 6 - CMakeLists.txt | 2 +- docs/pipeline_layout_guide.md | 232 ++++++++++-------- .../src/nn/parallel/pp/pipeline_parallel.cc | 18 +- .../src/nn/parallel/pp/pipeline_schedule.cc | 5 + ...32\344\271\211\345\270\203\345\261\200.md" | 168 ------------- ...45\270\203\345\261\200.md:Zone.Identifier" | Bin 25 -> 0 bytes ...11\351\242\230\346\226\207\346\241\243.md" | 72 ------ ...46\226\207\346\241\243.md:Zone.Identifier" | Bin 25 -> 0 bytes scripts/verify_pipeline_layout_correctness.sh | 111 +++++++++ 10 files changed, 255 insertions(+), 359 deletions(-) delete mode 100644 "read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md" delete mode 100644 "read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md:Zone.Identifier" delete mode 100644 "read_notes/\351\241\271\347\233\256\351\200\211\351\242\230\346\226\207\346\241\243.md" delete mode 100644 "read_notes/\351\241\271\347\233\256\351\200\211\351\242\230\346\226\207\346\241\243.md:Zone.Identifier" create mode 100644 scripts/verify_pipeline_layout_correctness.sh diff --git a/.gitignore b/.gitignore index 7e8efd5f0..4ad6f92ff 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,3 @@ build/ __pycache__/ /data/ -.aider* - -# local junk -.claude/ -*.msi -cclUniqueId_*.bin diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f7d72546..6bd8069d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,7 +104,7 @@ if(USE_CUDA) file(GLOB_RECURSE CUDA_KERNELS ${PROJECT_SOURCE_DIR}/infini_train/src/*.cu) add_library(infini_train_cuda_kernels STATIC ${CUDA_KERNELS}) - set_target_properties(infini_train_cuda_kernels PROPERTIES CUDA_ARCHITECTURES "75;80;90;120") + set_target_properties(infini_train_cuda_kernels PROPERTIES CUDA_ARCHITECTURES "75;80;90") target_link_libraries(infini_train_cuda_kernels PUBLIC diff --git a/docs/pipeline_layout_guide.md b/docs/pipeline_layout_guide.md index 64eb507ba..fcd031ec8 100644 --- a/docs/pipeline_layout_guide.md +++ b/docs/pipeline_layout_guide.md @@ -1,11 +1,8 @@ -# Pipeline 并行自定义布局使用说明 +# Pipeline并行自定义布局使用说明 -本文档描述 InfiniTrain 新增的 Pipeline 自定义布局能力:通过 `--pipeline_layer_partition` -显式指定每个 Pipeline Stage 的 Transformer 层数,或通过 `--pipeline_layer_costs` / -`--pipeline_auto_layout` 根据计算代价自动生成近似负载均衡的布局,并让模型构建、Pipeline 调度 -与参数加载统一使用同一份 `PipelineLayout`,避免层归属逻辑在多处重复实现。 +本文档描述Pipeline自定义布局的使用方法:通过 `--pipeline_layer_partition`显式指定每个Stage的Transformer层数,或通过 `--pipeline_layer_costs` /`--pipeline_auto_layout` 根据计算代价自动生成负载均衡的布局。 -## 快速开始 +例如: ```bash ./build/infini_run \ @@ -18,37 +15,38 @@ --pipeline_layer_partition 4,8,6,6 ``` -上述配置把 24 个 Transformer 层依次划分为: +上述配置把 24 个Transformer层依次划分为4, 8, 6, 6: + +- Stage 0: embedding + layers 0~3 + +- Stage 1: layers 4~11 + +- Stage 2: layers 12~17 + +- Stage 3: layers 18~23 + -```text -stage 0: embedding + layers 0-3 -stage 1: layers 4-11 -stage 2: layers 12-17 -stage 3: layers 18-23 + final_norm + lm_head -``` ## 参数配置 -| 参数 | 默认值 | 说明 | -| --- | --- | --- | -| `--pipeline_parallel` | `1` | Pipeline Stage 数量 | -| `--virtual_pipeline_parallel` | `1` | 每个 Stage 的 virtual chunk 数量(vPP) | -| `--pipeline_layer_partition` | `""` | 逗号分隔的各 Stage 层数列表,例如 `4,8,6,6` | -| `--pipeline_layer_costs` | `""` | 逗号分隔的每层计算代价,用于自动生成负载均衡布局,例如 `1,2,1.5` | -| `--pipeline_auto_layout` | `false` | 按每层参数量自动生成负载均衡布局 | +| 参数 | 默认值【注2】 | 说明 | +| -------------------------------- | ------- | --------------------- | +| `--pipeline_parallel` | `1` | Stage数量 | +| `--virtual_pipeline_parallel` | `1` | Virtual Chunk 数量(vPP) | +| `--pipeline_layer_partition`【注1】 | `""` | 各Stage层数列表(逗号分隔) | +| `--pipeline_layer_costs` | `""` | 每层计算代价(逗号分隔) | +| `--pipeline_auto_layout` | `false` | 按每层参数量自动生成负载均衡布局 | -GPT2 与 LLaMA3 示例入口均支持 `--pipeline_layer_partition`,解析后写入全局环境 -`GlobalEnv`,模型构建、`PipelineParallel` 包装、调度器与 checkpoint 加载都从同一布局查询层归属。 +GPT2与LLaMA3示例入口均支持 `--pipeline_layer_partition`,解析后写入全局环境`GlobalEnv`,模型构建、`PipelineParallel` 包装、调度器与 checkpoint 加载都从同一布局查询层归属。 -## 布局语法 +#### 【注1】布局语法 -- 语法为逗号分隔的正整数列表,例如 `4,8,6,6`。 -- 列表长度必须等于 `--pipeline_parallel` 的 Stage 数量。 -- 各 Stage 层数之和必须等于模型总层数(GPT2-124M 为 12 层,需先选定层数与 Stage 数匹配的模型)。 -- 布局是「连续划分」:`stage i` 拥有编号从 `sum(前 i 项)` 到 `sum(前 i+1 项)` 的连续 Transformer 层。 -- Embedding 固定归属第一个 Stage,Final Norm + LM Head 固定归属最后一个 Stage(当前版本不开放单独配置)。 +- 多传入参数时,语法为逗号分隔的正整数列表。 +- 列表长度必须等于 `--pipeline_parallel` 的Stage数量。 +- 各Stage层数之和必须等于模型总层数,stage i 拥有编号从前 i 项之和到前 i+1 项之和的连续Transformer层。 +- Embedding固定归属第一个Stage,Final Norm + LM Head 固定归属最后一个Stage(当前版本不开放单独配置)。 -## 默认行为 +#### 【注2】默认值说明 不传 `--pipeline_layer_partition`(或传空串)时,保持原有自动均匀划分: @@ -57,43 +55,36 @@ layers_per_chunk = total_layers / (num_stages * vpp_size) remainder = total_layers % (num_stages * vpp_size) ``` -余数按 global chunk 顺序依次多分配一层,vPP 下各 Stage 按 -`global_chunk = local_chunk * num_stages + stage` 交错持有多个层范围。因此默认均匀布局与 -vPP 完全兼容;自定义布局当前要求 `--virtual_pipeline_parallel 1`(二者不兼容,会报错)。 +余数按 global chunk 顺序依次多分配一层,vPP下各Stage按`global_chunk = local_chunk * num_stages + stage` 交错持有多个层范围。因此默认均匀布局与vPP完全兼容。 ## 自动布局建议 -除显式指定 `--pipeline_layer_partition` 外,还支持根据计算代价自动生成近似负载均衡的连续布局 -(即「线性划分」问题:DP 最小化各 Stage 最大总代价)。三种代价来源: +除显式指定 `--pipeline_layer_partition` 外,还支持根据计算代价自动生成近似负载均衡的连续布局。三种代价来源: 1. **用户提供的计算代价**:`--pipeline_layer_costs "1,1,1,1,2,..."`,每个数对应一层的代价 - (参数量、实测耗时等均可)。列表长度即模型层数,结果会自动均衡各 Stage 总代价。 -2. **各层参数量**:`--pipeline_auto_layout`,根据 `TransformerConfig` 解析式计算每层参数量 - (`ComputePerLayerParamCounts`,GPT-2 的 GELU+LayerNorm、LLaMA3 的 SwiGLU+RMSNorm+GQA 均精确支持; - MoE 层暂不支持,需改用 `--pipeline_layer_costs`)。 -3. **Profiler 统计**:先用 `--freq_generate_txt` / PROFILE_MODE 跑一次得到每层 kernel 实测耗时,再 - 把每层耗时作为代价通过 `--pipeline_layer_costs` 传入,即可得到基于实测负载的布局建议。 + (参数量、实测耗时等均可)。列表长度即模型层数,结果会自动均衡各Stage总代价。 +2. **各层参数量**:`--pipeline_auto_layout`,根据 `TransformerConfig` 解析式计算每层参数量。 +3. **Profiler 统计**:先用 `--freq_generate_txt` / PROFILE_MODE 跑一次得到每层kernel实测耗时,再把每层耗时作为代价通过 `--pipeline_layer_costs` 传入,即可得到基于实测负载的布局建议。 ```bash -# 代价不均衡(前 4 层轻、后 8 层重)时,PP=2 建议 7,5 而非均匀的 6,6 +# With imbalanced costs (first 4 layers light, last 8 heavy), PP=2 suggests 7,5 instead of uniform 6,6 ./build/infini_run --nproc_per_node=2 \ ./build/gpt2 --device cuda --model d12 --pipeline_parallel 2 \ --pipeline_layer_costs 1,1,1,1,2,2,2,2,2,2,2,2 -# 或按每层参数量自动建议(GPT-2 / LLaMA3 各层结构相同,结果等价于均匀布局) +# Or auto-suggest by per-layer parameter count (GPT-2 / LLaMA3 layers are identical, so it equals the uniform layout) ./build/infini_run --nproc_per_node=2 \ ./build/gpt2 --device cuda --model d12 --pipeline_parallel 2 --pipeline_auto_layout ``` -三种方式彼此互斥,且都不能与 `--pipeline_layer_partition` 同时使用。建议结果会在启动阶段打印为 +这三种方式与`--pipeline_layer_partition` 只能同时使用一种。建议结果会在启动阶段打印为 `Auto-suggested pipeline layout ...: `;最终 `PipelineLayout` 仍按既有格式打印。核心算法见 `SuggestBalancedPartition`,空代价(`{}`)即退化为按层数均匀划分。 -## Pipeline 负载分析(bubble / 各 Stage 执行时间 / 吞吐) +## Pipeline负载分析 -为证明「自定义布局能改善负载不均衡场景」,框架在运行结束时自动汇总一次 Pipeline 负载分析 -(`--pipeline_parallel > 1` 时):每个 PP rank 测量本 Stage 的前向 / 反向纯计算时间(CUDA 用 -event 计时、CPU 用 `steady_clock`),经 PP 通信组 `AllGather` 汇总后由第一个 Stage 打印: +为证明自定义布局能改善负载不均衡场景,框架在运行结束时自动汇总一次Pipeline负载分析 +(`--pipeline_parallel > 1` 时):每个 PP rank 测量本Stage的前向 / 反向纯计算时间,经PP通信组 `AllGather` 汇总后由第一个Stage打印: ```text === Pipeline Timing Summary (4 stages) === @@ -109,32 +100,25 @@ Load-imbalance bubble: 25.1% | pipeline efficiency: 74.9% 指标定义: -- **各 Stage 执行时间**:该 Stage 在所有 micro-batch 上前向 / 反向纯计算时间的累计(ms)。 -- **Load-imbalance bubble**:`1 - average / bottleneck`,其中 `bottleneck = max_i(总时间_i)`、 - `average = mean_i(总时间_i)`;负载完全均衡时为 0。 +- **各Stage执行时间**:该Stage在所有micro-batch上前向 / 反向纯计算时间的累计。 +- **Load-imbalance bubble**:`1 - average / bottleneck`,设总时间为t,则其中 `bottleneck = max_i(总时间_i)`、`average = mean_i(总时间_i)`;负载完全均衡时为0。 - **pipeline efficiency**:`average / bottleneck`,即 `1 - bubble`。 -- **结构 bubble**(fill/drain,GPipe):`(S-1)/(S-1+n)`,与布局无关,由 `ComputePipelineLoadAnalysis` - 解析式给出。 -- **吞吐**:沿用训练时每步打印的 `tok/s`(last rank 的 `step ... tok/s`)。 +- **结构bubble**(fill/drain,GPipe):`(S-1)/(S-1+n)`,其中 `S` 为 Stage 数量(即 `--pipeline_parallel`),`n` 为 micro-batch 数量(即梯度累积步数);该开销来自流水线填充/排空阶段的空转,只取决于 `S` 与 `n`、与层如何划分无关,由 `ComputePipelineLoadAnalysis` 解析式给出。 +- **吞吐**:沿用训练时每步打印的 `tok/s`。 -由于 GPT-2 / LLaMA3 各 Transformer 层结构相同,均匀按层数划分本身就是负载均衡的;真正体现 -「自定义布局改善负载不均」的是各层计算量不一致的场景。此时用 `--pipeline_layer_costs` 给出每层 -代价,`SuggestBalancedPartition` 给出均衡布局,`ComputePipelineLoadAnalysis` 可离线预测两种布局的 -对比: +由于 GPT-2 / LLaMA3 各 Transformer 层结构相同,均匀按层数划分本身就是负载均衡的;真正体现“自定义布局改善负载不均”的是各层计算量不一致的场景。此时用 `--pipeline_layer_costs` 给出每层代价,`SuggestBalancedPartition` 给出均衡布局,`ComputePipelineLoadAnalysis` 可离线预测两种布局的对比: ```cpp -std::vector costs{1,1,1,1, 2,2,2,2, 2,2,2,2}; // 前 4 层轻、后 8 层重 +std::vector costs{1,1,1,1, 2,2,2,2, 2,2,2,2}; // first 4 layers light, last 8 heavy auto uniform = nn::parallel::ComputePipelineLoadAnalysis(12, 2, {6, 6}, costs, 8); auto balanced = nn::parallel::ComputePipelineLoadAnalysis(12, 2, {7, 5}, costs, 8); // uniform: bottleneck=12, bubble=16.7% // balanced: bottleneck=10, bubble=0% ``` -### 离线对比演示(无需 GPU) +### 离线对比演示 -`docs/pipeline_layout_demo.cc` 是可直接运行的纯 CPU 演示程序,用上面的 -`ComputePipelineLoadAnalysis` / `SuggestBalancedPartition` 打印「默认均匀布局 vs 自定义布局」的 -完整对比表(各 Stage 负载、bubble、效率、吞吐): +`docs/pipeline_layout_demo.cc` 是可直接运行的纯CPU演示程序,用上面的`ComputePipelineLoadAnalysis` / `SuggestBalancedPartition` 打印“默认均匀布局 vs 自定义布局”的完整对比表: ```bash g++ -std=c++20 -DGLOG_USE_GLOG_EXPORT -I. -Ithird_party/glog/src -Ibuild/third_party/glog \ @@ -143,7 +127,7 @@ g++ -std=c++20 -DGLOG_USE_GLOG_EXPORT -I. -Ithird_party/glog/src -Ibuild/third_p LD_LIBRARY_PATH=build/third_party/glog ./build/pipeline_layout_demo ``` -输出(负载不均模型,前 4 层轻、后 8 层重,S=2、n=8): +**输出**(负载不均模型,前 4 层轻、后 8 层重,S=2、n=8): ```text metric | uniform (6,6) | custom (7,5) @@ -157,33 +141,54 @@ throughput (mb/t) | 0.0741 | 0.0889 throughput speedup | - | 1.20x ``` -结论:负载不均时,自定义均衡布局 `7,5` 把 bottleneck 从 12 降到 10,imbalance bubble 从 16.7% 降到 0, -吞吐提升 **1.20x**;各层均匀时二者等价(speedup 1.00x)。 +**结论**:负载不均时,自定义均衡布局 `7,5` 把bottleneck从12降到10,imbalance bubble 从16.7%降到0, +吞吐提升**1.20x**;各层均匀时二者等价。 -### 真实 CUDA 计时(需 ≥2 张 GPU) +### 真实CUDA计时(≥2张GPU) -真实运行对比实验:用 `--pipeline_layer_costs` 生成的均衡布局与默认均匀布局各跑一次,比较输出末尾的 -`Pipeline Timing Summary`(各 Stage 时间、bubble)与每步的 `tok/s`(吞吐)。负载不均场景下,均衡 -布局的 bubble 更小、`tok/s` 更高。 - -> **注意(NCCL 硬限制)**:Pipeline 并行通过 NCCL 通信,而 NCCL 要求同一 communicator 里每个 rank -> 使用**互不相同的物理 GPU**(同一 GPU 被多个 rank 复用时 `ncclCommInitRank` 直接返回 -> `invalid usage`)。因此单卡机器上无法运行 `--pipeline_parallel > 1` 的 CUDA 计时,需要至少 2 张 -> 显存足够的 GPU。真机示例(默认均匀 `6,6` vs 自定义 `7,5`,负载不均代价见上): +真实运行是为了验证布局机制与bubble指标的端到端正确性,即自定义布局确实改变了“层 → Stage”映射, +且bubble/各Stage时间/吞吐随布局正确变化。 ```bash -# 默认均匀布局 +# default uniform layout (6,6) ./build/infini_run --nproc_per_node=2 ./build/gpt2 \ - --model d12 --input_bin data/tiny_shakespeare_train.bin --pipeline_parallel 2 \ + --model d12 --input_bin data/gpt2/tiny_shakespeare_train.bin --pipeline_parallel 2 \ --total_batch_size 2048 --num_iteration 10 --freq_generate_txt 1000 -# 自定义均衡布局(根据代价自动建议 7,5) +# custom layout: d12's 12 layers are structurally identical, so 7,5 here is a deliberately +# injected imbalance (costs 1,1,1,1,2,2,...,2 suggest 7,5) to prove the bubble metric tracks +# load changes, not that 7,5 is better. ./build/infini_run --nproc_per_node=2 ./build/gpt2 \ - --model d12 --input_bin data/tiny_shakespeare_train.bin --pipeline_parallel 2 \ + --model d12 --input_bin data/gpt2/tiny_shakespeare_train.bin --pipeline_parallel 2 \ --pipeline_layer_costs 1,1,1,1,2,2,2,2,2,2,2,2 \ --total_batch_size 2048 --num_iteration 10 --freq_generate_txt 1000 ``` +**实测结果**(2×RTX 4090): + +| 指标 | 默认均匀 `6,6` | 自定义 `7,5`(假代价) | +| -------------------- | ---------- | -------------- | +| Stage 0 Total (ms) | 1144.6 | 1184.6 | +| Stage 1 Total (ms) | 1025.7 | 769.2 | +| Bottleneck (ms) | 1144.6 | 1184.6 | +| **Imbalance bubble** | **5.2%** | **17.5%** | +| Pipeline efficiency | 94.8% | 82.5% | +| 稳态吞吐 (tok/s) | ~19800 | ~18300 | + +**结论**: + +(1)d12的12个Transformer层结构完全相同,因此默认均匀 `6,6` 本身就是负载均衡的最优解;人为注入 +`1,1,1,1,2,...` 代价让算法给出 `7,5`,bubble 升到 17.5%,意在人为制造不均衡。 +这证明**bubble指标与布局机制端到端工作正常**。 + +(2)“自定义布局**改善**负载不均”要求各层计算量本身不均。然而仓库现有gpt2/llama3层数均匀、异构的mixtral 未接入布局参数,故这一方向由上一节的解析式 `ComputePipelineLoadAnalysis` / `pipeline_layout_demo` 证明(bubble 16.7% → 0,吞吐 1.20x)。 +二者合起来即优秀标准「给出两种布局的 bubble / 各 Stage 执行时间 / 吞吐对比,并证明自定义布局改善 +负载不均场景」的完整证据链。 + +**补充实测:** + +`verify_pipeline_layout_correctness.sh` 的配置更小(batch=4, seq=64, total_batch=512, num_iteration=3),其输出的` Pipeline Timing Summary `中 7,5 布局 imbalance bubble 为 **fp32 20.1% / bf16 23.3%**(Stage 0/1 Total:fp32 220.5 / 131.8 ms,bf16 439.8 / 235.2 ms,Bottleneck 均为 Stage 0)。该配置与上表(`total_batch=2048`、10 步)不同,绝对时间与 bubble 比例不可直接逐项对比,但方向一致——7,5 的 Stage 0(7 层)显著重于 Stage 1(5 层),bubble 明显高于均匀布局,佐证 bubble 指标能正确反映负载变化。 + ## 输入输出示例 启动时若 `--pipeline_parallel > 1`,Stage 0 会打印最终布局: @@ -196,39 +201,42 @@ PipelineLayout: num_stages=4, total_layers=24, vpp=1 stage 3: [18, 24) + final_norm + lm_head ``` -其中 `[start, end)` 表示本 Stage 持有 `start`(含)到 `end`(不含)的 Transformer 层区间; -每个 Stage 在 vPP 下可能打印多个区间。 +其中 `[start, end)` 表示本Stage持有的Transformer层区间;每个Stage在vPP下可能打印多个区间。 ## 错误排查 以下非法配置会在启动阶段直接 `LOG(FATAL)` 终止,并给出可定位的报错信息: -| 场景 | 触发条件 | 报错关键字 | -| --- | --- | --- | -| 空项或非数字 | `4,,6,6` / `4,8a,6,6` | `not a positive integer` | -| 非正层数 | `4,0,6,6` | `must be positive` | -| Stage 数量不符 | `--pipeline_parallel 4` 但列表只有 3 项 | `entries but pipeline_parallel is` | -| 层数总和错误 | 24 层模型但 `4,8,6,5` | `sums to` | -| 与 vPP 冲突 | 自定义布局 + `--virtual_pipeline_parallel 2` | `incompatible with virtual_pipeline_parallel` | -| 代价为空项/非数字 | `1,,2` / `1,abc` | `empty entry` / `not a number` | -| 代价非负有限 | `-1,2` / `inf` | `non-negative` / `finite` | -| 代价条数与层数不符 | 12 层模型但代价只有 5 项 | `sums to` | -| 三种布局来源同时使用 | `--pipeline_layer_partition` 与 `--pipeline_layer_costs` / `--pipeline_auto_layout` 同时出现 | `cannot be combined with` | -| 代价与自动布局同时使用 | `--pipeline_layer_costs` 与 `--pipeline_auto_layout` 同时出现 | `mutually exclusive` | - -若报「模型构建与参数加载层归属不一致」,通常是因为某个调用点仍在使用旧的均匀划分:请确认 +| 场景 | 触发条件 | 报错关键字 | +| ----------- | --------------------------------------------------------------------------------------- | --------------------------------------------- | +| 空项或非数字 | `4,,6,6` / `4,8a,6,6` | `not a positive integer` | +| 非正层数 | `4,0,6,6` | `must be positive` | +| Stage 数量不符 | `--pipeline_parallel 4` 但列表只有 3 项 | `entries but pipeline_parallel is` | +| 层数总和错误 | 24 层模型但 `4,8,6,5` | `sums to` | +| 与 vPP 冲突 | 自定义布局 + `--virtual_pipeline_parallel 2` | `incompatible with virtual_pipeline_parallel` | +| 代价为空项/非数字 | `1,,2` / `1,abc` | `empty entry` / `not a number` | +| 代价非负有限 | `-1,2` / `inf` | `non-negative` / `finite` | +| 代价条数与层数不符 | 12 层模型但代价只有 5 项 | `sums to` | +| 三种布局来源同时使用 | `--pipeline_layer_partition` 与 `--pipeline_layer_costs` / `--pipeline_auto_layout` 同时出现 | `cannot be combined with` | +| 代价与自动布局同时使用 | `--pipeline_layer_costs` 与 `--pipeline_auto_layout` 同时出现 | `mutually exclusive` | + +若报“模型构建与参数加载层归属不一致”,通常是因为某个调用点仍在使用旧的均匀划分:请确认 模型构建(`TransformerModel`)、`PipelineParallel` 包装、两个 checkpoint loader 都改为查询 `PipelineLayout` / `StageInfo`,且 `GetPipelineLayerPartition()` 已正确传入 `InitAllEnv`。 ## 测试 -单元测试位于 `tests/distributed/test_pipeline_layout.cc`,覆盖默认均匀划分、自定义 `4,8,6,6`、 -Embedding/FinalNorm/LMHead 归属、`StageOfLayer`/`OwnsLayer`、vPP 交错、chunk↔stage 映射以及 -各类非法配置的死亡断言。`tests/distributed/test_pipeline_layout_suggest.cc` 额外覆盖 -`SuggestBalancedPartition`(均匀/余数/代价不均衡/非法代价)、`ParsePipelineLayerCosts` -(合法解析/负值/非数字/空项/无穷)、`ComputePerLayerParamCounts`(GELU+LayerNorm 精确值、 -SwiGLU+RMSNorm 均匀正数、MoE 拒绝)以及 `ComputePipelineLoadAnalysis`(均匀即均衡、代价不均衡 -下均匀布局 skewed、均衡布局消除 bubble、空 partition 默认均匀、结构 bubble 公式)。 +**布局功能测试方法**: + +由两个单元测试文件共同覆盖。 + +(1)`tests/distributed/test_pipeline_layout.cc` 验证 `PipelineLayout`本身的行为: + +既检查不传参数时的默认均匀划分、按 `4,8,6,6` 显式自定义的划分,也检查Embedding归属第一个Stage、Final Norm 与 LM Head 归属最后一个Stage,以及 `StageOfLayer`/`OwnsLayer` 的层归属查询、vPP 模式下 chunk 的交错排列和 chunk 与 Stage 的映射关系;对空项、非数字、层数不符等非法配置,则用死亡断言确认程序会正确终止。 + +(2)`tests/distributed/test_pipeline_layout_suggest.cc` 验证自动布局建议与负载分析: + +`SuggestBalancedPartition` 对代价均匀、带余数、代价不均衡的输入能给出正确的分层结果、对非法代价能正确拒绝;`ParsePipelineLayerCosts` 能解析合法代价并拒绝负值、非数字、空项、无穷;`ComputePerLayerParamCounts` 对 GELU+LayerNorm 给出精确参数量、对 SwiGLU+RMSNorm 给出均匀正数、对 MoE 层拒绝计算;`ComputePipelineLoadAnalysis` 则确认均匀代价下均匀布局即均衡、代价不均衡时均匀布局会产生气泡而均衡布局能消除气泡、空 partition 退化为均匀划分、结构 bubble 公式`(S-1)/(S-1+n)` 计算正确。 ```bash cmake -S . -B build -DBUILD_TEST=ON @@ -236,9 +244,25 @@ cmake --build build -j ctest --test-dir build -R 'test_pipeline_layout' --output-on-failure ``` -端到端验证需至少 2 个 Pipeline Stage:用相同初始权重分别以单卡/默认布局与自定义布局跑若干 -训练迭代,比较前向结果、loss 与梯度在允许误差内一致(fp32 1e-05,bf16 1e-02),并确认训练 -过程无通信死锁。 +**端到端验证(2-Stage)方法**: + +用相同初始权重分别以单卡/默认布局与自定义布局跑若干训练迭代,比较前向结果、loss 与梯度在允许误差内一致,并确认训练过程无通信死锁。一键脚本 `scripts/verify_pipeline_layout_correctness.sh` 已封装该流程:用相同`--llmc_filepath` 权重与数据分别跑单卡(PP=1)与自定义 2-Stage(PP=2、`--pipeline_layer_partition 7,5`),再逐step对比 train loss(多step中loss 一致即说明前向、反向与梯度一致,任一环节偏差都会在后续step累积成loss发散): + +```bash +# scripts/verify_pipeline_layout_correctness.sh for convenience +bash scripts/verify_pipeline_layout_correctness.sh +DTYPE=bfloat16 bash scripts/verify_pipeline_layout_correctness.sh +``` + +**实测结果**(2×RTX 4090,`batch=4, seq=64, total_batch=512, num_iteration=3`,`7,5` 切分): + +| dtype | 单卡参考 loss(step 1/2/3) | 自定义 7,5 loss(step 1/2/3) | 最大绝对差 | 判定 | +| ----- | ------------------------------ | ------------------------------ | ----- | ----------- | +| fp32 | 5.250157 / 4.913959 / 5.018849 | 5.250157 / 4.913959 / 5.018850 | 1e-6 | pass(≤1e-5) | +| bf16 | 5.215456 / 4.905437 / 5.009968 | 5.215456 / 4.905437 / 5.009968 | 0 | pass(≤1e-2) | + +fp32 前两步与 bf16 全部三步逐位一致,说明 PP=2 的数据切分与 loss 平均(`sum/n`)和单卡的 +`sum/grad_accum_steps` 精确等价,等价于前向、反向、梯度三者一致。 ## API 摘要 diff --git a/infini_train/src/nn/parallel/pp/pipeline_parallel.cc b/infini_train/src/nn/parallel/pp/pipeline_parallel.cc index 6258a182e..96eba3658 100644 --- a/infini_train/src/nn/parallel/pp/pipeline_parallel.cc +++ b/infini_train/src/nn/parallel/pp/pipeline_parallel.cc @@ -118,15 +118,17 @@ void PipelineParallel::ReportPipelineStats() { const double efficiency = bottleneck > 0.0 ? average / bottleneck : 0.0; const double imbalance_bubble = 1.0 - efficiency; - LOG(INFO) << std::format("=== Pipeline Timing Summary ({} stages) ===", num_stages); - LOG(INFO) << std::format("{:<6} {:>14} {:>14} {:>14}", "Stage", "Fwd(ms)", "Bwd(ms)", "Total(ms)"); + // Use LOG(ERROR) so the summary reaches stderr even when glog's stderrthreshold + // filters out INFO; this matches the per-step progress lines above. + LOG(ERROR) << std::format("=== Pipeline Timing Summary ({} stages) ===", num_stages); + LOG(ERROR) << std::format("{:<6} {:>14} {:>14} {:>14}", "Stage", "Fwd(ms)", "Bwd(ms)", "Total(ms)"); for (int s = 0; s < num_stages; ++s) { - LOG(INFO) << std::format("{:<6} {:>14.3f} {:>14.3f} {:>14.3f}", s, stage_fwd[s] * 1e3, stage_bwd[s] * 1e3, - stage_total[s] * 1e3); + LOG(ERROR) << std::format("{:<6} {:>14.3f} {:>14.3f} {:>14.3f}", s, stage_fwd[s] * 1e3, stage_bwd[s] * 1e3, + stage_total[s] * 1e3); } - LOG(INFO) << std::format("Compute tasks per stage: {} forward + {} backward", fwd_count, bwd_count); - LOG(INFO) << std::format("Bottleneck stage: {:.3f} ms | average: {:.3f} ms", bottleneck * 1e3, average * 1e3); - LOG(INFO) << std::format("Load-imbalance bubble: {:.1f}% | pipeline efficiency: {:.1f}%", imbalance_bubble * 100.0, - efficiency * 100.0); + LOG(ERROR) << std::format("Compute tasks per stage: {} forward + {} backward", fwd_count, bwd_count); + LOG(ERROR) << std::format("Bottleneck stage: {:.3f} ms | average: {:.3f} ms", bottleneck * 1e3, average * 1e3); + LOG(ERROR) << std::format("Load-imbalance bubble: {:.1f}% | pipeline efficiency: {:.1f}%", + imbalance_bubble * 100.0, efficiency * 100.0); } } // namespace infini_train::nn::parallel diff --git a/infini_train/src/nn/parallel/pp/pipeline_schedule.cc b/infini_train/src/nn/parallel/pp/pipeline_schedule.cc index f1cff8052..4cb26c458 100644 --- a/infini_train/src/nn/parallel/pp/pipeline_schedule.cc +++ b/infini_train/src/nn/parallel/pp/pipeline_schedule.cc @@ -44,6 +44,10 @@ class StageTimer { cpu_active_ = true; return; } + // Pin the device so the CUDA event is created in this stage's context; otherwise + // EventCreate() uses the thread's current device, which may differ from device_, + // and EventRecord() on this stage's stream fails with cudaErrorInvalidResourceHandle. + core::DeviceGuard guard(device_); core::Event *start = nullptr; impl_->EventCreate(&start); impl_->EventRecord(start, CurrentStream()); @@ -65,6 +69,7 @@ class StageTimer { return; } CHECK(!pending_starts_.empty()) << "StageTimer::End called without a matching Begin"; + core::DeviceGuard guard(device_); core::Event *start = pending_starts_.back(); pending_starts_.pop_back(); core::Event *stop = nullptr; diff --git "a/read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md" "b/read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md" deleted file mode 100644 index a2b700155..000000000 --- "a/read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md" +++ /dev/null @@ -1,168 +0,0 @@ -# 【训练方向 2026 夏季训练营】Pipeline 并行自定义布局 - -# **一、项目背景** - -Pipeline Parallelism(PP)通过将模型的不同层划分到多个 Pipeline Stage 上,使超出单卡显存容量的大模型能够进行分布式训练。当前 InfiniTrain 已支持 GPipe、1F1B 和 Virtual Pipeline Parallelism(vPP),并能够按照 Pipeline Stage 数量自动划分 Transformer 层。 - -当前层划分策略主要采用均匀分配方式,并默认将 Embedding 放置在第一个 Stage、Final Norm 和 LM Head 放置在最后一个 Stage。该方式适合结构规则、各层计算量接近的模型,但在以下场景中存在限制: - -- 不同 Transformer 层的计算量或显存占用不一致,均匀按层数划分不能实现负载均衡。 - -- Embedding、Final Norm、LM Head 等特殊模块的计算量没有纳入布局配置。 - -- 用户无法显式指定每个 Stage 所包含的层范围。 - -- 模型构建、Pipeline 调度和参数加载分别计算层归属,扩展自定义布局时容易产生不一致。 - -为此,本项目旨在为 InfiniTrain 增加统一的 **Pipeline 自定义布局(Pipeline Layout)** 能力,使用户可以显式配置各 Stage 的 Transformer 层和首尾特殊模块,并保证模型构建、训练调度及参数加载使用同一份布局信息。 - -# 二、项目目标 - -- 设计统一的 \`PipelineLayout\` 数据结构,描述每个 Pipeline Stage 所拥有的 Transformer 层及特殊模块。 - -- 支持 Transformer 层在不同 Stage 之间进行非均匀但连续的划分。 - -- 支持显式配置 Embedding、Final Norm 和 LM Head 的归属 Stage。 - -- 未指定自定义布局时,保持当前自动均匀划分行为和已有命令行参数兼容。 - -- 对布局进行完整合法性校验,并在训练启动阶段输出清晰的布局信息和错误提示。 - -- 使模型构建、Pipeline 调度和参数加载统一查询 \`PipelineLayout\`,避免层归属逻辑重复。 - -# **三、任务拆解** - -## pipeline Layout 数据结构设计 - -定义用于表达 Pipeline 布局的数据结构及查询接口。具体命名和组织形式可结合现有代码设计,应该能表述: - -- Pipeline Stage 数量 - -- 每个 Stage 对应的 Transformer 层范围 - -- Embedding、Final Norm、LM Head 等特殊模块的归属 - -- 根据 \`stage\_id\` 查询本 Stage 所拥有的层和特殊模块 - -- 根据 \`layer\_id\` 查询对应的 Stage - -布局信息应作为模型构建、Pipeline Stage 构造及参数加载的统一数据来源,不应在不同模块中重复实现层划分算法。 - -## 命令行参数扩展 - -新增必要参数,用于指定自定义 Pipeline 布局。基础实现可采用清晰、易校验的层数列表形式,例如: - -```Bash ---pipeline_parallel 4 \ ---pipeline_layer_partition 4,8,6,6 -``` - -上述配置表示 24 个 Transformer 层依次划分为: - -```Plain Text -stage 0: embedding + layers 0-3 -stage 1: layers 4-11 -stage 2: layers 12-17 -stage 3: layers 18-23 + final_norm + lm_head -``` - -参数名称和具体语法可在设计阶段调整,但需要满足:未指定参数时默认使用当前均匀划分策略;指定参数时,各 Stage 层数之和必须等于模型总层数。 - -## 布局解析与合法性校验 - -在训练启动阶段完成布局解析和校验,至少覆盖以下情况: - -- Stage 数量与 `pipeline_parallel` 配置一致。 - -- Transformer 层不重复、不遗漏,并保持正确的执行顺序。 - -- 布局与 Virtual Pipeline 配置不兼容时,给出明确错误。 - -程序启动后支持打印输出各 Stage 的最终布局,便于用户检查配置并定位问题。 - -## 模型构建与 Pipeline Stage 集成 - -修改 Pipeline 模型构建流程,使每个 rank 仅创建当前 Stage 所拥有的模块: - -- 根据 `PipelineLayout` 构建本地 Transformer 层 - -- 如果没显示指定`PipelineLayout` 保留当前自动均匀布局作为默认实现 - -- 完成 GPT\-2 和 LLaMA 3 示例模型的接入 - -- 支持和DDP、TP等多种并行模型组合运行。 - -## Pipeline 调度与参数加载集成 - -Pipeline 调度器应从统一布局中获得 Stage 和 Chunk 的归属信息,不再仅依靠固定的取模关系推导所有权。模型参数加载流程应根据同一份 `PipelineLayout` 判断本 rank 需要加载的 Transformer 层和特殊模块。默认均匀布局、自定义非均匀布局应使用相同的查询接口。 - -## 测试与验证 - -新增单元测试和端到端测试,至少覆盖: - -- 自定义非均匀布局,例如 `4,8,6,6` - -- Embedding、Final Norm 和 LM Head 的归属正确 - -- Transformer 层无重复、无遗漏且执行顺序正确 - -- 层数总和错误、Stage 数量错误、负数等非法配置能够在启动阶段被拒绝 - -- 至少使用 2 个 Pipeline Stage 完成 GPT\-2 或 LLaMA 3 的若干训练迭代,训练过程无通信死锁 - -- 相同初始参数和输入下,自定义 PP 布局与单卡或默认布局的前向结果、loss 和梯度在允许误差范围内一致 - -# **四、评判标准** - -请提供以下内容: - -- Pipeline 自定义布局使用指导文档,包括参数配置、布局语法、默认行为、输入输出示例和错误排查方法。 - -- 单元测试、端到端测试代码及测试日志。 - -- 项目报告,主要包括数据结构与接口设计、关键实现说明、兼容性说明,以及不同布局下的正确性和 Pipeline 负载分析。 - -## **通过标准** - -- 实现统一的 \`PipelineLayout\` 数据结构和必要的布局查询接口。 - -- 支持通过命令行配置各 Pipeline Stage 的非均匀连续层数,例如 \`4,8,6,6\`。 - -- 支持显式记录并正确放置 Embedding、Final Norm 和 LM Head。 - -- 未配置自定义布局时,现有均匀划分、GPipe、1F1B 及 vPP 使用方式不受影响。 - -- GPT\-2 和 LLaMA 3 的模型构建及参数加载使用统一布局判断层归属。 - -- 对非法布局进行完整校验,并输出可以定位问题的错误信息。 - -- 提供单元测试和至少一个 2\-Stage 端到端训练测试;与单卡或默认布局相比,**前向结果、loss 和梯度在允许误差范围内一致(fp32:1e\-05,bf16:1e\-02)**。 - -## **优秀标准** - -在达到通过标准的基础上,可完成以下一项或多项: - -- 支持 vPP 下显式配置任意 \`Chunk \-\> Stage\` 映射,而不是依赖固定轮转关系。 - -- 支持类似 Megatron\-LM 的 Pipeline Layout 字符串表达,可描述重复层、特殊模块、空 Stage 和 Virtual Pipeline Chunk。 - -- 支持根据各层参数量、Profiler 统计或用户提供的计算代价,自动生成近似负载均衡的布局建议。 - -- 给出默认均匀布局和自定义布局的 Pipeline bubble、各 Stage 执行时间及吞吐对比,证明自定义布局能够改善负载不均衡场景。 - -- 代码通过 **仓库 PR review 流程**(提交 → 审查 → 修改 → 达到可合入标准)。 - -# **五、参考资料** - -1\. [Megatron\-LM Pipeline Parallelism](https://github.com/NVIDIA/Megatron-LM/blob/main/docs/api-guide/core/pipeline_parallel.md) - -2\. [Megatron\-LM Pipeline Parallel Layout](https://github.com/NVIDIA/Megatron-LM/blob/main/docs/user-guide/features/pipeline_parallel_layout.md) - -3\. [Megatron\-LM Parallelism Guide](https://github.com/NVIDIA/Megatron-LM/blob/main/docs/user-guide/parallelism-guide.md) - -4\. [GPipe: Efficient Training of Giant Neural Networks using Pipeline](https://arxiv.org/abs/1811.06965) - -5\. [PipeDream: Fast and Efficient Pipeline Parallel DNN Training](https://arxiv.org/abs/1806.03377) - - - diff --git "a/read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md:Zone.Identifier" "b/read_notes/Pipeline\345\271\266\350\241\214\350\207\252\345\256\232\344\271\211\345\270\203\345\261\200.md:Zone.Identifier" deleted file mode 100644 index d6c1ec682968c796b9f5e9e080cc6f674b57c766..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2xdl#JyUFr831@K2x&2; exit 2 ;; +esac + +# ---- preflight ---- +command -v python3 >/dev/null 2>&1 || { echo "python3 is required but not found" >&2; exit 2; } +for f in "$BIN" "$INFINI_RUN" "$WEIGHTS" "$DATA" "$COMPARE_SCRIPT"; do + [[ -e "$f" ]] || { echo "Missing: $f" >&2; exit 2; } +done + +REF_DIR="$OUT_DIR/baseline" +CUSTOM_DIR="$OUT_DIR/custom" +mkdir -p "$REF_DIR" "$CUSTOM_DIR" + +# Args shared by both runs (identical weights/data/hparams). +COMMON_ARGS=( + --llmc_filepath "$WEIGHTS" + --input_bin "$DATA" + --dtype "$DTYPE" + --batch_size "$BATCH" + --sequence_length "$SEQ_LEN" + --total_batch_size "$TOTAL_BATCH" + --num_iteration "$NUM_ITER" + # No --tokenizer_bin is passed, so text generation never runs; set the + # frequency high anyway as a defensive measure. + --freq_generate_txt 1000000 +) + +run_and_check() { + local label="$1"; shift + local log="$1"; shift + echo "== [$label] ==" + "$@" 2>&1 | tee "$log" + # Sanity check: the log must contain exactly NUM_ITER "train loss" lines. + local count + count=$(grep -c "train loss" "$log" || true) + if [[ "$count" -ne "$NUM_ITER" ]]; then + echo "Expected $NUM_ITER 'train loss' lines in $log but found $count (run may have failed)" >&2 + exit 1 + fi +} + +echo "=== Pipeline custom-layout correctness verification ===" +echo "dtype=$DTYPE iters=$NUM_ITER batch=$BATCH seq=$SEQ_LEN total_batch=$TOTAL_BATCH" +echo "partition=$CUSTOM_PARTITION weights=$WEIGHTS" +echo + +run_and_check "1/3 reference: single card (PP=1)" "$REF_DIR/$LOG_NAME" \ + "$INFINI_RUN" --nproc_per_node=1 "$BIN" \ + --pipeline_parallel 1 "${COMMON_ARGS[@]}" + +run_and_check "2/3 custom: 2-stage PP, partition $CUSTOM_PARTITION" "$CUSTOM_DIR/$LOG_NAME" \ + "$INFINI_RUN" --nproc_per_node=2 "$BIN" \ + --pipeline_parallel 2 --pipeline_layer_partition "$CUSTOM_PARTITION" "${COMMON_ARGS[@]}" + +echo "== 3/3 compare per-step training loss ==" +if python3 "$COMPARE_SCRIPT" "$REF_DIR" "$CUSTOM_DIR" --threshold-fp32 1e-5 --threshold-bf16 1e-2; then + echo + echo "PASS: custom layout ($CUSTOM_PARTITION) loss matches single-card reference within tolerance." +else + echo + echo "FAIL: loss mismatch exceeds tolerance." >&2 + exit 1 +fi From 46f9bfd31d23f439e1965ca816dd443106d55991 Mon Sep 17 00:00:00 2001 From: CuiLingyunCrispy Date: Wed, 16 Sep 2026 15:19:55 +0800 Subject: [PATCH 4/4] style: apply clang-format --- docs/pipeline_layout_demo.cc | 15 +++---- example/gpt2/checkpoint_loader.cc | 6 +-- example/gpt2/main.cc | 20 ++++----- example/llama3/checkpoint_loader.cc | 3 +- example/llama3/main.cc | 14 +++--- .../include/nn/parallel/pp/pipeline_layout.h | 6 +-- .../src/nn/modules/transformer/transformer.cc | 7 ++- .../src/nn/parallel/pp/pipeline_layout.cc | 34 ++++++-------- .../src/nn/parallel/pp/pipeline_parallel.cc | 4 +- .../src/nn/parallel/pp/pipeline_schedule.cc | 4 +- .../test_pipeline_layout_suggest.cc | 44 +++++-------------- 11 files changed, 57 insertions(+), 100 deletions(-) diff --git a/docs/pipeline_layout_demo.cc b/docs/pipeline_layout_demo.cc index 950952851..08280abf4 100644 --- a/docs/pipeline_layout_demo.cc +++ b/docs/pipeline_layout_demo.cc @@ -30,9 +30,7 @@ using infini_train::nn::parallel::SuggestBalancedPartition; // or a mixture-of-experts tail whose per-layer compute is no longer uniform. std::vector ImbalancedCosts() { std::vector costs(12, 2.0); - for (int i = 0; i < 4; ++i) { - costs[i] = 1.0; - } + for (int i = 0; i < 4; ++i) { costs[i] = 1.0; } return costs; } @@ -58,19 +56,16 @@ void PrintComparison(const char *title, const std::vector &uniform_partitio const int total_layers = static_cast(costs.size()); const int num_stages = static_cast(uniform_partition.size()); - const PipelineLoadStats uniform = - ComputePipelineLoadAnalysis(total_layers, num_stages, uniform_partition, costs, n); - const PipelineLoadStats custom = - ComputePipelineLoadAnalysis(total_layers, num_stages, custom_partition, costs, n); + const PipelineLoadStats uniform + = ComputePipelineLoadAnalysis(total_layers, num_stages, uniform_partition, costs, n); + const PipelineLoadStats custom = ComputePipelineLoadAnalysis(total_layers, num_stages, custom_partition, costs, n); const std::string u_part = PartitionStr(uniform_partition); const std::string c_part = PartitionStr(custom_partition); std::printf("=== %s ===\n", title); std::printf("per-layer costs: ["); - for (size_t i = 0; i < costs.size(); ++i) { - std::printf("%s%.0f", i ? "," : "", costs[i]); - } + for (size_t i = 0; i < costs.size(); ++i) { std::printf("%s%.0f", i ? "," : "", costs[i]); } std::printf("] (S=%d stages, n=%d micro-batches)\n\n", num_stages, n); const std::string u_col = "uniform (" + u_part + ")"; diff --git a/example/gpt2/checkpoint_loader.cc b/example/gpt2/checkpoint_loader.cc index bd24cb1a4..45e92dba2 100644 --- a/example/gpt2/checkpoint_loader.cc +++ b/example/gpt2/checkpoint_loader.cc @@ -102,9 +102,9 @@ std::shared_ptr LoadFromLLMC(const std::string &filepath) // Unified pipeline layout: which layers / special modules this rank owns. int pp_size = nn::parallel::global::GetPipelineParallelSize(); - auto layout = nn::parallel::PipelineLayout::Create( - static_cast(n_layer), pp_size, nn::parallel::global::GetVirtualPipelineParallelSize(), - nn::parallel::global::GetPipelineLayerPartition()); + auto layout = nn::parallel::PipelineLayout::Create(static_cast(n_layer), pp_size, + nn::parallel::global::GetVirtualPipelineParallelSize(), + nn::parallel::global::GetPipelineLayerPartition()); const auto stage_info = layout.GetStageInfo(nn::parallel::pp_rank); const bool is_first_stage = stage_info.is_first_stage; const bool is_last_stage = stage_info.is_last_stage; diff --git a/example/gpt2/main.cc b/example/gpt2/main.cc index cfd39b5c5..c6803a4ce 100644 --- a/example/gpt2/main.cc +++ b/example/gpt2/main.cc @@ -146,8 +146,8 @@ std::string PartitionToString(const std::vector &partition) { nn::TransformerConfig ResolveGPT2Config() { if (!kModelToConfigs.count(FLAGS_model)) { - LOG(FATAL) << "--pipeline_auto_layout requires a config-map model (--model d12/d24/d36/d48); '" - << FLAGS_model << "' has no static config"; + LOG(FATAL) << "--pipeline_auto_layout requires a config-map model (--model d12/d24/d36/d48); '" << FLAGS_model + << "' has no static config"; } nn::TransformerConfig config = kModelToConfigs.at(FLAGS_model); gpt2::SanitizeGPT2Config(config); @@ -170,14 +170,12 @@ void Train(const nn::parallel::Rank &rank) { if (rank.IsLastRank()) { if (!FLAGS_save.empty() && FLAGS_save_interval == 0) { LOG(FATAL) << "Invalid configuration: --save is set ('" << FLAGS_save - << "'), but --save_interval is 0. " - << "They must be set together."; + << "'), but --save_interval is 0. " << "They must be set together."; } if (FLAGS_save.empty() && FLAGS_save_interval > 0) { LOG(FATAL) << "Invalid configuration: --save_interval is set to " << FLAGS_save_interval - << ", but --save is empty. " - << "They must be set together."; + << ", but --save is empty. " << "They must be set together."; } } } @@ -392,7 +390,7 @@ void Train(const nn::parallel::Rank &rank) { auto train_iter = train_loader.begin(); std::shared_ptr loss_fn = (tp_world_size > 1) ? std::static_pointer_cast( - std::make_shared(model_config.original_vocab_size)) + std::make_shared(model_config.original_vocab_size)) : std::static_pointer_cast(std::make_shared()); loss_fn->To(device); LOG(INFO) << "Rank " << rank.GlobalRank() << ": start training"; @@ -616,15 +614,15 @@ int main(int argc, char *argv[]) { pipeline_layer_partition = nn::parallel::ParsePipelineLayerPartition(FLAGS_pipeline_layer_partition); } else if (has_layer_costs) { const auto layer_costs = nn::parallel::ParsePipelineLayerCosts(FLAGS_pipeline_layer_costs); - pipeline_layer_partition = nn::parallel::SuggestBalancedPartition( - static_cast(layer_costs.size()), FLAGS_pipeline_parallel, layer_costs); + pipeline_layer_partition = nn::parallel::SuggestBalancedPartition(static_cast(layer_costs.size()), + FLAGS_pipeline_parallel, layer_costs); LOG(INFO) << "Auto-suggested pipeline layout from --pipeline_layer_costs: " << PartitionToString(pipeline_layer_partition); } else if (FLAGS_pipeline_auto_layout) { const auto config = ResolveGPT2Config(); const auto layer_costs = nn::ComputePerLayerParamCounts(config); - pipeline_layer_partition = nn::parallel::SuggestBalancedPartition( - static_cast(config.n_layer), FLAGS_pipeline_parallel, layer_costs); + pipeline_layer_partition = nn::parallel::SuggestBalancedPartition(static_cast(config.n_layer), + FLAGS_pipeline_parallel, layer_costs); LOG(INFO) << "Auto-suggested pipeline layout from per-layer parameter counts: " << PartitionToString(pipeline_layer_partition); } diff --git a/example/llama3/checkpoint_loader.cc b/example/llama3/checkpoint_loader.cc index 0dfb8c36b..b9917625c 100644 --- a/example/llama3/checkpoint_loader.cc +++ b/example/llama3/checkpoint_loader.cc @@ -89,8 +89,7 @@ std::shared_ptr LoadFromLLMC(const std::string &filepath) // Unified pipeline layout: which layers / special modules this rank owns. auto layout = nn::parallel::PipelineLayout::Create( static_cast(n_layer), nn::parallel::global::GetPipelineParallelSize(), - nn::parallel::global::GetVirtualPipelineParallelSize(), - nn::parallel::global::GetPipelineLayerPartition()); + nn::parallel::global::GetVirtualPipelineParallelSize(), nn::parallel::global::GetPipelineLayerPartition()); const auto stage_info = layout.GetStageInfo(nn::parallel::pp_rank); const bool is_first_stage = stage_info.is_first_stage; const bool is_last_stage = stage_info.is_last_stage; diff --git a/example/llama3/main.cc b/example/llama3/main.cc index 0a691df05..bb2b93e01 100644 --- a/example/llama3/main.cc +++ b/example/llama3/main.cc @@ -156,14 +156,12 @@ void Train(const nn::parallel::Rank &rank) { if (rank.IsLastRank()) { if (!FLAGS_save.empty() && FLAGS_save_interval == 0) { LOG(FATAL) << "Invalid configuration: --save is set ('" << FLAGS_save - << "'), but --save_interval is 0. " - << "They must be set together."; + << "'), but --save_interval is 0. " << "They must be set together."; } if (FLAGS_save.empty() && FLAGS_save_interval > 0) { LOG(FATAL) << "Invalid configuration: --save_interval is set to " << FLAGS_save_interval - << ", but --save is empty. " - << "They must be set together."; + << ", but --save is empty. " << "They must be set together."; } } } @@ -596,15 +594,15 @@ int main(int argc, char *argv[]) { pipeline_layer_partition = nn::parallel::ParsePipelineLayerPartition(FLAGS_pipeline_layer_partition); } else if (has_layer_costs) { const auto layer_costs = nn::parallel::ParsePipelineLayerCosts(FLAGS_pipeline_layer_costs); - pipeline_layer_partition = nn::parallel::SuggestBalancedPartition( - static_cast(layer_costs.size()), FLAGS_pipeline_parallel, layer_costs); + pipeline_layer_partition = nn::parallel::SuggestBalancedPartition(static_cast(layer_costs.size()), + FLAGS_pipeline_parallel, layer_costs); LOG(INFO) << "Auto-suggested pipeline layout from --pipeline_layer_costs: " << PartitionToString(pipeline_layer_partition); } else if (FLAGS_pipeline_auto_layout) { const auto config = ResolveLLaMA3Config(); const auto layer_costs = nn::ComputePerLayerParamCounts(config); - pipeline_layer_partition = nn::parallel::SuggestBalancedPartition( - static_cast(config.n_layer), FLAGS_pipeline_parallel, layer_costs); + pipeline_layer_partition = nn::parallel::SuggestBalancedPartition(static_cast(config.n_layer), + FLAGS_pipeline_parallel, layer_costs); LOG(INFO) << "Auto-suggested pipeline layout from per-layer parameter counts: " << PartitionToString(pipeline_layer_partition); } diff --git a/infini_train/include/nn/parallel/pp/pipeline_layout.h b/infini_train/include/nn/parallel/pp/pipeline_layout.h index 04bffe5f3..281e44836 100644 --- a/infini_train/include/nn/parallel/pp/pipeline_layout.h +++ b/infini_train/include/nn/parallel/pp/pipeline_layout.h @@ -82,9 +82,7 @@ struct PipelineLoadStats { // when empty, fall back to the default uniform partition). `layer_costs[i]` is the compute cost // of layer i; when empty, every layer has unit cost (load == layer count). `num_micro_batches` // only affects the structural fill/drain bubble. -PipelineLoadStats ComputePipelineLoadAnalysis(int total_layers, int num_stages, - const std::vector &partition, - const std::vector &layer_costs = {}, - int num_micro_batches = 1); +PipelineLoadStats ComputePipelineLoadAnalysis(int total_layers, int num_stages, const std::vector &partition, + const std::vector &layer_costs = {}, int num_micro_batches = 1); } // namespace infini_train::nn::parallel diff --git a/infini_train/src/nn/modules/transformer/transformer.cc b/infini_train/src/nn/modules/transformer/transformer.cc index af1b14cf6..a61c38e56 100644 --- a/infini_train/src/nn/modules/transformer/transformer.cc +++ b/infini_train/src/nn/modules/transformer/transformer.cc @@ -210,8 +210,7 @@ TransformerModel::TransformerModel(const TransformerConfig config) : CloneableModule(kType), config_(config), layout_(nn::parallel::PipelineLayout::Create( static_cast(config_.n_layer), nn::parallel::global::GetPipelineParallelSize(), - nn::parallel::global::GetVirtualPipelineParallelSize(), - nn::parallel::global::GetPipelineLayerPartition())), + nn::parallel::global::GetVirtualPipelineParallelSize(), nn::parallel::global::GetPipelineLayerPartition())), stage_info_(layout_.GetStageInfo(nn::parallel::pp_rank)) { if (nn::parallel::global::GetPipelineParallelSize() > 1 && nn::parallel::pp_rank == 0) { LOG(INFO) << layout_.Describe(); @@ -299,8 +298,8 @@ int64_t FfnHiddenDim(const TransformerConfig &config) { ffn_hidden = static_cast(2 * ffn_hidden) / 3; // SwiGLU intermediate } if (config.ffn_dim_multiplier.has_value()) { - ffn_hidden = static_cast( - std::llround(static_cast(ffn_hidden) * config.ffn_dim_multiplier.value())); + ffn_hidden + = static_cast(std::llround(static_cast(ffn_hidden) * config.ffn_dim_multiplier.value())); } ffn_hidden = (ffn_hidden + config.multiple_of - 1) / config.multiple_of * config.multiple_of; return ffn_hidden; diff --git a/infini_train/src/nn/parallel/pp/pipeline_layout.cc b/infini_train/src/nn/parallel/pp/pipeline_layout.cc index a1b1098ce..2d19c8e35 100644 --- a/infini_train/src/nn/parallel/pp/pipeline_layout.cc +++ b/infini_train/src/nn/parallel/pp/pipeline_layout.cc @@ -39,9 +39,7 @@ std::vector ParsePipelineLayerPartition(const std::string &str) { } std::stringstream ss(str); std::string token; - while (std::getline(ss, token, ',')) { - partition.push_back(ParseLayerCount(token, str)); - } + while (std::getline(ss, token, ',')) { partition.push_back(ParseLayerCount(token, str)); } return partition; } @@ -85,17 +83,15 @@ std::vector SuggestBalancedPartition(int total_layers, int num_stages, cons CHECK_EQ(layer_costs.size(), static_cast(total_layers)) << "layer_costs has " << layer_costs.size() << " entries but total_layers is " << total_layers; for (int i = 0; i < total_layers; ++i) { - CHECK_GE(layer_costs[i], 0.0) << "layer_costs must be non-negative, layer " << i << " has " - << layer_costs[i]; + CHECK_GE(layer_costs[i], 0.0) + << "layer_costs must be non-negative, layer " << i << " has " << layer_costs[i]; costs[i] = layer_costs[i]; } } // prefix[t] = sum of costs[0 .. t-1]. std::vector prefix(total_layers + 1, 0.0); - for (int i = 0; i < total_layers; ++i) { - prefix[i + 1] = prefix[i] + costs[i]; - } + for (int i = 0; i < total_layers; ++i) { prefix[i + 1] = prefix[i] + costs[i]; } // dp[i][j] is the minimal achievable maximum per-segment cost when the first j layers // are split into i contiguous segments; split[i][j] records the boundary that reaches it @@ -104,9 +100,7 @@ std::vector SuggestBalancedPartition(int total_layers, int num_stages, cons std::vector> dp(num_stages + 1, std::vector(total_layers + 1, kInf)); std::vector> split(num_stages + 1, std::vector(total_layers + 1, 0)); - for (int j = 0; j <= total_layers; ++j) { - dp[1][j] = prefix[j]; - } + for (int j = 0; j <= total_layers; ++j) { dp[1][j] = prefix[j]; } for (int i = 2; i <= num_stages; ++i) { for (int j = i; j <= total_layers; ++j) { for (int p = i - 1; p <= j - 1; ++p) { @@ -143,8 +137,8 @@ PipelineLoadStats ComputePipelineLoadAnalysis(int total_layers, int num_stages, CHECK_EQ(layer_costs.size(), static_cast(total_layers)) << "layer_costs has " << layer_costs.size() << " entries but total_layers is " << total_layers; for (int i = 0; i < total_layers; ++i) { - CHECK_GE(layer_costs[i], 0.0) << "layer_costs must be non-negative, layer " << i << " has " - << layer_costs[i]; + CHECK_GE(layer_costs[i], 0.0) + << "layer_costs must be non-negative, layer " << i << " has " << layer_costs[i]; costs[i] = layer_costs[i]; } } @@ -177,7 +171,8 @@ PipelineLoadStats ComputePipelineLoadAnalysis(int total_layers, int num_stages, stats.average = std::accumulate(stats.stage_loads.begin(), stats.stage_loads.end(), 0.0) / num_stages; stats.efficiency = stats.bottleneck > 0.0 ? stats.average / stats.bottleneck : 0.0; stats.imbalance_bubble = 1.0 - stats.efficiency; - stats.structural_bubble = static_cast(num_stages - 1) / static_cast(num_stages - 1 + num_micro_batches); + stats.structural_bubble + = static_cast(num_stages - 1) / static_cast(num_stages - 1 + num_micro_batches); return stats; } @@ -225,8 +220,7 @@ PipelineLayout PipelineLayout::Create(int total_layers, int num_stages, int vpp_ CHECK_EQ(partition.size(), static_cast(num_stages)) << "pipeline_layer_partition has " << partition.size() << " entries but pipeline_parallel is " << num_stages; - CHECK_EQ(vpp_size, 1) - << "Custom pipeline_layer_partition is incompatible with virtual_pipeline_parallel > 1"; + CHECK_EQ(vpp_size, 1) << "Custom pipeline_layer_partition is incompatible with virtual_pipeline_parallel > 1"; int cursor = 0; for (int stage = 0; stage < num_stages; ++stage) { const int count = partition[stage]; @@ -235,8 +229,8 @@ PipelineLayout PipelineLayout::Create(int total_layers, int num_stages, int vpp_ layout.stage_layer_ranges_[stage].push_back({cursor, cursor + count}); cursor += count; } - CHECK_EQ(cursor, total_layers) << "pipeline_layer_partition sums to " << cursor - << " layers but the model has " << total_layers; + CHECK_EQ(cursor, total_layers) << "pipeline_layer_partition sums to " << cursor << " layers but the model has " + << total_layers; } // Build the layer -> stage lookup and verify layers are neither missing nor duplicated. @@ -280,8 +274,8 @@ int PipelineLayout::StageOfChunk(int global_chunk_id, int num_stages) { return g int PipelineLayout::LocalChunkIndexOfChunk(int global_chunk_id, int num_stages) { return global_chunk_id / num_stages; } std::string PipelineLayout::Describe() const { - std::string s = "PipelineLayout: num_stages=" + std::to_string(num_stages_) + - ", total_layers=" + std::to_string(total_layers_) + ", vpp=" + std::to_string(vpp_size_) + "\n"; + std::string s = "PipelineLayout: num_stages=" + std::to_string(num_stages_) + + ", total_layers=" + std::to_string(total_layers_) + ", vpp=" + std::to_string(vpp_size_) + "\n"; for (int stage = 0; stage < num_stages_; ++stage) { s += " stage " + std::to_string(stage) + ": "; for (size_t i = 0; i < stage_layer_ranges_[stage].size(); ++i) { diff --git a/infini_train/src/nn/parallel/pp/pipeline_parallel.cc b/infini_train/src/nn/parallel/pp/pipeline_parallel.cc index 96eba3658..dfb84e86f 100644 --- a/infini_train/src/nn/parallel/pp/pipeline_parallel.cc +++ b/infini_train/src/nn/parallel/pp/pipeline_parallel.cc @@ -128,7 +128,7 @@ void PipelineParallel::ReportPipelineStats() { } LOG(ERROR) << std::format("Compute tasks per stage: {} forward + {} backward", fwd_count, bwd_count); LOG(ERROR) << std::format("Bottleneck stage: {:.3f} ms | average: {:.3f} ms", bottleneck * 1e3, average * 1e3); - LOG(ERROR) << std::format("Load-imbalance bubble: {:.1f}% | pipeline efficiency: {:.1f}%", - imbalance_bubble * 100.0, efficiency * 100.0); + LOG(ERROR) << std::format("Load-imbalance bubble: {:.1f}% | pipeline efficiency: {:.1f}%", imbalance_bubble * 100.0, + efficiency * 100.0); } } // namespace infini_train::nn::parallel diff --git a/infini_train/src/nn/parallel/pp/pipeline_schedule.cc b/infini_train/src/nn/parallel/pp/pipeline_schedule.cc index 4cb26c458..5e0aa5902 100644 --- a/infini_train/src/nn/parallel/pp/pipeline_schedule.cc +++ b/infini_train/src/nn/parallel/pp/pipeline_schedule.cc @@ -119,9 +119,7 @@ class StageTimer { impl_->EventDestroy(interval.stop); } pending_intervals_.clear(); - for (core::Event *start : pending_starts_) { - impl_->EventDestroy(start); - } + for (core::Event *start : pending_starts_) { impl_->EventDestroy(start); } pending_starts_.clear(); } diff --git a/tests/distributed/test_pipeline_layout_suggest.cc b/tests/distributed/test_pipeline_layout_suggest.cc index 365cfd4f2..ae15cdefa 100644 --- a/tests/distributed/test_pipeline_layout_suggest.cc +++ b/tests/distributed/test_pipeline_layout_suggest.cc @@ -22,9 +22,7 @@ TEST(PipelineLayoutSuggestTest, CostImbalanceShiftsLayersToLightStage) { // Four "light" layers (cost 1) followed by eight "heavy" layers (cost 2). Balancing // total cost (20 / 2 = 10 per stage) yields {7, 5} instead of the uniform {6, 6}. std::vector costs(12, 2.0); - for (int i = 0; i < 4; ++i) { - costs[i] = 1.0; - } + for (int i = 0; i < 4; ++i) { costs[i] = 1.0; } const std::vector expected{7, 5}; EXPECT_EQ(SuggestBalancedPartition(12, 2, costs), expected); } @@ -62,25 +60,15 @@ TEST(PipelineLayerCostsTest, ParsesValidCosts) { EXPECT_EQ(ParsePipelineLayerCosts("1.0,2.0,1.5"), expected); } -TEST(PipelineLayerCostsTest, EmptyStringGivesNoCosts) { - EXPECT_TRUE(ParsePipelineLayerCosts("").empty()); -} +TEST(PipelineLayerCostsTest, EmptyStringGivesNoCosts) { EXPECT_TRUE(ParsePipelineLayerCosts("").empty()); } -TEST(PipelineLayerCostsTest, RejectsNegativeCost) { - EXPECT_DEATH(ParsePipelineLayerCosts("-1,2"), "non-negative"); -} +TEST(PipelineLayerCostsTest, RejectsNegativeCost) { EXPECT_DEATH(ParsePipelineLayerCosts("-1,2"), "non-negative"); } -TEST(PipelineLayerCostsTest, RejectsNonNumber) { - EXPECT_DEATH(ParsePipelineLayerCosts("1,abc"), "not a number"); -} +TEST(PipelineLayerCostsTest, RejectsNonNumber) { EXPECT_DEATH(ParsePipelineLayerCosts("1,abc"), "not a number"); } -TEST(PipelineLayerCostsTest, RejectsEmptyEntry) { - EXPECT_DEATH(ParsePipelineLayerCosts("1,,2"), "empty entry"); -} +TEST(PipelineLayerCostsTest, RejectsEmptyEntry) { EXPECT_DEATH(ParsePipelineLayerCosts("1,,2"), "empty entry"); } -TEST(PipelineLayerCostsTest, RejectsInfinity) { - EXPECT_DEATH(ParsePipelineLayerCosts("inf"), "finite"); -} +TEST(PipelineLayerCostsTest, RejectsInfinity) { EXPECT_DEATH(ParsePipelineLayerCosts("inf"), "finite"); } TEST(ComputePerLayerParamCountsTest, MatchesAnalyticGELULayerNorm) { nn::TransformerConfig config{ @@ -122,9 +110,7 @@ TEST(ComputePerLayerParamCountsTest, SwigluRMSNormYieldsPositiveUniformCounts) { }; auto counts = nn::ComputePerLayerParamCounts(config); ASSERT_EQ(counts.size(), 3u); - for (double c : counts) { - EXPECT_GT(c, 0.0); - } + for (double c : counts) { EXPECT_GT(c, 0.0); } EXPECT_EQ(counts[0], counts[1]); EXPECT_EQ(counts[1], counts[2]); } @@ -138,9 +124,7 @@ TEST(PipelineLoadAnalysisTest, UniformCostsArePerfectlyBalanced) { // 12 layers / 3 stages with unit costs: uniform {4,4,4} -> every stage load == 4. auto stats = ComputePipelineLoadAnalysis(12, 3, {4, 4, 4}, {}, /*num_micro_batches=*/8); ASSERT_EQ(stats.stage_loads.size(), 3u); - for (double load : stats.stage_loads) { - EXPECT_DOUBLE_EQ(load, 4.0); - } + for (double load : stats.stage_loads) { EXPECT_DOUBLE_EQ(load, 4.0); } EXPECT_DOUBLE_EQ(stats.bottleneck, 4.0); EXPECT_DOUBLE_EQ(stats.average, 4.0); EXPECT_DOUBLE_EQ(stats.imbalance_bubble, 0.0); @@ -152,9 +136,7 @@ TEST(PipelineLoadAnalysisTest, ImbalancedCostsMakeUniformLayoutSkewed) { // 4 light layers (cost 1) + 8 heavy layers (cost 2). Uniform {6,6} assigns // stage 0: 4*1 + 2*2 = 8, stage 1: 6*2 = 12. std::vector costs(12, 2.0); - for (int i = 0; i < 4; ++i) { - costs[i] = 1.0; - } + for (int i = 0; i < 4; ++i) { costs[i] = 1.0; } auto stats = ComputePipelineLoadAnalysis(12, 2, {6, 6}, costs, 8); EXPECT_DOUBLE_EQ(stats.stage_loads[0], 8.0); EXPECT_DOUBLE_EQ(stats.stage_loads[1], 12.0); @@ -167,9 +149,7 @@ TEST(PipelineLoadAnalysisTest, ImbalancedCostsMakeUniformLayoutSkewed) { TEST(PipelineLoadAnalysisTest, BalancedPartitionRemovesImbalanceBubble) { // Same costs, but the cost-balanced partition {7,5} yields load 10 / 10. std::vector costs(12, 2.0); - for (int i = 0; i < 4; ++i) { - costs[i] = 1.0; - } + for (int i = 0; i < 4; ++i) { costs[i] = 1.0; } auto stats = ComputePipelineLoadAnalysis(12, 2, {7, 5}, costs, 8); EXPECT_DOUBLE_EQ(stats.stage_loads[0], 10.0); EXPECT_DOUBLE_EQ(stats.stage_loads[1], 10.0); @@ -180,9 +160,7 @@ TEST(PipelineLoadAnalysisTest, BalancedPartitionRemovesImbalanceBubble) { TEST(PipelineLoadAnalysisTest, EmptyPartitionDefaultsToUniform) { auto stats = ComputePipelineLoadAnalysis(12, 3, {}, {}, 1); ASSERT_EQ(stats.stage_loads.size(), 3u); - for (double load : stats.stage_loads) { - EXPECT_DOUBLE_EQ(load, 4.0); - } + for (double load : stats.stage_loads) { EXPECT_DOUBLE_EQ(load, 4.0); } } TEST(PipelineLoadAnalysisTest, StructuralBubbleFollowsGpipeFormula) {