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.
483 lines
18 KiB
Markdown
483 lines
18 KiB
Markdown
# Network-Constrained AI Deployment
|
|
|
|
**Last updated:** 2026-02
|
|
**Status:** GA
|
|
**Category:** Hybrid Cloud & Edge AI
|
|
**Type:** reference
|
|
|
|
---
|
|
|
|
## Innhold
|
|
|
|
- [Introduksjon](#introduksjon)
|
|
- [Kjernekomponenter](#kjernekomponenter)
|
|
- [Model Size Reduction](#model-size-reduction)
|
|
- [Partial Model Loading](#partial-model-loading)
|
|
- [Bandwidth-Aware Batching](#bandwidth-aware-batching)
|
|
- [Latency Compensation Patterns](#latency-compensation-patterns)
|
|
- [Norsk offentlig sektor](#norsk-offentlig-sektor)
|
|
- [Beslutningsrammeverk](#beslutningsrammeverk)
|
|
- [For Cosmo](#for-cosmo)
|
|
|
|
## Introduksjon
|
|
|
|
Nettverksbegrensede miljoer — med lav bandwidth, hoey latens, eller intermitterende tilkobling — krever spesialtilpassede AI-deployments. Standard sky-baserte AI-arkitekturer som sender data frem og tilbake til cloud endpoints feiler i slike miljoer, enten pa grunn av uakseptabel latens eller fordi tilkoblingen simpelthen ikke er palitelig nok.
|
|
|
|
For norsk offentlig sektor er dette relevant i mange scenarier: rurale omrader med begrenset mobildekning, maritime miljoer med satellittkommunikasjon, tunneler og underjordiske anlegg, feltoperasjoner i krisesituasjoner, og industrielle miljoer med nettverksisolasjon av sikkerhetsgrunner. AI-losninger for slike miljoer ma optimaliseres for minimal nettverksbruk.
|
|
|
|
Microsoft tilbyr flere teknologier for nettverksbegrensede deployments: modellkvantisering og -komprimering med Olive/ONNX Runtime for mindre modeller, Azure IoT Edge med utvidet offline-stoette for edge-prosessering, delta-synkronisering for effektiv dataoverfoering, og bandwidth-bevisst batching for a maksimere utnyttelsen av tilgjengelig tilkobling.
|
|
|
|
---
|
|
|
|
## Kjernekomponenter
|
|
|
|
| Komponent | Formal | Teknologi |
|
|
|-----------|--------|-----------|
|
|
| ONNX Runtime | Optimalisert lokal inferens | Cross-platform |
|
|
| Olive | Modellkomprimering og kvantisering | Python |
|
|
| Azure IoT Edge | Edge-prosessering med buffring | Container runtime |
|
|
| Delta Sync | Inkrementell datasynkronisering | Protokoll |
|
|
| MQTT | Lettvekts meldingsprotokoll | IoT-standard |
|
|
| gRPC | Effektiv binart API-protokoll | Google RPC |
|
|
| Protocol Buffers | Kompakt serialisering | Google |
|
|
|
|
---
|
|
|
|
## Model Size Reduction
|
|
|
|
### Kvantiseringsstrategier for nettverksbegrensede miljoer
|
|
|
|
| Teknikk | Stoerrelses-reduksjon | Kvalitetstap | Nedlastningstid (1 Mbps) |
|
|
|---------|---------------------|-------------|--------------------------|
|
|
| FP32 (original) | Baseline (7 GB for 3.8B) | Ingen | 15+ timer |
|
|
| FP16 | 2x (3.5 GB) | Minimalt | 7+ timer |
|
|
| INT8 | 4x (1.75 GB) | < 1% | 3.5 timer |
|
|
| INT4 | 8x (875 MB) | 1-3% | 1.7 timer |
|
|
| INT4 + Pruning | 10-12x (600 MB) | 2-5% | 1.2 timer |
|
|
| Distillation | 20-50x (140-350 MB) | 5-15% | 15-40 min |
|
|
|
|
### Olive-basert komprimering
|
|
|
|
```python
|
|
# Olive pipeline for maksimal modellkomprimering
|
|
import json
|
|
|
|
olive_config = {
|
|
"input_model": {
|
|
"type": "HfModel",
|
|
"model_path": "microsoft/Phi-3-mini-4k-instruct"
|
|
},
|
|
"passes": {
|
|
# Steg 1: Konverter til ONNX
|
|
"conversion": {
|
|
"type": "OnnxConversion",
|
|
"target_opset": 17
|
|
},
|
|
# Steg 2: Grafoptimalisering
|
|
"optimization": {
|
|
"type": "OrtTransformersOptimization",
|
|
"model_type": "gpt2",
|
|
"opt_level": 2,
|
|
"only_onnxruntime": True
|
|
},
|
|
# Steg 3: Kvantisering til INT4
|
|
"quantization": {
|
|
"type": "OnnxMatMul4Quantizer",
|
|
"block_size": 32,
|
|
"is_symmetric": True,
|
|
"accuracy_level": 4
|
|
},
|
|
# Steg 4: Strukturell pruning (fjern unodvendige vekter)
|
|
"pruning": {
|
|
"type": "SlicGPT",
|
|
"sparsity": 0.25, # 25% reduksjon
|
|
"calibration_data_config": {
|
|
"name": "c4",
|
|
"split": "validation",
|
|
"num_samples": 128
|
|
}
|
|
}
|
|
},
|
|
"engine": {
|
|
"evaluator": {
|
|
"metrics": [
|
|
{
|
|
"name": "model_size",
|
|
"type": "custom_metric",
|
|
"priority": 1
|
|
},
|
|
{
|
|
"name": "latency",
|
|
"type": "latency",
|
|
"priority": 2
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
# Kjor Olive-pipeline
|
|
# olive run --config olive_compress.json
|
|
```
|
|
|
|
### Kunnskapsdesstillasjon for ultra-sma modeller
|
|
|
|
```python
|
|
# Destiller fra Phi-3 Medium (14B) til en 1B custom-modell
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
import torch
|
|
|
|
class ModelDistillation:
|
|
def __init__(self, teacher_model: str, student_config: dict):
|
|
self.teacher = AutoModelForCausalLM.from_pretrained(teacher_model)
|
|
self.student = self._create_student(student_config)
|
|
self.tokenizer = AutoTokenizer.from_pretrained(teacher_model)
|
|
|
|
def distill(self, dataset, epochs: int = 5, temperature: float = 2.0):
|
|
"""Destiller laerer-modell til elev-modell"""
|
|
optimizer = torch.optim.AdamW(self.student.parameters(), lr=1e-4)
|
|
|
|
for epoch in range(epochs):
|
|
for batch in dataset:
|
|
inputs = self.tokenizer(batch["text"], return_tensors="pt",
|
|
padding=True, truncation=True)
|
|
|
|
# Laerer-prediksjoner (soft targets)
|
|
with torch.no_grad():
|
|
teacher_logits = self.teacher(**inputs).logits
|
|
|
|
# Elev-prediksjoner
|
|
student_logits = self.student(**inputs).logits
|
|
|
|
# Destillasjonsloss: KL-divergens med temperatur
|
|
loss = self._distillation_loss(
|
|
student_logits, teacher_logits, temperature
|
|
)
|
|
|
|
loss.backward()
|
|
optimizer.step()
|
|
optimizer.zero_grad()
|
|
|
|
def _distillation_loss(self, student_logits, teacher_logits, temperature):
|
|
"""KL-divergens mellom student og teacher"""
|
|
import torch.nn.functional as F
|
|
soft_student = F.log_softmax(student_logits / temperature, dim=-1)
|
|
soft_teacher = F.softmax(teacher_logits / temperature, dim=-1)
|
|
return F.kl_div(soft_student, soft_teacher, reduction='batchmean') * (temperature ** 2)
|
|
```
|
|
|
|
---
|
|
|
|
## Partial Model Loading
|
|
|
|
### Modell-segmentering for inkrementell nedlasting
|
|
|
|
```python
|
|
# Delvis modellnedlasting for nettverksbegrensede miljoer
|
|
import os
|
|
import hashlib
|
|
from typing import Optional
|
|
|
|
class IncrementalModelLoader:
|
|
def __init__(self, model_dir: str, remote_url: str):
|
|
self.model_dir = model_dir
|
|
self.remote_url = remote_url
|
|
self.manifest_path = os.path.join(model_dir, "manifest.json")
|
|
|
|
def check_and_download(self, bandwidth_kbps: float) -> dict:
|
|
"""Sjekk modellstatus og last ned manglende deler"""
|
|
manifest = self._get_remote_manifest()
|
|
local_state = self._get_local_state()
|
|
|
|
missing_segments = []
|
|
total_download_bytes = 0
|
|
|
|
for segment in manifest["segments"]:
|
|
local_hash = local_state.get(segment["name"])
|
|
if local_hash != segment["hash"]:
|
|
missing_segments.append(segment)
|
|
total_download_bytes += segment["size"]
|
|
|
|
# Estimer nedlastningstid
|
|
download_time_seconds = (total_download_bytes * 8) / (bandwidth_kbps * 1000)
|
|
|
|
return {
|
|
"model_version": manifest["version"],
|
|
"total_segments": len(manifest["segments"]),
|
|
"missing_segments": len(missing_segments),
|
|
"download_size_mb": total_download_bytes / 1024 / 1024,
|
|
"estimated_time_minutes": download_time_seconds / 60,
|
|
"can_use_partial": manifest.get("supports_partial_inference", False),
|
|
"minimum_segments_for_inference": manifest.get("min_segments", 1)
|
|
}
|
|
|
|
def download_prioritized(self, bandwidth_kbps: float,
|
|
time_budget_minutes: float) -> str:
|
|
"""Last ned modellsegmenter prioritert innenfor tidsbudsjett"""
|
|
check = self.check_and_download(bandwidth_kbps)
|
|
|
|
if check["estimated_time_minutes"] <= time_budget_minutes:
|
|
# Full nedlasting mulig
|
|
return self._download_all_segments()
|
|
else:
|
|
# Prioritert delvis nedlasting
|
|
# Last ned kritiske segmenter foerst (embedding, attention heads)
|
|
return self._download_critical_first(time_budget_minutes, bandwidth_kbps)
|
|
|
|
def _download_critical_first(self, time_budget: float, bw: float) -> str:
|
|
"""Last ned de viktigste modelldelene foerst"""
|
|
priority_order = [
|
|
"embeddings", # Nodvendig for all inferens
|
|
"attention_layers", # Kjernekapabilitet
|
|
"ffn_layers", # Detaljert prosessering
|
|
"output_head" # Siste lag
|
|
]
|
|
# Download i prioritert rekkefoolge innenfor tidsbudsjett
|
|
downloaded = []
|
|
remaining_seconds = time_budget * 60
|
|
|
|
for priority in priority_order:
|
|
segment_size = self._get_segment_size(priority)
|
|
download_time = (segment_size * 8) / (bw * 1000)
|
|
|
|
if download_time <= remaining_seconds:
|
|
self._download_segment(priority)
|
|
downloaded.append(priority)
|
|
remaining_seconds -= download_time
|
|
else:
|
|
break
|
|
|
|
return f"Lastet ned {len(downloaded)}/{len(priority_order)} segmenter"
|
|
```
|
|
|
|
---
|
|
|
|
## Bandwidth-Aware Batching
|
|
|
|
### Adaptiv batchstoerrelser basert pa tilgjengelig bandwidth
|
|
|
|
```python
|
|
# Bandwidth-bevisst batch-synkronisering
|
|
import asyncio
|
|
import time
|
|
from dataclasses import dataclass
|
|
|
|
@dataclass
|
|
class BandwidthProfile:
|
|
estimated_kbps: float
|
|
latency_ms: float
|
|
reliability: float # 0-1, andel vellykkede overforinger
|
|
|
|
class AdaptiveBatchSync:
|
|
def __init__(self):
|
|
self.bandwidth_history: list[BandwidthProfile] = []
|
|
self.compression_enabled = True
|
|
|
|
async def measure_bandwidth(self) -> BandwidthProfile:
|
|
"""Mal tilgjengelig bandwidth med minimal data"""
|
|
test_data = b"x" * 1024 # 1 KB testpakke
|
|
start = time.time()
|
|
|
|
try:
|
|
# Send testpakke og mal round-trip
|
|
success = await self._send_probe(test_data)
|
|
elapsed = time.time() - start
|
|
|
|
profile = BandwidthProfile(
|
|
estimated_kbps=(len(test_data) * 8) / (elapsed * 1000),
|
|
latency_ms=elapsed * 1000,
|
|
reliability=1.0 if success else 0.0
|
|
)
|
|
except Exception:
|
|
profile = BandwidthProfile(
|
|
estimated_kbps=0, latency_ms=float('inf'), reliability=0.0
|
|
)
|
|
|
|
self.bandwidth_history.append(profile)
|
|
return profile
|
|
|
|
def calculate_optimal_batch(self, pending_items: int,
|
|
avg_item_size_kb: float) -> dict:
|
|
"""Beregn optimal batchstoorrelse basert pa nettverksforhold"""
|
|
if not self.bandwidth_history:
|
|
return {"batch_size": 1, "reason": "Ingen maalinger"}
|
|
|
|
recent = self.bandwidth_history[-5:] # Siste 5 maalinger
|
|
avg_bw = sum(p.estimated_kbps for p in recent) / len(recent)
|
|
avg_reliability = sum(p.reliability for p in recent) / len(recent)
|
|
|
|
if avg_reliability < 0.3:
|
|
# Svart upaalitelig — minimale batche
|
|
return {"batch_size": 1, "compress": True, "priority_only": True}
|
|
|
|
if avg_bw < 10: # < 10 kbps
|
|
# Ekstremt lav bandwidth
|
|
batch_size = min(5, pending_items)
|
|
return {
|
|
"batch_size": batch_size,
|
|
"compress": True,
|
|
"format": "protobuf",
|
|
"priority_only": True,
|
|
"estimated_time_s": (batch_size * avg_item_size_kb) / avg_bw
|
|
}
|
|
|
|
elif avg_bw < 100: # 10-100 kbps
|
|
# Lav bandwidth
|
|
batch_size = min(20, pending_items)
|
|
return {
|
|
"batch_size": batch_size,
|
|
"compress": True,
|
|
"format": "protobuf",
|
|
"priority_only": False
|
|
}
|
|
|
|
elif avg_bw < 1000: # 100 kbps - 1 Mbps
|
|
# Medium bandwidth
|
|
batch_size = min(100, pending_items)
|
|
return {
|
|
"batch_size": batch_size,
|
|
"compress": True,
|
|
"format": "json_gzip"
|
|
}
|
|
|
|
else: # > 1 Mbps
|
|
# God bandwidth
|
|
return {
|
|
"batch_size": min(500, pending_items),
|
|
"compress": False,
|
|
"format": "json"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Latency Compensation Patterns
|
|
|
|
### Strategier for latenskompensering
|
|
|
|
| Monster | Beskrivelse | Implementering |
|
|
|---------|-------------|----------------|
|
|
| Optimistisk UI | Vis resultat umiddelbart, korriger senere | Lokal prediksjon + sky-validering |
|
|
| Prefetching | Forhaandslast sannsynlige data | Prediktiv caching |
|
|
| Stale-while-revalidate | Vis cachet data mens ny hentes | Cache-lag med TTL |
|
|
| Lokal buffer | Buffer resultater lokalt | SQLite + event queue |
|
|
| Priority queue | Prioriter kritiske data | Vektet synk-koe |
|
|
| Komprimering | Reduser datamengde | gzip, protobuf, CBOR |
|
|
|
|
### Implementering av latenskompensering
|
|
|
|
```python
|
|
# Latenskompenserende AI-proxy
|
|
import asyncio
|
|
import gzip
|
|
import json
|
|
from collections import OrderedDict
|
|
|
|
class LatencyCompensatingProxy:
|
|
def __init__(self, local_model, cache_size: int = 1000):
|
|
self.local_model = local_model
|
|
self.cache = OrderedDict()
|
|
self.cache_size = cache_size
|
|
self.pending_validations = asyncio.Queue()
|
|
|
|
async def predict(self, input_data: dict) -> dict:
|
|
"""Prediksjon med latenskompensering"""
|
|
cache_key = self._hash_input(input_data)
|
|
|
|
# Sjekk cache forst
|
|
if cache_key in self.cache:
|
|
cached = self.cache[cache_key]
|
|
self.cache.move_to_end(cache_key)
|
|
return {**cached, "source": "cache"}
|
|
|
|
# Lokal prediksjon (umiddelbar)
|
|
local_result = self.local_model.predict(input_data)
|
|
|
|
# Cache resultatet
|
|
self._cache_result(cache_key, local_result)
|
|
|
|
# Koe for sky-validering i bakgrunnen
|
|
await self.pending_validations.put({
|
|
"cache_key": cache_key,
|
|
"input": input_data,
|
|
"local_result": local_result
|
|
})
|
|
|
|
return {**local_result, "source": "local", "validated": False}
|
|
|
|
async def background_validator(self):
|
|
"""Bakgrunnsvalidering mot sky-modell"""
|
|
while True:
|
|
item = await self.pending_validations.get()
|
|
try:
|
|
cloud_result = await self._cloud_predict(item["input"])
|
|
|
|
# Oppdater cache med validert resultat
|
|
self._cache_result(item["cache_key"], {
|
|
**cloud_result,
|
|
"validated": True
|
|
})
|
|
|
|
# Varsle om avvik
|
|
if self._significant_difference(
|
|
item["local_result"], cloud_result
|
|
):
|
|
await self._notify_correction(item, cloud_result)
|
|
|
|
except Exception:
|
|
pass # Sky utilgjengelig — behold lokal prediksjon
|
|
|
|
def _significant_difference(self, local: dict, cloud: dict) -> bool:
|
|
"""Sjekk om sky-resultat avviker vesentlig fra lokalt"""
|
|
if local.get("label") != cloud.get("label"):
|
|
return True
|
|
if abs(local.get("confidence", 0) - cloud.get("confidence", 0)) > 0.2:
|
|
return True
|
|
return False
|
|
```
|
|
|
|
---
|
|
|
|
## Norsk offentlig sektor
|
|
|
|
### Nettverksbegrensede scenarier i Norge
|
|
|
|
| Scenario | Typisk bandwidth | Latens | Tilgjengelighet |
|
|
|----------|-----------------|--------|-----------------|
|
|
| Rural mobildekning | 1-10 Mbps | 50-200 ms | 70-90% |
|
|
| Maritim (kyst) | 0.5-5 Mbps | 200-600 ms | 60-80% |
|
|
| Tunnel/underjordisk | 0 Mbps (isolert) | N/A | 0% |
|
|
| Svalbart | 1-50 Mbps | 500+ ms | 80% |
|
|
| Beredskap (krise) | 0.1-1 Mbps | Variable | 30-70% |
|
|
| Felt (skog/fjell) | 0-5 Mbps | 100-500 ms | 40-80% |
|
|
|
|
### Anbefalinger
|
|
|
|
- Dimensjoner alltid for verste-tilfelle tilkobling
|
|
- Bruk INT4-kvantiserte modeller som standard for edge-deployment
|
|
- Implementer bandwidth-maling for a tilpasse sync-strategi dynamisk
|
|
- Protobuf/CBOR for serialisering i stedet for JSON i lav-bandwidth-scenarier
|
|
- Prioriter anomalier og kritiske resultater i sync-koeen
|
|
|
|
---
|
|
|
|
## Beslutningsrammeverk
|
|
|
|
| Scenario | Anbefaling | Begrunnelse |
|
|
|----------|------------|-------------|
|
|
| < 100 kbps | Full lokal inferens + minimal sync | Ikke nok bandwidth for sky-AI |
|
|
| 100 kbps - 1 Mbps | Lokal inferens + delta-sync | Synkroniser resultater, ikke radata |
|
|
| 1-10 Mbps | Hybrid med progressiv enhancement | Sky-validering av lokale resultater |
|
|
| > 10 Mbps | Standard sky-AI med lokal fallback | Normal drift med offline-buffer |
|
|
| Intermitterende | Event sourcing + prioritert batch-sync | Palitelig leveranse over tid |
|
|
| Satelitt (hoey latens) | Full lokal med periodisk bulk-sync | Latens for hoey for interaktiv sky-AI |
|
|
|
|
---
|
|
|
|
## For Cosmo
|
|
|
|
- **Modellstoerrelse er den viktigste faktoren for nettverksbegrensede deployments** — bruk INT4-kvantisering som standard, og vurder destillasjon for ultralette modeller under 500 MB
|
|
- **Bandwidth-bevisst batching er pabudt** — maal tilgjengelig bandwidth kontinuerlig og tilpass batchstoerrelser og kompresjonsformat dynamisk
|
|
- **Protobuf/CBOR sparer 60-80% bandwidth** sammenlignet med JSON — bruk binaere serialiseringsformater for all edge-til-sky-kommunikasjon i lav-bandwidth-miljoer
|
|
- **Inkrementell modellnedlasting er kritisk** for modelloppdatering over lav bandwidth — last ned kun endrede lag/segmenter, og stoeett delvis modellbruk under nedlasting
|
|
- **For norsk offentlig sektor: Design for 100 kbps som worst case** — mange felt-scenarier i rural Norge har begrenset 4G-dekning, og maritime/beredskapsscenarier kan ha enda lavere bandwidth
|