ms-ai-architect/skills/ms-ai-security/references/performance-scalability/load-testing-ai-services.md
Kjell Tore Guttormsen ddce43d8b2 feat(ms-ai-architect): Spor 1 — Port-1-substrat migrert på 4 ikke-advisor-skills (243 Source + 327 Type + 325 TOC + stale-verified poison fjernet) [skip-docs]
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.
2026-07-04 10:19:11 +02:00

15 KiB

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

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

# 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

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

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

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

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

# 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

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.