Skip to content

Sync LoRA sessions as adapters; drop merged-weight LoRA sync - #90

Closed
qywu wants to merge 2 commits into
mainfrom
qywu/lora-adapter-sync
Closed

Sync LoRA sessions as adapters; drop merged-weight LoRA sync#90
qywu wants to merge 2 commits into
mainfrom
qywu/lora-adapter-sync

Conversation

@qywu

@qywu qywu commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

LoRA weight sync no longer folds the adapter into the base model and broadcasts merged full weights. The engine exports the session's adapter in PEFT format (adapter_model.safetensors + adapter_config.json, the format SGLang consumes) and the API layer loads it on every registered endpoint via /load_lora_adapter. When LoRA is used, inference endpoints are expected to support LoRA: registration now rejects endpoints that don't report enable_lora=true, or whose max_lora_rank is below the trainer's substrate rank.

Why remove merged sync

The design was documented in docs/.../weight-sync/overview.mdx § Sync with LoRA ("the sync merges LoRA weights into the base model before broadcasting") and implemented in handler.py::_prepare_lora_adapter_for_sync. Two problems, both observed live:

  1. It doesn't scale. The fold materializes W + (α/r)·B@A on a trainer whose base is already resident. A Qwen3.5-35B-A3B EP=2 trainer at 70GB+/GPU dies in canonical_lora_fold during the very first sync (CUDA out of memory ... 512.00 MiB with <65MB free). Every LoRA run above ~8B on ≤2 GPUs is structurally blocked.
  2. It's wasteful. Full-model bytes over NCCL every optimizer step for a ~100MB adapter delta.

Adapter publication is also what the internal large-MoE RL runs already do (merged_weight_sync: false in the q30-k3zero run configs).

Changes

  • weight_sync/handler.py: _prepare_lora_adapter_for_sync (fold) → _export_adapter_for_sync (collective save_lora_only PEFT export to <output_dir>/weight_sync_adapters/<model_id>/<version>); LoRA sessions return before any NCCL transport.
  • api_server/inference_endpoints.py: both sync paths (registration auto-sync and /sync_inference_weights) detect the adapter export and drive the existing _load_lora_on_inference_endpoints broadcast; model_id is threaded through SyncWeightsData; registration gains the LoRA capability gate.
  • docs: Sync with LoRA rewritten.

Open questions for review

  • Numerics/K₃: endpoint-side LoRA computes Wx + B(Ax) through SGLang's LoRA kernels instead of the trainer's exact merged-forward fold — this widens train↔inference K₃ vs merged sync. Ratio-clipped objectives absorb it; the zero-K₃ contract would need the Triton-LoRA-backend alignment treated explicitly.
  • QLoRA and the DSV4/GLM exact active-LoRA factor banks are not covered by the PEFT export; their guards still fail loudly (unchanged semantics).
  • Pool-scoped syncs: the adapter branch currently loads on all registered endpoints via the existing helper; honoring pools filtering needs a targeted variant.
  • Client counterpart: SamplingClients must request the adapter by name (model_id); xorl-client examples currently sample the base model under merged-sync assumptions (I can PR that side once this lands).

Testing

  • Both edited modules ast-parse; not yet exercised against a live server (draft). The failure mode this removes is reproducible: any 35B-A3B LoRA trainer on 2 GPUs, first sync_inference_weights.

LoRA weight sync no longer folds the adapter into the base model and
broadcasts merged full weights. Instead the engine exports the session's
adapter in PEFT format and the API server loads it on every registered
endpoint via SGLang /load_lora_adapter. Inference endpoints for a LoRA
training server are now required to support LoRA: registration rejects
endpoints that do not report enable_lora=true or whose max_lora_rank is
below the substrate rank.

Why: the fold materializes W + (alpha/r) B A on a trainer whose base is
already resident, which OOMs at large-model scale (observed live: a
Qwen3.5-35B-A3B EP=2 trainer at 70GB+/GPU dies in canonical_lora_fold
during sync), and it ships full-model bytes over NCCL for a ~100MB delta.
Adapter publication is what the internal large-MoE RL runs already do
(merged_weight_sync: false).

Notes for review:
- QLoRA composites and the DSV4/GLM exact active-LoRA banks are NOT covered
  by the PEFT export; their guards still fail loudly.
- Numerics: endpoint-side LoRA applies base + B(Ax) through SGLang's LoRA
  kernels rather than the trainer's exact merged-forward fold, which widens
  train/inference K3 relative to merged sync; ratio-clipped objectives
  (CISPO/PPO) absorb this, and the Triton LoRA backend is required for the
  on-policy LM-head contract.
- Client counterpart: SamplingClients must target the adapter name; the
  xorl-client examples currently sample the base model under merged sync.
@broly-code-security-scanner

broly-code-security-scanner Bot commented Aug 27, 2026

Copy link
Copy Markdown

Broly Security Scan

Note

Summary

1 actionable finding(s) in this PR

  • 🟡 1 medium

All actionable items are in the table below.

No finding is at or above high, so this check is not blocking. The findings above are still tracked and reported.

Severity Scanner Issue Location Dismiss Verdict
🟡 MEDIUM SAST Path traversal via unsanitized model_id in
adapter export directory construction. The
`model_id...
src/xorl/server/weight_sync/handler.py:503 d2 🔺 TRUE_POSITIVE · Confidence: HIGH

Dismiss false positives

Tick a box to dismiss the finding; untick it to bring the finding back. That is the same as replying /broly dismiss d1 and /broly undismiss d1. To record why it is a false positive, reply with /broly dismiss d1: your reason instead — Broly reuses those reasons to triage similar findings across the org.

  • d2 · 🟡 MEDIUM   · src/xorl/server/weight_sync/handler.py:503 · Path traversal via unsanitized model_id in adapter export directory constru...

Note

Re-scan this PR anytime with /broly scan — useful after /broly undismiss, or to refresh findings without a new push.

Broly — SAST (zai-org/GLM-5.2) · Secrets · SCA · IaC · GH Actions · Base Images · Supply Chain Threats · Exploit Chains · Adversarial Verification

We're continuously improving Broly's accuracy and finding quality — your feedback is valuable. False positives, missed findings, bugs, and feature requests all welcome.

Ask in #security-engineering   Powered by Together AI

train_config = getattr(self.trainer, "train_config", {}) or {}
base_dir = str(train_config.get("output_dir") or "outputs") if isinstance(train_config, dict) else "outputs"
version_token = _safe_abort_token(weight_version) if weight_version else "latest"
export_dir = os.path.join(base_dir, "weight_sync_adapters", resolved_model_id, version_token)
LoRA sessions do not participate in weight sync at all; adapters publish via
/load_lora_adapter and are documented with the LoRA adapter docs. The
weight-sync overview now only documents the dense full-weight transport.
@qywu

qywu commented Aug 27, 2026

Copy link
Copy Markdown
Member Author

Superseded by #94, which retargets this change at dev (per the new PR-CI convention) and adds two correctness fixes found in live validation on a Qwen3.5-35B-A3B stack: the sync-result whitelist dropping the adapter fields, and stale-adapter serving on re-sync (unload-before-load). Closing in favor of #94.

@qywu qywu closed this Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants