Skip to content

Commit 5da852b

Browse files
committed
Release v1.0.0: stable API, merge/unmerge, CI hardening
Promote to production-stable v1.0.0 with a reduced public API surface (21 symbols, internal ops moved to vlora.ops), VLoRAModel.merge()/unmerge() for hook-free inference, project() quality warnings, PEP 561 py.typed, and CI gates (ruff, mypy, 80% coverage floor). 225 tests pass at 89% coverage.
1 parent c102506 commit 5da852b

30 files changed

Lines changed: 750 additions & 116 deletions

.github/workflows/ci.yml

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,46 @@ on:
77
branches: [main]
88

99
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: "3.13"
19+
20+
- name: Install dependencies
21+
run: pip install -e ".[dev]"
22+
23+
- name: Ruff lint
24+
run: ruff check src/ tests/
25+
26+
- name: Ruff format check
27+
run: ruff format --check src/ tests/
28+
29+
typecheck:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
34+
- name: Set up Python
35+
uses: actions/setup-python@v5
36+
with:
37+
python-version: "3.13"
38+
39+
- name: Install dependencies
40+
run: pip install -e ".[dev]"
41+
42+
- name: Mypy
43+
run: mypy src/vlora/
44+
1045
test:
1146
runs-on: ubuntu-latest
1247
strategy:
1348
matrix:
14-
python-version: ["3.9", "3.11", "3.13"]
49+
python-version: ["3.9", "3.11", "3.12", "3.13"]
1550

1651
steps:
1752
- uses: actions/checkout@v4
@@ -24,5 +59,5 @@ jobs:
2459
- name: Install dependencies
2560
run: pip install -e ".[dev]"
2661

27-
- name: Run tests
28-
run: pytest tests/ -v
62+
- name: Run tests with coverage
63+
run: pytest tests/ -v --cov=vlora --cov-report=term-missing --cov-fail-under=80

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
44

