Skip to content

feat(integrations): add the axolotl QPeftPlugin for QA-LoRA and PEQA - #4

Merged
gapsong merged 3 commits into
mainfrom
fm/qpeft-axo-plugin
Oct 12, 2026
Merged

gapsong merged 3 commits into
mainfrom
fm/qpeft-axo-plugin

Conversation

@gapsong

@gapsong gapsong commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Intent

Update PR 4 (step 3 of the qpeft axolotl integration: QPeftPlugin for qa_lora and peqa) after PR 3 (GPTQ export, qpeft/export/gptq.py) was squash-merged into main and PR 4 now conflicts in README.md and AGENTS.md. Firstmate asked: rebase fm/qpeft-axo-plugin onto origin/main, resolve the README.md / AGENTS.md conflicts by keeping both sides' content, rerun the full test suite, push the update so PR 4 is clean and mergeable again; CI is skipped (no GitHub Actions; tests run locally only). Expected resolution: AGENTS.md Commands keeps both the tests/test_gptq_load.py line and the QPEFT_RUN_SLOW axolotl e2e line, then the 'GPTQ export facts' section, then the 'axolotl integration' section; README.md structure tests line reads '... tinygemm, GPTQ export, axolotl args and plugin'. No other content changes: the plugin commit and the two earlier no-mistakes fix commits stay as they are. Earlier captain decisions still hold and were already applied and reviewed: D1 (a) a later upstream axolotl save hook, not this step; D2 (a) QA-LoRA refuses GPTQ export in v1; the plugin refuses qpeft.export != qpeft at register() because wiring qpeft.export.export_gptq into the plugin is a later step (step 6), even though export_gptq now exists on main; README documents the manual plugins: line for axolotl <= 0.20.x and keeps axolotl>=0.20; efficient_qat is refused in the plugin (step 4). Known and out of scope: tests/test_examples.py::test_qat_trainer_example_runs (QPEFT_RUN_SLOW only) runs out of MPS memory on this 64 GB Mac in the E2E-QP backward; it fails identically on clean origin/main, and this branch does not touch examples/qat_trainer.py, qpeft/trainer.py or qpeft/block_ap.py. Do not add unrelated changes.

What Changed

  • Adds qpeft/integrations/axolotl/plugin.py with QPeftPlugin, registered under the axolotl.plugins entry point, plus a new .[axolotl] extra (axolotl>=0.20).
    With adapter: qpeft, it quantizes the model in load_adapter, sets qpeft's param_groups optimizer and a FirstStepMustMoveParams callback in post_trainer_create, and in an idempotent post_train runs verify_quant_model, merges, and saves the integer artifact to output_dir.
    It refuses method: efficient_qat, any qpeft.export other than qpeft, and inference/config-only loads.
    It also replaces save_pretrained on the model instance, so axolotl's final save and its Ctrl-C save into output_dir write no transformers weights, while Trainer checkpoints still save.
  • In args.py, the qpeft: block now also refuses inference, loraplus_lr_ratio, loraplus_lr_embedding, embedding_lr, embedding_lr_scale and lr_groups.
    In tuners_utils.py, get_quant_model now refuses before it changes the model when target_modules matches no nn.Linear, or when a matched layer's in_features does not fit group_size/bits, and the error lists every such layer.
  • Adds unit tests for the plugin, the new refusals and the injection check, plus a QPEFT_RUN_SLOW end-to-end test that runs the real axolotl train CLI (tests/test_axolotl_e2e.py, tests/axolotl_probe.py).
    Updates the README (install line, the manual plugins: line for axolotl <= 0.20.x, plugin usage, injection refusals, structure) and the AGENTS.md notes on axolotl.

Risk Assessment

✅ Low: I compared the old and new commits with git range-diff. The rebase changed only the AGENTS.md and README.md conflict regions, exactly as the intent asks. Commits f7531c0 and ce38373 are identical to their pre-rebase versions (11b463e, 7fd016f), and the plugin and tuner code was already reviewed. I traced the new "no target matched" refusal in inject_adapters: an already-quantized model returns before the check, so phase switches and QuantModel.from_pretrained are not affected.

