Repository navigation
Merge LoRA adapters safely in automatic Hugging Face exports - #308
ramanathan831 wants to merge 4 commits into
Conversation
…g weights Signed-off-by: Ramanathan Arunachalam <rarunachalam@nvidia.com>
|
Review of 1. Tied-base rejection depends on module registration order
With a plain Linear and a LoRA Linear sharing the same base weight, the existing test covers plain-first registration and correctly raises. Reversing registration order does not raise: base = torch.nn.Linear(4, 3, bias=False)
adapted = LoraInjectedLinear(base, rank=2, alpha=4)
network = torch.nn.ModuleDict({"adapted": adapted, "base": base})
wrapped = SimpleNamespace(model=SimpleNamespace(model=network))
_, manifest, _ = HFExportCallback(dtype="float32")._gather_weights(wrapped)
# Currently accepted; manifest contains only "adapted.weight".
# "base.weight" is silently omitted, although the plain and adapted
# paths need different effective weights when the adapter is nonzero.Could you check aliases before parameter deduplication and reject this ambiguous plain/LoRA shared-base case in either order? Please parameterize the existing tied-base test over both orders. This is a specific shared-weight edge case, not evidence that ordinary Qwen/VLM exports universally fail; I reproduced gathering/manifest behavior, not a full HF reload of this custom topology. 2. Existing in-place/offline merge results also change
A scalar BF16 base of
Accumulating the completed merge at higher precision is mathematically reasonable; I am not calling that result incorrect. However, it is not output-identical to the existing API and broadens the change beyond the missing automatic-export merge. For minimal downstream behavioral impact, could the new non-mutating helper be used for automatic export while retaining the existing 3. Downstream/backport validationPlease also note that mapped production-file backports do not necessarily carry the new public test files or GitHub GPU workflow. The downstream backport should carry equivalent regression coverage, preserve existing downstream tests, and verify non-LoRA exports as well as LoRA exports. Correcting merged LoRA weights and snapshot storage is justified; it should not be described as a zero-behavior-change backport. Independent validation: 8 LoRA utility cases and 7 CPU gather/export-guard cases passed in an isolated harness using the actual production/test functions, with package-level logging/base-class imports isolated. The alias-order counterexample and BF16 old/new difference above were also reproduced. I did not independently rerun the full HF reload suite, the two-H100 cases, remote uploads, or full training. |
|
Following up on the review above, these are the two concrete revisions requested for this PR: 1. Make LoRA shared-base handling independent of registration order. Build the alias information before parameter deduplication, e.g. using 2. Keep the existing in-place/offline merge API unchanged in this narrowly scoped fix. Retain the new non-mutating Together with the scheduler-state fix requested on #307, these are the three concrete changes from this review. For the I4 mapped backport, the intended boundary is: preserve unrelated existing valid workflows, configuration defaults and training state; allow the necessary differences from confirmed correctness fixes. Correct LoRA export merging and independent CPU snapshot storage can remain. There is no requirement to keep a broken export or to disable every bug fix by default. Conversely, this should not be represented as an unconditional zero-impact change: corrected LoRA exports necessarily differ from the previously incorrect artifacts. Carry equivalent downstream regression coverage with the production changes. |
Signed-off-by: Ramanathan Arunachalam <rarunachalam@nvidia.com>
|
Addressed both the original review and follow-up in 28e15be.
Validation: the new CPU regressions first reproduced ten failures, and the corrected FSDP fixture reproduced both alias-order failures on both ranks before the production fixes. On the final branch, 41 focused CPU tests and all five distributed cases per H100 rank pass. The workflow-selected H100 L0 suite passed with 754 passed and 8 skipped, the isolated attention check had 1 pass and 1 capability skip (FlashAttention-4 sparse backward is unavailable), and all 172 AutoML helper tests passed. Five L0 skips are the FSDP cases run separately; the other three are existing hardware/backend capability guards. Ruff/format, both full pre-commit configurations, changed-file Pyrefly, and diff checks also pass. These runs include actual local HF reloads and NCCL/FSDP collectives; they do not claim a new full-training or remote-upload run. |
Automatic HF checkpoint export wrote LoRA base and adapter parameters separately, so a plain Hugging Face loader could silently use unmerged weights. This PR exports merged dense LoRA/RSLoRA weights without changing live training parameters.
LoraInjectedLinearmodules.forward()and the existing in-place/offline merge APIs, including their rounding and ambient-autocast behavior. The new numerical policy applies only to automatic export.Validation on the revised branch:
main.git diff --checkpassed. AST comparison also confirms the existing forward/merge implementations are unchanged.The focused reloads use a tiny randomly initialized Qwen2 model and local safetensors. This revision does not claim new full-training, remote-upload, or downstream execution results.
Backport requirements: corrected LoRA artifacts, independent CPU snapshots, and explicit rejection of ambiguous aliases are intentional behavior changes. Preserve unrelated valid workflows, configuration defaults, training state, and the legacy merge numerical policy. This is not a zero-impact backport.
.file_mapping.jsonmaps the two production files, but neither their public test files nor the GitHub GPU workflow. Carry equivalent coverage fromhf_export_test.pyandlora_test.py, adapt destination imports, and preserve existing downstream tests. Include non-LoRA exports, valid ties, alias-order rejection, snapshot independence, BF16/autocast boundaries, and distributed gather/rejection checks. Run the distributed cases explicitly; single-process pytest skips them.