参数高效微调#

参数高效微调 (PEFT) 方法能够有效地将大型预训练模型适配到新任务。NVIDIA NIM for LLMs (NIM for LLMs) 支持由 NeMo 框架和 Hugging Face Transformers 库训练的 LoRA PEFT 适配器。当向 NIM 提交推理请求时,服务器支持动态多 LoRA 推理,从而能够使用不同的 LoRA 模型同时进行推理请求。

以下框图说明了使用 NIM 的动态多 LoRA 架构

NIM In-flight LoRA Diagram

  1. 使用 NVIDIA NeMo 框架或 Hugging Face PEFT 库训练的适配器被放置到适配器存储中,并被赋予唯一的名称。

  2. 当向 NIM 发出请求时,客户端可以通过包含 LoRA 模型名称来指定他们想要的特定自定义。

  3. 当 NIM 收到自定义模型的请求时,它会从适配器存储中将关联的适配器拉取到多层缓存中。一些适配器驻留在 GPU 内存中,而一些适配器驻留在主机内存中,这取决于它们最近的使用频率。

  4. 在执行期间,NIM 运行专门的 GPU 内核,使数据能够同时流经基础模型和多个不同的低秩适配器。这项技术使 NIM 能够同时响应对多个不同自定义模型的需求。

LoRA 设置概述#

您可以使用环境变量中定义的配置来扩展 NIM 以服务 LoRA 模型。您使用的底层 NIM 必须与 LoRA 的基础模型相匹配。例如,要设置与 llama3-8b-instruct 兼容的 LoRA 适配器,例如 llama3-8b-instruct-lora:hf-math-v1,请配置 nvcr.io/nim/meta/llama3-8b-instruct NIM。以下部分描述了配置 NIM 以服务兼容 LoRA 的过程。

LoRA 适配器#

从 NGC 或 Hugging Face 下载 LoRA 适配器,或使用您自己的自定义 LoRA 适配器。LoRA 适配器必须存储在单独的目录中,并且一个或多个 LoRA 目录位于 LOCAL_PEFT_DIRECTORY 目录中。加载的 LoRA 适配器的名称必须与适配器目录的名称匹配。NIM for LLMs 支持 NeMo 格式和 Hugging Face Transformers 兼容格式。

NeMo 格式#

NeMo 格式的 LoRA 目录必须包含一个带有 .nemo 扩展名的文件。.nemo 文件的名称不需要与其父目录的名称匹配。支持的目标模块为 ["gate_proj", "o_proj", "up_proj", "down_proj", "k_proj", "q_proj", "v_proj", "attention_qkv"]

Hugging Face Transformers 格式#

支持使用 Hugging Face Transformers 训练的 LoRA 适配器。LoRA 必须包含一个 adapter_config.json 文件和 {adapter_model.safetensors, adapter_model.bin} 文件之一。NIM 支持的目标模块为 ["gate_proj", "o_proj", "up_proj", "down_proj", "k_proj", "q_proj", "v_proj"]

LoRA 模型目录结构#

用于存储一个或多个 LoRA 的目录(您的 LOCAL_PEFT_DIRECTORY)应按照以下示例进行组织。在此示例中,loras 是您作为 LOCAL_PEFT_DIRECTORY 的值传递到 docker 容器中的目录名称。然后,加载的 LoRA 将被称为 llama3-8b-mathllama3-8b-math-hfllama3-8b-squadllama3-8b-squad-hf

loras
├── llama3-8b-math
│   └── llama3_8b_math.nemo
├── llama3-8b-math-hf
│   ├── adapter_config.json
│   └── adapter_model.bin
├── llama3-8b-squad
│   └── squad.nemo
└── llama3-8b-squad-hf
    ├── adapter_config.json
    └── adapter_model.safetensors

获取 LoRA 模型#

您可以从模型注册表下载预训练的适配器,或使用流行的框架(如 Hugging Face Transformers 和 NVIDIA NeMo)微调自定义适配器,以便与 NIM for LLMs 一起使用。请注意,LoRA 模型权重与特定的基础模型相关联。您必须仅部署使用与 NIM 正在服务的基础模型相同的模型进行调整的 LoRA 模型。

