Repository navigation
lora: honor lora_target_modules and fix the sidecar save crash - #9
jenny-luo-0727 wants to merge 2 commits into
Conversation
training.lora_target_modules was written to the adapter sidecar but never passed to apply_lora_torchtune, so every nn.Linear except lm_head was adapted regardless of the setting. Setting it via Hydra also crashed save_lora_adapter, because json cannot serialize an OmegaConf ListConfig. apply_lora_torchtune now takes target_modules (leaf names, matched like exclude) and raises on a name that matches no layer, an empty list, or a bare string. ZoneATrainer and ZoneDTrainer pass the config value through. With target_modules unset the result is unchanged (state dict bit-identical on a seeded toy backbone). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GmpnEoNqJpQjR59ie7ZMek
balajiadarsha
left a comment
There was a problem hiding this comment.
Thanks, this fixes both bugs as described. I checked the code that uses the adapters and adapting only some layers is safe there: merge_lora_into_base and eval_kegg_aurora.py merge whatever lora pairs are in the saved file, both trainers' watchdogs only check that some LoRA parameter is trainable, and SFT then GRPO with different target lists works because the SFT adapter is merged into the base weights first. I also ran the new tests locally with torchtune 0.6.1 and torchao 0.5.0: 7 passed.
Please address in this PR
- The BioReason demo notebook now contradicts the code.
applications/bioreason/bioreason_demo.ipynb, cellcell-049, still says "training.lora_target_modulesis metadata only: everynn.Linearin the backbone exceptlm_headgets an adapter." After this PR that's wrong, and it's the most user-facing description of the option. Suggest:nulladapts everynn.Linearexceptlm_head, and a list such as[q_proj, v_proj]adapts only those leaf names. - CI never runs the new tests (see inline comment on the test file).
Smaller points; fine as follow-ups
- The
ListConfigconversion happens late (see inline comment onsave_lora_adapter). - The sidecar records
target_modules, but nothing checks it on load.load_lora_adaptercompares onlyrank. Now that the option changes which layers get adapters, loading an adapter into a backbone adapted on different layers only logs unexpected keys, and a full SFT resume after changing the list fails with a raw state-dict key mismatch. Anexpected_target_modulescheck like the existingexpected_rankone, plus a note in the config comment that changing the list makes earlier checkpoints incompatible, would cover it.
Nit: getattr(config, "lora_target_modules", None) could be config.lora_target_modules since the field always exists on TrainingConfig, but it matches the surrounding style, so fine either way.
Handled in a separate PR, #10 (built on this branch, so merge this one first):
- The "matched no nn.Linear" error now lists the leaf names that can be targeted (
Available: ['down_proj', 'gate_proj', ...]), taken from the actual backbone, so it's correct for any architecture. - The startup log line now breaks the replaced layers down by name (
replaced 32 linear layers (q_proj: 16, v_proj: 16)).
Items 1 and 2 aren't covered by #10.
| import pytest | ||
| import torch.nn as nn | ||
|
|
||
| pytest.importorskip("torchtune") |
There was a problem hiding this comment.
requirements/ci.txt deliberately doesn't install torchtune, so CI skips every test in this file and nothing guards against the bug coming back. The most fragile part, the name checks (typo, empty list, bare string, excluded name) and which layers get selected, doesn't need torchtune. Suggest moving it into a torchtune-free helper in lora_utils.py, e.g. _select_lora_targets(backbone, exclude, target_modules) -> list[tuple[str, nn.Linear]], which apply_lora_torchtune calls. Those tests could then run in CI, and only the LoRALinear and sidecar tests would stay behind importorskip.
| meta = { | ||
| "rank": rank, | ||
| "alpha": alpha, | ||
| "target_modules": None if target_modules is None else list(target_modules), |
There was a problem hiding this comment.
This fixes the crash here, but TrainingConfig.lora_target_modules is still a ListConfig everywhere else at runtime. For example, both trainers write self.config.__dict__ to training_state.json with json.dump(..., default=str), so that file records the string "['q_proj', 'v_proj']" rather than a list. Converting once in src/train.py:463, the way _coerce_str_or_none already handles other fields (e.g. via OmegaConf.to_container), would give every consumer a plain list. Keeping this list() as a fallback for direct callers is fine.
| unmatched = sorted(targets - {name.split(".")[-1] for name, _ in to_replace}) | ||
| if unmatched: | ||
| raise ValueError( | ||
| f"target_modules {unmatched} matched no nn.Linear in the backbone " |
There was a problem hiding this comment.
#10 extends this message to list the available leaf names, which is easier to review there than to add here.
Review follow-ups on AI-ModCon#9. - Move layer selection and the target_modules name checks out of apply_lora_torchtune into _select_lora_targets, which needs no torchtune. The selection and bad-name tests now run in CI (which does not install torchtune); only the LoRALinear and sidecar tests still skip without it. - Convert lora_target_modules from an OmegaConf ListConfig to a plain list in build_training_config, so the config the trainers dump into training_state.json records a list rather than its string form. A bare string passes through unchanged, so apply_lora_torchtune still rejects it. - Update the BioReason demo notebook, which still described lora_target_modules as metadata only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thank you for the review. Addressed in 5c9e8c2:
I've left 4 ( Heads-up for #10: moving the selection loop into |
Description
training.lora_target_moduleshad no effect, and setting it crashed the first checkpoint save.TrainingConfig.lora_target_moduleswas never passed toapply_lora_torchtune, which had no parameter for it.ZoneATrainerandZoneDTrainertherefore wrapped everynn.Linearexceptlm_head, whatever the config said. The value was still written to the adapter's.meta.jsonsidecar, so the sidecar could list target modules that were never adapted.ListConfig.save_lora_adapterthen raisedTypeError: Object of type ListConfig is not JSON serializablewhen writing the sidecar.Changes:
apply_lora_torchtunetakestarget_modules: list[str] | None = None.Nonekeeps the current behavior: everynn.Linearexceptexclude. A list restricts LoRA to those leaf names (e.g.[q_proj, v_proj]), matched the same way as the existingexclude.nn.LinearraisesValueError, so a typo can't leave layers unadapted without notice. An empty list also raisesValueError. A bare string raisesTypeError, since iterating it would split the name into single characters.ZoneATrainerandZoneDTrainerpassconfig.lora_target_modulesthrough.save_lora_adapterconvertstarget_modulesto a plainlistbeforejson.dump.config.py,bioreason_sft.yamlandtrainer_zone_a.py.Type of Change
Compatibility
src.*import pathsprismCLI surfaceWith
lora_target_modulesunset (the default, and every shipped config), the result is unchanged. See the default-behaviour check under testing.How Has This Been Tested?
Run on a NERSC Perlmutter login node, CPU only (torch 2.14.1+cpu, Python 3.12), with
requirements/ci.txt, mirroringpr_quality.yml:The new tests need torchtune, which
requirements/ci.txtdoes not install, so they skip in the run above. They were run separately with torchtune 0.6.1 (PyPI), torchao 0.5.0 and omegaconf:Default-behaviour check: the old and new
apply_lora_torchtunewere run on the same seeded toy backbone, with linear layers both with and without bias, and withtarget_modulesunset. All 35 state-dict tensors are bit-identical and the set of trainable parameters is the same.New Dependencies, Data, or Assets
None.
Checklist
ruff check, ruff 0.16.9 perrequirements/ci.txt)AI/LLM Assistance
The fix and tests were written with Claude Code (Claude Opus 5.5).
Additional Context
The new tests use
pytest.importorskip("torchtune"), andrequirements/ci.txtdoes not install torchtune, so CI will skip them. They ran locally against torchtune 0.6.1 (PyPI) with torchao 0.5.0. The sidecar test also needsomegaconf.🤖 Generated with Claude Code
https://claude.ai/code/session_01GmpnEoNqJpQjR59ie7ZMek