spark_model/layers/
ep_dispatch.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! EP=2 token dispatch/combine routing for MoE expert parallelism.
4//!
5//! Instead of dense all-reduce after local expert compute, this module
6//! partitions tokens by expert ownership and dispatches only the tokens
7//! that need remote experts. Communication is O(dispatched_tokens * hidden)
8//! rather than O(total_tokens * hidden).
9//!
10//! See `tasks/ep2-token-dispatch-design.md` for the full design.
11
12/// Routing table for EP token dispatch.
13///
14/// Partitions top-K expert assignments into local (this rank owns the expert)
15/// and remote (partner rank owns the expert) buckets. Each entry is a
16/// (token_index, expert_id, weight) triple.
17#[derive(Debug)]
18pub struct EpRoutingTable {
19    /// Token indices routed to this rank's experts.
20    pub local_token_indices: Vec<u32>,
21    /// Expert IDs for local tokens (absolute, not rank-relative).
22    pub local_expert_ids: Vec<u32>,
23    /// Routing weights for local tokens.
24    pub local_weights: Vec<f32>,
25    /// Token indices routed to the remote rank's experts.
26    pub remote_token_indices: Vec<u32>,
27    /// Expert IDs for remote tokens (absolute, not rank-relative).
28    pub remote_expert_ids: Vec<u32>,
29    /// Routing weights for remote tokens.
30    pub remote_weights: Vec<f32>,
31}
32
33impl EpRoutingTable {
34    /// Number of (token, expert) pairs handled locally.
35    pub fn local_count(&self) -> usize {
36        self.local_token_indices.len()
37    }
38
39    /// Number of (token, expert) pairs dispatched to remote rank.
40    pub fn remote_count(&self) -> usize {
41        self.remote_token_indices.len()
42    }
43
44    /// Total number of (token, expert) pairs (should equal num_tokens * top_k).
45    pub fn total_count(&self) -> usize {
46        self.local_count() + self.remote_count()
47    }
48}
49
50/// Build EP routing table from flattened gate indices and weights.
51///
52/// Given the top-K expert assignments for M tokens, partitions them into
53/// local vs remote based on which rank owns each expert.
54///
55/// # Arguments
56/// * `gate_indices` - Flattened [M * top_k] expert indices from top-K selection
57/// * `gate_weights` - Flattened [M * top_k] routing weights from top-K selection
58/// * `num_tokens`   - Number of tokens (M)
59/// * `top_k`        - Number of experts per token
60/// * `local_expert_start` - First expert index owned by this rank (inclusive)
61/// * `local_expert_end`   - Last expert index owned by this rank (exclusive)
62///
63/// # Panics
64/// Panics if `gate_indices.len() != num_tokens * top_k` or
65/// `gate_weights.len() != num_tokens * top_k`.
66pub fn build_ep_routing_table(
67    gate_indices: &[u32],
68    gate_weights: &[f32],
69    num_tokens: usize,
70    top_k: usize,
71    local_expert_start: usize,
72    local_expert_end: usize,
73) -> EpRoutingTable {
74    let total = num_tokens * top_k;
75    assert_eq!(gate_indices.len(), total, "gate_indices length mismatch");
76    assert_eq!(gate_weights.len(), total, "gate_weights length mismatch");
77
78    // Pre-allocate with worst-case capacity (all local or all remote).
79    let mut local_token_indices = Vec::with_capacity(total);
80    let mut local_expert_ids = Vec::with_capacity(total);
81    let mut local_weights = Vec::with_capacity(total);
82    let mut remote_token_indices = Vec::with_capacity(total);
83    let mut remote_expert_ids = Vec::with_capacity(total);
84    let mut remote_weights = Vec::with_capacity(total);
85
86    for token_idx in 0..num_tokens {
87        for k in 0..top_k {
88            let flat_idx = token_idx * top_k + k;
89            let expert_id = gate_indices[flat_idx];
90            let weight = gate_weights[flat_idx];
91            let eid = expert_id as usize;
92
93            if eid >= local_expert_start && eid < local_expert_end {
94                local_token_indices.push(token_idx as u32);
95                local_expert_ids.push(expert_id);
96                local_weights.push(weight);
97            } else {
98                remote_token_indices.push(token_idx as u32);
99                remote_expert_ids.push(expert_id);
100                remote_weights.push(weight);
101            }
102        }
103    }
104
105    EpRoutingTable {
106        local_token_indices,
107        local_expert_ids,
108        local_weights,
109        remote_token_indices,
110        remote_expert_ids,
111        remote_weights,
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_all_local() {
121        // 2 tokens, top_k=2, all experts in local range [0, 256)
122        let indices = vec![3u32, 7, 100, 200];
123        let weights = vec![0.6f32, 0.4, 0.55, 0.45];
124        let table = build_ep_routing_table(&indices, &weights, 2, 2, 0, 256);
125
126        assert_eq!(table.local_count(), 4);
127        assert_eq!(table.remote_count(), 0);
128        assert_eq!(table.total_count(), 4);
129        assert_eq!(table.local_token_indices, vec![0, 0, 1, 1]);
130        assert_eq!(table.local_expert_ids, vec![3, 7, 100, 200]);
131    }
132
133    #[test]
134    fn test_all_remote() {
135        // 2 tokens, top_k=2, all experts in remote range [256, 512)
136        let indices = vec![300u32, 400, 256, 511];
137        let weights = vec![0.6f32, 0.4, 0.55, 0.45];
138        let table = build_ep_routing_table(&indices, &weights, 2, 2, 0, 256);
139
140        assert_eq!(table.local_count(), 0);
141        assert_eq!(table.remote_count(), 4);
142        assert_eq!(table.remote_token_indices, vec![0, 0, 1, 1]);
143        assert_eq!(table.remote_expert_ids, vec![300, 400, 256, 511]);
144    }
145
146    #[test]
147    fn test_mixed_routing() {
148        // 3 tokens, top_k=2, experts split across ranks
149        // Rank 0 owns [0, 256), Rank 1 owns [256, 512)
150        let indices = vec![
151            10u32, 300, // token 0: expert 10 (local), expert 300 (remote)
152            255, 256, // token 1: expert 255 (local), expert 256 (remote)
153            400, 500, // token 2: expert 400 (remote), expert 500 (remote)
154        ];
155        let weights = vec![0.7f32, 0.3, 0.5, 0.5, 0.6, 0.4];
156        let table = build_ep_routing_table(&indices, &weights, 3, 2, 0, 256);
157
158        assert_eq!(table.local_count(), 2);
159        assert_eq!(table.remote_count(), 4);
160        assert_eq!(table.local_token_indices, vec![0, 1]);
161        assert_eq!(table.local_expert_ids, vec![10, 255]);
162        assert_eq!(table.local_weights, vec![0.7, 0.5]);
163        assert_eq!(table.remote_token_indices, vec![0, 1, 2, 2]);
164        assert_eq!(table.remote_expert_ids, vec![300, 256, 400, 500]);
165        assert_eq!(table.remote_weights, vec![0.3, 0.5, 0.6, 0.4]);
166    }
167
168    #[test]
169    fn test_rank1_perspective() {
170        // Same scenario but from rank 1's perspective [256, 512)
171        let indices = vec![
172            10u32, 300, // token 0: expert 10 (remote for rank1), 300 (local)
173            255, 256, // token 1: expert 255 (remote), 256 (local)
174        ];
175        let weights = vec![0.7f32, 0.3, 0.5, 0.5];
176        let table = build_ep_routing_table(&indices, &weights, 2, 2, 256, 512);
177
178        assert_eq!(table.local_count(), 2);
179        assert_eq!(table.remote_count(), 2);
180        assert_eq!(table.local_token_indices, vec![0, 1]);
181        assert_eq!(table.local_expert_ids, vec![300, 256]);
182        assert_eq!(table.remote_token_indices, vec![0, 1]);
183        assert_eq!(table.remote_expert_ids, vec![10, 255]);
184    }
185
186    #[test]
187    fn test_single_token() {
188        let indices = vec![5u32, 260, 100];
189        let weights = vec![0.5f32, 0.3, 0.2];
190        let table = build_ep_routing_table(&indices, &weights, 1, 3, 0, 256);
191
192        assert_eq!(table.local_count(), 2); // experts 5, 100
193        assert_eq!(table.remote_count(), 1); // expert 260
194        assert_eq!(table.total_count(), 3);
195    }
196
197    #[test]
198    #[should_panic(expected = "gate_indices length mismatch")]
199    fn index_length_mismatch_panics() {
200        let indices = vec![1u32, 2, 3]; // 3 elements
201        let weights = vec![0.5f32; 4];
202        build_ep_routing_table(&indices, &weights, 2, 2, 0, 256);
203    }
204
205    #[test]
206    #[should_panic(expected = "gate_weights length mismatch")]
207    fn weight_length_mismatch_panics() {
208        let indices = vec![1u32, 2, 3, 4];
209        let weights = vec![0.5f32; 3];
210        build_ep_routing_table(&indices, &weights, 2, 2, 0, 256);
211    }
212}