从 NGC 下载 LoRA 适配器#

llama3-8b-instruct 的 LoRA 适配器#

export LOCAL_PEFT_DIRECTORY=~/loras
mkdir $LOCAL_PEFT_DIRECTORY
pushd $LOCAL_PEFT_DIRECTORY

# downloading NeMo-format loras
ngc registry model download-version "nim/meta/llama3-8b-instruct-lora:nemo-math-v1"
ngc registry model download-version "nim/meta/llama3-8b-instruct-lora:nemo-squad-v1"

# downloading vLLM-format loras
ngc registry model download-version "nim/meta/llama3-8b-instruct-lora:hf-math-v1"
ngc registry model download-version "nim/meta/llama3-8b-instruct-lora:hf-squad-v1"

popd

chmod -R 777 $LOCAL_PEFT_DIRECTORY

llama3-70b-instruct 的 LoRA 适配器#

export LOCAL_PEFT_DIRECTORY=~/loras
mkdir $LOCAL_PEFT_DIRECTORY
pushd $LOCAL_PEFT_DIRECTORY

# downloading NeMo-format loras
ngc registry model download-version "nim/meta/llama3-70b-instruct-lora:nemo-math-v1"
ngc registry model download-version "nim/meta/llama3-70b-instruct-lora:nemo-squad-v1"

# downloading vLLM-format loras
ngc registry model download-version "nim/meta/llama3-70b-instruct-lora:hf-math-v1"
ngc registry model download-version "nim/meta/llama3-70b-instruct-lora:hf-squad-v1"

popd

chmod -R 777 $LOCAL_PEFT_DIRECTORY

从 Hugging Face Hub 下载 LoRA 适配器#

如果您没有 huggingface-cli CLI 工具,请使用 pip install -U "huggingface_hub[cli]" 安装它。

export LOCAL_PEFT_DIRECTORY=~/loras
mkdir $LOCAL_PEFT_DIRECTORY

# download a LoRA from Hugging Face Hub
mkdir $LOCAL_PEFT_DIRECTORY/llama3-lora
huggingface-cli download <Hugging Face LoRA name> adapter_config.json adapter_model.safetensors --local-dir $LOCAL_PEFT_DIRECTORY/llama3-lora

chmod -R 777 $LOCAL_PEFT_DIRECTORY

使用您自己的自定义 LoRA 适配器#

如果您正在使用本地训练的自定义 LoRA 适配器,请创建一个 $LOCAL_PEFT_DIRECTORY 目录并将您的 LoRA 适配器复制到该目录。您的自定义 LoRA 适配器的名称必须遵循上一节中描述的命名约定。

从 NGC 下载 LoRA 示例中的 LoRA 模块使用 NeMo 框架来训练 LoRA 适配器。使用 NeMo 训练框架来自定义适配器瓶颈维度并指定应用 LoRA 的目标模块。LoRA 可以应用于 transformer 模型中的任何线性层,包括

  • Q、K、V 注意力投影

  • 注意力输出层

  • 两个 transformer MLP 层中的一个或两个

对于 QKV 投影,NeMo 的注意力实现将 QKV 融合到单个投影中,因此 LoRA 实现为组合的 QKV 学习单个低秩投影。

以下示例使用已训练并在 local_lora_path 存在的自定义 LoRA 适配器设置 NIM 的 PEFT 目录。

export LOCAL_PEFT_DIRECTORY=~/loras
mkdir $LOCAL_PEFT_DIRECTORY

# move a custom LoRA adapter to the local PEFT directory
cp local_lora_path $LOCAL_PEFT_DIRECTORY/llama3-lora

chmod -R 777 $LOCAL_PEFT_DIRECTORY

处理混合批次请求#

一个批次中的请求可能使用不同的 LoRA 适配器来支持不同的任务。因此,传统的 通用矩阵乘法 (GEMM) 不能用于一起计算所有请求。逐个顺序计算它们会导致显着的额外开销。为了解决这个问题,我们使用 NVIDIA CUTLASS 实现了批处理 GEMM,以将批处理的异构请求处理融合到单个内核中。这提高了 GPU 利用率和性能。

