首页 / 文章 / 在单个GPU上运行两种大语言模型工作负载:vLLM LoRA、智能体以及Azure ML微调功能

在单个GPU上运行两种大语言模型工作负载:vLLM LoRA、智能体以及Azure ML微调功能

通过同一张卡处理交互式和批量代理流量,同时利用需求导向的LoRA、ClickHouse分析功能、Blob存储对象,以及通过MLflow进行跟踪的Unsloth训练功能。

1917 词

核心挑战:两种工作负载、一个模型、一份预算

许多团队需要在同一套微调后的模型系列上实现交互式智能体聊天与批量分析功能,但GPU预算仅允许使用一张显卡。该架构利用Azure上的单张GPU同时支持两种大语言模型工作负载:一种是通过vLLM加载按需使用的LoRA适配器以实现低延迟推理;另一种则是将结构化事件写入ClickHouse的智能体分析路径,训练与微调则在Azure ML上借助Unsloth和MLflow完成。

推理层:vLLM + 按需LoRA

vLLM负责承载基础模型,而LoRA适配器则按需加载,这样无需为每种特殊功能(如服务语气控制、分析数据提取、工具调用等)都准备完整的模型副本。需保持适配器大小较小,否则当过多适配器频繁切换时会导致延迟急剧上升。

vllm serve your-org/base-vlm-7b \
  --enable-lora \
  --max-lora-rank 64 \
  --lora-modules domain-adapter=/data/lora-adapters/current/ \
  --host 0.0.0.0 \
  --port 8000 \
  --gpu-memory-utilization 0.90 \
  --max-model-len 8192
payload = {
    "model": "domain-adapter",
    "messages": [
        {"role": "user", "content": [
            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}},
            {"type": "text", "text": f"Analyse this for category: {category}, location: {label}"}
        ]}
    ],
    "max_tokens": 1024
}
response = requests.post(VLLM_API_URL, json=payload, timeout=30,
                          proxies={"http": None, "https": None})
from agents.extensions.models.litellm_model import LitellmModel

model = LitellmModel(
    model="openai/base-vlm-7b",
    base_url="http://10.x.x.x:8000/v1",
    api_key="not-needed"
)
from agents import Agent, Runner, function_tool as tool

@tool
def get_dashboard_snapshot_tool() -> Dict[str, Any]:
    """Get all KPIs in one call. Call this FIRST for any summary question."""
    with SessionLocal() as db:
        return jsonable_encoder(analytics_service.get_dashboard_snapshot(db))

@tool
def get_entity_ranking_tool(top: bool = True) -> List[Dict[str, Any]]:
    """Get ranked entity performance list. top=True for best, False for worst."""
    with SessionLocal() as db:
        return jsonable_encoder(analytics_service.get_performance_ranking(db, top))

overview_agent = Agent(
    name="OverviewAgent",
    instructions="Handle general dashboard and summary questions. Always call get_dashboard_snapshot_tool first.",
    tools=[get_dashboard_snapshot_tool, get_entity_ranking_tool],
    model=model,
)

status_agent = Agent(
    name="StatusAgent",
    instructions="Handle live status and stream/field health questions.",
    tools=[get_stream_status_tool, get_field_status_tool],
    model=model,
)

quality_agent = Agent(
    name="QualityAgent",
    instructions="Handle rejection-rate and data-quality questions.",
    tools=[get_rejection_stats_tool],
    model=model,
)

orchestrator = Agent(
    name="AnalyticsOrchestrator",
    instructions=(
        "Handle all user communication. Route each question to the right specialist tool "
        "and synthesize its result into a plain-text answer. Do not hallucinate metrics; "
        "only report what a specialist returns."
    ),
    tools=[
        overview_agent.as_tool(
            tool_name="overview_expert",
            tool_description="Answers general dashboard and summary questions.",
        ),
        status_agent.as_tool(
            tool_name="status_expert",
            tool_description="Answers live status and stream/field health questions.",
        ),
        quality_agent.as_tool(
            tool_name="quality_expert",
            tool_description="Answers rejection-rate and data-quality questions.",
        ),
    ],
)
CREATE TABLE events_queue (
    event_id UUID,
    entity_id UInt64,
    event_type LowCardinality(String),
    payload String,
    created_at DateTime64(3)
) ENGINE = Kafka
SETTINGS
    kafka_broker_list = 'your-namespace.servicebus.windows.net:9093',
    kafka_topic_list = 'app-events',
    kafka_group_name = 'clickhouse-consumer',
    kafka_format = 'JSONEachRow';

