# Network-Constrained AI Deployment

**Last updated:** 2026-02
**Status:** GA
**Category:** Hybrid Cloud & Edge AI

---

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