PEFT 环境变量#

您可以通过设置 NIM_PEFT_SOURCE 环境变量在 NIM for LLMs 中启用 PEFT。有关更多信息,请参见 环境变量

PEFT 缓存和动态混合批次 LoRA (Multi-LoRA)#

LoRA 推理由三个级别的 PEFT (LoRA) 存储和优化的内核组成,用于混合批次 LoRA 推理。

  1. PEFT 源。由 NIM_PEFT_SOURCE 配置,这是一个目录,其中存储了特定模型的所有服务的 LoRA。必须设置此环境变量才能使 PEFT LoRA 与 NIM 一起运行。可以存储在此处的 LoRA 数量没有限制。有关目录布局以及支持的格式和模块的详细信息,请参见 LoRA 模型目录结构。NIM for LLMs 在启动时在 NIM_PEFT_SOURCE 中搜索 LoRA。

    PEFT 源动态刷新。如果您设置 NIM_PEFT_REFRESH_INTERVAL,NIM for LLMs 会每 NIM_PEFT_REFRESH_INTERVAL 秒检查一次 LOCAL_PEFT_DIRECTORY,并添加它找到的任何新 LoRA。如果您添加一个新的 LoRA 适配器,例如 new-loraLOCAL_PEFT_DIRECTORY,则 new-lora 现在将在 NIM_PEFT_SOURCE 中。如果 NIM_PEFT_REFRESH_INTERVAL 设置为 10,则 NIM for LLMs 每 10 秒检查一次 NIM_PEFT_SOURCE 以查找新模型。在下一个刷新间隔,NIM for LLMs 检测到 “new-lora” 不在现有的 LoRA 列表中,并将其添加到可用模型列表中。如果您在 NIM_PEFT_REFRESH_INTERVAL 秒后检查 /v1/models,您将在模型列表中看到 “new-lora”。NIM_PEFT_REFRESH_INTERVAL 的默认值为 None,这意味着一旦在启动时从 NIM_PEFT_SOURCE 添加了 LoRA,NIM for LLMs 将不再检查 NIM_PEFT_SOURCE,如果您希望将新的 LoRA 添加到 LOCAL_PEFT_DIRECTORY 并使其显示在可用模型列表中,则必须重新启动服务。

  2. CPU PEFT 缓存。此缓存将 NIM_PEFT_SOURCE 中的 LoRA 子集保存在主机内存中。当为该 LoRA 发出请求时,LoRA 将加载到 CPU 缓存中。为了加快后续请求,LoRA 将保存在 CPU 内存中,直到缓存已满。当需要更多空间来存放不在缓存中的 LoRA 时,最近最少使用的 LoRA 将被删除。通过设置 NIM_MAX_CPU_LORAS 环境变量来指定 CPU PEFT 缓存中 LoRA 的最大数量。此环境变量确定可以保存在 CPU 内存中的 LoRA 的最大数量。CPU 缓存的大小也是所有活动请求中可能存在的不同 LoRA 数量的上限。如果活动 LoRA 的数量超过缓存的容量,则服务将返回 429,指示缓存已满,您应该减少活动 LoRA 的数量。

  3. GPU PEFT 缓存。此缓存通常保存 CPU PEFT 缓存中的 LoRA 子集。这是保存 LoRA 以进行推理的地方。LoRA 在计划执行时动态加载到 GPU 缓存中。与 CPU 缓存一样,只要有空间,LoRA 就会保留在 GPU 缓存中,并且首先删除最近最少使用的 LoRA。GPU 缓存的大小通过设置 NIM_MAX_GPU_LORAS 环境变量来配置。GPU 缓存中可以容纳的 LoRA 数量是可以在同一批次中执行的 LoRA 数量的上限。请注意,更大的数字会导致更高的内存使用率。

CPU 和 GPU 缓存都将根据 NIM_MAX_CPU_LORASNIM_MAX_GPU_LORASNIM_MAX_LORA_RANK 进行预分配。NIM_MAX_LORA_RANK 设置最大支持的低秩(适配器大小)。

