Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
241261a
refactor(lora): adapter tensors ride the base weight stream
yueming-yuan Aug 25, 2026
b2d8443
remove accidentally committed .rxignore (local devbox sync config)
yueming-yuan Aug 25, 2026
5792835
cascade .rxignore removal
yueming-yuan Aug 25, 2026
10efec6
cascade from yueming/hf-weight-iterator-move
yueming-yuan Aug 25, 2026
2c39805
cascade from yueming/hf-weight-iterator-move
yueming-yuan Aug 25, 2026
6344d53
cascade from yueming/hf-weight-iterator-colocate
yueming-yuan Aug 25, 2026
44359bd
rename the updater's lora helpers for clarity
yueming-yuan Aug 25, 2026
a40d8fa
cascade from yueming/hf-weight-iterator-colocate
yueming-yuan Aug 25, 2026
81561f0
cascade from yueming/hf-weight-iterator-dist
yueming-yuan Aug 25, 2026
b5572be
move record_lora_checksums into weight_update utils
yueming-yuan Aug 26, 2026
70482b4
sort imports
yueming-yuan Aug 26, 2026
af10bae
cascade from yueming/hf-weight-iterator-colocate
yueming-yuan Aug 26, 2026
fbe555b
rename uses_session_frame in the test mock
yueming-yuan Aug 26, 2026
d68b687
cascade from yueming/hf-weight-iterator-colocate
yueming-yuan Aug 26, 2026
b324a5f
drop comments that restate the code
yueming-yuan Aug 26, 2026
60ed310
cascade from yueming/hf-weight-iterator-move
yueming-yuan Aug 26, 2026
94133c9
cascade from yueming/hf-weight-iterator-move
yueming-yuan Aug 26, 2026
7002f78
cascade from yueming/hf-weight-iterator-bucketing
yueming-yuan Aug 26, 2026
b5dd6a1
cascade from yueming/hf-weight-iterator-move
yueming-yuan Aug 26, 2026
110bb97
cascade from yueming/hf-weight-iterator-bucketing
yueming-yuan Aug 26, 2026
f07587e
cascade from yueming/hf-weight-iterator-dist
yueming-yuan Aug 26, 2026
ac366a8
cascade from yueming/hf-weight-iterator-colocate
yueming-yuan Aug 26, 2026
6313fad
format
yueming-yuan Aug 26, 2026
3ae2a77
cascade from yueming/hf-weight-iterator-updater
yueming-yuan Aug 26, 2026
89e8dfe
cascade from yueming/hf-weight-iterator-colocate
yueming-yuan Aug 26, 2026
bc48803
reject hybrid colocated+distributed deployments for LoRA weight sync
yueming-yuan Aug 26, 2026
efc881b
cascade from yueming/hf-weight-iterator-refactor
yueming-yuan Aug 26, 2026
4d6d1e8
register adapters unpinned (pinning every slot trips the pool anti-st…
yueming-yuan Aug 26, 2026
e31818f
Trigger CI: engine rebuild with deferred-free aliasing fix
yueming-yuan Aug 26, 2026
f5881e5
cascade from yueming/hf-weight-iterator-updater
yueming-yuan Aug 27, 2026
4dd7c21
cascade from yueming/hf-weight-iterator-dist
yueming-yuan Aug 28, 2026
443ec12
cascade from yueming/hf-weight-iterator-dist
yueming-yuan Aug 28, 2026
cd9d89e
cascade from yueming/hf-weight-iterator-ep-allgather
yueming-yuan Sep 1, 2026
84c632c
cascade from yueming/hf-weight-iterator-colocate
yueming-yuan Sep 1, 2026
e0d5de7
cascade from yueming/hf-weight-iterator-updater
yueming-yuan Sep 1, 2026
1dbc39a
cascade from yueming/hf-weight-iterator-updater
yueming-yuan Sep 2, 2026
be0ba37
publish weight_version only after end_weight_update commits
yueming-yuan Sep 2, 2026
220ba84
register LoRA adapters inside the paused window
yueming-yuan Sep 2, 2026
83a5d7b
drop the comment on the version publish order
yueming-yuan Sep 2, 2026
218cc1c
drop the comments on adapter registration
yueming-yuan Sep 2, 2026
4a89f7d
mark the PP-local adapter branch as unreached behind the bridge guard
yueming-yuan Sep 2, 2026
5fa4f9a
merge origin/yueming/hf-weight-iterator-move
yueming-yuan Sep 2, 2026
6f6ec7c
cascade from yueming/hf-weight-iterator-colocate
yueming-yuan Sep 2, 2026
cda97d2
test: broadcast send_bucket no longer forwards a weight_version
yueming-yuan Sep 2, 2026
d672149
cascade from yueming/hf-weight-iterator-dist
yueming-yuan Sep 2, 2026
cefe854
assert the LoRA checksum manifest is recorded from a PP-gathered adapter
yueming-yuan Sep 3, 2026
3e5a042
merge origin/yueming/hf-weight-iterator-move
yueming-yuan Sep 3, 2026
f2be133
cascade from yueming/hf-weight-iterator-move
yueming-yuan Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion miles/backends/megatron_utils/hf_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def export_hf_model_direct(
weight_map: dict[str, str] = {}
total_size = 0
shard_index = 0
for hf_named_tensors in iterator.iter_hf_base_weights(megatron_local_weights):
for hf_named_tensors in iterator.iter_hf_weights(megatron_local_weights):
if not is_writer:
continue
shard_index += 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
WeightUpdatePlacement,
resolve_placement,
)
from miles.utils.lora import is_lora_weight_name


class MegatronHfWeightIteratorBase(HfWeightIteratorBase):
Expand All @@ -24,13 +25,28 @@ class MegatronHfWeightIteratorBase(HfWeightIteratorBase):
def _hf_atomic_update_groups(self):
return get_hf_atomic_update_groups(self.model_name, q_lora_rank=self.args.q_lora_rank)

def _export_lora_named_tensors(self, adapter):
# Both megatron exporters gather TP/EP but not PP.
return _gather_pp_full_adapter(self._export_pp_local_lora(adapter))
def _iter_hf_adapter_units(self, lora_name, adapter, *, materialize):
"""Both megatron exporters are PP-local after gathering TP/EP; the PP
gather runs only where the resolved placement asks for it."""
named_tensors = self._export_pp_local_lora(adapter)
if self.placement.gather_pp:

@guapisolo guapisolo Sep 1, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

human cmt

[P2] Track native-LoRA disaggregate reachability

This placement-gated gather fixes the hardcoded is_lora_sender = self.is_sender and shard == 0 behavior discussed in #2747: with gather_pp=False, adapter tensors can now remain PP-local and ride each shard's normal sender stream.

Native-LoRA disaggregate is still blocked by the actor-side megatron_to_hf_mode == "bridge" assertion, so this path is not production-reachable today and no supported run is affected.

Please leave a TODO linking this branch to removal of that guard when native-LoRA disaggregate is enabled.

named_tensors = _gather_pp_full_adapter(named_tensors)
if not materialize:
return
if not named_tensors:
raise RuntimeError(
f"LoRA weight sync failed: the adapter export produced zero tensors"
f"{f' for adapter {adapter!r}' if adapter is not None else ''}. "
"This usually means the Megatron-Bridge or SGLang version is incompatible."
)
if not any(is_lora_weight_name(name) for name, _tensor in named_tensors):
raise RuntimeError("LoRA weight sync failed: the adapter export contains no lora_A/lora_B names.")
for hf_name, tensor in named_tensors:
yield [(f"{lora_name}:{hf_name}", tensor)]

@abstractmethod
def _export_pp_local_lora(self, adapter) -> list[tuple[str, torch.Tensor]]:
"""The adapter's HF-named tensors, TP/EP gathered, PP-local."""
"""Backend hook: the adapter's HF-named tensors, TP/EP gathered, PP-local."""


def get_hf_weight_iterator(
Expand Down
22 changes: 12 additions & 10 deletions miles/backends/sglang_utils/sglang_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,11 +565,11 @@ def get_weight_version(self):
return response.json()["weight_version"]
response.raise_for_status()

def unload_lora_adapter(self, lora_name: str):
"""Unload LoRA adapter."""
def register_lora_adapter(self, lora_name: str, config_dict: dict, pinned: bool = True):
"""Create-or-refresh a LoRA adapter's identity and config (weights zeroed)."""
return self._make_request(
"unload_lora_adapter",
{"lora_name": lora_name},
"register_lora_adapter",
{"lora_name": lora_name, "config_dict": config_dict, "pinned": pinned},
)

def get_scheduler_actors(self) -> list:
Expand Down Expand Up @@ -695,13 +695,15 @@ def continue_generation(self):
response.raise_for_status()
return response

def begin_weight_update(self, selector: str = "all"):
"""Open a weight-update session on the engine (restores packed weights for loading)."""
return self._make_request("begin_weight_update", {"selector": selector})
def begin_weight_update(self, selector: str = "all", sync_base: bool = True):
"""Open a weight-update session on the engine. sync_base=False declares an
adapter-only session (no quant unpack; base tensors rejected)."""
return self._make_request("begin_weight_update", {"selector": selector, "sync_base": sync_base})

def end_weight_update(self):
"""Close the weight-update session (post-load + quant post-process on the full model)."""
return self._make_request("end_weight_update", {})
def end_weight_update(self, expected_lora_checksums=None):
"""Close the weight-update session: re-finalize base weights (sync_base
sessions) and apply the streamed LoRA stash."""
return self._make_request("end_weight_update", {"expected_lora_checksums": expected_lora_checksums})

def update_weight_version(self, weight_version: str):
return self._make_request(
Expand Down
55 changes: 1 addition & 54 deletions miles/backends/training_utils/weight_update/broadcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@
from miles.backends.training_utils.parallel import ParallelState
from miles.backends.training_utils.weight_update.hf_weight_iterator import WeightUpdatePlacement
from miles.backends.training_utils.weight_update.protocol import WeightTransferProtocol
from miles.backends.training_utils.weight_update.session import (
check_weight_sync_results,
unload_lora_adapter,
weight_update_selector,
)
from miles.backends.training_utils.weight_update.session import weight_update_selector
from miles.backends.training_utils.weight_update.transfer import derive_replica_position
from miles.utils.distributed_utils import init_process_group

Expand All @@ -33,7 +29,6 @@ def __init__(self, args: Namespace) -> None:
super().__init__(args)
self._model_update_groups = None
self._selector = weight_update_selector(args)
self._lora_loaded = False

def connect(
self,
Expand All @@ -56,7 +51,6 @@ def connect(
replica_rank, _ = derive_replica_position(parallel_state, placement)
self.is_sender = replica_rank == 0
shard = 0 if placement.gather_pp else parallel_state.pp.rank
self.is_lora_sender = self.is_sender and shard == 0
if self.is_sender:
self._group_name = f"miles-pp_{shard}"
disconnect_rollout_engines_from_distributed(
Expand Down Expand Up @@ -86,53 +80,6 @@ def send_bucket(self, bucket: list[tuple[str, torch.Tensor]], weight_version: in
# release must run after both successful and failed broadcasts.
ray.get(self.rollout_engine_lock.release.remote())

def send_adapter(
self, named_tensors: list[tuple[str, torch.Tensor]], *, lora_name: str, lora_config: dict, upsert: bool
) -> None:
"""Send adapter metadata over Ray, then broadcast the tensors (src=0).

Reuses the base broadcast group (``self._model_update_groups`` /
``self._group_name``); base and adapter syncs are strictly sequential, so
sharing the NCCL communicator is safe. No CUDA IPC, so it works across
nodes: the engine allocates buffers from the metadata and broadcast-receives
in order. ``upsert`` maps to the engine's in-place insert-or-overwrite RPC
(multi-LoRA slots); without it a stale adapter is unloaded first, since the
engine rejects a duplicate name.
"""
if not upsert and self._lora_loaded:
unload_lora_adapter(self.rollout_engines, lora_name)

names = [name for name, _ in named_tensors]
dtypes = [param.dtype for _, param in named_tensors]
shapes = [list(param.shape) for _, param in named_tensors]

refs = [
engine.load_lora_adapter_from_distributed.remote(
lora_name=lora_name,
config_dict=lora_config,
names=names,
dtypes=dtypes,
shapes=shapes,
group_name=self._group_name,
**({"upsert": True} if upsert else {}),
)
for engine in self.rollout_engines
]
# NCCL needs contiguous buffers (lora_B slices are strided); the list keeps them
# alive until the async broadcasts complete.
contiguous_tensors = [
param.data if param.data.is_contiguous() else param.data.contiguous() for _, param in named_tensors
]
handles = [
dist.broadcast(tensor, 0, group=self._model_update_groups, async_op=True) for tensor in contiguous_tensors
]
for handle in handles:
handle.wait()

check_weight_sync_results(ray.get(refs), is_lora=True)
if not upsert:
self._lora_loaded = True


def connect_rollout_engines_from_distributed(
args: Namespace,
Expand Down
121 changes: 11 additions & 110 deletions miles/backends/training_utils/weight_update/colocate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import hashlib
import logging
from argparse import Namespace
from collections.abc import Sequence
Expand Down Expand Up @@ -155,7 +154,6 @@ def connect(
# Every engine-covered rank sends: the per-engine gather_object is
# collective among the group's members.
self.is_sender = self._ipc_gather_group is not None
self.is_lora_sender = self.is_sender

# A LoRA sync must re-push the frozen base unless the engines keep it
# across pauses (CPU backup, persistent GPU copy, or remote engines);
Expand Down Expand Up @@ -190,66 +188,6 @@ def send_bucket(self, bucket: list[tuple[str, torch.Tensor]], weight_version: in
check_weight_sync_results(ray.get(refs or []), is_lora=False)
del long_lived_tensors

def send_adapter(
self, named_tensors: list[tuple[str, torch.Tensor]], *, lora_name: str, lora_config: dict, upsert: bool
) -> None:
if upsert:
raise NotImplementedError("multi-LoRA weight sync is not supported for colocated engines")
if self.use_distribute and self._is_distributed_src_rank:
raise NotImplementedError("LoRA weight sync is not yet supported for distributed (non-colocated) engines")

refs, long_lived_tensors = _send_to_colocated_engine(
hf_named_tensors=named_tensors,
ipc_engine=self._ipc_engine,
ipc_gather_src=self._ipc_gather_src,
ipc_gather_group=self._ipc_gather_group,
selector=self._selector,
lora_config=lora_config,
lora_name=lora_name,
check_equal=self.args.check_lora_weight_equal,
repack_lora_for_ipc=self.args.offload_train,
)
check_weight_sync_results(ray.get(refs or []), is_lora=True)
del long_lived_tensors
torch.cuda.ipc_collect()
torch.cuda.empty_cache()


def _repack_onto_fresh_storage(
named_tensors: list[tuple[str, torch.Tensor]],
) -> dict[str, torch.Tensor]:
"""Copy CUDA tensors into freshly allocated flat buffers and return views onto them.

``torch_memory_saver.region()`` is a ``torch.cuda.use_mem_pool`` context, so anything
allocated while building the model -- the LoRA adapter parameters included -- lives in
a MemPool the preloaded hook backs with cuMem. cuMem allocations cannot be exported
over the legacy CUDA IPC API that ``MultiprocessingSerializer`` uses, so handing their
storages straight to the engine fails with "CUDA error: invalid argument" on the first
sync. Allocating here, outside any region, gets normal caching-allocator memory whose
handles export fine. (This is also why the FlattenedTensorBucket path works: its
flattened tensor is likewise allocated at sync time.)

One buffer per (dtype, device) rather than one clone per tensor: the direct-dict
transport relies on the pickler memoizing storages so the engine receives a handful of
IPC handles instead of one per adapter tensor.
"""
groups: dict[tuple[torch.dtype, torch.device], list[tuple[str, torch.Tensor]]] = {}
for name, tensor in named_tensors:
if tensor.is_cuda:
groups.setdefault((tensor.dtype, tensor.device), []).append((name, tensor))

views: dict[str, torch.Tensor] = {}
for (dtype, device), items in groups.items():
flat = torch.empty(sum(t.numel() for _, t in items), dtype=dtype, device=device)
offset = 0
for name, tensor in items:
view = flat[offset : offset + tensor.numel()].view(tensor.shape)
view.copy_(tensor)
views[name] = view
offset += tensor.numel()

return {name: views.get(name, tensor) for name, tensor in named_tensors}


def _send_to_colocated_engine(
hf_named_tensors: list[tuple[str, torch.Tensor]],
Expand All @@ -258,27 +196,17 @@ def _send_to_colocated_engine(
ipc_gather_src,
ipc_gather_group,
weight_version=None,
lora_config: dict | None = None,
lora_name: str | None = None,
check_equal: bool = False,
selector: str = "all",
repack_lora_for_ipc: bool = False,
) -> tuple[list[ObjectRef], Any]:
# Placeholder ranks (GPU slots reserved but no engine) have no gather group.
# gather_object is only collective among group members, so we skip entirely.
if ipc_gather_group is None:
return [], None

is_lora = lora_config is not None
is_gather_src = dist.get_rank() == ipc_gather_src
long_live_tensors = []

if is_lora:
payload = _repack_onto_fresh_storage(hf_named_tensors) if repack_lora_for_ipc else dict(hf_named_tensors)
long_live_tensors.append(payload)
converted_named_tensors_by_dtypes = {}
serialized_lora = MultiprocessingSerializer.serialize(payload, output_str=True)
elif getattr(FlattenedTensorBucket, "supports_multi_dtypes", False):
if getattr(FlattenedTensorBucket, "supports_multi_dtypes", False):
converted_named_tensors_by_dtypes = {"dtype": hf_named_tensors}
else:
converted_named_tensors_by_dtypes = {}
Expand All @@ -288,7 +216,7 @@ def _send_to_colocated_engine(
converted_named_tensors_by_dtypes[dtype] = []
converted_named_tensors_by_dtypes[dtype].append((name, tensor))

serialized_tensors: list = [serialized_lora] if is_lora else []
serialized_tensors: list = []
for _dtype, named_tensors in converted_named_tensors_by_dtypes.items():
flattened_tensor_bucket = FlattenedTensorBucket(named_tensors=named_tensors)
flattened_tensor_data = {
Expand All @@ -308,41 +236,14 @@ def _send_to_colocated_engine(

refs = []
if is_gather_src:
if is_lora:
try:
ray.get(ipc_engine.unload_lora_adapter.remote(lora_name=lora_name))
except Exception as _unload_err:
logger.debug("lora unload before load skipped: %s", _unload_err)

expected_checksums = None
if check_equal:
expected_checksums = {
n: hashlib.sha256(
t.detach().cpu().contiguous().flatten().view(torch.uint8).numpy().tobytes()
).hexdigest()
for n, t in hf_named_tensors
}

refs.append(
ipc_engine.load_lora_adapter_from_tensors.remote(
lora_name=lora_name,
config_dict=lora_config,
serialized_named_tensors=[
per_rank[0] if per_rank else None for per_rank in serialized_named_tensors
],
expected_checksums=expected_checksums,
)
)

else:
num_dtypes = len(serialized_named_tensors[0])
for i in range(num_dtypes):
kwargs = {
"serialized_named_tensors": [tensors[i] for tensors in serialized_named_tensors],
"load_format": "flattened_bucket",
"weight_version": str(weight_version),
"selector": selector,
}
refs.append(ipc_engine.update_weights_from_tensor.remote(**kwargs))
num_dtypes = len(serialized_named_tensors[0])
for i in range(num_dtypes):
kwargs = {
"serialized_named_tensors": [tensors[i] for tensors in serialized_named_tensors],
"load_format": "flattened_bucket",
"weight_version": str(weight_version),
"selector": selector,
}
refs.append(ipc_engine.update_weights_from_tensor.remote(**kwargs))

return refs, long_live_tensors
1 change: 0 additions & 1 deletion miles/backends/training_utils/weight_update/delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ def connect(
self._group_name = "miles-disk-delta"
replica_rank, _ = derive_replica_position(parallel_state, placement)
self.is_sender = replica_rank == 0
self.is_lora_sender = self.is_sender and (placement.gather_pp or parallel_state.pp.rank == 0)

def begin_sync(self, weight_version: int, iter_buckets) -> bool:
# The first call only captures the baseline snapshot the next sync diffs against.
Expand Down
Loading
Loading