Layer Partitioning
May 20, 2026 ยท View on GitHub
This document describes the layer partitioning workflow. It compiles smaller MLIR fragments in parallel so the slowest model compile stages can use many cores.
The current implementation and validation use DeepSeek R1 as the worked example. Other models can use the same high-level flow, but they need their own partition strategy, runtime ABI checks, and correctness tests before being enabled by default.
Environment
Run from the repository root and activate the Python environment used for model import:
cd buddy-mlir
conda activate buddy
Default Build Integration
Layer partitioning is controlled by the CMake option
BUDDY_MODEL_LAYER_PARTITION, which defaults to ON for supported models. The
normal README build therefore uses the validated partitioned path: fine-grained
prefill partitions and coarser Pow-boundary decode partitions. The finer
DeepSeek decode split can still be selected for experiments with
BUDDY_DSR1_DECODE_SPLIT=fine, but it is not the default because it changed
later greedy tokens during validation.
The default build command is:
python3 tools/buddy-codegen/build_model.py \
--spec models/deepseek_r1/specs/f32.json \
--build-dir build
To use the original whole-graph compile path, disable the option explicitly:
python3 tools/buddy-codegen/build_model.py \
--spec models/deepseek_r1/specs/f32.json \
--build-dir build \
--cmake-args=-DBUDDY_MODEL_LAYER_PARTITION=OFF
The layer-partitioned path is automatically disabled for tiered KV cache builds,
RVV cross-compilation, and pre-generated MLIR directories that do not contain a
layer_partitioned/partition_manifest.json file.
By default, partitioned builds only emit the MLIR needed by the runtime:
partition kernels plus the combined forward_prefill.mlir and
forward_decode.mlir entry points. The standard whole-graph MLIR files are not
generated on this path because they are not consumed by the partitioned compile.
Per-partition debug wrappers can be enabled with
-DBUDDY_MODEL_LAYER_PARTITION_DEBUG_WRAPPERS=ON.
Model builds also default to BUDDY_MODEL_REUSE_WEIGHTS=ON. After the first
successful import writes the weight data and .buddy_weights_manifest.json,
later imports with the same model path, variant, precision, and expected weight
sizes reuse the existing weight files. This avoids repeating the multi-GB
weight extraction step during codegen or compiler iteration. Disable it with
-DBUDDY_MODEL_REUSE_WEIGHTS=OFF when intentionally replacing weights in the
same build directory.
For non-quantized weights, the importer writes weight data directly into the
runtime binary layout instead of first concatenating all tensors into one large
temporary NumPy array. The legacy concatenate-then-write path is still available
for debugging with --no-direct-plain-weight-export.
DeepSeek R1 Example
The commands below use the f32 DeepSeek R1 Distill Qwen 1.5B configuration as a manual, decomposed version of the default flow. They are useful for measuring baseline versus partitioned compile time or debugging the generated artifacts.
Generate Config And MLIR
Generate a config from the f32 spec, then import the model while emitting the
extra layer_partitioned MLIR directory:
python3 tools/buddy-codegen/gen_config.py \
--spec models/deepseek_r1/specs/f32.json \
-o /tmp/deepseek_r1_f32_config.json
python3 tools/buddy-codegen/import_model.py \
--config /tmp/deepseek_r1_f32_config.json \
--output-dir /tmp/deepseek_r1_codegen_exp \
--experimental-layer-partitioned \
--skip-full-mlir \
--skip-weights
The partitioned directory contains:
subgraph0_prefill<N>.mlirandsubgraph0_decode<N>.mlir: per-partition kernels. The validated DeepSeek default links both prefill and coarse decode partitions.forward_prefill.mlirandforward_decode.mlir: combined public entry points with the same runtime ABI as the original model.partition_manifest.json: a summary of partition counts and generated files.
Add --layer-partition-debug-wrappers to the import command if you need
forward_prefill<N>.mlir and forward_decode<N>.mlir wrappers for individual
partition debugging.
Compile Baseline And Partitioned MLIR
Compile the original full graphs as the baseline:
python3 tools/buddy-codegen/compile_pipeline.py \
--config /tmp/deepseek_r1_f32_config.json \
--compile-all \
--mlir-dir /tmp/deepseek_r1_codegen_exp \
--output-dir /tmp/deepseek_r1_codegen_exp/obj_baseline \
--buddy-opt build/bin/buddy-opt \
--llvm-tools-dir llvm/build/bin \
-j 4
Compile the validated partitioned path in parallel:
python3 tools/buddy-codegen/compile_pipeline.py \
--config /tmp/deepseek_r1_f32_config.json \
--compile-partitioned \
--mlir-dir /tmp/deepseek_r1_codegen_exp/layer_partitioned \
--output-dir /tmp/deepseek_r1_codegen_exp/obj_partitioned \
--buddy-opt build/bin/buddy-opt \
--llvm-tools-dir llvm/build/bin \
-j 48
The partitioned compile path rewrites very large all-zero tensor constants to
tensor.empty plus linalg.fill before lowering. This avoids embedding large
zero matrices into .rodata, which can otherwise create oversized object files
and linker relocation overflows.
Verify Partition Structure
Before linking or running the model, verify that partitioning preserves graph coverage and dependency order:
python3 tools/buddy-codegen/verify_layer_partition.py \
--config /tmp/deepseek_r1_f32_config.json \
--report /tmp/deepseek_r1_codegen_exp/layer_partition_verify.json
The verifier checks that every non-placeholder, non-output operation is present exactly once, the concatenated partition order matches the original operation order, and no producer partition appears after its consumer.
The command exits non-zero when a structural check fails. With --report, it
also writes a JSON summary containing the prefill/decode partition counts,
missing operations, duplicate operations, operation-order status, and
dependency-order violations. This is useful as a quick CI or local development
guard after changing partition strategies.
This check is structural only. It does not execute the compiled model or prove that the generated tokens are unchanged. Runtime validation should still compare greedy outputs or logits against the unpartitioned whole-graph model.
Link Runtime Library
The CMake build links the partitioned runtime library automatically. For manual
runtime testing, link all subgraph*.o partition kernels plus the combined
public entry points. Do not link forward_prefill<N>.o or
forward_decode<N>.o; they are debug wrappers and are not needed by the
runtime path.
Adapting To Other Models
To apply this flow to another model, add a model-specific split strategy under
models/<model_family>/codegen/partition_strategy.py. The generic importer
loads this module from config["model_family"] and calls
layer_split_strategy(kind) for prefill and decode. Validate the generated
public entry points against that model's runtime ABI. In practice this means
checking:
- partition boundaries match the model's layer structure;
- all cross-partition values are represented as explicit subgraph inputs and outputs;
- the combined
forward_*functions keep the same signature and result order as the original runtime entry points; - temporary tensors that cross partition boundaries have correct ownership and lifetime after bufferization;
- greedy inference or logits-level tests match the original unpartitioned model.
The verify_layer_partition.py script checks structural graph coverage and
dependency ordering, but it does not replace model-specific runtime validation.
Observed DeepSeek R1 Results
On the f32 DeepSeek R1 Distill Qwen 1.5B experiment:
- Baseline MLIR compile time: about 157 seconds total on the test machine
(
subgraph_prefill.oabout 134 seconds,subgraph_decode.oabout 20 seconds). - Validated full partitioned compile time with 48 jobs: about 2 seconds total
after using the lightweight
forwardlowering path for the combined partition dispatcher MLIR. The previous partitioned path spent about 16 seconds becauseforward_prefill.mlirused the full standard optimization pipeline even though it only contains subgraph calls and memref view plumbing. - Validated partition counts: 212 prefill partitions and 58 coarse decode partitions.
- Reusing existing DeepSeek f32 weights reduced a forced Stage 1 re-import from
the weight-extraction dominated path to about 25 seconds for import/export
work; the end-to-end forced rebuild including CMake checks, MLIR compile,
link, and
.raxpacking was about 46 seconds on the test machine. - Direct non-quantized weight export reduced first-time DeepSeek f32 weight
generation from about 115 seconds (
extract_weightsplusexport_weights) to about 19 seconds, while producing a byte-identicalarg0.data. - Partition verifier result: no missing operations, no duplicate operations, and no dependency-order violations.
- Runtime validation: exact prefill logits and exact decode logits for 16
greedy decode steps on the tested
Hiand joke prompts. - Fine-grained decode partitioning generated 169 decode partitions but changed later greedy tokens, so the default uses coarser Pow-boundary decode partitions.
These numbers are machine- and job-count-dependent, but they show the expected benefit of compiling smaller graph fragments in parallel.