Testing

In a throwaway venv with axolotl main d17227d, I drove the rebased plugin through the real axolotl train CLI, and all five end-to-end tests pass. QA-LoRA and PEQA artifacts reload in a fresh process and match the trained logits (PEQA bit-exact). Checkpoint resume reproduces the uninterrupted losses for both methods. Ctrl-C exits cleanly and writes no model file. All four refusal configs stopped before training with the expected messages. Targeted plugin and GPTQ export unit tests pass on the rebased code, but they were not a live run, so that scenario is marked untested. A Python 3.10 venv without axolotl confirms the plugin tests skip instead of breaking collection. The README.md and AGENTS.md resolution was checked only by reading the diff, so that scenario is also marked untested. The first e2e run failed only because of a venv setup gap (bitsandbytes is missing on macOS), which I fixed before re-running. There is no UI surface; the evidence is CLI logs. The only leftovers in the worktree are qpeft.egg-info and pycache directories from the editable install. git ignores them; my cleanup command was blocked by the permission prompt.

  • Live validation: ✅ go - 8 of 10 scenarios driven live against the product
Scenario Result Live Evidence
QA-LoRA trains through axolotl train; the integer artifact reloads in a fresh process and matches the trained logits within the fp16 fold tolerance; no model.safetensors is written ✅ pass live axolotl_e2e.log: test_the_artifact_reloads_in_a_fresh_process_as_trained[qa_lora] PASSED
PEQA trains through axolotl train; the integer artifact reloads in a fresh process bit-exact; a plain transformers load is refused ✅ pass live axolotl_e2e.log: test_the_artifact_reloads_in_a_fresh_process_as_trained[peqa] PASSED (max_diff 0.0)
Resuming a QA-LoRA or PEQA axolotl run from checkpoint-4 gives the same losses as the uninterrupted run, with qpeft's optimizer groups ✅ pass live axolotl_e2e.log: test_resume_from_a_checkpoint_gives_the_same_losses[qa_lora] PASSED, [peqa] PASSED
Ctrl-C during a qpeft axolotl run exits cleanly without hanging and leaves no model.safetensors and no qpeft_model.pt ✅ pass live axolotl_e2e.log: test_ctrl_c_leaves_no_model_file_that_loads_with_random_weights PASSED
A user sets qpeft.export: gptq; axolotl train refuses at register() before preparing the dataset, and no output is written ✅ pass live refuse_export_gptq.log
A user asks for method: efficient_qat with adapter: qpeft; axolotl train refuses and points to QATTrainer ✅ pass live refuse_efficient_qat.log
A user sets loraplus_lr_ratio or lr_groups with adapter: qpeft; axolotl config validation refuses instead of silently ignoring it ✅ pass live refuse_loraplus.log, refuse_lr_groups.log
On Python 3.10 without axolotl, pytest collects the axolotl test files without error and skips them ✅ pass live Python 3.10 venv: 57 passed, 4 skipped (could not import axolotl)
The rebased branch keeps main's GPTQ export working next to the plugin ⏸️ untested no The prior payload did not establish a live result: only targeted unit tests (tests/test_gptq_export.py and the plugin tests) were run, not a live export plus a real GPTQ load. The real load needs gptq…
The conflict resolution keeps both sides in the agreed order in AGENTS.md, and the README tests line ends 'GPTQ export, axolotl args and plugin' ⏸️ untested no The prior payload did not establish a live result: this is docs-only content with no runtime surface, and it was checked only by comparing git diff origin/main HEAD -- AGENTS.md README.md with the a…
Evidence: axolotl e2e log: 5 passed

test_the_artifact_reloads_in_a_fresh_process_as_trained[qa_lora] {'max_diff': 0.0166, 'max_abs_logit': 12.98, 'argmax_agree': 1.0} PASSED test_the_artifact_reloads_in_a_fresh_process_as_trained[peqa] {'max_diff': 0.0, 'argmax_agree': 1.0, 'plain_transformers_load': 'refused: Error no file named model.safetensors ...'} PASSED test_resume_from_a_checkpoint_gives_the_same_losses[qa_lora] PASSED test_resume_from_a_checkpoint_gives_the_same_losses[peqa] PASSED test_ctrl_c_leaves_no_model_file_that_loads_with_random_weights PASSED 5 passed in 149.75s

