Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
3e2a34e
fix: migrate from IBM alora to PEFT 0.18.1 native aLoRA
planetf1 Feb 6, 2026
ef974b1
fix: disable MPS on macOS with PyTorch < 2.8 for CPU fallback
planetf1 Feb 6, 2026
bcb3629
fix: restrict MPS patching to macOS only for cross-platform safety
planetf1 Feb 6, 2026
03f2248
Adds a test file -- remove before merging.
nrfulton Feb 7, 2026
28a80a7
Fixes output dirname where a relative path is used.
nrfulton Feb 7, 2026
6b23b48
Some cleanup of the aLoRA example.
nrfulton Feb 8, 2026
81f869d
no readme from model trainer.
nrfulton Feb 8, 2026
7096323
More notes on intrinsics.
nrfulton Feb 8, 2026
c14ce3d
raise Exception instead of giving warning.
nrfulton Feb 9, 2026
bca4b42
idk
nrfulton Feb 9, 2026
2a21bb6
Revert "idk"
nrfulton Feb 9, 2026
f902bb1
for jake to review.
nrfulton Feb 9, 2026
f346a65
Partial changes suggested in https://github.com/generative-computing/…
nrfulton Feb 9, 2026
5b4a576
adds io.yaml. still need to change data.
nrfulton Feb 9, 2026
a5d497a
chore: remove temporary test file
planetf1 Feb 9, 2026
94679a8
fix: add GPU memory check and better error handling
planetf1 Feb 9, 2026
ff0a4cd
feat: add --device flag for explicit device control
planetf1 Feb 9, 2026
1366277
docs: add --device flag to alora documentation
planetf1 Feb 9, 2026
eaace96
Partial work toward upload for intrinsics.
nrfulton Feb 10, 2026
689db12
adds intrinsic uploader.
nrfulton Feb 10, 2026
9cf2771
git
nrfulton Feb 10, 2026
4e7a854
fix default for intrinsic option.
nrfulton Feb 10, 2026
40072bc
Fixes intrinsic uploader.
nrfulton Feb 10, 2026
95cd72e
cleanup intrinsic upload script.
nrfulton Feb 10, 2026
e6e72bc
Creates a new CustomGraniteCommonAdapter and uses that instead of doing
nrfulton Feb 10, 2026
f2260fc
don't double-add to the catalog.
nrfulton Feb 10, 2026
47334e4
Merge branch 'fix/issue-385-peft-migration' of github.com:planetf1/me…
nrfulton Feb 10, 2026
8a7a11e
Merge branch 'main' into fix/issue-385-peft-migration
nrfulton Feb 10, 2026
5577311
improved readme for alora directory.
nrfulton Feb 10, 2026
5bbebb0
uploads alora docs.
nrfulton Feb 10, 2026
f5b5164
Merge branch 'fix/issue-385-peft-migration' of github.com:planetf1/me…
nrfulton Feb 10, 2026
d7bb4bf
reformat to work with intrinsic json stuff.
nrfulton Feb 10, 2026
73ca583
bigger dataset.
nrfulton Feb 10, 2026
77a29a5
Merge branch 'main' into fix/issue-385-peft-migration
nrfulton Feb 10, 2026
a6cb409
improving the stembolt example.
nrfulton Feb 11, 2026
0f0aa46
This is claude-generate code.
nrfulton Feb 11, 2026
935df38
Extract add-readme as standalone `m alora add-readme` subcommand
nrfulton Feb 11, 2026
0d5b8d6
cleanup from the readme generator.
nrfulton Feb 11, 2026
caad891
Refactor readme_generator to use Instruct + RejectionSampling
nrfulton Feb 11, 2026
d44d57e
Fix two bugs in README_TEMPLATE.jinja for intrinsic adapters
nrfulton Feb 11, 2026
7a7639e
list requirements that fail.
nrfulton Feb 11, 2026
da3ff16
fix some slop
nrfulton Feb 11, 2026
1f1a94e
finishes readme work, I think.
nrfulton Feb 11, 2026
73f53ea
precommit hooks are passing again.
nrfulton Feb 11, 2026
a8a4e9b
Merge branch 'fix/issue-385-peft-migration' of github.com:planetf1/me…
nrfulton Feb 11, 2026
c9a70b5
Add documentation for the readme generator to tutorial and aLora example
nrfulton Feb 11, 2026
d6b8fe8
cleans up slop
nrfulton Feb 11, 2026
ddaea57
Fix __main__ block in generated READMEs to use actual sample values
nrfulton Feb 11, 2026
da0c6d1
formatting
nrfulton Feb 11, 2026
eb24c12
Resolves conflicts -- preferring old models in alora code because I'm…
nrfulton Feb 11, 2026
18ce5c2
Adds hints.
nrfulton Feb 11, 2026
d88ecf3
skip ALoRA examples.
nrfulton Feb 11, 2026
e806ceb
Guard README deletion by a conditional.
nrfulton Feb 11, 2026
731e27e
fix: resolve issues with aloras
jakelorocco Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions cli/alora/README_TEMPLATE.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
library_name: peft
base_model: {{ base_model }}
tags:
- mellea
---

