Skip to main content
Version: Next

LoRA and aLoRA adapters

Off-the-shelf language models sometimes fail on domain-specific tasks — particularly requirement validation over proprietary terminology or specialized classification schemes not well-represented in general training data. Mellea lets you train a LoRA or aLoRA adapter on your own labeled dataset and use it as a requirement validator in any Mellea program.

Prerequisites: pip install "mellea[cli,hf]". Training requires a GPU or Apple Silicon Mac with sufficient VRAM for the chosen base model. Uploading requires a Hugging Face account.

Backend note: Custom-trained adapters can only be loaded into LocalHFBackend. They do not work with Ollama, OpenAI, or other remote backends.

Granite Switch models ship with pre-trained adapter functions embedded in the model weights, which can be used via OpenAIBackend with load_embedded_adapters=True. See Adapter functions for details.

LoRA vs aLoRA

Both adapter types fine-tune a base model on your data. The difference is inference cost:

LoRAaLoRA
Inference overheadProcesses full context each callActivated at a single token — minimal overhead
Best forGeneral fine-tuningFast inner-loop checks, requirement validation
Training timeSimilarSimilar

For requirement validation in Mellea (short binary checks inside a generation loop), aLoRA is the better choice. Use --adapter lora if you need a more general fine-tune and can absorb the inference cost.

Data format

Training data is a .jsonl file with one JSON object per line. Each object must have:

  • item — the input text to classify
  • label — the string classification label
{"item": "Observed black soot on intake. Seal seems compromised under thermal load.", "label": "piston_rings"}
{"item": "Rotor misalignment caused torsion on connecting rod. High vibration at 3100 RPM.", "label": "connecting_rod"}
{"item": "Combustion misfire traced to a cracked mini-carburetor flange.", "label": "mini_carburetor"}
{"item": "Stembolt makes a whistling sound and does not complete the sealing process.", "label": "no_failure"}

Labels can be any strings. The adapter learns to predict the label from the item text.

Train an adapter

m alora train data.jsonl \
--basemodel ibm-granite/granite-3.2-8b-instruct \
--outfile ./checkpoints/my_adapter \
--adapter alora \
--epochs 6 \
--learning-rate 6e-6 \
--batch-size 2 \
--max-length 1024 \
--grad-accum 4

The trained adapter weights are saved to ./checkpoints/my_adapter/.

Parameters

FlagTypeDefaultDescription
datafilestrrequiredPath to .jsonl training file
--basemodelstrrequiredHugging Face model ID or local path
--outfilestrrequiredDirectory to save adapter weights
--adapterstraloraAdapter type: alora or lora
--devicestrautoDevice: auto, cpu, cuda, or mps
--epochsint6Number of training epochs
--learning-ratefloat6e-6Learning rate
--batch-sizeint2Per-device batch size
--max-lengthint1024Max tokenized sequence length
--grad-accumint4Gradient accumulation steps
--promptfilestrNoneJSON file overriding the invocation prompt

The default invocation prompt is <|start_of_role|>check_requirement<|end_of_role|>. Provide --promptfile only if your adapter needs a different prompt format. The file must contain {"invocation_prompt": "..."}.

Upload to Hugging Face

huggingface-cli login # one-time setup

m alora upload ./checkpoints/my_adapter \
--name your-org/my-adapter

This creates the Hugging Face repository if it does not exist and uploads the adapter weights. Requires HF_TOKEN set or a prior huggingface-cli login.

Warning: Before uploading to a public repository, review whether your training data includes proprietary, confidential, or personal information. Language models can memorize details from small domain-specific datasets.

If you intend to use the adapter as a Mellea adapter function (so that it can be loaded by model ID rather than local path), pass --intrinsic and provide an io.yaml file:

m alora upload ./checkpoints/my_adapter \
--name your-org/my-adapter \
--intrinsic \
--io-yaml ./io.yaml

Use the adapter in Mellea

Load the trained adapter into a LocalHFBackend using CustomIntrinsicAdapter.

Note: CustomIntrinsicAdapter is deprecated in favor of the Adapter / WeightsBinding model, but is still the only working way to load a locally-trained custom adapter — Adapter's weight-loading is not yet implemented. This example will move to Adapter once that lands; see #1144.

from mellea.backends.huggingface import LocalHFBackend
from mellea.backends.adapters.adapter import CustomIntrinsicAdapter
from mellea.stdlib.context import ChatContext
from mellea import MelleaSession
from mellea.stdlib.requirements import ALoraRequirement

backend = LocalHFBackend(model_id="ibm-granite/granite-3.2-8b-instruct")

adapter = CustomIntrinsicAdapter(
model_id="your-org/my-adapter", # HF repo ID or local checkpoint path
base_model_name="granite-3.2-8b-instruct",
intrinsic_name="custom-failure-check",
)
backend.add_adapter(adapter)

m = MelleaSession(backend, ctx=ChatContext())

failure_check = ALoraRequirement(
"The failure mode must not be 'no_failure'.",
intrinsic_name="custom-failure-check",
)
result = m.instruct(
"Write a triage summary based on this technician note: {{note}}",
user_variables={"note": "High vibration at 3100 RPM, connecting rod suspected."},
requirements=[failure_check],
)
print(str(result))
# Output will vary — LLM responses depend on model and temperature.

Note: CustomIntrinsicAdapter emits an advisory UserWarning because custom capability names are not part of Mellea's built-in capability registry. The adapter is still registered for routing.

ALoraRequirement routes validation through the adapter with the matching intrinsic_name. Create CustomIntrinsicAdapter before the requirement: its compatibility shim registers the custom name, which lets ALoraRequirement resolve it. The adapter runs at the check_requirement prompt position. Its io.yaml must transform the output into the {"requirement_check": {"score": <float>}} response schema; label-only adapter output is not compatible with ALoraRequirement.

How automatic routing works

When an adapter is loaded via backend.add_adapter(), Mellea automatically routes req() validation calls through it rather than falling back to LLM-as-a-judge. The rule is: use the most specific available method. In practice this means the aLoRA adapter is preferred whenever one is loaded, with three exceptions:

  1. backend.default_to_constraint_checking_alora is set to False — the adapter is loaded but routing is suppressed for the entire backend instance.
  2. The requirement uses the LLMaJRequirement subtype explicitly — the caller is asking for LLM-as-a-judge regardless of what adapters are loaded.
  3. The adapter is unavailable (e.g. cannot be loaded) — Mellea falls back to LLM-as-a-judge automatically. This is the only fallback case: if the adapter runs but its output fails schema validation, validate() does not fall back. Instead it surfaces the schema error on ValidationResult.error and fails the check closed (bool(result) is False), so callers can tell an unparsable adapter response apart from an ordinary "requirement not met".

If you want to force the adapter path even when using generate_from_context directly (bypassing the normal validate() call), use ALoraRequirement from mellea.stdlib.requirements — this bypasses default_to_constraint_checking_alora, but still requires a matching adapter to actually be registered. If none is found, Mellea logs a warning and falls back to regular generation rather than erroring.

Disable adapter validation

To run without adapter validation (for benchmarking or debugging):

backend.default_to_constraint_checking_alora = False

Set it back to True to re-enable. This flag is per-backend instance and does not affect other sessions.

See also: Adapter functions | The Requirements System | Write Custom Verifiers | CLI Reference