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, and parent (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 supports latest‑wins, merge‑average, and manual override strategies.
  • CLI – an argparse‑based interface exposing subcommands: init, add, list, and resolve.

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

  1. Clone the repository (or copy the files) and create a virtual environment:
python -m venv venv
source venv/bin/activate
pip install pytest
  1. Initialize a new version repository:
python cli.py init
  1. Add an adapter (assuming you have a .bin file from Hugging Face PEFT):
python cli.py add my-first-adapter bert-base-uncased ./my_adapter.bin
  1. List stored adapters:
python cli.py list
  1. Resolve conflicts for a given base model:
python cli.py resolve bert-base-uncased
  1. 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

  1. Persistent metadata store – replace the JSON index with SQLite or PostgreSQL to support atomic updates and concurrent writes, enabling safe multi‑process usage.
  2. 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.
  3. Observability – emit structured logs and Prometheus metrics for add/list/resolve latency and conflict frequency, making the system debuggable in production.
  4. Fault tolerance – implement retry logic with exponential backoff for storage operations and use checksums to detect corruption during transfer.
  5. Benchmarking suite – create synthetic workloads that generate thousands of adapters and measure resolution time, providing data to tune merge strategies.
  6. 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


Building something like this? I’m an AI/systems contractor open to new projects. Book a 30-min call.