55
Format follows [Keep a Changelog](https://keepachangelog.com/).
66

7+
## [1.0.0] - 2026-04-11
8+
9+
### Added
10+
- **`VLoRAModel.merge()` / `unmerge()`** — bake LoRA deltas into base model weights for hook-free inference. `is_merged` property tracks state. Errors on quantized base models.
11+
- **Quality warning in `project()`** — optional `warn_threshold` parameter warns when subspace coverage is low, suggesting `absorb()` to expand the basis.
12+
- **PEP 561 `py.typed` marker** — enables type checker support for downstream users.
13+
- **CI hardening** — ruff lint + format check, mypy type checking, pytest-cov with 80% coverage floor, Python 3.12 added to test matrix.
14+
- **Edge case and numerical precision tests** — rank-1 adapters, single-layer adapters, project→reconstruct roundtrip bounds, save→load serialization verification.
15+
16+
### Changed
17+
- **API surface reduced** — low-level ops (`compute_svd`, `project_onto_subspace`, `gram_schmidt`, NF4 functions, `incremental_svd_update`, etc.) removed from top-level `vlora` namespace. Still accessible via `from vlora.ops import ...`.
18+
- **Development status** — classifier updated from "Alpha" to "Production/Stable".
19+
20+
### Migration from 0.3.0
21+
Replace `from vlora import compute_svd` with `from vlora.ops import compute_svd` (and similarly for `project_onto_subspace`, `reconstruct_from_subspace`, `gram_schmidt`, `explained_variance_ratio`, `select_num_components`, `incremental_svd_update`, `NF4_QUANT_TABLE`, `nf4_quantize_dequantize`, `nf4_pack`, `nf4_unpack`). No other breaking changes.
22+
723
## [0.3.0] - 2026-03-30
824

925
### Added

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
<strong>Various LoRA adapters. One shared basis.</strong>
77
</p>
88

9+
<p align="center">
10+
<a href="https://github.com/vlora-dev/vlora/actions/workflows/ci.yml"><img src="https://github.com/vlora-dev/vlora/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
11+
<a href="https://pypi.org/project/vlora-dev/"><img src="https://img.shields.io/pypi/v/vlora-dev" alt="PyPI"></a>
12+
<a href="https://pypi.org/project/vlora-dev/"><img src="https://img.shields.io/pypi/pyversions/vlora-dev" alt="Python"></a>
13+
</p>
14+
915
Your adapters share more structure than you think. vLoRA finds the common basis and stores each adapter as a tiny coefficient vector — up to 122× compression at scale. Based on the [Share paper](https://arxiv.org/abs/2602.06043).
1016

1117
## Install
@@ -101,6 +107,19 @@ output = model(input_ids)
101107
print(model.available_tasks) # ["task_0", "task_1", ...]
102108
```
103109

110+
### Merging into Base Weights
111+
112+
For deployment with a single adapter, bake deltas directly into the base model — zero hook overhead:
113+
114+
```python
115+
model = VLoRAModel(base_model, subspace)
116+
model.merge(task_id="task_0") # deltas baked into weights
117+
output = model(input_ids) # pure base model forward, no hooks
118+
119+
model.unmerge() # restore original weights
120+
model.set_task("task_1") # back to hook-based inference
121+
```
122+
104123
## QLoRA Support
105124

106125
vLoRA has first-class support for [QLoRA](https://arxiv.org/abs/2305.14314) workflows. QLoRA compresses the **base model** (FP16 → 4-bit NF4), while vLoRA compresses the **adapter space** — these are orthogonal and stack multiplicatively.
@@ -445,6 +464,22 @@ subspace.save("updated_subspace/")
445464
}
446465
```
447466

467+
## Migrating from v0.x
468+
469+
Low-level math operations have been moved from the top-level `vlora` namespace to `vlora.ops`:
470+
471+
```python
472+
# Before (v0.x)
473+
from vlora import compute_svd, gram_schmidt, nf4_pack
474+
475+
# After (v1.0)
476+
from vlora.ops import compute_svd, gram_schmidt, nf4_pack
477+
```
478+
479+
Moved symbols: `compute_svd`, `project_onto_subspace`, `reconstruct_from_subspace`, `gram_schmidt`, `explained_variance_ratio`, `select_num_components`, `incremental_svd_update`, `NF4_QUANT_TABLE`, `nf4_quantize_dequantize`, `nf4_pack`, `nf4_unpack`.
480+
481+
All other public APIs remain unchanged.
482+
448483
## License
449484

450485
Apache 2.0

pyproject.toml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "vlora-dev"
7-
version = "0.3.0"
7+
version = "1.0.0"
88
description = "Various LoRA adapters. One shared basis. Up to 122x compression at scale."
99
readme = "README.md"
1010
license = "Apache-2.0"
1111
requires-python = ">=3.9"
1212
authors = [{ name = "Tim Veseli" }]
1313
keywords = ["lora", "llm", "parameter-efficient", "fine-tuning", "subspace"]
1414
classifiers = [
15-
"Development Status :: 3 - Alpha",
15+
"Development Status :: 5 - Production/Stable",
1616
"Intended Audience :: Science/Research",
1717
"License :: OSI Approved :: Apache Software License",
1818
"Programming Language :: Python :: 3",
@@ -35,14 +35,18 @@ hf = ["transformers>=4.38", "huggingface-hub>=0.20"]
3535
docs = ["mkdocs-material>=9.5", "mkdocstrings[python]>=0.24"]
3636
dev = [
3737
"pytest>=7.0",
38+
"pytest-cov>=4.0",
3839
"huggingface-hub>=0.20",
3940
"pre-commit>=3.0",
4041
"ruff>=0.8",
42+
"mypy>=1.0",
4143
]
4244

4345
[project.urls]
44-
Homepage = "https://github.com/tveseli/vlora"
45-
Repository = "https://github.com/tveseli/vlora"
46+
Homepage = "https://github.com/vlora-dev/vlora"
47+
Repository = "https://github.com/vlora-dev/vlora"
48+
Changelog = "https://github.com/vlora-dev/vlora/blob/main/CHANGELOG.md"
49+
Issues = "https://github.com/vlora-dev/vlora/issues"
4650

4751
[tool.hatch.build.targets.wheel]
4852
packages = ["src/vlora"]
@@ -56,9 +60,16 @@ line-length = 100
5660

5761
[tool.ruff.lint]
5862
select = ["E", "F", "I", "W"]
63+
ignore = ["E501", "E741", "E402"]
5964

6065
[tool.mypy]
6166
python_version = "3.9"
6267
ignore_missing_imports = true
6368
warn_return_any = true
6469
check_untyped_defs = true
70+
follow_imports = "skip"
71+
exclude = ["build/", "dist/"]
72+
73+
[[tool.mypy.overrides]]
74+
module = ["torch.*", "safetensors.*"]
75+
follow_imports = "skip"

src/vlora/__init__.py

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,22 @@
55
maintain one shared basis and per-task coefficient vectors.
66
"""
77

8-
__version__ = "0.3.0"
8+
__version__ = "1.0.0"
99

10-
from vlora.io import LoRAWeights, load_adapter, load_adapter_from_hub, save_adapter
11-
from vlora.ops import (
12-
NF4_QUANT_TABLE,
13-
compute_svd,
14-
explained_variance_ratio,
15-
gram_schmidt,
16-
nf4_pack,
17-
nf4_quantize_dequantize,
18-
nf4_unpack,
19-
project_onto_subspace,
20-
reconstruct_from_subspace,
21-
select_num_components,
22-
)
23-
from vlora.model import VLoRAModel
24-
from vlora.ops import incremental_svd_update
2510
from vlora.analysis import (
2611
adapter_diff,
2712
compute_similarity_matrix,
2813
find_clusters,
2914
find_outliers,
3015
subspace_coverage,
3116
)
17+
from vlora.io import LoRAWeights, load_adapter, load_adapter_from_hub, save_adapter
18+
from vlora.merge import dare_merge, task_arithmetic, ties_merge
19+
from vlora.model import VLoRAModel
3220
from vlora.pipeline import absorb_task, extract_adapter, init_subspace
3321
from vlora.router import TaskRouter
3422
from vlora.subspace import SharedSubspace, TaskProjection
3523
from vlora.training import SubspaceTrainer, orthogonal_init
36-
from vlora.merge import task_arithmetic, ties_merge, dare_merge
3724

3825
__all__ = [
3926
# Core
@@ -48,18 +35,6 @@
4835
"init_subspace",
4936
"absorb_task",
5037
"extract_adapter",
51-
# Ops
52-
"compute_svd",
53-
"project_onto_subspace",
54-
"reconstruct_from_subspace",
55-
"gram_schmidt",
56-
"explained_variance_ratio",
57-
"select_num_components",
58-
# NF4 quantization (QLoRA-style)
59-
"NF4_QUANT_TABLE",
60-
"nf4_quantize_dequantize",
61-
"nf4_pack",
62-
"nf4_unpack",
6338
# Analysis
6439
"compute_similarity_matrix",
6540
"find_clusters",
@@ -73,8 +48,6 @@
7348
# Training
7449
"SubspaceTrainer",
7550
"orthogonal_init",
76-
# Incremental
77-
"incremental_svd_update",
7851
# Merging
7952
"task_arithmetic",
8053
"ties_merge",

src/vlora/analysis.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
from __future__ import annotations
44

5+
from typing import TYPE_CHECKING
6+
57
import torch
68
from torch import Tensor
79

810
from vlora.io import LoRAWeights
911
from vlora.ops import project_onto_subspace
1012

11-
from typing import TYPE_CHECKING
12-
1313
if TYPE_CHECKING:
1414
from vlora.subspace import SharedSubspace
1515

src/vlora/cli.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import click
1111

12-
from vlora.io import LoRAWeights, load_adapter, save_adapter
12+
from vlora.io import load_adapter, save_adapter
1313
from vlora.ops import explained_variance_ratio
1414
from vlora.subspace import SharedSubspace
1515

@@ -65,7 +65,7 @@ def info(subspace_path: str, as_json: bool):
6565
click.echo(f" Tasks: {len(sub.tasks)}")
6666

6767
if sub.tasks:
68-
click.echo(f"\n Task IDs:")
68+
click.echo("\n Task IDs:")
6969
for tid in sorted(sub.tasks.keys()):
7070
click.echo(f" - {tid}")
7171

@@ -102,7 +102,7 @@ def compress(adapter_dirs: tuple[str, ...], output: str, num_components: int | N
102102
task_ids.append(path.name)
103103
click.echo(f" Loaded: {path.name}")
104104

105-
click.echo(f" Building subspace...")
105+
click.echo(" Building subspace...")
106106
sub = SharedSubspace.from_adapters(
107107
adapters,
108108
task_ids=task_ids,
@@ -210,7 +210,7 @@ def analyze(adapter_dirs: tuple[str, ...], threshold: float, as_json: bool):
210210
for n in names:
211211
click.echo(f" Loaded: {n}")
212212

213-
click.echo(f"\n Pairwise Cosine Similarity:")
213+
click.echo("\n Pairwise Cosine Similarity:")
214214
header = " " + " " * 20 + " ".join(f"{n[:8]:>8}" for n in names)
215215
click.echo(header)
216216
for i, name in enumerate(names):
@@ -242,7 +242,7 @@ def validate(subspace_path: str):
242242
import torch
243243

244244
sub = SharedSubspace.load(subspace_path)
245-
issues = {"errors": [], "warnings": []}
245+
issues: dict[str, list[str]] = {"errors": [], "warnings": []}
246246

247247
click.echo(f"\n Validating: {subspace_path}")
248248
click.echo(f" Tasks: {len(sub.tasks)}, Layers: {len(sub.layer_names)}, k={sub.num_components}")
@@ -298,7 +298,7 @@ def validate(subspace_path: str):
298298
for warn in issues["warnings"]:
299299
click.echo(f" [WARN] {warn}")
300300
if not issues["errors"] and not issues["warnings"]:
301-
click.echo(f"\n All checks passed.")
301+
click.echo("\n All checks passed.")
302302

303303
click.echo()
304304

@@ -347,7 +347,6 @@ def diff(subspace_path: str, task_a: str, task_b: str):
347347
@click.argument("subspace_path", type=click.Path(exists=True))
348348
def benchmark(subspace_path: str):
349349
"""Benchmark subspace operations: reconstruct, project, absorb."""
350-
import torch
351350

352351
sub = SharedSubspace.load(subspace_path)
353352
task_ids = sorted(sub.tasks.keys())
@@ -418,11 +417,11 @@ def merge(adapter_dirs: tuple[str, ...], output: str, method: str, weights: str
418417

419418
fn = MERGE_METHODS[method]
420419
if method == "ties":
421-
merged = fn(adapters, density=density, weights=parsed_weights)
420+
merged = fn(adapters, density=density, weights=parsed_weights) # type: ignore[operator]
422421
elif method == "dare":
423-
merged = fn(adapters, drop_rate=drop_rate, weights=parsed_weights, seed=seed)
422+
merged = fn(adapters, drop_rate=drop_rate, weights=parsed_weights, seed=seed) # type: ignore[operator]
424423
else:
425-
merged = fn(adapters, weights=parsed_weights)
424+
merged = fn(adapters, weights=parsed_weights) # type: ignore[operator]
426425

427426
save_adapter(merged, output)
428427
click.echo(f" Merged adapter saved to: {output}")

src/vlora/integrations/huggingface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import logging
2828
from typing import Any
2929

30-
import torch
3130
import torch.nn as nn
3231
from torch import Tensor
3332

@@ -101,6 +100,7 @@ def _make_differentiable_hook(self, layer_name: str):
101100
The reconstruction is differentiable — gradients flow from the
102101
loss through the hook back to the loadings parameters.
103102
"""
103+
assert self._trainer is not None
104104
params = self._trainer.params
105105
la = params[f"{layer_name}.loadings_a"]
106106
lb = params[f"{layer_name}.loadings_b"]

src/vlora/merge.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from __future__ import annotations
1414

1515
import logging
16-
from typing import Literal
1716

1817
import torch
1918
from torch import Tensor

0 commit comments

Comments
 (0)