Zwei LLM-Arbeitslasten auf einer GPU: vLLM LoRA, Agenten und Azure ML-Fine-Tuning
Bieten Sie interaktiven und batchbasierten Agentenverkehr von einer einzigen Karte an – mit Demand LoRA, ClickHouse-Analysen, Blob-Artefakten sowie Unsloth-Training, das in MLflow erfasst wird.
Die zentrale Herausforderung: Zwei Arbeitslasten, ein Modell, ein Budget
Viele Teams benötigen sowohl interaktive Agenten-Chat-Funktionen als auch Batch-Analysen mit derselben Familie feinabgestimmter Modelle – doch das GPU-Budget erlaubt nur eine Karte. Diese Architektur bedient zwei LLM-Arbeitslasten mit einer einzigen GPU in Azure: einen Weg für Inferenz mit niedriger Latenz, bei dem über vLLM nach Bedarf LoRA-Adapter geladen werden, sowie einen Weg für agentbasierte Analysen, bei dem strukturierte Ereignisse in ClickHouse geschrieben werden, wobei das Training und Feinabstimmen über Azure ML mit Unsloth und MLflow erfolgt.
Die Inferenzschicht: vLLM + nach Bedarf geladene LoRA-Adapter
vLLM enthält das Basismodell. LoRA-Adapter werden nach Bedarf geladen, sodass spezialisierte Funktionalitäten (Support-Ton, Analyseextraktion, verschiedene Tool-Aufrufe) nicht jeweils eine vollständige Kopie des Modells erfordern. Halten Sie die Größe der Adapter klein; zu viele wechselnde Adapter führen aufgrund von Überlastung zu Latenzsprüngen.
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
Die Agentenschicht: drei spezialisierte Agenten, ein Orchesterator
Geteilte Verantwortlichkeiten:
- Router / Orchesterator – klassifiziert die Absicht und wählt Werkzeuge aus
- Retrieval-Spezialist – holt den Domänenkontext ab
- Analytics-Spezialist – liefert strukturierte Fakten für die Speicherung
Durch gemeinsame GPU-Inferenz sind sorgfältige Konkurrenzbeschränkungen sowie Queuing erforderlich, damit interaktive Chats nicht durch Batch-Agents-Operationen beeinträchtigt werden.
Der Analytics-Backend: ClickHouse
Die für die Produktanalyse wichtigen Ausgaben des Agents landen in ClickHouse: Latenz, Werkzeugauswahlen, abgerufene Dokument-ID-Listen sowie Nutzerergebnisse. Die spaltenbasierte Speicherung eignet sich besser für Ereignisströme mit hoher Kardinalität als das Durchleiten aller Daten über eine OLTP-Datenbank.
Azure Blob Storage: die Verbindungsstelle für MLOps
Checkpointe, Datensätze, Adapter-Dateien sowie Bewertungsberichte werden in Blob Storage gespeichert. Trainingsaufgaben lesen und schreiben hier; die Inferenz-Funktionen laden die genehmigten Adapter nach Version herunter. Betrachten Sie die Blob-Pfade als Teil des API-Vertrags zwischen dem Training und der Bereitstellung.
Der Trainingspipeline: Unsloth auf Azure ML
Tun Sie mit Unsloth Feinabstimmungen, um auf den Rechenressourcen von Azure ML Geschwindigkeit und Speichereffizienz zu erreichen. Verfolgen Sie die Laufvorgänge in MLflow: Hyperparameter, Bewertungsmetriken sowie URI-Adressen der Dateien. Promovieren Sie nur solche Adapter, die auf einem festgelegten Bewertungssatz eine bessere Leistung als der Baseline erbringen.
Datenauswertung vor dem Training
Überprüfen Sie das Schema, die Entfernung sensibler personenbezogener Daten, das Gleichgewicht der Klassen sowie mögliche Durchsickernisse zwischen Trainings- und Evaluierungsdaten, bevor GPU-Zeit verbraucht wird. Bei Validierungsfehlern wird der Prozess abgebrochen.
Fine-Tuning mit Unsloth, erfasst in MLflow
Protokollieren Sie die Gewichte der Adapter in Blob-Dateien mit unveränderlichen Versionsschlüsseln. Die Inferenzkonfiguration verweist explizit auf diese IDs – in der Produktion darf es ohne feste Angabe keine „neueste“ Version geben.
Sicheres Bereitstellen beider Arbeitslasten
- Getrennte Warteschlangen für interaktive und batchbasierte Aufgaben
- Maximalwerte für die gleichzeitige Verarbeitung pro Adapter
- Vorabaktivierung eines Standard-Adapters; spezielle Adapter werden nur bei Bedarf geladen
- Übermittlung von GPU-Nutzungsgrad und Warteschlangentiefe an denselben ClickHouse-Datenpool (oder Metriken-Stack)
- Rücksetzung von Adaptern durch Ändern eines Konfigurationszeigers, nicht durch Neuinstallation der gesamten VM
Zusammenfassung
Eine GPU kann zwei Produktoberflächen hosten, sofern die bedarfsorientierte LoRA-Ladung, die Warteschlangenisolation sowie strenge Vorgaben für MLOps-Artefakte in erstklassiger Qualität umgesetzt werden. vLLM + Unsloth + Azure ML + Blob + ClickHouse stellt eine praktische MLOps-Lösung für Teams dar, die sich derzeit keine doppelten Inferenz-Infrastrukturen leisten können, aber dennoch spezialisierte Funktionalitäten und Analysen benötigen.
Hinweis zur Kapazitätsplanung: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Die bedarfsorientierte LoRA-Ladung ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Vorteile einer einzigen GPU zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zunächst Batch-Aufgaben, wenn die interaktiven SLOs nicht eingehalten werden.
Kapazitätsplanungshinweis: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Kapazitätsplanungshinweis: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Kapazitätsplanungshinweis: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Kapazitätsplanungshinweis: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Bemerkung zur Kapazitätsplanung: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Bemerkung zur Kapazitätsplanung: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Kapazitätsplanungshinweis: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Kapazitätsplanungshinweis: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Kapazitätsplanungshinweis: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Kapazitätsplanungshinweis: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Bemerkung zur Kapazitätsplanung: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Bemerkung zur Kapazitätsplanung: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und streichen Sie zuerst Batch-Arbeiten, wenn die interaktiven SLOs nicht eingehalten werden.
Hinweis zur Kapazitätsplanung: Messen Sie die Anzahl der Tokens pro Sekunde mit einem Adapter im Vergleich zu N Adaptern unter gleichzeitiger interaktiver Last. Demand LoRA ist nicht kostenlos – die Ladekosten sowie die Speicherverfrachtung können die Einsparungen durch „einen GPU“ zunichtemachen, wenn der Produktverkehr die Warteschlangenklassen ignoriert. Setzen Sie eine feste Obergrenze für die gleichzeitige Anzahl der Adapter und erteilen Sie Batch-Aufgaben Vorrang, sobald die interaktiven SLOs nicht mehr eingehalten werden.