Build adapter from recipe file¶
Recipe file: small JSON plus .npy arrays it names. astro-weights build turns recipe and base model into PEFT adapter; astro-weights export writes GGUF LoRA and model card from that adapter. Anyone with recipe and base model can rebuild published adapter and compare. Core install only; GGUF needs gguf extra.
Write recipe¶
Tiny stand-in base model (one safetensors file) and two-step recipe:
from pathlib import Path
import numpy as np
from safetensors.numpy import save_file
from astro_weights.edits import save_recipe
rng = np.random.default_rng(0)
module = "model.layers.1.mlp.down_proj"
# tiny stand-in for a base model directory
Path("base").mkdir(exist_ok=True)
W0 = rng.standard_normal((16, 48)).astype(np.float32)
save_file({module + ".weight": W0}, "base/model.safetensors")
# recipe plus the arrays it names, in one directory
Path("recipe").mkdir(exist_ok=True)
np.save("recipe/v.npy", rng.standard_normal(16).astype(np.float32))
np.save("recipe/b.npy", rng.standard_normal(16).astype(np.float32))
np.save("recipe/a.npy", rng.standard_normal(48).astype(np.float32))
save_recipe(
"recipe/recipe.json",
{
"format": 1,
"base_model": "org/tiny",
"steps": [
{
"recipe": "project_out",
"module": module,
"params": {"strength": 0.7},
"arrays": {"v": "v.npy"},
},
{
"recipe": "rank1",
"module": "model.layers.2.mlp.down_proj",
"arrays": {"b": "b.npy", "a": "a.npy"},
},
],
},
)
save_recipe writes JSON with sorted keys and trailing newline.
Build, inspect, export¶
astro-weights build recipe/recipe.json --base base --out my-adapter --card apache-2.0
astro-weights inspect my-adapter
astro-weights export my-adapter --gguf my-adapter.gguf --arch qwen35
Prints:
model.layers.1.mlp.down_proj 16x48 rank 1
model.layers.2.mlp.down_proj 16x48 rank 1
wrote my-adapter
base model: org/tiny
rank: 1
modules: 2
module out x in |B|_F |A|_F |BA|_F
model.layers.1.mlp.down_proj 16x48 0.7 6.2 4.34
model.layers.2.mlp.down_proj 16x48 2.99 4.52 13.5
manifest: ok
wrote my-adapter.gguf
my-adapter/ holds adapter_config.json, adapter_model.safetensors, astro_manifest.json and README.md (from --card). Base has no weight for layers.2; rank1 does not read base weight, so build still works. Flags and exit codes: CLI reference.
Recipe format¶
| Key | Value |
|---|---|
format |
1 |
base_model |
non-empty string; goes into adapter |
steps |
non-empty list; one step per module |
steps[].recipe |
recipe name, table below |
steps[].module |
module name, e.g. model.layers.1.mlp.down_proj |
steps[].params |
scalars, exact names below; floats finite |
steps[].arrays |
array name to .npy file name; plain file name in recipe directory, no subdirectory |
Only these recipes allowed in a step. Same names as keyword arguments of functions in astro_weights.edits.
recipe |
params |
arrays |
reads base weight |
|---|---|---|---|
rank1 |
none | b, a |
no |
masked_rank1 |
alpha |
u, mask |
yes |
column_shift |
n_in (int) |
shift, idx, coef |
no |
project_out |
strength |
v |
yes |
scale_column |
j (int), s |
none | yes |
scale_head |
head (int), n_heads (int), s |
none | yes |
zero_head |
head (int), n_heads (int) |
none | yes |
- Unknown or missing key in
paramsorarrays:ValueError. Second step on same module:ValueError. .npyloads withallow_pickle=False; path resolving outside recipe directory:ValueError.- Recipe from someone else is untrusted input: size caps, no pickle, no code run; full list in SECURITY.md.
- Built edit keeps recipe plus sha256 of every array (
meta["recipe"]["arrays_sha256"]) inastro_manifest.json.
Base weights¶
build reads <module>.weight from --base directory: follows model.safetensors.index.json when present, else reads model.safetensors. read_base_weight(model_dir, key) does same in Python; read_tensor(file, key) reads one tensor from one file; tensor_sha256(file, key) hashes raw stored bytes. F32, F16 and BF16 only, returned as float32. numpy only, no torch. Any problem (missing key, unsupported dtype, truncated file, unsafe shard name in index): ValueError.
Same in Python¶
build_edit plus save_peft do what build does. Continues example above:
from astro_weights.edits import build_edit, load_recipe
from astro_weights.export import read_base_weight, read_peft, save_peft
recipe = load_recipe("recipe/recipe.json") # validates; arrays not loaded yet
edit = build_edit(
recipe,
lambda m: read_base_weight("base", m + ".weight"),
"recipe", # directory the .npy names resolve against
)
print(sorted(edit.meta["recipe"]["arrays_sha256"]))
save_peft(edit, "rebuilt")
rebuilt, problems = read_peft("rebuilt")
print(
problems,
rebuilt.meta["recipe"]["arrays_sha256"] == edit.meta["recipe"]["arrays_sha256"],
)
Prints:
To check shipped adapter against recipe, compare edit.meta["recipe"]["arrays_sha256"] with same field from read_peft("my-adapter").