# {{ adapter_name }} Intrinsic Adapter

{{ high_level_description }}

## Training Data

{{ dataset_description }}

### Examples

**Input examples:**
{% for sample in samples %}
- {{ sample.input }}
{% endfor %}

**Output examples:**
{% for sample in samples %}
- {{ sample.output }}
{% endfor %}

## How to Use

To use this intrinsic adapter in your application, you need to define an adapter class
and an intrinsic component. The adapter connects to the HuggingFace model repository
where the trained weights are stored, and the intrinsic wraps it as a composable Mellea
component that can be used with any compatible backend.

Make sure you have Mellea installed (`uv pip install mellea`), then use the code below to
integrate this intrinsic into your application.

```python
import mellea.stdlib.functional as mfuncs
from mellea.core import Context
from mellea.backends import Backend
from mellea.stdlib.components.simple import SimpleComponent
from mellea.backends.adapters import AdapterMixin
from mellea.backends.adapters.adapter import CustomGraniteCommonAdapter
from mellea.stdlib.components.intrinsic import Intrinsic


_INTRINSIC_MODEL_ID = "{{ userid }}/{{ intrinsic_name }}"
_INTRINSIC_ADAPTER_NAME = "{{ intrinsic_name }}"


class {{ intrinsic_name_camelcase }}Adapter(CustomGraniteCommonAdapter):
def __init__(self, base_model_name: str):
super().__init__(
model_id=_INTRINSIC_MODEL_ID,
intrinsic_name=_INTRINSIC_ADAPTER_NAME,
base_model_name=base_model_name,
)


class {{ intrinsic_name_camelcase }}Intrinsic(Intrinsic, SimpleComponent):
def __init__(self, {{ arglist }}):
Intrinsic.__init__(self, intrinsic_name=_INTRINSIC_ADAPTER_NAME)
SimpleComponent.__init__(self, {{ arglist_as_kwargs }})

def format_for_llm(self):
return SimpleComponent.format_for_llm(self)

async def async_{{ intrinsic_name }}({{ arglist }}, ctx: Context, backend: Backend | AdapterMixin):
# Backend.add_adapter should be idempotent, but we'll go ahead and check just in case.
if adapter.qualified_name not in backend.list_adapters():
backend.add_adapter({{ intrinsic_name_camelcase }}Adapter(backend.base_model_name))
action = {{ intrinsic_name_camelcase }}Intrinsic("{{ intrinsic_name }}", {{ arglist_without_type_annotations }})
mot = await backend.generate_from_context(action, ctx)
return mot


def {{ intrinsic_name }}({{ arglist }}, ctx: Context, backend: Backend | AdapterMixin):
# Backend.add_adapter should be idempotent, but we'll go ahead and check just in case.
adapter = {{ intrinsic_name_camelcase }}Adapter(backend.base_model_name)
if adapter.qualified_name not in backend.list_adapters():
backend.add_adapter(adapter)
action = {{ intrinsic_name_camelcase }}Intrinsic({{ arglist_without_type_annotations }})
return mfuncs.act(action, ctx, backend)

if __name__ == "__main__":
from mellea.backends.huggingface import LocalHFBackend
from mellea.backends.model_ids import IBM_GRANITE_4_MICRO_3B
from mellea.stdlib.context import ChatContext
backend = LocalHFBackend(IBM_GRANITE_4_MICRO_3B)
result, ctx = {{ intrinsic_name }}({{ example_call_kwargs }}, ctx=ChatContext(), backend=backend)
print(result.value)
```
112 changes: 110 additions & 2 deletions cli/alora/commands.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import json
import os
import tempfile

