ms-ai-architect/skills/ms-ai-security/references/performance-scalability/load-testing-ai-services.md
Kjell Tore Guttormsen dd1036ab8a fix(ms-ai-architect): Foundry URL-navnerom-migrering (ai-foundry → foundry/foundry-classic, 141 filer)
Task #5 del 1/3 (URL-migrering). Verifiseringen motbeviste STATE.md-premisset om ren prefix-swap: rebrand er per-URL, ikke mekanisk. En blind sed ai-foundry→foundry ville lagd 56 nye 404-er (classic-stiene finnes ikke under nytt foundry/-prefiks — bekreftet empirisk).

Metode: resolverte alle 237 unike KB-URLer mot live redirects (curl -L), bygde full-URL→full-URL-mapping fra faktisk url_effective. Bevarer locale-form, query (?view=) og #fragment per lenke.

- 231 navnerom-erstatninger over 141 filer (408 forekomster):
  - 161 → azure/foundry/ (98 ren prefix-swap + 10 sti-reorg + reorg-tilfeller)
  - 69 → azure/foundry-classic/ (eldre hub-spor: assistants, hub-DR, on-your-data; faktisk redirect-mål per operatorvalg)
  - 1 → azure/foundry-local/

- 2 døde lenker (404) fikset til verifiserte mål:
  - agent-service → azure/foundry/agents/overview
  - concepts/evaluation-evaluators/ → azure/foundry/how-to/evaluate-generative-ai-app

- 5 path-/display-referanser (uten https://, i backticks/lenketekst) rettet manuelt.

- 6 slug-baserte ai-foundry-treff urørt (scope-grense): managed-grafana-dashboard, security-baseline, power-platform prompt-builder, architecture baseline-chat (sistnevnte slug-rebrand i annet navnerom — mulig fremtidig funn).

- Parkert til task #5 del 2/3: Norway East GPT-5-datasuverenitet-fiks + modellkatalog-utvidelse (5.3/5.4/5.5, gpt-oss, sora-2).

Verifisert: 0 gjenværende azure/ai-foundry/-navnerom i skills/. validate-plugin.sh 219 PASS. test-kb-integrity.sh 117/117 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01REiKFhP4w6xGXXqWKpPCJJ
2026-06-18 13:37:06 +02:00

15 KiB

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

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

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