feat(ms-ai-architect): add plugin to open marketplace (v1.5.0 baseline)

Initial addition of ms-ai-architect plugin to the open-source marketplace.
Private content excluded: orchestrator/ (Linear tooling), docs/utredning/
(client investigation), generated test reports and PDF export script.
skill-gen tooling moved from orchestrator/ to scripts/skill-gen/.

Security scan: WARNING (risk 20/100) — no secrets, no injection found.
False positive fixed: added gitleaks:allow to Python variable reference
in output-validation-grounding-verification.md line 109.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-04-07 17:17:17 +02:00
commit 6a7632146e
490 changed files with 213249 additions and 2 deletions

View file

@ -0,0 +1,464 @@
# Asynchronous Processing Patterns
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Asynkron prosessering er en arkitekturstrategi der AI-forespørsler behandles uavhengig av den opprinnelige klientforbindelsen. I stedet for at klienten venter synkront på et svar fra Azure OpenAI (som kan ta fra 500ms til flere minutter for reasoning-modeller), plasseres forespørselen i en kø, behandles i bakgrunnen, og resultatet leveres via polling, webhook eller push-notifikasjon.
For Azure OpenAI tilbyr Microsoft flere innebygde asynkrone mekanismer: Batch API for store volum, Background Tasks i Responses API for langvarige oppgaver, og Webhooks for hendelsesbasert leveranse. I tillegg kan organisasjoner bygge egne asynkrone arkitekturer med Azure Service Bus, Azure Queue Storage eller Azure Event Hubs som mellomlag.
I norsk offentlig sektor er asynkron prosessering spesielt relevant for dokumentanalyse, saksbehandlingsstøtte og rapportgenerering — oppgaver der brukeren ikke trenger umiddelbart svar, men der volumet kan være svært høyt i perioder (f.eks. ved frister for høringssvar eller klagebehandling).
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Azure Service Bus | Enterprise message broker med køer og topics | Azure Service Bus |
| Azure Queue Storage | Enkel, kostnadseffektiv meldingskø | Azure Storage |
| Azure Event Hubs | Høy-throughput event streaming | Azure Event Hubs |
| Azure Functions | Serverless compute for kø-triggered prosessering | Azure Functions |
| Batch API | Innebygd asynkron batch-prosessering | Azure OpenAI |
| Background Tasks | Langvarige oppgaver i Responses API | Azure OpenAI |
| Webhooks | Hendelsesbasert notifikasjon | Azure OpenAI |
## Queue-based Architectures
### Service Bus-basert AI-prosessering
```python
# Producer: Legg forespørsler i kø
from azure.servicebus import ServiceBusClient, ServiceBusMessage
import json
class AIRequestProducer:
"""Queue AI requests via Azure Service Bus."""
def __init__(self, connection_string: str, queue_name: str = "ai-requests"):
self.client = ServiceBusClient.from_connection_string(connection_string)
self.sender = self.client.get_queue_sender(queue_name)
async def submit_request(
self,
request_id: str,
messages: list[dict],
priority: str = "normal",
callback_url: str = None
) -> str:
"""Submit AI request to queue. Returns request ID for polling."""
payload = {
"request_id": request_id,
"messages": messages,
"priority": priority,
"callback_url": callback_url,
"submitted_at": datetime.utcnow().isoformat()
}
message = ServiceBusMessage(
body=json.dumps(payload),
message_id=request_id,
subject=priority,
session_id=request_id if priority == "urgent" else None,
time_to_live=timedelta(hours=24)
)
await self.sender.send_messages(message)
return request_id
# Consumer: Prosesser forespørsler fra kø
from azure.servicebus.aio import ServiceBusClient as AsyncServiceBusClient
from openai import AsyncAzureOpenAI
class AIRequestConsumer:
"""Process AI requests from Service Bus queue."""
def __init__(
self,
sb_connection: str,
queue_name: str,
openai_client: AsyncAzureOpenAI,
max_concurrent: int = 10
):
self.sb_client = AsyncServiceBusClient.from_connection_string(
sb_connection)
self.queue_name = queue_name
self.openai = openai_client
self.semaphore = asyncio.Semaphore(max_concurrent)
async def process_messages(self):
"""Continuously process messages from queue."""
async with self.sb_client.get_queue_receiver(
self.queue_name,
max_wait_time=30
) as receiver:
async for message in receiver:
asyncio.create_task(
self._handle_message(receiver, message))
async def _handle_message(self, receiver, message):
async with self.semaphore:
try:
payload = json.loads(str(message))
# Prosesser med Azure OpenAI
response = await self.openai.chat.completions.create(
model="gpt-4o",
messages=payload["messages"],
max_tokens=2000
)
# Lagre resultat
await self._store_result(
payload["request_id"],
response.choices[0].message.content
)
# Callback hvis konfigurert
if payload.get("callback_url"):
await self._send_callback(
payload["callback_url"],
payload["request_id"],
response.choices[0].message.content
)
await receiver.complete_message(message)
except Exception as e:
if message.delivery_count < 3:
await receiver.abandon_message(message)
else:
await receiver.dead_letter_message(
message,
reason=str(e))
```
### Azure Functions Queue Trigger
```csharp
// Azure Function: Prosesser AI-forespørsler fra Storage Queue
using Azure.AI.OpenAI;
using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
public class AIRequestProcessor
{
private readonly AzureOpenAIClient _openAIClient;
public AIRequestProcessor(AzureOpenAIClient openAIClient)
{
_openAIClient = openAIClient;
}
[Function("ProcessAIRequest")]
[ServiceBusOutput("ai-results", Connection = "ServiceBusConnection")]
public async Task<ServiceBusMessage> Run(
[ServiceBusTrigger("ai-requests",
Connection = "ServiceBusConnection")]
ServiceBusReceivedMessage message,
FunctionContext context)
{
var logger = context.GetLogger("ProcessAIRequest");
var request = JsonSerializer.Deserialize<AIRequest>(
message.Body.ToString());
logger.LogInformation(
"Processing request {RequestId}", request!.RequestId);
var chatClient = _openAIClient.GetChatClient("gpt-4o");
var response = await chatClient.CompleteChatAsync(
request.Messages.Select(m =>
new UserChatMessage(m.Content)).ToList());
var result = new AIResult
{
RequestId = request.RequestId,
Output = response.Value.Content[0].Text,
CompletedAt = DateTime.UtcNow,
TokensUsed = response.Value.Usage.TotalTokenCount
};
return new ServiceBusMessage(
JsonSerializer.Serialize(result))
{
MessageId = request.RequestId,
Subject = "completed"
};
}
}
```
## Event-Driven Design
### Azure OpenAI med Event Grid
```python
# Event-driven pattern: Trigger AI-prosessering fra dokumenter
# Ny blob → Event Grid → Function → OpenAI → Result store
from azure.functions import Blueprint, EventGridEvent
from openai import AzureOpenAI
import json
bp = Blueprint()
@bp.event_grid_trigger(arg_name="event")
@bp.cosmos_db_output(
arg_name="resultDoc",
database_name="ai-results",
container_name="completions",
connection="CosmosConnection"
)
async def process_document_event(
event: EventGridEvent,
resultDoc: func.Out[str]
):
"""Process document when uploaded to Blob Storage."""
data = event.get_json()
blob_url = data["url"]
# Hent dokumentinnhold
document_text = await download_and_extract(blob_url)
# Prosesser med Azure OpenAI
client = AzureOpenAI(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_key=os.environ["AZURE_OPENAI_KEY"],
api_version="2024-10-21"
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Analyser dette dokumentet..."},
{"role": "user", "content": document_text[:128000]}
],
max_tokens=2000
)
result = {
"id": event.id,
"source_blob": blob_url,
"analysis": response.choices[0].message.content,
"tokens_used": response.usage.total_tokens,
"processed_at": datetime.utcnow().isoformat()
}
resultDoc.set(json.dumps(result))
```
## Request-Response Decoupling
### Background Tasks med Azure OpenAI Responses API
```python
from openai import AzureOpenAI
import time
def submit_background_task(client: AzureOpenAI, prompt: str) -> str:
"""Submit long-running task using background mode."""
response = client.responses.create(
model="o3", # Reasoning modell — kan ta minutter
input=prompt,
background=True # Kjør asynkront
)
return response.id
def poll_for_result(
client: AzureOpenAI,
response_id: str,
max_wait_seconds: int = 600,
poll_interval: int = 5
) -> dict:
"""Poll for background task completion."""
start = time.time()
while time.time() - start < max_wait_seconds:
result = client.responses.retrieve(response_id)
if result.status == "completed":
return {
"status": "completed",
"output": result.output,
"duration_seconds": round(time.time() - start, 1)
}
elif result.status == "failed":
return {"status": "failed", "error": result.error}
time.sleep(poll_interval)
return {"status": "timeout"}
# Bruk: Kompleks analyse som kan ta flere minutter
response_id = submit_background_task(
client,
"Analyser dette reguleringsverket og identifiser alle krav..."
)
# Klienten kan gjøre andre ting mens vi venter
result = poll_for_result(client, response_id)
```
## Status Polling and Webhooks
### Webhook-basert notifikasjon
```python
# Webhook handler for Azure OpenAI events
from flask import Flask, request, Response
import hmac
import hashlib
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["OPENAI_WEBHOOK_SECRET"]
@app.route("/webhooks/openai", methods=["POST"])
def handle_openai_webhook():
"""Handle Azure OpenAI webhook events."""
# Verifiser signatur
signature = request.headers.get("Webhook-Signature")
webhook_id = request.headers.get("Webhook-ID")
if not verify_signature(request.data, signature):
return Response("Invalid signature", status=400)
# Idempotency check
if is_already_processed(webhook_id):
return Response(status=200)
event = request.get_json()
# Prosesser event
if event.get("type") == "batch.completed":
handle_batch_complete(event["data"])
elif event.get("type") == "fine_tuning.job.succeeded":
handle_finetuning_complete(event["data"])
mark_as_processed(webhook_id)
return Response(status=200)
def verify_signature(payload: bytes, signature: str) -> bool:
"""Verify webhook signature."""
expected = hmac.new(
WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Polling-basert status-sjekk med exponential backoff
import asyncio
async def poll_with_backoff(
check_fn,
initial_interval: float = 2.0,
max_interval: float = 60.0,
backoff_factor: float = 1.5,
timeout: float = 3600.0
) -> dict:
"""Poll with exponential backoff until completion or timeout."""
interval = initial_interval
elapsed = 0.0
while elapsed < timeout:
result = await check_fn()
if result.get("status") in ("completed", "failed"):
return result
await asyncio.sleep(interval)
elapsed += interval
interval = min(interval * backoff_factor, max_interval)
return {"status": "timeout", "elapsed": elapsed}
```
### REST API for Status Polling
```csharp
// ASP.NET Core: Status polling endpoint for async AI requests
[ApiController]
[Route("api/ai")]
public class AIRequestController : ControllerBase
{
private readonly ICosmosDbService _cosmosDb;
private readonly IServiceBusSender _sender;
[HttpPost("requests")]
public async Task<IActionResult> SubmitRequest(
[FromBody] AIRequestDto request)
{
var requestId = Guid.NewGuid().ToString();
// Legg i kø for asynkron prosessering
await _sender.SendAsync(new ServiceBusMessage(
JsonSerializer.Serialize(request))
{
MessageId = requestId
});
// Returner 202 Accepted med Location header
return AcceptedAtAction(
nameof(GetStatus),
new { requestId },
new { requestId, status = "queued" });
}
[HttpGet("requests/{requestId}/status")]
public async Task<IActionResult> GetStatus(string requestId)
{
var result = await _cosmosDb.GetRequestStatus(requestId);
if (result == null)
return NotFound();
if (result.Status == "completed")
return Ok(result);
// Returnér 200 med status og Retry-After header
Response.Headers.Append("Retry-After", "5");
return Ok(new { requestId, status = result.Status });
}
}
```
## Norsk offentlig sektor
- **Saksbehandlingssystemer**: Asynkron prosessering er ideelt for AI-assistert saksbehandling der analyse kan ta tid. Saksbehandler sender inn dokument, fortsetter med annet arbeid, og mottar notifikasjon når analysen er ferdig.
- **Arkivloven**: Sørg for at alle mellomliggende meldinger i køer (Service Bus, Queue Storage) krypteres og at sensitive data ikke lagres utover nødvendig prosesseringstid.
- **Personvern**: Dead letter queues kan inneholde personopplysninger — konfigurer automatisk sletting og monitorering av DLQ-dybde.
- **Tilgjengelighet**: Asynkrone mønstre forbedrer brukeropplevelsen for tjenester med krav om universell utforming — brukere slipper å vente på skjermen.
- **Batch-prosessering**: Bruk Azure OpenAI Batch API for periodiske oppgaver (nattlige rapporter, ukentlige analyser) med 50% kostnadsreduksjon.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Bruker venter på svar (<3s) | Synkron + streaming | Best brukeropplevelse for korte svar |
| Dokumentanalyse (minutter) | Service Bus kø + polling | Bruker kan gjøre annet arbeid |
| Reasoning-modell (o3/o1) | Background Tasks API | Innebygd asynkron prosessering |
| Stort batch-volum (1000+) | Azure OpenAI Batch API | 50% kostnadsreduksjon |
| Event-drevet pipeline | Event Grid + Functions | Automatisk trigger ved nye data |
| Kritisk pålitelighet | Service Bus + DLQ | Garantert leveranse og feilhåndtering |
## Referanser
- [Azure OpenAI Batch API](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/batch) — Batch processing
- [Azure OpenAI Responses API — Background tasks](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/responses) — Background mode
- [Azure OpenAI Webhooks](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/webhooks) — Event notifications
- [Event-driven architecture style](https://learn.microsoft.com/azure/architecture/guide/architecture-styles/event-driven) — Architecture patterns
- [Azure Functions on Container Apps](https://learn.microsoft.com/azure/container-apps/functions-unified-platform) — Event-driven compute
## For Cosmo
- **Bruk denne referansen** når kunden har AI-workloads som ikke krever umiddelbart svar, eller når de opplever timeout-problemer med langvarige AI-forespørsler.
- Azure OpenAI Background Tasks er den enkleste løsningen for reasoning-modeller (o3, o1) som kan ta minutter — sett `background: true`.
- For enterprise-arkitekturer, anbefal Service Bus fremfor Queue Storage — gir sessions, dead letter queues og transaksjonsstøtte.
- Implementer alltid idempotency i webhook-handlere og consumers — meldinger kan leveres mer enn én gang.
- Batch API bør være standard for alle ikke-sanntids workloads — 50% kostnadsreduksjon er en enkel gevinst.

View file

@ -0,0 +1,583 @@
# Auto-Scaling AI Infrastructure
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Auto-scaling er en fundamental kapabilitet for AI-infrastruktur i Azure, der arbeidslaster kan variere dramatisk basert pa brukertrafikk, batch-prosessering og hendelsesdrevne triggere. For norsk offentlig sektor er auto-scaling spesielt viktig fordi trafikkmonstre er svart forutsigbare (arbeidstid, sesongvariasjon) men ogsaa kan ha uforutsigbare topper (hoeringsfrister, mediadekning).
Azure tilbyr auto-scaling pa flere nivaer: fra Azure Container Apps med KEDA for mikrotjenester, via Azure Kubernetes Service for komplekse orkestreringer, til VM Scale Sets for GPU-tunge arbeidslaster. Valget avhenger av arbeidslastens natur, latenskrav og kostnadsbudsjett.
Denne referansen dekker skaleringsstrategier for AI-infrastruktur med fokus pa Azure-tjenester som er relevante for norsk offentlig sektor, inkludert metrikkvalg, cooldown-perioder, kapasitetsplanlegging og kostnadsoptimalisering gjennom intelligent skalering.
## Grunnleggende skaleringstyper
### Horisontal vs. vertikal skalering
| Aspekt | Horisontal (scale out/in) | Vertikal (scale up/down) |
|--------|--------------------------|--------------------------|
| Metode | Legge til/fjerne instanser | Endre storrelse pa instans |
| Nedetid | Ingen | Ofte nodvendig |
| Grense | Tilnaermet ubegrenset | Storste tilgjengelige VM |
| Automatisering | Fullt automatisert | Vanskelig a automatisere |
| Anbefalt for AI | Ja (foretrekkes) | Kun initiell sizing |
**Anbefaling:** Bruk horisontal skalering for alle AI-arbeidslaster. Vertikal skalering bor kun brukes for initial sizing eller der applikasjonen ikke stotter flere instanser.
### Azure-tjenester med auto-scaling
| Tjeneste | Skaleringsmekanisme | Skaler til null | Maks instanser |
|----------|---------------------|-----------------|----------------|
| Azure Container Apps | KEDA (hendelsesdrevet) | Ja | 1000 |
| Azure Kubernetes Service | HPA/KEDA + Cluster Autoscaler | Nei (min 1 node) | 5000 noder |
| Azure Functions | Innebygd auto-scale | Ja (Consumption) | 200 (Consumption) |
| Azure App Service | Azure Monitor autoscale | Nei | 30 (Standard) |
| VM Scale Sets | Azure Monitor autoscale | Nei | 1000 |
## Azure Container Apps for AI-arbeidslaster
### KEDA-basert skalering
Azure Container Apps bruker KEDA (Kubernetes Event-driven Autoscaling) for deklarativ, hendelsesdrevet skalering:
```json
{
"properties": {
"template": {
"containers": [
{
"name": "ai-inference-service",
"image": "myregistry.azurecr.io/ai-inference:latest",
"resources": {
"cpu": 2.0,
"memory": "4Gi"
}
}
],
"scale": {
"minReplicas": 1,
"maxReplicas": 50,
"rules": [
{
"name": "http-scaling",
"http": {
"metadata": {
"concurrentRequests": "10"
}
}
},
{
"name": "queue-scaling",
"custom": {
"type": "azure-servicebus",
"metadata": {
"queueName": "ai-processing-queue",
"namespace": "svv-ai-servicebus",
"messageCount": "5"
}
}
}
]
}
}
}
}
```
### Skaleringsoppforsel
Container Apps folger disse standardverdiene:
| Parameter | Verdi | Beskrivelse |
|-----------|-------|-------------|
| Polling interval | 30 sekunder | Hvor ofte KEDA spoerrer hendelseskilder |
| Cool down period | 300 sekunder | Ventetid for nedskalering til minimum etter siste hendelse |
| Scale up stabilization | 0 sekunder | Ingen ventetid for oppskalering |
| Scale down stabilization | 300 sekunder | Ventetid for nedskalering |
| Scale up step | 1, 4, 8, 16, 32... | Eksponentiell oppskalering |
| Scale down step | 100% | Alle unodvendige replikaer fjernes |
| Skaleringsalgoritme | `ceil(currentMetric / targetMetric)` | Beregner onskede replikaer |
### Skaleringseksempel
Med regelen `messageCount: 5` og 20 meldinger i ko:
```
desiredReplicas = ceil(20 / 5) = 4 replikaer
```
Tidslinje for oppskalering:
```
T+0s: 0 replikaer (idle)
T+30s: KEDA oppdager 20 meldinger -> starter 1 replika
T+60s: Fortsatt meldinger -> skalerer til 4
T+90s: Flere meldinger -> skalerer til 8
T+120s: Ytterligere -> skalerer til 16 (om nodvendig)
...
T+N: Koen er tom
T+N+300s: Cool down utloper -> skalerer ned til minReplicas
```
### HTTP-basert skalering for AI API
```json
{
"scale": {
"minReplicas": 2,
"maxReplicas": 100,
"rules": [
{
"name": "ai-api-http",
"http": {
"metadata": {
"concurrentRequests": "5"
}
}
}
]
}
}
```
**Viktig for AI-tjenester:** Sett `concurrentRequests` lavt (3-10) fordi AI-inferens er CPU/GPU-intensivt. Standard web-applikasjoner taler 50-100 samtidige requests, men AI-endepunkter overbelastes raskt.
### Bicep-mal for AI Container App
```bicep
resource aiService 'Microsoft.App/containerApps@2023-05-01' = {
name: 'ai-inference-service'
location: 'swedencentral'
properties: {
environmentId: containerAppEnv.id
configuration: {
ingress: {
external: true
targetPort: 8000
transport: 'http'
}
}
template: {
containers: [
{
name: 'inference'
image: '${acrName}.azurecr.io/ai-inference:latest'
resources: {
cpu: json('2.0')
memory: '4Gi'
}
probes: [
{
type: 'Readiness'
httpGet: {
path: '/health'
port: 8000
}
initialDelaySeconds: 10
periodSeconds: 5
}
]
}
]
scale: {
minReplicas: 2 // Alltid minst 2 for HA
maxReplicas: 50
rules: [
{
name: 'http-rule'
http: {
metadata: {
concurrentRequests: '8'
}
}
}
]
}
}
}
}
```
## Skaleringsmetrikker og triggere
### Valg av riktige metrikker
| Metrikk | Best for | Fordeler | Ulemper |
|---------|---------|----------|---------|
| HTTP concurrent requests | API-endepunkter | Direkte relatert til last | Skalerer ikke for bakgrunnsoppgaver |
| Ko-lengde (Service Bus) | Asynkron prosessering | Presist for batch | Kan ikke fange CPU-belastning |
| CPU-bruk | Generelt | Universelt | Reaktivt, ikke proaktivt |
| Minne-bruk | ML-modeller | Fanger OOM-risiko | Sent signal |
| Tilpasset metrikk | Spesifikke behov | Presist for brukstilfelle | Krever instrumentering |
### Hendelsesdrevne triggere for AI
```json
{
"scale": {
"minReplicas": 0,
"maxReplicas": 100,
"rules": [
{
"name": "servicebus-trigger",
"custom": {
"type": "azure-servicebus",
"metadata": {
"queueName": "document-analysis",
"namespace": "svv-ai-bus",
"messageCount": "3"
},
"auth": [
{
"secretRef": "servicebus-connection",
"triggerParameter": "connection"
}
]
}
},
{
"name": "storage-queue-trigger",
"custom": {
"type": "azure-queue",
"metadata": {
"queueName": "image-processing",
"accountName": "svvaistorage",
"queueLength": "5"
}
}
}
]
}
}
```
### Azure Monitor Autoscale for VM Scale Sets
For GPU-baserte AI-arbeidslaster pa VM Scale Sets:
```json
{
"properties": {
"profiles": [
{
"name": "AI-Inference-Profile",
"capacity": {
"minimum": "2",
"maximum": "20",
"default": "2"
},
"rules": [
{
"metricTrigger": {
"metricName": "Percentage CPU",
"metricResourceUri": "/subscriptions/.../vmScaleSets/ai-gpu-cluster",
"timeGrain": "PT1M",
"statistic": "Average",
"timeWindow": "PT5M",
"timeAggregation": "Average",
"operator": "GreaterThan",
"threshold": 70
},
"scaleAction": {
"direction": "Increase",
"type": "ChangeCount",
"value": "2",
"cooldown": "PT5M"
}
},
{
"metricTrigger": {
"metricName": "Percentage CPU",
"metricResourceUri": "/subscriptions/.../vmScaleSets/ai-gpu-cluster",
"timeGrain": "PT1M",
"statistic": "Average",
"timeWindow": "PT10M",
"timeAggregation": "Average",
"operator": "LessThan",
"threshold": 30
},
"scaleAction": {
"direction": "Decrease",
"type": "ChangeCount",
"value": "1",
"cooldown": "PT10M"
}
}
]
}
]
}
}
```
## Cooldown-perioder og stabilisering
### Forstaa cooldown
Cooldown-perioder forhindrer "flapping" (rask opp- og nedskalering):
| Scenario | Anbefalt cooldown | Begrunnelse |
|----------|-------------------|-------------|
| AI-chatbot API | 3-5 min oppskalering, 10 min nedskalering | Rask respons pa trafikk, langsom nedtrapping |
| Batch-prosessering | 1 min oppskalering, 5 min nedskalering | Rask oppskalering for koproseering |
| GPU-inferens | 5-10 min oppskalering, 15-30 min nedskalering | VM-oppstart tar tid |
| RAG-pipeline | 3 min oppskalering, 10 min nedskalering | Balanse mellom respons og kostnad |
### Tidsbasert skalering (schedule)
For forutsigbare trafikkmonstre i offentlig sektor:
```json
{
"profiles": [
{
"name": "Arbeidstid",
"capacity": {
"minimum": "5",
"maximum": "50",
"default": "10"
},
"recurrence": {
"frequency": "Week",
"schedule": {
"timeZone": "W. Europe Standard Time",
"days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"hours": [7],
"minutes": [0]
}
}
},
{
"name": "Kveld-og-helg",
"capacity": {
"minimum": "1",
"maximum": "10",
"default": "2"
},
"recurrence": {
"frequency": "Week",
"schedule": {
"timeZone": "W. Europe Standard Time",
"days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"hours": [17],
"minutes": [0]
}
}
}
]
}
```
### Kombinert tidsbasert + reaktiv skalering
Den mest effektive strategien kombinerer begge:
```
Arbeidstid (07-17):
Baseline: 10 instanser (schedule)
Reaktiv: Skaler til 50 ved CPU > 70% (auto)
Kveld (17-07):
Baseline: 2 instanser (schedule)
Reaktiv: Skaler til 10 ved CPU > 70% (auto)
Spesielle perioder (hoeringsfrister, arsoppgjor):
Baseline: 20 instanser (manuelt justert schedule)
Reaktiv: Skaler til 100 ved behov (auto)
```
## Kapasitetsplanlegging
### Dimensjonering av AI-arbeidslaster
For a dimensjonere riktig, kartlegg disse parameterne:
| Parameter | Metode | Eksempel |
|-----------|--------|---------|
| Gjennomsnittlig requests/sek | Historisk data, Azure Monitor | 50 req/s i arbeidstid |
| Topp-requests/sek | P99 fra historisk data | 200 req/s (4x gjennomsnitt) |
| Request-varighet | Application Insights | 2-5 sek per AI-kall |
| Concurrent users | Estimat basert pa ansatte/innbyggere | 500 samtidige |
| Token throughput | Azure OpenAI-metrikker | 100K TPM |
### Kapasitetsformel
```
Nodvendige instanser = ceil(
(topp_requests_per_sekund * gjennomsnittlig_request_tid) /
concurrent_capacity_per_instans
)
Eksempel:
200 req/s * 3 sek = 600 samtidige requests
Hver instans handterer 10 samtidige = 60 instanser
+ 20% buffer = 72 instanser (maks)
Baseline: 20 instanser (gjennomsnittlig last)
```
### Azure Load Testing for AI-endepunkter
```yaml
# Azure Load Testing konfigurasjon
version: v0.1
testId: ai-endpoint-load-test
testPlan: ai-load-test.jmx
engineInstances: 5
configuration:
env:
- name: ENDPOINT_URL
value: https://ai-service.swedencentral.azurecontainerapps.io
- name: CONCURRENT_USERS
value: "100"
- name: RAMP_UP_SECONDS
value: "60"
- name: TEST_DURATION_SECONDS
value: "300"
failureCriteria:
- avg(response_time_ms) > 5000
- percentage(error) > 5
- p99(response_time_ms) > 15000
```
## Kostnadsoptimalisering gjennom skalering
### Strategier for kostnadskontroll
| Strategi | Beskrivelse | Besparelse |
|----------|-------------|-----------|
| Scale to zero | Sett minReplicas=0 for ikke-kritiske tjenester | 100% i tomgangstid |
| Spot/Preemptible VMs | Bruk for batch-prosessering og trening | 60-90% |
| Reserved Instances | 1- eller 3-ars commitment for baseline | 30-60% |
| Scheduling | Reduser kapasitet utenfor arbeidstid | 40-60% |
| Right-sizing | Bruk minste nodvendige VM-storrelse | 20-40% |
| GPU-deling | Dele GPU mellom flere tjenester | 50-70% |
### Container Apps kostnadskontroll
```json
{
"scale": {
"minReplicas": 0,
"maxReplicas": 20,
"rules": [
{
"name": "cost-optimized-http",
"http": {
"metadata": {
"concurrentRequests": "15"
}
}
}
]
}
}
```
**Faktureringsregler for Container Apps:**
- **0 replikaer:** Ingen fakturering
- **Idle replikaer (i minne, ingen prosessering):** Lavere "idle"-sats
- **Aktive replikaer:** Full fakturering
### Azure Savings Plans
For forutsigbar baseline-bruk:
```
Eksempel: AI-tjeneste med 10 instanser baseline
- Pay-as-you-go: 10 * $0.50/time = $120/dag
- 1-ars Savings Plan: 10 * $0.35/time = $84/dag (30% besparelse)
- 3-ars Savings Plan: 10 * $0.25/time = $60/dag (50% besparelse)
```
## Azure OpenAI-spesifikk skalering
### PTU vs. Standard for variabel last
For Azure OpenAI er skaleringsmodellen annerledes enn tradisjonell infrastruktur:
| Lastprofil | Anbefalt deployment | Begrunnelse |
|-----------|-------------------|-------------|
| Stabil, forutsigbar | PTU (100% baseline) | Lavest kostnad og latens |
| Variabel med kjent baseline | PTU + Standard spillover | PTU for baseline, Standard for topper |
| Svart variabel | Standard | Betal kun for bruk |
| Batch-prosessering | Global Batch | 50% rabatt, separat kvote |
### Smart load balancing med prioriteter
```python
# Arkitektur: PTU som primar, Standard som fallback
BACKENDS = [
{
"name": "ptu-sweden",
"url": "https://aoai-ptu-sweden.openai.azure.com/",
"priority": 1, # Forst: Bruk PTU-kapasitet
"type": "ptu"
},
{
"name": "standard-sweden",
"url": "https://aoai-std-sweden.openai.azure.com/",
"priority": 2, # Fallback: Standard i samme region
"type": "standard"
},
{
"name": "standard-northeurope",
"url": "https://aoai-std-ne.openai.azure.com/",
"priority": 3, # Siste utvei: Annen region
"type": "standard"
}
]
```
## Overvaking av skalering
### Viktige metrikker
| Metrikk | Kilde | Terskel |
|---------|-------|---------|
| Replica count | Container Apps metrics | Varsle ved >80% av maks |
| CPU utilization per replica | Container Apps metrics | Varsle ved >80% |
| Request queue length | Service Bus metrics | Varsle ved >100 meldinger |
| Scale events | Activity Log | Spoer frekvens |
| Failed scale operations | Activity Log | Varsle umiddelbart |
| Cost per day | Cost Management | Varsle ved budsjettgrense |
### KQL for skaleringsanalyse
```kusto
// Analyse av skaleringsaktivitet
ContainerAppSystemLogs
| where RevisionName contains "ai-inference"
| where Log contains "Scaling"
| summarize
scale_up_events = countif(Log contains "scaling up"),
scale_down_events = countif(Log contains "scaling down"),
max_replicas = max(toint(extract("replicas=(\\d+)", 1, Log)))
by bin(TimeGenerated, 1h)
| order by TimeGenerated desc
```
## Sjekkliste for auto-scaling
| Nr | Tiltak | Prioritet |
|----|--------|-----------|
| 1 | Definer SLA/SLO for responstid | Kritisk |
| 2 | Velg riktig skaleringsmetrikk for arbeidslast | Kritisk |
| 3 | Sett fornuftig minReplicas (0 for ikke-kritisk, 2+ for HA) | Hoy |
| 4 | Konfigurer cooldown-perioder for a unnga flapping | Hoy |
| 5 | Implementer tidsbasert skalering for kjente monstre | Medium |
| 6 | Last-test for a validere skaleringsparametere | Medium |
| 7 | Sett opp kostnadsalarmer for a fange runaway-skalering | Medium |
| 8 | Bruk readiness probes for a sikre healthy instanser | Medium |
| 9 | Implementer graceful shutdown for lange AI-operasjoner | Medium |
| 10 | Dokumenter skaleringslogikk i ADR | Anbefalt |
## For Cosmo
- **Horisontal skalering er standard** for AI-arbeidslaster. Azure Container Apps med KEDA er forstevalgdet for mikrotjenester og API-lag. VM Scale Sets for GPU-tunge arbeidslaster.
- **Kombinert schedule + reaktiv skalering** gir best resultat for offentlig sektor: forutsigbar baseline i arbeidstid, lav kapasitet pa kveld/helg, med reaktiv oppskalering for uforutsette topper.
- **Scale to zero reduserer kostnader dramatisk** for utviklings- og testmiljoer. I produksjon, hold minimum 2 replikaer for hoy tilgjengelighet.
- **AI-endepunkter krever lavere concurrency-terskel** enn vanlige web-APIer. Sett concurrentRequests til 3-10, ikke 50-100 som for tradisjonelle tjenester.
- **PTU + Standard spillover** er den mest kostnadseffektive arkitekturen for Azure OpenAI med variabel last. PTU for baseline, Standard for topper, Global Batch for asynkron prosessering.

View file

@ -0,0 +1,560 @@
# Batch API Usage and Optimization
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Azure OpenAI Batch API er designet for storskala, asynkron prosessering av AI-arbeidslaster. Med 50% lavere kostnad enn Global Standard-prising og separat kvote som ikke pavirker online-trafikken, er Batch API ideelt for norsk offentlig sektor som trenger a prosessere store volumer av dokumenter, klassifiseringer eller analyser.
For offentlige virksomheter som Statens vegvesen, Nav, eller Skatteetaten kan Batch API brukes til masseprosessering av henvendelser, dokumentanalyse, oversettelser og datautvinning uten a forstyrre sanntidstjenestene. Tjenesten er spesielt verdifull for periodiske oppgaver som kvartalsvis rapportering, arsavslutning, eller migrering av historiske data.
Denne referansen dekker hele arbeidsflyten for Batch API, fra filsammensetning og opplasting til kostnadsberegning og feilhhandtering, med fokus pa optimalisering for store volumer.
## Oversikt over Batch API
### Nokkelegenskaper
| Egenskap | Verdi |
|----------|-------|
| Kostnadsreduksjon | 50% lavere enn Global Standard |
| Malsatt leveringstid | 24 timer |
| Maksimal filstorrelse | 200 MB (direkte), 1 GB (via Blob Storage) |
| Maksimalt antall requests per fil | 100 000 |
| Maks batch-filer per ressurs | 500 (uten utlop), 10 000 (med utlop) |
| Kvotetype | Separat enqueued token-kvote |
| Stottede modeller | GPT-4o, GPT-4o mini, GPT-4.1, o3-mini m.fl. |
| Deployment-type | GlobalBatch eller DataZoneBatch |
### Batch API vs. Standard API
| Aspekt | Standard API | Batch API |
|--------|-------------|-----------|
| Prosessering | Synkron, umiddelbar | Asynkron, 24-timers mal |
| Kostnad | Full pris | 50% rabatt |
| Kvote | Delt TPM-kvote | Separat enqueued token-kvote |
| Bruksomrade | Sanntid, interaktivt | Masseprosessering, analyse |
| Pavirkning pa online | Ja, deler kapasitet | Nei, separat kapasitet |
| Filformat | JSON per request | JSONL (samlet fil) |
## Batch Job-sammensetning
### JSONL-filformat
Batch API bruker JSON Lines-format (`.jsonl`), der hver linje er en selvstendig JSON-objekt:
```jsonl
{"custom_id": "req-001", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-batch", "messages": [{"role": "system", "content": "Klassifiser henvendelsen som KLAGE, SPORSMAL, eller TILBAKEMELDING."}, {"role": "user", "content": "Jeg er svart misfornoyd med ventetiden pa fornyelse av forerkort."}], "max_tokens": 50}}
{"custom_id": "req-002", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-batch", "messages": [{"role": "system", "content": "Klassifiser henvendelsen som KLAGE, SPORSMAL, eller TILBAKEMELDING."}, {"role": "user", "content": "Hvordan soker jeg om nytt forerkort?"}], "max_tokens": 50}}
{"custom_id": "req-003", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-batch", "messages": [{"role": "system", "content": "Klassifiser henvendelsen som KLAGE, SPORSMAL, eller TILBAKEMELDING."}, {"role": "user", "content": "Fint arbeid med den nye tunnelen i Rogaland!"}], "max_tokens": 50}}
```
**Viktige regler:**
- `custom_id` er obligatorisk og lar deg koble respons til input
- `model` ma vaere identisk pa alle linjer og matche deployment-navnet
- Responser returneres IKKE i samme rekkefolge som input
- For best ytelse: send store filer fremfor mange sma filer
### Responses API-format (nyere)
```jsonl
{"custom_id": "req-001", "method": "POST", "url": "/v1/responses", "body": {"model": "gpt-4o-batch", "input": [{"role": "user", "content": "Oppsummer dette dokumentet: ..."}], "max_output_tokens": 500}}
```
### Programmatisk filgenerering (Python)
```python
import json
from pathlib import Path
from typing import Iterator
def generate_batch_file(
items: list[dict],
system_prompt: str,
model: str,
output_path: str,
max_tokens: int = 200
) -> Path:
"""Generer JSONL batch-fil fra en liste med items."""
path = Path(output_path)
with open(path, "w", encoding="utf-8") as f:
for i, item in enumerate(items):
request = {
"custom_id": f"req-{i:06d}",
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": item["content"]}
],
"max_tokens": max_tokens,
"temperature": 0.1 # Lav temperatur for konsistens
}
}
f.write(json.dumps(request, ensure_ascii=False) + "\n")
file_size = path.stat().st_size
print(f"Generert batch-fil: {path}")
print(f"Antall requests: {len(items)}")
print(f"Filstorrelse: {file_size / (1024*1024):.1f} MB")
return path
def chunk_batch_file(
input_path: str,
max_requests: int = 100_000,
max_size_mb: int = 190
) -> list[str]:
"""Del opp en stor batch-fil i mindre filer innenfor grensene."""
chunks = []
current_chunk = []
current_size = 0
chunk_index = 0
with open(input_path, "r", encoding="utf-8") as f:
for line in f:
line_size = len(line.encode("utf-8"))
if (len(current_chunk) >= max_requests or
(current_size + line_size) > max_size_mb * 1024 * 1024):
# Skriv gjeldende chunk og start ny
chunk_path = f"{input_path}.chunk_{chunk_index:03d}.jsonl"
with open(chunk_path, "w", encoding="utf-8") as cf:
cf.writelines(current_chunk)
chunks.append(chunk_path)
current_chunk = []
current_size = 0
chunk_index += 1
current_chunk.append(line)
current_size += line_size
# Skriv siste chunk
if current_chunk:
chunk_path = f"{input_path}.chunk_{chunk_index:03d}.jsonl"
with open(chunk_path, "w", encoding="utf-8") as cf:
cf.writelines(current_chunk)
chunks.append(chunk_path)
return chunks
```
## Filopplasting og -handtering
### Opplasting via Python SDK
```python
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
# Last opp batch-fil med utlopsdato (14 dager)
file_response = client.files.create(
file=open("batch_requests.jsonl", "rb"),
purpose="batch",
extra_body={
"expires_after": {
"seconds": 1209600, # 14 dager
"anchor": "created_at"
}
}
)
print(f"Fil-ID: {file_response.id}")
print(f"Status: {file_response.status}")
print(f"Storrelse: {file_response.bytes} bytes")
```
### Opplasting via REST API
```bash
curl https://YOUR_RESOURCE.openai.azure.com/openai/files?api-version=2025-03-01-preview \
-H "api-key: $AZURE_OPENAI_API_KEY" \
-F "purpose=batch" \
-F "file=@batch_requests.jsonl" \
-F "expires_after.seconds=1209600" \
-F "expires_after.anchor=created_at"
```
### Stor fil via Azure Blob Storage (BYOS)
For filer over 200 MB (opptil 1 GB):
```python
from azure.storage.blob import BlobServiceClient
# 1. Last opp til Azure Blob Storage
blob_service = BlobServiceClient.from_connection_string(conn_str)
container = blob_service.get_container_client("batch-files")
with open("large_batch.jsonl", "rb") as data:
container.upload_blob(
name="large_batch.jsonl",
data=data,
overwrite=True
)
# 2. Konfigurer Azure OpenAI til a bruke Blob Storage
# Se: https://learn.microsoft.com/azure/ai-foundry/openai/how-to/batch-blob-storage
```
### Filgrenser
| Grense | Verdi | Med utlopsdato |
|--------|-------|----------------|
| Maks input-filstorrelse | 200 MB | 200 MB |
| Maks input-filstorrelse (BYOS) | 1 GB | 1 GB |
| Maks requests per fil | 100 000 | 100 000 |
| Maks input-filer per ressurs | 500 | 10 000 |
| Utlopstid | Ingen utlop | 14-30 dager |
## Batch Job-oppretting og -overvaking
### Opprett batch job
```python
# Opprett batch job
batch_response = client.batches.create(
input_file_id=file_response.id,
endpoint="/v1/chat/completions",
completion_window="24h",
extra_body={
"output_expires_after": {
"seconds": 1209600,
"anchor": "created_at"
}
}
)
print(f"Batch-ID: {batch_response.id}")
print(f"Status: {batch_response.status}")
```
### Overvak batch-status
```python
import time
def monitor_batch(client, batch_id: str, poll_interval: int = 60):
"""Overvak batch job til den er ferdig."""
while True:
batch = client.batches.retrieve(batch_id)
print(f"Status: {batch.status}")
print(f" Requests total: {batch.request_counts.total}")
print(f" Completed: {batch.request_counts.completed}")
print(f" Failed: {batch.request_counts.failed}")
if batch.status in ("completed", "failed", "cancelled", "expired"):
return batch
time.sleep(poll_interval)
# Overvak med 60 sekunders intervall
final_batch = monitor_batch(client, batch_response.id)
```
### Batch-statusflyten
```
validating -> in_progress -> completed
-> failed
-> cancelled (manuelt)
-> expired (sjelden)
```
### Hent resultater
```python
if final_batch.status == "completed":
# Hent resultatfil
output_file_id = final_batch.output_file_id
result_content = client.files.content(output_file_id)
# Parse resultater
results = {}
for line in result_content.text.strip().split("\n"):
result = json.loads(line)
custom_id = result["custom_id"]
response_body = result["response"]["body"]
if result["response"]["status_code"] == 200:
content = response_body["choices"][0]["message"]["content"]
results[custom_id] = {
"status": "success",
"content": content,
"tokens": response_body["usage"]
}
else:
results[custom_id] = {
"status": "error",
"error": result.get("error", {})
}
# Sjekk feilfil
if final_batch.error_file_id:
error_content = client.files.content(final_batch.error_file_id)
errors = [json.loads(line) for line in error_content.text.strip().split("\n")]
print(f"Antall feil: {len(errors)}")
```
## Kostnadsberegning og besparelser
### Prissammenligning
| Modell | Standard input (per 1M tokens) | Batch input (per 1M tokens) | Besparelse |
|--------|-------------------------------|---------------------------|-----------|
| GPT-4o | $2.50 | $1.25 | 50% |
| GPT-4o mini | $0.15 | $0.075 | 50% |
| GPT-4.1 | $2.00 | $1.00 | 50% |
*Priser er illustrative og kan variere. Sjekk azure.microsoft.com/pricing for oppdaterte priser.*
### Kostnadsestimering
```python
def estimate_batch_cost(
num_requests: int,
avg_input_tokens: int,
avg_output_tokens: int,
model: str = "gpt-4o"
) -> dict:
"""Estimer kostnad for batch vs. standard prosessering."""
# Priser per 1M tokens (NOK, ca. kurs)
prices = {
"gpt-4o": {
"standard_input": 27.50, # NOK per 1M tokens
"standard_output": 110.00,
"batch_input": 13.75, # 50% rabatt
"batch_output": 55.00 # 50% rabatt
},
"gpt-4o-mini": {
"standard_input": 1.65,
"standard_output": 6.60,
"batch_input": 0.825,
"batch_output": 3.30
}
}
p = prices.get(model, prices["gpt-4o"])
total_input_tokens = num_requests * avg_input_tokens
total_output_tokens = num_requests * avg_output_tokens
standard_cost = (
(total_input_tokens / 1_000_000) * p["standard_input"] +
(total_output_tokens / 1_000_000) * p["standard_output"]
)
batch_cost = (
(total_input_tokens / 1_000_000) * p["batch_input"] +
(total_output_tokens / 1_000_000) * p["batch_output"]
)
return {
"num_requests": num_requests,
"total_tokens": total_input_tokens + total_output_tokens,
"standard_cost_nok": round(standard_cost, 2),
"batch_cost_nok": round(batch_cost, 2),
"savings_nok": round(standard_cost - batch_cost, 2),
"savings_percent": 50
}
# Eksempel: 50 000 henvendelser, 500 input tokens, 100 output tokens
estimate = estimate_batch_cost(50_000, 500, 100, "gpt-4o")
print(f"Standard: {estimate['standard_cost_nok']} NOK")
print(f"Batch: {estimate['batch_cost_nok']} NOK")
print(f"Besparelse: {estimate['savings_nok']} NOK ({estimate['savings_percent']}%)")
```
### Kostnadsoptimalisering for batch
| Strategi | Beskrivelse | Effekt |
|----------|-------------|--------|
| Bruk GPT-4o mini | For enklere oppgaver (klassifisering, utvinning) | 90%+ billigere enn GPT-4o |
| Lav max_tokens | Tilpass til forventet output | Unngaer overfakturering |
| Lav temperatur | Mer konsistent, potensielt kortere output | 5-15% |
| Strukturert output | JSON schema for forutsigbar lengde | 10-20% |
| Store batch-filer | Samle mange requests i en fil | Bedre throughput |
## Retry og feilhhandtering
### Koe-handtering ved kvotegrense
Nar batch jobs er for store for tilgjengelig kvote, bruk fail-fast med eksponentiell backoff:
```python
import time
from openai import AzureOpenAI, BadRequestError
def submit_batch_with_retry(
client: AzureOpenAI,
file_id: str,
max_retries: int = 10,
initial_wait: int = 300 # 5 minutter
):
"""Submit batch job med automatisk retry ved kvotegrense."""
for attempt in range(max_retries):
try:
batch = client.batches.create(
input_file_id=file_id,
endpoint="/v1/chat/completions",
completion_window="24h"
)
print(f"Batch opprettet: {batch.id}")
return batch
except BadRequestError as e:
if "enqueued token limit" in str(e).lower():
wait_time = initial_wait * (2 ** attempt)
print(f"Kvotegrense nadd. Venter {wait_time}s for forsok {attempt + 1}/{max_retries}")
time.sleep(wait_time)
else:
raise
raise Exception(f"Kunne ikke opprette batch etter {max_retries} forsok")
```
### Handtering av delvise feil
```python
def process_batch_results(client, batch_id: str) -> dict:
"""Prosesser batch-resultater og segreger suksess/feil."""
batch = client.batches.retrieve(batch_id)
results = {"success": [], "errors": [], "stats": {}}
# Hent suksessfulle resultater
if batch.output_file_id:
output = client.files.content(batch.output_file_id)
for line in output.text.strip().split("\n"):
result = json.loads(line)
if result["response"]["status_code"] == 200:
results["success"].append(result)
else:
results["errors"].append(result)
# Hent dedikerte feil
if batch.error_file_id:
errors = client.files.content(batch.error_file_id)
for line in errors.text.strip().split("\n"):
results["errors"].append(json.loads(line))
# Statistikk
results["stats"] = {
"total": batch.request_counts.total,
"completed": batch.request_counts.completed,
"failed": batch.request_counts.failed,
"success_rate": (
batch.request_counts.completed / batch.request_counts.total * 100
if batch.request_counts.total > 0 else 0
)
}
return results
def retry_failed_requests(
client: AzureOpenAI,
failed_results: list,
original_file_path: str,
model: str
) -> str:
"""Generer ny batch-fil fra feilede requests for retry."""
# Les originale requests for a finne matchende custom_ids
original_requests = {}
with open(original_file_path, "r") as f:
for line in f:
req = json.loads(line)
original_requests[req["custom_id"]] = req
# Generer retry-fil
retry_path = original_file_path.replace(".jsonl", "_retry.jsonl")
failed_ids = {r["custom_id"] for r in failed_results}
with open(retry_path, "w") as f:
for custom_id in failed_ids:
if custom_id in original_requests:
f.write(json.dumps(original_requests[custom_id]) + "\n")
print(f"Retry-fil generert: {retry_path} ({len(failed_ids)} requests)")
return retry_path
```
## Bruksomrader for norsk offentlig sektor
### Masseklassifisering av innbyggerhenvendelser
```python
# Eksempel: Klassifiser 100 000 henvendelser fra innbyggerportal
system_prompt = """Klassifiser henvendelsen. Svar med JSON:
{"kategori": "KLAGE|SPORSMAL|TILBAKEMELDING|SOKNAD",
"prioritet": "HOY|NORMAL|LAV",
"etat": "FORERKORT|KJORETOYREG|VEIPROSJEKT|ANNET"}"""
batch_file = generate_batch_file(
items=henvendelser,
system_prompt=system_prompt,
model="gpt-4o-mini-batch",
output_path="henvendelser_batch.jsonl",
max_tokens=100
)
```
### Dokumentanalyse og oppsummering
```python
# Eksempel: Oppsummer 10 000 hoeringsuttalelser
system_prompt = """Oppsummer hoeringsuttalelsen i 2-3 setninger.
Identifiser hovedstandpunkt og eventuelle konkrete forslag."""
batch_file = generate_batch_file(
items=hoeringsuttalelser,
system_prompt=system_prompt,
model="gpt-4o-batch",
output_path="hoering_batch.jsonl",
max_tokens=300
)
```
### Sprakvasking og oversettelse
```python
# Eksempel: Oversett 50 000 dokumentfragmenter til nynorsk
system_prompt = "Oversett teksten fra bokmaal til nynorsk. Bevar fagterminologi."
```
## Sjekkliste for batch-optimalisering
| Nr | Tiltak | Prioritet |
|----|--------|-----------|
| 1 | Bruk store filer (ikke mange sma) | Hoy |
| 2 | Sett utlop pa filer (expires_after) for a unnga 500-filgrensen | Hoy |
| 3 | Velg GPT-4o mini for enklere oppgaver | Hoy |
| 4 | Implementer retry med eksponentiell backoff | Hoy |
| 5 | Tilpass max_tokens til faktisk behov | Medium |
| 6 | Bruk strukturert output (JSON schema) | Medium |
| 7 | Overvak med polling (60s intervall) | Medium |
| 8 | Implementer retry for feilede requests | Medium |
| 9 | Bruk Blob Storage for filer over 200 MB | Ved behov |
| 10 | Sett opp alerting for batch completion | Anbefalt |
## For Cosmo
- **50% kostnadsreduksjon** gjor Batch API til forstevalgdet for all ikke-sanntids AI-prosessering i offentlig sektor. Masseklassifisering, dokumentanalyse og oversettelse bor alltid bruke batch.
- **Datasuverenitet:** Batch API prosesserer data i enhver Azure OpenAI-region for Global Batch. Bruk DataZoneBatch for a begrense til EU-regioner, eller Regional Batch for strengeste krav.
- **Completion window pa 24 timer** er et mal, ikke en garanti. Jobs som tar lengre tid utloper ikke, men du kan kansellere og fa resultater for fullfort arbeid.
- **Enqueued token-kvote** er separat fra online-kvote, sa batch-prosessering pavirker ikke sanntidstjenester. Ideelt for nattlige batch-kjoeringer.
- **Retry-pattern er kritisk:** Store batch jobs kan feile pa kvotegrenser. Implementer alltid fail-fast med eksponentiell backoff og retry av feilede requests.

View file

@ -0,0 +1,566 @@
# CDN and Edge Caching for AI Workloads
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Content Delivery Networks (CDN) og edge computing er etablerte teknologier for a akselerere webinnhold, men bruken i AI-kontekst krever en nyansert tilnaerming. AI-responser er dynamiske og ofte personaliserte, noe som gjor tradisjonell caching mer kompleks. Likevel finnes det betydelige muligheter for a redusere latens og kostnader ved a cache AI-relatert innhold pa riktig mate.
For norsk offentlig sektor, der brukere er geografisk spredt over hele landet, kan edge computing og smart caching redusere opplevd responstid betydelig. Azure Front Door med sine 118+ edge-lokasjoner og globale lastbalansering er den primaere tjenesten for dette formalet.
Denne referansen dekker strategier for a bruke Azure Front Door, CDN-caching og edge compute for AI-arbeidslaster, med fokus pa hva som kan og ikke bor caches, samt geografisk routing for optimal ytelse.
## Azure Front Door for AI-endepunkter
### Oversikt
Azure Front Door er en global CDN med lastbalansering, TLS-terminering og edge caching. For AI-arbeidslaster fungerer den som et intelligent lag mellom brukere og backend-tjenester:
| Funksjon | Beskrivelse | Relevans for AI |
|----------|-------------|-----------------|
| Global lastbalansering | Ruter trafikk til naermeste/friskeste backend | Multi-region AI-deployments |
| TLS-terminering | Terminerer SSL pa edge | Reduserer latens med ~50-100 ms |
| Edge caching | Cacher statisk og semi-statisk innhold | Embeddings, modellmetadata |
| WAF | Web Application Firewall | Beskytter AI-endepunkter |
| DDoS-beskyttelse | Layer 3/4/7 beskyttelse | Kritisk for publiserte AI-APIer |
| Traffic acceleration | Split TCP, anycast | 30-40% raskere for dynamisk innhold |
### Arkitektur: Front Door foran AI-tjenester
```
Innbygger (Tromso) --> Azure Front Door Edge (Oslo/Stockholm)
|
+---------+---------+
| |
Sweden Central North Europe
(AI-primaer) (AI-failover)
| |
Azure OpenAI Azure OpenAI
Container Apps Container Apps
```
### Front Door-konfigurasjon for AI
```bicep
resource frontDoor 'Microsoft.Cdn/profiles@2023-05-01' = {
name: 'fd-ai-services'
location: 'global'
sku: {
name: 'Premium_AzureFrontDoor' // Premium for WAF
}
}
resource aiEndpoint 'Microsoft.Cdn/profiles/afdEndpoints@2023-05-01' = {
parent: frontDoor
name: 'ai-api-endpoint'
location: 'global'
properties: {
enabledState: 'Enabled'
}
}
// Origin group med health probes
resource aiOriginGroup 'Microsoft.Cdn/profiles/originGroups@2023-05-01' = {
parent: frontDoor
name: 'ai-backends'
properties: {
loadBalancingSettings: {
sampleSize: 4
successfulSamplesRequired: 3
additionalLatencyInMilliseconds: 50
}
healthProbeSettings: {
probePath: '/health'
probeRequestType: 'HEAD'
probeProtocol: 'Https'
probeIntervalInSeconds: 30
}
sessionAffinityState: 'Disabled' // Viktig: Ingen session affinity for AI
}
}
// Primaer origin: Sweden Central
resource primaryOrigin 'Microsoft.Cdn/profiles/originGroups/origins@2023-05-01' = {
parent: aiOriginGroup
name: 'sweden-central'
properties: {
hostName: 'ai-service.swedencentral.azurecontainerapps.io'
httpPort: 80
httpsPort: 443
originHostHeader: 'ai-service.swedencentral.azurecontainerapps.io'
priority: 1
weight: 1000
}
}
// Failover origin: North Europe
resource failoverOrigin 'Microsoft.Cdn/profiles/originGroups/origins@2023-05-01' = {
parent: aiOriginGroup
name: 'north-europe'
properties: {
hostName: 'ai-service.northeurope.azurecontainerapps.io'
httpPort: 80
httpsPort: 443
originHostHeader: 'ai-service.northeurope.azurecontainerapps.io'
priority: 2
weight: 1000
}
}
```
## CDN Caching-regler for AI-responser
### Hva kan og bor caches?
| Innholdstype | Cachebar? | TTL | Begrunnelse |
|-------------|-----------|-----|-------------|
| Statiske assets (JS/CSS/bilder) | Ja | 1 dag - 1 uke | Standard CDN-bruk |
| AI-modellmetadata (tilgjengelige modeller) | Ja | 5-15 min | Endres sjelden |
| Embedding-resultater (identisk input) | Ja, med forsiktighet | 1-24 timer | Deterministisk output |
| Chat completion-responser | Nei | N/A | Dynamisk, personalisert |
| RAG-soekeresultater | Nei | N/A | Avhenger av kunnskapsbase |
| Streaming-responser (SSE) | Nei | N/A | Real-time, ikke cachebart |
| Health check-endepunkter | Nei | N/A | Ma vaere sanntid |
| Token-telling/estimat | Ja | 1-5 min | Stabil beregning |
### Cache-regler i Front Door
```bicep
// Route for statisk innhold (caching aktivert)
resource staticRoute 'Microsoft.Cdn/profiles/afdEndpoints/routes@2023-05-01' = {
parent: aiEndpoint
name: 'static-content'
properties: {
originGroup: { id: aiOriginGroup.id }
patternsToMatch: ['/static/*', '/assets/*', '/models/metadata']
supportedProtocols: ['Https']
cacheConfiguration: {
queryStringCachingBehavior: 'IgnoreQueryString'
compressionSettings: {
isCompressionEnabled: true
contentTypesToCompress: [
'application/json'
'text/javascript'
'text/css'
]
}
cacheBehavior: 'OverrideAlways'
cacheDuration: '01:00:00' // 1 time
}
}
}
// Route for AI API-endepunkter (caching deaktivert)
resource apiRoute 'Microsoft.Cdn/profiles/afdEndpoints/routes@2023-05-01' = {
parent: aiEndpoint
name: 'ai-api'
properties: {
originGroup: { id: aiOriginGroup.id }
patternsToMatch: ['/api/chat/*', '/api/completions/*']
supportedProtocols: ['Https']
cacheConfiguration: {
queryStringCachingBehavior: 'UseQueryString'
cacheBehavior: 'HonorOrigin' // Respekter Cache-Control fra backend
}
}
}
// Route for streaming-endepunkter (ingen caching, ingen buffering)
resource streamRoute 'Microsoft.Cdn/profiles/afdEndpoints/routes@2023-05-01' = {
parent: aiEndpoint
name: 'ai-stream'
properties: {
originGroup: { id: aiOriginGroup.id }
patternsToMatch: ['/api/chat/stream/*']
supportedProtocols: ['Https']
cacheConfiguration: {
cacheBehavior: 'Disabled'
}
}
}
```
### Backend Cache-Control headers
For korrekt cache-oppforsel ma backend sette riktige headers:
```python
from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/models/metadata")
async def get_model_metadata():
"""Modellmetadata - kan caches."""
return JSONResponse(
content={"models": ["gpt-4o", "gpt-4o-mini"]},
headers={
"Cache-Control": "public, max-age=900", # 15 minutter
"Vary": "Accept-Encoding"
}
)
@app.post("/api/chat/completions")
async def chat_completions():
"""Chat completions - skal IKKE caches."""
response = await process_chat()
return JSONResponse(
content=response,
headers={
"Cache-Control": "no-store, no-cache, must-revalidate",
"Pragma": "no-cache"
}
)
@app.post("/api/embeddings")
async def get_embeddings(request: EmbeddingRequest):
"""Embeddings - kan caches for identiske inputs."""
# Generer cache key basert pa input
cache_key = hashlib.sha256(request.input.encode()).hexdigest()
return JSONResponse(
content=embedding_result,
headers={
"Cache-Control": "public, max-age=86400", # 24 timer
"ETag": f'"{cache_key}"',
"Vary": "Content-Type"
}
)
```
### Advarsel: Unnga caching av personlig innhold
```
ADVARSEL: Feilkonfigurert caching kan fore til personvernbrudd!
ALDRI cache:
- Chat-responser som inneholder personopplysninger
- Responser basert pa brukeridentitet
- API-kall med Authorization-header
- Streaming-endepunkter (SSE)
Azure Front Door cacher basert pa URL og query-parametre.
Hvis to brukere sender identisk request til et cachet endepunkt,
vil bruker B se bruker As respons.
For norsk offentlig sektor: Brudd pa personopplysningsloven (GDPR)
kan resultere i bot fra Datatilsynet.
```
## Semantic Caching for AI
### Tradisjonell cache vs. semantic cache
| Aspekt | Tradisjonell cache | Semantic cache |
|--------|-------------------|----------------|
| Match-kriterium | Eksakt URL/key | Semantisk likhet (vector) |
| Hit rate | Lav for AI (unik input) | Hoy (lignende sporsmaal matcher) |
| Infrastruktur | Standard CDN/Redis | Redis med RediSearch + embeddings |
| Kostnad | Lav | Moderat (embedding + Redis) |
| Latens ved hit | ~1 ms | ~5-20 ms |
| Relevans for AI | Begrenset | Hoy |
### Semantic Caching med Azure API Management
```xml
<!-- APIM policy for semantic caching -->
<policies>
<inbound>
<base />
<!-- Sjekk semantic cache for matche -->
<azure-openai-semantic-cache-lookup
score-threshold="0.8"
embeddings-backend-id="embeddings-backend"
embeddings-backend-auth="system-assigned" />
</inbound>
<backend>
<forward-request buffer-response="false" />
</backend>
<outbound>
<base />
<!-- Lagre respons i semantic cache -->
<azure-openai-semantic-cache-store duration="3600" />
</outbound>
</policies>
```
**Forutsetninger for semantic caching:**
1. Azure API Management (alle tiers)
2. Azure Managed Redis med RediSearch-modul
3. Azure OpenAI Embeddings-deployment
4. Managed identity-autentisering
### Nar bruke semantic caching
| Bruksomrade | Egnet? | Begrunnelse |
|-------------|--------|-------------|
| FAQ-chatbot for innbyggere | Ja | Mange lignende sporsmaal |
| Intern kunnskapsbase-SOK | Ja | Gjentakende sporsmaal |
| Dokumentanalyse (unik input) | Nei | Unik input per dokument |
| Kreativ innholdsgenerering | Nei | Variasjon er onskelig |
| Klassifisering med fast prompt | Ja | Identisk/lignende input |
| Ovesettelse | Delvis | Identiske setninger kan caches |
## Edge Compute for pre-prosessering
### Pre-prosessering pa edge
For AI-arbeidslaster kan visse operasjoner kjores naermere brukeren:
| Operasjon | Kjor pa edge? | Teknologi |
|-----------|--------------|-----------|
| Input-validering | Ja | Azure Functions / Container Apps |
| Token-telling (estimat) | Ja | tiktoken lokalt |
| PII-deteksjon (enkel) | Ja | Regex-basert filtrering |
| Rate limiting | Ja | APIM / Front Door WAF |
| Request routing | Ja | Front Door Rules Engine |
| Prompt assembly | Ja | Edge function |
| AI-inferens | Nei | Krever GPU/TPU i backend |
| RAG retrieval | Delvis | Embedding pa edge, sok i backend |
### Azure Functions pa Edge (med Container Apps)
```python
# Edge pre-processing function
import re
from typing import Optional
def pre_process_ai_request(
user_input: str,
max_input_length: int = 10000
) -> dict:
"""Pre-prosesser AI-request pa edge for lavere latens og sikkerhet."""
result = {
"processed_input": user_input,
"metadata": {},
"blocked": False
}
# 1. Inputvalidering
if len(user_input) > max_input_length:
result["processed_input"] = user_input[:max_input_length]
result["metadata"]["truncated"] = True
# 2. Enkel PII-deteksjon (pre-filtering)
pii_patterns = {
"fodselsnummer": r'\b\d{6}\s?\d{5}\b', # Norsk fodselsnummer
"kontonummer": r'\b\d{4}\.\d{2}\.\d{5}\b',
"telefonnummer": r'\b(?:\+47)?\s?\d{3}\s?\d{2}\s?\d{3}\b'
}
detected_pii = []
for pii_type, pattern in pii_patterns.items():
if re.search(pattern, user_input):
detected_pii.append(pii_type)
if detected_pii:
result["metadata"]["detected_pii"] = detected_pii
# Vurder a blokkere eller varsle basert pa policy
# 3. Token-estimat (uten full tiktoken)
estimated_tokens = len(user_input.split()) * 1.3
result["metadata"]["estimated_tokens"] = int(estimated_tokens)
return result
```
### Request Routing basert pa innhold
```xml
<!-- Front Door Rules Engine: Rut basert pa request-egenskaper -->
<rules>
<rule name="route-simple-queries">
<!-- Korte requests -> GPT-4o mini for lavest latens -->
<conditions>
<condition>
<matchVariable>RequestBody</matchVariable>
<operator>LengthLessThan</operator>
<matchValues>500</matchValues>
</condition>
</conditions>
<actions>
<routeConfigurationOverride>
<originGroup>/originGroups/fast-model-backends</originGroup>
</routeConfigurationOverride>
</actions>
</rule>
<rule name="route-complex-queries">
<!-- Lange requests -> GPT-4o for bedre kvalitet -->
<conditions>
<condition>
<matchVariable>RequestBody</matchVariable>
<operator>LengthGreaterThan</operator>
<matchValues>2000</matchValues>
</condition>
</conditions>
<actions>
<routeConfigurationOverride>
<originGroup>/originGroups/quality-model-backends</originGroup>
</routeConfigurationOverride>
</actions>
</rule>
</rules>
```
## Geografisk routing og optimalisering
### Trafikkruting for Norge
For norsk offentlig sektor med brukere over hele landet:
| Brukerplassering | Naermeste Edge PoP | Backend-region | Forventet latens |
|-----------------|-------------------|----------------|-----------------|
| Oslo/Ostlandet | Oslo/Stockholm | Sweden Central | 5-15 ms |
| Bergen/Vestland | Amsterdam/Stockholm | Sweden Central | 15-25 ms |
| Tromso/Nord-Norge | Stockholm | Sweden Central | 20-35 ms |
| Trondheim/Trondelag | Stockholm | Sweden Central | 15-25 ms |
### Multi-region deployment med Azure Front Door
```bicep
// Geografisk routing-konfigurasjon
resource routePolicy 'Microsoft.Cdn/profiles/afdEndpoints/routes@2023-05-01' = {
parent: aiEndpoint
name: 'geo-optimized-route'
properties: {
originGroup: { id: aiOriginGroup.id }
patternsToMatch: ['/api/*']
supportedProtocols: ['Https']
// Front Door bruker anycast for automatisk naermeste-edge-routing
// Backend-valg baseres pa latens + health probes
}
}
```
### Latensbasert routing
Azure Front Door velger automatisk backend med lavest latens:
```
1. Bruker i Tromso sender request
2. DNS resolver -> naermeste Front Door PoP (Stockholm)
3. Front Door maler latens til alle backends:
- Sweden Central: 10 ms
- North Europe: 35 ms
4. Request rutes til Sweden Central
5. Hvis Sweden Central er nede: automatisk failover til North Europe
```
### Health Probes for AI-backends
```python
# Health endpoint for AI-tjeneste
from fastapi import FastAPI
import time
app = FastAPI()
# Enkel health check
@app.get("/health")
async def health():
return {"status": "healthy", "timestamp": time.time()}
# Detaljert health check (for intern bruk, ikke via Front Door)
@app.get("/health/detailed")
async def detailed_health():
checks = {}
# Sjekk Azure OpenAI-tilgang
try:
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "ping"}],
max_tokens=1
)
checks["azure_openai"] = "healthy"
except Exception as e:
checks["azure_openai"] = f"unhealthy: {str(e)}"
# Sjekk vector store
try:
await search_client.search("test", top=1)
checks["search_index"] = "healthy"
except Exception:
checks["search_index"] = "unhealthy"
overall = "healthy" if all(v == "healthy" for v in checks.values()) else "degraded"
return {"status": overall, "checks": checks}
```
## DDoS-beskyttelse for AI-endepunkter
### Front Door + WAF for AI-APIer
AI-endepunkter er spesielt sarbare for misbruk pa grunn av hoye kostnader per request:
```bicep
resource wafPolicy 'Microsoft.Network/FrontDoorWebApplicationFirewallPolicies@2022-05-01' = {
name: 'waf-ai-protection'
location: 'global'
properties: {
policySettings: {
enabledState: 'Enabled'
mode: 'Prevention'
}
customRules: {
rules: [
{
name: 'RateLimitAIEndpoints'
priority: 100
ruleType: 'RateLimitRule'
rateLimitDurationInMinutes: 1
rateLimitThreshold: 100 // Maks 100 requests per minutt per IP
matchConditions: [
{
matchVariable: 'RequestUri'
operator: 'Contains'
matchValue: ['/api/chat', '/api/completions']
}
]
action: 'Block'
}
{
name: 'BlockLargePayloads'
priority: 200
ruleType: 'MatchRule'
matchConditions: [
{
matchVariable: 'RequestBody'
operator: 'GreaterThan'
matchValue: ['1048576'] // 1 MB maks request body
transforms: ['Trim']
}
]
action: 'Block'
}
]
}
}
}
```
## Ytelsesgevinster: Oppsummering
| Teknikk | Typisk latensreduksjon | Kostnadsreduksjon | Kompleksitet |
|---------|----------------------|-------------------|-------------|
| Front Door TLS-terminering | 50-100 ms | Ingen | Lav |
| Traffic acceleration (split TCP) | 30-40% dynamisk | Ingen | Lav |
| Static asset caching | 90%+ for assets | Redusert backend-trafikk | Lav |
| Semantic caching | 80-95% ved hit | Eliminerer AI-kall ved hit | Hoy |
| Edge pre-processing | 10-50 ms | Blokkerer unodvendige kall | Medium |
| Geographic routing | 10-40 ms | Ingen direkte | Lav |
| DDoS/rate limiting | Indirekte (beskyttelse) | Hindrer misbrukskostnader | Medium |
## For Cosmo
- **Azure Front Door er obligatorisk** for alle publiserte AI-endepunkter. Det gir TLS-terminering, DDoS-beskyttelse, geographic routing og traffic acceleration med minimal konfigurasjon.
- **Cache ALDRI chat completion-responser.** Feilkonfigurert caching kan lekke personopplysninger mellom brukere. Kun statisk innhold, modellmetadata og embeddings kan caches trygt.
- **Semantic caching via APIM + Redis** er den mest verdifulle cache-teknikken for AI. For FAQ-chatbots kan det eliminere 50-70% av backend-kall og redusere bade latens og kostnad.
- **Edge pre-processing** (PII-deteksjon, inputvalidering, token-estimat) reduserer unodvendig backend-trafikk og forbedrer sikkerhet. Implementer som en enkel middleware foran AI-endepunktet.
- **Rate limiting pa WAF-niva** er kritisk for AI-endepunkter fordi hvert kall har hoy kostnad. Sett restriktive grenser (50-200 requests/min per IP) og juster etter behov.

View file

@ -0,0 +1,431 @@
# Concurrent Request Optimization
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Concurrent request optimization handler om å maksimere antall samtidige forespørsler mot Azure OpenAI uten å overbelaste tjenesten eller miste forespørsler. Den optimale graden av samtidighet avhenger av deployment-type (Standard vs. PTU), tildelt kvote (TPM/RPM), modellens responstid og klientens evne til å håndtere parallelle forbindelser.
For Standard deployments bestemmer RPM-kvoten den harde grensen for samtidige forespørsler per minutt, men den faktiske grensen er ofte lavere fordi lange forespørsler blokkerer kapasitet. For PTU deployments er grensen definert av utilization — når prosessert kapasitet nærmer seg 100% av tildelte PTUs, begynner 429-feil. I begge tilfeller er nøkkelen å finne sweet spot der throughput er maksimert uten overdreven throttling.
For norsk offentlig sektor, der AI-applikasjoner kan betjene hundrevis av samtidige saksbehandlere, er concurrent request optimization avgjørende for å sikre jevn brukeropplevelse uten at noen opplever timeout eller feil.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Semaphore | Begrens concurrent requests klient-side | asyncio / SemaphoreSlim |
| Token Bucket | Rate limiting med burst-støtte | Custom / APIM |
| Connection Pool | Gjenbruk HTTP-forbindelser | HttpClient / aiohttp |
| Circuit Breaker | Forhindre kaskade ved overbelastning | Polly / custom |
| Queue | Buffer forespørsler under peak | Service Bus / in-memory |
## Concurrency Level Tuning
### Finn optimal concurrency
```python
import asyncio
import time
from openai import AsyncAzureOpenAI, RateLimitError
async def find_optimal_concurrency(
client: AsyncAzureOpenAI,
model: str = "gpt-4o",
test_prompt: str = "Oppsummer dette kort.",
max_tokens: int = 200,
test_levels: list[int] = None,
requests_per_level: int = 50
) -> dict:
"""Find optimal concurrency level through progressive testing."""
if test_levels is None:
test_levels = [1, 5, 10, 20, 30, 50, 75, 100]
results = []
for concurrency in test_levels:
semaphore = asyncio.Semaphore(concurrency)
stats = {"success": 0, "throttled": 0, "errors": 0, "latencies": []}
async def send_one():
async with semaphore:
start = time.time()
try:
await client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": test_prompt}],
max_tokens=max_tokens
)
stats["latencies"].append(
(time.time() - start) * 1000)
stats["success"] += 1
except RateLimitError:
stats["throttled"] += 1
except Exception:
stats["errors"] += 1
start = time.time()
await asyncio.gather(
*[send_one() for _ in range(requests_per_level)])
duration = time.time() - start
total = stats["success"] + stats["throttled"] + stats["errors"]
throttle_rate = stats["throttled"] / max(total, 1) * 100
result = {
"concurrency": concurrency,
"throughput_rps": round(stats["success"] / duration, 2),
"throttle_rate_pct": round(throttle_rate, 1),
"p50_ms": round(sorted(stats["latencies"])[
len(stats["latencies"]) // 2], 0)
if stats["latencies"] else 0,
"p95_ms": round(sorted(stats["latencies"])[
int(len(stats["latencies"]) * 0.95)], 0)
if stats["latencies"] else 0,
"success": stats["success"],
"throttled": stats["throttled"]
}
results.append(result)
print(f"Concurrency {concurrency}: "
f"{result['throughput_rps']} RPS, "
f"{throttle_rate:.1f}% throttled, "
f"P50={result['p50_ms']}ms")
# Stopp hvis throttle rate er for høy
if throttle_rate > 30:
print(f"Stopping: throttle rate too high at {concurrency}")
break
# Finn optimal: best throughput med <5% throttling
acceptable = [r for r in results if r["throttle_rate_pct"] < 5]
if acceptable:
optimal = max(acceptable, key=lambda r: r["throughput_rps"])
else:
optimal = results[0]
return {
"optimal_concurrency": optimal["concurrency"],
"optimal_throughput_rps": optimal["throughput_rps"],
"all_results": results
}
```
### Adaptive concurrency control
```python
class AdaptiveConcurrencyController:
"""Dynamically adjust concurrency based on response signals."""
def __init__(
self,
initial_concurrency: int = 10,
min_concurrency: int = 1,
max_concurrency: int = 100,
increase_threshold: float = 0.02, # Øk hvis <2% throttled
decrease_threshold: float = 0.10, # Reduser hvis >10% throttled
adjustment_interval: float = 10.0 # Juster hvert 10. sekund
):
self.current = initial_concurrency
self.min_concurrency = min_concurrency
self.max_concurrency = max_concurrency
self.increase_threshold = increase_threshold
self.decrease_threshold = decrease_threshold
self.adjustment_interval = adjustment_interval
self._semaphore = asyncio.Semaphore(initial_concurrency)
self._window_success = 0
self._window_throttled = 0
self._last_adjustment = time.time()
async def acquire(self):
"""Acquire a concurrency slot."""
await self._semaphore.acquire()
def release(self, was_throttled: bool = False):
"""Release slot and record outcome."""
self._semaphore.release()
if was_throttled:
self._window_throttled += 1
else:
self._window_success += 1
self._maybe_adjust()
def _maybe_adjust(self):
"""Periodically adjust concurrency."""
now = time.time()
if now - self._last_adjustment < self.adjustment_interval:
return
total = self._window_success + self._window_throttled
if total < 10: # Ikke nok data
return
throttle_rate = self._window_throttled / total
old = self.current
if throttle_rate < self.increase_threshold:
# Trygt å øke
self.current = min(
self.current + max(1, self.current // 10),
self.max_concurrency)
elif throttle_rate > self.decrease_threshold:
# Må redusere
self.current = max(
self.current - max(1, self.current // 5),
self.min_concurrency)
if self.current != old:
# Opprett ny semaphore med justert limit
self._semaphore = asyncio.Semaphore(self.current)
print(f"Concurrency adjusted: {old} → {self.current} "
f"(throttle rate: {throttle_rate:.1%})")
self._window_success = 0
self._window_throttled = 0
self._last_adjustment = now
```
## Request Queueing Strategies
### Priority queue for AI-forespørsler
```python
import asyncio
import heapq
from enum import IntEnum
from dataclasses import dataclass, field
from typing import Any
class Priority(IntEnum):
URGENT = 1
HIGH = 2
NORMAL = 3
LOW = 4
BACKGROUND = 5
@dataclass(order=True)
class PrioritizedRequest:
priority: int
timestamp: float = field(compare=True)
request: Any = field(compare=False)
future: asyncio.Future = field(compare=False, repr=False)
class PriorityRequestQueue:
"""Priority queue for AI requests with concurrency control."""
def __init__(self, max_concurrent: int = 20):
self._queue: list[PrioritizedRequest] = []
self._semaphore = asyncio.Semaphore(max_concurrent)
self._processing = True
async def submit(
self,
request: dict,
priority: Priority = Priority.NORMAL
) -> asyncio.Future:
"""Submit request with priority. Returns future."""
future = asyncio.get_event_loop().create_future()
item = PrioritizedRequest(
priority=priority.value,
timestamp=time.time(),
request=request,
future=future
)
heapq.heappush(self._queue, item)
return future
async def process_loop(self, process_fn):
"""Continuously process queued requests."""
while self._processing:
if not self._queue:
await asyncio.sleep(0.01)
continue
await self._semaphore.acquire()
item = heapq.heappop(self._queue)
asyncio.create_task(
self._process_item(item, process_fn))
async def _process_item(self, item, process_fn):
try:
result = await process_fn(item.request)
if not item.future.done():
item.future.set_result(result)
except Exception as e:
if not item.future.done():
item.future.set_exception(e)
finally:
self._semaphore.release()
```
## Deadlock Prevention
### Unngå resource starvation
```python
class DeadlockPreventionWrapper:
"""Prevent deadlocks in concurrent AI request processing."""
def __init__(
self,
client: AsyncAzureOpenAI,
max_concurrent: int = 20,
request_timeout: float = 120.0,
starvation_timeout: float = 300.0
):
self.client = client
self.semaphore = asyncio.Semaphore(max_concurrent)
self.request_timeout = request_timeout
self.starvation_timeout = starvation_timeout
self._active_requests: dict[str, float] = {}
async def execute(self, request_id: str, **kwargs):
"""Execute with timeout and starvation protection."""
# Timeout på semaphore acquire — forhindrer deadlock
try:
await asyncio.wait_for(
self.semaphore.acquire(),
timeout=self.starvation_timeout
)
except asyncio.TimeoutError:
raise TimeoutError(
f"Request {request_id} starved waiting for "
f"concurrency slot for {self.starvation_timeout}s. "
f"Consider increasing max_concurrent or reducing "
f"request volume.")
self._active_requests[request_id] = time.time()
try:
# Timeout på selve forespørselen
result = await asyncio.wait_for(
self.client.chat.completions.create(**kwargs),
timeout=self.request_timeout
)
return result
except asyncio.TimeoutError:
raise TimeoutError(
f"Request {request_id} timed out after "
f"{self.request_timeout}s")
finally:
self._active_requests.pop(request_id, None)
self.semaphore.release()
@property
def active_count(self) -> int:
return len(self._active_requests)
def get_stuck_requests(self, threshold_seconds: float = 60) -> list:
"""Identify requests that may be stuck."""
now = time.time()
return [
{"id": rid, "age_seconds": round(now - start, 1)}
for rid, start in self._active_requests.items()
if now - start > threshold_seconds
]
```
## Resource Contention Resolution
### Token bucket for fair scheduling
```python
import time
class TokenBucket:
"""Token bucket rate limiter for fair resource sharing."""
def __init__(
self,
tokens_per_second: float,
max_burst: int = 10
):
self.rate = tokens_per_second
self.max_burst = max_burst
self._tokens = max_burst
self._last_refill = time.time()
self._lock = asyncio.Lock()
async def acquire(self, tokens: int = 1) -> float:
"""Acquire tokens, waiting if necessary. Returns wait time."""
async with self._lock:
self._refill()
if self._tokens >= tokens:
self._tokens -= tokens
return 0
# Beregn ventetid
deficit = tokens - self._tokens
wait_time = deficit / self.rate
await asyncio.sleep(wait_time)
self._refill()
self._tokens -= tokens
return wait_time
def _refill(self):
now = time.time()
elapsed = now - self._last_refill
self._tokens = min(
self.max_burst,
self._tokens + elapsed * self.rate
)
self._last_refill = now
class FairScheduler:
"""Fair scheduling across multiple tenants/users."""
def __init__(self, total_rps: float, num_tenants: int):
self.total_rps = total_rps
per_tenant_rps = total_rps / num_tenants
self.buckets: dict[str, TokenBucket] = {}
self._default_rps = per_tenant_rps
def get_bucket(self, tenant_id: str) -> TokenBucket:
if tenant_id not in self.buckets:
self.buckets[tenant_id] = TokenBucket(
tokens_per_second=self._default_rps,
max_burst=int(self._default_rps * 2)
)
return self.buckets[tenant_id]
```
## Norsk offentlig sektor
- **Fair use**: I multi-tenant løsninger der flere enheter deler samme Azure OpenAI-deployment, bruk per-tenant rate limiting for å sikre rettferdig fordeling.
- **Brukeropplevelse**: Sett starvation timeout til maks ventetid brukere aksepterer (typisk 30-60 sekunder). Returner informativ feilmelding ved timeout.
- **Overvåking**: Logg concurrent request-nivå, kø-dybde og starvation-hendelser i Application Insights for kapasitetsplanlegging.
- **Skalering**: Planlegg for peak-perioder (morgen 08-10, etter lunsj 12-13) med høyere concurrent limits eller ekstra kvote.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Ukjent workload | Start med 10 concurrent, juster | Unngå initial throttling |
| Forutsigbar, jevn trafikk | Statisk semaphore på optimal nivå | Enklest å implementere |
| Variable peaks | Adaptive concurrency controller | Automatisk tilpasning |
| Multi-tenant | Priority queue + per-tenant bucket | Fair resource sharing |
| Kritisk latens | Lav concurrency + PTU | Forutsigbar responstid |
## Referanser
- [Manage Azure OpenAI quota](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/quota) — RPM/TPM grenser
- [Performance and latency](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/latency) — Concurrent requests og throughput
- [Provisioned throughput](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/provisioned-get-started) — PTU utilization
## For Cosmo
- **Bruk denne referansen** når kunden opplever timeout, starvation eller ujevn ytelse i AI-applikasjoner med mange samtidige brukere.
- Start konservativt (10-20 concurrent) og øk gradvis mens du monitorerer throttle rate — aldri gå rett til 100 concurrent.
- Adaptive concurrency control er anbefalt for produksjon — statiske verdier fungerer dårlig når trafikkmønstre endres.
- Prioritetskøer er viktige for multi-tenant: sørg for at kritiske oppgaver (saksbehandler-beslutninger) ikke blokkeres av bakgrunnsjobber.
- Deadlock prevention med timeouts er obligatorisk — uten det kan en hengende forespørsel blokkere alle slots permanent.

View file

@ -0,0 +1,350 @@
# Connection Pooling Patterns
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Connection pooling er en kritisk ytelsesoptimalisering for applikasjoner som kommuniserer med Azure AI Services. Hver HTTP-forbindelse til Azure OpenAI eller andre AI-endepunkter krever TCP-håndtrykk og eventuelt TLS-forhandling, noe som legger til betydelig latens per forespørsel. Uten connection pooling opprettes og lukkes forbindelser for hver eneste forespørsel, noe som fører til port-utmattelse, økt responstid og unødvendig CPU-bruk.
I .NET-økosystemet er `HttpClient` den sentrale klassen for HTTP-kommunikasjon, og Microsofts offisielle retningslinjer er tydelige: bruk én statisk `HttpClient`-instans per logisk klient, eller bruk `IHttpClientFactory` for å håndtere DNS-endringer og connection lifecycle. Azure OpenAI SDK-ene (både Python og C#) håndterer connection pooling internt, men korrekt konfigurasjon er fortsatt nødvendig for optimal ytelse.
For norsk offentlig sektor der AI-tjenester typisk nås via private endpoints og ofte går gjennom Azure API Management, er connection pooling spesielt viktig. Nettverkskjeden (klient → APIM → Private Endpoint → Azure OpenAI) multipliserer latens-kostnaden av nye forbindelser, og korrekt pooling kan redusere p50-latens med 30-50 ms per forespørsel.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| HttpClient / HttpClientFactory | HTTP connection management i .NET | System.Net.Http |
| SocketsHttpHandler | Underliggende socket-håndtering med pool | .NET 6+ |
| aiohttp.ClientSession | Asynkron HTTP med connection pool i Python | aiohttp |
| httpx.AsyncClient | Moderne asynkron HTTP-klient med pool | httpx |
| Azure OpenAI SDK | Innebygd connection management | azure-ai-openai |
| Azure API Management | Gateway med backend connection pooling | Azure APIM |
## Pool Sizing-strategier
### Beregning av optimal pool-størrelse
Pool-størrelsen bør baseres på forventet concurrent request-volum og responstid fra backend-tjenesten.
```python
# Beregn optimal pool-størrelse
# Formel: pool_size = concurrent_requests * avg_response_time_seconds / target_utilization
def calculate_pool_size(
concurrent_users: int,
avg_response_time_ms: float,
requests_per_user_per_second: float = 1.0,
target_utilization: float = 0.75
) -> int:
"""Calculate optimal connection pool size for AI workloads."""
concurrent_requests = concurrent_users * requests_per_user_per_second
avg_response_time_s = avg_response_time_ms / 1000
# Connections needed = concurrent requests * time each holds a connection
connections_needed = concurrent_requests * avg_response_time_s
# Add headroom for bursts
pool_size = int(connections_needed / target_utilization)
# Azure OpenAI typical ranges
return max(pool_size, 10) # Minimum 10 connections
# Eksempel: 50 samtidige brukere, 800ms avg responstid
optimal = calculate_pool_size(
concurrent_users=50,
avg_response_time_ms=800,
requests_per_user_per_second=0.5
)
print(f"Anbefalt pool-størrelse: {optimal}") # ~27 connections
```
### .NET HttpClientFactory-konfigurasjon
```csharp
// Program.cs - Optimal HttpClient-konfigurasjon for Azure OpenAI
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
var builder = WebApplication.CreateBuilder(args);
// Registrer named HttpClient for Azure OpenAI
builder.Services.AddHttpClient("AzureOpenAI", client =>
{
client.BaseAddress = new Uri(
builder.Configuration["AzureOpenAI:Endpoint"]!);
client.DefaultRequestHeaders.Add("api-key",
builder.Configuration["AzureOpenAI:ApiKey"]!);
client.Timeout = TimeSpan.FromSeconds(120);
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
// Pool-konfigurasjon
MaxConnectionsPerServer = 50, // Maks connections per host
PooledConnectionLifetime = TimeSpan.FromMinutes(5), // DNS refresh
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
// Keep-alive
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests,
KeepAlivePingDelay = TimeSpan.FromSeconds(30),
KeepAlivePingTimeout = TimeSpan.FromSeconds(10),
// Performance
EnableMultipleHttp2Connections = true,
AutomaticDecompression = System.Net.DecompressionMethods.GZip
})
.SetHandlerLifetime(TimeSpan.FromMinutes(10)); // Handler rotation
// Registrer Azure OpenAI-klient med pooled HttpClient
builder.Services.AddSingleton(sp =>
{
var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient("AzureOpenAI");
return new Azure.AI.OpenAI.AzureOpenAIClient(
new Uri(builder.Configuration["AzureOpenAI:Endpoint"]!),
new Azure.AzureKeyCredential(
builder.Configuration["AzureOpenAI:ApiKey"]!));
});
```
## Keep-alive-konfigurasjon
### HTTP/2 Multiplexing
Azure OpenAI støtter HTTP/2, som muliggjør multipleksing av flere forespørsler over én enkelt TCP-forbindelse:
```csharp
// HTTP/2 multiplexing for Azure OpenAI
var handler = new SocketsHttpHandler
{
// Aktiver HTTP/2 med multipleksing
EnableMultipleHttp2Connections = true,
// Keep-alive for langvarige streams
KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always,
KeepAlivePingDelay = TimeSpan.FromSeconds(15),
KeepAlivePingTimeout = TimeSpan.FromSeconds(5),
// Connection lifecycle
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
MaxConnectionsPerServer = 100
};
var client = new HttpClient(handler)
{
DefaultRequestVersion = HttpVersion.Version20,
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower
};
```
### Python aiohttp Session Management
```python
import aiohttp
import asyncio
from openai import AsyncAzureOpenAI
async def create_optimized_session() -> aiohttp.ClientSession:
"""Create an optimized aiohttp session for Azure OpenAI."""
connector = aiohttp.TCPConnector(
limit=100, # Total connection pool size
limit_per_host=50, # Max connections per host
ttl_dns_cache=300, # DNS cache TTL i sekunder
keepalive_timeout=30, # Keep-alive timeout
enable_cleanup_closed=True,
force_close=False # Gjenbruk connections
)
timeout = aiohttp.ClientTimeout(
total=120, # Total timeout
connect=10, # Connection timeout
sock_read=60 # Read timeout
)
return aiohttp.ClientSession(
connector=connector,
timeout=timeout,
headers={"Connection": "keep-alive"}
)
# Bruk med Azure OpenAI Python SDK
async def create_pooled_openai_client() -> AsyncAzureOpenAI:
"""Create Azure OpenAI client with optimized connection pooling."""
import httpx
transport = httpx.AsyncHTTPTransport(
retries=3,
limits=httpx.Limits(
max_connections=100,
max_keepalive_connections=50,
keepalive_expiry=30
)
)
http_client = httpx.AsyncClient(
transport=transport,
timeout=httpx.Timeout(120.0, connect=10.0)
)
return AsyncAzureOpenAI(
azure_endpoint="https://my-aoai.openai.azure.com",
api_key="...",
api_version="2024-10-21",
http_client=http_client
)
```
## Connection Recycling
### Håndtering av DNS-endringer
Når Azure OpenAI-endepunkter er bak Traffic Manager eller Azure Front Door, endres IP-adresser regelmessig. Connection pooling må balansere gjenbruk med DNS-fornyelse:
```csharp
// Connection recycling pattern
public class ConnectionRecyclingConfig
{
// PooledConnectionLifetime: Tvinger nye DNS-oppslag
// Sett lavere enn DNS TTL for failover-scenarier
public TimeSpan PooledConnectionLifetime { get; set; }
= TimeSpan.FromMinutes(5);
// PooledConnectionIdleTimeout: Fjern ubrukte connections
public TimeSpan PooledConnectionIdleTimeout { get; set; }
= TimeSpan.FromMinutes(2);
// Handler lifetime for IHttpClientFactory
// Roterer hele handleren — nye connections med ny DNS
public TimeSpan HandlerLifetime { get; set; }
= TimeSpan.FromMinutes(10);
}
// Anti-pattern: ALDRI gjør dette
// ❌ var client = new HttpClient(); // Ny for hver forespørsel
// ❌ using var client = new HttpClient(); // Disponeres for tidlig
// Korrekt pattern: Singleton eller factory
// ✅ Statisk HttpClient med SocketsHttpHandler
// ✅ IHttpClientFactory i DI
```
## Load Distribution
### Round-robin over multiple Azure OpenAI-instanser
```python
import asyncio
import random
from dataclasses import dataclass, field
from typing import Optional
from openai import AsyncAzureOpenAI
@dataclass
class AzureOpenAIBackend:
endpoint: str
api_key: str
priority: int = 1
is_healthy: bool = True
retry_after: Optional[float] = None
client: Optional[AsyncAzureOpenAI] = field(default=None, repr=False)
class ConnectionPoolLoadBalancer:
"""Load balancer with dedicated connection pools per backend."""
def __init__(self, backends: list[AzureOpenAIBackend]):
self.backends = backends
# Separat connection pool per backend
for backend in self.backends:
backend.client = AsyncAzureOpenAI(
azure_endpoint=backend.endpoint,
api_key=backend.api_key,
api_version="2024-10-21",
max_retries=0 # Vi håndterer retry selv
)
def _select_backend(self) -> AzureOpenAIBackend:
"""Select backend by priority, then random among same priority."""
import time
# Filtrer friske backends
available = [
b for b in self.backends
if b.is_healthy or (
b.retry_after and time.time() > b.retry_after
)
]
if not available:
available = self.backends # Fallback til alle
# Velg laveste prioritet (høyest prioritet)
min_priority = min(b.priority for b in available)
candidates = [b for b in available if b.priority == min_priority]
return random.choice(candidates)
async def chat_completion(self, messages: list, **kwargs):
"""Route request to best available backend."""
import time
for attempt in range(len(self.backends)):
backend = self._select_backend()
try:
response = await backend.client.chat.completions.create(
messages=messages, **kwargs
)
backend.is_healthy = True
return response
except Exception as e:
if hasattr(e, 'status_code') and e.status_code == 429:
retry_after = getattr(e, 'retry_after', 10)
backend.retry_after = time.time() + retry_after
backend.is_healthy = False
elif hasattr(e, 'status_code') and e.status_code >= 500:
backend.is_healthy = False
else:
raise
raise Exception("All backends exhausted")
```
## Norsk offentlig sektor
Connection pooling har spesielle hensyn for norsk offentlig sektor:
- **Data residency**: Alle connections må gå via regioner som oppfyller Schrems II-kravene. Ved bruk av Azure Norway East som primær region, konfigurer `PooledConnectionLifetime` kort nok til å håndtere failover til Sweden Central.
- **Private endpoints**: Connection pools som bruker Private Link har andre DNS-oppløsningsmønstre. Konfigurer `ttl_dns_cache` i Python eller `PooledConnectionLifetime` i .NET til å matche DNS TTL for privatelink-soner (typisk 10 sekunder).
- **NSMs grunnprinsipper**: Logging av connection pool-metrikker (aktive connections, pool hits/misses, connection errors) er påkrevd for å oppfylle krav om overvåking av nettverkstrafikk.
- **Anskaffelsesreglement**: Ved bruk av tredjepartsbiblioteker for connection pooling, verifiser at de er godkjent for bruk i offentlig sektor (åpen kildekode med akseptabel lisens).
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Enkelttjeneste, lav trafikk (<10 RPS) | Statisk HttpClient med default pool | Enkel oppsett, tilstrekkelig ytelse |
| Enkelttjeneste, høy trafikk (>50 RPS) | HttpClientFactory med SocketsHttpHandler | DNS rotation, pool sizing, monitoring |
| Multi-region med failover | Separat pool per region + load balancer | Isolerer feil, støtter weighted routing |
| Via Azure APIM | APIM backend pool + klient-side pool | APIM håndterer backend-balansering |
| Streaming/SSE-respons | Dedikert pool med lange timeouts | Streaming holder connections åpne lenger |
| Azure Functions (serverless) | Static HttpClient i startup | Unngå cold start connection overhead |
## Referanser
- [Guidelines for using HttpClient](https://learn.microsoft.com/dotnet/fundamentals/networking/http/httpclient-guidelines) — HttpClient best practices
- [Pool HTTP connections with HttpClientFactory](https://learn.microsoft.com/aspnet/core/performance/performance-best-practices) — ASP.NET performance
- [Manage connections in Azure Functions](https://learn.microsoft.com/azure/azure-functions/manage-connections) — Serverless connection management
- [Use a gateway in front of multiple Azure OpenAI deployments](https://learn.microsoft.com/azure/architecture/ai-ml/guide/azure-openai-gateway-multi-backend) — Multi-backend gateway patterns
## For Cosmo
- **Bruk denne referansen** når kunden rapporterer høy latens, port-utmattelse, eller timeout-feil mot Azure OpenAI — connection pooling er ofte root cause.
- Anbefal `IHttpClientFactory` for .NET og `httpx.AsyncClient` med `Limits` for Python — aldri instansier `HttpClient` per forespørsel.
- For multi-region AI-arkitekturer, opprett separate connection pools per region med individuelle health checks og retry-after tracking.
- Sett `PooledConnectionLifetime` til 2-5 minutter for å balansere DNS-fornyelse med connection gjenbruk — kortere for failover-scenarier.
- Monitorer alltid `connections.active`, `connections.idle` og `pool.exhausted` metrikker i Application Insights for å oppdage pool-problemer tidlig.

View file

@ -0,0 +1,362 @@
# GPU and Compute Sizing for AI
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
GPU- og compute-dimensjonering for AI-workloads på Azure handler om å velge riktig balanse mellom ytelse, kostnad og tilgjengelighet. For de fleste organisasjoner som bruker Azure OpenAI Service er GPU-valg abstrahert bak Provisioned Throughput Units (PTU) — du spesifiserer ønsket throughput, og Azure allokerer nødvendig GPU-kapasitet. Men for custom model hosting via Azure Machine Learning, Azure Kubernetes Service eller Azure Container Instances er eksplisitt GPU-valg nødvendig.
Azure tilbyr et bredt spekter av GPU-akselererte VM-serier: NC-serien (NVIDIA T4) for inferens, ND-serien (NVIDIA A100/H100) for trening, og NV-serien for visualisering. For AI-inferens er de viktigste faktorene GPU-minne (for modellstørrelse), compute throughput (TFLOPS), og minnebåndbredde (GB/s). For Azure OpenAI PTU-deployments håndterer Microsoft denne dimensjoneringen — din oppgave er å estimere PTU-behov basert på workload shape.
For norsk offentlig sektor er GPU-dimensjonering relevant ved deployment av open-source modeller, fine-tuned modeller som hostes on-premises eller i Azure ML, og ved evaluering av PTU vs. Standard deployments for Azure OpenAI.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Azure OpenAI PTU | Managed GPU-kapasitet for OpenAI-modeller | Azure OpenAI |
| NC-series VMs | NVIDIA T4 — kostnadseffektiv inferens | Azure VMs |
| ND-series VMs | NVIDIA A100/H100 — trening og stor-modell inferens | Azure VMs |
| Azure ML Endpoints | Managed inferens med GPU-akselerasjon | Azure ML |
| Azure Container Apps | GPU-støtte for containerisert AI | Azure Container Apps |
| Capacity Calculator | PTU-estimering verktøy | Azure AI Foundry |
## GPU Type Comparison
### Azure GPU VM-serier for AI
| VM-serie | GPU | GPU-minne | Use case | Pris-segment |
|----------|-----|-----------|----------|-------------|
| NC4as_T4_v3 | 1x NVIDIA T4 | 16 GB | Liten modell-inferens | Lavest |
| NC24ads_A100_v4 | 1x NVIDIA A100 | 80 GB | Medium modell-inferens/trening | Middels |
| NC96ads_A100_v4 | 4x NVIDIA A100 | 320 GB | Stor modell-trening | Høy |
| ND96asr_v4 | 8x NVIDIA A100 | 640 GB | LLM-trening, multi-GPU | Svært høy |
| ND96isr_H100_v5 | 8x NVIDIA H100 | 640 GB | Frontier-modell trening | Høyest |
| NC40ads_H100_v5 | 1x NVIDIA H100 | 80 GB | Stor modell-inferens | Høy |
### Modellstørrelse og GPU-krav
```python
def estimate_gpu_requirements(
model_params_billions: float,
precision: str = "fp16", # fp32, fp16, int8, int4
batch_size: int = 1,
sequence_length: int = 4096,
overhead_factor: float = 1.2 # 20% overhead for KV-cache etc.
) -> dict:
"""Estimate GPU memory requirements for model inference."""
bytes_per_param = {
"fp32": 4,
"fp16": 2,
"bf16": 2,
"int8": 1,
"int4": 0.5
}
if precision not in bytes_per_param:
raise ValueError(f"Unknown precision: {precision}")
# Modellvekter
model_memory_gb = (
model_params_billions * 1e9 *
bytes_per_param[precision] / 1e9
)
# KV-cache (estimat)
# KV cache ≈ 2 * num_layers * hidden_dim * seq_len * batch * bytes
# Forenklet estimat: ~10% av modellstørrelse per 4K context
kv_cache_gb = model_memory_gb * 0.1 * (sequence_length / 4096) * batch_size
# Total med overhead
total_gb = (model_memory_gb + kv_cache_gb) * overhead_factor
# Anbefalt GPU
gpu_options = [
{"name": "T4", "memory_gb": 16, "tflops_fp16": 65},
{"name": "A10G", "memory_gb": 24, "tflops_fp16": 125},
{"name": "A100 40GB", "memory_gb": 40, "tflops_fp16": 312},
{"name": "A100 80GB", "memory_gb": 80, "tflops_fp16": 312},
{"name": "H100 80GB", "memory_gb": 80, "tflops_fp16": 989},
]
suitable_gpus = []
for gpu in gpu_options:
gpus_needed = max(1, int(total_gb / gpu["memory_gb"]) + 1)
if gpus_needed <= 8: # Max 8 GPUs per node
suitable_gpus.append({
"gpu": gpu["name"],
"gpus_needed": gpus_needed,
"total_memory_gb": gpus_needed * gpu["memory_gb"],
"headroom_gb": round(
gpus_needed * gpu["memory_gb"] - total_gb, 1)
})
return {
"model_params_b": model_params_billions,
"precision": precision,
"model_memory_gb": round(model_memory_gb, 1),
"kv_cache_gb": round(kv_cache_gb, 1),
"total_required_gb": round(total_gb, 1),
"suitable_gpus": suitable_gpus
}
# Eksempler
print(estimate_gpu_requirements(7, "fp16")) # Llama 3 8B — 1x T4
print(estimate_gpu_requirements(70, "int8")) # Llama 3 70B — 1x A100 80GB
print(estimate_gpu_requirements(70, "fp16")) # Llama 3 70B — 2x A100 80GB
```
## Memory Requirements
### GPU-minne budsjett for inferens
```
Total GPU-minne behov:
┌─────────────────────────────────────────┐
│ Modellvekter (størst posten) │
│ ├── FP16: params * 2 bytes │
│ ├── INT8: params * 1 byte │
│ └── INT4: params * 0.5 bytes │
│ │
│ KV-cache (vokser med context length) │
│ ├── Per token: ~0.5-2 KB (avh. modell) │
│ └── 128K context: kan bli flere GB │
│ │
│ Aktiverings-minne (batch-avhengig) │
│ ├── Skalerer lineært med batch size │
│ └── Typisk 5-15% av modellstørrelse │
│ │
│ Overhead (CUDA, framework) │
│ └── Typisk 10-20% ekstra │
└─────────────────────────────────────────┘
```
### Azure ML Deployment Sizing
```python
# Azure ML endpoint deployment med GPU
from azure.ai.ml import MLClient
from azure.ai.ml.entities import (
ManagedOnlineDeployment,
ManagedOnlineEndpoint
)
# Definer endpoint
endpoint = ManagedOnlineEndpoint(
name="llama-inference",
auth_mode="key"
)
# GPU-deployment basert på modellstørrelse
deployment_configs = {
"small_model": { # 7B parameters
"instance_type": "Standard_NC4as_T4_v3",
"instance_count": 1,
"model_format": "int8",
"expected_tps": 30
},
"medium_model": { # 13B-34B parameters
"instance_type": "Standard_NC24ads_A100_v4",
"instance_count": 1,
"model_format": "fp16",
"expected_tps": 25
},
"large_model": { # 70B parameters
"instance_type": "Standard_NC48ads_A100_v4",
"instance_count": 1, # 2x A100 80GB
"model_format": "int8",
"expected_tps": 15
}
}
# Deploy
deployment = ManagedOnlineDeployment(
name="llama-70b-int8",
endpoint_name=endpoint.name,
model="azureml://registries/azureml-meta/models/Llama-3.3-70B-Instruct",
instance_type="Standard_NC48ads_A100_v4",
instance_count=2, # For high availability
request_settings={
"request_timeout_ms": 120000,
"max_concurrent_requests_per_instance": 10
},
liveness_probe={"initial_delay": 600},
environment_variables={
"TENSOR_PARALLEL_SIZE": "2", # Shard modell over 2 GPUs
"MAX_MODEL_LEN": "8192",
"GPU_MEMORY_UTILIZATION": "0.9"
}
)
```
## Batch Size Influence
### Batch size vs. throughput vs. latens
```python
def model_batch_size_tradeoff(
gpu_memory_gb: float,
model_memory_gb: float,
kv_cache_per_request_gb: float,
target_latency_ms: float
) -> dict:
"""Model the relationship between batch size and performance."""
available_memory = gpu_memory_gb - model_memory_gb
max_batch_from_memory = int(available_memory / kv_cache_per_request_gb)
results = []
for batch_size in range(1, min(max_batch_from_memory + 1, 65)):
# Throughput øker med batch size (GPU utilization)
# Men per-request latens øker også
memory_used = model_memory_gb + (
batch_size * kv_cache_per_request_gb)
utilization = min(memory_used / gpu_memory_gb, 0.95)
# Throughput skalerer sub-lineært med batch size
throughput_factor = batch_size ** 0.7 # Empirisk
latency_factor = 1 + (batch_size - 1) * 0.15 # +15% per ekstra request
estimated_latency = target_latency_ms * latency_factor
results.append({
"batch_size": batch_size,
"memory_gb": round(memory_used, 1),
"utilization_pct": round(utilization * 100, 1),
"relative_throughput": round(throughput_factor, 2),
"estimated_latency_ms": round(estimated_latency)
})
# Finn sweet spot: beste throughput innenfor latens-krav
acceptable = [
r for r in results
if r["estimated_latency_ms"] <= target_latency_ms * 3
]
optimal = max(acceptable, key=lambda r: r["relative_throughput"])
return {
"max_batch_from_memory": max_batch_from_memory,
"optimal_batch_size": optimal["batch_size"],
"all_results": results[:10] # Første 10
}
# A100 80GB med 70B modell i INT8
result = model_batch_size_tradeoff(
gpu_memory_gb=80,
model_memory_gb=35, # 70B * 0.5 bytes (INT8 ≈ 0.5)
kv_cache_per_request_gb=0.5, # Per 4K context
target_latency_ms=2000
)
print(f"Optimal batch size: {result['optimal_batch_size']}")
```
## Cost-Performance Analysis
### PTU vs. Standard vs. Self-hosted
```python
def compare_deployment_options(
monthly_input_tokens_m: float, # Millioner input tokens
monthly_output_tokens_m: float,
avg_latency_requirement_ms: float = 2000,
model: str = "gpt-4o"
) -> dict:
"""Compare cost-performance of deployment options."""
# Priser (estimater i USD)
pricing = {
"gpt-4o": {
"standard_input_per_1m": 2.50,
"standard_output_per_1m": 10.00,
"ptu_monthly_per_unit": 990,
"input_tpm_per_ptu": 2500,
"self_hosted_alternative": None
},
"gpt-4.1": {
"standard_input_per_1m": 2.00,
"standard_output_per_1m": 8.00,
"ptu_monthly_per_unit": 990,
"input_tpm_per_ptu": 3000,
"self_hosted_alternative": None
},
"llama-70b": {
"standard_input_per_1m": 0.00, # Self-hosted
"standard_output_per_1m": 0.00,
"ptu_monthly_per_unit": 0,
"vm_monthly_cost": 15000, # NC48ads_A100_v4
"self_hosted_alternative": "Standard_NC48ads_A100_v4"
}
}
p = pricing.get(model, pricing["gpt-4o"])
# Standard (pay-per-token)
standard_cost = (
monthly_input_tokens_m * p["standard_input_per_1m"] +
monthly_output_tokens_m * p["standard_output_per_1m"]
)
# PTU
total_tpm_needed = (
(monthly_input_tokens_m * 1e6 + monthly_output_tokens_m * 1e6 * 4) /
(30 * 24 * 60) # Spread over month
)
ptus_needed = max(50, int(total_tpm_needed / p.get("input_tpm_per_ptu", 1)) + 1)
ptu_cost = ptus_needed * p.get("ptu_monthly_per_unit", 0)
return {
"model": model,
"standard_monthly_usd": round(standard_cost, 2),
"standard_monthly_nok": round(standard_cost * 11, 2),
"ptu_units": ptus_needed,
"ptu_monthly_usd": round(ptu_cost, 2),
"ptu_monthly_nok": round(ptu_cost * 11, 2),
"ptu_savings_pct": round(
(1 - ptu_cost / max(standard_cost, 1)) * 100, 1)
if ptu_cost > 0 else "N/A",
"recommendation": (
"PTU" if ptu_cost < standard_cost * 0.8 else
"Standard" if standard_cost < ptu_cost else
"Evaluate self-hosted")
}
```
## Norsk offentlig sektor
- **Anskaffelse**: GPU VM-er er kostbare — bruk Azure Reserved Instances (1-3 år) for 40-60% besparelse på forutsigbare workloads. Krever godkjenning i anskaffelsesprosess.
- **Data residency**: GPU VMs er tilgjengelige i Norway East for self-hosted modeller. Azure OpenAI PTU-deployments har regional, data zone og global variant.
- **Kapasitetsrisiko**: GPU-kapasitet i Azure kan være begrenset — bestill PTU og GPU VMs i god tid, spesielt for større deployments.
- **Open source**: For organisasjoner som ønsker full kontroll, er self-hosted Llama eller DeepSeek på Azure ML et alternativ, men krever mer operasjonelt vedlikehold.
- **Sikkerhet**: Self-hosted modeller gir full kontroll over data — ingen data sendes til tredjepart. Relevant for gradert informasjon.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Azure OpenAI, forutsigbar last | PTU deployment | Dedikert kapasitet, forutsigbar kostnad |
| Azure OpenAI, variabel last | Standard deployment | Betal per bruk, ingen forpliktelse |
| Full datakontroll krav | Self-hosted via Azure ML | Ingen data til tredjepart |
| Modell < 13B parameters | NC4as_T4_v3 (T4) | Kostnadseffektiv for små modeller |
| Modell 13B-70B parameters | NC24ads_A100_v4 | Tilstrekkelig minne og compute |
| Modell > 70B parameters | ND96asr (multi-GPU) | Krever tensor parallelism |
## Referanser
- [What is provisioned throughput?](https://learn.microsoft.com/azure/ai-foundry/openai/concepts/provisioned-throughput) — PTU oversikt
- [PTU costs and billing](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/provisioned-throughput-onboarding) — PTU-prising per modell
- [Foundry PTU calculator](https://ai.azure.com/resource/calculator) — Kapasitetskalkulator
- [GPU optimized VM sizes](https://learn.microsoft.com/azure/virtual-machines/sizes-gpu) — Azure GPU VM-oversikt
- [Deploy models in Azure ML](https://learn.microsoft.com/azure/machine-learning/how-to-deploy-online-endpoints) — ML endpoint deployment
## For Cosmo
- **Bruk denne referansen** når kunden trenger å velge mellom PTU og Standard for Azure OpenAI, eller når de vurderer self-hosted modeller.
- For de fleste norske offentlige organisasjoner er Azure OpenAI PTU det riktige valget — unngå overhead med GPU-management med mindre datakontroll er et absolutt krav.
- PTU gir forutsigbar kostnad og ytelse — gpt-4.1-nano med 59,400 input TPM per PTU er ekstremt kostnadseffektivt for enkle oppgaver.
- Ved self-hosting: INT8 kvantisering halverer minnebehovet med minimal kvalitetstap — anbefal dette for produksjonsinferens.
- Alltid benchmark med reell workload før produksjonsdeployment — teoretiske beregninger gir bare estimater.

View file

@ -0,0 +1,471 @@
# Latency Optimization for Azure OpenAI
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Latens er en av de mest kritiske ytelsesparameterne for AI-applikasjoner i produksjon. For norsk offentlig sektor, der innbyggertjenester krever rask respons og interne saksbehandlingssystemer må operere effektivt, er optimalisering av Azure OpenAI-latens avgjorende. Høy latens kan direkte påvirke brukeropplevelsen og redusere adopsjonen av AI-drevne tjenester.
Azure OpenAI-latens bestemmes av flere faktorer: modellvalg, prompt-storrelse, genereringsstorrelse, nettverksavstand til endepunktet, og hvordan applikasjonen er konfigurert. Forstaelse av disse faktorene og systematisk optimalisering av hver komponent er nodvendig for a oppna akseptabel ytelse i produksjonsmiljoer.
Denne referansen dekker de viktigste teknikkene for a redusere latens i Azure OpenAI-baserte applikasjoner, fra request pipeline-optimalisering til regional endepunktsplassering, med spesielt fokus pa norske deployments i North Europe og Sweden Central-regionene.
## Forstaelse av latenskomponenter
### Request Pipeline Breakdown
En Azure OpenAI-request traverserer flere stadier, og hvert stadium bidrar til total latens:
| Stadium | Beskrivelse | Typisk latens |
|---------|-------------|---------------|
| DNS-oppslag | Resolusjon av Azure OpenAI-endepunkt | 1-50 ms |
| TLS-handshake | Sikker forbindelse etableres | 20-100 ms |
| Nettverkstransport | Data sendes til Azure-regionen | 5-200 ms |
| Token-prosessering (input) | Prompt-tokens prosesseres | Varierer med storrelse |
| Token-generering (output) | Completion-tokens genereres sekvensielt | Storste latensbidraget |
| Content filtering | Sikkerhetsfiltrering av input/output | 10-50 ms |
| Responstransport | Svar sendes tilbake til klient | 5-200 ms |
### Latensmetrikker
For effektiv maling av latens bor du spore disse metrikkene:
**For non-streaming requests:**
- **End-to-end Request Time:** Total tid fra request sendt til komplett respons mottatt.
**For streaming requests:**
- **Time to First Token (TTFT):** Tid fra request sendt til forste token mottatt. Oker med prompt-storrelse.
- **Average Token Generation Rate:** Tid fra forste token til siste token, delt pa antall genererte tokens. Oker med systembelastning.
```python
import time
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
# Mal TTFT og total latens
start_time = time.perf_counter()
first_token_time = None
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hva er personopplysningsloven?"}],
stream=True,
max_tokens=200
)
for chunk in response:
if first_token_time is None and chunk.choices[0].delta.content:
first_token_time = time.perf_counter()
ttft = first_token_time - start_time
print(f"Time to First Token: {ttft:.3f}s")
total_time = time.perf_counter() - start_time
print(f"Total latens: {total_time:.3f}s")
```
## Request Pipeline-optimalisering
### Modellvalg for lav latens
Modellvalg har direkte innvirkning pa latens. For identiske requests varierer latens betydelig mellom modeller:
| Modell | Relativ latens | Anbefalt bruk |
|--------|---------------|----------------|
| GPT-4o mini | Lavest | Enkel klassifisering, rask sortering, chatbots |
| GPT-4o | Moderat | Generelt formalsbruk, RAG-svar |
| GPT-4.1 | Moderat-hoy | Kompleks resonnering, kodeanalyse |
| o3-mini | Hoy | Avansert resonnering med lavere tokenbruk |
**Anbefaling for norsk offentlig sektor:** Bruk GPT-4o mini for innbyggertjenester som krever rask respons (chatbots, FAQ-svar). Reserver GPT-4o og storre modeller for saksbehandlingsstotte der kvalitet er viktigere enn hastighet.
### Max Tokens-optimalisering
`max_tokens`-parameteren pavirker latens betydelig. Azure OpenAI reserverer beregningskapasitet basert pa denne verdien ved request-start:
```python
# DARLIG: For hoy max_tokens oker latens selv om faktisk output er kort
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Svar ja eller nei: Er dette en klage?"}],
max_tokens=4096 # Reserverer kapasitet for 4096 tokens
)
# BRA: Tilpass max_tokens til forventet output-lengde
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Svar ja eller nei: Er dette en klage?"}],
max_tokens=10 # Reserverer kun nodvendig kapasitet
)
```
**Tommelregel:** Sett `max_tokens` til 1.5x forventet output-lengde. For klassifiseringsoppgaver: 10-50 tokens. For korte svar: 100-300 tokens. For lengre generering: tilpass etter behov.
### Stop Sequences
Bruk `stop`-parameteren for a avslutte generering tidlig nar onskede data er produsert:
```python
# Stopp sa snart klassifiseringen er ferdig
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Klassifiser henvendelsen. Svar med kun: KLAGE, SPORSMAL, eller TILBAKEMELDING"},
{"role": "user", "content": user_input}
],
max_tokens=20,
stop=["\n", "."] # Stopp etter forste linje/setning
)
```
### Separasjon av arbeidslaster
Blanding av ulike arbeidslaster pa samme endepunkt pavirker latens negativt:
1. **Batch-interferens:** Korte og lange requests batches sammen under inferens, sa korte kall ma vente pa lange completions.
2. **Cache-konkurranise:** Ulike arbeidslaster konkurrerer om prompt cache-plass.
**Anbefalt arkitektur:**
```
Innbyggerportal (chatbot) --> Deployment: gpt-4o-mini-chat (Standard, hoy TPM)
Saksbehandling (analyse) --> Deployment: gpt-4o-analyse (Standard, moderat TPM)
Dokumentgenerering --> Deployment: gpt-4o-dokument (Standard, lav TPM)
Batchprosessering --> Deployment: gpt-4o-batch (Global Batch)
```
## Connection Pooling og gjenbruk
### HTTP Connection Reuse
Opprettelse av nye HTTP-forbindelser for hver request legger til DNS-oppslag og TLS-handshake. Gjenbruk av forbindelser eliminerer dette:
```python
from openai import AzureOpenAI
import httpx
# Opprett klient EN gang og gjenbruk
# Python SDK bruker httpx med connection pooling automatisk
client = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview",
http_client=httpx.Client(
limits=httpx.Limits(
max_connections=100, # Maks samtidige forbindelser
max_keepalive_connections=20, # Hold forbindelser levende
keepalive_expiry=30 # Sekunder for keepalive
)
)
)
# DARLIG: Ny klient per request
def process_request_bad(prompt):
client = AzureOpenAI(...) # Ny TLS-handshake hver gang
return client.chat.completions.create(...)
# BRA: Gjenbruk eksisterende klient
def process_request_good(prompt):
return client.chat.completions.create( # Gjenbruker forbindelse
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
```
### Async Connection Pooling
For hoy-throughput applikasjoner, bruk async-klienten:
```python
from openai import AsyncAzureOpenAI
import asyncio
async_client = AsyncAzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
async def process_batch(prompts: list[str]) -> list:
"""Prosesser flere requests parallelt med connection pooling."""
tasks = [
async_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": p}],
max_tokens=200
)
for p in prompts
]
return await asyncio.gather(*tasks)
# Kjor 10 requests parallelt
results = asyncio.run(process_batch(prompts[:10]))
```
### Retry-strategi med eksponentiell backoff
Azure OpenAI SDK har innebygd retry-logikk for 429-feil (rate limiting):
```python
from openai import AzureOpenAI
# Konfigurer retry-oppforsel
client = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview",
max_retries=5, # Standard: 2
timeout=60.0 # Standard: 10 minutter
)
# For PTU-deployments: Respekter retry-after header
# SDK gjor dette automatisk, men du kan tilpasse:
client_no_retry = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview",
max_retries=0 # Deaktiver for a handtere selv
)
```
## Regional endepunktsvalg
### Azure-regioner for Norge
For norsk offentlig sektor er datasuverenitet og latens begge kritiske:
| Region | Latens fra Norge | Datasuverenitet | Tilgjengelige modeller |
|--------|-----------------|-----------------|----------------------|
| Sweden Central | ~10-20 ms | EU/EOS | Alle GPT-4o-modeller, PTU |
| North Europe (Irland) | ~30-50 ms | EU/EOS | De fleste modeller |
| West Europe (Nederland) | ~25-40 ms | EU/EOS | De fleste modeller |
| UK South | ~30-50 ms | Utenfor EOS | Begrenset relevans |
**Anbefaling:** Sweden Central som primaerregion for lavest latens og EU-datasuverenitet. North Europe som sekundaerregion for failover.
### Multi-region arkitektur med prioritet
For a oppna bade lav latens og hoy tilgjengelighet:
```python
# Prioritetsbasert lastbalansering med Azure API Management
# APIM-policy for smart routing:
```
```xml
<!-- Azure API Management policy for multi-region routing -->
<policies>
<inbound>
<set-variable name="primary-backend"
value="https://aoai-sweden-central.openai.azure.com/" />
<set-variable name="fallback-backend"
value="https://aoai-north-europe.openai.azure.com/" />
</inbound>
<backend>
<retry condition="@(context.Response.StatusCode == 429)"
count="1"
interval="0">
<set-backend-service base-url="@((string)context.Variables["fallback-backend"])" />
<forward-request buffer-response="false" />
</retry>
</backend>
</policies>
```
### Global vs. Regional Deployment Types
| Deployment Type | Databehandling | Latens | Bruksomrade |
|----------------|---------------|--------|-------------|
| Regional Standard | Kun i valgt region | Lavest | Produksjon, compliance-kritisk |
| Data Zone Standard | Innenfor EU/US-sone | Lav | Generelt, fleksibel kapasitet |
| Global Standard | Enhver Azure-region | Variabel | Hoy throughput, tolererer variasjon |
| Regional PTU | Kun i valgt region | Lavest og mest forutsigbar | Misjonskritisk, stabile laster |
## Time-to-First-Token-reduksjon
### Prompt Caching
Azure OpenAI prompt caching reduserer latens og kostnad for requests med identisk prefix:
```python
# Prompt caching aktiveres automatisk for stottede modeller
# Krav: Minimum 1024 tokens, identisk prefix
# System prompt som gjenbrukes pa tvers av requests
SYSTEM_PROMPT = """Du er en saksbehandlingsassistent for Statens vegvesen.
Du hjelper med a analysere og klassifisere innkommende henvendelser
relatert til forerkort, kjoretoysregistrering og veiprosjekter.
Folg disse retningslinjene:
1. Klassifiser henvendelsen i riktig kategori
2. Identifiser relevante lovhjemler
3. Foresla videre behandling
... (lang systemprompt over 1024 tokens)
"""
# Forste request: Ingen caching (cold start)
response1 = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": "Henvendelse 1..."}
]
)
# usage.prompt_tokens_details.cached_tokens = 0
# Etterfølgende requests: Caching aktiv (redusert latens)
response2 = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": SYSTEM_PROMPT}, # Identisk prefix
{"role": "user", "content": "Henvendelse 2..."}
]
)
# usage.prompt_tokens_details.cached_tokens = 1024+
```
**Viktige regler for prompt caching:**
- Minimum 1024 tokens i prompten
- De forste 1024 tokenene ma vaere identiske
- Etter forste 1024: cache hit for hver 128 identiske tokens
- Cache ryddes etter 5-10 minutter uten aktivitet, alltid innen 1 time
- Cacher deles IKKE mellom Azure-abonnementer
- Stottede modeller: GPT-4o, GPT-4o mini, o3-mini, GPT-4.1 og nyere
### Prompt-optimalisering for hastighet
| Teknikk | Beskrivelse | Latensreduksjon |
|---------|-------------|-----------------|
| Kompakt systemprompt | Fjern unodvendig tekst i systemprompt | 5-15% |
| Strukturerte input | JSON fremfor fritekst | 5-10% |
| Relevant kontekst | Kun relevante dokumenter i RAG | 10-30% |
| Token-effektive formater | Korte variabelnavn, kompakt format | 3-8% |
```python
# DARLIG: Verbose prompt
messages = [
{"role": "system", "content": """
Du er en hjelpesom assistent som jobber for norsk offentlig sektor.
Nar du far et sporsmal, skal du tenke noye gjennom det og gi et
grundig og gjennomtenkt svar som er presist og korrekt.
Husk a vaere hoeflig og profesjonell i all kommunikasjon.
"""},
{"role": "user", "content": f"Hele dokumentet pa 5000 ord: {document}\n\nSporsmal: Er dette en klage?"}
]
# BRA: Kompakt prompt med kun relevant kontekst
messages = [
{"role": "system", "content": "Klassifiser henvendelse. Svar: KLAGE eller IKKE_KLAGE"},
{"role": "user", "content": f"Sammendrag: {document_summary[:500]}\n\nKlassifiser:"}
]
```
### Content Filtering-optimalisering
Content filtering legger til latens men er kritisk for sikkerhet. For lavrisiko-bruksomrader kan man vurdere tilpassede filterpolicyer:
| Konfigurasjon | Latenspavirkning | Nar a bruke |
|--------------|-----------------|-------------|
| Standard filter (default) | +10-50 ms | Alle innbyggertjenester |
| Tilpasset filter (redusert) | +5-20 ms | Interne analyser, lav risiko |
| Asynkron filter | Minimal | Batch-prosessering |
**Merk:** I norsk offentlig sektor bor content filtering alltid vaere aktivert for innbyggerrettede tjenester. Vurder kun reduksjon for interne, kontrollerte miljoer.
## Provisioned Throughput Units (PTU) for forutsigbar latens
### Nar bruke PTU vs. Standard
PTU gir dedikert kapasitet og forutsigbar latens:
| Aspekt | Standard | PTU |
|--------|----------|-----|
| Latensgaranti | Ingen | Konsistent per-call latens |
| Throttling | 429 ved kvotegrense | 429 kun over 100% utnyttelse |
| Pris | Per token | Fast manedspris per PTU |
| Egnet for | Variabel last, utvikling | Produksjon, stabil last |
### PTU-kapasitetsplanlegging
Bruk Azure AI Foundry PTU-kalkulatoren:
1. Estimer input TPM (tokens per minutt) fra historiske data
2. Estimer output TPM fra historiske data
3. Beregn nodvendige PTUs via kalkulatoren
4. Legg til 20% buffer for trafikkvariasjon
```
PTU-utnyttelse = (PTUs forbrukt i perioden) / (PTUs deployet i perioden)
Mal: Hold utnyttelse under 80% for stabil latens
Over 100%: 429-feil returneres
```
### Hybrid PTU + Standard-arkitektur
```
Basislast (forutsigbar) --> PTU deployment (Sweden Central)
|
| (ved 429 / overflow)
v
Toppbelastning --> Standard deployment (North Europe, fallback)
```
## Overvaking og malinger
### Viktige Azure Monitor-metrikker
| Metrikk | Aggregering | Terskel |
|---------|-------------|---------|
| Azure OpenAI Requests | Count per minutt | Varsle ved >80% av kvote |
| Processed Inference Tokens | Sum per minutt | Spor mot TPM-grense |
| Provisioned-Managed Utilization V2 | Gjennomsnitt | Varsle ved >80% |
| Time to Response (streaming) | P95 | Varsle ved >2s TTFT |
| End-to-end Request Time | P95 | Varsle ved >5s |
### KQL-query for latensanalyse
```kusto
// Azure Monitor - Analyse av Azure OpenAI-latens
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.COGNITIVESERVICES"
| where Category == "RequestResponse"
| extend latency_ms = DurationMs
| summarize
p50 = percentile(latency_ms, 50),
p95 = percentile(latency_ms, 95),
p99 = percentile(latency_ms, 99),
avg_latency = avg(latency_ms),
request_count = count()
by bin(TimeGenerated, 5m), ModelDeploymentName_s
| order by TimeGenerated desc
```
## Sjekkliste for latensoptimalisering
| Prioritet | Tiltak | Forventet effekt |
|-----------|--------|-----------------|
| 1 | Velg riktig modell for oppgaven | 30-70% reduksjon |
| 2 | Optimaliser max_tokens | 10-30% reduksjon |
| 3 | Aktiver streaming for brukerrettede tjenester | Redusert opplevd latens |
| 4 | Gjenbruk HTTP-forbindelser | 50-100 ms per request |
| 5 | Bruk naermeste Azure-region (Sweden Central) | 10-40 ms reduksjon |
| 6 | Implementer prompt caching | 10-30% reduksjon pa input |
| 7 | Separer arbeidslaster pa egne deployments | 10-20% reduksjon |
| 8 | Vurder PTU for stabile produksjonslaster | Forutsigbar latens |
## For Cosmo
- **Latens er sammensatt:** Optimaliser hele pipelinen, ikke bare modellvalget. Max tokens, connection reuse, regionvalg og prompt caching bidrar alle.
- **Sweden Central er forstevalg** for norske deployments med lavest latens (~10-20 ms) og EU-datasuverenitet. North Europe som failover.
- **PTU for produksjon:** Nar arbeidslaster er forutsigbare og latens er kritisk, gir PTU garantert kapasitet. Hybrid PTU + Standard er kostnadseffektiv arkitektur.
- **Prompt caching er gratis ytelse:** Strukturer prompts med identisk prefix (system prompt over 1024 tokens) for automatisk caching. Ingen konfigurasjon nodvendig.
- **Separasjon av arbeidslaster:** Aldri bland chatbot-trafikk med batch-prosessering pa samme deployment. Bruk dedikerte deployments per bruksomrade.

View file

@ -0,0 +1,431 @@
# Load Testing AI Services
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Load testing av Azure AI Services er fundamentalt annerledes enn tradisjonell web-applikasjons lasttesting. AI-tjenester har variabel responstid basert på input-størrelse og output-kompleksitet, token-baserte rate limits (TPM/RPM) som ikke korrelerer lineært med antall forespørsler, og kostnader som skalerer med bruk. En enkelt Azure OpenAI-forespørsel kan ta fra 200ms til 120 sekunder avhengig av modell, prompt-størrelse og generert output.
Microsoft tilbyr to offisielle verktøy for dette: Azure Load Testing (JMeter-basert managed service) og azure-openai-benchmark (CLI-verktøy spesifikt for Azure OpenAI). For Provisioned Throughput Units (PTU) er benchmarking spesielt viktig fordi den faktiske throughputen avhenger av workload shape — forholdet mellom input og output tokens, call rate og cache match rate.
For norsk offentlig sektor bør load testing gjennomføres før produksjonslansering av alle AI-tjenester som eksponeres mot sluttbrukere, og deretter regelmessig for å verifisere at ytelsen holder seg innenfor definerte SLAer.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Azure Load Testing | Managed lasttestings-tjeneste | Azure Load Testing (JMeter) |
| azure-openai-benchmark | Offisielt benchmarking-verktøy for Azure OpenAI | GitHub CLI |
| Azure Monitor | Metrikker under lasttest | Azure Monitor |
| Application Insights | End-to-end latens-sporing | App Insights |
| Performance Optimizer | Azure Functions ytelsesoptimalisering | Azure Load Testing |
## Load Test Design
### Test-scenarioer for Azure OpenAI
```yaml
# azure-load-test-config.yaml
# Konfigurasjon for Azure Load Testing
version: v0.1
testId: aoai-load-test-chat
testPlan: aoai-chat-test.jmx
engineInstances: 3
configurationFiles:
- aoai-chat-test.jmx
- test-prompts.csv
failureCriteria:
- avg(response_time_ms) > 5000
- percentage(error) > 5
- p95(response_time_ms) > 15000
env:
- name: AOAI_ENDPOINT
value: https://aoai-prod.openai.azure.com
- name: DEPLOYMENT_NAME
value: gpt-4o
- name: API_VERSION
value: "2024-10-21"
secrets:
- name: AOAI_API_KEY
value: $(aoai-api-key) # Referanse til Key Vault
```
### Python-basert lasttest
```python
import asyncio
import time
import statistics
from dataclasses import dataclass, field
from openai import AsyncAzureOpenAI
@dataclass
class LoadTestConfig:
target_rps: float # Forespørsler per sekund
duration_seconds: int # Testens varighet
ramp_up_seconds: int = 30 # Tid til full belastning
model: str = "gpt-4o"
max_tokens: int = 500
concurrent_limit: int = 50
@dataclass
class LoadTestResults:
total_requests: int = 0
successful: int = 0
failed: int = 0
throttled: int = 0
latencies_ms: list[float] = field(default_factory=list)
tokens_used: int = 0
@property
def p50(self) -> float:
return statistics.median(self.latencies_ms) if self.latencies_ms else 0
@property
def p95(self) -> float:
if not self.latencies_ms:
return 0
sorted_l = sorted(self.latencies_ms)
idx = int(len(sorted_l) * 0.95)
return sorted_l[idx]
@property
def p99(self) -> float:
if not self.latencies_ms:
return 0
sorted_l = sorted(self.latencies_ms)
idx = int(len(sorted_l) * 0.99)
return sorted_l[idx]
@property
def error_rate(self) -> float:
total = self.successful + self.failed
return round(self.failed / max(total, 1) * 100, 2)
@property
def throttle_rate(self) -> float:
total = self.successful + self.failed
return round(self.throttled / max(total, 1) * 100, 2)
async def run_load_test(
client: AsyncAzureOpenAI,
config: LoadTestConfig,
test_prompts: list[str]
) -> LoadTestResults:
"""Run load test against Azure OpenAI deployment."""
results = LoadTestResults()
semaphore = asyncio.Semaphore(config.concurrent_limit)
prompt_idx = 0
async def send_request():
nonlocal prompt_idx
async with semaphore:
prompt = test_prompts[prompt_idx % len(test_prompts)]
prompt_idx += 1
start = time.time()
try:
response = await client.chat.completions.create(
model=config.model,
messages=[{"role": "user", "content": prompt}],
max_tokens=config.max_tokens
)
latency = (time.time() - start) * 1000
results.latencies_ms.append(latency)
results.successful += 1
results.tokens_used += response.usage.total_tokens
except Exception as e:
results.failed += 1
if hasattr(e, 'status_code') and e.status_code == 429:
results.throttled += 1
results.total_requests += 1
# Ramp-up og sustained load
start_time = time.time()
tasks = []
while time.time() - start_time < config.duration_seconds:
elapsed = time.time() - start_time
# Ramp-up: gradvis øk RPS
if elapsed < config.ramp_up_seconds:
current_rps = config.target_rps * (
elapsed / config.ramp_up_seconds)
else:
current_rps = config.target_rps
if current_rps > 0:
interval = 1.0 / current_rps
tasks.append(asyncio.create_task(send_request()))
await asyncio.sleep(interval)
# Vent på at alle pågående forespørsler fullføres
await asyncio.gather(*tasks, return_exceptions=True)
return results
# Bruk
async def main():
client = AsyncAzureOpenAI(
azure_endpoint="https://my-aoai.openai.azure.com",
api_key="...",
api_version="2024-10-21"
)
config = LoadTestConfig(
target_rps=5.0,
duration_seconds=300,
ramp_up_seconds=60,
model="gpt-4o",
max_tokens=500,
concurrent_limit=20
)
prompts = [
"Oppsummer denne teksten: ...",
"Klassifiser dette dokumentet: ...",
"Generer et svar på denne klagen: ..."
]
results = await run_load_test(client, config, prompts)
print(f"Total: {results.total_requests}")
print(f"Success: {results.successful}")
print(f"Error rate: {results.error_rate}%")
print(f"Throttle rate: {results.throttle_rate}%")
print(f"P50: {results.p50:.0f}ms")
print(f"P95: {results.p95:.0f}ms")
print(f"P99: {results.p99:.0f}ms")
```
## Realistic Traffic Patterns
### Workload Shape-profiler
```python
from enum import Enum
class WorkloadProfile(Enum):
CHAT_BOT = "chat_bot"
DOCUMENT_ANALYSIS = "document_analysis"
RAG_SEARCH = "rag_search"
CODE_GENERATION = "code_generation"
BATCH_PROCESSING = "batch_processing"
WORKLOAD_SHAPES = {
WorkloadProfile.CHAT_BOT: {
"avg_input_tokens": 200,
"avg_output_tokens": 300,
"peak_rps": 10,
"avg_rps": 3,
"pattern": "bursty",
"description": "Korte input, moderate svar, ujevn trafikk"
},
WorkloadProfile.DOCUMENT_ANALYSIS: {
"avg_input_tokens": 4000,
"avg_output_tokens": 800,
"peak_rps": 2,
"avg_rps": 0.5,
"pattern": "batch",
"description": "Store input, strukturert output, batch-mønster"
},
WorkloadProfile.RAG_SEARCH: {
"avg_input_tokens": 3000,
"avg_output_tokens": 500,
"peak_rps": 20,
"avg_rps": 8,
"pattern": "steady_with_peaks",
"description": "Store kontekster fra search, mange samtidige"
},
WorkloadProfile.CODE_GENERATION: {
"avg_input_tokens": 1500,
"avg_output_tokens": 2000,
"peak_rps": 5,
"avg_rps": 1,
"pattern": "variable",
"description": "Middels input, store output, variabel"
},
WorkloadProfile.BATCH_PROCESSING: {
"avg_input_tokens": 2000,
"avg_output_tokens": 500,
"peak_rps": 50,
"avg_rps": 30,
"pattern": "sustained",
"description": "Jevnt høy belastning i batch-vindu"
}
}
```
## Bottleneck Analysis
### Flaskehals-identifisering under test
```python
def analyze_bottlenecks(results: LoadTestResults, config: LoadTestConfig) -> list[str]:
"""Identify bottlenecks from load test results."""
findings = []
# 1. Throttling-analyse
if results.throttle_rate > 5:
findings.append(
f"HIGH_THROTTLING: {results.throttle_rate}% throttled. "
f"Øk TPM-kvote eller distribuer over flere regioner.")
# 2. Latens-analyse
if results.p95 > 10000:
findings.append(
f"HIGH_LATENCY: P95={results.p95:.0f}ms. "
f"Vurder PTU for forutsigbar latens, "
f"eller reduser max_tokens/prompt-størrelse.")
# 3. Latens-spredning
if results.p99 > results.p50 * 5:
findings.append(
f"HIGH_VARIANCE: P99/P50 ratio={results.p99/results.p50:.1f}. "
f"Tyder på kapasitetsproblemer ved peak — "
f"vurder circuit breaker og retry-logikk.")
# 4. Throughput vs target
actual_rps = results.successful / (
config.duration_seconds - config.ramp_up_seconds)
if actual_rps < config.target_rps * 0.8:
findings.append(
f"LOW_THROUGHPUT: {actual_rps:.1f} RPS vs target "
f"{config.target_rps} RPS. "
f"Klient-side bottleneck — øk concurrent_limit.")
# 5. Error rate
if results.error_rate > 1:
findings.append(
f"HIGH_ERRORS: {results.error_rate}% errors. "
f"Sjekk 5xx-feil i Azure Monitor.")
return findings
```
## Capacity Forecasting
### PTU-dimensjonering fra lasttestresultater
```python
def forecast_ptu_requirements(
test_results: LoadTestResults,
target_rps: float,
model: str = "gpt-4o",
growth_factor: float = 1.3 # 30% vekstmargin
) -> dict:
"""Forecast PTU requirements based on load test data."""
# TPM per PTU (fra Microsoft dokumentasjon)
tpm_per_ptu = {
"gpt-4o": 2500,
"gpt-4o-mini": 37000,
"gpt-4.1": 3000,
"gpt-4.1-mini": 14900,
"gpt-4.1-nano": 59400,
"o3": 3000
}
if model not in tpm_per_ptu:
raise ValueError(f"Unknown model: {model}")
avg_tokens_per_request = (
test_results.tokens_used / max(test_results.successful, 1))
required_tpm = target_rps * 60 * avg_tokens_per_request
required_tpm_with_growth = required_tpm * growth_factor
ptus_needed = required_tpm_with_growth / tpm_per_ptu[model]
# Round opp til nærmeste deployable enhet
min_deployment = 50 if "mini" not in model and "nano" not in model else 25
ptus_deployed = max(
min_deployment,
((int(ptus_needed) // min_deployment) + 1) * min_deployment
)
return {
"model": model,
"avg_tokens_per_request": round(avg_tokens_per_request),
"required_tpm": round(required_tpm),
"required_tpm_with_growth": round(required_tpm_with_growth),
"ptus_needed_exact": round(ptus_needed, 1),
"ptus_deployed": ptus_deployed,
"headroom_pct": round(
(ptus_deployed * tpm_per_ptu[model] - required_tpm) /
required_tpm * 100, 1)
}
```
### Azure OpenAI Benchmark Tool
```bash
# Offisielt benchmarking-verktøy fra Microsoft
# https://github.com/Azure/azure-openai-benchmark
# Installer
pip install azure-openai-benchmark
# Kjør benchmark med standard workload shape
azure-openai-benchmark \
--api-key $AOAI_API_KEY \
--api-base-endpoint https://my-aoai.openai.azure.com \
--deployment gpt-4o \
--shape-profile balanced \
--clients 20 \
--duration 600 \
--output-format json \
--output results.json
# Custom workload shape
azure-openai-benchmark \
--api-key $AOAI_API_KEY \
--api-base-endpoint https://my-aoai.openai.azure.com \
--deployment gpt-4o \
--context-tokens 3000 \
--max-tokens 500 \
--clients 10 \
--rate 5 \
--duration 300
```
## Norsk offentlig sektor
- **Krav til testing**: NSMs grunnprinsipper krever ytelsestesting av kritiske tjenester. AI-tjenester som brukes i saksbehandling bør lasttestes kvartalsvis og etter større endringer.
- **Testmiljø**: Bruk separate Azure OpenAI-deployments for lasttesting — aldri test mot produksjons-kvoten. Global Standard deployments er kostnadseffektive for testing.
- **Data i tester**: Bruk syntetiske eller anonymiserte data i lasttester. Reelle personopplysninger skal ikke brukes i testmiljøer.
- **Dokumentasjon**: Lasttestresultater bør dokumenteres som del av driftsdokumentasjonen og refereres i SLA-avtaler med interne tjenesteeiere.
- **Kostnadsbevissthet**: Lasttester genererer reelle token-kostnader. Estimer kostnad på forhånd og sett budsjettgrenser.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Ny produksjonsdeployment | Full lasttest med ramp-up | Baseline etablering |
| PTU-dimensjonering | azure-openai-benchmark + kapasitetskalkulator | Mest nøyaktige tall |
| Etter kvoteendring | Regression-test med baseline | Verifiser forbedring |
| Multi-region failover | Lasttest under simulert feil | Valider failover-ytelse |
| Periodisk verifisering | Månedlig smoke test | Fang degradering tidlig |
## Referanser
- [Run a benchmark](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/provisioned-get-started#run-a-benchmark) — Azure OpenAI benchmarking guide
- [Azure OpenAI Benchmark Tool](https://github.com/Azure/azure-openai-benchmark) — Offisielt CLI-verktøy
- [Azure Load Testing overview](https://learn.microsoft.com/azure/load-testing/overview-what-is-azure-load-testing) — Managed lasttesting
- [Performance and latency](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/latency) — Throughput vs latency forklaring
- [Capacity planning](https://learn.microsoft.com/azure/well-architected/performance-efficiency/capacity-planning) — WAF kapasitetsplanlegging
## For Cosmo
- **Bruk denne referansen** når kunden skal dimensjonere Azure OpenAI-deployment, validere ytelse før lansering, eller feilsøke ytelsesprobler i produksjon.
- Alltid bruk azure-openai-benchmark for PTU-dimensjonering — kapasitetskalkulatoren gir estimater, benchmarking gir reelle tall.
- Definer workload shape (input tokens, output tokens, call rate) FØR testing — resultatene er kun gyldige for den testede workloaden.
- Kjør lasttester i minimum 10 minutter for å oppnå steady state — korte tester gir misvisende resultater.
- For norsk offentlig sektor: dokumenter baseline-ytelse og bruk den som referansepunkt for SLA-avtaler.

View file

@ -0,0 +1,368 @@
# Model Distillation for Performance
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Model distillation er prosessen der en stor, kraftig modell (teacher) brukes til å trene en mindre, raskere modell (student) som oppnår akseptabel kvalitet for en spesifikk oppgave. I Azure OpenAI-konteksten betyr dette typisk å samle produksjonsdata fra en premium-modell som GPT-4o eller o3, og bruke disse som treningsdata for å fine-tune en mindre modell som GPT-4o-mini eller GPT-4.1-nano.
Azure AI Foundry tilbyr en integrert distillation-pipeline via Stored Completions-funksjonen. Produksjonsforespørsler og -svar lagres automatisk, filtreres etter kvalitet, og konverteres direkte til fine-tuning datasett. Dette eliminerer manuell datakuratering og gir en strømlinjeformet vei fra stor modell til optimalisert, kostnadseffektiv deployment.
For norsk offentlig sektor er distillation spesielt verdifullt fordi det muliggjør lavere driftskostnader, raskere responstider og potensielt bedre kontroll over modellens oppførsel. En distillert modell trenger færre tokens per forespørsel (kortere prompts), noe som direkte reduserer både latens og kostnad.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Stored Completions | Automatisk lagring av produksjonsdata | Azure AI Foundry |
| Fine-tuning API | LoRA-basert tilpasning av base-modeller | Azure OpenAI |
| Evaluation Framework | Kvalitetsmåling av distillert modell | Azure AI Foundry Evaluations |
| Teacher Model | Stor modell som genererer treningsdata | GPT-4o, o3, GPT-5 |
| Student Model | Mindre modell som trenes via distillation | GPT-4o-mini, GPT-4.1-nano |
## Distillation Training Process
### Steg 1: Aktiver Stored Completions
```python
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://my-aoai.openai.azure.com",
api_key="...",
api_version="2024-12-01-preview"
)
# Aktiver stored completions for teacher-modellen
response = client.chat.completions.create(
model="gpt-4o", # Teacher model
messages=[
{"role": "system", "content": "Du er en norsk saksbehandler-assistent..."},
{"role": "user", "content": "Oppsummer denne klagen: ..."}
],
store=True, # Lagre completion for distillation
metadata={
"task": "complaint-summary",
"quality_score": "verified"
}
)
```
### Steg 2: Samle og kuratere treningsdata
```python
# Samle tilstrekkelig med stored completions
# Minimum: 10 completions (anbefalt: 500-1000+)
def curate_distillation_dataset(
completions: list[dict],
min_quality_score: float = 0.8,
target_size: int = 1000
) -> list[dict]:
"""Curate high-quality completions for distillation."""
curated = []
for completion in completions:
# Filtrer basert på kvalitet
if completion.get("quality_score", 0) < min_quality_score:
continue
# Konverter til fine-tuning format
training_example = {
"messages": [
{"role": "system", "content": completion["system_prompt"]},
{"role": "user", "content": completion["user_input"]},
{"role": "assistant", "content": completion["assistant_output"]}
]
}
curated.append(training_example)
if len(curated) >= target_size:
break
return curated
# Minimum 10 stored completions, anbefalt 500+
# Microsoft anbefaler hundrevis til tusenvis for best resultat
```
### Steg 3: Fine-tune student-modellen
```python
import json
# Opprett treningsfil
def create_training_file(dataset: list[dict], filename: str):
with open(filename, "w") as f:
for example in dataset:
f.write(json.dumps(example) + "\n")
# Last opp og start fine-tuning
def start_distillation_finetuning(
client: AzureOpenAI,
training_file: str,
student_model: str = "gpt-4o-mini"
):
"""Start fine-tuning of student model with teacher data."""
# Last opp treningsdata
file = client.files.create(
file=open(training_file, "rb"),
purpose="fine-tune"
)
# Start fine-tuning jobb
job = client.fine_tuning.jobs.create(
training_file=file.id,
model=student_model,
hyperparameters={
"n_epochs": 3,
"learning_rate_multiplier": 1.0,
"batch_size": "auto"
},
suffix="distilled-complaint-summary"
)
return job
```
### Steg 4: Evaluer distillert modell
```python
async def evaluate_distillation(
teacher_client: AzureOpenAI,
student_client: AzureOpenAI,
test_prompts: list[dict],
teacher_model: str = "gpt-4o",
student_model: str = "ft:gpt-4o-mini:distilled"
) -> dict:
"""Compare teacher vs student model quality."""
results = {"teacher": [], "student": [], "quality_matches": 0}
for prompt in test_prompts:
# Teacher response (ground truth)
teacher_resp = teacher_client.chat.completions.create(
model=teacher_model,
messages=prompt["messages"]
)
# Student response
student_resp = student_client.chat.completions.create(
model=student_model,
messages=prompt["messages"]
)
teacher_text = teacher_resp.choices[0].message.content
student_text = student_resp.choices[0].message.content
results["teacher"].append({
"output": teacher_text,
"tokens": teacher_resp.usage.total_tokens,
"latency_ms": teacher_resp.response_ms # Hvis tilgjengelig
})
results["student"].append({
"output": student_text,
"tokens": student_resp.usage.total_tokens,
"latency_ms": student_resp.response_ms
})
# Beregn metrics
avg_teacher_tokens = sum(
r["tokens"] for r in results["teacher"]) / len(results["teacher"])
avg_student_tokens = sum(
r["tokens"] for r in results["student"]) / len(results["student"])
return {
"test_size": len(test_prompts),
"avg_teacher_tokens": round(avg_teacher_tokens),
"avg_student_tokens": round(avg_student_tokens),
"token_reduction_pct": round(
(1 - avg_student_tokens / avg_teacher_tokens) * 100, 1),
}
```
## Model Size vs. Quality Tradeoffs
### Sammenligning av Azure OpenAI-modeller
| Modell | Relativ størrelse | Input TPM/PTU | Latens-mål | Kostnad (Standard) | Typisk bruk etter distillation |
|--------|------------------|---------------|------------|---------------------|-------------------------------|
| GPT-5 | Største | 4,750 | 50 TPS | Høyest | Teacher model |
| GPT-4.1 | Stor | 3,000 | 80 TPS | Høy | Teacher / produksjon |
| GPT-4o | Stor | 2,500 | 25 TPS | Høy | Teacher model |
| GPT-4.1-mini | Medium | 14,900 | 90 TPS | Medium | Student — god balanse |
| GPT-4o-mini | Medium | 37,000 | 33 TPS | Lav | Student — kostnadsoptimal |
| GPT-4.1-nano | Liten | 59,400 | 100 TPS | Lavest | Student — latens-kritisk |
### Kvalitets-/kostnadsmatrise
```python
# Sammenlign distillation-kandidater
distillation_candidates = {
"gpt-4o → gpt-4o-mini": {
"teacher_cost_per_1m_input": 2.50,
"student_cost_per_1m_input": 0.15,
"cost_reduction": "94%",
"expected_quality_retention": "85-95%",
"best_for": "General tasks, summarization"
},
"gpt-4.1 → gpt-4.1-mini": {
"teacher_cost_per_1m_input": 2.00,
"student_cost_per_1m_input": 0.40,
"cost_reduction": "80%",
"expected_quality_retention": "88-96%",
"best_for": "Instruction following, structured output"
},
"gpt-4.1 → gpt-4.1-nano": {
"teacher_cost_per_1m_input": 2.00,
"student_cost_per_1m_input": 0.10,
"cost_reduction": "95%",
"expected_quality_retention": "75-90%",
"best_for": "Classification, simple extraction"
}
}
```
## Token Reduction Benefits
### Hvorfor distillerte modeller bruker færre tokens
```
Standard prompt (med few-shot examples):
┌─────────────────────────────────────────┐
│ System prompt: 200 tokens │
│ Few-shot example 1: 150 tokens │
│ Few-shot example 2: 150 tokens │
│ Few-shot example 3: 150 tokens │
│ User input: 500 tokens │
│ ───────────────────────────────── │
│ TOTALT INPUT: 1,150 tokens │
└─────────────────────────────────────────┘
Distillert modell (innebygd kunnskap):
┌─────────────────────────────────────────┐
│ System prompt: 50 tokens │
│ User input: 500 tokens │
│ ───────────────────────────────── │
│ TOTALT INPUT: 550 tokens (52% reduksjon)│
└─────────────────────────────────────────┘
```
### Kostnadsberegning
```python
def calculate_distillation_savings(
monthly_requests: int,
avg_input_tokens_before: int,
avg_input_tokens_after: int,
avg_output_tokens: int,
teacher_input_price_per_1m: float,
teacher_output_price_per_1m: float,
student_input_price_per_1m: float,
student_output_price_per_1m: float,
finetuning_cost: float = 500 # Engangskostnad for fine-tuning
) -> dict:
"""Calculate monthly savings from model distillation."""
# Teacher-kostnad
teacher_input_cost = (
monthly_requests * avg_input_tokens_before / 1_000_000
* teacher_input_price_per_1m)
teacher_output_cost = (
monthly_requests * avg_output_tokens / 1_000_000
* teacher_output_price_per_1m)
teacher_total = teacher_input_cost + teacher_output_cost
# Student-kostnad
student_input_cost = (
monthly_requests * avg_input_tokens_after / 1_000_000
* student_input_price_per_1m)
student_output_cost = (
monthly_requests * avg_output_tokens / 1_000_000
* student_output_price_per_1m)
student_total = student_input_cost + student_output_cost
monthly_savings = teacher_total - student_total
roi_months = finetuning_cost / monthly_savings if monthly_savings > 0 else float('inf')
return {
"teacher_monthly_nok": round(teacher_total * 11, 2), # USD → NOK
"student_monthly_nok": round(student_total * 11, 2),
"monthly_savings_nok": round(monthly_savings * 11, 2),
"savings_pct": round((1 - student_total / teacher_total) * 100, 1),
"roi_months": round(roi_months, 1)
}
# Eksempel: Statens vegvesen dokumentanalyse
savings = calculate_distillation_savings(
monthly_requests=100_000,
avg_input_tokens_before=1200, # Med few-shot
avg_input_tokens_after=550, # Distillert
avg_output_tokens=300,
teacher_input_price_per_1m=2.50,
teacher_output_price_per_1m=10.00,
student_input_price_per_1m=0.15,
student_output_price_per_1m=0.60,
finetuning_cost=500
)
print(f"Månedlig besparelse: {savings['monthly_savings_nok']} NOK")
print(f"ROI: {savings['roi_months']} måneder")
```
## Use Case Suitability
### Når distillation er egnet
| Use case | Egnethet | Begrunnelse |
|----------|----------|-------------|
| Dokumentklassifisering | Svært egnet | Enkel oppgave, høy konsistens |
| Oppsummering | Egnet | Forutsigbart format, godt distillert |
| Sentiment-analyse | Svært egnet | Binær/tertsiær output |
| Kodeforklaring | Moderat egnet | Krever presisjon, men mønsterbart |
| Kreativ skriving | Lite egnet | Variasjon er ønskelig |
| Kompleks resonnering | Lite egnet | Mister nuanser ved distillation |
| Flerspråklig oversettelse | Moderat egnet | Avhenger av språkpar og domene |
### Når distillation IKKE bør brukes
```
❌ Oppgaven krever konstant oppdatert kunnskap (bruk RAG i stedet)
❌ Output-variabilitet er viktig (kreative oppgaver)
❌ Volumet er for lavt (< 1000 forespørsler/mnd) — besparelsen dekker ikke fine-tuning-kostnad
❌ Oppgaven endrer seg ofte — modellen må re-trenes
❌ Sikkerhetskritiske beslutninger der teacher-modellens resonnering er viktig
```
## Norsk offentlig sektor
- **Personvern og GDPR**: Stored Completions lagrer brukerdata — sørg for at databehandleravtale dekker fine-tuning-formål. Treningsdata kan ikke eksporteres fra Azure AI Foundry.
- **Utredningsinstruksen**: Distillation bør dokumenteres som et tiltak for kostnadsoptimalisering i AI-utredninger. Beregn besparelser over 3-5 år for å rettferdiggjøre initial investering.
- **Forvaltningsloven**: Hvis den distillerte modellen brukes til vedtaksstøtte, dokumenter at kvaliteten er validert og at den oppfyller krav til forsvarlig saksbehandling.
- **Anskaffelser**: Fine-tuning hosting koster per time (uavhengig av bruk). Sammenlign totalkostnad inkludert hosting mot standard pay-per-token.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Høyt volum, enkel oppgave | Distiller til nano/mini | Størst kostnadsbesparelse |
| Middels volum, moderat kompleksitet | Distiller til mini | God balanse kvalitet/kostnad |
| Lavt volum (<1K/mnd) | Behold teacher | Fine-tuning-kostnad > besparelse |
| Hyppig endring i oppgave | Unngå distillation | Re-training overhead |
| Latens-kritisk (<500ms) | Distiller til nano + PTU | Lavest mulig responstid |
## Referanser
- [Azure OpenAI stored completions & distillation](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/stored-completions) — Distillation workflow
- [Fine-tuning considerations](https://learn.microsoft.com/azure/ai-foundry/openai/concepts/fine-tuning-considerations) — Når fine-tuning er riktig
- [Customize a model with fine-tuning](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/fine-tuning) — Fine-tuning guide
- [Choose the right AI model](https://learn.microsoft.com/azure/architecture/ai-ml/guide/choose-ai-model) — Modellvalg-guide
## For Cosmo
- **Bruk denne referansen** når kunden har høyt volum av repetitive AI-oppgaver og ønsker å redusere kostnader uten å miste kvalitet.
- Stored Completions → Distill-flyten i Azure AI Foundry er den enkleste veien — ingen manuell datakuratering nødvendig.
- Anbefal alltid evaluering med reelle testdata før produksjonsdeployment av distillert modell — kvalitetstap varierer sterkt per oppgave.
- GPT-4.1-nano gir 59,400 input TPM per PTU vs. 3,000 for GPT-4.1 — en 20x throughput-økning for enkle oppgaver.
- Fine-tuned modeller har hosting-kostnad per time — beregn break-even punkt basert på forventet volum.

View file

@ -0,0 +1,549 @@
# Performance Benchmarking Frameworks
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Et performance benchmarking framework for Azure AI Services gir en strukturert tilnærming til å måle, sammenligne og spore ytelse over tid. Uten et rammeverk blir ytelsesmålinger ad hoc, ikke-reproduserbare og vanskelige å sammenligne mellom modellversjoner, deployment-konfigurasjoner eller arkitekturendringer.
Microsoft tilbyr et offisielt benchmarking-verktøy (azure-openai-benchmark) spesifikt for Azure OpenAI, samt Azure Load Testing for bredere lasttesting. I tillegg tilbyr Azure AI Foundry innebygde evalueringsverktøy som kan brukes for å måle modellkvalitet. Et komplett benchmarking framework kombinerer disse verktøyene med egendefinerte metrikker, baseline-etablering og automatisk regresjonsdeteksjon.
For norsk offentlig sektor er et benchmarking framework viktig for å dokumentere ytelseskrav i tjenesteavtaler, verifisere at nye modellversjoner møter kvalitetskrav, og for å sikre at AI-tjenester oppfyller krav til responstid i henhold til digitaliseringsstrategien.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| azure-openai-benchmark | Offisielt Azure OpenAI benchmarking CLI | GitHub/Python |
| Azure Load Testing | Managed lasttesting med JMeter | Azure Load Testing |
| Azure AI Foundry Evaluations | Modellkvalitets-evaluering | Azure AI Foundry |
| Azure Monitor | Metrikk-innsamling og visualisering | Azure Monitor |
| Application Insights | End-to-end request tracing | App Insights |
| Custom Benchmark Suite | Prosjektspesifikke ytelsestester | Python/C# |
## Metric Definition Standards
### Kjernemetrikker for AI-ytelse
```python
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
class MetricCategory(Enum):
LATENCY = "latency"
THROUGHPUT = "throughput"
QUALITY = "quality"
COST = "cost"
AVAILABILITY = "availability"
@dataclass
class BenchmarkMetric:
name: str
category: MetricCategory
unit: str
description: str
target: Optional[float] = None
warning_threshold: Optional[float] = None
critical_threshold: Optional[float] = None
# Standard metrikkdefinisjoner for Azure OpenAI
STANDARD_METRICS = [
BenchmarkMetric(
name="time_to_first_token",
category=MetricCategory.LATENCY,
unit="ms",
description="Tid fra forespørsel sendt til første token mottatt",
target=500,
warning_threshold=1000,
critical_threshold=3000
),
BenchmarkMetric(
name="end_to_end_latency_p50",
category=MetricCategory.LATENCY,
unit="ms",
description="P50 total responstid inkl. alle tokens",
target=2000,
warning_threshold=5000,
critical_threshold=15000
),
BenchmarkMetric(
name="end_to_end_latency_p95",
category=MetricCategory.LATENCY,
unit="ms",
description="P95 total responstid",
target=5000,
warning_threshold=10000,
critical_threshold=30000
),
BenchmarkMetric(
name="tokens_per_second",
category=MetricCategory.THROUGHPUT,
unit="tokens/s",
description="Output tokens generert per sekund",
target=40,
warning_threshold=20,
critical_threshold=10
),
BenchmarkMetric(
name="requests_per_second",
category=MetricCategory.THROUGHPUT,
unit="req/s",
description="Vellykkede forespørsler per sekund",
target=5,
warning_threshold=2,
critical_threshold=1
),
BenchmarkMetric(
name="throttle_rate",
category=MetricCategory.AVAILABILITY,
unit="%",
description="Andel forespørsler som fikk 429",
target=0,
warning_threshold=5,
critical_threshold=20
),
BenchmarkMetric(
name="error_rate",
category=MetricCategory.AVAILABILITY,
unit="%",
description="Andel feilede forespørsler (ekskl. 429)",
target=0,
warning_threshold=1,
critical_threshold=5
),
BenchmarkMetric(
name="cost_per_request_nok",
category=MetricCategory.COST,
unit="NOK",
description="Gjennomsnittlig kostnad per forespørsel",
target=0.50,
warning_threshold=1.00,
critical_threshold=5.00
),
BenchmarkMetric(
name="prompt_cache_hit_rate",
category=MetricCategory.COST,
unit="%",
description="Andel input-tokens som treffer prompt cache",
target=60,
warning_threshold=30,
critical_threshold=10
)
]
```
## Baseline Establishment
### Systematisk baseline-etablering
```python
import json
import asyncio
from datetime import datetime
from dataclasses import asdict
@dataclass
class BenchmarkBaseline:
model: str
deployment_type: str
region: str
date: str
workload_shape: dict
metrics: dict
environment: dict
class BaselineEstablisher:
"""Establish performance baseline for AI deployments."""
def __init__(self, client, model: str, deployment_type: str, region: str):
self.client = client
self.model = model
self.deployment_type = deployment_type
self.region = region
async def establish_baseline(
self,
test_prompts: list[dict],
num_iterations: int = 100,
concurrency_levels: list[int] = None
) -> BenchmarkBaseline:
"""Run comprehensive baseline benchmark."""
if concurrency_levels is None:
concurrency_levels = [1, 5, 10, 20]
all_results = {}
for concurrency in concurrency_levels:
results = await self._run_at_concurrency(
test_prompts, num_iterations, concurrency)
all_results[f"concurrency_{concurrency}"] = results
# Beregn aggregerte metrikker
baseline_metrics = self._aggregate_metrics(all_results)
baseline = BenchmarkBaseline(
model=self.model,
deployment_type=self.deployment_type,
region=self.region,
date=datetime.utcnow().isoformat(),
workload_shape={
"num_prompts": len(test_prompts),
"avg_input_tokens": self._avg_tokens(test_prompts),
"iterations": num_iterations,
"concurrency_levels": concurrency_levels
},
metrics=baseline_metrics,
environment={
"api_version": "2024-10-21",
"sdk_version": "1.x"
}
)
return baseline
async def _run_at_concurrency(
self, prompts, iterations, concurrency
) -> dict:
"""Run benchmark at specific concurrency level."""
import time
semaphore = asyncio.Semaphore(concurrency)
latencies = []
ttfts = []
token_counts = []
errors = 0
throttled = 0
async def send_one(prompt):
nonlocal errors, throttled
async with semaphore:
start = time.time()
try:
# Streaming for TTFT measurement
first_token_time = None
total_tokens = 0
stream = await self.client.chat.completions.create(
model=self.model,
messages=prompt["messages"],
stream=True,
max_tokens=500
)
async for chunk in stream:
if first_token_time is None and \
chunk.choices and \
chunk.choices[0].delta.content:
first_token_time = time.time()
if chunk.choices and chunk.choices[0].delta.content:
total_tokens += 1
end = time.time()
latencies.append((end - start) * 1000)
if first_token_time:
ttfts.append((first_token_time - start) * 1000)
token_counts.append(total_tokens)
except Exception as e:
errors += 1
if hasattr(e, 'status_code') and e.status_code == 429:
throttled += 1
tasks = []
for i in range(iterations):
prompt = prompts[i % len(prompts)]
tasks.append(send_one(prompt))
start_time = time.time()
await asyncio.gather(*tasks)
total_duration = time.time() - start_time
return {
"latency_p50": sorted(latencies)[len(latencies)//2] if latencies else 0,
"latency_p95": sorted(latencies)[int(len(latencies)*0.95)] if latencies else 0,
"latency_p99": sorted(latencies)[int(len(latencies)*0.99)] if latencies else 0,
"ttft_p50": sorted(ttfts)[len(ttfts)//2] if ttfts else 0,
"ttft_p95": sorted(ttfts)[int(len(ttfts)*0.95)] if ttfts else 0,
"throughput_rps": round(len(latencies) / total_duration, 2),
"tps": round(sum(token_counts) / total_duration, 1),
"error_rate": round(errors / iterations * 100, 2),
"throttle_rate": round(throttled / iterations * 100, 2)
}
def _aggregate_metrics(self, all_results: dict) -> dict:
"""Aggregate results across concurrency levels."""
return {
"optimal_concurrency": max(
all_results.keys(),
key=lambda k: all_results[k]["throughput_rps"]
),
"by_concurrency": all_results
}
def _avg_tokens(self, prompts):
return round(sum(
len(str(p).split()) for p in prompts
) / len(prompts))
def save_baseline(self, baseline: BenchmarkBaseline, path: str):
"""Save baseline to JSON file."""
with open(path, "w") as f:
json.dump(asdict(baseline), f, indent=2, default=str)
```
## Regression Detection
### Automatisk regresjonsdeteksjon
```python
from dataclasses import dataclass
@dataclass
class RegressionResult:
metric_name: str
baseline_value: float
current_value: float
change_pct: float
severity: str # "none", "warning", "critical"
direction: str # "improved", "degraded", "stable"
class RegressionDetector:
"""Detect performance regressions against baseline."""
def __init__(
self,
baseline: BenchmarkBaseline,
warning_threshold_pct: float = 20,
critical_threshold_pct: float = 50
):
self.baseline = baseline
self.warning_pct = warning_threshold_pct
self.critical_pct = critical_threshold_pct
def compare(self, current_metrics: dict) -> list[RegressionResult]:
"""Compare current metrics against baseline."""
results = []
# Definer retning: for noen metrikker er lavere bedre
lower_is_better = {
"latency_p50", "latency_p95", "latency_p99",
"ttft_p50", "ttft_p95",
"error_rate", "throttle_rate",
"cost_per_request_nok"
}
baseline_data = self.baseline.metrics.get(
"by_concurrency", {}).get(
self.baseline.metrics.get("optimal_concurrency", ""),
{})
for metric_name, baseline_value in baseline_data.items():
if metric_name not in current_metrics:
continue
current_value = current_metrics[metric_name]
if baseline_value == 0:
continue
change_pct = (
(current_value - baseline_value) / baseline_value * 100)
# Bestem om endring er forbedring eller forverring
is_lower_better = metric_name in lower_is_better
if is_lower_better:
degraded = change_pct > 0
else:
degraded = change_pct < 0
abs_change = abs(change_pct)
if abs_change < 5:
severity = "none"
direction = "stable"
elif degraded:
severity = (
"critical" if abs_change > self.critical_pct
else "warning" if abs_change > self.warning_pct
else "none")
direction = "degraded"
else:
severity = "none"
direction = "improved"
results.append(RegressionResult(
metric_name=metric_name,
baseline_value=round(baseline_value, 2),
current_value=round(current_value, 2),
change_pct=round(change_pct, 1),
severity=severity,
direction=direction
))
return results
def generate_report(self, results: list[RegressionResult]) -> str:
"""Generate human-readable regression report."""
lines = [
"# Performance Regression Report",
f"Baseline: {self.baseline.date}",
f"Model: {self.baseline.model}",
f"Region: {self.baseline.region}",
""
]
critical = [r for r in results if r.severity == "critical"]
warnings = [r for r in results if r.severity == "warning"]
improvements = [r for r in results if r.direction == "improved"]
if critical:
lines.append("## CRITICAL Regressions")
for r in critical:
lines.append(
f"- **{r.metric_name}**: "
f"{r.baseline_value} → {r.current_value} "
f"({r.change_pct:+.1f}%)")
if warnings:
lines.append("\n## Warnings")
for r in warnings:
lines.append(
f"- {r.metric_name}: "
f"{r.baseline_value} → {r.current_value} "
f"({r.change_pct:+.1f}%)")
if improvements:
lines.append("\n## Improvements")
for r in improvements:
lines.append(
f"- {r.metric_name}: "
f"{r.baseline_value} → {r.current_value} "
f"({r.change_pct:+.1f}%)")
return "\n".join(lines)
```
## Comparative Analysis Methods
### A/B-testing av modeller og konfigurasjoner
```python
class ABBenchmarkComparator:
"""Compare performance between two configurations."""
def __init__(self):
self.results_a = None
self.results_b = None
async def compare_configs(
self,
config_a: dict,
config_b: dict,
test_prompts: list[dict],
iterations: int = 100
) -> dict:
"""Run same workload against two configs and compare."""
# Kjør A
self.results_a = await self._benchmark(
config_a, test_prompts, iterations)
# Kjør B
self.results_b = await self._benchmark(
config_b, test_prompts, iterations)
# Sammenlign
comparison = {}
for metric in self.results_a:
if metric in self.results_b:
val_a = self.results_a[metric]
val_b = self.results_b[metric]
if val_a != 0:
change = (val_b - val_a) / val_a * 100
else:
change = 0
comparison[metric] = {
"config_a": round(val_a, 2),
"config_b": round(val_b, 2),
"change_pct": round(change, 1),
"winner": "A" if self._is_better(metric, val_a, val_b)
else "B"
}
return comparison
def _is_better(self, metric: str, val_a: float, val_b: float) -> bool:
"""Determine if A is better than B for given metric."""
lower_better = {"latency", "error", "throttle", "cost", "ttft"}
is_lower_better = any(k in metric for k in lower_better)
return (val_a < val_b) if is_lower_better else (val_a > val_b)
# CI/CD integrasjon
async def ci_benchmark_gate(
baseline_path: str,
client,
model: str,
test_prompts: list[dict],
max_regression_pct: float = 20
) -> bool:
"""Run benchmark as CI/CD quality gate."""
with open(baseline_path) as f:
baseline_data = json.load(f)
baseline = BenchmarkBaseline(**baseline_data)
# Kjør benchmark
establisher = BaselineEstablisher(client, model, "standard", "norwayeast")
current = await establisher._run_at_concurrency(test_prompts, 50, 10)
# Sjekk regresjoner
detector = RegressionDetector(baseline, warning_threshold_pct=max_regression_pct)
results = detector.compare(current)
critical = [r for r in results if r.severity == "critical"]
if critical:
print("BENCHMARK GATE FAILED:")
for r in critical:
print(f" {r.metric_name}: {r.change_pct:+.1f}% regression")
return False
print("BENCHMARK GATE PASSED")
return True
```
## Norsk offentlig sektor
- **Dokumentasjon**: Benchmark-resultater bør lagres som del av prosjektdokumentasjonen og refereres i tjenesteavtaler.
- **Regelmessighet**: Kjør benchmarks månedlig og etter alle modelloppgraderinger, arkitekturendringer eller kvotejusteringer.
- **Kvalitetskrav**: Definer akseptable ytelsesgrenser i samarbeid med tjenesteeier — bruk STANDARD_METRICS som utgangspunkt.
- **Åpenhet**: For AI-tjenester som eksponeres mot borgere, dokumenter forventet responstid og tilgjengelighet.
- **CI/CD**: Integrer benchmark-gate i deployment-pipeline for å fange regresjoner før de når produksjon.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Ny deployment | Etabler baseline med full suite | Referansepunkt for fremtidige sammenligninger |
| Modelloppgradering | A/B sammenligning mot baseline | Verifiser at ny modell er like god eller bedre |
| Kvoteendring | Kjør throughput-benchmark | Mål faktisk forbedring |
| Produksjonsalert | Sammenlign mot baseline | Identifiser om det er regresjon |
| Kvartalsvis review | Full benchmark suite | Fang gradvis degradering |
## Referanser
- [Azure OpenAI Benchmark Tool](https://github.com/Azure/azure-openai-benchmark) — Offisielt CLI-verktøy
- [Azure Load Testing](https://learn.microsoft.com/azure/load-testing/overview-what-is-azure-load-testing) — Managed lasttesting
- [Performance and latency](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/latency) — Ytelseskonsepter
- [Evaluate generative AI models](https://learn.microsoft.com/azure/ai-foundry/how-to/evaluate-generative-ai-app) — Kvalitetsevaluering
- [Azure Monitor metrics](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/monitor-openai) — Azure OpenAI monitoring
## For Cosmo
- **Bruk denne referansen** når kunden trenger å etablere ytelsesbaselines, sette opp regelmessig ytelsestesting, eller integrere benchmarks i CI/CD.
- Et benchmark framework er IKKE valgfritt for produksjons-AI — uten baseline kan du ikke oppdage regresjoner eller validere forbedringer.
- Bruk det offisielle azure-openai-benchmark for PTU-dimensjonering, og custom Python-benchmarks for applikasjonsspesifikke metrikker.
- Kjør benchmarks i minimum 10 minutter per scenario for å oppnå steady state — korte tester gir misvisende resultater.
- Integrer ci_benchmark_gate i deployment pipeline — aldri deploy til produksjon uten å verifisere ytelse mot baseline.

View file

@ -0,0 +1,374 @@
# Prompt Caching for Performance
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Azure OpenAI prompt caching er en innebygd mekanisme som reduserer latens og kostnad for forespørsler med identiske prefixer. Når de første 1024+ tokens i en prompt er identiske med en tidligere forespørsel, gjenbruker tjenesten de allerede beregnede token-representasjonene i stedet for å prosessere dem på nytt. Dette gir raskere time-to-first-token (TTFT) og lavere kostnad — cached tokens faktureres med rabatt for Standard deployments og opptil 100% rabatt for Provisioned (PTU) deployments.
Prompt caching er automatisk aktivert for alle støttede modeller (GPT-4o og nyere) uten ekstra konfigurasjon. Cachen er basert på en hash av de første ~256 tokens og krever minimum 1024 identiske tokens for å trigge. Etter den initiale 1024-token terskelen caches ytterligere identiske tokens i blokker på 128. Cacher tømmes typisk innen 5-10 minutter uten aktivitet og alltid innen 24 timer.
For norsk offentlig sektor der AI-applikasjoner ofte har lange, statiske system-prompts (inkludert regelverk, instruksjoner og eksempler), er prompt caching en svært effektiv optimaliseringsstrategi som kan gi 30-50% kostnadsreduksjon uten noen endring i output-kvalitet.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Prompt Cache | Automatisk caching av identiske prefixer | Azure OpenAI |
| prompt_cache_key | Valgfri parameter for å påvirke cache routing | Azure OpenAI API |
| cached_tokens | API-respons felt som viser cache hits | prompt_tokens_details |
| Semantic Cache | Ekstern cache for semantisk like forespørsler | Azure Cosmos DB |
| Multi-layer Caching | Kombinert caching-strategi | Arkitektur-mønster |
## Cache Eligibility Requirements
### Tekniske krav for prompt caching
```python
# Krav for at prompt caching skal fungere:
CACHE_REQUIREMENTS = {
"minimum_prefix_length": 1024, # Tokens
"hash_prefix_length": 256, # Tokens brukt for routing-hash
"subsequent_block_size": 128, # Etter 1024, cache i 128-blokker
"cache_ttl_inactive": "5-10 min",
"cache_ttl_max": "24 timer",
"cross_subscription": False, # Cache deles IKKE mellom abonnement
"supported_models": [
"gpt-4o-*",
"gpt-4o-mini-*",
"gpt-4.1-*",
"gpt-4.1-mini-*",
"gpt-4.1-nano-*",
"o1-*",
"o3-*",
"o3-mini-*"
],
"supported_operations": [
"chat-completions",
"completions",
"responses",
"real-time"
]
}
# Sjekk om en prompt er cache-eligible
def is_cache_eligible(messages: list[dict], model: str = "gpt-4o") -> dict:
"""Check if a prompt is eligible for caching."""
import tiktoken
enc = tiktoken.encoding_for_model(model)
# Beregn total tokens for alle meldinger
total_tokens = 0
for msg in messages:
total_tokens += len(enc.encode(msg["content"]))
total_tokens += 4 # Role tokens overhead
return {
"total_tokens": total_tokens,
"eligible": total_tokens >= 1024,
"cacheable_tokens": max(0, (total_tokens // 128) * 128)
if total_tokens >= 1024 else 0,
"recommendation": (
"Eligible for caching" if total_tokens >= 1024
else f"Need {1024 - total_tokens} more tokens in prefix"
)
}
```
## Prefix Strategy Design
### Optimaliser prompt-struktur for caching
```python
def design_cacheable_prompt(
system_instructions: str,
few_shot_examples: list[dict],
reference_documents: str,
user_query: str
) -> tuple[list[dict], dict]:
"""
Design prompt with optimal structure for caching.
Prinsipp: Statisk innhold FØRST, dynamisk innhold SIST.
Alt fra starten til det dynamiske innholdet caches.
"""
messages = []
# --- CACHEABLE PREFIX START ---
# 1. System prompt (statisk per applikasjon)
messages.append({
"role": "system",
"content": system_instructions
})
# 2. Few-shot eksempler (statisk per oppgave)
for example in few_shot_examples:
messages.append({"role": "user", "content": example["input"]})
messages.append({"role": "assistant", "content": example["output"]})
# 3. Referansedokumenter (statisk per sesjon)
if reference_documents:
messages.append({
"role": "user",
"content": f"Referansemateriale:\n\n{reference_documents}"
})
messages.append({
"role": "assistant",
"content": "Forstått. Jeg vil bruke referansematerialet."
})
# --- CACHEABLE PREFIX SLUTT ---
# 4. Dynamisk brukerforespørsel (varierer — IKKE cached)
messages.append({
"role": "user",
"content": user_query
})
# Beregn cache-statistikk
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
static_tokens = sum(
len(enc.encode(m["content"])) + 4
for m in messages[:-1] # Alt unntatt siste melding
)
dynamic_tokens = len(enc.encode(user_query)) + 4
stats = {
"static_prefix_tokens": static_tokens,
"dynamic_tokens": dynamic_tokens,
"cache_eligible": static_tokens >= 1024,
"cache_hit_savings_pct": round(
static_tokens / (static_tokens + dynamic_tokens) * 100, 1
) if static_tokens >= 1024 else 0
}
return messages, stats
# Eksempel: Saksbehandler-assistent for Statens vegvesen
messages, stats = design_cacheable_prompt(
system_instructions="""Du er en AI-assistent for saksbehandlere i
Statens vegvesen. Du hjelper med å analysere klager på vedtak om
førerkort, vurdere om klagen har grunnlag, og foreslå svar.
Regelverk du skal referere til:
- Vegtrafikkloven § 24-34
- Førerkortforskriften
- Forvaltningsloven § 28-36 (klagebehandling)
Format: Alltid bruk overskrifter, vurder hvert punkt separat,
og avslutt med en samlet anbefaling.""",
few_shot_examples=[
{
"input": "Klage: Jeg fikk avslag på fornyelse...",
"output": "## Vurdering\n### Regelverksvurdering..."
},
{
"input": "Klage: Mitt førerkort ble inndratt...",
"output": "## Vurdering\n### Regelverksvurdering..."
}
],
reference_documents="Vedtaket av 15.01.2025 om avslag...",
user_query="Analyser denne nye klagen: ..."
)
print(f"Cacheable prefix: {stats['static_prefix_tokens']} tokens")
print(f"Cache savings: ~{stats['cache_hit_savings_pct']}%")
```
### prompt_cache_key for forbedret hit rate
```python
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://my-aoai.openai.azure.com",
api_key="...",
api_version="2024-12-01-preview"
)
# Bruk prompt_cache_key for å forbedre routing
# Forespørsler med samme key og prefix routes til samme cache
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
prompt_cache_key="svv-complaint-handler-v2", # Gruppert caching
max_tokens=1000
)
# Sjekk cache hit
cached = response.usage.prompt_tokens_details.cached_tokens
total_prompt = response.usage.prompt_tokens
print(f"Cached tokens: {cached} / {total_prompt}")
print(f"Cache hit rate: {cached / total_prompt * 100:.1f}%")
# Advarsel: Mer enn ~15 RPM med samme prefix + cache_key
# kan overflow til andre maskiner og redusere cache-effektivitet
```
## Cost Reduction Calculation
### Beregn besparelser fra prompt caching
```python
def calculate_caching_savings(
monthly_requests: int,
avg_total_input_tokens: int,
avg_cached_tokens: int, # Tokens som treffer cache
model: str = "gpt-4o",
deployment_type: str = "standard" # "standard" eller "provisioned"
) -> dict:
"""Calculate cost savings from prompt caching."""
# Priser (USD per 1M tokens, estimater)
pricing = {
"gpt-4o": {
"standard": {"input": 2.50, "cached_discount": 0.50},
"provisioned": {"input": 0, "cached_discount": 1.0}
},
"gpt-4.1": {
"standard": {"input": 2.00, "cached_discount": 0.50},
"provisioned": {"input": 0, "cached_discount": 1.0}
},
"gpt-4o-mini": {
"standard": {"input": 0.15, "cached_discount": 0.50},
"provisioned": {"input": 0, "cached_discount": 1.0}
}
}
p = pricing.get(model, pricing["gpt-4o"])
dt = p.get(deployment_type, p["standard"])
non_cached_tokens = avg_total_input_tokens - avg_cached_tokens
# Uten caching
cost_without_cache = (
monthly_requests * avg_total_input_tokens / 1_000_000 * dt["input"])
# Med caching
cached_cost = (
monthly_requests * avg_cached_tokens / 1_000_000 *
dt["input"] * (1 - dt["cached_discount"]))
non_cached_cost = (
monthly_requests * non_cached_tokens / 1_000_000 * dt["input"])
cost_with_cache = cached_cost + non_cached_cost
savings = cost_without_cache - cost_with_cache
return {
"monthly_requests": monthly_requests,
"cache_hit_rate": round(
avg_cached_tokens / avg_total_input_tokens * 100, 1),
"cost_without_cache_nok": round(cost_without_cache * 11, 2),
"cost_with_cache_nok": round(cost_with_cache * 11, 2),
"monthly_savings_nok": round(savings * 11, 2),
"savings_pct": round(savings / max(cost_without_cache, 0.01) * 100, 1),
"note": (
"PTU: cached tokens er 100% rabatt" if deployment_type == "provisioned"
else "Standard: cached tokens er 50% rabatt")
}
# Eksempel: 50K forespørsler/mnd med RAG-pipeline
savings = calculate_caching_savings(
monthly_requests=50_000,
avg_total_input_tokens=3000,
avg_cached_tokens=2000, # System prompt + examples cached
model="gpt-4o",
deployment_type="standard"
)
print(f"Månedlig besparelse: {savings['monthly_savings_nok']} NOK")
print(f"Besparelse: {savings['savings_pct']}%")
```
## Cache Invalidation
### Håndtering av cache-endringer
```python
class CacheAwarePromptManager:
"""Manage prompts with cache invalidation awareness."""
def __init__(self, base_system_prompt: str, version: str = "v1"):
self.base_system_prompt = base_system_prompt
self.version = version
self._prefix_hash = self._compute_hash(base_system_prompt)
def _compute_hash(self, text: str) -> str:
import hashlib
return hashlib.sha256(text.encode()).hexdigest()[:16]
def update_system_prompt(self, new_prompt: str):
"""
Oppdater system prompt. MERK: Dette invaliderer ALL cache
for denne applikasjonen fordi prefix endres.
Anbefaling: Gjør endringer i off-peak timer.
"""
new_hash = self._compute_hash(new_prompt)
if new_hash != self._prefix_hash:
print(f"WARNING: System prompt endret. "
f"Cache invalideres for alle forespørsler.")
print(f"Gammel hash: {self._prefix_hash}")
print(f"Ny hash: {new_hash}")
print(f"Anbefaling: Deploy endringen i off-peak timer "
f"for å minimere cache miss-kostnaden.")
self.base_system_prompt = new_prompt
self._prefix_hash = new_hash
def get_cache_key(self) -> str:
"""Get cache key for prompt_cache_key parameter."""
return f"app-{self.version}-{self._prefix_hash}"
# Cache invalidation triggers:
# 1. Endring i system prompt → Umiddelbar invalidering
# 2. Endring i few-shot examples → Invalidering fra det punktet
# 3. Inaktivitet > 5-10 min → Automatisk tømming
# 4. > 24 timer siden siste bruk → Garantert tømming
# 5. En eneste endret karakter i prefix → Full cache miss
```
## Norsk offentlig sektor
- **Kostnadseffektivitet**: Prompt caching er "gratis" optimalisering — ingen konfigurasjon nødvendig, bare riktig prompt-design. Spar 30-50% på input-token kostnader.
- **PTU-deployments**: For PTU er cached tokens 100% gratis — dette betyr at riktig prefix-design kan doble effektiv throughput.
- **Personvern**: Prompt caches er isolert per Azure-abonnement og deles ikke mellom kunder. Data i cache følger samme databehandling som vanlige forespørsler.
- **Forutsigbarhet**: Cache hit rate kan monitoreres via `cached_tokens` i API-responsen — bygg dashboards for å spore cache-effektivitet over tid.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Lang system prompt (>500 tokens) | Design for caching | Mest å vinne |
| Mange few-shot examples | Flytt til prefix, bruk caching | Reduser input-kostnad |
| RAG med statisk kontekst | Cache system + kontekst, varier spørsmål | Høy hit rate |
| Unik prompt per forespørsel | Caching gir lite | Prefix endres for ofte |
| PTU deployment | Maksimer caching | 100% rabatt på cached tokens |
| Høy RPM (>15 per prefix) | Bruk prompt_cache_key | Forbedrer routing |
## Referanser
- [Prompt caching](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/prompt-caching) — Offisiell guide
- [Provisioned throughput](https://learn.microsoft.com/azure/ai-foundry/openai/concepts/provisioned-throughput) — PTU caching-fordeler
- [Semantic cache with Cosmos DB](https://learn.microsoft.com/azure/cosmos-db/gen-ai/semantic-cache) — Ekstern caching
- [Application design for AI workloads](https://learn.microsoft.com/azure/well-architected/ai/application-design) — Multi-layer caching
## For Cosmo
- **Bruk denne referansen** når kunden vil redusere kostnader eller latens for Azure OpenAI-workloads med repetitive prompt-strukturer.
- Hovedregelen: Statisk innhold FØRST i prompten, dynamisk innhold SIST — alt statisk prefix caches automatisk.
- Minimum 1024 tokens i identisk prefix for cache hit — legg til referansemateriale eller detaljerte instruksjoner for å nå terskelen.
- For PTU: cached tokens teller 100% rabatt mot utilization — dette er den mest effektive optimaliseringen for PTU-deployments.
- En eneste endret karakter i prefix gir full cache miss — vær forsiktig med dynamiske elementer (timestamps, request-IDs) i starten av prompts.

View file

@ -0,0 +1,432 @@
# Rate Limit Management
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Azure OpenAI bruker to rate limit-mekanismer: Tokens-per-Minute (TPM) og Requests-per-Minute (RPM). Når en av disse grensene overskrides, returnerer tjenesten HTTP 429 (Too Many Requests) med en `Retry-After` header som angir hvor mange sekunder klienten bør vente. For Standard deployments er rate limits direkte koblet til den tildelte kvoten, mens Provisioned Throughput (PTU) deployments returnerer 429 når utilization overstiger 100%.
Rate limit management er en av de mest kritiske aspektene ved produksjonsdrift av Azure OpenAI. Uten robust håndtering vil brukere oppleve sporadiske feil, og applikasjonen kan miste forespørsler under belastningstopper. Microsofts offisielle SDK-er (Python og JavaScript) har innebygd retry-logikk med eksponentiell backoff, men dette dekker kun grunnleggende scenarier. For produksjonsarkitekturer trengs mer sofistikerte strategier som multi-region failover, proaktiv throttling og quota monitoring.
For norsk offentlig sektor, der AI-tjenester kan være forretningskritiske for saksbehandling, er det avgjørende å ha en veldefinert strategi for rate limit management som sikrer at tjenesten er tilgjengelig selv under belastningstopper.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| TPM/RPM Quota | Rate limiting per deployment | Azure OpenAI |
| Retry-After header | Server-side ventetid-instruksjon | HTTP 429 respons |
| Azure APIM | Gateway med rate limiting policies | Azure API Management |
| Circuit Breaker | Forhindre kaskade-feil | APIM / custom |
| Quota Management API | Programmatisk kvotejustering | Azure Management REST API |
| Azure Monitor | Rate limit-metrikker og alerting | Azure Monitor |
## Exponential Backoff Implementation
### Python SDK innebygd retry
```python
from openai import AzureOpenAI
# SDK har innebygd retry med exponential backoff
client = AzureOpenAI(
azure_endpoint="https://my-aoai.openai.azure.com",
api_key="...",
api_version="2024-10-21",
max_retries=3, # Default: 2
timeout=120.0 # Total timeout i sekunder
)
# Per-request override
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
extra_headers={"max_retries": "5"} # Maks 5 forsøk for denne
)
```
### Custom retry med respekt for Retry-After
```python
import asyncio
import time
import random
from openai import AsyncAzureOpenAI, RateLimitError, APIError
class RateLimitHandler:
"""Advanced rate limit handling with exponential backoff."""
def __init__(
self,
client: AsyncAzureOpenAI,
max_retries: int = 5,
base_delay: float = 1.0,
max_delay: float = 60.0,
jitter: bool = True
):
self.client = client
self.max_retries = max_retries
self.base_delay = base_delay
self.max_delay = max_delay
self.jitter = jitter
self._consecutive_429s = 0
async def chat_completion(self, **kwargs) -> dict:
"""Execute chat completion with advanced retry logic."""
last_exception = None
for attempt in range(self.max_retries + 1):
try:
response = await self.client.chat.completions.create(**kwargs)
self._consecutive_429s = 0 # Reset on success
return response
except RateLimitError as e:
self._consecutive_429s += 1
last_exception = e
# Respekter Retry-After header
retry_after = getattr(e, 'retry_after', None)
if retry_after:
delay = float(retry_after)
else:
# Exponential backoff: 1s, 2s, 4s, 8s, 16s...
delay = min(
self.base_delay * (2 ** attempt),
self.max_delay
)
# Legg til jitter for å unngå thundering herd
if self.jitter:
delay *= (0.5 + random.random())
print(f"Rate limited (attempt {attempt + 1}/"
f"{self.max_retries}). "
f"Waiting {delay:.1f}s...")
await asyncio.sleep(delay)
except APIError as e:
if e.status_code and e.status_code >= 500:
# Server error — retry
delay = self.base_delay * (2 ** attempt)
await asyncio.sleep(delay)
last_exception = e
else:
raise # Client error — ikke retry
raise last_exception # Alle forsøk brukt opp
@property
def is_throttled(self) -> bool:
"""Check if we're currently experiencing throttling."""
return self._consecutive_429s >= 3
```
### .NET Polly-basert retry
```csharp
using Polly;
using Polly.Retry;
// Konfigurer retry policy med Polly
var retryPolicy = Policy
.Handle<Azure.RequestFailedException>(ex => ex.Status == 429)
.Or<Azure.RequestFailedException>(ex => ex.Status >= 500)
.WaitAndRetryAsync(
retryCount: 5,
sleepDurationProvider: (retryAttempt, exception, context) =>
{
// Bruk Retry-After header hvis tilgjengelig
if (exception is Azure.RequestFailedException rfEx)
{
var retryAfter = rfEx.GetRawResponse()?
.Headers.TryGetValue("Retry-After", out var value)
== true ? value : null;
if (retryAfter != null &&
double.TryParse(retryAfter, out var seconds))
{
return TimeSpan.FromSeconds(seconds);
}
}
// Fallback: exponential backoff med jitter
var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));
var jitter = TimeSpan.FromMilliseconds(
Random.Shared.Next(0, 1000));
return baseDelay + jitter;
},
onRetryAsync: (exception, timespan, retryAttempt, context) =>
{
Console.WriteLine(
$"Retry {retryAttempt} after {timespan.TotalSeconds:F1}s "
+ $"due to {exception.Message}");
return Task.CompletedTask;
}
);
```
## Quota Request Process
### Overvåk og juster kvote programmatisk
```python
import requests
def get_quota_usage(
subscription_id: str,
resource_group: str,
account_name: str,
access_token: str
) -> dict:
"""Get current quota usage for Azure OpenAI deployments."""
url = (
f"https://management.azure.com/subscriptions/{subscription_id}"
f"/resourceGroups/{resource_group}"
f"/providers/Microsoft.CognitiveServices"
f"/accounts/{account_name}"
f"/deployments?api-version=2023-05-01"
)
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(url, headers=headers)
deployments = response.json()["value"]
usage = []
for d in deployments:
props = d["properties"]
usage.append({
"deployment": d["name"],
"model": props["model"]["name"],
"tpm_allocated": props.get("rateLimits", [{}])[0].get(
"count", 0) if props.get("rateLimits") else 0,
"sku": props.get("sku", {}).get("name", "unknown")
})
return usage
def update_deployment_quota(
subscription_id: str,
resource_group: str,
account_name: str,
deployment_name: str,
new_tpm: int,
access_token: str
):
"""Update TPM quota for a deployment."""
url = (
f"https://management.azure.com/subscriptions/{subscription_id}"
f"/resourceGroups/{resource_group}"
f"/providers/Microsoft.CognitiveServices"
f"/accounts/{account_name}"
f"/deployments/{deployment_name}?api-version=2023-05-01"
)
body = {
"sku": {
"name": "Standard",
"capacity": new_tpm // 1000 # TPM i tusen-enheter
}
}
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
response = requests.patch(url, json=body, headers=headers)
return response.json()
```
## Multi-Region Failover
### Automatisk failover ved rate limiting
```python
from dataclasses import dataclass, field
from typing import Optional
import time
@dataclass
class RegionalEndpoint:
region: str
endpoint: str
api_key: str
priority: int = 1
is_healthy: bool = True
throttled_until: float = 0
consecutive_errors: int = 0
class MultiRegionRateLimitHandler:
"""Handle rate limits by failing over to other regions."""
def __init__(self, endpoints: list[RegionalEndpoint]):
self.endpoints = sorted(endpoints, key=lambda e: e.priority)
def _get_available_endpoint(self) -> Optional[RegionalEndpoint]:
"""Get best available endpoint respecting throttle state."""
now = time.time()
for ep in self.endpoints:
if ep.is_healthy and now > ep.throttled_until:
return ep
# Alle throttled — returner den som er tidligst klar
available = sorted(
self.endpoints,
key=lambda e: e.throttled_until
)
return available[0] if available else None
async def execute(self, **kwargs) -> dict:
"""Execute request with multi-region failover."""
for attempt in range(len(self.endpoints) * 2):
endpoint = self._get_available_endpoint()
if not endpoint:
raise Exception("No endpoints available")
# Vent hvis throttled
wait_time = max(0, endpoint.throttled_until - time.time())
if wait_time > 0:
await asyncio.sleep(wait_time)
try:
client = AsyncAzureOpenAI(
azure_endpoint=endpoint.endpoint,
api_key=endpoint.api_key,
api_version="2024-10-21",
max_retries=0 # Vi håndterer retry selv
)
response = await client.chat.completions.create(**kwargs)
endpoint.consecutive_errors = 0
endpoint.is_healthy = True
return response
except RateLimitError as e:
retry_after = getattr(e, 'retry_after', 10)
endpoint.throttled_until = time.time() + float(retry_after)
endpoint.consecutive_errors += 1
print(f"Region {endpoint.region} throttled for "
f"{retry_after}s. Trying next region...")
continue
except APIError as e:
if e.status_code >= 500:
endpoint.consecutive_errors += 1
if endpoint.consecutive_errors >= 3:
endpoint.is_healthy = False
continue
raise
raise Exception("All regions exhausted")
# Konfigurasjon
handler = MultiRegionRateLimitHandler([
RegionalEndpoint(
region="norwayeast",
endpoint="https://aoai-norway.openai.azure.com",
api_key="...",
priority=1
),
RegionalEndpoint(
region="swedencentral",
endpoint="https://aoai-sweden.openai.azure.com",
api_key="...",
priority=2
),
RegionalEndpoint(
region="westeurope",
endpoint="https://aoai-westeu.openai.azure.com",
api_key="...",
priority=3
)
])
```
## Usage Monitoring
### KQL-spørringer for rate limit monitoring
```python
# Overvåk throttling i Azure Monitor
THROTTLE_MONITORING = """
AzureMetrics
| where ResourceProvider == "MICROSOFT.COGNITIVESERVICES"
| where MetricName == "AzureOpenAIRequests"
| extend StatusCode = tostring(split(DimensionValue, ",")[0])
| summarize
TotalRequests = count(),
Successful = countif(StatusCode == "200"),
Throttled = countif(StatusCode == "429"),
ServerErrors = countif(StatusCode startswith "5")
by bin(TimeGenerated, 5m), Resource
| extend
ThrottleRate = round(Throttled * 100.0 / TotalRequests, 2),
ErrorRate = round(ServerErrors * 100.0 / TotalRequests, 2)
| where ThrottleRate > 0 or ErrorRate > 0
| order by TimeGenerated desc
"""
# Alert: Varsle når throttle rate overstiger terskel
THROTTLE_ALERT = """
AzureMetrics
| where MetricName == "AzureOpenAIRequests"
| extend StatusCode = tostring(split(DimensionValue, ",")[0])
| summarize
Total = count(),
Throttled = countif(StatusCode == "429")
by bin(TimeGenerated, 5m)
| extend ThrottleRate = Throttled * 100.0 / Total
| where ThrottleRate > 10
"""
# Quota utilization trend
QUOTA_UTILIZATION = """
AzureMetrics
| where MetricName in ("ProcessedPromptTokens", "GeneratedCompletionTokens")
| summarize
PromptTPM = sumif(Total, MetricName == "ProcessedPromptTokens"),
CompletionTPM = sumif(Total, MetricName == "GeneratedCompletionTokens")
by bin(TimeGenerated, 1m)
| extend TotalTPM = PromptTPM + CompletionTPM
| order by TimeGenerated desc
"""
```
## Norsk offentlig sektor
- **SLA-implikasjoner**: Standard Azure OpenAI deployments har ingen latens-SLA — 429-feil er forventet atferd under høy belastning. Dokumenter dette i tjenesteavtaler med interne brukere.
- **Kvoteplanlegging**: Statlige organisasjoner bør planlegge TPM-kvote basert på forventet bruksmønster med 30-50% margin. Kvoteøkninger kan ta tid å behandle.
- **Multi-region compliance**: Ved failover til andre regioner, sørg for at databehandleravtale dekker alle regioner. For sensitivt innhold, bruk kun EU-baserte regioner.
- **Overvåking**: Sett opp Azure Monitor-alerts for throttle rate > 5% og utilization > 80% for proaktiv kvotejustering.
- **Beredskap**: Ha en eskaleringsplan for kvoteøkninger som inkluderer kontaktinformasjon for Microsoft-support.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Sporadisk throttling (<5%) | Innebygd SDK retry | Tilstrekkelig for lav frekvens |
| Hyppig throttling (5-20%) | Øk kvote + multi-region failover | Kvoten er for lav for trafikken |
| Kritisk tjeneste, null toleranse | PTU deployment | Garantert kapasitet |
| Variabel trafikk med peaks | APIM med token rate limiting | Jevner ut trafikkmønstre |
| Multi-tenant applikasjon | Per-tenant rate limiting i APIM | Fair share mellom brukere |
## Referanser
- [Manage Azure OpenAI quota](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/quota) — Kvotehåndtering
- [Azure OpenAI quotas and limits](https://learn.microsoft.com/azure/ai-foundry/openai/quotas-limits) — Grenser per modell
- [Azure OpenAI SDK retry handling](https://learn.microsoft.com/azure/ai-foundry/openai/supported-languages) — SDK retry-konfigurasjon
- [Use a gateway in front of Azure OpenAI](https://learn.microsoft.com/azure/architecture/ai-ml/guide/azure-openai-gateway-multi-backend) — Multi-region gateway
## For Cosmo
- **Bruk denne referansen** når kunden opplever 429-feil, planlegger kvotestrategi, eller designer multi-region failover for Azure OpenAI.
- Alltid sjekk og respekter `Retry-After` headeren — SDK-ene gjør dette automatisk, men custom-klienter må implementere det.
- Multi-region failover er den mest robuste løsningen: prioriter Norway East → Sweden Central → West Europe for norske kunder.
- PTU eliminerer rate limiting helt (innenfor tildelt kapasitet) — anbefal for forretningskritiske workloads.
- Proaktiv kvotemonitorering er billigere enn reaktiv feilhåndtering — sett opp alerts FØR throttling oppstår.

View file

@ -0,0 +1,342 @@
# Regional Deployment for Latency Reduction
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Multi-region deployment av Azure OpenAI-tjenester er en strategi for å minimere latens, øke tilgjengelighet og oppfylle krav til dataresidency. Azure OpenAI tilbyr flere deployment-typer som adresserer ulike regionale behov: Global Standard (automatisk routing til region med tilgjengelig kapasitet), Data Zone (data holdes innenfor en geografisk sone som EU), Regional Standard (fast region) og tilsvarende Provisioned-varianter.
For norsk offentlig sektor er regionvalg spesielt viktig på grunn av Schrems II, Personopplysningsloven og krav fra sektorregulering. Azure Norway East er den foretrukne primærregionen, med Sweden Central som sekundær. Azure Front Door og Azure API Management kan brukes som global router foran multiple Azure OpenAI-instanser for å oppnå latens-basert routing med automatisk failover.
Latensforskjellen mellom regioner kan være betydelig: en forespørsel fra Oslo til Norway East har typisk 2-5ms nettverkslatens, mens samme forespørsel til East US legger til 80-120ms. For interaktive AI-applikasjoner der brukeropplevelsen avhenger av time-to-first-token (TTFT), er nær region-plassering en viktig optimaliseringsfaktor.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Azure Front Door | Global load balancing med latens-basert routing | Azure Front Door |
| Azure Traffic Manager | DNS-basert trafikk-routing | Azure Traffic Manager |
| Azure API Management (multi-region) | Gateway med regionalt distribuerte gateways | Azure APIM |
| Private Link | Privat nettverkstilgang til Azure OpenAI | Azure Private Link |
| Azure OpenAI Deployment Types | Global, Data Zone, Regional | Azure OpenAI |
## Region Selection Criteria
### Deployment-typer og regionvalg
| Deployment Type | Data Location | Routing | Bruksområde |
|----------------|---------------|---------|-------------|
| Global Standard | Any Azure region | Automatisk til ledig kapasitet | Høyest tilgjengelighet, lavest kostnad |
| Data Zone Standard | Innenfor geografisk sone (EU/US) | Automatisk innen sone | EU data residency |
| Regional Standard | Fast spesifisert region | Ingen routing | Full kontroll over plassering |
| Global Provisioned | Any Azure region | Automatisk | PTU med global routing |
| Data Zone Provisioned | Innenfor sone | Automatisk innen sone | PTU med data residency |
| Regional Provisioned | Fast region | Ingen | PTU med full regionkontroll |
### Regionsvalg for norsk offentlig sektor
```python
# Regionsprioriteringer for norske offentlige virksomheter
REGION_PRIORITIES = {
"tier_1_preferred": {
"regions": ["norwayeast"],
"rationale": "Primær: Norsk region, lavest latens, data i Norge",
"data_residency": "Norway",
"network_latency_from_oslo_ms": 2
},
"tier_2_fallback": {
"regions": ["swedencentral"],
"rationale": "Sekundær: Nær region, EU data residency",
"data_residency": "EU/EEA",
"network_latency_from_oslo_ms": 8
},
"tier_3_extended": {
"regions": ["westeurope", "northeurope"],
"rationale": "Tertiær: EU-regioner for høy tilgjengelighet",
"data_residency": "EU/EEA",
"network_latency_from_oslo_ms": 25
},
"avoid_for_sensitive": {
"regions": ["eastus", "eastus2", "westus"],
"rationale": "Unngå for personopplysninger — utenfor EU/EØS",
"data_residency": "US",
"network_latency_from_oslo_ms": 90
}
}
def select_regions_for_workload(
data_classification: str, # "public", "internal", "confidential"
latency_requirement_ms: float = 100,
availability_requirement: float = 99.9
) -> list[dict]:
"""Select appropriate regions based on requirements."""
if data_classification == "confidential":
return [REGION_PRIORITIES["tier_1_preferred"]]
elif data_classification == "internal":
regions = [
REGION_PRIORITIES["tier_1_preferred"],
REGION_PRIORITIES["tier_2_fallback"]
]
if availability_requirement > 99.9:
regions.append(REGION_PRIORITIES["tier_3_extended"])
return regions
else: # public
return [
REGION_PRIORITIES["tier_1_preferred"],
REGION_PRIORITIES["tier_2_fallback"],
REGION_PRIORITIES["tier_3_extended"]
]
```
## Traffic Routing Strategies
### Azure API Management multi-region
```xml
<!-- APIM Policy: Latens-basert routing til Azure OpenAI backends -->
<policies>
<inbound>
<base />
<!-- Definer backend-pool med prioritet -->
<set-variable name="backends" value="@{
var backends = new JArray();
backends.Add(new JObject(
new JProperty("url",
"https://aoai-norway.openai.azure.com"),
new JProperty("priority", 1),
new JProperty("region", "norwayeast")));
backends.Add(new JObject(
new JProperty("url",
"https://aoai-sweden.openai.azure.com"),
new JProperty("priority", 2),
new JProperty("region", "swedencentral")));
backends.Add(new JObject(
new JProperty("url",
"https://aoai-westeu.openai.azure.com"),
new JProperty("priority", 3),
new JProperty("region", "westeurope")));
return backends.ToString();
}" />
<!-- Route til region basert på APIM gateway location -->
<set-backend-service
base-url="@{
var region = context.Deployment.Region;
if (region.Contains("norway"))
return "https://aoai-norway.openai.azure.com";
if (region.Contains("sweden"))
return "https://aoai-sweden.openai.azure.com";
return "https://aoai-westeu.openai.azure.com";
}" />
</inbound>
<backend>
<!-- Retry til neste region ved feil -->
<retry condition="@(
context.Response.StatusCode == 429 ||
context.Response.StatusCode >= 500)"
count="2"
interval="0"
first-fast-retry="true">
<choose>
<when condition="@(
context.Response.StatusCode == 429)">
<!-- Bytt til neste region -->
<set-backend-service
base-url="@{
// Roter til neste backend i prioritet
return context.Variables
.GetValueOrDefault<string>(
"fallback-url",
"https://aoai-sweden.openai.azure.com");
}" />
</when>
</choose>
<forward-request />
</retry>
</backend>
</policies>
```
### Azure Front Door konfigurasjon
```bash
# Opprett Azure Front Door med latens-basert routing til OpenAI
# 1. Opprett Front Door profil
az afd profile create \
--resource-group rg-ai-networking \
--profile-name fd-ai-gateway \
--sku Premium_AzureFrontDoor
# 2. Opprett endpoint
az afd endpoint create \
--resource-group rg-ai-networking \
--profile-name fd-ai-gateway \
--endpoint-name ai-openai \
--enabled-state Enabled
# 3. Opprett origin group med latens-basert routing
az afd origin-group create \
--resource-group rg-ai-networking \
--profile-name fd-ai-gateway \
--origin-group-name aoai-backends \
--probe-request-type GET \
--probe-protocol Https \
--probe-path "/openai/deployments?api-version=2024-10-21" \
--probe-interval-in-seconds 30 \
--sample-size 4 \
--successful-samples-required 3 \
--additional-latency-in-milliseconds 50
# 4. Legg til origins (Azure OpenAI instanser)
az afd origin create \
--resource-group rg-ai-networking \
--profile-name fd-ai-gateway \
--origin-group-name aoai-backends \
--origin-name aoai-norway \
--host-name aoai-norway.openai.azure.com \
--origin-host-header aoai-norway.openai.azure.com \
--priority 1 \
--weight 1000 \
--enabled-state Enabled \
--https-port 443
az afd origin create \
--resource-group rg-ai-networking \
--profile-name fd-ai-gateway \
--origin-group-name aoai-backends \
--origin-name aoai-sweden \
--host-name aoai-sweden.openai.azure.com \
--origin-host-header aoai-sweden.openai.azure.com \
--priority 2 \
--weight 500 \
--enabled-state Enabled \
--https-port 443
```
## Cross-Region Redundancy
### Active-active deployment pattern
```python
# Multi-region health check og failover
from dataclasses import dataclass
import aiohttp
import asyncio
@dataclass
class RegionHealth:
region: str
endpoint: str
is_healthy: bool
latency_ms: float
last_check: float
class MultiRegionHealthChecker:
"""Monitor health across Azure OpenAI regions."""
def __init__(self, regions: list[dict], check_interval: int = 30):
self.regions = regions
self.check_interval = check_interval
self.health: dict[str, RegionHealth] = {}
async def check_all(self):
"""Check health of all regions."""
tasks = [
self._check_region(r["region"], r["endpoint"], r["api_key"])
for r in self.regions
]
await asyncio.gather(*tasks)
async def _check_region(self, region: str, endpoint: str, api_key: str):
start = time.time()
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{endpoint}/openai/deployments"
f"?api-version=2024-10-21",
headers={"api-key": api_key},
timeout=aiohttp.ClientTimeout(total=10)
) as resp:
latency = (time.time() - start) * 1000
self.health[region] = RegionHealth(
region=region,
endpoint=endpoint,
is_healthy=resp.status < 400,
latency_ms=round(latency, 1),
last_check=time.time()
)
except Exception:
self.health[region] = RegionHealth(
region=region,
endpoint=endpoint,
is_healthy=False,
latency_ms=9999,
last_check=time.time()
)
def get_best_region(self) -> str:
"""Get the healthiest, lowest-latency region."""
healthy = [
h for h in self.health.values()
if h.is_healthy
]
if not healthy:
return self.regions[0]["region"]
return min(healthy, key=lambda h: h.latency_ms).region
```
## Data Residency Requirements
### EU/EØS data residency-matrise
| Krav | Global Standard | Data Zone (EU) | Regional (Norway East) |
|------|----------------|----------------|----------------------|
| Data prosesseres i EU | Nei (global) | Ja | Ja |
| Data lagres i Norge | Nei | Nei (EU) | Ja |
| Schrems II-kompatibel | Delvis | Ja | Ja |
| Personopplysninger OK | Avhenger av DPA | Ja med DPA | Ja med DPA |
| Gradert informasjon | Nei | Nei | Avhenger av sertifisering |
| Metadata i EU | Nei | Ja | Ja |
## Norsk offentlig sektor
- **Primær region**: Norway East for alle workloads med personopplysninger. Sweden Central som failover.
- **Data Zone**: Bruk Data Zone deployments (Standard eller Provisioned) for automatisk EU-routing med data residency-garanti.
- **Private Link**: Konfigurer Private Endpoints for Azure OpenAI i hver region for å unngå at data traverserer offentlig internett.
- **Utredningsinstruksen**: Dokumenter regionvalg og data residency-implikasjoner i AI-utredningen.
- **Anskaffelsesreglement**: Ved bruk av Global deployments, verifiser at Microsoft DPA dekker alle regioner data kan prosesseres i.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Lav latens, norske brukere | Regional Norway East | 2ms nettverkslatens |
| EU data residency krav | Data Zone EU | Automatisk routing innen EU |
| Høy tilgjengelighet (99.99%) | Multi-region med Front Door | Overlevere regional outage |
| Sensitive personopplysninger | Regional Norway East, Private Link | Full kontroll, ingen global routing |
| Global brukerbase | Global Standard | Automatisk latens-optimalisering |
| PTU med failover | Data Zone Provisioned + Standard fallback | PTU for normal, Standard for peak |
## Referanser
- [Use a gateway for multi-backend Azure OpenAI](https://learn.microsoft.com/azure/architecture/ai-ml/guide/azure-openai-gateway-multi-backend) — Multi-region patterns
- [Azure Front Door](https://learn.microsoft.com/azure/frontdoor/front-door-overview) — Global load balancing
- [APIM multi-region deployment](https://learn.microsoft.com/azure/api-management/api-management-howto-deploy-multi-region) — Regional gateway
- [Azure OpenAI deployment types](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/deployment-types) — Global vs Regional
- [AI Ready — Establish AI reliability](https://learn.microsoft.com/azure/cloud-adoption-framework/scenarios/ai/ready) — Multi-region best practices
## For Cosmo
- **Bruk denne referansen** når kunden trenger å velge Azure-region for Azure OpenAI, designer multi-region arkitektur, eller har krav til data residency.
- For norsk offentlig sektor: start med Regional Norway East + Data Zone EU failover — dette dekker de fleste krav.
- Azure API Management multi-region gir den mest fleksible løsningen med policy-basert routing og circuit breaker — anbefal dette for enterprise.
- Latensforskjellen mellom Norway East (2ms) og East US (90ms) er merkbar for interaktive applikasjoner — regionvalg påvirker brukeropplevelsen direkte.
- Private Link er obligatorisk for sensitive workloads — sørg for at Private Endpoints konfigureres i ALLE regioner som brukes.

View file

@ -0,0 +1,478 @@
# Response Chunking Strategies
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Response chunking handler om hvordan store AI-modellresponser fra Azure OpenAI brytes opp og leveres til klienter. Det finnes to hovedtilnærminger: streaming via Server-Sent Events (SSE) der modellens output leveres token-for-token i sanntid, og chunking av store responser der output deles opp i semantisk meningsfulle blokker for videre prosessering.
Streaming er den mest brukte chunking-strategien for Azure OpenAI. Når `stream: true` settes i API-kallet, returnerer tjenesten delta-oppdateringer som Server-Sent Events ettersom tokens genereres. Dette gir brukeren umiddelbar feedback (time-to-first-token typisk 200-500ms) i stedet for å vente på hele responsen (som kan ta 5-30 sekunder for lange output). For programmatisk prosessering der hele responsen trengs, er chunking av det endelige resultatet i semantisk koherente blokker viktig for downstream-systemer.
For norsk offentlig sektor der AI brukes til å generere lange dokumenter (saksframlegg, utredninger, rapporter), er response chunking avgjørende for å levere god brukeropplevelse og for å kunne prosessere store responser effektivt i saksbehandlingssystemer.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Server-Sent Events (SSE) | Real-time streaming av tokens | HTTP SSE |
| stream_options | Konfigurer streaming-oppførsel | Azure OpenAI API |
| Application Gateway | SSE proxy og load balancing | Azure App Gateway |
| API Management | SSE-støtte med policy-basert routing | Azure APIM |
| SignalR | Real-time push til web-klienter | Azure SignalR |
## Streaming med Server-Sent Events
### Python streaming-implementasjon
```python
from openai import AzureOpenAI
import sys
client = AzureOpenAI(
azure_endpoint="https://my-aoai.openai.azure.com",
api_key="...",
api_version="2024-10-21"
)
def stream_chat_completion(messages: list[dict], model: str = "gpt-4o"):
"""Stream response with real-time token delivery."""
collected_content = []
stream = client.chat.completions.create(
model=model,
messages=messages,
stream=True,
stream_options={"include_usage": True}, # Få token-bruk til slutt
max_tokens=2000
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
token = chunk.choices[0].delta.content
collected_content.append(token)
sys.stdout.write(token)
sys.stdout.flush()
# Siste chunk inneholder usage
if hasattr(chunk, 'usage') and chunk.usage:
return {
"content": "".join(collected_content),
"prompt_tokens": chunk.usage.prompt_tokens,
"completion_tokens": chunk.usage.completion_tokens,
"total_tokens": chunk.usage.total_tokens
}
return {"content": "".join(collected_content)}
# Asynkron streaming
async def async_stream_completion(
client: AsyncAzureOpenAI,
messages: list[dict],
model: str = "gpt-4o",
on_token: callable = None
):
"""Async stream with callback per token."""
chunks = []
async with client.chat.completions.create(
model=model,
messages=messages,
stream=True,
stream_options={"include_usage": True}
) as stream:
async for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
token = chunk.choices[0].delta.content
chunks.append(token)
if on_token:
await on_token(token)
return "".join(chunks)
```
### .NET streaming med IAsyncEnumerable
```csharp
using Azure.AI.OpenAI;
using OpenAI.Chat;
public class StreamingService
{
private readonly AzureOpenAIClient _client;
public async IAsyncEnumerable<string> StreamCompletionAsync(
string deploymentName,
IList<ChatMessage> messages,
int maxTokens = 2000)
{
var chatClient = _client.GetChatClient(deploymentName);
var options = new ChatCompletionOptions
{
MaxOutputTokenCount = maxTokens
};
// Stream deltas
await foreach (var update in
chatClient.CompleteChatStreamingAsync(messages, options))
{
foreach (var part in update.ContentUpdate)
{
if (!string.IsNullOrEmpty(part.Text))
{
yield return part.Text;
}
}
}
}
// Bruk i ASP.NET controller
public async Task StreamToClient(
HttpContext context,
string deploymentName,
IList<ChatMessage> messages)
{
context.Response.ContentType = "text/event-stream";
context.Response.Headers.Append("Cache-Control", "no-cache");
context.Response.Headers.Append("Connection", "keep-alive");
var writer = new StreamWriter(context.Response.Body);
await foreach (var token in StreamCompletionAsync(
deploymentName, messages))
{
await writer.WriteAsync($"data: {token}\n\n");
await writer.FlushAsync();
}
await writer.WriteAsync("data: [DONE]\n\n");
await writer.FlushAsync();
}
}
```
## Semantic Chunking Approaches
### Chunk store responser i meningsfulle blokker
```python
import re
from dataclasses import dataclass
@dataclass
class SemanticChunk:
index: int
content: str
chunk_type: str # "heading", "paragraph", "code", "list", "table"
token_count: int
def semantic_chunk_response(
response_text: str,
max_chunk_tokens: int = 500,
model: str = "gpt-4o"
) -> list[SemanticChunk]:
"""Split AI response into semantically coherent chunks."""
import tiktoken
enc = tiktoken.encoding_for_model(model)
chunks = []
current_chunk = []
current_tokens = 0
chunk_type = "paragraph"
# Del på naturlige grenser
lines = response_text.split('\n')
for line in lines:
line_tokens = len(enc.encode(line))
# Identifiser chunk-type
if line.startswith('#'):
chunk_type = "heading"
elif line.startswith('```'):
chunk_type = "code"
elif line.startswith('- ') or line.startswith('* '):
chunk_type = "list"
elif line.startswith('|'):
chunk_type = "table"
else:
chunk_type = "paragraph"
# Ny chunk ved heading eller ved token-grense
if (line.startswith('#') and current_chunk) or \
(current_tokens + line_tokens > max_chunk_tokens and current_chunk):
chunks.append(SemanticChunk(
index=len(chunks),
content='\n'.join(current_chunk),
chunk_type=chunk_type,
token_count=current_tokens
))
current_chunk = []
current_tokens = 0
current_chunk.append(line)
current_tokens += line_tokens
# Siste chunk
if current_chunk:
chunks.append(SemanticChunk(
index=len(chunks),
content='\n'.join(current_chunk),
chunk_type=chunk_type,
token_count=current_tokens
))
return chunks
```
### Streaming accumulator med chunk-deteksjon
```python
class StreamingChunkAccumulator:
"""Accumulate streaming tokens into semantic chunks."""
def __init__(
self,
on_chunk_complete: callable = None,
chunk_boundary_pattern: str = r'\n#{1,3}\s'
):
self.buffer = []
self.chunks = []
self.on_chunk_complete = on_chunk_complete
self.boundary_pattern = re.compile(chunk_boundary_pattern)
async def feed_token(self, token: str):
"""Feed a streaming token to the accumulator."""
self.buffer.append(token)
# Sjekk om vi har nådd en chunk-grense
current_text = ''.join(self.buffer)
if self.boundary_pattern.search(current_text):
# Del på grensen
parts = self.boundary_pattern.split(current_text, maxsplit=1)
if len(parts) > 1:
completed = parts[0]
remaining = current_text[len(completed):]
if completed.strip():
chunk = SemanticChunk(
index=len(self.chunks),
content=completed.strip(),
chunk_type=self._detect_type(completed),
token_count=len(completed.split()) # Estimat
)
self.chunks.append(chunk)
if self.on_chunk_complete:
await self.on_chunk_complete(chunk)
self.buffer = [remaining]
def finalize(self) -> list[SemanticChunk]:
"""Finalize and return all chunks."""
remaining = ''.join(self.buffer).strip()
if remaining:
self.chunks.append(SemanticChunk(
index=len(self.chunks),
content=remaining,
chunk_type=self._detect_type(remaining),
token_count=len(remaining.split())
))
return self.chunks
def _detect_type(self, text: str) -> str:
if text.startswith('```'):
return "code"
if text.startswith('#'):
return "heading"
if text.startswith('- ') or text.startswith('* '):
return "list"
return "paragraph"
```
## Client-Side Reassembly
### Web-klient med progressiv rendering
```typescript
// TypeScript: Client-side SSE consumption with chunk assembly
interface StreamChunk {
content: string;
isComplete: boolean;
tokenCount: number;
}
class AIResponseAssembler {
private chunks: string[] = [];
private onUpdate: (text: string) => void;
private onComplete: (text: string, stats: object) => void;
constructor(
onUpdate: (text: string) => void,
onComplete: (text: string, stats: object) => void
) {
this.onUpdate = onUpdate;
this.onComplete = onComplete;
}
async streamFromEndpoint(url: string, body: object): Promise<void> {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...body, stream: true }),
});
if (!response.body) throw new Error('No response body');
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
let fullText = '';
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
this.onComplete(fullText, {
totalChunks: this.chunks.length,
totalLength: fullText.length
});
return;
}
try {
const parsed = JSON.parse(data);
const token = parsed.choices?.[0]?.delta?.content || '';
if (token) {
fullText += token;
this.chunks.push(token);
this.onUpdate(fullText);
}
} catch { /* skip malformed */ }
}
}
}
}
}
```
## Error Handling in Chunks
### Robust feilhåndtering for streaming
```python
class ResilientStreamProcessor:
"""Handle errors during streaming response."""
def __init__(self, client: AsyncAzureOpenAI, max_retries: int = 3):
self.client = client
self.max_retries = max_retries
async def stream_with_recovery(
self,
messages: list[dict],
model: str = "gpt-4o",
max_tokens: int = 2000
) -> dict:
"""Stream with automatic recovery on failure."""
accumulated = []
total_tokens_generated = 0
for attempt in range(self.max_retries):
try:
stream = await self.client.chat.completions.create(
model=model,
messages=messages,
stream=True,
max_tokens=max_tokens - total_tokens_generated
)
async for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
token = chunk.choices[0].delta.content
accumulated.append(token)
total_tokens_generated += 1
# Sjekk for finish_reason
if chunk.choices and chunk.choices[0].finish_reason:
return {
"content": "".join(accumulated),
"finish_reason": chunk.choices[0].finish_reason,
"attempts": attempt + 1,
"recovered": attempt > 0
}
# Stream fullført uten finish_reason
return {
"content": "".join(accumulated),
"finish_reason": "stop",
"attempts": attempt + 1,
"recovered": attempt > 0
}
except Exception as e:
if attempt < self.max_retries - 1:
# Fortsett fra der vi stoppet
partial = "".join(accumulated)
if partial:
# Legg til partial output som assistant-melding
messages = messages + [
{"role": "assistant", "content": partial},
{"role": "user", "content": "Fortsett fra der du stoppet."}
]
await asyncio.sleep(2 ** attempt)
else:
return {
"content": "".join(accumulated),
"finish_reason": "error",
"error": str(e),
"attempts": attempt + 1
}
```
## Norsk offentlig sektor
- **Universell utforming**: Streaming gir bedre brukeropplevelse for skjermlesere og sakte nettverk — bruker ser innhold progressivt i stedet for å vente.
- **Saksbehandlingssystemer**: Chunk store AI-responser i semantiske blokker (overskrifter, avsnitt, tabeller) for enkel integrasjon i saksbehandlingsdokumenter.
- **Logging og audit**: Ved streaming, logg den komplette responsen etter fullføring for arkiverings- og revisjonskrav.
- **Application Gateway**: Konfigurer response buffer disabled for SSE-støtte gjennom Azure Application Gateway eller API Management.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Interaktiv chat UI | SSE streaming | Umiddelbar bruker-feedback |
| Batch dokumentprosessering | Ikke-streaming + semantic chunking | Enklere feilhåndtering |
| API-til-API integrasjon | Ikke-streaming | Enklere å parse komplett respons |
| Lang respons (>2000 tokens) | Streaming + chunk accumulator | Reduser opplevd ventetid |
| Kritisk pålitelighet | Streaming med recovery | Gjenoppta ved feil |
## Referanser
- [Azure OpenAI streaming](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/responses) — Streaming API
- [Server-Sent Events with Application Gateway](https://learn.microsoft.com/azure/application-gateway/use-server-sent-events) — SSE proxy
- [API Management SSE configuration](https://learn.microsoft.com/azure/api-management/how-to-server-sent-events) — APIM SSE
- [Server-Sent Events with App Gateway for Containers](https://learn.microsoft.com/azure/application-gateway/for-containers/server-sent-events) — Container SSE
## For Cosmo
- **Bruk denne referansen** når kunden implementerer streaming i AI-applikasjoner, trenger å chunke store responser, eller har feilhåndteringsproblemer med SSE.
- Streaming er alltid anbefalt for brukervendte applikasjoner — time-to-first-token reduseres fra sekunder til millisekunder.
- Konfigurer `stream_options: { include_usage: true }` for å få token-bruk i siste chunk — uten dette mangler kostnadssporing.
- Ved bruk av Application Gateway eller API Management som proxy: deaktiver response buffering for SSE-kompatibilitet.
- Implementer alltid recovery-logikk for streaming — nettverksavbrudd er uunngåelig i produksjon, og delvis generert output bør gjenbrukes.

View file

@ -0,0 +1,634 @@
# Streaming Response Patterns
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Streaming av AI-responser er en kritisk teknikk for a forbedre brukeropplevelsen i interaktive AI-applikasjoner. Istedenfor a vente pa at hele responsen genereres for den vises, lar streaming brukeren se svaret bygges opp token for token. For norsk offentlig sektor, der innbyggerportaler og saksbehandlingssystemer i okende grad integrerer AI, er streaming avgjorende for akseptabel responstid.
Azure OpenAI stotter streaming gjennom Server-Sent Events (SSE)-protokollen, som er en enkel, unidireksjonell strommingsmekanisme over HTTP. Denne tilnaermingen er spesielt verdifull for chat-grensesnitt, dokumentgenerering og andre bruksomrader der brukeren forventer umiddelbar tilbakemelding.
Denne referansen dekker arkitekturmonstre for streaming i Azure OpenAI-baserte applikasjoner, fra grunnleggende SSE-implementasjon til avansert feilhandtering og mellomlag-konfigurasjon.
## Server-Sent Events (SSE) Grunnleggende
### Hva er SSE?
Server-Sent Events er en W3C-standard for enveis stromming fra server til klient over HTTP:
| Egenskap | SSE | WebSocket | Long Polling |
|----------|-----|-----------|--------------|
| Retning | Server -> Klient | Bidireksjonell | Klient -> Server -> Klient |
| Protokoll | HTTP/1.1+ | WebSocket (ws://) | HTTP |
| Automatisk reconnect | Ja (innebygd) | Nei (manuell) | Nei |
| Kompleksitet | Lav | Hoy | Middels |
| Azure OpenAI-stotte | Ja | Ja (Realtime API) | Nei |
### SSE-format
Azure OpenAI returnerer data i SSE-format:
```
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Transfer-Encoding: chunked
Cache-Control: no-cache
Connection: keep-alive
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"Hei"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1694268190,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
```
**Viktige SSE-regler:**
- Hver hendelse er prefixet med `data: `
- Hendelser separeres med to linjeskift (`\n\n`)
- Siste hendelse er alltid `data: [DONE]`
- `delta`-feltet inneholder inkrementelt innhold (ikke kumulativt)
- `finish_reason` er `null` til generering er ferdig
## Grunnleggende Streaming-implementasjon
### Python med Azure OpenAI SDK
```python
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
def stream_chat_response(user_message: str) -> str:
"""Stream en chat completion og bygg opp komplett respons."""
full_response = ""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Du er en hjelpesom assistent."},
{"role": "user", "content": user_message}
],
stream=True,
max_tokens=500
)
for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_response += content
print(content, end="", flush=True) # Vis inkrementelt
print() # Ny linje etter ferdig streaming
return full_response
```
### Async Python Streaming
```python
from openai import AsyncAzureOpenAI
import asyncio
async_client = AsyncAzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
async def stream_async(user_message: str):
"""Asynkron streaming for hoy-throughput applikasjoner."""
response = await async_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": user_message}],
stream=True,
max_tokens=500
)
collected_content = []
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
collected_content.append(content)
yield content # Yield for videre prosessering
return "".join(collected_content)
```
### TypeScript/JavaScript Streaming
```typescript
import { AzureOpenAI } from "openai";
const client = new AzureOpenAI({
endpoint: "https://your-resource.openai.azure.com/",
apiKey: "your-api-key",
apiVersion: "2025-03-01-preview",
});
async function* streamChatResponse(
userMessage: string
): AsyncGenerator<string> {
const stream = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: userMessage }],
stream: true,
max_tokens: 500,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
yield content;
}
}
}
// Bruk i en web-handler
async function handleStreamRequest(req: Request): Promise<Response> {
const encoder = new TextEncoder();
const readableStream = new ReadableStream({
async start(controller) {
for await (const token of streamChatResponse("Hva er GDPR?")) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ content: token })}\n\n`));
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
return new Response(readableStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
}
```
## Chunked Transfer Encoding
### HTTP-konfigurasjon for streaming
For at streaming skal fungere gjennom hele infrastrukturen, ma alle mellomlag konfigureres korrekt:
| Komponent | Nodvendig konfigurasjon |
|-----------|------------------------|
| Azure OpenAI | `stream: true` i request |
| API Management | `buffer-response="false"` i forward-request |
| Application Gateway | Deaktiver response buffering |
| Azure Front Door | Route-spesifikk konfigurasjon |
| Klient (browser) | `Accept: text/event-stream` header |
### API Management for SSE
```xml
<!-- APIM policy for SSE pass-through -->
<policies>
<inbound>
<base />
</inbound>
<backend>
<!-- KRITISK: buffer-response="false" for streaming -->
<forward-request timeout="120"
fail-on-error-status-code="true"
buffer-response="false" />
</backend>
<outbound>
<base />
<!-- VIKTIG: Deaktiver body-logging for SSE-APIer -->
</outbound>
<on-error>
<base />
</on-error>
</policies>
```
**Viktige APIM-hensyn for SSE:**
1. Deaktiver response buffering (`buffer-response="false"`)
2. Deaktiver `validate-content`-policy (buffrer respons)
3. Deaktiver request/response body-logging for Azure Monitor og Application Insights
4. Deaktiver response caching for streaming-endepunkter
5. Okt timeout (minimum 120 sekunder)
6. Hold forbindelser i live med TCP keepalive
### Application Gateway for SSE
```json
{
"properties": {
"responseBufferPolicy": {
"responseSendTimeoutInSeconds": 120,
"bufferResponseBody": false
},
"backendHttpSettings": {
"requestTimeout": 120,
"connectionDraining": {
"enabled": true,
"drainTimeoutInSec": 30
}
}
}
}
```
### Azure Front Door Route Policy
For SSE gjennom Azure Front Door:
```json
{
"routePolicy": {
"routeTimeout": "0s"
}
}
```
**Merk:** Idle timeout for Application Gateway for Containers er 5 minutter. Send keepalive-meldinger for a forhindre at forbindelsen lukkes:
```
: keep-alive\n\n
```
## Client-Side Stream Handling
### React/Next.js Frontend
```typescript
// React hook for SSE streaming fra Azure OpenAI
import { useState, useCallback } from "react";
interface StreamState {
content: string;
isStreaming: boolean;
error: string | null;
}
function useAIStream() {
const [state, setState] = useState<StreamState>({
content: "",
isStreaming: false,
error: null,
});
const startStream = useCallback(async (prompt: string) => {
setState({ content: "", isStreaming: true, error: null });
try {
const response = await fetch("/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({ message: prompt }),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) throw new Error("No reader available");
let accumulated = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") continue;
try {
const parsed = JSON.parse(data);
if (parsed.content) {
accumulated += parsed.content;
setState((prev) => ({
...prev,
content: accumulated,
}));
}
} catch {
// Ignorer parsing-feil for ufullstendige chunks
}
}
}
}
setState((prev) => ({ ...prev, isStreaming: false }));
} catch (error) {
setState((prev) => ({
...prev,
isStreaming: false,
error: error instanceof Error ? error.message : "Ukjent feil",
}));
}
}, []);
return { ...state, startStream };
}
```
### Python SSE Client
```python
import httpx
import json
from typing import AsyncGenerator
async def consume_sse_stream(
url: str,
payload: dict,
api_key: str
) -> AsyncGenerator[str, None]:
"""Konsumer SSE-strom fra Azure OpenAI via HTTP."""
headers = {
"Content-Type": "application/json",
"api-key": api_key,
"Accept": "text/event-stream"
}
async with httpx.AsyncClient(timeout=120.0) as client:
async with client.stream("POST", url, json=payload, headers=headers) as response:
response.raise_for_status()
buffer = ""
async for chunk in response.aiter_text():
buffer += chunk
while "\n\n" in buffer:
event, buffer = buffer.split("\n\n", 1)
for line in event.split("\n"):
if line.startswith("data: "):
data = line[6:]
if data == "[DONE]":
return
try:
parsed = json.loads(data)
content = parsed["choices"][0]["delta"].get("content", "")
if content:
yield content
except (json.JSONDecodeError, KeyError, IndexError):
continue
```
## Error Recovery in Streams
### Haandtering av avbrutte strommer
Streaming-forbindelser kan avbrytes av flere arsaker:
| Feiltype | Arsak | Handtering |
|----------|-------|------------|
| Nettverksavbrudd | Ustabil forbindelse | Reconnect med checkpoint |
| Timeout | Idle > 4 min (Azure LB) | Keepalive-meldinger |
| 429 Rate Limit | Kapasitetsgrense | Retry med backoff |
| 500 Server Error | Midlertidig serverfeil | Retry etter pause |
| Content Filter | Innhold blokkert | Vis melding til bruker |
### Robust Streaming med Retry
```python
import asyncio
import time
from openai import AsyncAzureOpenAI, APIStatusError, APIConnectionError
async_client = AsyncAzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
async def resilient_stream(
messages: list,
max_retries: int = 3,
model: str = "gpt-4o"
) -> AsyncGenerator[str, None]:
"""Streaming med automatisk retry og feilhandtering."""
collected_tokens = []
attempt = 0
while attempt < max_retries:
try:
response = await async_client.chat.completions.create(
model=model,
messages=messages,
stream=True,
max_tokens=1000
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
token = chunk.choices[0].delta.content
collected_tokens.append(token)
yield token
# Sjekk finish_reason
if chunk.choices and chunk.choices[0].finish_reason:
reason = chunk.choices[0].finish_reason
if reason == "content_filter":
yield "\n[Innhold filtrert av sikkerhetsfilter]"
return # Ferdig
return # Stromming fullfort
except APIStatusError as e:
attempt += 1
if e.status_code == 429:
retry_after = int(e.response.headers.get("retry-after", "5"))
await asyncio.sleep(retry_after)
elif e.status_code >= 500:
await asyncio.sleep(2 ** attempt) # Eksponentiell backoff
else:
raise # Ikke-gjenforsokbar feil
except APIConnectionError:
attempt += 1
await asyncio.sleep(2 ** attempt)
raise Exception(f"Streaming feilet etter {max_retries} forsok")
```
### Streaming med Partial Response Recovery
```python
async def stream_with_checkpoint(
messages: list,
on_token: callable,
on_complete: callable,
on_error: callable
):
"""Stream med checkpoint for delvis gjenoppretting."""
partial_response = []
last_chunk_time = time.time()
try:
response = await async_client.chat.completions.create(
model="gpt-4o",
messages=messages,
stream=True,
max_tokens=1000
)
async for chunk in response:
current_time = time.time()
# Detekter unormalt lang pause mellom chunks
if current_time - last_chunk_time > 30:
# Mulig hengende forbindelse
break
last_chunk_time = current_time
if chunk.choices and chunk.choices[0].delta.content:
token = chunk.choices[0].delta.content
partial_response.append(token)
await on_token(token)
if chunk.choices and chunk.choices[0].finish_reason == "stop":
await on_complete("".join(partial_response))
return
# Hvis vi nar hit uten "stop", har streamingen avbrultt
if partial_response:
await on_complete(
"".join(partial_response) +
"\n\n[Merk: Respons kan vaere ufullstendig]"
)
except Exception as e:
if partial_response:
await on_error(e, "".join(partial_response))
else:
await on_error(e, None)
```
## Nar bruke streaming vs. non-streaming
| Scenario | Anbefaling | Begrunnelse |
|----------|-----------|-------------|
| Chat-grensesnitt | Streaming | Bedre opplevd responstid |
| Innbyggerportal | Streaming | Visuell tilbakemelding under generering |
| Batch-klassifisering | Non-streaming | Kun sluttresultat er relevant |
| Dokumentanalyse | Non-streaming | Strukturert output, ingen inkrementell visning |
| Saksbehandlingsforslag | Streaming | Lang generering, bruker venter |
| API-integrasjon (maskin-til-maskin) | Non-streaming | Enklere feilhandtering |
| Sanntidsoversetning | Streaming | Lavest opplevd latens |
## Avanserte monstre
### Server-side Streaming Proxy med FastAPI
```python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import AsyncAzureOpenAI
app = FastAPI()
client = AsyncAzureOpenAI(
azure_endpoint="https://your-resource.openai.azure.com/",
api_key="your-api-key",
api_version="2025-03-01-preview"
)
@app.post("/api/chat/stream")
async def chat_stream(request: ChatRequest):
"""Server-side proxy for Azure OpenAI streaming."""
async def generate():
try:
response = await client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": request.message}],
stream=True,
max_tokens=1000
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
data = {"content": chunk.choices[0].delta.content}
yield f"data: {json.dumps(data)}\n\n"
yield "data: [DONE]\n\n"
except Exception as e:
error_data = {"error": str(e)}
yield f"data: {json.dumps(error_data)}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # Deaktiver nginx buffering
}
)
```
### Token-telling under streaming
```python
import tiktoken
async def stream_with_token_counting(messages: list, model: str = "gpt-4o"):
"""Stream med sanntids token-telling for kostnadsovervaking."""
encoding = tiktoken.encoding_for_model(model)
output_tokens = 0
response = await async_client.chat.completions.create(
model=model,
messages=messages,
stream=True,
stream_options={"include_usage": True} # Inkluder bruksdata
)
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
output_tokens += len(encoding.encode(content))
yield content
# Sjekk usage i siste chunk
if chunk.usage:
actual_tokens = chunk.usage.completion_tokens
cached_tokens = getattr(
chunk.usage.prompt_tokens_details, 'cached_tokens', 0
)
print(f"Faktisk token-bruk: {actual_tokens}")
print(f"Cache-treff: {cached_tokens}")
```
## Ytelsesmal for streaming
| Metrikk | Mal (P95) | Kritisk terskel |
|---------|-----------|-----------------|
| Time to First Token | < 500 ms | > 2000 ms |
| Inter-token latens | < 50 ms | > 200 ms |
| Reconnect-tid | < 2 s | > 10 s |
| Stream completion rate | > 99% | < 95% |
## For Cosmo
- **Streaming er obligatorisk** for alle brukerrettede AI-grensesnitt. Forskjellen i opplevd latens er dramatisk: 200 ms TTFT vs. 3-5 sekunders ventetid for komplett respons.
- **Infrastruktur-konfigurasjon er kritisk:** Hele kjeden (APIM, App Gateway, Front Door) ma ha response buffering deaktivert. En enkelt feilkonfigurert komponent blokkerer all streaming.
- **Feilhandtering i strommer krever eget design:** Implementer alltid reconnect-logikk, partial response recovery, og eksponentiell backoff for 429/5xx-feil.
- **Content filtering pavirker streaming:** `finish_reason: content_filter` kan oppsta midt i en strom. Klient-koden ma handtere dette gracefully med en brukermelding.
- **Token-telling under streaming:** Bruk `stream_options: {"include_usage": true}` for a fa eksakt token-bruk i siste chunk, viktig for kostnadsovervaking.

View file

@ -0,0 +1,447 @@
# Throughput Optimization Strategies
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Throughput-optimalisering for Azure OpenAI og Azure AI Services handler om å maksimere antall fullførte forespørsler per sekund innenfor de tildelte kvotene. Azure OpenAI måler throughput i tokens per minutt (TPM) og forespørsler per minutt (RPM), og den reelle throughputen avhenger av en kompleks kombinasjon av input-størrelse, output-størrelse, modelltype og samtidige forespørsler.
For Standard deployments bestemmer den tildelte kvoten (TPM) en øvre grense for gjennomstrømming, men faktisk throughput kan være lavere på grunn av per-forespørsel latens. For Provisioned Throughput Units (PTU) er kapasiteten dedikert, og throughputen avhenger av workload shape — forholdet mellom input- og output-tokens. Microsofts offisielle benchmarking-verktøy (azure-openai-benchmark) er anbefalt for å måle reell throughput for spesifikke workloads.
I norsk offentlig sektor, der AI-løsninger ofte betjener tusenvis av saksbehandlere eller borgere samtidig, er throughput-optimalisering direkte knyttet til brukeropplevelse og kostnadseffektivitet. En 2x forbedring i throughput kan bety halverte Azure-kostnader for samme arbeidsmengde.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Token quota (TPM/RPM) | Rate limiting for Standard deployments | Azure OpenAI Quota |
| Provisioned Throughput Units | Dedikert kapasitet med garantert throughput | Azure OpenAI PTU |
| Batch API | 50% rabatt for asynkrone batch-jobber | Azure OpenAI Global Batch |
| Azure Load Testing | Lasttesting og throughput-måling | Azure Load Testing |
| Azure Monitor | Throughput-metrikker og overvåking | Azure Monitor |
| azure-openai-benchmark | Offisielt benchmarking-verktøy | GitHub CLI tool |
## Parallel Request Execution
### Asynkron parallellisering i Python
```python
import asyncio
import time
from openai import AsyncAzureOpenAI
from dataclasses import dataclass
@dataclass
class ThroughputResult:
total_requests: int
successful: int
failed: int
total_tokens: int
duration_seconds: float
requests_per_second: float
tokens_per_second: float
async def parallel_completions(
client: AsyncAzureOpenAI,
messages_batch: list[list[dict]],
model: str = "gpt-4o",
max_concurrent: int = 20,
max_tokens: int = 500
) -> ThroughputResult:
"""Execute chat completions in parallel with controlled concurrency."""
semaphore = asyncio.Semaphore(max_concurrent)
results = {"success": 0, "failed": 0, "tokens": 0}
async def process_one(messages: list[dict]):
async with semaphore:
try:
response = await client.chat.completions.create(
model=model,
messages=messages,
max_tokens=max_tokens
)
results["success"] += 1
results["tokens"] += response.usage.total_tokens
except Exception as e:
results["failed"] += 1
if hasattr(e, 'status_code') and e.status_code == 429:
# Retry-After: vent og prøv igjen
retry_after = getattr(e, 'retry_after', 5)
await asyncio.sleep(retry_after)
await process_one(messages) # Retry
start = time.time()
await asyncio.gather(*[process_one(m) for m in messages_batch])
duration = time.time() - start
return ThroughputResult(
total_requests=len(messages_batch),
successful=results["success"],
failed=results["failed"],
total_tokens=results["tokens"],
duration_seconds=round(duration, 2),
requests_per_second=round(results["success"] / duration, 2),
tokens_per_second=round(results["tokens"] / duration, 2)
)
# Eksempel: Prosesser 1000 forespørsler med 20 samtidige
async def main():
client = AsyncAzureOpenAI(
azure_endpoint="https://my-aoai.openai.azure.com",
api_key="...",
api_version="2024-10-21"
)
batch = [
[{"role": "user", "content": f"Oppsummer dokument {i}"}]
for i in range(1000)
]
result = await parallel_completions(client, batch, max_concurrent=20)
print(f"Throughput: {result.requests_per_second} RPS, "
f"{result.tokens_per_second} tokens/s")
```
### .NET Parallel Processing med SemaphoreSlim
```csharp
using Azure.AI.OpenAI;
using System.Collections.Concurrent;
public class ThroughputOptimizer
{
private readonly AzureOpenAIClient _client;
private readonly SemaphoreSlim _semaphore;
private readonly ConcurrentBag<RequestMetric> _metrics = new();
public ThroughputOptimizer(AzureOpenAIClient client, int maxConcurrency = 20)
{
_client = client;
_semaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);
}
public async Task<ThroughputReport> ProcessBatchAsync(
IReadOnlyList<ChatMessage[]> requests,
string deploymentName,
CancellationToken cancellationToken = default)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var tasks = requests.Select(messages =>
ProcessSingleAsync(messages, deploymentName, cancellationToken));
await Task.WhenAll(tasks);
sw.Stop();
var successful = _metrics.Where(m => m.Success).ToList();
return new ThroughputReport
{
TotalRequests = requests.Count,
Successful = successful.Count,
Failed = _metrics.Count - successful.Count,
TotalTokens = successful.Sum(m => m.TotalTokens),
DurationMs = sw.ElapsedMilliseconds,
RequestsPerSecond = Math.Round(
successful.Count / (sw.ElapsedMilliseconds / 1000.0), 2),
TokensPerSecond = Math.Round(
successful.Sum(m => m.TotalTokens) /
(sw.ElapsedMilliseconds / 1000.0), 2)
};
}
private async Task ProcessSingleAsync(
ChatMessage[] messages,
string deploymentName,
CancellationToken ct)
{
await _semaphore.WaitAsync(ct);
try
{
var chatClient = _client.GetChatClient(deploymentName);
var response = await chatClient.CompleteChatAsync(messages);
_metrics.Add(new RequestMetric
{
Success = true,
TotalTokens = response.Value.Usage.TotalTokenCount
});
}
catch (Exception)
{
_metrics.Add(new RequestMetric { Success = false });
}
finally
{
_semaphore.Release();
}
}
}
```
## Request Buffering Strategies
### Mikro-batching for høy throughput
```python
import asyncio
from collections import deque
from typing import Callable, Any
class RequestBuffer:
"""Buffer requests and flush in micro-batches for throughput optimization."""
def __init__(
self,
process_fn: Callable,
max_batch_size: int = 10,
flush_interval_ms: int = 100,
max_queue_size: int = 1000
):
self.process_fn = process_fn
self.max_batch_size = max_batch_size
self.flush_interval = flush_interval_ms / 1000
self.queue: deque = deque(maxlen=max_queue_size)
self._running = False
async def enqueue(self, request: dict) -> asyncio.Future:
"""Add request to buffer, returns future with result."""
future = asyncio.get_event_loop().create_future()
self.queue.append({"request": request, "future": future})
if len(self.queue) >= self.max_batch_size:
await self._flush()
return await future
async def _flush(self):
"""Process all buffered requests."""
batch = []
futures = []
while self.queue and len(batch) < self.max_batch_size:
item = self.queue.popleft()
batch.append(item["request"])
futures.append(item["future"])
if batch:
try:
results = await self.process_fn(batch)
for future, result in zip(futures, results):
future.set_result(result)
except Exception as e:
for future in futures:
if not future.done():
future.set_exception(e)
async def run(self):
"""Run flush loop."""
self._running = True
while self._running:
if self.queue:
await self._flush()
await asyncio.sleep(self.flush_interval)
```
## Queue Depth Tuning
### Optimal kø-dybde for Azure OpenAI
```python
import math
def calculate_optimal_queue_depth(
tpm_quota: int,
avg_input_tokens: int,
avg_output_tokens: int,
avg_latency_ms: float,
target_utilization: float = 0.85
) -> dict:
"""Calculate optimal queue depth based on quota and latency."""
# Beregn maks concurrent requests basert på quota
total_tokens_per_request = avg_input_tokens + avg_output_tokens
max_rpm = tpm_quota / total_tokens_per_request
# Maks concurrent basert på latens
requests_per_second = max_rpm / 60
avg_latency_s = avg_latency_ms / 1000
# Little's Law: L = λ * W
# L = concurrent requests, λ = arrival rate, W = service time
optimal_concurrent = requests_per_second * avg_latency_s
# Queue depth = concurrent * buffer factor
queue_depth = math.ceil(optimal_concurrent * (1 / target_utilization))
return {
"max_rpm": round(max_rpm),
"max_rps": round(requests_per_second, 2),
"optimal_concurrent": math.ceil(optimal_concurrent),
"recommended_queue_depth": queue_depth,
"theoretical_max_tps": round(
tpm_quota / 60 / total_tokens_per_request *
total_tokens_per_request, 0
)
}
# Eksempel: 240K TPM quota, typisk RAG-workload
result = calculate_optimal_queue_depth(
tpm_quota=240_000,
avg_input_tokens=2000,
avg_output_tokens=500,
avg_latency_ms=1200
)
print(result)
# {'max_rpm': 96, 'max_rps': 1.6, 'optimal_concurrent': 2,
# 'recommended_queue_depth': 3, ...}
```
## System Bottleneck Identification
### Identifisering av flaskehalser med Azure Monitor
```python
# KQL-spørringer for throughput-analyse
# 1. Token throughput per minutt
PROCESSED_TOKENS_QUERY = """
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.COGNITIVESERVICES"
| where Category == "RequestResponse"
| extend promptTokens = toint(properties_s.promptTokens)
| extend completionTokens = toint(properties_s.completionTokens)
| summarize
TotalPromptTPM = sum(promptTokens),
TotalCompletionTPM = sum(completionTokens),
TotalTPM = sum(promptTokens) + sum(completionTokens),
RequestCount = count()
by bin(TimeGenerated, 1m), deploymentName_s
| order by TimeGenerated desc
"""
# 2. Identifiser throttling-mønstre
THROTTLING_ANALYSIS = """
AzureMetrics
| where MetricName == "AzureOpenAIRequests"
| extend StatusCode = tostring(split(DimensionValue, ",")[0])
| summarize
Total = count(),
Throttled = countif(StatusCode == "429"),
ServerErrors = countif(StatusCode startswith "5"),
ThrottleRate = round(
countif(StatusCode == "429") * 100.0 / count(), 2)
by bin(TimeGenerated, 5m)
| where ThrottleRate > 0
| order by TimeGenerated desc
"""
# 3. Latens-distribusjon for å finne bottlenecks
LATENCY_PERCENTILES = """
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.COGNITIVESERVICES"
| extend DurationMs = todouble(DurationMs)
| summarize
P50 = percentile(DurationMs, 50),
P90 = percentile(DurationMs, 90),
P95 = percentile(DurationMs, 95),
P99 = percentile(DurationMs, 99),
Avg = avg(DurationMs)
by bin(TimeGenerated, 5m), deploymentName_s
| order by TimeGenerated desc
"""
```
### Bottleneck Decision Tree
```
Lav throughput?
├── Høy throttle rate (>5% 429s)?
│ ├── Ja → Øk TPM-kvote eller legg til regioner
│ └── Nei → Sjekk latens
├── Høy latens (P95 > 5s)?
│ ├── Input tokens > 4K? → Reduser prompt-størrelse
│ ├── Output tokens > 2K? → Reduser max_tokens
│ └── Lav token count? → Sjekk nettverkslatens
├── Lav concurrent requests?
│ ├── Klient-side bottleneck → Øk parallellisering
│ └── Connection pool for liten → Øk pool size
└── Utilization < 50%?
└── Under-provisjonert? → Sjekk quota allocation
```
## Implementeringsmønstre
### Batch API for ikke-tidskritisk prosessering
```python
from openai import AzureOpenAI
import json
def create_batch_file(requests: list[dict], filename: str = "batch.jsonl"):
"""Create JSONL file for Azure OpenAI Batch API."""
with open(filename, "w") as f:
for i, req in enumerate(requests):
batch_request = {
"custom_id": f"request-{i}",
"method": "POST",
"url": "/chat/completions",
"body": {
"model": "gpt-4o", # Must match deployment name
"messages": req["messages"],
"max_tokens": req.get("max_tokens", 1000)
}
}
f.write(json.dumps(batch_request) + "\n")
def submit_batch(client: AzureOpenAI, filename: str):
"""Submit batch job — 50% cost reduction, 24hr turnaround."""
# Upload file
batch_file = client.files.create(
file=open(filename, "rb"),
purpose="batch"
)
# Create batch job
batch_job = client.batches.create(
input_file_id=batch_file.id,
endpoint="/chat/completions",
completion_window="24h"
)
return batch_job
```
## Norsk offentlig sektor
- **Kostnadseffektivitet**: Bruk Batch API for alle ikke-sanntids workloads (dokumentanalyse, klassifisering, oppsummering) for å oppnå 50% kostnadsreduksjon. Dette er spesielt relevant for store etater med høyt dokumentvolum.
- **Kapasitetsplanlegging**: Start med å estimere TPM-behov basert på forventet brukermønster (antall saksbehandlere * forespørsler per time * tokens per forespørsel). Bestill PTU for forutsigbare workloads.
- **SLA-krav**: Provisioned throughput gir forutsigbar ytelse med latens-SLA (99% > N tokens/sekund per PTU). Standard deployments har ingen latens-SLA.
- **Data residency**: Global Batch behandler data i Azure OpenAI-lokasjoner globalt — bruk Data Zone Batch for å holde data innenfor EU/EØS.
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Sanntids chat (<2s respons) | Standard/PTU + streaming | Lavest brukervendt latens |
| Dokumentprosessering (1000+ docs) | Batch API | 50% kostnadsreduksjon, 24h turnaround |
| Forutsigbar høy trafikk | Provisioned Throughput (PTU) | Garantert kapasitet og latens |
| Variable workloads | Standard + auto-scale quota | Betal per bruk, fleksibel skalering |
| Multi-model pipeline | Parallell execution + queue | Maksimer samlet throughput |
## Referanser
- [Performance and latency](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/latency) — Azure OpenAI latency og throughput
- [Azure OpenAI Batch API](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/batch) — Batch processing guide
- [Provisioned throughput onboarding](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/provisioned-throughput-onboarding) — PTU sizing og kostnader
- [Azure OpenAI Benchmark Tool](https://github.com/Azure/azure-openai-benchmark) — Offisielt benchmarking-verktøy
## For Cosmo
- **Bruk denne referansen** når kunden trenger å maksimere throughput for AI-workloads, eller når de opplever at de ikke utnytter sin tildelte kvote effektivt.
- Batch API gir 50% kostnadsreduksjon og bør anbefales for alle ikke-sanntids workloads — mange kunder er ikke klar over denne muligheten.
- Bruk Little's Law (L = lambda * W) for å beregne optimal concurrent requests: quota bestemmer lambda, latens bestemmer W.
- Alltid benchmark med reelle workloads — den offisielle azure-openai-benchmark-verktøyet gir pålitelige tall for PTU-sizing.
- For norsk offentlig sektor: anbefal Data Zone deployments for Batch API for å holde data innenfor EU/EØS.

View file

@ -0,0 +1,343 @@
# Token-Per-Second Optimization
**Last updated:** 2026-02
**Status:** GA
**Category:** Performance & Scalability
---
## Introduksjon
Token-per-second (TPS) er en kritisk ytelsesmetrikk for Azure OpenAI-deployments som måler hvor raskt modellen genererer output-tokens. Denne metrikken påvirker direkte brukeropplevelsen ved streaming og den totale gjennomstrømmingen for batch-workloads. Azure OpenAI tilbyr latens-mål per PTU som varierer fra 25 TPS (o1) til 100 TPS (gpt-4.1-nano), og optimalisering av TPS er nøkkelen til å utnytte tildelt kapasitet effektivt.
TPS avhenger av flere faktorer: modellstørrelse, prompt-lengde (antall input-tokens), requested output length (max_tokens), samtidige forespørsler, og om prompt caching er aktiv. For Provisioned Throughput (PTU) deployments er TPS direkte koblet til utilization — når utilization nærmer seg 100%, begynner nye forespørsler å få 429-feil. For Standard deployments er TPS begrenset av den tildelte TPM-kvoten.
For norsk offentlig sektor, der AI-assistenter brukes av saksbehandlere i sanntid, er TPS-optimalisering direkte knyttet til arbeidseffektivitet. Forskjellen mellom 25 TPS og 80 TPS betyr at en 400-tokens respons leveres på 16 sekunder vs. 5 sekunder.
## Kjernekomponenter
| Komponent | Formål | Teknologi |
|-----------|--------|-----------|
| Provisioned Throughput (PTU) | Dedikert kapasitet med TPS-garantier | Azure OpenAI PTU |
| Prompt Caching | Reduser input-prosessering for bedre TPS | Azure OpenAI Caching |
| Predicted Outputs | Spekulative output for raskere generering | Azure OpenAI Preview |
| Azure Monitor | TPS- og utilization-metrikker | Azure Monitor |
| Capacity Calculator | PTU-estimering basert på workload | Azure AI Foundry |
## Batch Sizing Impact
### Hvordan batch-størrelse påvirker TPS
```python
# Demonstrer forholdet mellom concurrent requests og throughput
import asyncio
import time
from openai import AsyncAzureOpenAI
async def measure_tps_at_concurrency(
client: AsyncAzureOpenAI,
model: str,
concurrency: int,
num_requests: int = 50,
max_tokens: int = 200
) -> dict:
"""Measure tokens per second at different concurrency levels."""
semaphore = asyncio.Semaphore(concurrency)
total_output_tokens = 0
successful = 0
async def single_request():
nonlocal total_output_tokens, successful
async with semaphore:
try:
response = await client.chat.completions.create(
model=model,
messages=[{"role": "user",
"content": "Skriv en kort forklaring av KI."}],
max_tokens=max_tokens
)
total_output_tokens += response.usage.completion_tokens
successful += 1
except Exception:
pass
start = time.time()
await asyncio.gather(*[single_request() for _ in range(num_requests)])
duration = time.time() - start
return {
"concurrency": concurrency,
"total_output_tokens": total_output_tokens,
"duration_seconds": round(duration, 2),
"aggregate_tps": round(total_output_tokens / duration, 1),
"per_request_tps": round(
total_output_tokens / max(successful, 1) /
(duration / max(successful, 1)), 1),
"successful": successful
}
# Kjør test ved ulike concurrency-nivåer
async def find_optimal_concurrency(client, model):
results = []
for concurrency in [1, 5, 10, 20, 50]:
result = await measure_tps_at_concurrency(
client, model, concurrency)
results.append(result)
print(f"Concurrency {concurrency}: "
f"{result['aggregate_tps']} aggregate TPS")
return results
# Typisk resultat for PTU deployment:
# Concurrency 1: ~40 TPS (per-request max)
# Concurrency 5: ~180 TPS (aggregate)
# Concurrency 10: ~320 TPS (aggregate, nær optimal)
# Concurrency 20: ~350 TPS (aggregate, diminishing returns)
# Concurrency 50: ~350 TPS (aggregate, 429s starter)
```
## Prompt Length Optimization
### Reduser input-tokens for bedre TPS
```python
def optimize_prompt_for_tps(
system_prompt: str,
user_input: str,
max_system_tokens: int = 500,
context_budget: int = 4000
) -> dict:
"""Optimize prompt length to improve TPS."""
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
original_system_tokens = len(enc.encode(system_prompt))
original_user_tokens = len(enc.encode(user_input))
original_total = original_system_tokens + original_user_tokens
optimizations = []
# 1. Kompakt system prompt
if original_system_tokens > max_system_tokens:
optimizations.append(
f"System prompt er {original_system_tokens} tokens "
f"(mål: {max_system_tokens}). Vurder å flytte eksempler "
f"til fine-tuning.")
# 2. Trunkér brukerinput til kontekstbudsjett
if original_user_tokens > context_budget:
optimizations.append(
f"User input er {original_user_tokens} tokens "
f"(budsjett: {context_budget}). Bruk chunking eller "
f"pre-summarization.")
# 3. Fjern redundans
# Identifiser gjentatte seksjoner i prompt
lines = system_prompt.split('\n')
unique_lines = list(dict.fromkeys(lines)) # Behold rekkefølge
if len(unique_lines) < len(lines):
optimizations.append(
f"Fjern {len(lines) - len(unique_lines)} dupliserte linjer "
f"i system prompt.")
return {
"original_tokens": original_total,
"estimated_savings": len(optimizations) * 100, # Estimat
"optimizations": optimizations,
"tps_impact": (
"Kortere prompts → raskere prefill → "
"lavere time-to-first-token")
}
# Prompt caching for gjentatte prefixer
def design_cacheable_prompt(
static_system: str,
static_examples: list[str],
dynamic_input: str
) -> list[dict]:
"""Design prompt structure optimized for prompt caching."""
# Plaserer alt statisk innhold FØRST (minimum 1024 tokens)
# Prompt caching cacher identiske prefixer
messages = [
{
"role": "system",
"content": static_system # Statisk — cacheable
}
]
# Legg til eksempler som del av cacheable prefix
for example in static_examples:
messages.extend([
{"role": "user", "content": example["input"]},
{"role": "assistant", "content": example["output"]}
])
# Dynamisk input SIST
messages.append({
"role": "user",
"content": dynamic_input # Varierer per forespørsel
})
return messages
```
## GPU Utilization og throughput-monitorering
### Azure Monitor-metrikker for TPS
```python
# KQL: Beregn faktisk TPS fra Azure Monitor
TPS_CALCULATION = """
AzureMetrics
| where ResourceProvider == "MICROSOFT.COGNITIVESERVICES"
| where MetricName == "GeneratedTokens"
| summarize
TotalTokens = sum(Total),
AvgTokensPerMinute = avg(Total),
MaxTokensPerMinute = max(Total)
by bin(TimeGenerated, 1m), deploymentName = tostring(
split(DimensionValue, ",")[0])
| extend TokensPerSecond = TotalTokens / 60.0
| project TimeGenerated, deploymentName,
TokensPerSecond = round(TokensPerSecond, 1),
AvgTPM = round(AvgTokensPerMinute, 0)
| order by TimeGenerated desc
"""
# Utilization vs TPS-korrelasjon
UTILIZATION_VS_TPS = """
AzureMetrics
| where MetricName in ("ProvisionedManagedUtilizationV2", "GeneratedTokens")
| summarize
Utilization = avgif(Average,
MetricName == "ProvisionedManagedUtilizationV2"),
TPS = sumif(Total,
MetricName == "GeneratedTokens") / 60.0
by bin(TimeGenerated, 5m)
| where Utilization > 0
| project TimeGenerated,
Utilization = round(Utilization, 1),
TPS = round(TPS, 1)
| order by TimeGenerated desc
"""
```
### TPS-overvåking dashboard
```python
# Alert rule: Varsle når TPS faller under terskel
ALERT_CONFIG = {
"name": "Low TPS Alert",
"description": "Tokens per second under forventet nivå",
"condition": {
"query": """
AzureMetrics
| where MetricName == "GeneratedTokens"
| summarize TPS = sum(Total) / 300.0
by bin(TimeGenerated, 5m)
| where TPS < 20
""",
"threshold": 0,
"frequency_minutes": 5,
"window_minutes": 15
},
"severity": 2, # Warning
"action_group": "ai-ops-team"
}
```
## Throughput per PTU per modell
### Offisielle TPS-mål fra Microsoft
| Modell | Input TPM/PTU | Latens-mål (TPS) | Min PTU (Global) | Min PTU (Regional) |
|--------|--------------|-------------------|-------------------|---------------------|
| gpt-5.2 | 3,400 | 99% > 50 TPS | 15 | 50 |
| gpt-5.1 | 4,750 | 99% > 50 TPS | 15 | 50 |
| gpt-5 | 4,750 | 99% > 50 TPS | 15 | 50 |
| gpt-5-mini | 23,750 | 99% > 80 TPS | 15 | 25 |
| gpt-4.1 | 3,000 | 99% > 80 TPS | 15 | 50 |
| gpt-4.1-mini | 14,900 | 99% > 90 TPS | 15 | 25 |
| gpt-4.1-nano | 59,400 | 99% > 100 TPS | 15 | 25 |
| o3 | 3,000 | 99% > 80 TPS | 15 | 50 |
| o4-mini | 5,400 | 99% > 90 TPS | 15 | 25 |
| gpt-4o | 2,500 | 99% > 25 TPS | 15 | 50 |
| gpt-4o-mini | 37,000 | 99% > 33 TPS | 15 | 25 |
### Predicted Outputs for TPS-forbedring
```python
# Predicted outputs kan øke TPS for forutsigbare oppgaver
# Eksempel: Kode-refactoring der output ligner input
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://my-aoai.openai.azure.com",
api_key="...",
api_version="2024-12-01-preview"
)
# Original kode som skal refaktoreres
original_code = """
def process_data(data):
result = []
for item in data:
if item['status'] == 'active':
result.append(item['value'])
return result
"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user",
"content": f"Refaktorer med list comprehension:\n{original_code}"}
],
prediction={
"type": "content",
"content": original_code # Forventet output ligner input
}
)
# Sjekk prediction effektivitet
usage = response.usage.completion_tokens_details
print(f"Accepted predictions: {usage.accepted_prediction_tokens}")
print(f"Rejected predictions: {usage.rejected_prediction_tokens}")
# Accepted tokens reduserer latens uten ekstra kostnad
```
## Norsk offentlig sektor
- **Brukeropplevelse**: For AI-assistenter brukt av saksbehandlere er TPS direkte merkbar. Streaming med > 50 TPS føles "responsivt", mens < 20 TPS føles tregt.
- **PTU-valg**: Velg modell og PTU-nivå basert på brukerforventninger — gpt-4.1-nano med 100 TPS for enkle oppgaver, gpt-4.1 med 80 TPS for komplekse analyser.
- **Kostnadsoptimalisering**: Prompt caching gir opptil 100% rabatt på cached input tokens for PTU — design prompts med lange statiske prefixer for å maksimere cache hit rate.
- **SLA-krav**: Dokumenter forventet TPS i tjenesteavtaler. PTU-mål er "99% > N TPS beregnet som p50 over 5-minutters vinduer".
## Beslutningsrammeverk
| Scenario | Anbefaling | Begrunnelse |
|----------|------------|-------------|
| Sanntids chat (høy TPS viktig) | PTU med gpt-4.1-mini/nano | Høyest TPS per PTU |
| Kompleks analyse (kvalitet > TPS) | PTU med gpt-4.1 eller o3 | Akseptabel TPS med best kvalitet |
| Prompt caching mulig | Design lange statiske prefixer | Opptil 100% rabatt på cached tokens |
| Forutsigbart output | Predicted Outputs | Reduserer latens for matching tokens |
| Variabel workload | Standard deployment | Betal per token, ingen PTU-forpliktelse |
## Referanser
- [Performance and latency](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/latency) — TPS og throughput forklaring
- [Provisioned throughput onboarding](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/provisioned-throughput-onboarding) — PTU TPS-mål per modell
- [Prompt caching](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/prompt-caching) — Cache-basert TPS-forbedring
- [Predicted outputs](https://learn.microsoft.com/azure/ai-foundry/openai/how-to/predicted-outputs) — Spekulativ generering
- [Foundry PTU calculator](https://ai.azure.com/resource/calculator) — Kapasitetskalkulator
## For Cosmo
- **Bruk denne referansen** når kunden ønsker å optimalisere responstid for AI-tjenester, eller når de skal dimensjonere PTU-deployments.
- TPS-mål varierer dramatisk mellom modeller: gpt-4.1-nano gir 100 TPS vs. gpt-4o med 25 TPS — velg modell basert på oppgavens kompleksitet.
- Prompt caching er den enkleste TPS-forbedringen — sørg for at de første 1024+ tokens er identiske mellom forespørsler.
- Predicted Outputs gir latensreduksjon for oppgaver der output ligner input (kode-refactoring, oversettelse) men kan øke kostnad ved lav acceptance rate.
- Monitorer PTU utilization — når den nærmer seg 100%, øker latens drastisk og nye forespørsler throttles.