|
1 | 1 | # SPDX-License-Identifier: Apache-2.0 |
2 | | - |
| 2 | +import pickle |
3 | 3 | import functools |
4 | 4 | import inspect |
5 | 5 | import re |
6 | | - |
| 6 | +from typing import Any |
| 7 | +import contextlib |
| 8 | +import numpy as np |
7 | 9 | import torch |
8 | 10 | import torch.distributed as dist |
9 | 11 | from megatron.core import parallel_state as mpu |
|
24 | 26 | convert_qwen3_moe_lora_to_hf, |
25 | 27 | ) |
26 | 28 |
|
| 29 | +from areal.infra.platforms import current_platform |
| 30 | + |
27 | 31 |
|
28 | 32 | @functools.cache |
29 | 33 | def _accepts_hf_config(fn) -> bool: |
@@ -1446,3 +1450,85 @@ def _iter_single(single_module): |
1446 | 1450 | return |
1447 | 1451 |
|
1448 | 1452 | yield from _iter_single(model_module) |
| 1453 | + |
| 1454 | + |
| 1455 | +def all_gather_object_tensor( |
| 1456 | + object_list: list[Any], |
| 1457 | + obj: Any, |
| 1458 | + group: Any | None = None, |
| 1459 | +) -> None: |
| 1460 | + world_size = dist.get_world_size(group=group) |
| 1461 | + |
| 1462 | + if len(object_list) != world_size: |
| 1463 | + raise ValueError( |
| 1464 | + f"object_list must have length {world_size}, got {len(object_list)}" |
| 1465 | + ) |
| 1466 | + |
| 1467 | + device = current_platform.current_device() |
| 1468 | + |
| 1469 | + # Serialize object. |
| 1470 | + payload = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL) |
| 1471 | + local_n = len(payload) |
| 1472 | + |
| 1473 | + # Gather byte lengths. |
| 1474 | + local_size = torch.tensor([local_n], dtype=torch.int64, device=device) |
| 1475 | + sizes_tensor = torch.empty(world_size, dtype=torch.int64, device=device) |
| 1476 | + |
| 1477 | + dist.all_gather_into_tensor( |
| 1478 | + sizes_tensor, |
| 1479 | + local_size, |
| 1480 | + group=group, |
| 1481 | + ) |
| 1482 | + |
| 1483 | + sizes = [int(x) for x in sizes_tensor.cpu().tolist()] |
| 1484 | + max_size = max(sizes) |
| 1485 | + |
| 1486 | + # Pad on CPU first. |
| 1487 | + payload_u8 = np.frombuffer(payload, dtype=np.uint8) |
| 1488 | + padded_u8 = np.zeros(max_size, dtype=np.uint8) |
| 1489 | + padded_u8[:local_n] = payload_u8 |
| 1490 | + |
| 1491 | + # Use int32 for NPU collective compatibility. |
| 1492 | + # Values are still 0..255, but dtype is int32. |
| 1493 | + send_np = padded_u8.astype(np.int32) |
| 1494 | + send_buf = torch.from_numpy(send_np).to(device) |
| 1495 | + |
| 1496 | + gathered = torch.empty( |
| 1497 | + world_size * max_size, |
| 1498 | + dtype=torch.int32, |
| 1499 | + device=device, |
| 1500 | + ) |
| 1501 | + |
| 1502 | + dist.all_gather_into_tensor( |
| 1503 | + gathered, |
| 1504 | + send_buf.contiguous(), |
| 1505 | + group=group, |
| 1506 | + ) |
| 1507 | + |
| 1508 | + # Synchronize once after collective. |
| 1509 | + gathered_cpu = gathered.view(world_size, max_size).cpu().numpy() |
| 1510 | + |
| 1511 | + for i, size in enumerate(sizes): |
| 1512 | + data_u8 = gathered_cpu[i, :size].astype(np.uint8, copy=False) |
| 1513 | + object_list[i] = pickle.loads(data_u8.tobytes()) |
| 1514 | + |
| 1515 | + |
| 1516 | +# context manager for patching all_gather_object |
| 1517 | +# hccl has issues with all_gather_object sometimes where it can run out of input reading the incoming stream |
| 1518 | +# this is a context manager that replaces torch's all_gather_object with a variant that |
| 1519 | +# pickles cpu objects and turns them into npu tensors before doing a regular all_gather |
| 1520 | +# usage: |
| 1521 | +# with patch_torch_all_gather_object(): |
| 1522 | +# # code that runs all_gather_object |
| 1523 | +@contextlib.contextmanager |
| 1524 | +def patch_torch_all_gather_object(): |
| 1525 | + old_fn = dist.all_gather_object |
| 1526 | + |
| 1527 | + def patched_all_gather_object(object_list, obj, group=None): |
| 1528 | + return all_gather_object_tensor(object_list, obj, group=group) |
| 1529 | + |
| 1530 | + dist.all_gather_object = patched_all_gather_object |
| 1531 | + try: |
| 1532 | + yield |
| 1533 | + finally: |
| 1534 | + dist.all_gather_object = old_fn |
0 commit comments