/home/noah/src/realizar/src/parallel.rs
Line | Count | Source |
1 | | //! Multi-GPU and Distributed Inference |
2 | | //! |
3 | | //! Per spec §10: Implements parallelism strategies for 70B+ model inference. |
4 | | //! Reference: [11] Shoeybi et al. (2019) "Megatron-LM: Training Multi-Billion Parameter LMs" |
5 | | //! |
6 | | //! ## Parallelism Strategies |
7 | | //! |
8 | | //! | Strategy | Description | Use Case | Scaling | |
9 | | //! |----------|-------------|----------|---------| |
10 | | //! | Tensor Parallel (TP) | Split tensors across GPUs | Within node | 2-8 GPUs | |
11 | | //! | Pipeline Parallel (PP) | Split layers across GPUs | Across nodes | 2-64 GPUs | |
12 | | //! | Data Parallel (DP) | Replicate model, split batches | High throughput | Any | |
13 | | //! |
14 | | //! ## Performance Target |
15 | | //! |
16 | | //! Per spec §1.3: >85% scaling efficiency for 2-8 GPUs (Amdahl's law measurement) |
17 | | |
18 | | // Module-level clippy allows |
19 | | #![allow(clippy::must_use_candidate)] |
20 | | #![allow(clippy::return_self_not_must_use)] |
21 | | #![allow(clippy::missing_errors_doc)] |
22 | | |
23 | | use serde::{Deserialize, Serialize}; |
24 | | use std::collections::HashMap; |
25 | | use std::sync::Arc; |
26 | | use thiserror::Error; |
27 | | |
28 | | /// Error type for parallelism operations |
29 | | #[derive(Debug, Error)] |
30 | | pub enum ParallelError { |
31 | | /// Invalid rank |
32 | | #[error("Invalid rank {rank} for world size {world_size}")] |
33 | | InvalidRank { |
34 | | /// The invalid rank value |
35 | | rank: usize, |
36 | | /// The total world size |
37 | | world_size: usize, |
38 | | }, |
39 | | |
40 | | /// Invalid world size |
41 | | #[error("Invalid world size: {0}")] |
42 | | InvalidWorldSize(usize), |
43 | | |
44 | | /// Communication error |
45 | | #[error("Communication error: {0}")] |
46 | | CommunicationError(String), |
47 | | |
48 | | /// Tensor shape mismatch |
49 | | #[error("Tensor shape mismatch: expected {expected:?}, got {got:?}")] |
50 | | ShapeMismatch { |
51 | | /// Expected shape |
52 | | expected: Vec<usize>, |
53 | | /// Actual shape |
54 | | got: Vec<usize>, |
55 | | }, |
56 | | |
57 | | /// Pipeline stage error |
58 | | #[error("Pipeline stage error: {0}")] |
59 | | PipelineError(String), |
60 | | |
61 | | /// Not initialized |
62 | | #[error("Parallel context not initialized")] |
63 | | NotInitialized, |
64 | | } |
65 | | |
66 | | /// Reduce operation for collective communications |
67 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
68 | | pub enum ReduceOp { |
69 | | /// Sum all values |
70 | | Sum, |
71 | | /// Take maximum |
72 | | Max, |
73 | | /// Take minimum |
74 | | Min, |
75 | | /// Average (Sum / world_size) |
76 | | Avg, |
77 | | } |
78 | | |
79 | | /// Parallel configuration |
80 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
81 | | pub struct ParallelConfig { |
82 | | /// Tensor parallel size (within node) |
83 | | pub tp_size: usize, |
84 | | /// Pipeline parallel size (across nodes) |
85 | | pub pp_size: usize, |
86 | | /// Data parallel size (batch distribution) |
87 | | pub dp_size: usize, |
88 | | /// Current global rank |
89 | | pub rank: usize, |
90 | | /// Total world size |
91 | | pub world_size: usize, |
92 | | } |
93 | | |
94 | | impl ParallelConfig { |
95 | | /// Create a new parallel configuration |
96 | | /// |
97 | | /// # Arguments |
98 | | /// |
99 | | /// * `tp_size` - Tensor parallel size (typically 2, 4, or 8) |
100 | | /// * `pp_size` - Pipeline parallel size (number of stages) |
101 | | /// * `dp_size` - Data parallel size (batch replication) |
102 | | /// * `rank` - Current process rank |
103 | 11 | pub fn new( |
104 | 11 | tp_size: usize, |
105 | 11 | pp_size: usize, |
106 | 11 | dp_size: usize, |
107 | 11 | rank: usize, |
108 | 11 | ) -> Result<Self, ParallelError> { |
109 | 11 | let world_size = tp_size * pp_size * dp_size; |
110 | | |
111 | 11 | if world_size == 0 { |
112 | 4 | return Err(ParallelError::InvalidWorldSize(0)); |
113 | 7 | } |
114 | | |
115 | 7 | if rank >= world_size { |
116 | 1 | return Err(ParallelError::InvalidRank { rank, world_size }); |
117 | 6 | } |
118 | | |
119 | 6 | Ok(Self { |
120 | 6 | tp_size, |
121 | 6 | pp_size, |
122 | 6 | dp_size, |
123 | 6 | rank, |
124 | 6 | world_size, |
125 | 6 | }) |
126 | 11 | } |
127 | | |
128 | | /// Create single-GPU configuration (no parallelism) |
129 | 4 | pub fn single() -> Self { |
130 | 4 | Self { |
131 | 4 | tp_size: 1, |
132 | 4 | pp_size: 1, |
133 | 4 | dp_size: 1, |
134 | 4 | rank: 0, |
135 | 4 | world_size: 1, |
136 | 4 | } |
137 | 4 | } |
138 | | |
139 | | /// Get tensor parallel rank within TP group |
140 | 4 | pub fn tp_rank(&self) -> usize { |
141 | 4 | self.rank % self.tp_size |
142 | 4 | } |
143 | | |
144 | | /// Get pipeline parallel stage |
145 | 4 | pub fn pp_stage(&self) -> usize { |
146 | 4 | (self.rank / self.tp_size) % self.pp_size |
147 | 4 | } |
148 | | |
149 | | /// Get data parallel rank |
150 | 1 | pub fn dp_rank(&self) -> usize { |
151 | 1 | self.rank / (self.tp_size * self.pp_size) |
152 | 1 | } |
153 | | |
154 | | /// Check if this is the first TP rank |
155 | 1 | pub fn is_tp_first(&self) -> bool { |
156 | 1 | self.tp_rank() == 0 |
157 | 1 | } |
158 | | |
159 | | /// Check if this is the last TP rank |
160 | 1 | pub fn is_tp_last(&self) -> bool { |
161 | 1 | self.tp_rank() == self.tp_size - 1 |
162 | 1 | } |
163 | | |
164 | | /// Check if this is the first PP stage |
165 | 1 | pub fn is_pp_first(&self) -> bool { |
166 | 1 | self.pp_stage() == 0 |
167 | 1 | } |
168 | | |
169 | | /// Check if this is the last PP stage |
170 | 1 | pub fn is_pp_last(&self) -> bool { |
171 | 1 | self.pp_stage() == self.pp_size - 1 |
172 | 1 | } |
173 | | } |
174 | | |
175 | | impl Default for ParallelConfig { |
176 | 0 | fn default() -> Self { |
177 | 0 | Self::single() |
178 | 0 | } |
179 | | } |
180 | | |
181 | | /// Mock tensor for parallelism testing |
182 | | /// In production, this would be replaced with trueno::Tensor |
183 | | #[derive(Debug, Clone)] |
184 | | pub struct ParallelTensor { |
185 | | /// Shape of the tensor |
186 | | pub shape: Vec<usize>, |
187 | | /// Data (f32 for simplicity) |
188 | | pub data: Vec<f32>, |
189 | | } |
190 | | |
191 | | impl ParallelTensor { |
192 | | /// Create a new tensor |
193 | 15 | pub fn new(shape: Vec<usize>, data: Vec<f32>) -> Result<Self, ParallelError> { |
194 | 15 | let expected_size: usize = shape.iter().product(); |
195 | 15 | if data.len() != expected_size { |
196 | 0 | return Err(ParallelError::ShapeMismatch { |
197 | 0 | expected: vec![expected_size], |
198 | 0 | got: vec![data.len()], |
199 | 0 | }); |
200 | 15 | } |
201 | 15 | Ok(Self { shape, data }) |
202 | 15 | } |
203 | | |
204 | | /// Create a zeros tensor |
205 | 1 | pub fn zeros(shape: Vec<usize>) -> Self { |
206 | 1 | let size: usize = shape.iter().product(); |
207 | 1 | Self { |
208 | 1 | shape, |
209 | 1 | data: vec![0.0; size], |
210 | 1 | } |
211 | 1 | } |
212 | | |
213 | | /// Get a narrow slice along a dimension |
214 | 4 | pub fn narrow(&self, dim: usize, start: usize, length: usize) -> Result<Self, ParallelError> { |
215 | 4 | if dim >= self.shape.len() { |
216 | 0 | return Err(ParallelError::ShapeMismatch { |
217 | 0 | expected: vec![dim], |
218 | 0 | got: self.shape.clone(), |
219 | 0 | }); |
220 | 4 | } |
221 | | |
222 | | // For 2D tensors (matrices), implement proper narrowing |
223 | 4 | if self.shape.len() == 2 { |
224 | 4 | let rows = self.shape[0]; |
225 | 4 | let cols = self.shape[1]; |
226 | | |
227 | 4 | if dim == 0 { |
228 | | // Narrow rows |
229 | 3 | let mut new_data = Vec::with_capacity(length * cols); |
230 | 8 | for row in start3 ..(start + length)3 { |
231 | 8 | let row_start = row * cols; |
232 | 8 | new_data.extend_from_slice(&self.data[row_start..row_start + cols]); |
233 | 8 | } |
234 | 3 | let new_shape = vec![length, cols]; |
235 | 3 | return Ok(Self { |
236 | 3 | shape: new_shape, |
237 | 3 | data: new_data, |
238 | 3 | }); |
239 | 1 | } |
240 | | // Narrow columns |
241 | 1 | let mut new_data = Vec::with_capacity(rows * length); |
242 | 2 | for row in 0..rows1 { |
243 | 2 | let row_start = row * cols; |
244 | 2 | new_data |
245 | 2 | .extend_from_slice(&self.data[row_start + start..row_start + start + length]); |
246 | 2 | } |
247 | 1 | let new_shape = vec![rows, length]; |
248 | 1 | return Ok(Self { |
249 | 1 | shape: new_shape, |
250 | 1 | data: new_data, |
251 | 1 | }); |
252 | 0 | } |
253 | | |
254 | | // For 1D tensors |
255 | 0 | if self.shape.len() == 1 { |
256 | 0 | let new_data = self.data[start..start + length].to_vec(); |
257 | 0 | return Ok(Self { |
258 | 0 | shape: vec![length], |
259 | 0 | data: new_data, |
260 | 0 | }); |
261 | 0 | } |
262 | | |
263 | | // Fallback: simplified implementation |
264 | 0 | let new_data = self.data[start..start + length].to_vec(); |
265 | 0 | let mut new_shape = self.shape.clone(); |
266 | 0 | new_shape[dim] = length; |
267 | 0 | Ok(Self { |
268 | 0 | shape: new_shape, |
269 | 0 | data: new_data, |
270 | 0 | }) |
271 | 4 | } |
272 | | |
273 | | /// Transpose for 2D tensors |
274 | 3 | pub fn transpose(&self) -> Result<Self, ParallelError> { |
275 | 3 | if self.shape.len() != 2 { |
276 | 0 | return Err(ParallelError::ShapeMismatch { |
277 | 0 | expected: vec![2], |
278 | 0 | got: vec![self.shape.len()], |
279 | 0 | }); |
280 | 3 | } |
281 | | |
282 | 3 | let rows = self.shape[0]; |
283 | 3 | let cols = self.shape[1]; |
284 | 3 | let mut new_data = vec![0.0; rows * cols]; |
285 | | |
286 | 8 | for i in 0..rows3 { |
287 | 38 | for j in 0..cols8 { |
288 | 38 | new_data[j * rows + i] = self.data[i * cols + j]; |
289 | 38 | } |
290 | | } |
291 | | |
292 | 3 | Ok(Self { |
293 | 3 | shape: vec![cols, rows], |
294 | 3 | data: new_data, |
295 | 3 | }) |
296 | 3 | } |
297 | | |
298 | | /// Matrix multiplication (simplified) |
299 | 3 | pub fn matmul(&self, other: &Self) -> Result<Self, ParallelError> { |
300 | 3 | if self.shape.len() != 2 || other.shape.len() != 2 { |
301 | 0 | return Err(ParallelError::ShapeMismatch { |
302 | 0 | expected: vec![2, 2], |
303 | 0 | got: vec![self.shape.len(), other.shape.len()], |
304 | 0 | }); |
305 | 3 | } |
306 | | |
307 | 3 | let m = self.shape[0]; |
308 | 3 | let k = self.shape[1]; |
309 | 3 | let n = other.shape[1]; |
310 | | |
311 | 3 | if k != other.shape[0] { |
312 | 0 | return Err(ParallelError::ShapeMismatch { |
313 | 0 | expected: vec![k], |
314 | 0 | got: vec![other.shape[0]], |
315 | 0 | }); |
316 | 3 | } |
317 | | |
318 | 3 | let mut result = vec![0.0; m * n]; |
319 | | |
320 | 3 | for i in 0..m { |
321 | 8 | for j in 0..n3 { |
322 | 8 | let mut sum = 0.0; |
323 | 36 | for l in 0..k8 { |
324 | 36 | sum += self.data[i * k + l] * other.data[l * n + j]; |
325 | 36 | } |
326 | 8 | result[i * n + j] = sum; |
327 | | } |
328 | | } |
329 | | |
330 | 3 | Ok(Self { |
331 | 3 | shape: vec![m, n], |
332 | 3 | data: result, |
333 | 3 | }) |
334 | 3 | } |
335 | | |
336 | | /// Add another tensor element-wise |
337 | 1 | pub fn add(&self, other: &Self) -> Result<Self, ParallelError> { |
338 | 1 | if self.shape != other.shape { |
339 | 0 | return Err(ParallelError::ShapeMismatch { |
340 | 0 | expected: self.shape.clone(), |
341 | 0 | got: other.shape.clone(), |
342 | 0 | }); |
343 | 1 | } |
344 | | |
345 | 1 | let data: Vec<f32> = self |
346 | 1 | .data |
347 | 1 | .iter() |
348 | 1 | .zip(&other.data) |
349 | 4 | .map1 (|(a, b)| a + b) |
350 | 1 | .collect(); |
351 | 1 | Ok(Self { |
352 | 1 | shape: self.shape.clone(), |
353 | 1 | data, |
354 | 1 | }) |
355 | 1 | } |
356 | | |
357 | | /// Sum all elements |
358 | 1 | pub fn sum(&self) -> f32 { |
359 | 1 | self.data.iter().sum() |
360 | 1 | } |
361 | | |
362 | | /// Number of elements |
363 | 1 | pub fn numel(&self) -> usize { |
364 | 1 | self.data.len() |
365 | 1 | } |
366 | | } |
367 | | |
368 | | /// Mock communicator for collective operations |
369 | | /// In production, this would use NCCL or MPI |
370 | | #[derive(Debug, Clone)] |
371 | | pub struct Communicator { |
372 | | /// World size |
373 | | world_size: usize, |
374 | | /// Current rank |
375 | | rank: usize, |
376 | | /// test buffers for testing |
377 | | #[allow(dead_code)] |
378 | | buffers: Arc<std::sync::RwLock<HashMap<usize, Vec<f32>>>>, |
379 | | } |
380 | | |
381 | | impl Communicator { |
382 | | /// Create a new communicator |
383 | 15 | pub fn new(world_size: usize, rank: usize) -> Result<Self, ParallelError> { |
384 | 15 | if rank >= world_size { |
385 | 2 | return Err(ParallelError::InvalidRank { rank, world_size }); |
386 | 13 | } |
387 | 13 | Ok(Self { |
388 | 13 | world_size, |
389 | 13 | rank, |
390 | 13 | buffers: Arc::new(std::sync::RwLock::new(HashMap::new())), |
391 | 13 | }) |
392 | 15 | } |
393 | | |
394 | | /// All-reduce operation |
395 | 3 | pub fn all_reduce( |
396 | 3 | &self, |
397 | 3 | tensor: &ParallelTensor, |
398 | 3 | op: ReduceOp, |
399 | 3 | ) -> Result<ParallelTensor, ParallelError> { |
400 | | // In a real implementation, this would use NCCL |
401 | | // For testing, we simulate single-process behavior |
402 | 3 | match op { |
403 | | ReduceOp::Sum => { |
404 | | // Single process: multiply by world_size to simulate sum from all ranks |
405 | 2 | let data: Vec<f32> = tensor |
406 | 2 | .data |
407 | 2 | .iter() |
408 | 4 | .map2 (|x| x * self.world_size as f32) |
409 | 2 | .collect(); |
410 | 2 | Ok(ParallelTensor { |
411 | 2 | shape: tensor.shape.clone(), |
412 | 2 | data, |
413 | 2 | }) |
414 | | }, |
415 | | ReduceOp::Avg => { |
416 | | // Average: no change in single process (sum / world_size = value) |
417 | 1 | Ok(tensor.clone()) |
418 | | }, |
419 | | ReduceOp::Max | ReduceOp::Min => { |
420 | | // Single process: return as-is |
421 | 0 | Ok(tensor.clone()) |
422 | | }, |
423 | | } |
424 | 3 | } |
425 | | |
426 | | /// All-gather operation |
427 | 1 | pub fn all_gather(&self, tensor: &ParallelTensor) -> Result<ParallelTensor, ParallelError> { |
428 | | // Simulate all-gather by replicating data world_size times |
429 | 1 | let mut data = Vec::with_capacity(tensor.data.len() * self.world_size); |
430 | 2 | for _ in 0..self.world_size1 { |
431 | 2 | data.extend_from_slice(&tensor.data); |
432 | 2 | } |
433 | | |
434 | 1 | let mut new_shape = tensor.shape.clone(); |
435 | 1 | if !new_shape.is_empty() { |
436 | 1 | new_shape[0] *= self.world_size; |
437 | 1 | }0 |
438 | | |
439 | 1 | Ok(ParallelTensor { |
440 | 1 | shape: new_shape, |
441 | 1 | data, |
442 | 1 | }) |
443 | 1 | } |
444 | | |
445 | | /// Reduce-scatter operation |
446 | 0 | pub fn reduce_scatter( |
447 | 0 | &self, |
448 | 0 | tensor: &ParallelTensor, |
449 | 0 | op: ReduceOp, |
450 | 0 | ) -> Result<ParallelTensor, ParallelError> { |
451 | | // Reduce then scatter: each rank gets 1/world_size of the result |
452 | 0 | let chunk_size = tensor.data.len() / self.world_size; |
453 | 0 | let start = self.rank * chunk_size; |
454 | 0 | let end = start + chunk_size; |
455 | | |
456 | 0 | let chunk_data: Vec<f32> = match op { |
457 | 0 | ReduceOp::Sum => tensor.data[start..end] |
458 | 0 | .iter() |
459 | 0 | .map(|x| x * self.world_size as f32) |
460 | 0 | .collect(), |
461 | 0 | ReduceOp::Avg | ReduceOp::Max | ReduceOp::Min => tensor.data[start..end].to_vec(), |
462 | | }; |
463 | | |
464 | 0 | let mut new_shape = tensor.shape.clone(); |
465 | 0 | if !new_shape.is_empty() { |
466 | 0 | new_shape[0] /= self.world_size; |
467 | 0 | } |
468 | | |
469 | 0 | Ok(ParallelTensor { |
470 | 0 | shape: new_shape, |
471 | 0 | data: chunk_data, |
472 | 0 | }) |
473 | 0 | } |
474 | | |
475 | | /// Barrier synchronization |
476 | 1 | pub fn barrier(&self) -> Result<(), ParallelError> { |
477 | | // In real implementation, this would synchronize all processes |
478 | 1 | Ok(()) |
479 | 1 | } |
480 | | |
481 | | /// Get world size |
482 | 1 | pub fn world_size(&self) -> usize { |
483 | 1 | self.world_size |
484 | 1 | } |
485 | | |
486 | | /// Get rank |
487 | 1 | pub fn rank(&self) -> usize { |
488 | 1 | self.rank |
489 | 1 | } |
490 | | } |
491 | | |
492 | | /// Tensor Parallelism for multi-GPU inference |
493 | | /// Reference: [11] Megatron-LM tensor parallelism |
494 | | #[derive(Debug)] |
495 | | pub struct TensorParallel { |
496 | | /// Number of tensor parallel ranks |
497 | | tp_size: usize, |
498 | | /// Current rank within TP group |
499 | | rank: usize, |
500 | | /// Communication group |
501 | | comm: Communicator, |
502 | | } |
503 | | |
504 | | impl TensorParallel { |
505 | | /// Create a new tensor parallel context |
506 | 10 | pub fn new(tp_size: usize, rank: usize) -> Result<Self, ParallelError> { |
507 | 10 | if tp_size == 0 { |
508 | 1 | return Err(ParallelError::InvalidWorldSize(0)); |
509 | 9 | } |
510 | 9 | if rank >= tp_size { |
511 | 2 | return Err(ParallelError::InvalidRank { |
512 | 2 | rank, |
513 | 2 | world_size: tp_size, |
514 | 2 | }); |
515 | 7 | } |
516 | | |
517 | 7 | let comm = Communicator::new(tp_size, rank)?0 ; |
518 | | |
519 | 7 | Ok(Self { |
520 | 7 | tp_size, |
521 | 7 | rank, |
522 | 7 | comm, |
523 | 7 | }) |
524 | 10 | } |
525 | | |
526 | | /// Get chunk size for weight sharding |
527 | 5 | pub fn chunk_size(&self, total_size: usize) -> usize { |
528 | 5 | total_size / self.tp_size |
529 | 5 | } |
530 | | |
531 | | /// Column-parallel linear (for MLP first layer, attention QKV) |
532 | | /// |
533 | | /// Each rank holds weight[:, rank*chunk:(rank+1)*chunk] |
534 | | /// No communication needed as outputs are independent |
535 | 1 | pub fn column_parallel_linear( |
536 | 1 | &self, |
537 | 1 | input: &ParallelTensor, |
538 | 1 | weight: &ParallelTensor, |
539 | 1 | bias: Option<&ParallelTensor>, |
540 | 1 | ) -> Result<ParallelTensor, ParallelError> { |
541 | | // Get local weight slice |
542 | 1 | let output_dim = weight.shape[0]; |
543 | 1 | let chunk = self.chunk_size(output_dim); |
544 | 1 | let local_weight = weight.narrow(0, self.rank * chunk, chunk)?0 ; |
545 | | |
546 | | // Transpose weight for matmul: (out_chunk, in) -> (in, out_chunk) |
547 | 1 | let weight_t = local_weight.transpose()?0 ; |
548 | | |
549 | | // Local matmul: (batch, in) @ (in, out_chunk) -> (batch, out_chunk) |
550 | 1 | let mut local_output = input.matmul(&weight_t)?0 ; |
551 | | |
552 | | // Add local bias if present |
553 | 1 | if let Some(b0 ) = bias { |
554 | 0 | let local_bias = b.narrow(0, self.rank * chunk, chunk)?; |
555 | | // Broadcast bias addition |
556 | 0 | let bias_expanded = ParallelTensor { |
557 | 0 | shape: local_output.shape.clone(), |
558 | 0 | data: local_output |
559 | 0 | .data |
560 | 0 | .iter() |
561 | 0 | .enumerate() |
562 | 0 | .map(|(i, v)| v + local_bias.data[i % local_bias.data.len()]) |
563 | 0 | .collect(), |
564 | | }; |
565 | 0 | local_output = bias_expanded; |
566 | 1 | } |
567 | | |
568 | 1 | Ok(local_output) |
569 | 1 | } |
570 | | |
571 | | /// Row-parallel linear (for MLP second layer, attention output) |
572 | | /// |
573 | | /// Each rank holds weight[rank*chunk:(rank+1)*chunk, :] |
574 | | /// Requires all-reduce to sum partial results |
575 | 1 | pub fn row_parallel_linear( |
576 | 1 | &self, |
577 | 1 | input: &ParallelTensor, |
578 | 1 | weight: &ParallelTensor, |
579 | 1 | bias: Option<&ParallelTensor>, |
580 | 1 | ) -> Result<ParallelTensor, ParallelError> { |
581 | | // Get local weight slice (rows) |
582 | 1 | let input_dim = weight.shape[0]; |
583 | 1 | let chunk = self.chunk_size(input_dim); |
584 | 1 | let local_weight = weight.narrow(0, self.rank * chunk, chunk)?0 ; |
585 | | |
586 | | // Transpose for matmul |
587 | 1 | let weight_t = local_weight.transpose()?0 ; |
588 | | |
589 | | // Local matmul |
590 | 1 | let local_output = input.matmul(&weight_t)?0 ; |
591 | | |
592 | | // All-reduce to sum partial results |
593 | 1 | let mut output = self.comm.all_reduce(&local_output, ReduceOp::Sum)?0 ; |
594 | | |
595 | | // Add bias only on rank 0 to avoid double counting |
596 | 1 | if self.rank == 0 { |
597 | 1 | if let Some(b0 ) = bias { |
598 | 0 | output = output.add(b)?; |
599 | 1 | } |
600 | 0 | } |
601 | | |
602 | 1 | Ok(output) |
603 | 1 | } |
604 | | |
605 | | /// Get TP rank |
606 | 1 | pub fn rank(&self) -> usize { |
607 | 1 | self.rank |
608 | 1 | } |
609 | | |
610 | | /// Get TP size |
611 | 2 | pub fn tp_size(&self) -> usize { |
612 | 2 | self.tp_size |
613 | 2 | } |
614 | | } |
615 | | |
616 | | /// Pipeline stage info |
617 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
618 | | pub struct PipelineStage { |
619 | | /// Stage index |
620 | | pub index: usize, |
621 | | /// Start layer index |
622 | | pub start_layer: usize, |
623 | | /// End layer index (exclusive) |
624 | | pub end_layer: usize, |
625 | | /// Number of layers in this stage |
626 | | pub num_layers: usize, |
627 | | } |
628 | | |
629 | | /// Pipeline Parallelism for multi-node inference |
630 | | /// Reference: [11] GPipe-style pipeline parallelism |
631 | | #[derive(Debug)] |
632 | | pub struct PipelineParallel { |
633 | | /// Number of pipeline stages |
634 | | pp_size: usize, |
635 | | /// Current stage |
636 | | stage: usize, |
637 | | /// Stage info |
638 | | stage_info: PipelineStage, |
639 | | /// Micro-batch size for pipelining |
640 | | micro_batch_size: usize, |
641 | | /// Stats |
642 | | stats: PipelineStats, |
643 | | } |
644 | | |
645 | | /// Pipeline execution statistics |
646 | | #[derive(Debug, Clone, Default, Serialize, Deserialize)] |
647 | | pub struct PipelineStats { |
648 | | /// Total micro-batches processed |
649 | | pub micro_batches_processed: u64, |
650 | | /// Total pipeline bubbles (idle time) |
651 | | pub bubble_time_ms: f64, |
652 | | /// Average stage latency |
653 | | pub avg_stage_latency_ms: f64, |
654 | | /// Total forward passes |
655 | | pub forward_passes: u64, |
656 | | } |
657 | | |
658 | | impl PipelineParallel { |
659 | | /// Create a new pipeline parallel context |
660 | | /// |
661 | | /// # Arguments |
662 | | /// |
663 | | /// * `pp_size` - Number of pipeline stages |
664 | | /// * `stage` - Current stage index (0 to pp_size-1) |
665 | | /// * `total_layers` - Total number of layers to distribute |
666 | | /// * `micro_batch_size` - Size of micro-batches for pipelining |
667 | 16 | pub fn new( |
668 | 16 | pp_size: usize, |
669 | 16 | stage: usize, |
670 | 16 | total_layers: usize, |
671 | 16 | micro_batch_size: usize, |
672 | 16 | ) -> Result<Self, ParallelError> { |
673 | 16 | if pp_size == 0 { |
674 | 0 | return Err(ParallelError::InvalidWorldSize(0)); |
675 | 16 | } |
676 | 16 | if stage >= pp_size { |
677 | 0 | return Err(ParallelError::InvalidRank { |
678 | 0 | rank: stage, |
679 | 0 | world_size: pp_size, |
680 | 0 | }); |
681 | 16 | } |
682 | | |
683 | | // Distribute layers evenly across stages |
684 | 16 | let layers_per_stage = total_layers / pp_size; |
685 | 16 | let extra_layers = total_layers % pp_size; |
686 | | |
687 | | // Earlier stages get extra layers if uneven |
688 | 16 | let start_layer = stage * layers_per_stage + stage.min(extra_layers); |
689 | 16 | let num_layers = layers_per_stage + usize::from(stage < extra_layers); |
690 | 16 | let end_layer = start_layer + num_layers; |
691 | | |
692 | 16 | let stage_info = PipelineStage { |
693 | 16 | index: stage, |
694 | 16 | start_layer, |
695 | 16 | end_layer, |
696 | 16 | num_layers, |
697 | 16 | }; |
698 | | |
699 | 16 | Ok(Self { |
700 | 16 | pp_size, |
701 | 16 | stage, |
702 | 16 | stage_info, |
703 | 16 | micro_batch_size, |
704 | 16 | stats: PipelineStats::default(), |
705 | 16 | }) |
706 | 16 | } |
707 | | |
708 | | /// Get stage info |
709 | 5 | pub fn stage_info(&self) -> &PipelineStage { |
710 | 5 | &self.stage_info |
711 | 5 | } |
712 | | |
713 | | /// Get micro-batch size |
714 | 2 | pub fn micro_batch_size(&self) -> usize { |
715 | 2 | self.micro_batch_size |
716 | 2 | } |
717 | | |
718 | | /// Check if this is the first stage |
719 | 5 | pub fn is_first_stage(&self) -> bool { |
720 | 5 | self.stage == 0 |
721 | 5 | } |
722 | | |
723 | | /// Check if this is the last stage |
724 | 5 | pub fn is_last_stage(&self) -> bool { |
725 | 5 | self.stage == self.pp_size - 1 |
726 | 5 | } |
727 | | |
728 | | /// Get number of stages |
729 | 2 | pub fn num_stages(&self) -> usize { |
730 | 2 | self.pp_size |
731 | 2 | } |
732 | | |
733 | | /// Get current stage index |
734 | 1 | pub fn stage(&self) -> usize { |
735 | 1 | self.stage |
736 | 1 | } |
737 | | |
738 | | /// Calculate theoretical bubble ratio (idle time fraction) |
739 | | /// Bubble ratio = (pp_size - 1) / (pp_size + num_microbatches - 1) |
740 | 1 | pub fn bubble_ratio(&self, num_microbatches: usize) -> f32 { |
741 | 1 | if num_microbatches == 0 { |
742 | 0 | return 1.0; |
743 | 1 | } |
744 | 1 | (self.pp_size - 1) as f32 / (self.pp_size + num_microbatches - 1) as f32 |
745 | 1 | } |
746 | | |
747 | | /// Get statistics |
748 | 1 | pub fn stats(&self) -> &PipelineStats { |
749 | 1 | &self.stats |
750 | 1 | } |
751 | | |
752 | | /// Record a micro-batch processed |
753 | 2 | pub fn record_micro_batch(&mut self, stage_latency_ms: f64) { |
754 | 2 | self.stats.micro_batches_processed += 1; |
755 | 2 | self.stats.forward_passes += 1; |
756 | | |
757 | | // Update running average |
758 | 2 | let n = self.stats.micro_batches_processed as f64; |
759 | 2 | self.stats.avg_stage_latency_ms = |
760 | 2 | (self.stats.avg_stage_latency_ms * (n - 1.0) + stage_latency_ms) / n; |
761 | 2 | } |
762 | | } |
763 | | |
764 | | /// ZeRO-Inference memory offload |
765 | | /// Reference: [10] Microsoft DeepSpeed ZeRO-Inference |
766 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
767 | | #[allow(clippy::struct_excessive_bools)] // Config struct - bools are appropriate |
768 | | pub struct ZeroOffload { |
769 | | /// Offload optimizer states to CPU |
770 | | pub offload_optimizer: bool, |
771 | | /// Offload parameters to CPU |
772 | | pub offload_params: bool, |
773 | | /// Offload activations to CPU |
774 | | pub offload_activations: bool, |
775 | | /// Pin memory for faster CPU-GPU transfer |
776 | | pub pin_memory: bool, |
777 | | /// Overlap compute and communication |
778 | | pub overlap_comm: bool, |
779 | | } |
780 | | |
781 | | impl Default for ZeroOffload { |
782 | 7 | fn default() -> Self { |
783 | 7 | Self { |
784 | 7 | offload_optimizer: true, |
785 | 7 | offload_params: false, |
786 | 7 | offload_activations: false, |
787 | 7 | pin_memory: true, |
788 | 7 | overlap_comm: true, |
789 | 7 | } |
790 | 7 | } |
791 | | } |
792 | | |
793 | | impl ZeroOffload { |
794 | | /// Create inference-optimized config (offload everything) |
795 | 4 | pub fn inference() -> Self { |
796 | 4 | Self { |
797 | 4 | offload_optimizer: false, // No optimizer in inference |
798 | 4 | offload_params: true, |
799 | 4 | offload_activations: true, |
800 | 4 | pin_memory: true, |
801 | 4 | overlap_comm: true, |
802 | 4 | } |
803 | 4 | } |
804 | | |
805 | | /// Estimate memory savings ratio |
806 | 4 | pub fn memory_savings_ratio(&self) -> f32 { |
807 | 4 | let mut ratio = 1.0; |
808 | 4 | if self.offload_params { |
809 | 2 | ratio *= 0.5; // Params on CPU |
810 | 2 | } |
811 | 4 | if self.offload_activations { |
812 | 2 | ratio *= 0.7; // Activations on CPU |
813 | 2 | } |
814 | 4 | 1.0 - ratio |
815 | 4 | } |
816 | | } |
817 | | |
818 | | /// Distributed inference context combining all parallelism strategies |
819 | | #[derive(Debug)] |
820 | | pub struct DistributedContext { |
821 | | /// Parallel configuration |
822 | | config: ParallelConfig, |
823 | | /// Tensor parallelism (if enabled) |
824 | | tensor_parallel: Option<TensorParallel>, |
825 | | /// Pipeline parallelism (if enabled) |
826 | | pipeline_parallel: Option<PipelineParallel>, |
827 | | /// ZeRO offload settings |
828 | | zero_offload: ZeroOffload, |
829 | | /// Initialized flag |
830 | | initialized: bool, |
831 | | } |
832 | | |
833 | | impl DistributedContext { |
834 | | /// Create a new distributed context |
835 | 4 | pub fn new(config: ParallelConfig) -> Result<Self, ParallelError> { |
836 | 4 | let tensor_parallel = if config.tp_size > 1 { |
837 | 1 | Some(TensorParallel::new(config.tp_size, config.tp_rank())?0 ) |
838 | | } else { |
839 | 3 | None |
840 | | }; |
841 | | |
842 | | // Note: Pipeline parallel requires layer count, initialized separately |
843 | 4 | let pipeline_parallel = None; |
844 | | |
845 | 4 | Ok(Self { |
846 | 4 | config, |
847 | 4 | tensor_parallel, |
848 | 4 | pipeline_parallel, |
849 | 4 | zero_offload: ZeroOffload::default(), |
850 | 4 | initialized: true, |
851 | 4 | }) |
852 | 4 | } |
853 | | |
854 | | /// Initialize pipeline parallelism |
855 | 1 | pub fn init_pipeline( |
856 | 1 | &mut self, |
857 | 1 | total_layers: usize, |
858 | 1 | micro_batch_size: usize, |
859 | 1 | ) -> Result<(), ParallelError> { |
860 | 1 | if self.config.pp_size > 1 { |
861 | 1 | self.pipeline_parallel = Some(PipelineParallel::new( |
862 | 1 | self.config.pp_size, |
863 | 1 | self.config.pp_stage(), |
864 | 1 | total_layers, |
865 | 1 | micro_batch_size, |
866 | 0 | )?); |
867 | 0 | } |
868 | 1 | Ok(()) |
869 | 1 | } |
870 | | |
871 | | /// Set ZeRO offload configuration |
872 | 1 | pub fn set_zero_offload(&mut self, zero: ZeroOffload) { |
873 | 1 | self.zero_offload = zero; |
874 | 1 | } |
875 | | |
876 | | /// Get parallel configuration |
877 | 0 | pub fn config(&self) -> &ParallelConfig { |
878 | 0 | &self.config |
879 | 0 | } |
880 | | |
881 | | /// Get tensor parallel context |
882 | 3 | pub fn tensor_parallel(&self) -> Option<&TensorParallel> { |
883 | 3 | self.tensor_parallel.as_ref() |
884 | 3 | } |
885 | | |
886 | | /// Get pipeline parallel context |
887 | 3 | pub fn pipeline_parallel(&self) -> Option<&PipelineParallel> { |
888 | 3 | self.pipeline_parallel.as_ref() |
889 | 3 | } |
890 | | |
891 | | /// Get mutable pipeline parallel context |
892 | 0 | pub fn pipeline_parallel_mut(&mut self) -> Option<&mut PipelineParallel> { |
893 | 0 | self.pipeline_parallel.as_mut() |
894 | 0 | } |
895 | | |
896 | | /// Get ZeRO offload config |
897 | 1 | pub fn zero_offload(&self) -> &ZeroOffload { |
898 | 1 | &self.zero_offload |
899 | 1 | } |
900 | | |
901 | | /// Check if distributed execution is enabled |
902 | 2 | pub fn is_distributed(&self) -> bool { |
903 | 2 | self.config.world_size > 1 |
904 | 2 | } |
905 | | |
906 | | /// Check if initialized |
907 | 1 | pub fn is_initialized(&self) -> bool { |
908 | 1 | self.initialized |
909 | 1 | } |
910 | | } |
911 | | |
912 | | #[cfg(test)] |
913 | | mod tests { |
914 | | use super::*; |
915 | | |
916 | | // ========================================================================= |
917 | | // ParallelConfig Tests |
918 | | // ========================================================================= |
919 | | |
920 | | #[test] |
921 | 1 | fn test_parallel_config_new() { |
922 | 1 | let config = ParallelConfig::new(2, 2, 2, 0).expect("test"); |
923 | 1 | assert_eq!(config.tp_size, 2); |
924 | 1 | assert_eq!(config.pp_size, 2); |
925 | 1 | assert_eq!(config.dp_size, 2); |
926 | 1 | assert_eq!(config.world_size, 8); |
927 | 1 | assert_eq!(config.rank, 0); |
928 | 1 | } |
929 | | |
930 | | #[test] |
931 | 1 | fn test_parallel_config_single() { |
932 | 1 | let config = ParallelConfig::single(); |
933 | 1 | assert_eq!(config.tp_size, 1); |
934 | 1 | assert_eq!(config.pp_size, 1); |
935 | 1 | assert_eq!(config.dp_size, 1); |
936 | 1 | assert_eq!(config.world_size, 1); |
937 | 1 | assert_eq!(config.rank, 0); |
938 | 1 | } |
939 | | |
940 | | #[test] |
941 | 1 | fn test_parallel_config_invalid_rank() { |
942 | 1 | let result = ParallelConfig::new(2, 2, 2, 100); |
943 | 1 | assert!(result.is_err()); |
944 | 1 | } |
945 | | |
946 | | #[test] |
947 | 1 | fn test_parallel_config_invalid_world_size() { |
948 | 1 | let result = ParallelConfig::new(0, 0, 0, 0); |
949 | 1 | assert!(result.is_err()); |
950 | 1 | } |
951 | | |
952 | | #[test] |
953 | 1 | fn test_parallel_config_ranks() { |
954 | | // World size = 2 * 2 * 2 = 8 |
955 | | // Rank 5: tp_rank=1, pp_stage=0, dp_rank=1 |
956 | 1 | let config = ParallelConfig::new(2, 2, 2, 5).expect("test"); |
957 | 1 | assert_eq!(config.tp_rank(), 1); |
958 | 1 | assert_eq!(config.pp_stage(), 0); |
959 | 1 | assert_eq!(config.dp_rank(), 1); |
960 | 1 | } |
961 | | |
962 | | #[test] |
963 | 1 | fn test_parallel_config_first_last_checks() { |
964 | 1 | let config = ParallelConfig::new(2, 2, 1, 0).expect("test"); |
965 | 1 | assert!(config.is_tp_first()); |
966 | 1 | assert!(!config.is_tp_last()); |
967 | 1 | assert!(config.is_pp_first()); |
968 | 1 | assert!(!config.is_pp_last()); |
969 | 1 | } |
970 | | |
971 | | // ========================================================================= |
972 | | // ParallelTensor Tests |
973 | | // ========================================================================= |
974 | | |
975 | | #[test] |
976 | 1 | fn test_parallel_tensor_new() { |
977 | 1 | let tensor = |
978 | 1 | ParallelTensor::new(vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).expect("test"); |
979 | 1 | assert_eq!(tensor.shape, vec![2, 3]); |
980 | 1 | assert_eq!(tensor.numel(), 6); |
981 | 1 | } |
982 | | |
983 | | #[test] |
984 | 1 | fn test_parallel_tensor_zeros() { |
985 | 1 | let tensor = ParallelTensor::zeros(vec![2, 3]); |
986 | 1 | assert_eq!(tensor.sum(), 0.0); |
987 | 1 | } |
988 | | |
989 | | #[test] |
990 | 1 | fn test_parallel_tensor_narrow_rows() { |
991 | 1 | let tensor = ParallelTensor::new(vec![4, 2], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]) |
992 | 1 | .expect("test"); |
993 | 1 | let narrowed = tensor.narrow(0, 1, 2).expect("test"); |
994 | 1 | assert_eq!(narrowed.shape, vec![2, 2]); |
995 | 1 | assert_eq!(narrowed.data, vec![3.0, 4.0, 5.0, 6.0]); |
996 | 1 | } |
997 | | |
998 | | #[test] |
999 | 1 | fn test_parallel_tensor_narrow_cols() { |
1000 | 1 | let tensor = ParallelTensor::new(vec![2, 4], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]) |
1001 | 1 | .expect("test"); |
1002 | 1 | let narrowed = tensor.narrow(1, 1, 2).expect("test"); |
1003 | 1 | assert_eq!(narrowed.shape, vec![2, 2]); |
1004 | 1 | assert_eq!(narrowed.data, vec![2.0, 3.0, 6.0, 7.0]); |
1005 | 1 | } |
1006 | | |
1007 | | #[test] |
1008 | 1 | fn test_parallel_tensor_transpose() { |
1009 | 1 | let tensor = |
1010 | 1 | ParallelTensor::new(vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).expect("test"); |
1011 | 1 | let transposed = tensor.transpose().expect("test"); |
1012 | 1 | assert_eq!(transposed.shape, vec![3, 2]); |
1013 | 1 | assert_eq!(transposed.data, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]); |
1014 | 1 | } |
1015 | | |
1016 | | #[test] |
1017 | 1 | fn test_parallel_tensor_matmul() { |
1018 | | // [1, 2] @ [[1, 2], [3, 4]] = [7, 10] |
1019 | 1 | let a = ParallelTensor::new(vec![1, 2], vec![1.0, 2.0]).expect("test"); |
1020 | 1 | let b = ParallelTensor::new(vec![2, 2], vec![1.0, 2.0, 3.0, 4.0]).expect("test"); |
1021 | 1 | let c = a.matmul(&b).expect("test"); |
1022 | 1 | assert_eq!(c.shape, vec![1, 2]); |
1023 | 1 | assert_eq!(c.data, vec![7.0, 10.0]); |
1024 | 1 | } |
1025 | | |
1026 | | #[test] |
1027 | 1 | fn test_parallel_tensor_add() { |
1028 | 1 | let a = ParallelTensor::new(vec![2, 2], vec![1.0, 2.0, 3.0, 4.0]).expect("test"); |
1029 | 1 | let b = ParallelTensor::new(vec![2, 2], vec![5.0, 6.0, 7.0, 8.0]).expect("test"); |
1030 | 1 | let c = a.add(&b).expect("test"); |
1031 | 1 | assert_eq!(c.data, vec![6.0, 8.0, 10.0, 12.0]); |
1032 | 1 | } |
1033 | | |
1034 | | // ========================================================================= |
1035 | | // Communicator Tests |
1036 | | // ========================================================================= |
1037 | | |
1038 | | #[test] |
1039 | 1 | fn test_communicator_new() { |
1040 | 1 | let comm = Communicator::new(4, 2).expect("test"); |
1041 | 1 | assert_eq!(comm.world_size(), 4); |
1042 | 1 | assert_eq!(comm.rank(), 2); |
1043 | 1 | } |
1044 | | |
1045 | | #[test] |
1046 | 1 | fn test_communicator_invalid_rank() { |
1047 | 1 | let result = Communicator::new(4, 10); |
1048 | 1 | assert!(result.is_err()); |
1049 | 1 | } |
1050 | | |
1051 | | #[test] |
1052 | 1 | fn test_communicator_all_reduce_sum() { |
1053 | 1 | let comm = Communicator::new(4, 0).expect("test"); |
1054 | 1 | let tensor = ParallelTensor::new(vec![2], vec![1.0, 2.0]).expect("test"); |
1055 | 1 | let result = comm.all_reduce(&tensor, ReduceOp::Sum).expect("test"); |
1056 | | // test: multiply by world_size |
1057 | 1 | assert_eq!(result.data, vec![4.0, 8.0]); |
1058 | 1 | } |
1059 | | |
1060 | | #[test] |
1061 | 1 | fn test_communicator_all_reduce_avg() { |
1062 | 1 | let comm = Communicator::new(4, 0).expect("test"); |
1063 | 1 | let tensor = ParallelTensor::new(vec![2], vec![1.0, 2.0]).expect("test"); |
1064 | 1 | let result = comm.all_reduce(&tensor, ReduceOp::Avg).expect("test"); |
1065 | 1 | assert_eq!(result.data, vec![1.0, 2.0]); |
1066 | 1 | } |
1067 | | |
1068 | | #[test] |
1069 | 1 | fn test_communicator_all_gather() { |
1070 | 1 | let comm = Communicator::new(2, 0).expect("test"); |
1071 | 1 | let tensor = ParallelTensor::new(vec![2], vec![1.0, 2.0]).expect("test"); |
1072 | 1 | let result = comm.all_gather(&tensor).expect("test"); |
1073 | 1 | assert_eq!(result.shape, vec![4]); |
1074 | 1 | assert_eq!(result.data, vec![1.0, 2.0, 1.0, 2.0]); |
1075 | 1 | } |
1076 | | |
1077 | | #[test] |
1078 | 1 | fn test_communicator_barrier() { |
1079 | 1 | let comm = Communicator::new(4, 0).expect("test"); |
1080 | 1 | assert!(comm.barrier().is_ok()); |
1081 | 1 | } |
1082 | | |
1083 | | // ========================================================================= |
1084 | | // TensorParallel Tests |
1085 | | // ========================================================================= |
1086 | | |
1087 | | #[test] |
1088 | 1 | fn test_tensor_parallel_new() { |
1089 | 1 | let tp = TensorParallel::new(4, 2).expect("test"); |
1090 | 1 | assert_eq!(tp.tp_size(), 4); |
1091 | 1 | assert_eq!(tp.rank(), 2); |
1092 | 1 | } |
1093 | | |
1094 | | #[test] |
1095 | 1 | fn test_tensor_parallel_invalid_rank() { |
1096 | 1 | let result = TensorParallel::new(4, 10); |
1097 | 1 | assert!(result.is_err()); |
1098 | 1 | } |
1099 | | |
1100 | | #[test] |
1101 | 1 | fn test_tensor_parallel_chunk_size() { |
1102 | 1 | let tp = TensorParallel::new(4, 0).expect("test"); |
1103 | 1 | assert_eq!(tp.chunk_size(100), 25); |
1104 | 1 | assert_eq!(tp.chunk_size(16), 4); |
1105 | 1 | } |
1106 | | |
1107 | | #[test] |
1108 | 1 | fn test_tensor_parallel_column_linear() { |
1109 | 1 | let tp = TensorParallel::new(2, 0).expect("test"); |
1110 | | |
1111 | | // Input: (1, 4), Weight: (8, 4) split to (4, 4) per rank |
1112 | 1 | let input = ParallelTensor::new(vec![1, 4], vec![1.0, 1.0, 1.0, 1.0]).expect("test"); |
1113 | 1 | let weight = |
1114 | 32 | ParallelTensor::new1 (vec!1 [8, 4], (0..32)1 .map1 (|i| i as f32).collect1 ()).expect1 ("test"1 ); |
1115 | | |
1116 | 1 | let output = tp |
1117 | 1 | .column_parallel_linear(&input, &weight, None) |
1118 | 1 | .expect("test"); |
1119 | | // Output should be (1, 4) - chunk of full output |
1120 | 1 | assert_eq!(output.shape, vec![1, 4]); |
1121 | 1 | } |
1122 | | |
1123 | | #[test] |
1124 | 1 | fn test_tensor_parallel_row_linear() { |
1125 | 1 | let tp = TensorParallel::new(2, 0).expect("test"); |
1126 | | |
1127 | | // Row parallel: Weight (4, 8) split to (2, 8) per rank |
1128 | | // After transpose: (8, 2) |
1129 | | // Input needs to be (batch, 8) to matmul with (8, 2) -> output (batch, 2) |
1130 | 1 | let input = ParallelTensor::new(vec![1, 8], vec![1.0; 8]).expect("test"); |
1131 | 1 | let weight = |
1132 | 32 | ParallelTensor::new1 (vec!1 [4, 8], (0..32)1 .map1 (|i| i as f32).collect1 ()).expect1 ("test"1 ); |
1133 | | |
1134 | 1 | let output = tp.row_parallel_linear(&input, &weight, None).expect("test"); |
1135 | | // Output shape after row parallel |
1136 | 1 | assert!(!output.data.is_empty()); |
1137 | | // After all-reduce, output shape is (1, 2) |
1138 | 1 | assert_eq!(output.shape[0], 1); |
1139 | 1 | } |
1140 | | |
1141 | | // ========================================================================= |
1142 | | // PipelineParallel Tests |
1143 | | // ========================================================================= |
1144 | | |
1145 | | #[test] |
1146 | 1 | fn test_pipeline_parallel_new() { |
1147 | 1 | let pp = PipelineParallel::new(4, 1, 24, 4).expect("test"); |
1148 | 1 | assert_eq!(pp.num_stages(), 4); |
1149 | 1 | assert_eq!(pp.stage(), 1); |
1150 | 1 | assert_eq!(pp.micro_batch_size(), 4); |
1151 | 1 | } |
1152 | | |
1153 | | #[test] |
1154 | 1 | fn test_pipeline_parallel_layer_distribution() { |
1155 | | // 24 layers across 4 stages = 6 layers each |
1156 | 1 | let pp = PipelineParallel::new(4, 0, 24, 4).expect("test"); |
1157 | 1 | let info = pp.stage_info(); |
1158 | 1 | assert_eq!(info.start_layer, 0); |
1159 | 1 | assert_eq!(info.end_layer, 6); |
1160 | 1 | assert_eq!(info.num_layers, 6); |
1161 | | |
1162 | 1 | let pp2 = PipelineParallel::new(4, 3, 24, 4).expect("test"); |
1163 | 1 | let info2 = pp2.stage_info(); |
1164 | 1 | assert_eq!(info2.start_layer, 18); |
1165 | 1 | assert_eq!(info2.end_layer, 24); |
1166 | 1 | } |
1167 | | |
1168 | | #[test] |
1169 | 1 | fn test_pipeline_parallel_uneven_layers() { |
1170 | | // 25 layers across 4 stages: 7, 6, 6, 6 |
1171 | 1 | let pp = PipelineParallel::new(4, 0, 25, 4).expect("test"); |
1172 | 1 | assert_eq!(pp.stage_info().num_layers, 7); |
1173 | | |
1174 | 1 | let pp1 = PipelineParallel::new(4, 1, 25, 4).expect("test"); |
1175 | 1 | assert_eq!(pp1.stage_info().num_layers, 6); |
1176 | 1 | } |
1177 | | |
1178 | | #[test] |
1179 | 1 | fn test_pipeline_parallel_first_last() { |
1180 | 1 | let first = PipelineParallel::new(4, 0, 24, 4).expect("test"); |
1181 | 1 | assert!(first.is_first_stage()); |
1182 | 1 | assert!(!first.is_last_stage()); |
1183 | | |
1184 | 1 | let last = PipelineParallel::new(4, 3, 24, 4).expect("test"); |
1185 | 1 | assert!(!last.is_first_stage()); |
1186 | 1 | assert!(last.is_last_stage()); |
1187 | 1 | } |
1188 | | |
1189 | | #[test] |
1190 | 1 | fn test_pipeline_parallel_bubble_ratio() { |
1191 | 1 | let pp = PipelineParallel::new(4, 0, 24, 4).expect("test"); |
1192 | | // Bubble = (4-1) / (4 + 8 - 1) = 3/11 ≈ 0.27 |
1193 | 1 | let ratio = pp.bubble_ratio(8); |
1194 | 1 | assert!(ratio > 0.2 && ratio < 0.4); |
1195 | 1 | } |
1196 | | |
1197 | | #[test] |
1198 | 1 | fn test_pipeline_parallel_stats() { |
1199 | 1 | let mut pp = PipelineParallel::new(4, 0, 24, 4).expect("test"); |
1200 | 1 | pp.record_micro_batch(10.0); |
1201 | 1 | pp.record_micro_batch(12.0); |
1202 | | |
1203 | 1 | let stats = pp.stats(); |
1204 | 1 | assert_eq!(stats.micro_batches_processed, 2); |
1205 | 1 | assert_eq!(stats.forward_passes, 2); |
1206 | 1 | assert!((stats.avg_stage_latency_ms - 11.0).abs() < 0.1); |
1207 | 1 | } |
1208 | | |
1209 | | // ========================================================================= |
1210 | | // ZeroOffload Tests |
1211 | | // ========================================================================= |
1212 | | |
1213 | | #[test] |
1214 | 1 | fn test_zero_offload_default() { |
1215 | 1 | let zero = ZeroOffload::default(); |
1216 | 1 | assert!(zero.offload_optimizer); |
1217 | 1 | assert!(!zero.offload_params); |
1218 | 1 | assert!(zero.pin_memory); |
1219 | 1 | } |
1220 | | |
1221 | | #[test] |
1222 | 1 | fn test_zero_offload_inference() { |
1223 | 1 | let zero = ZeroOffload::inference(); |
1224 | 1 | assert!(!zero.offload_optimizer); |
1225 | 1 | assert!(zero.offload_params); |
1226 | 1 | assert!(zero.offload_activations); |
1227 | 1 | } |
1228 | | |
1229 | | #[test] |
1230 | 1 | fn test_zero_offload_memory_savings() { |
1231 | 1 | let zero = ZeroOffload::default(); |
1232 | 1 | let savings = zero.memory_savings_ratio(); |
1233 | 1 | assert!(savings >= 0.0 && savings <= 1.0); |
1234 | | |
1235 | 1 | let zero_inference = ZeroOffload::inference(); |
1236 | 1 | let savings_inference = zero_inference.memory_savings_ratio(); |
1237 | 1 | assert!(savings_inference > savings); |
1238 | 1 | } |
1239 | | |
1240 | | // ========================================================================= |
1241 | | // DistributedContext Tests |
1242 | | // ========================================================================= |
1243 | | |
1244 | | #[test] |
1245 | 1 | fn test_distributed_context_single() { |
1246 | 1 | let config = ParallelConfig::single(); |
1247 | 1 | let ctx = DistributedContext::new(config).expect("test"); |
1248 | | |
1249 | 1 | assert!(!ctx.is_distributed()); |
1250 | 1 | assert!(ctx.is_initialized()); |
1251 | 1 | assert!(ctx.tensor_parallel().is_none()); |
1252 | 1 | assert!(ctx.pipeline_parallel().is_none()); |
1253 | 1 | } |
1254 | | |
1255 | | #[test] |
1256 | 1 | fn test_distributed_context_with_tp() { |
1257 | 1 | let config = ParallelConfig::new(4, 1, 1, 0).expect("test"); |
1258 | 1 | let ctx = DistributedContext::new(config).expect("test"); |
1259 | | |
1260 | 1 | assert!(ctx.is_distributed()); |
1261 | 1 | assert!(ctx.tensor_parallel().is_some()); |
1262 | 1 | assert_eq!(ctx.tensor_parallel().expect("test").tp_size(), 4); |
1263 | 1 | } |
1264 | | |
1265 | | #[test] |
1266 | 1 | fn test_distributed_context_init_pipeline() { |
1267 | 1 | let config = ParallelConfig::new(1, 4, 1, 0).expect("test"); |
1268 | 1 | let mut ctx = DistributedContext::new(config).expect("test"); |
1269 | | |
1270 | 1 | ctx.init_pipeline(24, 4).expect("test"); |
1271 | 1 | assert!(ctx.pipeline_parallel().is_some()); |
1272 | 1 | assert_eq!(ctx.pipeline_parallel().expect("test").num_stages(), 4); |
1273 | 1 | } |
1274 | | |
1275 | | #[test] |
1276 | 1 | fn test_distributed_context_zero_offload() { |
1277 | 1 | let config = ParallelConfig::single(); |
1278 | 1 | let mut ctx = DistributedContext::new(config).expect("test"); |
1279 | | |
1280 | 1 | ctx.set_zero_offload(ZeroOffload::inference()); |
1281 | 1 | assert!(ctx.zero_offload().offload_params); |
1282 | 1 | } |
1283 | | |
1284 | | // ========================================================================= |
1285 | | // ReduceOp Tests |
1286 | | // ========================================================================= |
1287 | | |
1288 | | #[test] |
1289 | 1 | fn test_reduce_op_serialization() { |
1290 | 1 | let op = ReduceOp::Sum; |
1291 | 1 | let json = serde_json::to_string(&op).expect("test"); |
1292 | 1 | let deserialized: ReduceOp = serde_json::from_str(&json).expect("test"); |
1293 | 1 | assert_eq!(op, deserialized); |
1294 | 1 | } |
1295 | | |
1296 | | // ========================================================================= |
1297 | | // Error Tests |
1298 | | // ========================================================================= |
1299 | | |
1300 | | #[test] |
1301 | 1 | fn test_parallel_error_display() { |
1302 | 1 | let err = ParallelError::InvalidRank { |
1303 | 1 | rank: 10, |
1304 | 1 | world_size: 4, |
1305 | 1 | }; |
1306 | 1 | assert!(err.to_string().contains("10")); |
1307 | 1 | assert!(err.to_string().contains("4")); |
1308 | | |
1309 | 1 | let err2 = ParallelError::CommunicationError("timeout".to_string()); |
1310 | 1 | assert!(err2.to_string().contains("timeout")); |
1311 | 1 | } |
1312 | | |
1313 | | // ========================================================================= |
1314 | | // Extended Coverage Tests: ParallelConfig |
1315 | | // ========================================================================= |
1316 | | |
1317 | | #[test] |
1318 | 1 | fn test_parallel_config_world_size_calculation_ext_cov() { |
1319 | 1 | let config = ParallelConfig::new(2, 2, 2, 0).expect("test"); |
1320 | 1 | assert_eq!(config.world_size, 8); |
1321 | 1 | } |
1322 | | |
1323 | | #[test] |
1324 | 1 | fn test_parallel_config_single_debug_ext_cov() { |
1325 | 1 | let config = ParallelConfig::single(); |
1326 | 1 | let debug_str = format!("{:?}", config); |
1327 | 1 | assert!(debug_str.contains("tp_size")); |
1328 | 1 | assert!(debug_str.contains("pp_size")); |
1329 | 1 | } |
1330 | | |
1331 | | #[test] |
1332 | 1 | fn test_parallel_config_invalid_zero_tp_ext_cov() { |
1333 | 1 | let result = ParallelConfig::new(0, 1, 1, 0); |
1334 | 1 | assert!(result.is_err()); |
1335 | 1 | } |
1336 | | |
1337 | | #[test] |
1338 | 1 | fn test_parallel_config_invalid_zero_pp_ext_cov() { |
1339 | 1 | let result = ParallelConfig::new(1, 0, 1, 0); |
1340 | 1 | assert!(result.is_err()); |
1341 | 1 | } |
1342 | | |
1343 | | #[test] |
1344 | 1 | fn test_parallel_config_invalid_zero_dp_ext_cov() { |
1345 | 1 | let result = ParallelConfig::new(1, 1, 0, 0); |
1346 | 1 | assert!(result.is_err()); |
1347 | 1 | } |
1348 | | |
1349 | | // ========================================================================= |
1350 | | // Extended Coverage Tests: ReduceOp |
1351 | | // ========================================================================= |
1352 | | |
1353 | | #[test] |
1354 | 1 | fn test_reduce_op_all_variants_ext_cov() { |
1355 | 1 | let ops = [ReduceOp::Sum, ReduceOp::Min, ReduceOp::Max, ReduceOp::Avg]; |
1356 | 5 | for op4 in ops { |
1357 | 4 | let json = serde_json::to_string(&op).expect("serialize"); |
1358 | 4 | let _: ReduceOp = serde_json::from_str(&json).expect("deserialize"); |
1359 | 4 | } |
1360 | 1 | } |
1361 | | |
1362 | | #[test] |
1363 | 1 | fn test_reduce_op_clone_ext_cov() { |
1364 | 1 | let op = ReduceOp::Max; |
1365 | 1 | let cloned = op; |
1366 | 1 | assert_eq!(op, cloned); |
1367 | 1 | } |
1368 | | |
1369 | | #[test] |
1370 | 1 | fn test_reduce_op_debug_ext_cov() { |
1371 | 1 | let op = ReduceOp::Avg; |
1372 | 1 | let debug_str = format!("{:?}", op); |
1373 | 1 | assert!(debug_str.contains("Avg")); |
1374 | 1 | } |
1375 | | |
1376 | | // ========================================================================= |
1377 | | // Extended Coverage Tests: ParallelError |
1378 | | // ========================================================================= |
1379 | | |
1380 | | #[test] |
1381 | 1 | fn test_parallel_error_all_variants_ext_cov() { |
1382 | 1 | let errors: [ParallelError; 5] = [ |
1383 | 1 | ParallelError::InvalidRank { |
1384 | 1 | rank: 5, |
1385 | 1 | world_size: 4, |
1386 | 1 | }, |
1387 | 1 | ParallelError::InvalidWorldSize(0), |
1388 | 1 | ParallelError::CommunicationError("timeout".to_string()), |
1389 | 1 | ParallelError::ShapeMismatch { |
1390 | 1 | expected: vec![2, 3], |
1391 | 1 | got: vec![3, 2], |
1392 | 1 | }, |
1393 | 1 | ParallelError::PipelineError("stage error".to_string()), |
1394 | 1 | ]; |
1395 | 6 | for err5 in errors { |
1396 | 5 | let _ = err.to_string(); |
1397 | 5 | } |
1398 | 1 | } |
1399 | | |
1400 | | #[test] |
1401 | 1 | fn test_parallel_error_shape_mismatch_ext_cov() { |
1402 | 1 | let err = ParallelError::ShapeMismatch { |
1403 | 1 | expected: vec![10, 20], |
1404 | 1 | got: vec![20, 10], |
1405 | 1 | }; |
1406 | 1 | let msg = err.to_string(); |
1407 | 1 | assert!(msg.contains("10") || msg.contains("20")0 ); |
1408 | 1 | } |
1409 | | |
1410 | | #[test] |
1411 | 1 | fn test_parallel_error_debug_ext_cov() { |
1412 | 1 | let err = ParallelError::NotInitialized; |
1413 | 1 | let debug_str = format!("{:?}", err); |
1414 | 1 | assert!(debug_str.contains("NotInitialized")); |
1415 | 1 | } |
1416 | | |
1417 | | // ========================================================================= |
1418 | | // Extended Coverage Tests: ZeroOffload |
1419 | | // ========================================================================= |
1420 | | |
1421 | | #[test] |
1422 | 1 | fn test_zero_offload_clone_ext_cov() { |
1423 | 1 | let zero = ZeroOffload::inference(); |
1424 | 1 | let cloned = zero.clone(); |
1425 | 1 | assert_eq!(zero.offload_params, cloned.offload_params); |
1426 | 1 | assert_eq!(zero.offload_activations, cloned.offload_activations); |
1427 | 1 | } |
1428 | | |
1429 | | #[test] |
1430 | 1 | fn test_zero_offload_debug_ext_cov() { |
1431 | 1 | let zero = ZeroOffload::default(); |
1432 | 1 | let debug_str = format!("{:?}", zero); |
1433 | 1 | assert!(debug_str.contains("offload_optimizer")); |
1434 | 1 | assert!(debug_str.contains("pin_memory")); |
1435 | 1 | } |
1436 | | |
1437 | | #[test] |
1438 | 1 | fn test_zero_offload_memory_savings_extremes_ext_cov() { |
1439 | | // Test with all offload options enabled |
1440 | 1 | let full_offload = ZeroOffload { |
1441 | 1 | offload_optimizer: true, |
1442 | 1 | offload_params: true, |
1443 | 1 | offload_activations: true, |
1444 | 1 | pin_memory: true, |
1445 | 1 | overlap_comm: true, |
1446 | 1 | }; |
1447 | 1 | let savings = full_offload.memory_savings_ratio(); |
1448 | 1 | assert!(savings >= 0.0); |
1449 | 1 | assert!(savings <= 1.0); |
1450 | | |
1451 | | // Test with no offload |
1452 | 1 | let no_offload = ZeroOffload { |
1453 | 1 | offload_optimizer: false, |
1454 | 1 | offload_params: false, |
1455 | 1 | offload_activations: false, |
1456 | 1 | pin_memory: false, |
1457 | 1 | overlap_comm: false, |
1458 | 1 | }; |
1459 | 1 | let no_savings = no_offload.memory_savings_ratio(); |
1460 | 1 | assert!(no_savings >= 0.0); |
1461 | 1 | assert!(no_savings < savings); |
1462 | 1 | } |
1463 | | |
1464 | | // ========================================================================= |
1465 | | // Extended Coverage Tests: PipelineStats |
1466 | | // ========================================================================= |
1467 | | |
1468 | | #[test] |
1469 | 1 | fn test_pipeline_stats_clone_debug_ext_cov() { |
1470 | 1 | let stats = PipelineStats { |
1471 | 1 | micro_batches_processed: 100, |
1472 | 1 | forward_passes: 100, |
1473 | 1 | bubble_time_ms: 5.0, |
1474 | 1 | avg_stage_latency_ms: 10.5, |
1475 | 1 | }; |
1476 | 1 | let cloned = stats.clone(); |
1477 | 1 | assert_eq!( |
1478 | | stats.micro_batches_processed, |
1479 | | cloned.micro_batches_processed |
1480 | | ); |
1481 | | |
1482 | 1 | let debug_str = format!("{:?}", stats); |
1483 | 1 | assert!(debug_str.contains("micro_batches_processed")); |
1484 | 1 | assert!(debug_str.contains("bubble_time_ms")); |
1485 | 1 | } |
1486 | | |
1487 | | // ========================================================================= |
1488 | | // Extended Coverage Tests: TensorParallel |
1489 | | // ========================================================================= |
1490 | | |
1491 | | #[test] |
1492 | 1 | fn test_tensor_parallel_chunk_size_ext_cov() { |
1493 | 1 | let tp = TensorParallel::new(4, 0).expect("test"); |
1494 | 1 | let chunk = tp.chunk_size(1000); |
1495 | 1 | assert_eq!(chunk, 250); // 1000 / 4 = 250 |
1496 | 1 | } |
1497 | | |
1498 | | #[test] |
1499 | 1 | fn test_tensor_parallel_debug_ext_cov() { |
1500 | 1 | let tp = TensorParallel::new(8, 2).expect("test"); |
1501 | 1 | let debug_str = format!("{:?}", tp); |
1502 | 1 | assert!(debug_str.contains("tp_size")); |
1503 | 1 | assert!(debug_str.contains("rank")); |
1504 | 1 | } |
1505 | | |
1506 | | #[test] |
1507 | 1 | fn test_tensor_parallel_invalid_rank_ext_cov() { |
1508 | 1 | let result = TensorParallel::new(4, 10); |
1509 | 1 | assert!(result.is_err()); |
1510 | 1 | } |
1511 | | |
1512 | | #[test] |
1513 | 1 | fn test_tensor_parallel_invalid_size_ext_cov() { |
1514 | 1 | let result = TensorParallel::new(0, 0); |
1515 | 1 | assert!(result.is_err()); |
1516 | 1 | } |
1517 | | |
1518 | | // ========================================================================= |
1519 | | // Extended Coverage Tests: Communicator |
1520 | | // ========================================================================= |
1521 | | |
1522 | | #[test] |
1523 | 1 | fn test_communicator_debug_ext_cov() { |
1524 | 1 | let comm = Communicator::new(4, 0).expect("test"); |
1525 | 1 | let debug_str = format!("{:?}", comm); |
1526 | 1 | assert!(debug_str.contains("world_size")); |
1527 | 1 | assert!(debug_str.contains("rank")); |
1528 | 1 | } |
1529 | | |
1530 | | #[test] |
1531 | 1 | fn test_communicator_invalid_rank_ext_cov() { |
1532 | 1 | let result = Communicator::new(4, 10); |
1533 | 1 | assert!(result.is_err()); |
1534 | 1 | } |
1535 | | |
1536 | | // ========================================================================= |
1537 | | // Extended Coverage Tests: PipelineParallel |
1538 | | // ========================================================================= |
1539 | | |
1540 | | #[test] |
1541 | 1 | fn test_pipeline_parallel_stage_info_ext_cov() { |
1542 | | // PipelineParallel::new(pp_size, stage, total_layers, micro_batch_size) |
1543 | 1 | let pp = PipelineParallel::new(4, 0, 24, 4).expect("test"); |
1544 | 1 | let info = pp.stage_info(); |
1545 | 1 | assert_eq!(info.start_layer, 0); |
1546 | 1 | assert_eq!(info.num_layers, 6); // 24 / 4 = 6 layers per stage |
1547 | 1 | } |
1548 | | |
1549 | | #[test] |
1550 | 1 | fn test_pipeline_parallel_debug_ext_cov() { |
1551 | 1 | let pp = PipelineParallel::new(4, 0, 24, 4).expect("test"); |
1552 | 1 | let debug_str = format!("{:?}", pp); |
1553 | 1 | assert!(debug_str.contains("pp_size")); |
1554 | 1 | assert!(debug_str.contains("stage")); |
1555 | 1 | } |
1556 | | |
1557 | | #[test] |
1558 | 1 | fn test_pipeline_parallel_micro_batch_size_ext_cov() { |
1559 | 1 | let pp = PipelineParallel::new(4, 0, 24, 8).expect("test"); |
1560 | 1 | assert_eq!(pp.micro_batch_size(), 8); |
1561 | 1 | } |
1562 | | |
1563 | | #[test] |
1564 | 1 | fn test_pipeline_parallel_first_last_stage_ext_cov() { |
1565 | 1 | let first = PipelineParallel::new(4, 0, 24, 4).expect("test"); |
1566 | 1 | let last = PipelineParallel::new(4, 3, 24, 4).expect("test"); |
1567 | 1 | let middle = PipelineParallel::new(4, 1, 24, 4).expect("test"); |
1568 | | |
1569 | 1 | assert!(first.is_first_stage()); |
1570 | 1 | assert!(!first.is_last_stage()); |
1571 | | |
1572 | 1 | assert!(!last.is_first_stage()); |
1573 | 1 | assert!(last.is_last_stage()); |
1574 | | |
1575 | 1 | assert!(!middle.is_first_stage()); |
1576 | 1 | assert!(!middle.is_last_stage()); |
1577 | 1 | } |
1578 | | } |