Inicio / Artículos / Dos cargas de trabajo de LLM en una GPU: vLLM LoRA, agentes y ajuste fino de Azure ML

Dos cargas de trabajo de LLM en una GPU: vLLM LoRA, agentes y ajuste fino de Azure ML

Permite gestionar el tráfico de agentes interactivos y por lotes desde una sola tarjeta, con LoRA según la demanda, análisis en ClickHouse, artefactos en formato Blob y entrenamiento con Unsloth, todo ello monitoreado mediante MLflow.

1917 palabras

El desafío principal: dos cargas de trabajo, un modelo, un presupuesto

Muchos equipos necesitan tanto chat con agentes interactivos como análisis por lotes utilizando la misma familia de modelos afinados, pero el presupuesto para GPUs solo permite una tarjeta. Esta arquitectura permite manejar dos cargas de trabajo con LLM desde una sola GPU en Azure: un camino de inferencia de baja latencia con adaptadores LoRA cargados según la demanda a través de vLLM, y un camino de análisis basado en agentes que escribe eventos estructurados en ClickHouse, con entrenamiento y afinación en Azure ML utilizando Unsloth y MLflow.

La capa de inferencia: vLLM + LoRA a demanda

vLLM alberga el modelo base. Los adaptadores LoRA se cargan según sea necesario, de modo que los comportamientos especializados (tono de soporte, extracción de análisis, variantes de llamada a herramientas) no requieran una réplica completa cada uno. Mantenga los tamaños de los adaptadores pequeños; aparecen picos de latencia cuando se rotan demasiados adaptadores.

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

La capa de agentes: tres agentes especializados, un orquestador

División de responsabilidades:

  1. Ruteador/orquestador — clasifica la intención y elige las herramientas
  2. Especialista en recuperación de información — obtiene el contexto del dominio
  3. Especialista en análisis — emite datos estructurados para su almacenamiento

La inferencia mediante GPU compartida exige límites cuidadosos de concurrencia y gestión de colas para que el chat interactivo no se vea afectado por los procesos en lotes de los agentes.

El backend de análisis: ClickHouse

Los datos generados por los agentes que son importantes para el análisis de productos se almacenan en ClickHouse: latencia, elección de herramientas, IDs de documentos recuperados y resultados de los usuarios. El almacenamiento columnar se adapta mejor a flujos de eventos con alta cardinalidad que forzar todo a través de la base de datos OLTP.

Azure Blob Storage: la capa de unión en MLOps

Los puntos de control, conjuntos de datos, artefactos de adaptadores y reportes de evaluación se almacenan en Blob. Los trabajos de entrenamiento leen/escriben aquí; la inferencia carga los adaptadores aprobados según su versión. Considere las rutas de blob como parte del contrato API entre el entrenamiento y la prestación de servicios.

El pipeline de entrenamiento: Unsloth en Azure ML

Ajuste los parámetros con Unsloth para lograr mayor velocidad y eficiencia de memoria en la computación de Azure ML. Haga seguimiento de las ejecuciones en MLflow: hiperparámetros, métricas de evaluación y URI de los artefactos. Solo promueva aquellos adaptadores que superen un umbral en un conjunto de evaluación fijo.

Validación de datos antes del entrenamiento

Valide el esquema, la eliminación de datos PII, el equilibrio entre clases y las fugas de información entre los conjuntos de entrenamiento y evaluación antes de que se agoten los minutos de la GPU. Se detiene el proceso ante errores en la validación.

Afinamiento con Unsloth, registrado en MLflow

Registre los pesos del adaptador en Blob con IDs de versión inmutables. La configuración de inferencia hace referencia explícitamente a esos IDs; no se debe usar la versión “más reciente” en producción sin un identificador fijo.

Servir ambos tipos de cargas de trabajo de forma segura

  • Colas separadas para procesos interactivos y por lotes
  • Límites de concurrencia por adaptador
  • Calentar un adaptador predeterminado; cargar especializados de forma lenta
  • Publicar los datos de utilización de la GPU y la profundidad de la cola en el mismo ClickHouse (o sistema de métricas)
  • Retroceder a una versión anterior del adaptador cambiando un puntero de configuración, y no reinstalando toda la VM

Conclusiones

Una GPU puede albergar dos interfaces de producto si se prioriza la carga dinámica con LoRA, la isolación de colas y la disciplina en los artefactos MLOps. vLLM + Unsloth + Azure ML + Blob + ClickHouse constituyen una solución práctica de MLOps para equipos que aún no pueden permitirse flotas de inferencia duplicadas pero que necesitan funcionalidades y análisis especializados.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. La carga dinámica con LoRA no es gratuita; el costo de la carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto en el número de adaptadores simultáneos y elimine primero los trabajos por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de carga y la fragmentación de memoria pueden anular los ahorros que ofrece “una GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para los adaptadores simultáneos y descarte primero el trabajo por lotes cuando se incumplan los SLOs interactivos.

Nota de planificación de capacidad: mida los tokens por segundo con un adaptador frente a N adaptadores bajo carga interactiva concurrente. Demand LoRA no es gratuito; el costo de procesamiento y la fragmentación de memoria pueden anular los ahorros que ofrece “una sola GPU” si el tráfico del producto ignora las clases de cola. Establezca un límite estricto para el número de adaptadores simultáneos y elimine primero los trabajos por lotes cuando se incumplan los objetivos de rendimiento interactivos.