Modeling and API Simulation

View as Markdown

Topograph models are YAML files used to simulate discovered topology without querying a real cloud API, NetQ instance, InfiniBand fabric, or Kubernetes cluster. They are primarily used by tests and local development, but they are also useful when validating a scheduler integration against known topology shapes.

A model describes the same canonical topology that real providers eventually produce:

  • A variable-depth switch tree, used for Slurm topology/tree output and Kubernetes fabric.topograph.run/tier-N labels
  • Node membership in an accelerator domain and optional sub-domain, used for block topology and the accelerator.topograph.run/domain and accelerator.topograph.run/sub-domain labels
  • Optional per-node labels and provider-specific annotations used by provider simulations

Model loading lives in pkg/models. Model fixtures live under tests/models/.

Where Models Are Used

Models are consumed in several simulation and local-development flows.

KWOK Clusters

kwok-nodes renders virtual Kubernetes Node objects from a model file as a plain YAML manifest. The helper script scripts/create-kind-kwok-cluster.sh then creates or reuses a local kind cluster, installs KWOK into it, and applies that manifest. This is useful when you want to run Topograph against a Kubernetes API without provisioning real nodes.

Prerequisites:

  • kind installed and available on PATH
  • kubectl installed and available on PATH
  • A Docker-compatible runtime supported by kind
  • Network access to GitHub releases when installing KWOK manifests

Build the manifest renderer:

$make build

Render a KWOK node manifest from one of the embedded model fixtures:

$bin/kwok-nodes -model medium.yaml -output /tmp/kwok-nodes.yaml

Create or reuse a kind cluster named topograph, install KWOK, and apply the generated nodes:

$scripts/create-kind-kwok-cluster.sh -m medium.yaml

Create or reuse a named kind cluster from an explicit model path, and keep the generated manifest:

$scripts/create-kind-kwok-cluster.sh \
> --cluster topo-demo \
> --model tests/models/nvl72.yaml \
> --output /tmp/nvl72-kwok-nodes.yaml \
> --gpus 8

To pass a kind cluster configuration file, add --kind-config path/to/kind.yaml. To pin KWOK installation to a release, add --kwok-release vX.Y.Z; otherwise the script uses GitHub’s latest KWOK release download URL.

The utility uses the model-derived instance-to-hostname mapping, so model hostname 1101 becomes Kubernetes node 1101 with:

  • topograph.run/instance: i-1101
  • topograph.run/region: <derived-region-or-none>
  • kwok.x-k8s.io/node=fake as both a label and annotation
  • Model-derived labels such as topology.kubernetes.io/region and topology.kubernetes.io/zone
  • Model-derived annotations such as accelerator.topology.test/domain

Generated Kubernetes node names come from model hostnames and are normalized to valid lowercase DNS names. For example, model hostname I21 becomes Kubernetes node i21, while its generated instance ID i-I21 is stored in topograph.run/instance.

The script applies the manifest with kubeconfig context kind-<cluster>, matching the context name created by kind create cluster --name=<cluster>.

Test Provider

The test provider simulates the Topograph API lifecycle itself. It can return successful topology output, delayed completion, malformed-request failures, provider failures, or a request that remains pending.

Use it when testing clients that call:

  • POST /v1/generate
  • GET /v1/topology?uid=<request-id>

For the complete API status-code simulation behavior, see Test Mode and Test Provider.

Provider Simulations

Several providers also have simulation variants, such as:

  • aws-sim
  • gcp-sim
  • oci-sim
  • nebius-sim
  • nscale-sim
  • lambdai-sim
  • dsx-sim

These providers load a model file and then simulate that provider’s API responses. This is useful when you want to exercise the normal provider translation logic without real provider credentials or infrastructure.

Simulation providers share these common parameters:

ParameterRequiredDescription
modelFileNameYesModel file to load. A basename such as medium.yaml is loaded from tests/models/; absolute and relative paths are also supported.
api_errorNoProvider-specific test hook used by unit tests to simulate API failures.
trimTiersNoNumber of topology tiers to trim where supported by the simulated provider.

Example request:

1{
2 "provider": {
3 "name": "aws-sim",
4 "params": {
5 "modelFileName": "medium.yaml"
6 }
7 },
8 "engine": {
9 "name": "slurm",
10 "params": {
11 "plugin": "topology/block"
12 }
13 }
14}

Model File Shape

A model usually has one required top-level section and one optional topology section:

1blocks:
2 - ...
3switches:
4 ...

switches is a map and blocks is a list. blocks[].nodes is where model files declare compute node names; it creates the node records, applies block labels and annotations, and optionally attaches those nodes to a leaf switch through blocks[].switch. switches may be omitted for block-only models.

Switches

The switches map describes the network hierarchy. Each key is the switch ID. Each value may contain:

FieldDescription
labelsLabels inherited by descendant nodes. Common keys are topology.kubernetes.io/region and topology.kubernetes.io/zone.
annotationsProvider-specific simulation metadata inherited by descendant nodes. Use accelerator.topology.test/domain for accelerator-domain membership and, where supported, accelerator.topology.test/sub-domain for a nested accelerator grouping.
switchesChild switch IDs.

Example:

1switches:
2 core:
3 labels:
4 topology.kubernetes.io/region: us-west
5 switches: [spine]
6 spine:
7 labels:
8 topology.kubernetes.io/zone: zone1
9 switches: [leaf1, leaf2]