CREATE MATERIALIZED VIEW events_mv TO events AS
SELECT * FROM events_queue;
azureblob://
├── ml-artifacts/
│   ├── lora-adapters/{v1, v2, v3}
│   ├── base-models/base-vlm-7b/
│   └── eval-results/v3/
├── training-data/{raw, processed, annotations}
└── inference-inputs/{year}/{month}/{day}/{entity_id}/{request_id}.bin
import great_expectations as gx

context = gx.get_context()
batch = context.sources.pandas_default.read_json("annotations_batch.jsonl")
suite = context.get_expectation_suite("annotation_schema_v1")
results = context.run_validation_operator(
    "action_list_operator",
    assets_to_validate=[batch],
    run_id="training-v4-ingestion"
)
if not results["success"]:
    raise ValueError(f"Data validation failed: {results['statistics']}")
import mlflow
from unsloth import FastVisionModel

mlflow.set_experiment("domain-lora-finetuning")
with mlflow.start_run(run_name=f"lora-v{adapter_version}") as run:
    model, tokenizer = FastVisionModel.from_pretrained(
        "unsloth/base-vlm-7b",
        load_in_4bit=True,
    )
    model = FastVisionModel.get_peft_model(
        model,
        r=64,
        lora_alpha=128,
        finetune_vision_layers=True,
    )
    mlflow.log_params({
        "base_model": "base-vlm-7b",
        "lora_rank": 64,
        "lora_alpha": 128,
        "epochs": 3,
        "dataset_version": "v4",
    })
    for epoch in range(epochs):
        train_loss = train_one_epoch(model, dataloader)
        mlflow.log_metric("train_loss", train_loss, step=epoch)
    metrics = evaluate(model, eval_dataloader)
    mlflow.log_metrics({"eval_f1": metrics["f1"]})
    mlflow.log_artifacts(output_dir, artifact_path="lora-adapter")
    mlflow.set_tag("promotion_status", "candidate")
# azure-ml-lora-job.yml
$schema: https://azuremlschemas.azureedge.net/latest/commandJob.schema.json
type: command
code: ./training/
command: >
  python finetune_lora_unsloth.py
  --dataset ${{inputs.training_data}}
  --output-dir ${{outputs.lora_adapter}}
  --lora-rank 64 --lora-alpha 128 --epochs 3
inputs:
  training_data:
    type: uri_folder
    path: azureml://datastores/training_blob/paths/domain/v4/
outputs:
  lora_adapter:
    type: uri_folder
    path: azureml://datastores/artifacts_blob/paths/lora-adapters/v4/
environment: azureml:vlm-unsloth-env:1
compute: azureml:gpu-cluster-nc24ads   # single A100, sufficient for Unsloth LoRA fine-tuning
experiment_name: domain-lora-finetuning
from azure.ai.ml.entities import Model

model = Model(
    name="domain-lora-adapter",
    version="4",
    path="azureml://datastores/artifacts_blob/paths/lora-adapters/v4/",
    tags={
        "base_model": "base-vlm-7b",
        "lora_rank": "64",
        "eval_f1": "0.93",   # illustrative
        "promotion_status": "candidate",
    }
)
ml_client.models.create_or_update(model)
def run_eval(adapter_version: str, eval_dataset_version: str):
    candidate = evaluate_adapter(adapter_path=..., eval_data=...)
    production = evaluate_adapter(adapter_path=get_production_adapter_path(), eval_data=...)
    passed = (
        candidate["f1"] >= production["f1"] - 0.01
        and candidate["f1"] >= MIN_F1_THRESHOLD
      )
    return passed, candidate
New annotation batch → Great Expectations validation
    → dataset versioned in Azure ML
    → Azure ML training job (Unsloth, single GPU, spot instance)
    → offline eval vs. production, per-category
    → [pass] tag adapter "production" in registry
    → sync adapter to local disk on the vLLM host
    → rolling restart of vLLM with new --lora-modules path
# .gitlab-ci.yml
stages:
  - validate
  - train
  - eval
  - deploy

validate-data:
  stage: validate
  script:
    - python eval/validate_dataset.py --version $DATASET_VERSION
