{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://github.com/Dimitrios-Kafetzis/Distributed-Inference-Testbed/docs/schemas/scenario.schema.json",
  "title": "Experiment scenario",
  "description": "All parameters of an experiment (network conditions, compute load, model, partition strategy) in one object that NNPM versions and replays.",
  "type": "object",
  "additionalProperties": false,
  "required": ["name", "model"],
  "properties": {
    "schema_version": { "const": 1 },
    "name": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]*$" },
    "description": { "type": "string" },
    "pipeline": { "enum": ["experiment", "benchmarking"], "default": "experiment" },
    "runtime": { "type": "string", "default": "testbed", "description": "a registered runtime: testbed (default), headsplit, or a plug-in; its settings go in the block of the same name" },
    "model": { "type": "string", "minLength": 1 },
    "model_args": { "type": "object", "description": "adapter parameters, validated against the model's parameter schema (for example num_classes, seq_len)" },
    "input": {
      "type": "object",
      "additionalProperties": false,
      "description": "what the devices feed to the model; paths are on the device, relative to its data directory",
      "properties": {
        "source": { "enum": ["synthetic", "numpy_file", "image_file", "image_folder", "text_prompt"], "default": "synthetic" },
        "path": { "type": ["string", "null"], "description": "file (numpy_file, image_file) or directory (image_folder)" },
        "index": { "type": "integer", "minimum": 0, "default": 0, "description": "image_folder: which image of the sorted listing" },
        "text": { "type": ["string", "null"], "description": "text_prompt: the prompt" },
        "tokenizer": { "type": ["string", "null"], "description": "text_prompt: tokeniser checkpoint (directory or name)" }
      }
    },
    "checks": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "partition_equivalence": { "type": "boolean", "default": false, "description": "after profiling, every device compares each partition point with the unpartitioned model; a deviation above tolerance aborts the run" },
        "tolerance": { "type": "number", "exclusiveMinimum": 0, "default": 0.0001, "description": "maximum relative deviation" }
      }
    },
    "devices": {
      "anyOf": [
        { "type": "null" },
        { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true }
      ],
      "description": "End devices to use; null means every End Device in nodes.json"
    },
    "repetitions": { "type": "integer", "minimum": 1, "default": 1 },
    "warmup": { "type": "integer", "minimum": 0, "default": 1 },
    "seed": { "type": "integer", "minimum": 0, "default": 0 },
    "settle_s": { "type": "number", "minimum": 0, "default": 5 },
    "start_lead_s": { "type": "number", "minimum": 0, "default": 3 },
    "edge_server": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "policy": { "enum": ["RR", "SJF"], "default": "SJF" },
        "nominal_frequency": { "type": "boolean", "default": true },
        "profile_capacity": { "type": "boolean", "default": true },
        "port": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 5300 }
      }
    },
    "inference": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "default": false },
        "partition_points": { "type": "array", "items": { "type": "integer", "minimum": 0 } },
        "repetitions": { "type": "integer", "minimum": 1, "default": 1 }
      }
    },
    "network": {
      "type": "object",
      "description": "Shaping per node id; the key 'default' applies to every selected device",
      "additionalProperties": { "$ref": "#/$defs/network" }
    },
    "load": {
      "type": "object",
      "description": "Compute load per node id; the key 'default' applies to every selected device",
      "additionalProperties": { "$ref": "#/$defs/load" }
    },
    "heterogeneity": {
      "type": "object",
      "additionalProperties": { "$ref": "#/$defs/heterogeneity" }
    },
    "monitoring": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "default": true },
        "interval_s": { "type": "number", "exclusiveMinimum": 0, "default": 1.0 },
        "power": { "type": "boolean", "default": false },
        "power_interval_s": { "type": "number", "exclusiveMinimum": 0, "default": 0.5 },
        "latency_pings": { "type": "integer", "minimum": 1, "default": 10 },
        "throughput": { "type": "boolean", "default": false },
        "throughput_duration_s": { "type": "number", "exclusiveMinimum": 0, "default": 5 }
      }
    },
    "optimisation": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "default": true },
        "algorithm": { "type": "string", "default": "greedy" },
        "objective": { "enum": ["total_delay", "variance"], "default": "total_delay" },
        "initial_scheme": { "type": ["string", "null"], "description": "JSON list, {node: s} or 'NN%'" },
        "max_total_delays_ms": { "type": ["object", "array", "null"] },
        "transmission_mode": { "enum": ["size_over_rate", "rtt_only"], "default": "rtt_only" },
        "num_solutions": { "type": "integer", "minimum": 1, "default": 5 },
        "max_time_s": { "type": "number", "exclusiveMinimum": 0, "default": 300 }
      }
    },
    "headsplit": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "controller": { "type": "string", "description": "the device that runs the HeadSplit controller", "x-nnpm": { "widget": "node" } },
        "workers": { "type": "array", "items": { "type": "string" }, "description": "the devices that run the workers", "x-nnpm": { "widget": "nodes" } },
        "model": { "type": "string", "default": "TinyLlama/TinyLlama-1.1B-Chat-v1.0", "description": "Hugging Face model name (a checkpoint distributed to the devices, or the Hugging Face cache)" },
        "tokens": { "type": "integer", "minimum": 1, "default": 50 },
        "prompt_length": { "type": "integer", "minimum": 1, "default": 256 },
        "mode": { "enum": ["headsplit", "head_only", "layer_based", "all"], "default": "headsplit" },
        "profile": { "type": "string", "default": "artifacts/profile.json", "description": "speculation-accuracy profile (profile_offline.py) relative to the HeadSplit directory" },
        "prompt": { "type": ["string", "null"] },
        "worker_memory_bytes": { "type": "integer", "minimum": 1, "default": 3221225472 },
        "epsilon": { "type": "number", "exclusiveMinimum": 0, "default": 0.05 },
        "alpha_min": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.70 },
        "memory_replace_threshold": { "type": "number", "exclusiveMinimum": 0, "maximum": 1, "default": 0.85 },
        "rtt_margin": { "type": "number", "minimum": 1, "default": 1.15 },
        "timeout_s": { "type": "number", "exclusiveMinimum": 0, "default": 3600 },
        "extra_args": { "type": "array", "items": { "type": "string" }, "x-nnpm": { "widget": "hidden" } }
      },
      "required": ["controller", "workers"]
    },
    "sweep": {
      "type": "object",
      "additionalProperties": false,
      "required": ["path", "values"],
      "properties": {
        "path": { "type": "string", "description": "dotted path of the field to vary, e.g. network.default.added_rtt_ms" },
        "values": { "type": "array", "minItems": 1 }
      }
    },
    "sweep_origin": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "path", "value"],
      "description": "written by the sweep expansion into each variant: the sweep it came from",
      "properties": {
        "name": { "type": "string" },
        "path": { "type": "string" },
        "value": {}
      }
    }
  },
  "$defs": {
    "network": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "delay_ms": { "type": ["number", "null"], "minimum": 0 },
        "from_map": { "type": "boolean", "default": false, "description": "derive the delay from the marker positions" },
        "variation": {
          "type": "object",
          "additionalProperties": false,
          "required": ["mode"],
          "properties": {
            "mode": { "enum": ["constant", "uniform", "square"] },
            "min_ms": { "type": "number", "minimum": 0 },
            "max_ms": { "type": "number", "minimum": 0 },
            "period_s": { "type": "number", "exclusiveMinimum": 0 }
          }
        },
        "jitter_ms": { "type": ["number", "null"], "minimum": 0 },
        "loss_percent": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
        "rate_mbps": { "type": ["number", "null"], "exclusiveMinimum": 0 },
        "added_rtt_ms": { "type": ["number", "null"], "minimum": 0, "description": "extra round-trip time to the edge server (applied as egress delay)" },
        "background_traffic_mbps": { "type": ["number", "null"], "exclusiveMinimum": 0 },
        "destinations": { "type": "array", "items": { "type": "string" }, "description": "node ids for per-destination shaping; empty = all traffic" }
      }
    },
    "load": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "mode": { "enum": ["fixed", "continuous", "regime", "processes"], "default": "fixed" },
        "cpu_percent": { "type": "number", "minimum": 0, "maximum": 100 },
        "memory_percent": { "type": "number", "minimum": 0, "maximum": 95 },
        "cpu_range": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 },
        "memory_range": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 },
        "regime": { "enum": ["light", "moderate", "heavy"] },
        "processes": { "type": "integer", "minimum": 1 },
        "cores": { "type": "integer", "minimum": 1, "default": 1 },
        "cycle_s": { "type": "number", "exclusiveMinimum": 0, "default": 10 },
        "period_ms": { "type": "number", "exclusiveMinimum": 0, "default": 100 }
      }
    },
    "heterogeneity": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "max_freq_mhz": { "type": ["number", "null"], "exclusiveMinimum": 0 },
        "memory_limit_mb": { "type": ["number", "null"], "exclusiveMinimum": 0 },
        "threads": { "type": ["integer", "null"], "minimum": 1 }
      }
    }
  }
}