import typer

alora_app = typer.Typer(
Expand All @@ -11,6 +15,7 @@ def alora_train(
outfile: str = typer.Option(..., help="Path to save adapter weights"),
promptfile: str = typer.Option(None, help="Path to load the prompt format file"),
adapter: str = typer.Option("alora", help="Adapter type: alora or lora"),
device: str = typer.Option("auto", help="Device: auto, cpu, cuda, or mps"),
epochs: int = typer.Option(6, help="Number of training epochs"),
learning_rate: float = typer.Option(6e-6, help="Learning rate"),
batch_size: int = typer.Option(2, help="Per-device batch size"),
Expand All @@ -25,6 +30,7 @@ def alora_train(
base_model=basemodel,
output_file=outfile,
adapter=adapter,
device=device,
epochs=epochs,
learning_rate=learning_rate,
batch_size=batch_size,
Expand All @@ -35,16 +41,118 @@ def alora_train(


def alora_upload(
weightfile: str = typer.Argument(..., help="Path to saved adapter weights"),
weight_path: str = typer.Argument(..., help="Path to saved adapter weights"),
name: str = typer.Option(
..., help="Destination model name (e.g., acme/carbchecker-alora)"
),
intrinsic: bool = typer.Option(
default=False,
help="Formats model upload using granite-intrinsic. An io.yaml file must be provided.",
),
io_yaml: str = typer.Option(
default=None,
help="Location of a granite-common io.yaml file. See https://nfulton.org/blog/alora_io_yaml.html",
),
):
"""Upload trained adapter to remote model registry."""
from cli.alora.intrinsic_uploader import upload_intrinsic
from cli.alora.upload import upload_model

upload_model(weight_path=weightfile, model_name=name)
assert not intrinsic or io_yaml, (
"If --intrinsic is set then you must provide an io.yaml"
)

# Change the structure of the repo so that it's an intrinsic.
if intrinsic:
# get the base model and adapter type from the adapter config file.
with open(os.path.join(weight_path, "adapter_config.json")) as fh:
config = json.load(fh)
assert "base_model_name_or_path" in config.keys(), (
"All adapter config files should have a base_model_name_or_path."
)
base_model = config["base_model_name_or_path"]
adapter_type = "alora" if "alora_invocation_tokens" in config else "lora"

assert adapter_type in ["lora", "alora"]
upload_intrinsic(
weight_path=weight_path,
model_name=name,
base_model=base_model,
type=adapter_type, # type: ignore
io_yaml=io_yaml,
)
else:
upload_model(weight_path=weight_path, model_name=name)

print("✅ Upload complete!")


def alora_add_readme(
datafile: str = typer.Argument(..., help="JSONL file with item/label pairs"),
basemodel: str = typer.Option(..., help="Base model ID or path"),
promptfile: str = typer.Option(None, help="Path to load the prompt format file"),
name: str = typer.Option(
..., help="Destination model name (e.g., acme/carbchecker-alora)"
),
hints: str = typer.Option(
default=None, help="File containing any additional hints."
),
io_yaml: str = typer.Option(
default=None,
help="Location of a granite-common io.yaml file. See https://nfulton.org/blog/alora_io_yaml.html",
),
):
"""Generate and upload an INTRINSIC_README.md for a trained adapter."""
from huggingface_hub import HfFolder, create_repo, upload_file

from cli.alora.readme_generator import generate_readme

with tempfile.TemporaryDirectory() as tmp_dir:
readme_path = os.path.join(tmp_dir, "README.md")
generate_readme(
dataset_path=datafile,
base_model=basemodel,
prompt_file=promptfile,
output_path=readme_path,
name=name,
hints=open(hints).read() if hints is not None else None,
)

print(open(readme_path).read())
continue_answer: str | None = None
while continue_answer is None or continue_answer not in ["yes", "no"]:
if continue_answer is not None:
print("Please answer with only 'yes' or 'no'.")
answer = input(
f"\nWe auto-generated a README using Mellea. Should we upload this README to {name} (yes/no)? "
)
continue_answer = answer.strip().lower()
if continue_answer == "no":
print("ABORTING.")
import sys

sys.exit(-1)
else:
assert continue_answer == "yes"

token = HfFolder.get_token()
if token is None:
raise OSError(
"Hugging Face token not found. Run `huggingface-cli login` first."
)

create_repo(repo_id=name, token=token, private=True, exist_ok=True)
upload_file(
path_or_fileobj=readme_path,
path_in_repo="README.md",
repo_id=name,
commit_message="Upload intrinsic README.",
token=token,
)

print(f"README uploaded to {name}")


alora_app.command("train")(alora_train)
alora_app.command("upload")(alora_upload)
alora_app.command("add-readme")(alora_add_readme)
81 changes: 81 additions & 0 deletions cli/alora/intrinsic_uploader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import os
import shutil
import tempfile
from typing import Literal

import git
from huggingface_hub import HfFolder, RepoUrl, create_repo, upload_file, upload_folder


def upload_intrinsic(
weight_path: str,
model_name: str,
base_model: str,
type: Literal["lora", "alora"],
io_yaml: str,
private: bool = True,
):
try:
assert os.path.exists(weight_path)
assert os.path.exists(io_yaml)
assert private, "not implemented."

token = HfFolder.get_token()
if token is None:
raise OSError(
"Hugging Face token not found. Run `huggingface-cli login` first."
)

_url: RepoUrl = create_repo(
repo_id=model_name, token=token, private=private, exist_ok=True
)
hf_path = _url.url
print(hf_path)

temp_dir = tempfile.mkdtemp()

# repo = git.Repo.clone_from(hf_path, temp_dir)

# use granite-3.3-2b-instruct if the base model is granite-3.3-2b-instruct
# use granite-3.3-2b-instruct if the base model is ibm-granite/granite-3.3-2b-instruct
assert len(base_model.split("/")) <= 2
base_model_path = (
base_model if "/" not in base_model else base_model.split("/")[1]
)

# Create directory structure: intrinsic_name / base_model_path / adapter_type
target_dir = os.path.join(temp_dir, model_name, base_model_path, type)
os.makedirs(target_dir, exist_ok=True)

# Copy the io_yaml file to the target directory
shutil.copy2(io_yaml, weight_path)

# Copy the model files to the target directory.
if "README.md" in os.listdir(weight_path):
os.remove(os.path.join(weight_path, "README.md"))
shutil.copytree(weight_path, target_dir, dirs_exist_ok=True)

# Commit and push changes
assert len(model_name.split("/")) == 2
intrinsic_name = model_name.split("/")[1]
upload_folder(
repo_id=model_name,
folder_path=target_dir,
path_in_repo=os.path.join(intrinsic_name, base_model_path, type),
commit_message="Upload adapter weights as intrinsic.",
token=token,
)

# Upload INTRINSIC_README.md as the repo root README.md if it exists.
readme_path = os.path.join(weight_path, "INTRINSIC_README.md")
if os.path.exists(readme_path):
upload_file(
path_or_fileobj=readme_path,
path_in_repo="README.md",
repo_id=model_name,
commit_message="Upload intrinsic README.",
token=token,
)
finally:
# Clean up temporary directory
shutil.rmtree(temp_dir)
Loading