Fine-Tuning with LoRA#
LoRA (Low-Rank Adaptation) lets you serve a base model plus one or more fine-tuned adapters without retraining or rebuilding the full model.
NIM LLM supports two LoRA serving modes:
Static LoRA: Adapters are discovered from a directory at startup.
Dynamic LoRA: Adapters can be loaded and unloaded while the server is running.
Initial LoRA Setup#
Before you configure static LoRA or dynamic LoRA, complete the shared setup in this section. These settings tell NIM LLM where to find adapters and ensure that the deployment uses a LoRA-capable profile.
Configure Adapter Discovery#
Complete the following setup for both static LoRA and dynamic LoRA:
Mount a directory (containing LoRA adapters) to the container.
Set
NIM_PEFT_SOURCEto that directory.Optionally, pass native vLLM LoRA flags.
Expected adapter layout under NIM_PEFT_SOURCE:
/opt/nim/loras/
├── adapter_a/
│ ├── adapter_config.json
│ └── adapter_model.safetensors # or adapter_model.bin
└── adapter_b/
├── adapter_config.json
└── adapter_model.bin
Only valid, readable adapter directories are loaded.
NIM passes the following LoRA-related flags to vLLM:
--enable-lora--max-loras--max-cpu-loras--max-lora-rank
Select a LoRA-Capable Profile#
When available for your model, select a -feat_lora profile so the deployment uses LoRA-compatible runtime settings.
Example:
export NIM_MODEL_PROFILE=vllm-fp16-tp1-pp1-feat_lora
If you are not sure which profiles are available in your deployment, query the model and profile metadata from your environment. Then choose the LoRA-capable profile.
Static LoRA#
After you complete the required LoRA setup, NIM LLM uses static LoRA when NIM_PEFT_REFRESH_INTERVAL is not set. In static LoRA mode, the NIM container discovers adapters in NIM_PEFT_SOURCE during startup and loads valid adapters. If you add, remove, or update adapters after startup, restart the NIM container to apply those changes.
Dynamic LoRA#
Use dynamic LoRA when you need to add or remove adapters without restarting the deployment. You can manage adapters through directory monitoring, runtime API calls, or both.
Configure Dynamic LoRA Updates#
After you complete the shared LoRA setup, set NIM_PEFT_REFRESH_INTERVAL (polling interval in seconds) to enable dynamic LoRA through directory monitoring.
When NIM_PEFT_SOURCE and NIM_PEFT_REFRESH_INTERVAL are set, NIM starts the LoRA watcher and enables runtime LoRA updates for vLLM.
You can also use vLLM runtime endpoints for manual control:
POST /v1/load_lora_adapterPOST /v1/unload_lora_adapter
Load and Unload Adapters at Runtime#
To manage adapters through the directory watcher, use the following actions:
Load: copy a new adapter folder into
NIM_PEFT_SOURCE.Unload: remove an adapter folder from
NIM_PEFT_SOURCE.Wait one refresh interval for
/v1/modelsto reflect changes.
To manage adapters through the manual API, use the following actions:
Load with
POST /v1/load_lora_adapter.Unload with
POST /v1/unload_lora_adapter.
If you use both the watcher and the manual API together, an adapter that the API removes but that is still present in the directory can be reloaded by the watcher during the next scan.
Serve Multiple Adapters#
You can serve multiple adapters at the same time, subject to GPU memory limits.
Use /v1/models to discover available adapter IDs, then send the adapter ID in the request model field.
Code Examples#
The following example shows a minimal local workflow for serving a fine-tuned Llama model with LoRA. It includes shared setup, static and dynamic startup commands, model discovery, and an inference request that targets a loaded adapter.
Serve a Fine-Tuned Llama Model with LoRA#
To serve a fine-tuned Llama model with LoRA, complete the following steps:
Set up the environment variables and create the LoRA adapter directory.
# Common setup export LOCAL_NIM_CACHE=$PWD/.cache mkdir -p "$LOCAL_NIM_CACHE" export NGC_API_KEY=<your_ngc_api_key> export CUDA_VISIBLE_DEVICES=0 export NIM_MODEL_PROFILE=<lora-capable-profile> # Prepare adapters mkdir -p "$PWD/loras"
Start the model by using one of the following options:
Use static LoRA loading.
docker run -it --rm --gpus all \ -v "$LOCAL_NIM_CACHE:/opt/nim/.cache" \ -v "$PWD/loras:/opt/nim/loras" \ -p 8000:8000 \ -e NGC_API_KEY \ -e NIM_MODEL_PROFILE \ -e CUDA_VISIBLE_DEVICES \ -e NIM_PEFT_SOURCE=/opt/nim/loras \ <nim-llm-image>
Use dynamic LoRA loading with the watcher enabled.
docker run -it --rm --gpus all \ -v "$LOCAL_NIM_CACHE:/opt/nim/.cache" \ -v "$PWD/loras:/opt/nim/loras" \ -p 8000:8000 \ -e NGC_API_KEY \ -e NIM_MODEL_PROFILE \ -e CUDA_VISIBLE_DEVICES \ -e NIM_PEFT_SOURCE=/opt/nim/loras \ -e NIM_PEFT_REFRESH_INTERVAL=10 \ <nim-llm-image>
Verify that the models loaded.
curl -s localhost:8000/v1/models | jq
Send an inference request to an adapter.
curl -X POST http://localhost:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "my_lora_adapter", "messages": [{"role": "user", "content": "Hello!"}], "max_tokens": 64 }' | jq
LoRA with NVIDIA Dynamo#
This release documents LoRA with NVIDIA Dynamo in single-container mode. The container runs a Dynamo SmartRouter frontend and one GPU worker.
LoRA in Dynamo mode keeps the same NIM_PEFT_SOURCE static/dynamic experience described above, with one difference in the management plane:
Adapters are served through Dynamo’s native LoRA API —
POST/GET/DELETE/v1/loras— hosted on the worker’s Dynamo system-status server (theNIM_HEALTH_PORTport), not on the frontend.The legacy NIM endpoints
POST /v1/load_lora_adapterandPOST /v1/unload_lora_adapterare not available in Dynamo mode.The frontend’s
/v1/modelsstill lists the base model plus loaded adapters, and you send inference to an adapter by its id in the requestmodelfield as in standard NIM.
Setting NIM_PEFT_SOURCE (and NIM_PEFT_REFRESH_INTERVAL for dynamic mode) starts a watcher that loads and unloads adapters through /v1/loras, so the directory-based workflow is unchanged: drop an adapter folder into NIM_PEFT_SOURCE to load it, remove it to unload.
Adapters appear progressively, not all at once (differs from standard NIM). The worker reports Ready as soon as the base model is loaded; the watcher then loads adapters one at a time through
/v1/loras. So in static mode the adapters do not all show up in/v1/modelsthe instant the worker is Ready — they appear progressively, and the time for the full set to register scales with the number and size of the adapters. Standard NIM (vanilla vLLM) instead loads every adapter as part of engine startup, so all adapters are already in/v1/modelsthe moment the server reports ready. In Dynamo mode, poll/v1/models(or the worker’s/v1/loras) until the expected adapter count appears rather than assuming they are all present at Ready.
Single container#
Run the container in single-container Dynamo mode (NIM_DYNAMO_SINGLE=1) with NIM_PEFT_SOURCE, the same as standard NIM:
docker run -it --rm --gpus all \
-v "$LOCAL_NIM_CACHE:/opt/nim/.cache" \
-v "$PWD/loras:/opt/nim/loras" \
-p 8000:8000 -p 9090:9090 \
-e NGC_API_KEY \
-e NIM_DYNAMO_SINGLE=1 \
-e NIM_PEFT_SOURCE=/opt/nim/loras \
-e NIM_PEFT_REFRESH_INTERVAL=10 \
<dynamo-enabled-nim-image>
OpenAI API and
/v1/modelsare on port 8000 (the frontend).LoRA management (
/v1/loras) and worker health (/v1/health/ready) are on port 9090 (the worker system-status server). 9090 is the default forNIM_HEALTH_PORT, so no override is needed.
Inspect or drive adapters directly:
curl -s localhost:9090/v1/loras # list adapters loaded on the worker
curl -s localhost:8000/v1/models | jq # base + adapters (for inference)
Omit NIM_PEFT_REFRESH_INTERVAL for static (boot-time) loading.
Multi-worker on Kubernetes#
NIM 3.0 does not publish a LoRA DGD recipe or a public multi-worker Kubernetes LoRA procedure. The runtime-management subsection that follows applies only to the single-container deployment shown previously.
Managing Adapters at Runtime#
In a single-container deployment, the worker’s Dynamo system server, not the
frontend, serves the LoRA management API (POST, GET, and DELETE on
/v1/loras). Reach the worker on NIM_HEALTH_PORT, which defaults to 9090.
POST /v1/loras loads an adapter on the worker that receives the request.
# On the single-container worker system port:
curl -s localhost:9090/v1/loras # list loaded adapters
curl -s -X POST localhost:9090/v1/loras \
-H 'Content-Type: application/json' \
-d '{"lora_name":"my-lora","source":{"uri":"file:///opt/nim/loras/my-lora"}}'
curl -s -X DELETE localhost:9090/v1/loras/my-lora # unload
Not supported#
The following configurations are not supported:
LoRA with the SGLang backend in Dynamo mode (vLLM only).
hf://adapter sources, unless the worker runtime registers a Hugging Face LoRA source; uses3://orfile:///otherwise.
Troubleshooting#
Adapter never loads (directory not accessible / cannot read <file> / missing required file)#
Applies to both standard NIM and Dynamo mode. The symptom is a warning that repeats every refresh interval, e.g.:
LoRA adapter '<name>': directory not accessible at <NIM_PEFT_SOURCE>/<name>
and the adapter never appears in /v1/models (nor, in Dynamo mode, in /v1/loras).
This is a file-permission problem. NIM containers run as a non-root user (uid 1000 by default), and the LoRA watcher re-validates every adapter directory under NIM_PEFT_SOURCE on each scan. For NIM’s user it requires:
the adapter directory — read + execute (
r-x), to list and traverse it;adapter_config.json— read (r);the weights file (
adapter_model.safetensorsoradapter_model.bin) — read (r).
If any check fails the adapter is skipped and the warning repeats on every poll.
The usual cause is that the adapters were staged by a different user than NIM runs as — commonly a root process that wrote them owner-only (0700). The NGC CLI in particular forces 0700 regardless of umask. Group ownership alone doesn’t help without the group permission bits.
Fix — make the adapters readable by NIM’s user, then the watcher loads them on the next poll (no restart):
chmod -R go+rX <adapter directory> # dirs -> r-x, files -> r, for group + other
Then stage adapters readable to begin with, so no manual chmod is needed:
Docker: the host directory you bind-mount at
NIM_PEFT_SOURCE, and the adapter files in it, must be readable by uid 1000 — e.g.chmod -R go+rX ./lorasbefore-v "$PWD/loras:/opt/nim/loras".Kubernetes: run the staging job/pod with NIM’s group and a permissive umask —
securityContext: { runAsGroup: 1000, fsGroup: 1000 }plusumask 0022— or append&& chmod -R go+rX <dir>to the download step. Two gotchas:runAsGrouprequires an explicitrunAsUser(userunAsUser: 0for a root-based downloader), andfsGroupwithfsGroupChangePolicy: OnRootMismatchwill not re-chown files added after the volume was first mounted.