PEFT 缓存内存需求#

缓存 LoRA 所需的内存由您希望缓存的 LoRA 的秩和数量决定。LoRA 的大小大致为 low_rank * inner_dim * num_modules * num_layers,其中 inner_dim 是您正在适配的层的隐藏维度,num_modules 是您每层适配的模块数量(例如,qkv 张量)。请注意,inner_dim 可能因模块而异。

TensorRT-LLM 后端: 缓存预先分配了足够的内存,以便所有 LoRA 都具有 NIM_MAX_LORA_RANK。LoRA 不需要具有相同的秩,如果在推理时使用秩较低的 LoRA,则将有超过指定的 NIM_MAX_GPU_LORASNIM_MAX_CPU_LORAS 的 LoRA 适合缓存。例如,如果 GPU 缓存配置为 8 个秩为 64 的 LoRA,则 NIM for LLMs 可以运行一批 32 个秩为 16 的 LoRA。

除了权重的缓存之外,TensorRT-LLM 引擎还为 LoRA 激活预先分配了额外的内存。所需的空间与 max_batch_size * max_lora_rank 成比例缩放。NIM for LLMs 会自动估计启动时激活和 PEFT 缓存所需的内存,并为键值缓存保留剩余内存。

启动带有 PEFT 的 NIM for LLMs#

本节包括在 LLama 3.1 8B Instruct 上微调的 LoRA 的设置说明。为 LLama 3.1 8B Instruct 设置 LoRA 的工作流程是类似的。请注意,Llama3 70B 的更大的底层模型尺寸会导致其 LoRA 版本具有更大的内存需求。请参见 PEFT 缓存内存需求

导出所有非默认环境变量,然后运行服务器。如果您使用来自 NGC 下载的四个模型,那么您将有一个基础模型和四个 LoRA 可用于推理。有关 --gpus all 的说明,请参阅 GPU 选择 部分。

export LOCAL_PEFT_DIRECTORY=~/loras
mkdir $LOCAL_PEFT_DIRECTORY
export NIM_PEFT_SOURCE=/home/nvs/loras
export NIM_PEFT_REFRESH_INTERVAL=3600   # will check NIM_PEFT_SOURCE for newly added models every hour
export CONTAINER_NAME=llama-3.1-8b-instruct

export NIM_CACHE_PATH=~/nim-cache
mkdir -p "$NIM_CACHE_PATH"
chmod -R 777 $NIM_CACHE_PATH

docker run -it --rm --name=$CONTAINER_NAME \
    --runtime=nvidia \
    --gpus all \
    --shm-size=16GB \
    -e NGC_API_KEY=$NGC_API_KEY \
    -e NIM_PEFT_SOURCE \
    -e NIM_PEFT_REFRESH_INTERVAL \
    -v $NIM_CACHE_PATH:/opt/nim/.cache \
    -v $LOCAL_PEFT_DIRECTORY:$NIM_PEFT_SOURCE \
    -p 8000:8000 \
    nvcr.io/nim/meta/llama-3.1-8b-instruct:latest

运行 Multi-LoRA 推理#

可以通过运行以下命令找到可用于推理的模型列表

curl -X GET 'http://0.0.0.0:8000/v1/models'

为了使输出更易于阅读,请将 curl 命令的结果通过管道传输到像 jqpython -m json.tool 这样的工具中。例如:curl -s http://0.0.0.0:8000/v1/models | jq

输出

