Modelplane Modelplane docs

Qwen2.5-72B

A 72B dense chat model served from an AWQ INT4 quantization on a single 80GB GPU per replica: one Standalone engine fed by a ModelCache. The platform side comes in two shapes - an A100 on AKS and an H100 on Nebius - and the ML side is the same manifest for both. The deployment carries no clusterSelector and two replicas, and each pool holds exactly one GPU, so with both platforms applied one replica lands on each. The service then splits traffic between the two GPUs by weight, which the last section uses to compare them. To serve on just one platform, apply one tab and drop replicas to 1.

These manifests mirror the repository’s AKS and Nebius demos. Apply the platform side first, then the ML side.

Platform

inference-class-aks.yaml
# InferenceClass for the A100 shape on AKS. One NVIDIA A100 80GB on a
# Standard_NC24ads_A100_v4. The GPU is declared as a DRA device: the scheduler
# matches a ModelDeployment's nodeSelector against this capacity, then DRA
# binds the physical GPU to the serving pod.
apiVersion: modelplane.ai/v1alpha1
kind: InferenceClass
metadata:
  name: aks-a100-1x-nc24ads
spec:
  description: "AKS Standard_NC24ads_A100_v4, 1x NVIDIA A100 80GB"
  provisioning:
    provider: AKS
    aks:
      vmSize: Standard_NC24ads_A100_v4
      diskSizeGb: 200
      accelerator:
        type: nvidia-a100
        count: 1
  devices:
  - name: gpu
    claim: DRA
    driver: gpu.nvidia.com
    deviceClassName: gpu.nvidia.com
    count: 1
    attributes:
      architecture: { string: Ampere }
      cudaComputeCapability: { version: "8.0.0" }
    capacity:
      # The H100's usable VRAM as the NVIDIA DRA driver reports it, not the
      # nominal 80GB.
      memory: { value: "81920Mi" }
inference-cluster-aks.yaml
# AKS InferenceCluster with one A100 node pool in swedencentral. No
# clusterSelector targets it; the ModelDeployment matches on device capacity
# alone, so it lands here or on any other compatible cluster in the fleet.
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
  name: aks-a100-single
  labels:
    modelplane.ai/region: swedencentral
spec:
  cluster:
    source: AKS
    aks:
      location: swedencentral
  nodePools:
  - name: gpua100
    className: aks-a100-1x-nc24ads
    nodeCount: 1
    minNodeCount: 1
    maxNodeCount: 1
inference-class-nebius.yaml
# InferenceClass for the H100 shape on Nebius. One NVIDIA H100 80GB on the
# gpu-h100-sxm platform, with the cuda13.0 driver preset mk8s preinstalls on
# the pool's nodes. The GPU is declared as a DRA device: the scheduler matches
# a ModelDeployment's nodeSelector against this capacity, then DRA binds the
# physical GPU to the serving pod.
apiVersion: modelplane.ai/v1alpha1
kind: InferenceClass
metadata:
  name: nebius-h100-1x-sxm
spec:
  description: "Nebius gpu-h100-sxm, 1x NVIDIA H100 80GB"
  provisioning:
    provider: Nebius
    nebius:
      platform: gpu-h100-sxm
      preset: 1gpu-16vcpu-200gb
      diskSizeGb: 200
      driversPreset: cuda13.0
      accelerator:
        type: nvidia-h100
        count: 1
  devices:
  - name: gpu
    claim: DRA
    driver: gpu.nvidia.com
    deviceClassName: gpu.nvidia.com
    count: 1
    attributes:
      architecture: { string: Hopper }
      cudaComputeCapability: { version: "9.0.0" }
    capacity:
      # The H100's usable VRAM as the NVIDIA DRA driver reports it, not the
      # nominal 80GB.
      memory: { value: "81559Mi" }
inference-cluster-nebius.yaml
# Nebius InferenceCluster with one H100 node pool that scales from zero: no
# minNodeCount, so the autoscaling floor defaults to nodeCount (0) and the GPU
# node only appears when a deployment's pod goes Pending.
apiVersion: modelplane.ai/v1alpha1
kind: InferenceCluster
metadata:
  name: nebius-h100-single
  labels:
    modelplane.ai/region: eu-north
