# Tutorial 502: Convert to PEFT LoRA Adapter > **Prerequisites** > > - [Weights Management](https://tinker-docs.thinkingmachines.ai/tutorials/core-concepts/weights/index.md) > **Run it interactively [[source]](https://github.com/thinking-machines-lab/tinker-cookbook/blob/main/tutorials/502_lora_adapter.py)** > > ```bash > curl -O https://raw.githubusercontent.com/thinking-machines-lab/tinker-cookbook/main/tutorials/502_lora_adapter.py && marimo edit 502_lora_adapter.py > ``` Instead of merging LoRA weights into the base model, you can export a **standalone PEFT adapter**. This is the preferred approach for serving with vLLM or SGLang, where you keep one base model and hot-swap lightweight adapters. **PEFT format vs merged:** | | Merged model | PEFT adapter | | ----------------- | ---------------------- | -------------------------------------------------- | | **Size** | Full model (GBs) | Just the LoRA matrices (MBs) | | **Deployment** | Load like any HF model | Load base model + attach adapter | | **Multi-adapter** | One model per adapter | One base + many adapters | | **Use with** | Any framework | vLLM `--lora-modules`, SGLang `--lora-paths`, PEFT | ## Setup: create a checkpoint First we need a Tinker checkpoint to export. We create a training client, run one step of SFT, and save the weights. ```python api_key = mo.ui.text(kind="password", label="Paste your Tinker API key") api_key # noqa: B018 ``` ```python import os import tinker from tinker_cookbook import renderers from tinker_cookbook.supervised.data import conversation_to_datum from tinker_cookbook.tokenizer_utils import get_tokenizer mo.stop( "TINKER_API_KEY" not in os.environ and not api_key.value, "Paste your API key above", ) if api_key.value: os.environ["TINKER_API_KEY"] = api_key.value BASE_MODEL = "Qwen/Qwen3.5-4B" service_client = tinker.ServiceClient() training_client = await service_client.create_lora_training_client_async( base_model=BASE_MODEL, rank=16 ) _tokenizer = get_tokenizer(BASE_MODEL) _renderer = renderers.get_renderer("qwen3_5_disable_thinking", _tokenizer) _messages = [ {"role": "user", "content": "What is Tinker?"}, {"role": "assistant", "content": "Tinker is a cloud training API for LLM fine-tuning."}, ] _datum = conversation_to_datum(_messages, _renderer, max_length=512) _fwd = await training_client.forward_backward_async([_datum], loss_fn="cross_entropy") _opt = await training_client.optim_step_async(tinker.AdamParams(learning_rate=1e-4)) await _fwd.result_async() await _opt.result_async() _save_result = training_client.save_weights_for_sampler(name="adapter-tutorial") sampler_path = _save_result.result().path print(f"Base model: {BASE_MODEL}") print(f"Checkpoint: {sampler_path}") ``` **Output** ```text Base model: Qwen/Qwen3.5-4B Checkpoint: tinker://aa0139d7-a065-5b83-a583-071863da9f71:train:0/sampler_weights/adapter-tutorial ``` ## Step 1: Download the checkpoint Use `weights.download()` to fetch a Tinker checkpoint to local disk. ```python from tinker_cookbook import weights adapter_dir = weights.download( tinker_path=sampler_path, output_dir="/tmp/tinker-tutorials/lora-adapter/adapter", ) print(f"Adapter downloaded to: {adapter_dir}") ``` **Output** ```text Adapter downloaded to: /tmp/tinker-tutorials/lora-adapter/adapter ``` ## Step 2: Convert to PEFT format `build_lora_adapter` remaps Tinker's internal adapter keys to match the HuggingFace model's parameter names (which serving frameworks expect). No base model weights are downloaded or merged -- this is a lightweight operation. ```python PEFT_OUTPUT = "/tmp/tinker-tutorials/lora-adapter/peft_adapter" weights.build_lora_adapter( base_model=BASE_MODEL, adapter_path=adapter_dir, output_path=PEFT_OUTPUT, ) print(f"PEFT adapter saved to: {PEFT_OUTPUT}") ``` **Output** ```text PEFT adapter saved to: /tmp/tinker-tutorials/lora-adapter/peft_adapter ``` ## Step 3: Inspect the output The PEFT adapter directory contains just two files: - `adapter_config.json` -- metadata (base model, rank, alpha, target modules) - `adapter_model.safetensors` -- the LoRA weight matrices ```python import json for f in sorted(os.listdir(PEFT_OUTPUT)): size_mb = os.path.getsize(os.path.join(PEFT_OUTPUT, f)) / 1e6 print(f" {f:40s} {size_mb:>8.2f} MB") # Show the adapter config with open(os.path.join(PEFT_OUTPUT, "adapter_config.json")) as fh: config = json.load(fh) print("\nadapter_config.json:") print(json.dumps(config, indent=2)) ``` **Output** ```text adapter_config.json 0.00 MB adapter_model.safetensors 145.89 MB adapter_config.json: { "peft_type": "LORA", "auto_mapping": null, "base_model_name_or_path": "Qwen/Qwen3.5-4B", "bias": "none", "fan_in_fan_out": false, "inference_mode": true, "init_lora_weights": true, "lora_alpha": 32, "lora_dropout": 0.0, "modules_to_save": null, "r": 16, "rank_pattern": {}, "alpha_pattern": {}, "target_modules": [ "down_proj", "embed_tokens", "gate_proj", "in_proj_k", "in_proj_q", "in_proj_v", "in_proj_z", "k_proj", "o_proj", "out_proj", "q_proj", "up_proj", "v_proj" ], "task_type": "CAUSAL_LM" } ``` ## Loading the adapter **With PEFT / transformers:** ```python from peft import PeftModel from transformers import AutoModelForCausalLM base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3.5-4B") model = PeftModel.from_pretrained(base, "./peft_adapter") ``` **With vLLM (multi-adapter serving):** ```bash vllm serve Qwen/Qwen3.5-4B \ --lora-modules my_adapter=./peft_adapter ``` **With SGLang:** ```bash python -m sglang.launch_server \ --model Qwen/Qwen3.5-4B \ --lora-paths my_adapter=./peft_adapter ``` ## Next steps - **[Export a Merged HuggingFace Model](https://tinker-docs.thinkingmachines.ai/tutorials/deployment/export-hf/index.md)** -- Merge LoRA into a standalone model - **[Publish to HuggingFace Hub](https://tinker-docs.thinkingmachines.ai/tutorials/deployment/publish-hub/index.md)** -- Upload the adapter with a custom model card