|
10 | 10 | from dataclasses import dataclass |
11 | 11 |
|
12 | 12 | import torch |
| 13 | +import torch.distributed._functional_collectives as funcol |
13 | 14 | from torch import distributed as dist |
14 | 15 | from torch.profiler import record_function |
15 | 16 |
|
@@ -357,239 +358,29 @@ def build_gp_context( |
357 | 358 | ) |
358 | 359 |
|
359 | 360 |
|
360 | | -class AllToAllCollect(torch.autograd.Function): |
361 | | - """ |
362 | | - Autograd function that uses all-to-all to collect only the needed |
363 | | - remote atom embeddings, replacing the all-gather approach. |
364 | | -
|
365 | | - Forward: Sends local atom embeddings to ranks that need them, |
366 | | - receives remote atom embeddings that we need. Returns only the |
367 | | - received remote embeddings (NOT concatenated with local). |
368 | | -
|
369 | | - Backward: Reverses the communication — sends gradient of received |
370 | | - embeddings back to their owners, receives gradient of sent |
371 | | - embeddings. |
372 | | -
|
373 | | - Optimizations over naive all-to-all: |
374 | | - - Uses ``all_to_all_single`` on NCCL to avoid Python list creation |
375 | | - from ``split()`` — communicates packed tensors directly. |
376 | | - - Returns the pre-allocated receive buffer directly instead of |
377 | | - ``torch.cat(recv_list)`` — avoids a redundant copy. |
378 | | - - Accepts precomputed ``send_splits``/``recv_splits`` to avoid |
379 | | - repeated ``.tolist()`` calls per layer. |
380 | | - """ |
381 | | - |
382 | | - @staticmethod |
383 | | - @torch.compiler.disable |
384 | | - def forward( |
385 | | - ctx, |
386 | | - x_local: torch.Tensor, |
387 | | - send_indices: torch.Tensor, |
388 | | - send_counts: torch.Tensor, |
389 | | - recv_counts: torch.Tensor, |
390 | | - gp_group: dist.ProcessGroup, |
391 | | - rank: int, |
392 | | - world_size: int, |
393 | | - precomputed_send_splits: list[int] | None = None, |
394 | | - precomputed_recv_splits: list[int] | None = None, |
395 | | - precomputed_total_recv: int | None = None, |
396 | | - ) -> torch.Tensor: |
397 | | - """ |
398 | | - Forward all-to-all embedding collection. |
399 | | -
|
400 | | - Args: |
401 | | - x_local: Local atom embeddings, |
402 | | - shape (local_atoms, *feature_dims). |
403 | | - send_indices: Local indices of atoms to send, |
404 | | - ordered by dest rank. |
405 | | - send_counts: Number of atoms to send to each rank. |
406 | | - recv_counts: Number of atoms to receive from each rank. |
407 | | - gp_group: GP process group. |
408 | | - rank: GP rank. |
409 | | - world_size: GP world size. |
410 | | - precomputed_send_splits: Optional cached |
411 | | - send_counts.tolist(). |
412 | | - precomputed_recv_splits: Optional cached |
413 | | - recv_counts.tolist(). |
414 | | - precomputed_total_recv: Optional cached |
415 | | - sum(recv_splits). |
416 | | -
|
417 | | - Returns: |
418 | | - Received remote embeddings, |
419 | | - shape (sum(recv_counts), *feature_dims). |
420 | | - """ |
421 | | - ctx.send_indices = send_indices |
422 | | - ctx.send_counts = send_counts |
423 | | - ctx.recv_counts = recv_counts |
424 | | - ctx.gp_group = gp_group |
425 | | - ctx.rank = rank |
426 | | - ctx.world_size = world_size |
427 | | - ctx.local_size = x_local.shape[0] |
428 | | - # Cache precomputed splits for backward |
429 | | - ctx.precomputed_send_splits = precomputed_send_splits |
430 | | - ctx.precomputed_recv_splits = precomputed_recv_splits |
431 | | - |
432 | | - feature_shape = x_local.shape[1:] |
433 | | - |
434 | | - # Gather atoms to send (index_select into contiguous buffer) |
435 | | - if send_indices.numel() > 0: |
436 | | - x_send = x_local[send_indices].contiguous() |
437 | | - else: |
438 | | - x_send = torch.empty( |
439 | | - 0, |
440 | | - *feature_shape, |
441 | | - device=x_local.device, |
442 | | - dtype=x_local.dtype, |
443 | | - ) |
444 | | - |
445 | | - # Use precomputed splits if available |
446 | | - send_splits = ( |
447 | | - precomputed_send_splits |
448 | | - if precomputed_send_splits is not None |
449 | | - else send_counts.tolist() |
450 | | - ) |
451 | | - recv_splits = ( |
452 | | - precomputed_recv_splits |
453 | | - if precomputed_recv_splits is not None |
454 | | - else recv_counts.tolist() |
455 | | - ) |
456 | | - total_recv = ( |
457 | | - precomputed_total_recv |
458 | | - if precomputed_total_recv is not None |
459 | | - else sum(recv_splits) |
460 | | - ) |
461 | | - x_recv = torch.empty( |
462 | | - total_recv, |
463 | | - *feature_shape, |
464 | | - device=x_local.device, |
465 | | - dtype=x_local.dtype, |
466 | | - ) |
467 | | - |
468 | | - # Perform all-to-all communication |
469 | | - backend = dist.get_backend(gp_group) |
470 | | - if backend == "nccl": |
471 | | - # Use all_to_all_single for NCCL |
472 | | - dist.all_to_all_single( |
473 | | - x_recv, |
474 | | - x_send, |
475 | | - output_split_sizes=recv_splits, |
476 | | - input_split_sizes=send_splits, |
477 | | - group=gp_group, |
478 | | - ) |
479 | | - else: |
480 | | - # Gloo fallback: use list-based pairwise send/recv |
481 | | - send_list = list(x_send.split(send_splits)) |
482 | | - recv_list = list(x_recv.split(recv_splits)) |
483 | | - _safe_all_to_all(recv_list, send_list, group=gp_group) |
484 | | - |
485 | | - # x_recv already contains all received data in rank order |
486 | | - return x_recv |
487 | | - |
488 | | - @staticmethod |
489 | | - @torch.compiler.disable |
490 | | - def backward(ctx, grad_received: torch.Tensor): |
491 | | - """ |
492 | | - Reverse the all-to-all: send gradients back to the ranks that |
493 | | - originally sent us the embeddings. |
494 | | - """ |
495 | | - send_counts = ctx.send_counts |
496 | | - recv_counts = ctx.recv_counts |
497 | | - send_indices = ctx.send_indices |
498 | | - gp_group = ctx.gp_group |
499 | | - local_size = ctx.local_size |
500 | | - |
501 | | - feature_shape = grad_received.shape[1:] |
502 | | - |
503 | | - # In backward, the roles are reversed |
504 | | - bwd_send_splits = ( |
505 | | - ctx.precomputed_recv_splits |
506 | | - if ctx.precomputed_recv_splits is not None |
507 | | - else recv_counts.tolist() |
508 | | - ) |
509 | | - bwd_recv_splits = ( |
510 | | - ctx.precomputed_send_splits |
511 | | - if ctx.precomputed_send_splits is not None |
512 | | - else send_counts.tolist() |
513 | | - ) |
514 | | - |
515 | | - total_bwd_recv = sum(bwd_recv_splits) |
516 | | - grad_send_back = torch.empty( |
517 | | - total_bwd_recv, |
518 | | - *feature_shape, |
519 | | - device=grad_received.device, |
520 | | - dtype=grad_received.dtype, |
521 | | - ) |
522 | | - |
523 | | - # Reverse all-to-all |
524 | | - backend = dist.get_backend(gp_group) |
525 | | - if backend == "nccl": |
526 | | - dist.all_to_all_single( |
527 | | - grad_send_back, |
528 | | - grad_received.contiguous(), |
529 | | - output_split_sizes=bwd_recv_splits, |
530 | | - input_split_sizes=bwd_send_splits, |
531 | | - group=gp_group, |
532 | | - ) |
533 | | - else: |
534 | | - # Gloo fallback |
535 | | - bwd_send_list = list(grad_received.split(bwd_send_splits)) |
536 | | - bwd_recv_list = list(grad_send_back.split(bwd_recv_splits)) |
537 | | - _safe_all_to_all(bwd_recv_list, bwd_send_list, group=gp_group) |
538 | | - |
539 | | - # Scatter received gradients back to local positions |
540 | | - grad_local = torch.zeros( |
541 | | - local_size, |
542 | | - *feature_shape, |
543 | | - device=grad_received.device, |
544 | | - dtype=grad_received.dtype, |
545 | | - ) |
546 | | - |
547 | | - if total_bwd_recv > 0: |
548 | | - grad_local.index_add_(0, send_indices, grad_send_back) |
549 | | - |
550 | | - # Return gradients for x_local only; None for all other inputs |
551 | | - return ( |
552 | | - grad_local, |
553 | | - None, |
554 | | - None, |
555 | | - None, |
556 | | - None, |
557 | | - None, |
558 | | - None, |
559 | | - None, |
560 | | - None, |
561 | | - None, |
562 | | - ) |
563 | | - |
564 | | - |
565 | 361 | def all_to_all_collect( |
566 | 362 | x_local: torch.Tensor, |
567 | 363 | gp_ctx: GPContext, |
568 | 364 | ) -> torch.Tensor: |
569 | 365 | """ |
570 | | - High-level function to collect remote embeddings via all-to-all. |
| 366 | + Collect the remote atom embeddings this rank's edges need. |
571 | 367 |
|
572 | | - Returns the received remote embeddings (NOT including local). |
573 | | - The caller should concatenate [x_local, received] and use |
574 | | - gp_ctx.global_to_local to index into this combined tensor. |
| 368 | + Returns only the received embeddings; the caller concatenates them with |
| 369 | + the local ones to form the tensor ``edge_index_local`` indexes into. |
| 370 | + Autograd supplies the reverse exchange and the scatter-add back into the |
| 371 | + local embeddings. |
575 | 372 |
|
576 | 373 | Args: |
577 | 374 | x_local: Local atom embeddings, shape (local_atoms, *features). |
578 | 375 | gp_ctx: Graph parallel context. |
579 | 376 |
|
580 | 377 | Returns: |
581 | | - x_received: Remote atom embeddings, |
582 | | - shape (total_needed, *features). |
| 378 | + Remote atom embeddings, shape (total_recv, *features). |
583 | 379 | """ |
584 | | - return AllToAllCollect.apply( |
585 | | - x_local, |
586 | | - gp_ctx.send_indices, |
587 | | - gp_ctx.send_counts, |
588 | | - gp_ctx.recv_counts, |
589 | | - gp_utils.get_gp_group(), |
590 | | - gp_ctx.rank, |
591 | | - gp_ctx.world_size, |
592 | | - gp_ctx.send_splits, |
| 380 | + x_send = x_local[gp_ctx.send_indices] |
| 381 | + return funcol.all_to_all_single_autograd( |
| 382 | + x_send, |
593 | 383 | gp_ctx.recv_splits, |
594 | | - gp_ctx.total_recv, |
| 384 | + gp_ctx.send_splits, |
| 385 | + gp_utils.get_gp_group(), |
595 | 386 | ) |
0 commit comments