1use ferrum_types::{Device, FerrumError, Result};
18
19#[derive(Debug, Clone)]
21pub struct TensorParallelConfig {
22 pub world_size: usize,
24 pub rank: usize,
26 pub device: Device,
28 pub sequence_parallel: bool,
30 pub reduce_scatter: bool,
32}
33
34impl Default for TensorParallelConfig {
35 fn default() -> Self {
36 Self {
37 world_size: 1,
38 rank: 0,
39 device: Device::CPU,
40 sequence_parallel: false,
41 reduce_scatter: true,
42 }
43 }
44}
45
46impl TensorParallelConfig {
47 pub fn new(world_size: usize, rank: usize, device: Device) -> Self {
49 Self {
50 world_size,
51 rank,
52 device,
53 ..Default::default()
54 }
55 }
56
57 pub fn is_parallel(&self) -> bool {
59 self.world_size > 1
60 }
61
62 pub fn shard_size(&self, dim_size: usize) -> usize {
64 assert!(
65 dim_size.is_multiple_of(self.world_size),
66 "Dimension {} must be divisible by world size {}",
67 dim_size,
68 self.world_size
69 );
70 dim_size / self.world_size
71 }
72
73 pub fn shard_offset(&self, dim_size: usize) -> usize {
75 self.shard_size(dim_size) * self.rank
76 }
77
78 pub fn shard_range(&self, dim_size: usize) -> (usize, usize) {
80 let size = self.shard_size(dim_size);
81 let start = size * self.rank;
82 (start, start + size)
83 }
84}
85
86pub struct TensorParallelGroup {
88 config: TensorParallelConfig,
90 devices: Vec<Device>,
92}
93
94impl TensorParallelGroup {
95 pub fn new(devices: Vec<Device>, rank: usize) -> Result<Self> {
97 if devices.is_empty() {
98 return Err(FerrumError::config(
99 "No devices provided for tensor parallel group",
100 ));
101 }
102 if rank >= devices.len() {
103 return Err(FerrumError::config(format!(
104 "Rank {} >= num devices {}",
105 rank,
106 devices.len()
107 )));
108 }
109
110 let config = TensorParallelConfig::new(devices.len(), rank, devices[rank].clone());
111
112 Ok(Self { config, devices })
113 }
114
115 pub fn config(&self) -> &TensorParallelConfig {
117 &self.config
118 }
119
120 pub fn devices(&self) -> &[Device] {
122 &self.devices
123 }
124
125 pub fn world_size(&self) -> usize {
127 self.config.world_size
128 }
129
130 pub fn rank(&self) -> usize {
132 self.config.rank
133 }
134
135 pub fn device(&self) -> &Device {
137 &self.config.device
138 }
139
140 pub fn is_master(&self) -> bool {
142 self.config.rank == 0
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum LayerParallelType {
149 ColumnParallel,
151 RowParallel,
153 Replicated,
155}
156
157#[derive(Debug, Clone)]
159pub struct WeightShard {
160 pub name: String,
162 pub parallel_type: LayerParallelType,
164 pub shard_dim: usize,
166 pub shard_range: (usize, usize),
168}
169
170impl WeightShard {
171 pub fn column_parallel(
173 name: impl Into<String>,
174 dim_size: usize,
175 config: &TensorParallelConfig,
176 ) -> Self {
177 Self {
178 name: name.into(),
179 parallel_type: LayerParallelType::ColumnParallel,
180 shard_dim: 0, shard_range: config.shard_range(dim_size),
182 }
183 }
184
185 pub fn row_parallel(
187 name: impl Into<String>,
188 dim_size: usize,
189 config: &TensorParallelConfig,
190 ) -> Self {
191 Self {
192 name: name.into(),
193 parallel_type: LayerParallelType::RowParallel,
194 shard_dim: 1, shard_range: config.shard_range(dim_size),
196 }
197 }
198
199 pub fn replicated(name: impl Into<String>) -> Self {
201 Self {
202 name: name.into(),
203 parallel_type: LayerParallelType::Replicated,
204 shard_dim: 0,
205 shard_range: (0, 0), }
207 }
208}
209
210#[derive(Debug, Clone)]
212pub struct TransformerParallelMapping {
213 pub heads_per_rank: usize,
215 pub kv_heads_per_rank: usize,
217 pub head_dim: usize,
219 pub hidden_per_rank: usize,
221 pub intermediate_per_rank: usize,
223}
224
225impl TransformerParallelMapping {
226 pub fn new(
228 num_heads: usize,
229 num_kv_heads: usize,
230 head_dim: usize,
231 hidden_dim: usize,
232 intermediate_dim: usize,
233 tp_size: usize,
234 ) -> Result<Self> {
235 if !num_heads.is_multiple_of(tp_size) {
237 return Err(FerrumError::config(format!(
238 "num_heads {} must be divisible by tp_size {}",
239 num_heads, tp_size
240 )));
241 }
242 if !num_kv_heads.is_multiple_of(tp_size) {
243 return Err(FerrumError::config(format!(
244 "num_kv_heads {} must be divisible by tp_size {}",
245 num_kv_heads, tp_size
246 )));
247 }
248 if !intermediate_dim.is_multiple_of(tp_size) {
249 return Err(FerrumError::config(format!(
250 "intermediate_dim {} must be divisible by tp_size {}",
251 intermediate_dim, tp_size
252 )));
253 }
254
255 Ok(Self {
256 heads_per_rank: num_heads / tp_size,
257 kv_heads_per_rank: num_kv_heads / tp_size,
258 head_dim,
259 hidden_per_rank: hidden_dim, intermediate_per_rank: intermediate_dim / tp_size,
261 })
262 }
263
264 pub fn q_proj_size(&self) -> usize {
266 self.heads_per_rank * self.head_dim
267 }
268
269 pub fn k_proj_size(&self) -> usize {
271 self.kv_heads_per_rank * self.head_dim
272 }
273
274 pub fn v_proj_size(&self) -> usize {
276 self.kv_heads_per_rank * self.head_dim
277 }
278
279 pub fn o_proj_in_size(&self) -> usize {
281 self.heads_per_rank * self.head_dim
282 }
283
284 pub fn attention_weight_shards(
286 &self,
287 layer_idx: usize,
288 config: &TensorParallelConfig,
289 ) -> Vec<WeightShard> {
290 let prefix = format!("model.layers.{}.self_attn", layer_idx);
291
292 vec![
293 WeightShard::column_parallel(
295 format!("{}.q_proj.weight", prefix),
296 self.q_proj_size() * config.world_size,
297 config,
298 ),
299 WeightShard::column_parallel(
300 format!("{}.k_proj.weight", prefix),
301 self.k_proj_size() * config.world_size,
302 config,
303 ),
304 WeightShard::column_parallel(
305 format!("{}.v_proj.weight", prefix),
306 self.v_proj_size() * config.world_size,
307 config,
308 ),
309 WeightShard::row_parallel(
311 format!("{}.o_proj.weight", prefix),
312 self.o_proj_in_size() * config.world_size,
313 config,
314 ),
315 ]
316 }
317
318 pub fn mlp_weight_shards(
320 &self,
321 layer_idx: usize,
322 config: &TensorParallelConfig,
323 ) -> Vec<WeightShard> {
324 let prefix = format!("model.layers.{}.mlp", layer_idx);
325
326 vec![
327 WeightShard::column_parallel(
329 format!("{}.gate_proj.weight", prefix),
330 self.intermediate_per_rank * config.world_size,
331 config,
332 ),
333 WeightShard::column_parallel(
334 format!("{}.up_proj.weight", prefix),
335 self.intermediate_per_rank * config.world_size,
336 config,
337 ),
338 WeightShard::row_parallel(
340 format!("{}.down_proj.weight", prefix),
341 self.intermediate_per_rank * config.world_size,
342 config,
343 ),
344 ]
345 }
346}
347
348#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_tensor_parallel_config() {
358 let config = TensorParallelConfig::new(4, 2, Device::CUDA(2));
359 assert!(config.is_parallel());
360 assert_eq!(config.shard_size(128), 32);
361 assert_eq!(config.shard_offset(128), 64);
362 assert_eq!(config.shard_range(128), (64, 96));
363 }
364
365 #[test]
366 fn test_tensor_parallel_group() {
367 let devices = vec![
368 Device::CUDA(0),
369 Device::CUDA(1),
370 Device::CUDA(2),
371 Device::CUDA(3),
372 ];
373 let group = TensorParallelGroup::new(devices, 1).unwrap();
374
375 assert_eq!(group.world_size(), 4);
376 assert_eq!(group.rank(), 1);
377 assert_eq!(group.device(), &Device::CUDA(1));
378 assert!(!group.is_master());
379 }
380
381 #[test]
382 fn test_transformer_parallel_mapping() {
383 let mapping = TransformerParallelMapping::new(
385 32, 8, 128, 4096, 11009, 4, );
392
393 assert!(mapping.is_err());
395
396 let mapping = TransformerParallelMapping::new(32, 8, 128, 4096, 11008, 4);
398 assert!(mapping.is_ok());
399
400 let mapping = mapping.unwrap();
401 assert_eq!(mapping.heads_per_rank, 8);
402 assert_eq!(mapping.kv_heads_per_rank, 2);
403 }
404
405 #[test]
406 fn test_weight_shard() {
407 let config = TensorParallelConfig::new(4, 1, Device::CUDA(1));
408 let shard = WeightShard::column_parallel("test.weight", 4096, &config);
409
410 assert_eq!(shard.parallel_type, LayerParallelType::ColumnParallel);
411 assert_eq!(shard.shard_range, (1024, 2048));
412 }
413
414 #[test]
417 fn mapping_qwen3_4b_tp2() {
418 let m = TransformerParallelMapping::new(32, 8, 128, 2560, 9728, 2).unwrap();
420 assert_eq!(m.heads_per_rank, 16);
421 assert_eq!(m.kv_heads_per_rank, 4);
422 assert_eq!(m.head_dim, 128);
423 assert_eq!(m.q_proj_size(), 2048); assert_eq!(m.k_proj_size(), 512); assert_eq!(m.v_proj_size(), 512);
426 assert_eq!(m.o_proj_in_size(), 2048);
427 assert_eq!(m.intermediate_per_rank, 4864); assert_eq!(m.hidden_per_rank, 2560); }
430
431 #[test]
432 fn mapping_llama_70b_tp8() {
433 let m = TransformerParallelMapping::new(64, 8, 128, 8192, 28672, 8).unwrap();
435 assert_eq!(m.heads_per_rank, 8);
436 assert_eq!(m.kv_heads_per_rank, 1);
437 assert_eq!(m.q_proj_size(), 1024);
438 assert_eq!(m.k_proj_size(), 128);
439 assert_eq!(m.intermediate_per_rank, 3584);
440 }
441
442 #[test]
443 fn mapping_tinyllama_tp2() {
444 let m = TransformerParallelMapping::new(32, 4, 64, 2048, 5632, 2).unwrap();
446 assert_eq!(m.heads_per_rank, 16);
447 assert_eq!(m.kv_heads_per_rank, 2);
448 assert_eq!(m.q_proj_size(), 1024);
449 assert_eq!(m.k_proj_size(), 128);
450 assert_eq!(m.intermediate_per_rank, 2816);
451 }
452
453 #[test]
454 fn mapping_tp1_noop() {
455 let m = TransformerParallelMapping::new(32, 8, 128, 4096, 11008, 1).unwrap();
457 assert_eq!(m.heads_per_rank, 32);
458 assert_eq!(m.kv_heads_per_rank, 8);
459 assert_eq!(m.intermediate_per_rank, 11008);
460 }
461
462 #[test]
463 fn mapping_rejects_indivisible_heads() {
464 assert!(TransformerParallelMapping::new(7, 7, 64, 448, 1024, 2).is_err());
465 }
466
467 #[test]
468 fn mapping_rejects_indivisible_kv_heads() {
469 assert!(TransformerParallelMapping::new(32, 3, 64, 2048, 5632, 2).is_err());
470 }
471
472 #[test]
473 fn mapping_rejects_indivisible_intermediate() {
474 assert!(TransformerParallelMapping::new(32, 8, 128, 4096, 11009, 4).is_err());
475 }
476
477 #[test]
478 fn attention_weight_shards_correct() {
479 let m = TransformerParallelMapping::new(32, 8, 128, 4096, 11008, 4).unwrap();
480 let cfg = TensorParallelConfig::new(4, 2, Device::CUDA(2));
481 let shards = m.attention_weight_shards(5, &cfg);
482
483 assert_eq!(shards.len(), 4); assert_eq!(shards[0].name, "model.layers.5.self_attn.q_proj.weight");
486 assert_eq!(shards[0].parallel_type, LayerParallelType::ColumnParallel);
487 assert_eq!(shards[0].shard_range, (2048, 3072));
488 assert_eq!(shards[1].shard_range, (512, 768));
490 assert_eq!(shards[2].shard_range, (512, 768));
492 assert_eq!(shards[3].name, "model.layers.5.self_attn.o_proj.weight");
494 assert_eq!(shards[3].parallel_type, LayerParallelType::RowParallel);
495 assert_eq!(shards[3].shard_range, (2048, 3072));
496 }
497
498 #[test]
499 fn mlp_weight_shards_correct() {
500 let m = TransformerParallelMapping::new(32, 8, 128, 4096, 11008, 4).unwrap();
501 let cfg = TensorParallelConfig::new(4, 0, Device::CUDA(0));
502 let shards = m.mlp_weight_shards(3, &cfg);
503
504 assert_eq!(shards.len(), 3); assert_eq!(shards[0].name, "model.layers.3.mlp.gate_proj.weight");
507 assert_eq!(shards[0].parallel_type, LayerParallelType::ColumnParallel);
508 assert_eq!(shards[0].shard_range, (0, 2752));
509 assert_eq!(shards[1].shard_range, (0, 2752));
511 assert_eq!(shards[2].name, "model.layers.3.mlp.down_proj.weight");
513 assert_eq!(shards[2].parallel_type, LayerParallelType::RowParallel);
514 assert_eq!(shards[2].shard_range, (0, 2752));
515 }
516
517 #[test]
518 fn weight_shard_replicated() {
519 let shard = WeightShard::replicated("model.norm.weight");
520 assert_eq!(shard.parallel_type, LayerParallelType::Replicated);
521 assert_eq!(shard.shard_range, (0, 0));
522 }
523
524 #[test]
525 fn config_shard_range_all_ranks_cover_full_dim() {
526 let dim = 4096;
527 let tp_size = 4;
528 let mut covered = vec![false; dim];
529
530 for rank in 0..tp_size {
531 let cfg = TensorParallelConfig::new(tp_size, rank, Device::CUDA(rank));
532 let (start, end) = cfg.shard_range(dim);
533 assert_eq!(end - start, dim / tp_size);
534 for i in start..end {
535 assert!(!covered[i], "Overlap at index {i}");
536 covered[i] = true;
537 }
538 }
539 assert!(covered.iter().all(|&c| c), "Not all indices covered");
540 }
541
542 #[test]
543 fn config_non_parallel() {
544 let cfg = TensorParallelConfig::new(1, 0, Device::CPU);
545 assert!(!cfg.is_parallel());
546 assert_eq!(cfg.shard_size(4096), 4096);
547 assert_eq!(cfg.shard_range(4096), (0, 4096));
548 }
549
550 #[test]
551 fn group_rejects_invalid_rank() {
552 let devices = vec![Device::CUDA(0), Device::CUDA(1)];
553 assert!(TensorParallelGroup::new(devices, 5).is_err());
554 }
555
556 #[test]
557 fn group_rejects_empty_devices() {
558 assert!(TensorParallelGroup::new(vec![], 0).is_err());
559 }
560
561 #[test]
562 fn row_parallel_shard_dim_is_1() {
563 let cfg = TensorParallelConfig::new(2, 0, Device::CUDA(0));
564 let shard = WeightShard::row_parallel("test", 4096, &cfg);
565 assert_eq!(shard.shard_dim, 1); }
567
568 #[test]
569 fn column_parallel_shard_dim_is_0() {
570 let cfg = TensorParallelConfig::new(2, 0, Device::CUDA(0));
571 let shard = WeightShard::column_parallel("test", 4096, &cfg);
572 assert_eq!(shard.shard_dim, 0); }
574}