tenseleyflow/documentlanguagemodel / 706aae7

Browse files

export: expose --adapter-mix-method (linear|svd); update CLI reference (audit-08 N4)

Authored by espadonne
SHA
706aae74eb2211d53fcd8ee44f7976c2a65de985
Parents
7cb08b9
Tree
fde4826

4 changed files

StatusFile+-
M docs/cli/reference.md 2 1
M src/dlm/cli/commands.py 25 1
M src/dlm/export/weighted_merge.py 20 2
M tests/unit/cli/test_export_adapter_flag.py 21 0
docs/cli/reference.mdmodified
@@ -110,7 +110,8 @@ dlm export <path> [--quant Q] [--merged [--dequantize]]
110110
 | `--draft TAG` | auto | Override the speculative-decoding draft model. |
111111
 | `--no-draft` | false | Disable speculative decoding. Mutex with `--draft`. |
112112
 | `--adapter NAME` | None | Export a single named adapter from `training.adapters`. Rejected on single-adapter documents. Mutex with `--adapter-mix`. |
113
-| `--adapter-mix SPEC` | None | Weighted composition like `knowledge:1.0,tone:0.5`. Produces one Ollama model by merging the named adapters linearly at export time. LoRA-only; QLoRA sources require `--dequantize --merged`. Mutex with `--adapter`. |
113
+| `--adapter-mix SPEC` | None | Weighted composition like `knowledge:1.0,tone:0.5`. Produces one Ollama model by merging the named adapters at export time. LoRA-only; QLoRA sources require `--dequantize --merged`. Mutex with `--adapter`. |
114
+| `--adapter-mix-method` | `linear` | PEFT merge strategy: `linear` (default; fast weighted sum) or `svd` (higher fidelity, heavier compute). Only meaningful with `--adapter-mix`. |
114115
 
115116
 ### `dlm pack`
116117
 
src/dlm/cli/commands.pymodified
@@ -576,6 +576,18 @@ def export_cmd(
576576
             ),
577577
         ),
578578
     ] = None,
579
+    adapter_mix_method: Annotated[
580
+        str,
581
+        typer.Option(
582
+            "--adapter-mix-method",
583
+            help=(
584
+                "PEFT combination strategy for --adapter-mix. `linear` "
585
+                "(default) sums LoRA deltas; `svd` recomposes via SVD "
586
+                "(higher fidelity, heavier compute). Only meaningful "
587
+                "with --adapter-mix."
588
+            ),
589
+        ),
590
+    ] = "linear",
579591
     verbose: Annotated[
580592
         bool,
581593
         typer.Option("--verbose", help="Log each subprocess command as it launches."),
@@ -651,6 +663,12 @@ def export_cmd(
651663
                 "documents (this doc does not declare `training.adapters`)."
652664
             )
653665
             raise typer.Exit(code=2)
666
+        if adapter_mix_method not in ("linear", "svd"):
667
+            console.print(
668
+                f"[red]export:[/red] --adapter-mix-method must be "
669
+                f"`linear` or `svd`, got {adapter_mix_method!r}."
670
+            )
671
+            raise typer.Exit(code=2)
654672
         try:
655673
             entries = parse_mix_spec(adapter_mix)
656674
             validate_mix_against_declared(entries, set(adapters_declared))
@@ -718,7 +736,13 @@ def export_cmd(
718736
         base_model = AutoModelForCausalLM.from_pretrained(
719737
             str(cached.path), revision=spec.revision
720738
         )
721
-        merged = build_weighted_merged(base_model, store, spec, entries_typed)
739
+        merged = build_weighted_merged(
740
+            base_model,
741
+            store,
742
+            spec,
743
+            entries_typed,
744
+            combination_type=adapter_mix_method,  # type: ignore[arg-type]
745
+        )
722746
         merge_dir = store.cache_dir_for(
723747
             "_export_merged_" + "_".join(n for n, _ in mix_entries)
724748
         )
src/dlm/export/weighted_merge.pymodified
@@ -25,7 +25,7 @@ from __future__ import annotations
2525
 
2626
 import re
2727
 from dataclasses import dataclass
28
-from typing import TYPE_CHECKING, Any, Final
28
+from typing import TYPE_CHECKING, Any, Final, Literal
2929
 
3030
 from dlm.export.errors import ExportError
3131
 
@@ -131,17 +131,35 @@ def validate_mix_against_declared(
131131
         )
132132
 
133133
 
134
+CombinationType = Literal["linear", "svd"]
135
+"""Allowed `add_weighted_adapter.combination_type` values (audit-08 N4).
136
+
137
+- `linear` (default): weighted sum of LoRA deltas. Fast; exact for
138
+  same-rank adapters; approximate for mixed ranks.
139
+- `svd`: SVD-based recomposition. Higher fidelity across mixed ranks
140
+  at meaningful compute cost.
141
+
142
+PEFT exposes more (`cat`, `ties`, `dare_linear`, ...); we gate to the
143
+two the project has tested end-to-end. Extending is a one-line
144
+`Literal` bump once a new combination is validated.
145
+"""
146
+
147
+
134148
 def build_weighted_merged(  # pragma: no cover - heavy path
135149
     base_model: Any,
136150
     store: StorePath,
137151
     spec: BaseModelSpec,  # noqa: ARG001  # reserved for future use
138152
     entries: list[MixEntry],
153
+    *,
154
+    combination_type: CombinationType = "linear",
139155
 ) -> Any:
140156
     """Load each adapter, combine them via `add_weighted_adapter`.
141157
 
142158
     Returns a `PeftModel` with adapter `_export_merged` active. The
143159
     caller writes the merged adapter to an ephemeral directory and
144160
     hands it to the existing GGUF pipeline.
161
+
162
+    `combination_type` controls PEFT's merge strategy (audit-08 N4).
145163
     """
146164
     from peft import PeftModel
147165
 
@@ -163,7 +181,7 @@ def build_weighted_merged( # pragma: no cover - heavy path
163181
         adapters=[e.name for e in entries],
164182
         weights=[e.weight for e in entries],
165183
         adapter_name=_MERGED_ADAPTER_NAME,
166
-        combination_type="linear",
184
+        combination_type=combination_type,
167185
     )
168186
     model.set_adapter(_MERGED_ADAPTER_NAME)
169187
     return model
tests/unit/cli/test_export_adapter_flag.pymodified
@@ -162,3 +162,24 @@ class TestExportAdapterMixValidation:
162162
         text = _joined(result)
163163
         assert "not declared" in text
164164
         assert "ghost" in text
165
+
166
+    def test_unknown_mix_method_rejected(self, tmp_path: Path) -> None:
167
+        """Audit-08 N4: --adapter-mix-method accepts only linear|svd."""
168
+        doc = _init_multi(tmp_path)
169
+        runner = CliRunner()
170
+        result = runner.invoke(
171
+            app,
172
+            [
173
+                "--home",
174
+                str(tmp_path / "home"),
175
+                "export",
176
+                str(doc),
177
+                "--adapter-mix",
178
+                "knowledge:1.0,tone:0.5",
179
+                "--adapter-mix-method",
180
+                "dare_linear",  # supported by PEFT but not exposed here
181
+                "--skip-ollama",
182
+            ],
183
+        )
184
+        assert result.exit_code == 2
185
+        assert "linear` or `svd`" in _joined(result)