============================= test session starts ==============================
platform darwin -- Python 3.12.14, pytest-9.1.1, pluggy-1.6.0 -- /tmp/qpeft-test/venv/bin/python
rootdir: ~/.no-mistakes/worktrees/87bf05bcdbbf/01M4R27CPVNAVZAVMBCQV0W4Z2
configfile: pyproject.toml
plugins: platformdirs-4.13.0, anyio-4.14.2
collecting ... collected 5 items

tests/test_axolotl_e2e.py::test_the_artifact_reloads_in_a_fresh_process_as_trained[qa_lora] qa_lora {'max_diff': 0.0166015625, 'max_abs_logit': 12.9765625, 'argmax_agree': 1.0, 'plain_transformers_load': 'refused: Error no file named model.safetensors, or pytorch_model.bin, found in directory /private/var/folders/nq/mt2sb54s2b99md8pfc25w7fc0000gn/T/pytest-of-gapsong/pytest-190/test_the_artifact_reloads_in_a0/out.'}
PASSED
tests/test_axolotl_e2e.py::test_the_artifact_reloads_in_a_fresh_process_as_trained[peqa] peqa {'max_diff': 0.0, 'max_abs_logit': 12.28125, 'argmax_agree': 1.0, 'plain_transformers_load': 'refused: Error no file named model.safetensors, or pytorch_model.bin, found in directory /private/var/folders/nq/mt2sb54s2b99md8pfc25w7fc0000gn/T/pytest-of-gapsong/pytest-190/test_the_artifact_reloads_in_a1/out.'}
PASSED
tests/test_axolotl_e2e.py::test_resume_from_a_checkpoint_gives_the_same_losses[qa_lora] qa_lora {1: 8.406447410583496, 2: 7.926770210266113, 3: 8.069069862365723, 4: 8.45705509185791, 5: 8.129191398620605, 6: 7.997562885284424, 7: 7.8021016120910645, 8: 8.190009117126465} {1: 8.406447410583496, 2: 7.926770210266113, 3: 8.069069862365723, 4: 8.45705509185791, 5: 8.129191398620605, 6: 7.997562885284424, 7: 7.8021016120910645, 8: 8.190009117126465}
PASSED
tests/test_axolotl_e2e.py::test_resume_from_a_checkpoint_gives_the_same_losses[peqa] peqa {1: 7.960805416107178, 2: 8.187658309936523, 3: 8.256669998168945, 4: 8.347188949584961, 5: 7.529917240142822, 6: 6.787193298339844, 7: 6.619660377502441, 8: 6.2444682121276855} {1: 7.960805416107178, 2: 8.187658309936523, 3: 8.256669998168945, 4: 8.347188949584961, 5: 7.529917240142822, 6: 6.787193298339844, 7: 6.619660377502441, 8: 6.2444682121276855}
PASSED
tests/test_axolotl_e2e.py::test_ctrl_c_leaves_no_model_file_that_loads_with_random_weights PASSED

======================== 5 passed in 149.75s (0:02:29) =========================
Evidence: axolotl train refusal: qpeft.export gptq

UnsupportedSchemeError: adapter: qpeft does not run qpeft.export: gptq in axolotl yet: the plugin writes only the qpeft artifact (qpeft_config.json + qpeft_model.pt), GPTQ export is not wired in. Use export: qpeft. (QA-LoRA refuses GPTQ export by design.)

