Skip to main content

rlx_distributed/
partition.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Pipeline-parallel layer partitioning.
6//!
7//! Layers are split into `world_size` contiguous blocks and assigned to
8//! ranks in **reverse**: rank 0 owns the *last* block (so it produces the
9//! logits and samples), rank `world-1` owns the *first* block (so it does
10//! the token embedding). This matches mlx-lm's `PipelineMixin` and means a
11//! token's activations flow rank `world-1 → … → 1 → 0`.
12//!
13//! The split is exact (every layer assigned exactly once) and as even as
14//! possible: the first `num_layers % world` forward-blocks get one extra
15//! layer.
16
17use std::ops::Range;
18
19/// Where a rank sits in the pipeline. Drives whether it embeds tokens,
20/// just transforms hidden states, or produces logits.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum BlockRole {
23    /// `world == 1`: this rank runs the whole model (embed → layers → head).
24    Single,
25    /// Highest rank: owns the first layers, embeds the input tokens, and
26    /// sends hidden states toward rank 0.
27    First,
28    /// Interior rank: receives hidden states, runs its layers, forwards.
29    Middle,
30    /// Rank 0: owns the last layers, runs the final norm + LM head, and
31    /// produces logits to sample from.
32    Last,
33}
34
35/// Role of `rank` in a `world`-sized pipeline.
36pub fn block_role(rank: u32, world: u32) -> BlockRole {
37    if world <= 1 {
38        BlockRole::Single
39    } else if rank == world - 1 {
40        BlockRole::First
41    } else if rank == 0 {
42        BlockRole::Last
43    } else {
44        BlockRole::Middle
45    }
46}
47
48/// The contiguous, half-open range of layer indices `[start, end)` that
49/// `rank` owns in a `world`-sized pipeline.
50///
51/// rank 0 → last block, rank `world-1` → first block. Panics if
52/// `rank >= world`.
53pub fn pipeline_layer_range(num_layers: usize, rank: u32, world: u32) -> Range<usize> {
54    let world = world.max(1) as usize;
55    let rank = rank as usize;
56    assert!(rank < world, "rank {rank} out of range for world {world}");
57
58    let per = num_layers / world;
59    let extra = num_layers % world;
60    // This rank owns forward-block `j` (reverse assignment).
61    let j = world - 1 - rank;
62    // Forward block i has `per + (i < extra)` layers; the first `extra`
63    // blocks carry the remainder.
64    let size_of = |i: usize| per + if i < extra { 1 } else { 0 };
65    let start: usize = (0..j).map(size_of).sum();
66    let len = size_of(j);
67    start..(start + len)
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn roles_map_correctly() {
76        assert_eq!(block_role(0, 1), BlockRole::Single);
77        assert_eq!(block_role(0, 4), BlockRole::Last);
78        assert_eq!(block_role(3, 4), BlockRole::First);
79        assert_eq!(block_role(1, 4), BlockRole::Middle);
80        assert_eq!(block_role(2, 4), BlockRole::Middle);
81    }
82
83    #[test]
84    fn even_split_is_reversed() {
85        // 16 layers / 4 ranks: rank 0 = last 4, rank 3 = first 4.
86        assert_eq!(pipeline_layer_range(16, 0, 4), 12..16);
87        assert_eq!(pipeline_layer_range(16, 1, 4), 8..12);
88        assert_eq!(pipeline_layer_range(16, 2, 4), 4..8);
89        assert_eq!(pipeline_layer_range(16, 3, 4), 0..4);
90    }
91
92    #[test]
93    fn uneven_split_tiles_exactly() {
94        // 10 layers / 4 ranks: forward block sizes 3,3,2,2.
95        // reverse → rank0=[8,10), rank1=[6,8), rank2=[3,6), rank3=[0,3).
96        assert_eq!(pipeline_layer_range(10, 0, 4), 8..10);
97        assert_eq!(pipeline_layer_range(10, 1, 4), 6..8);
98        assert_eq!(pipeline_layer_range(10, 2, 4), 3..6);
99        assert_eq!(pipeline_layer_range(10, 3, 4), 0..3);
100
101        // Reassembled in rank order (world-1 down to 0) covers [0,10) once.
102        let mut covered = [0u8; 10];
103        for r in 0..4u32 {
104            for i in pipeline_layer_range(10, r, 4) {
105                covered[i] += 1;
106            }
107        }
108        assert!(covered.iter().all(|&c| c == 1), "every layer covered once");
109    }
110
111    #[test]
112    fn single_rank_owns_all() {
113        assert_eq!(pipeline_layer_range(28, 0, 1), 0..28);
114    }
115
116    #[test]
117    fn more_ranks_than_layers_is_safe() {
118        // 2 layers, 4 ranks: two ranks get a layer, two get empty ranges.
119        let total: usize = (0..4u32).map(|r| pipeline_layer_range(2, r, 4).len()).sum();
120        assert_eq!(total, 2);
121    }
122}