Steg 9 (R4): unified migrate-corpus.mjs --write over engineering/governance/ infrastructure/security. 327 filer mutert, verified=null, prosa byte-identisk (fra første ## seksjon), advisor urørt (0 endringer). To applier-fixes oppdaget under kjøring (TDD, RED→GREEN): - insertHeaderFields: anker faller nå tilbake når en meta-linje selv passerer 500B (2 filer pakket et avsnitt i **Status:** → Type/Source landet utenfor scan-vinduet, applierens post-write-assertion fanget + restaurerte). - normalizeStaleVerified: fjerner nå ALLE stale non-date **Verified:** i 500B-vinduet, inkl. stray body-dup rett under --- (9 mlops-genaiops-filer var ellers falskt "verified"/fresh, droppet fra worklist). Operatør-godkjent utvidelse av carve-out; kun stray metadata-linjer, aldri prosa. test-transform-criterion: precondition oppdatert til post-migrasjons-sannhet (fila bærer nå Source). Suite 728/728 grønn.
449 lines
15 KiB
Markdown
449 lines
15 KiB
Markdown
# Load Testing AI Services
|
|
|
|
**Last updated:** 2026-06-24
|
|
**Status:** GA
|
|
**Category:** Performance & Scalability
|
|
**Type:** reference
|
|
**Source:** https://learn.microsoft.com/azure/load-testing/overview-what-is-azure-load-testing
|
|
|
|
---
|
|
|
|
## Innhold
|
|
|
|
- [Introduksjon](#introduksjon)
|
|
- [Kjernekomponenter](#kjernekomponenter)
|
|
- [Load Test Design](#load-test-design)
|
|
- [Realistic Traffic Patterns](#realistic-traffic-patterns)
|
|
- [Bottleneck Analysis](#bottleneck-analysis)
|
|
- [Capacity Forecasting](#capacity-forecasting)
|
|
- [Norsk offentlig sektor](#norsk-offentlig-sektor)
|
|
- [Beslutningsrammeverk](#beslutningsrammeverk)
|
|
- [Referanser](#referanser)
|
|
- [For Cosmo](#for-cosmo)
|
|
|
|
## 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.
|
|
# Global & Data Zone Provisioned (anbefalt default): minimum 15 PTU, increment 5 for alle GPT-/o-modeller.
|
|
# Regional Provisioned: minimum 50 (gpt-4.1/o3/gpt-4o), 25 for mini/nano-modeller.
|
|
min_deployment = 15
|
|
increment = 5
|
|
ptus_deployed = max(
|
|
min_deployment,
|
|
((int(ptus_needed) // increment) + 1) * increment
|
|
)
|
|
|
|
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/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/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.
|