spec:
  cluster:
    source: Nebius
    nebius: {}
  nodePools:
  - name: gpu-h100
    className: nebius-h100-1x-sxm
    nodeCount: 0
    maxNodeCount: 1

Deployment

model-cache.yaml
# The cache the engine serves from, hydrated once per matched cluster. The AWQ
# quant repo is public, so no authSecret is needed.
apiVersion: modelplane.ai/v1alpha1
kind: ModelCache
metadata:
  name: qwen-72b-awq
  namespace: ml-team
spec:
  source: HuggingFace
  huggingFace:
    repo: Qwen/Qwen2.5-72B-Instruct-AWQ
    sizeGiB: 150
model-deployment.yaml
# Qwen2.5-72B Instruct (AWQ INT4) served on a single 80GB GPU per replica by
# vLLM. The model layer is cloud-agnostic: the >=70Gi CEL selector matches the
# A100 class on AKS (81920Mi) and the H100 class on Nebius (81559Mi) alike.
# Replicas are fleet-wide, not per-cluster, and each pool holds exactly one
# such GPU, so with both platforms applied the two replicas land one on the
# A100 and one on the H100. With a single platform applied, drop replicas to 1.
#
# 72B in AWQ INT4 is ~40Gi of weights, leaving room for the KV cache on an
# 80GB GPU. --max-model-len caps the context at 8192 - raise it only as far as
# the leftover VRAM allows.
apiVersion: modelplane.ai/v1alpha1
kind: ModelDeployment
metadata:
  name: qwen-72b
  namespace: ml-team
spec:
  replicas: 2
  template:
    spec:
      modelCacheRef:
        name: qwen-72b-awq
      engines:
      - name: qwen-72b
        members:
        - role: Standalone
          nodeSelector:
            devices:
            - name: gpu
              count: 1
              selectors:
              - cel: |
                  device.capacity["gpu.nvidia.com"].memory.compareTo(quantity("70Gi")) >= 0
          template:
            spec:
              containers:
              - name: engine
                image: vllm/vllm-openai:v0.23.0
                args:
                - --model=/mnt/models
                - --served-model-name=qwen-72b
                - --max-model-len=8192
model-service.yaml
# Exposes the qwen-72b deployment's endpoints as a single OpenAI-compatible
# URL. Modelplane labels each composed ModelEndpoint with the deployment name,
# so this selector reaches every replica. Read the public address from
# status.address:
#   kubectl get ms qwen-72b -n ml-team -o jsonpath='{.status.address}'
apiVersion: modelplane.ai/v1alpha1
kind: ModelService
metadata:
  name: qwen-72b
  namespace: ml-team
spec:
  endpoints:
  - selector:
      matchLabels:
        modelplane.ai/deployment: qwen-72b

Compare the A100 and the H100

Replicas are fleet-wide, not per-cluster: the deployment’s replicas: 2 means two complete serving instances, and because each pool holds a single 80GB GPU they land one on the A100 and one on the H100. Modelplane labels each replica’s endpoint with the cluster it runs on, so the service can split traffic between the platforms by weight. This service pairs the deployment label with each cluster label and gives each GPU half of the live traffic behind the same URL:

model-service-split.yaml
# Splits the service's traffic between the two platforms. Each entry pairs the
# deployment label with the cluster label Modelplane puts on every endpoint, and
# the weights fix each side's share of requests regardless of how many replicas
# match. 50/50 sends both GPUs the same workload; shift the ratio to favor one.
apiVersion: modelplane.ai/v1alpha1
kind: ModelService
metadata:
  name: qwen-72b
  namespace: ml-team
spec:
  endpoints:
  - weight: 50
    selector:
      matchLabels:
        modelplane.ai/deployment: qwen-72b
        modelplane.ai/cluster: aks-a100-single
  - weight: 50
    selector:
      matchLabels:
        modelplane.ai/deployment: qwen-72b
        modelplane.ai/cluster: nebius-h100-single

Both GPUs now serve the same workload, so their engine metrics give a direct performance comparison: scrape each replica’s latency and throughput as in Collecting engine metrics and read the two side by side. Weights are relative, so once one platform wins, shift the 50/50 toward it - 80/20, and as far as 100/0 - without touching the deployment.