submit-training:
  stage: train
  needs: [validate-data]
  script:
    - az ml job create --file azure-ml-lora-job.yml
      --set inputs.dataset_version=$DATASET_VERSION
      --workspace-name $AML_WORKSPACE --resource-group $RESOURCE_GROUP
run-eval:
  stage: eval
  needs: [submit-training]
  script:
    - python eval/run_eval.py --version $DATASET_VERSION
    - python scripts/promote_adapter.py --version $DATASET_VERSION
deploy:
  stage: deploy
  needs: [run-eval]
  when: manual
  script:
    - az containerapp update --name vllm-server --resource-group $RESOURCE_GROUP
      --set-env-vars LORA_ADAPTER_VERSION=$DATASET_VERSION

智能体层:三个专用智能体与一个协调器

职责分工:

  1. 路由器/协调器 —— 分类意图并选择工具
  2. 检索专家 —— 获取领域相关上下文
  3. 分析专家 —— 生成结构化数据以供存储

由于使用共享的GPU进行推理,必须严格控制并发数量并设置队列机制,以避免交互式聊天被批量智能体处理所阻塞。

分析后端:ClickHouse

对产品分析至关重要的代理输出数据会存储在ClickHouse中:包括延迟、工具选择、检索到的文档ID以及用户最终结果。与将所有数据都通过OLTP数据库处理相比,列式存储更适用于高基数事件流。

Azure Blob Storage:MLOps的粘合层

检查点、数据集、适配器相关文件以及评估报告都存储在Blob中。训练任务会在此读写数据;推理阶段则根据版本加载已批准的适配器。应将Blob路径视为训练端与服务端之间的API契约的一部分。

训练流程:Azure ML上的Unsloth

利用Azure ML的计算资源,通过Unsloth进行微调以提高速度和内存效率。使用MLflow记录训练过程:包括超参数、评估指标以及相关文件的URI。只有那些在固定评估集上表现优于基准值的适配器才能被采用。

训练前的数据验证

在GPU时间耗尽之前,需验证模式结构、个人身份信息清除情况、类别分布均衡性以及训练集与评估集之间的数据泄露问题。一旦出现验证错误即立即终止流程。

使用Unsloth进行微调,并通过MLflow进行跟踪

将适配器权重以不可更改的版本号形式存储到Blob中。推理配置会明确引用这些版本号——若没有指定固定版本,则生产环境中不可使用“最新版本”。

安全地处理两种类型的工作负载

  • 为交互式任务和批量任务设置独立的队列
  • 为每个适配器设定并发上限
  • 预热默认适配器,仅在需要时加载专用适配器
  • 将GPU使用率和队列深度数据发送至同一个ClickHouse系统(或指标收集平台)
  • 通过调整配置指针来回滚适配器,而非重新部署整个虚拟机

总结要点

如果能够妥善处理LoRA按需加载、队列隔离以及MLOps相关规范,那么一个GPU就可以支持两个产品界面。对于那些暂时无力组建多个推理集群,但又需要特定功能与分析能力的团队而言,vLLM + Unsloth + Azure ML + Blob + ClickHouse构成了一种实用的MLOps解决方案。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理token数。按需加载LoRA并非免费服务——如果产品流量不考虑队列优先级,加载成本和内存碎片化问题可能会抵消“单个GPU”带来的优势。应设定适配器的最大数量限制,当交互式服务水平指标出现偏差时,应优先处理批量任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一个GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批处理任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一个GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批处理任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一个GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批处理任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一个GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批处理任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一台GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批量任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一台GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批量任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一个GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批处理任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一个GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批处理任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一台GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批量任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一台GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批量任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一个GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批量任务。

容量规划提示:在并发交互负载下,需测量使用一个适配器与N个适配器时的每秒处理令牌数。Demand LoRA并非免费方案——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用一个GPU”所带来的优势。应为同时运行的适配器设定严格上限,一旦交互式服务水平指标出现偏差,应优先处理批量任务。

容量规划提示:在并发交互负载下,使用一个适配器与N个适配器时的令牌/秒数需进行测量。Demand LoRA并非免费服务——如果产品流量不考虑队列类别,加载成本和内存碎片化可能会抵消“使用单个GPU”所带来的优势。应对同时运行的适配器数量设定严格限制,一旦交互式服务水平指标出现偏差,应优先处理批量任务。