# 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.