Switch rules:

  • A switch can have at most one parent switch.
  • Empty leaf switches may be omitted from the switches map. A child switch named in a parent’s switches list is created automatically when it has no top-level definition.
  • A switch that defines labels, annotations, or child switches must have a top-level entry.
  • If a block names a switch with blocks[].switch, that block’s nodes are attached to the switch before switch validation runs.

Blocks

The blocks list describes sets of compute instances with similar hardware and connectivity characteristics. Each entry may contain:

FieldDescription
switchOptional leaf switch ID. When set, this block’s nodes are attached to that switch.
nodesRequired non-empty list of hostnames in this block. Compact ranges are supported. The model-backed test provider generates each instance ID by prefixing the hostname with i-.
labelsOptional node labels applied to nodes generated from this block.
annotationsOptional provider-specific simulation metadata applied to nodes generated from this block. Use accelerator.topology.test/domain to identify an accelerator domain.

Example:

1blocks:
2- switch: leaf1
3 nodes: ["n[1-2]"]
4 annotations:
5 accelerator.topology.test/domain: nvl1

Block rules:

  • The blocks section is the only place model files declare compute node names.
  • Each block entry must declare at least one node.
  • switch is optional. When set, it must reference a switch declared at the top level or named as a child in the switch hierarchy.
  • blocks[].nodes creates node entries automatically.

Compact Ranges

Model node lists support compact ranges:

1blocks:
2- nodes: ["n[1-4]", "gpu[001-004]", node9]

These expand to:

n1, n2, n3, n4, gpu001, gpu002, gpu003, gpu004, node9

Ranges are accepted in:

  • blocks[].nodes

Derived Data

After YAML parsing, Topograph completes the model before simulation uses it:

  • Compact ranges in Capacity block node ranges are expanded.
  • Block switch references attach block nodes to switches.
  • Switch names are copied from their map keys.
  • Nodes are created from blocks[].nodes.
  • Node NetLayers is derived from the switch path from leaf to root.
  • Node labels are built by merging labels from the switch path and block labels.
  • Node annotations are built by merging annotations from the switch path and block annotations.
  • Instances is derived from node names and grouped by labels.topology.kubernetes.io/region; nodes without a region use none.

These derived fields are not written in YAML.

Complete Examples

Blocks With Switches

This model creates nodes from block membership and attaches them to a leaf switch.

1switches:
2 core:
3 switches: [leaf]
4
5blocks:
6- switch: leaf
7 nodes: ["n[1-2]"]
8 annotations:
9 accelerator.topology.test/domain: nvl1
10- switch: leaf
11 nodes: [n3]
12 annotations:
13 accelerator.topology.test/domain: nvl2

After loading:

  • n1, n2, and n3 are hostnames mapped from instance IDs i-n1, i-n2, and i-n3
  • n1 and n2 belong to the first block and have the accelerator.topology.test/domain: nvl1 annotation
  • n3 belongs to the second block and has the accelerator.topology.test/domain: nvl2 annotation
  • All three nodes have network layers [leaf, core]

Blocks Without Switches

This model omits switches. Nodes are still created, block metadata is still applied, and generated instances have no network layers.

1blocks:
2- nodes: ["n[1-2]"]
3 annotations:
4 accelerator.topology.test/domain: nvl1

After loading:

  • n1 and n2 belong to the first block
  • n1 and n2 have the accelerator.topology.test/domain: nvl1 annotation
  • n1 and n2 have no network layers

Simulating the API

To simulate the Topograph API lifecycle, configure the test provider:

1http:
2 port: 49021
3 ssl: false
4
5provider: test
6engine: slurm
7
8requestAggregationDelay: 2s

Then submit a request that names a model:

1{
2 "provider": {
3 "name": "test",
4 "params": {
5 "generateResponseCode": 202,
6 "topologyResponseCode": 200,
7 "modelFileName": "small-tree.yaml"
8 }
9 },
10 "engine": {
11 "name": "slurm"
12 }
13}

Expected flow:

  1. POST /v1/generate returns 202 Accepted and a request ID.
  2. GET /v1/topology?uid=<request-id> returns 202 Accepted while the request is queued or processing.
  3. When processing completes, /v1/topology returns 200 OK with the selected engine output.

To simulate API failures, set generateResponseCode, topologyResponseCode, and errorMessage in provider.params. For example:

1{
2 "provider": {
3 "name": "test",
4 "params": {
5 "generateResponseCode": 202,
6 "topologyResponseCode": 500,
7 "errorMessage": "simulated provider failure"
8 }
9 },
10 "engine": {
11 "name": "slurm"
12 }
13}

Choosing the Right Simulation Path

Use the test provider when you want to validate API-client behavior:

  • Request IDs
  • Polling
  • Pending responses
  • Error status codes
  • Retry behavior

Use a *-sim provider when you want to validate provider-specific topology translation:

  • AWS, GCP, OCI, Nebius, Nscale, Lambda AI, or DSX topology paths
  • Pagination behavior in simulated provider APIs
  • Engine output generated from provider-shaped data
  • Tree and block topology output from the same model

Validation Checklist

Before using a new model in a regression test:

  • Confirm every switch child has only one parent.
  • Confirm every block switch reference points at an existing switch.
  • Confirm no node appears under two blocks.
  • Run the relevant provider simulation test or API flow with the target engine.