{
  "object": "list",
  "data": [
    {
      "id": "meta/llama3-8b-instruct",
      "object": "model",
      "created": 1715702314,
      "owned_by": "vllm",
      "root": "meta/llama3-8b-instruct",
      "parent": null,
      "permission": [
        {
          "id": "modelperm-8d8a74889cfb423c97b1002a0f0a0fa1",
          "object": "model_permission",
          "created": 1715702314,
          "allow_create_engine": false,
          "allow_sampling": true,
          "allow_logprobs": true,
          "allow_search_indices": false,
          "allow_view": true,
          "allow_fine_tuning": false,
          "organization": "*",
          "group": null,
          "is_blocking": false
        }
      ]
    },
    {
      "id": "llama3-8b-instruct-lora_vnemo-math-v1",
      "object": "model",
      "created": 1715702314,
      "owned_by": "vllm",
      "root": "meta/llama3-8b-instruct",
      "parent": null,
      "permission": [
        {
          "id": "modelperm-7c9916a6ba414093a6befe6e28937a34",
          "object": "model_permission",
          "created": 1715702314,
          "allow_create_engine": false,
          "allow_sampling": true,
          "allow_logprobs": true,
          "allow_search_indices": false,
          "allow_view": true,
          "allow_fine_tuning": false,
          "organization": "*",
          "group": null,
          "is_blocking": false
        }
      ]
    },
    {
      "id": "llama3-8b-instruct-lora_vhf-math-v1",
      "object": "model",
      "created": 1715702314,
      "owned_by": "vllm",
      "root": "meta/llama3-8b-instruct",
      "parent": null,
      "permission": [
        {
          "id": "modelperm-e88bf7b1b63e4a35b831e17e0b98cb67",
          "object": "model_permission",
          "created": 1715702314,
          "allow_create_engine": false,
          "allow_sampling": true,
          "allow_logprobs": true,
          "allow_search_indices": false,
          "allow_view": true,
          "allow_fine_tuning": false,
          "organization": "*",
          "group": null,
          "is_blocking": false
        }
      ]
    },
    {
      "id": "llama3-8b-instruct-lora_vnemo-squad-v1",
      "object": "model",
      "created": 1715702314,
      "owned_by": "vllm",
      "root": "meta/llama3-8b-instruct",
      "parent": null,
      "permission": [
        {
          "id": "modelperm-fbfcfd4e59974a0bad146d7ddda23f45",
          "object": "model_permission",
          "created": 1715702314,
          "allow_create_engine": false,
          "allow_sampling": true,
          "allow_logprobs": true,
          "allow_search_indices": false,
          "allow_view": true,
          "allow_fine_tuning": false,
          "organization": "*",
          "group": null,
          "is_blocking": false
        }
      ]
    },
    {
      "id": "llama3-8b-instruct-hf-squad-v1",
      "object": "model",
      "created": 1715702314,
      "owned_by": "vllm",
      "root": "meta/llama3-8b-instruct",
      "parent": null,
      "permission": [
        {
          "id": "modelperm-7a5509ab60f94e78b0433e7740b05934",
          "object": "model_permission",
          "created": 1715702314,
          "allow_create_engine": false,
          "allow_sampling": true,
          "allow_logprobs": true,
          "allow_search_indices": false,
          "allow_view": true,
          "allow_fine_tuning": false,
          "organization": "*",
          "group": null,
          "is_blocking": false
        }
      ]
    }
  ]
}

接下来,针对基础模型或任何 LoRA 提交完成或聊天完成推理请求。您可以向 /v1/models 返回的任何和所有模型发出推理请求。首次向 LoRA 适配器发出推理请求时,可能会有加载时间,但是后续对同一 LoRA 适配器的请求将具有更低的延迟,因为权重是从缓存中流式传输的。

curl -X 'POST' \
  'http://0.0.0.0:8000/v1/completions' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "llama3-8b-instruct-lora_vhf-math-v1",
    "prompt": "John buys 10 packs of magic cards. Each pack has 20 cards and 1/4 of those cards are uncommon. How many uncommon cards did he get?",
    "max_tokens": 128
  }'

这将产生以下输出

{
  "id": "cmpl-7996e1f532804a278535a632906bae07",
  "object": "text_completion",
  "created": 1715664944,
  "model": "llama3-8b-instruct-lora_vhf-math-v1",
  "choices": [
    {
      "index": 0,
      "text": " (total) 10*20= <<10*20=200>>200\n200*1/4=<<200*1/4=50>>50\n50 of John's cards are uncommon cards.\n#### 50",
      "logprobs": null,
      "finish_reason": "stop",
      "stop_reason": null
    }
  ],
  "usage": {
    "prompt_tokens": 35,
    "total_tokens": 82,
    "completion_tokens": 47
  }
}