[2026-10-12 14:46:14,774] [INFO] [axolotl.integrations.base] Attempting to load plugin: axolotl.integrations.context_parallel.ContextParallelPlugin
[2026-10-12 14:46:14,779] [INFO] [axolotl.integrations.base] Plugin loaded successfully: axolotl.integrations.context_parallel.ContextParallelPlugin
[2026-10-12 14:46:14,779] [INFO] [axolotl.integrations.base] Attempting to load plugin: axolotl.integrations.expert_parallel.ExpertParallelPlugin
[2026-10-12 14:46:14,786] [INFO] [axolotl.integrations.base] Plugin loaded successfully: axolotl.integrations.expert_parallel.ExpertParallelPlugin
[2026-10-12 14:46:14,786] [INFO] [axolotl.integrations.base] Attempting to load plugin: qpeft.integrations.axolotl.plugin.QPeftPlugin
[2026-10-12 14:46:14,797] [INFO] [axolotl.integrations.base] Plugin loaded successfully: qpeft.integrations.axolotl.plugin.QPeftPlugin
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/train.py", line 209, in <module>
    fire.Fire(do_cli)
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 135, in Fire
    component_trace = _Fire(component, args, parsed_flag_args, context, name)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 468, in _Fire
    component, remaining_args = _CallAndUpdateTrace(
                                ^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 684, in _CallAndUpdateTrace
    component = fn(*varargs, **kwargs)
                ^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/train.py", line 107, in do_cli
    parsed_cfg = load_cfg_fn(config, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/telemetry/errors.py", line 68, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/config.py", line 297, in load_cfg
    prepare_plugins(cfg)
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/config.py", line 216, in prepare_plugins
    prepare_config_plugins(cfg)
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/utils/config/__init__.py", line 628, in prepare_plugins
    plugin_manager.plugins[plugin_name].register(cfg)
  File "~/.no-mistakes/worktrees/87bf05bcdbbf/01M4R27CPVNAVZAVMBCQV0W4Z2/qpeft/integrations/axolotl/plugin.py", line 54, in register
    raise UnsupportedSchemeError(
qpeft.quant_schemes.base.UnsupportedSchemeError: adapter: qpeft does not run qpeft.export: gptq in axolotl yet: the plugin writes only the qpeft artifact (qpeft_config.json + qpeft_model.pt), GPTQ export is not wired in. Use export: qpeft. (QA-LoRA refuses GPTQ export by design.)
Evidence: axolotl train refusal: efficient_qat

UnsupportedSchemeError: adapter: qpeft does not run method efficient_qat in axolotl yet: its Block-AP phase is not wired into the plugin. Supported here: ['qa_lora', 'peqa'].

[2026-10-12 14:46:17,957] [INFO] [axolotl.integrations.base] Attempting to load plugin: axolotl.integrations.context_parallel.ContextParallelPlugin
[2026-10-12 14:46:17,959] [INFO] [axolotl.integrations.base] Plugin loaded successfully: axolotl.integrations.context_parallel.ContextParallelPlugin
[2026-10-12 14:46:17,959] [INFO] [axolotl.integrations.base] Attempting to load plugin: axolotl.integrations.expert_parallel.ExpertParallelPlugin
[2026-10-12 14:46:17,959] [INFO] [axolotl.integrations.base] Plugin loaded successfully: axolotl.integrations.expert_parallel.ExpertParallelPlugin
[2026-10-12 14:46:17,959] [INFO] [axolotl.integrations.base] Attempting to load plugin: qpeft.integrations.axolotl.plugin.QPeftPlugin
[2026-10-12 14:46:17,964] [INFO] [axolotl.integrations.base] Plugin loaded successfully: qpeft.integrations.axolotl.plugin.QPeftPlugin
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/train.py", line 209, in <module>
    fire.Fire(do_cli)
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 135, in Fire
    component_trace = _Fire(component, args, parsed_flag_args, context, name)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 468, in _Fire
    component, remaining_args = _CallAndUpdateTrace(
                                ^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 684, in _CallAndUpdateTrace
    component = fn(*varargs, **kwargs)
                ^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/train.py", line 107, in do_cli
    parsed_cfg = load_cfg_fn(config, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/telemetry/errors.py", line 68, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/config.py", line 297, in load_cfg
    prepare_plugins(cfg)
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/config.py", line 216, in prepare_plugins
    prepare_config_plugins(cfg)
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/utils/config/__init__.py", line 628, in prepare_plugins
    plugin_manager.plugins[plugin_name].register(cfg)
  File "~/.no-mistakes/worktrees/87bf05bcdbbf/01M4R27CPVNAVZAVMBCQV0W4Z2/qpeft/integrations/axolotl/plugin.py", line 48, in register
    raise UnsupportedSchemeError(
qpeft.quant_schemes.base.UnsupportedSchemeError: adapter: qpeft does not run method efficient_qat in axolotl yet: its Block-AP phase is not wired into the plugin. Supported here: ['qa_lora', 'peqa']. For EfficientQAT use qpeft.QATTrainer (examples/qat_trainer.py).
Evidence: axolotl train refusal: loraplus_lr_ratio

adapter: qpeft refuses this config: - loraplus_lr_ratio: qpeft builds its own optimizer groups (param_groups) and would ignore it.

[2026-10-12 14:46:21,428] [INFO] [axolotl.integrations.base] Attempting to load plugin: axolotl.integrations.context_parallel.ContextParallelPlugin
[2026-10-12 14:46:21,429] [INFO] [axolotl.integrations.base] Plugin loaded successfully: axolotl.integrations.context_parallel.ContextParallelPlugin
[2026-10-12 14:46:21,429] [INFO] [axolotl.integrations.base] Attempting to load plugin: axolotl.integrations.expert_parallel.ExpertParallelPlugin
[2026-10-12 14:46:21,430] [INFO] [axolotl.integrations.base] Plugin loaded successfully: axolotl.integrations.expert_parallel.ExpertParallelPlugin
[2026-10-12 14:46:21,430] [INFO] [axolotl.integrations.base] Attempting to load plugin: qpeft.integrations.axolotl.plugin.QPeftPlugin
[2026-10-12 14:46:21,435] [INFO] [axolotl.integrations.base] Plugin loaded successfully: qpeft.integrations.axolotl.plugin.QPeftPlugin
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/train.py", line 209, in <module>
    fire.Fire(do_cli)
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 135, in Fire
    component_trace = _Fire(component, args, parsed_flag_args, context, name)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 468, in _Fire
    component, remaining_args = _CallAndUpdateTrace(
                                ^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 684, in _CallAndUpdateTrace
    component = fn(*varargs, **kwargs)
                ^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/train.py", line 107, in do_cli
    parsed_cfg = load_cfg_fn(config, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/telemetry/errors.py", line 68, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/config.py", line 306, in load_cfg
    cfg = validate_config(
          ^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/utils/config/__init__.py", line 583, in validate_config
    _model_with_inherited_default_fallback(
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/utils/config/__init__.py", line 106, in _model_with_inherited_default_fallback
    return model_cls(**data)
           ^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/pydantic/main.py", line 263, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for AxolotlConfigWCapabilities
  Value error, adapter: qpeft refuses this config:
  - `loraplus_lr_ratio`: qpeft builds its own optimizer groups (param_groups) and would ignore it. [type=value_error, input_value={'adapter': 'qpeft', 'bas...56, 'lora_dropout': 0.0}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.13/v/value_error
Evidence: axolotl train refusal: lr_groups

adapter: qpeft refuses this config: - lr_groups: qpeft builds its own optimizer groups (param_groups) and would ignore it.

[2026-10-12 14:46:24,583] [INFO] [axolotl.integrations.base] Attempting to load plugin: axolotl.integrations.context_parallel.ContextParallelPlugin
[2026-10-12 14:46:24,584] [INFO] [axolotl.integrations.base] Plugin loaded successfully: axolotl.integrations.context_parallel.ContextParallelPlugin
[2026-10-12 14:46:24,584] [INFO] [axolotl.integrations.base] Attempting to load plugin: axolotl.integrations.expert_parallel.ExpertParallelPlugin
[2026-10-12 14:46:24,585] [INFO] [axolotl.integrations.base] Plugin loaded successfully: axolotl.integrations.expert_parallel.ExpertParallelPlugin
[2026-10-12 14:46:24,585] [INFO] [axolotl.integrations.base] Attempting to load plugin: qpeft.integrations.axolotl.plugin.QPeftPlugin
[2026-10-12 14:46:24,590] [INFO] [axolotl.integrations.base] Plugin loaded successfully: qpeft.integrations.axolotl.plugin.QPeftPlugin
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/train.py", line 209, in <module>
    fire.Fire(do_cli)
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 135, in Fire
    component_trace = _Fire(component, args, parsed_flag_args, context, name)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 468, in _Fire
    component, remaining_args = _CallAndUpdateTrace(
                                ^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/fire/core.py", line 684, in _CallAndUpdateTrace
    component = fn(*varargs, **kwargs)
                ^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/train.py", line 107, in do_cli
    parsed_cfg = load_cfg_fn(config, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/telemetry/errors.py", line 68, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/cli/config.py", line 306, in load_cfg
    cfg = validate_config(
          ^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/utils/config/__init__.py", line 583, in validate_config
    _model_with_inherited_default_fallback(
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/axolotl/utils/config/__init__.py", line 106, in _model_with_inherited_default_fallback
    return model_cls(**data)
           ^^^^^^^^^^^^^^^^^
  File "/tmp/qpeft-test/venv/lib/python3.12/site-packages/pydantic/main.py", line 263, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 1 validation error for AxolotlConfigWCapabilities
  Value error, adapter: qpeft refuses this config:
  - `lr_groups`: qpeft builds its own optimizer groups (param_groups) and would ignore it. [type=value_error, input_value={'adapter': 'qpeft', 'bas...56, 'lora_dropout': 0.0}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.13/v/value_error
- Outcome: ⚠️ 1 info across 1 run (13m7s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 2 issues found → auto-fixed ✅
  • ⚠️ AGENTS.md - merge conflict rebasing onto origin/main
  • ⚠️ README.md - merge conflict rebasing onto origin/main

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Review** - passed

✅ No issues found.

⚠️ **Test** - 1 info
  • ℹ️ AGENTS.md - On macOS, axolotl main d17227d does not install bitsandbytes but imports it in axolotl train. So pip install -e &#34;.[axolotl]&#34; alone gives an e2e run that crashes at import bitsandbytes until you install bitsandbytes by hand. AGENTS.md does not mention this. It is outside the scope of this rebase, so I only report it.
  • Live validation: ✅ go - 8 of 10 scenarios driven live against the product
Scenario Result Live Evidence
QA-LoRA trains through axolotl train; the integer artifact reloads in a fresh process and matches the trained logits within the fp16 fold tolerance; no model.safetensors is written ✅ pass live axolotl_e2e.log: test_the_artifact_reloads_in_a_fresh_process_as_trained[qa_lora] PASSED
PEQA trains through axolotl train; the integer artifact reloads in a fresh process bit-exact; a plain transformers load is refused ✅ pass live axolotl_e2e.log: test_the_artifact_reloads_in_a_fresh_process_as_trained[peqa] PASSED (max_diff 0.0)
Resuming a QA-LoRA or PEQA axolotl run from checkpoint-4 gives the same losses as the uninterrupted run, with qpeft's optimizer groups ✅ pass live axolotl_e2e.log: test_resume_from_a_checkpoint_gives_the_same_losses[qa_lora] PASSED, [peqa] PASSED
Ctrl-C during a qpeft axolotl run exits cleanly without hanging and leaves no model.safetensors and no qpeft_model.pt ✅ pass live axolotl_e2e.log: test_ctrl_c_leaves_no_model_file_that_loads_with_random_weights PASSED
A user sets qpeft.export: gptq; axolotl train refuses at register() before preparing the dataset, and no output is written ✅ pass live refuse_export_gptq.log
A user asks for method: efficient_qat with adapter: qpeft; axolotl train refuses and points to QATTrainer ✅ pass live refuse_efficient_qat.log
A user sets loraplus_lr_ratio or lr_groups with adapter: qpeft; axolotl config validation refuses instead of silently ignoring it ✅ pass live refuse_loraplus.log, refuse_lr_groups.log
On Python 3.10 without axolotl, pytest collects the axolotl test files without error and skips them ✅ pass live Python 3.10 venv: 57 passed, 4 skipped (could not import axolotl)
The rebased branch keeps main's GPTQ export working next to the plugin ⏸️ untested no The prior payload did not establish a live result: only targeted unit tests (tests/test_gptq_export.py and the plugin tests) were run, not a live export plus a real GPTQ load. The real load needs gptq…
The conflict resolution keeps both sides in the agreed order in AGENTS.md, and the README tests line ends 'GPTQ export, axolotl args and plugin' ⏸️ untested no The prior payload did not establish a live result: this is docs-only content with no runtime surface, and it was checked only by comparing git diff origin/main HEAD -- AGENTS.md README.md with the a…
  • QPEFT_RUN_SLOW=1 /tmp/qpeft-test/venv/bin/python -m pytest tests/test_axolotl_e2e.py -v -s (1st run hit a venv gap: axolotl imports bitsandbytes but does not install it on macOS; after installing it, the 2nd run passed 5 of 5 in 2.5 min)
  • python -m axolotl.cli.train export_gptq.yml (adapter: qpeft, qpeft.export: gptq) -> refused at register()
  • python -m axolotl.cli.train efficient_qat.yml -> refused at register()
  • python -m axolotl.cli.train loraplus.yml (loraplus_lr_ratio: 16) -> refused at config validation
  • python -m axolotl.cli.train lr_groups.yml -> refused at config validation
  • pytest tests/test_axolotl_plugin.py tests/test_axolotl_args.py tests/test_tuners_utils.py tests/test_gptq_export.py in the Python 3.12 venv with axolotl
  • pytest tests/test_axolotl_plugin.py tests/test_axolotl_args.py tests/test_axolotl_e2e.py in a Python 3.10 venv without axolotl (no collection error; the axolotl-dependent tests skip)
  • git diff origin/main HEAD -- AGENTS.md README.md (checked the conflict resolution against the agreed text)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Step 3 of the axolotl integration: `adapter: qpeft` in an axolotl YAML
trains QA-LoRA or PEQA and leaves the merged integer artifact
(qpeft_config.json + qpeft_model.pt) in output_dir.

qpeft/integrations/axolotl/plugin.py, QPeftPlugin:
- Found through the `axolotl.plugins` entry point; every hook is a
  no-op unless `adapter: qpeft`, and the import adds only qpeft modules.
- register refuses method efficient_qat before axolotl validates the
  config (Block-AP is step 4).
- load_adapter builds the config through args.QPeftBlock and calls
  get_quant_model in place; it refuses inference / config-only loads.
- post_trainer_create sets trainer.optimizer from param_groups with
  the trainer's own AdamW class and kwargs (axolotl never calls the
  plugin create_optimizer hook; HF keeps a preset optimizer), and adds
  FirstStepMustMoveParams.
- post_train runs verify_quant_model, merges and saves; idempotent,
  because axolotl calls it twice. Logs under axolotl.integrations.qpeft.
- Interim guard until axolotl has a plugin save hook (D1): the plugin
  owns save_pretrained on the model instance. A save into output_dir
  writes no transformers weights (the qpeft artifact is the model;
  after Ctrl-C it writes nothing and says so), so no model.safetensors
  that loads with random weights is left. Checkpoints still save.
  The Ctrl-C message uses os.write: axolotl's stdout tee holds a
  non-reentrant lock that a log call in its signal handler deadlocks on.

Core: get_quant_model now refuses target_modules that match no layer,
and names every target layer that group_size or packing cannot split,
before the first layer is swapped (it used to stop at the first layer
with the model half converted).

Also: args.py refuses `inference` (axolotl inference would quantize the
base again); `axolotl` extra; numpy in the dev extra.

Tests: tests/test_axolotl_plugin.py (hooks on a tiny Llama with a real
HF Trainer); tests/test_axolotl_e2e.py (QPEFT_RUN_SLOW=1: the real
`axolotl train` CLI for both methods, fresh-process reload, resume,
Ctrl-C).
@gapsong
gapsong force-pushed the fm/qpeft-axo-plugin branch from 7fd016f to ce38373 Compare October 12, 2026 12:58
@gapsong
gapsong merged commit 7603463 into main Oct 12, 2026
2 checks passed
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.

1 participant