Skip to content

Commit 292e496

Browse files
hlyliAdiactive
authored andcommitted
Add npu-specific changes to support mbridge save
1 parent 118ea74 commit 292e496

2 files changed

Lines changed: 109 additions & 21 deletions

File tree

areal/engine/megatron_engine.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def _ms_sanitized_get_full_args():
9898
convert_to_hf,
9999
get_named_parameters,
100100
remove_padding,
101+
patch_torch_all_gather_object,
101102
)
102103
from areal.engine.megatron_utils.megatron_lora import get_vllm_lora_target_modules
103104
from areal.engine.megatron_utils.packed_context_parallel import (
@@ -1913,26 +1914,27 @@ def _save_model_to_hf(
19131914
source_path=base_model_path,
19141915
)
19151916
else:
1916-
if self.mcore_config.use_mbridge_save:
1917-
# when loading model using AreaL's fast hf load, the safetensor_io is never set
1918-
if (
1919-
not hasattr(self.bridge, "safetensor_io")
1920-
or self.bridge.safetensor_io is None
1921-
):
1922-
self.bridge.safetensor_io = self.bridge._get_safetensor_io(
1923-
self.config.path
1917+
with patch_torch_all_gather_object():
1918+
if self.mcore_config.use_mbridge_save:
1919+
# when loading model using AreaL's fast hf load, the safetensor_io is never set
1920+
if (
1921+
not hasattr(self.bridge, "safetensor_io")
1922+
or self.bridge.safetensor_io is None
1923+
):
1924+
self.bridge.safetensor_io = self.bridge._get_safetensor_io(
1925+
self.config.path
1926+
)
1927+
self.bridge.save_weights(models=self.model, weights_path=path)
1928+
else:
1929+
save_weights_to_hf_with_mbridge_fast(
1930+
bridge=self.bridge,
1931+
models=self.model,
1932+
weights_path=path,
1933+
base_model_path=base_model_path,
1934+
max_shard_size_byte=int(3e9),
1935+
max_workers=None,
1936+
fp8_direct_convert=self.fp8_direct_convert,
19241937
)
1925-
self.bridge.save_weights(models=self.model, weights_path=path)
1926-
else:
1927-
save_weights_to_hf_with_mbridge_fast(
1928-
bridge=self.bridge,
1929-
models=self.model,
1930-
weights_path=path,
1931-
base_model_path=base_model_path,
1932-
max_shard_size_byte=int(3e9),
1933-
max_workers=None,
1934-
fp8_direct_convert=self.fp8_direct_convert,
1935-
)
19361938

19371939
if self.config.is_critic:
19381940
save_critic_value_head(self.model, path)

areal/engine/megatron_utils/megatron.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# SPDX-License-Identifier: Apache-2.0
2-
2+
import pickle
33
import functools
44
import inspect
55
import re
6-
6+
from typing import Any
7+
import contextlib
8+
import numpy as np
79
import torch
810
import torch.distributed as dist
911
from megatron.core import parallel_state as mpu
@@ -24,6 +26,8 @@
2426
convert_qwen3_moe_lora_to_hf,
2527
)
2628

29+
from areal.infra.platforms import current_platform
30+
2731

2832
@functools.cache
2933
def _accepts_hf_config(fn) -> bool:
@@ -1446,3 +1450,85 @@ def _iter_single(single_module):
14461450
return
14471451

14481452
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

Comments
 (0)