diff --git a/README.md b/README.md index 13ca5c5e..28cddcf7 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,13 @@ bash run-experiment.sh path-to-experiment/script.sh A folder will be created under `path-to-experiment/results` containing the result of running the experiment. +### Configuration format + +A model's configuration can be written as a `.conf` (the legacy format) or as a +YAML/JSON file — both are supported, chosen per file by extension. See +[Running with a YAML config](doc/dev/yaml-config.md) for the YAML format and +worked examples. + ## Features CODES provides comprehensive simulation capabilities for: diff --git a/doc/dev/yaml-config.md b/doc/dev/yaml-config.md new file mode 100644 index 00000000..1be0864c --- /dev/null +++ b/doc/dev/yaml-config.md @@ -0,0 +1,774 @@ +# Running with a YAML config + +CODES accepts a YAML (or JSON) configuration anywhere it accepts a legacy +`.conf` today. Both formats are supported side by side — the format is chosen per +file, by extension: + +- `.yaml`, `.yml`, `.json` → the YAML front-end +- anything else → the legacy `.conf` text parser + +Point an existing executable at a YAML file exactly as you would a `.conf` — the +config path is the trailing positional argument, after ROSS's `--`: + +```bash +./model-net-synthetic --sync=1 --num_messages=1 -- my-network.yaml +``` + +Nothing else changes: the YAML front-end compiles the friendly format down to the +same internal configuration the `.conf` parser produces, so every model reads it +unchanged. A YAML config and its `.conf` twin drive a model to identical results. + +The YAML front-end is always available — RapidYAML is vendored in-tree +(`thirdparty/rapidyaml/`) and built unconditionally, so there is nothing to +enable and no configure-time option to set. + +## Shape of a config + +A config has up to five top-level blocks: + +```yaml +schema_version: 1 # required; this build understands version 1 +components: # named component configs referenced by the topology +topology: # flat network, parametric fabric, or explicit LP groups +sections: # config a model reads directly, carried through verbatim +include: # other config files to reuse (base; this file overrides) +``` + +- **`schema_version`** is required, an integer, and must be a version this build + knows (currently `1`). A newer, unknown version is a hard error rather than a + best-effort guess. +- **`components`** is a map of name → component. A **component** pairs a model + (`model:`) with its parameters; a flat-network component also names the NIC + model it runs over (`network:`). Components are referenced by name from the + topology. The key `type:` is reserved for a future schema version and is + rejected rather than passed through as a model param. +- **`topology`** selects the layout, via one of three `format`s: `flat` is an + all-to-all point-to-point network described by a component and a node count; + `parametric` is an HPC fabric described by shape parameters; `groups` lays the + LP groups out directly (the escape hatch for configs that aren't a single + network). Each is documented in its own section below. +- **`sections`** carries config a model reads directly by name (DIRECTOR, + NETWORK_SURROGATE, resource, …) through verbatim — see `sections:` below. +- **`include`** reuses other config files — see `include:` below. + +Validation is strict: unknown top-level keys, unknown topology keys, a block a +component or fabric doesn't consume, and (for flat topologies) an unexpected +`edges`/graph block are all **errors, not silent drops**. A malformed value +(e.g. a non-integer node count) is likewise rejected with a diagnostic. On any +such error the run aborts at config load with that diagnostic — on every rank, +since every rank compiles the same bytes deterministically. + +### How it compiles + +The compiler walks the friendly form and emits the `LPGROUPS` (LP layout) and +`PARAMS` (model knobs) the models already read: + +- a flat network becomes one repetition per node — a `[workload, NIC]` LP pair; +- a parametric fabric derives its repetition / per-router / per-group counts from + the `shape` (the same arithmetic the model does internally) and lays out the + `[workload, terminal, router]` LPs; +- any scalar key the compiler doesn't special-case is **passed through verbatim** + to `PARAMS`, so advanced model knobs need no compiler change. + +--- + +# Flat networks + +The flat point-to-point network models — `simplenet`, `simplep2p`, and `loggp` — +are described as a single **component** plus a **node count**. The component +bundles the workload model (`model:`) with the NIC model it runs over +(`network:`), and carries the NIC's parameters directly. The topology supplies +only how many peer nodes to instantiate: + +```yaml +topology: + format: flat + component: + nodes: +``` + +`nodes` becomes the number of compute-node slots (repetitions). All flat models +are uniform all-to-all — there is no per-node or per-edge form in a flat config; +a model whose links vary per pair reads them from its own file (see `simplep2p` +and `loggp` below). + +## simplenet + +A uniform all-to-all network with a single startup latency and bandwidth. No +external file — the rates sit directly on the component. + +Component keys: `net_startup_ns`, `net_bw_mbps`, plus the usual `packet_size`, +`message_size`, `modelnet_scheduler`. + +```yaml +schema_version: 1 + +components: + compute_node: + model: nw-lp # the workload + network: simplenet # the NIC model the workload runs over + packet_size: 512 + message_size: 464 + modelnet_scheduler: fcfs + net_startup_ns: 1.5 + net_bw_mbps: 20000 + +topology: + format: flat + component: compute_node + nodes: 16 +``` + +## simplep2p + +Point-to-point with a per-pair latency/bandwidth matrix. The matrices come from +the existing files, referenced by path on the component (`net_latency_ns_file`, +`net_bw_mbps_file`); the model resolves them relative to the config file's +directory, so a bare name sits next to the config. + +```yaml +schema_version: 1 + +components: + compute_node: + model: nw-lp + network: simplep2p + message_size: 464 + packet_size: 1024 + modelnet_scheduler: fcfs + net_latency_ns_file: modelnet-test-latency.conf + net_bw_mbps_file: modelnet-test-bw.conf + +topology: + format: flat + component: compute_node + nodes: 3 +``` + +## loggp + +The LogGP point-to-point model. Its parameter table (`G`, latency, etc.) comes +from a network-config file referenced by path (`net_config_file`), resolved +relative to the config file's directory like simplep2p's matrices. + +```yaml +schema_version: 1 + +components: + compute_node: + model: nw-lp + network: loggp + message_size: 464 + modelnet_scheduler: fcfs-full + net_config_file: ng-mpi-tukey.dat + +topology: + format: flat + component: compute_node + nodes: 16 +``` + +--- + +# Parametric fabrics (HPC networks) + +HPC fabrics are not drawn node by node — their connectivity follows from a +handful of shape parameters. They use `format: parametric` and a `fabric` block, +with a `hosts.component` naming the per-terminal workload: + +```yaml +topology: + format: parametric + fabric: + model: + shape: { ... } # size knobs; the compiler derives LP counts from these + links: { ... } # optional per-link-class bandwidth / vc_size sugar + routing: { ... } # routing.algorithm -> the model's "routing" param + connections:{ ... } # file-enumerated fabrics only: intra/inter wiring files + # any other scalar key here passes straight through to PARAMS + hosts: + component: # the workload on every terminal +``` + +Building blocks shared by all fabrics: + +- **`shape`** — the size knobs specific to each model (below). The compiler runs + the same shape→counts arithmetic the model does, so you set the small, + meaningful numbers and the repetition/group counts follow. +- **`links`** — optional sugar for the `local` / `global` / `cn` link-class + pattern: `local: { bandwidth: 5.25, vc_size: 4096 }` expands to + `local_bandwidth=5.25` and `local_vc_size=4096`. Models that name their link + parameters flat (e.g. `torus`'s `link_bandwidth`) just set those keys directly + instead — anything not recognized is passed through verbatim. +- **`routing`** — `routing.algorithm: minimal` becomes the model's `routing` + param; any other `routing.*` key passes through under its own name. +- **`connections`** — file-enumerated fabrics only: `intra`/`inter` name the + binary wiring files (`intra-group-connections` / `inter-group-connections`). +- **pass-through** — any remaining scalar key on the `fabric` (e.g. + `packet_size`, `chunk_size`, `message_size`, `modelnet_scheduler`) lands in + `PARAMS` unchanged. A list value (e.g. `slimfly`'s generator sets) is emitted + as a multi-value key. + +The `hosts.component`'s own scalar params, if it declares any, are also appended +to `PARAMS` (after the fabric's keys). A parametric host component must not set +`network:` — the fabric defines the network itself, so that key is rejected here. + +Supported fabric `model`s split into **internally-generated** (the compiler emits +only shape params and the model generates its wiring) and **file-enumerated** (the +wiring is read from binary connection files produced by the generator scripts). + +## Internally-generated fabrics + +### dragonfly + +Regular (Kim–Dally) dragonfly. The whole layout follows from one number, +`num_routers` (routers per group): the model uses `num_cn = num_routers / 2` +terminals per router and `num_groups = num_routers * num_cn + 1`. + +Shape: `num_routers`. + +```yaml +schema_version: 1 + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly + shape: + num_routers: 8 # routers per group; the rest of the layout follows + links: + local: { bandwidth: 5.25, vc_size: 4096 } + global: { bandwidth: 4.7, vc_size: 8192 } + cn: { bandwidth: 5.25, vc_size: 4096 } + routing: + algorithm: adaptive + packet_size: 512 + chunk_size: 32 + num_vcs: 1 + modelnet_scheduler: fcfs + message_size: 512 + hosts: + component: compute_host +``` + +### torus + +An n-dimensional torus. It folds routing into the terminal node, so there is +**no separate router LP**; the node count is the product of the per-dimension +lengths. `dim_length` is a comma-separated string the model parses itself, so it +is written as a quoted scalar (not a YAML list) — a strictly comma-separated list +of positive integers whose count must equal `n_dims` (a mismatch, an empty +segment, or a trailing comma is a config error). Bandwidth is a single flat +`link_bandwidth`, not a per-link-class block. + +Shape: `n_dims`, `dim_length` (repetitions = product of `dim_length`, one entry +per `n_dims`). + +```yaml +schema_version: 1 + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: torus + shape: + n_dims: 3 + dim_length: "4,2,2" # node count = 4*2*2 = 16 + packet_size: 512 + chunk_size: 256 + message_size: 464 + modelnet_scheduler: fcfs + link_bandwidth: 2.0 + buffer_size: 4096 + num_vc: 1 + hosts: + component: compute_host +``` + +### slimfly + +An MMS (McKay–Miller–Širáň) slimfly built on `num_routers`: the two Cayley +subgraphs give `2 * num_routers^2` routers total, each hosting `num_terminals` +terminals. The generator sets are list-valued, written as YAML sequences and +emitted as multi-value params (`generator_set_X=("1","4")`). + +Shape: `num_routers`, `num_terminals` (repetitions = `2 * num_routers^2`). + +```yaml +schema_version: 1 + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: slimfly + shape: + num_routers: 5 # -> 2 * 5^2 = 50 routers + num_terminals: 3 + links: + local: { bandwidth: 9.0, vc_size: 25600 } + global: { bandwidth: 9.0, vc_size: 25600 } + cn: { bandwidth: 9.0, vc_size: 25600 } + routing: + algorithm: minimal + generator_set_X: [1, 4] + generator_set_X_prime: [2, 3] + packet_size: 512 + chunk_size: 256 + message_size: 464 + modelnet_scheduler: fcfs + num_vcs: 4 + global_channels: 5 + local_channels: 2 + link_delay: 0 + hosts: + component: compute_host +``` + +### express-mesh + +An n-dimensional express mesh with a separate router LP. The router count is the +product of the per-dimension lengths, and each router hosts `num_cn` terminals. +Like torus it uses flat bandwidth keys (`link_bandwidth`, `cn_bandwidth`, ...) +rather than a `links` block, and `dim_length` is a quoted scalar — a strictly +comma-separated list of positive integers whose count must equal `n_dims`. + +Shape: `n_dims`, `dim_length` (router count = product of `dim_length`, one entry +per `n_dims`), `num_cn`. + +```yaml +schema_version: 1 + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: express-mesh + shape: + n_dims: 3 + dim_length: "4,4,4" # router count = 4*4*4 = 64 + num_cn: 3 # terminals per router + routing: + algorithm: static + message_size: 512 + packet_size: 4096 + chunk_size: 4096 + modelnet_scheduler: round-robin + gap: 1 + num_vcs: 1 + link_bandwidth: 12.5 + cn_bandwidth: 12.5 + vc_size: 65536 + cn_vc_size: 65536 + soft_delay: 0 + router_delay: 90 + hosts: + component: compute_host +``` + +### fattree + +A multi-level fat-tree. One repetition per edge switch, each hosting +`switch_radix / 2` terminals, with one switch LP per level. `switch_radix` must +be even — an odd radix is a config error rather than a silent truncation of the +terminal split. The fattree switch is not a separate model-net method, so it +does not appear in `modelnet_order`. Bandwidth is set with flat keys +(`link_bandwidth`, `cn_bandwidth`). + +Shape: `num_levels`, `switch_count`, `switch_radix` +(repetitions = `switch_count`, terminals per switch = `switch_radix / 2`). + +```yaml +schema_version: 1 + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: fattree + shape: + num_levels: 3 + switch_count: 32 + switch_radix: 8 + routing: + algorithm: adaptive + ft_type: 0 + packet_size: 512 + message_size: 512 + chunk_size: 512 + modelnet_scheduler: fcfs + router_delay: 90 + terminal_radix: 1 + soft_delay: 1000 + vc_size: 65536 + cn_vc_size: 65536 + link_bandwidth: 12.5 + cn_bandwidth: 12.5 + rail_routing: adaptive + hosts: + component: compute_host +``` + +## File-enumerated fabrics + +These fabrics read their wiring from binary connection files produced by the +existing generator scripts. Add a `connections` block naming the `intra`/`inter` +files by path; the model reads them unchanged. For a file-enumerated fabric the +`shape` counts are **inputs that must stay consistent with the connection +files** — they are not free to choose independently of the wiring. Every shape +key listed below is required unless marked optional — including +`num_global_channels`, which plays no part in the LP counts but must match the +wiring: left out, the model would fall back to a default (10) with only a +warning, so the compiler demands it up front instead. + +```yaml + connections: + intra: /path/to/-intra + inter: /path/to/-inter +``` + +Paths are resolved by the model as given (relative to the run directory), so +tests use absolute paths (`@CMAKE_SOURCE_DIR@` substituted by CMake) to stay +independent of where the run happens; the `.yaml.in` twins in the tree show the +pattern. + +### dragonfly-dally + +Total routers = `num_groups * num_planes * num_routers` (with `num_planes` +defaulting to 1); each router hosts `num_cns_per_router` terminals. + +Shape: `num_routers`, `num_groups`, `num_cns_per_router`, `num_global_channels`, +optional `num_planes`. + +```yaml +schema_version: 1 + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-dally + shape: + num_routers: 4 + num_groups: 9 + num_cns_per_router: 2 + num_global_channels: 2 + links: + local: { bandwidth: 2.0, vc_size: 16384 } + global: { bandwidth: 2.0, vc_size: 16384 } + cn: { bandwidth: 2.0, vc_size: 32768 } + routing: + algorithm: minimal + connections: + intra: /abs/path/conf/dragonfly-dally/dfdally-72-intra + inter: /abs/path/conf/dragonfly-dally/dfdally-72-inter + packet_size: 4096 + chunk_size: 4096 + message_size: 736 + modelnet_scheduler: fcfs + minimal-bias: 1 + df-dally-vc: 1 + hosts: + component: compute_host +``` + +### dragonfly-plus + +One repetition per group. Each group's routers split into a spine and a leaf +level (`num_router_spine + num_router_leaf` router LPs), and only the leaf +routers host terminals (`num_cns_per_router` each), so terminals per group = +`num_router_leaf * num_cns_per_router`. + +Shape: `num_router_spine`, `num_router_leaf`, `num_groups`, `num_cns_per_router`. + +```yaml +schema_version: 1 + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-plus + shape: + num_router_spine: 4 + num_router_leaf: 4 + num_groups: 5 + num_cns_per_router: 4 + links: + local: { bandwidth: 5.25, vc_size: 8192 } + global: { bandwidth: 1.5, vc_size: 16384 } + cn: { bandwidth: 8.0, vc_size: 8192 } + routing: + algorithm: prog-adaptive + connections: + intra: /abs/path/conf/dragonfly-plus/dfp-test-intra + inter: /abs/path/conf/dragonfly-plus/dfp-test-inter + packet_size: 1024 + chunk_size: 1024 + message_size: 608 + modelnet_scheduler: fcfs + num_level_chans: 1 + num_global_connections: 4 + route_scoring_metric: delta + hosts: + component: compute_host +``` + +### dragonfly-custom + +Each group is a `num_router_rows × num_router_cols` mesh of routers, so total +routers = `num_groups * num_router_rows * num_router_cols`; each router hosts +`num_cns_per_router` terminals. + +Shape: `num_router_rows`, `num_router_cols`, `num_groups`, `num_cns_per_router`, +`num_global_channels`. + +```yaml +schema_version: 1 + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-custom + shape: + num_router_rows: 6 + num_router_cols: 16 + num_groups: 8 + num_cns_per_router: 4 + num_global_channels: 4 + links: + local: { bandwidth: 12.5, vc_size: 65536 } + global: { bandwidth: 12.5, vc_size: 65536 } + cn: { bandwidth: 12.5, vc_size: 65536 } + routing: + algorithm: adaptive + connections: + intra: /abs/path/scripts/dragonfly-custom/example/intra-theta-8group + inter: /abs/path/scripts/dragonfly-custom/example/inter-theta-8group + packet_size: 4096 + message_size: 656 + chunk_size: 4096 + modelnet_scheduler: fcfs + router_delay: 90 + hosts: + component: compute_host +``` + +--- + +# Explicit LP groups: `format: groups` + +The flat and parametric forms above each describe a single network. Some configs +aren't a single network at all — a storage cluster, a mapping test, several +partitions side by side — and there is nothing for the compiler to derive. For +those, lay the LP groups out directly with `format: groups`: + +```yaml +schema_version: 1 + +topology: + format: groups + params: # -> PARAMS, written out verbatim + message_size: 512 + groups: # -> LPGROUPS, one entry per group + TRITON_GRP: + repetitions: 1 + lps: # LP type -> count within each repetition + nw-lp: 1 + lsm: 1 +``` + +Each group names its `repetitions` and, under `lps`, the LP types with their +per-repetition counts — a direct, validated transcription of a `.conf` +`LPGROUPS`. There is no network to derive from, so `modelnet_order` and any other +knobs come from whatever you put in `params`. `params:` follows the same +scalar/list/nested-map rules as a `sections:` block (below): a scalar becomes a +single-value key, a list a multi-value key, and a nested map a subsection. Group +and LP-type names are free-form (they match what each model registers). + +**Annotations.** `codes_mapping` lets the same LP type appear more than once in a +group under different annotations. Write the annotation on the LP-type key as +`type@annotation`: + +```yaml + lps: + a: 1 + a@foo: 1 # LP type "a", annotation "foo" -- a distinct entry from "a" +``` + +Combine this with `sections:` (below) for a model that also reads its own config +section — e.g. a storage model's `lsm` or `resource` block. +`tests/conf/lsm-test.yaml` and `tests/conf/buffer_test.yaml` are full twins doing +exactly that. + +--- + +# Config a model reads directly: `sections:` + +Not every subsystem's config is topology the compiler derives. Many read their +own named section straight from the config — the storage model reads `resource`, +the surrogate/director stack reads `NETWORK_SURROGATE`, `APPLICATION_SURROGATE`, +and `DIRECTOR`, and so on. Those keys aren't transformed, just read, so the YAML +front-end carries them through **verbatim** under a top-level `sections:` block: + +```yaml +schema_version: 1 +topology: { ... } # friendly, derived, strictly validated + +sections: # emitted verbatim as top-level config sections + resource: + available: 8192 + network_surrogate: + enable: 1 + fixed_switch_timestamps: [25.0e6, 400.0e6] # a list -> a multi-value key + torch_jit: # a nested block -> a subsection + mode: single-static-model-for-all-terminals +``` + +Inside a section, a scalar becomes a single-value key, a **list** becomes a +multi-value key (`("a","b")`), and a **nested map** becomes a subsection. There +is no fixed schema — a section can carry whatever keys the model reads. + +**Section names are case-insensitive.** The all-caps convention (`PARAMS`, +`DIRECTOR`, `NETWORK_SURROGATE`) is a historical carryover; write `resource` or +`network_surrogate` (or any case) and the model finds it. **Keys inside a section +stay case-sensitive** — a model reads them by exact name, so `available` and +`Available` are different keys. `LPGROUPS` and `PARAMS` are reserved: the compiler +emits them from the topology, so they can't appear under `sections:` (put model +parameters on the component or fabric instead). + +## Adding a config section + +When a new feature needs configuration, decide which of two tiers it belongs to. +The test is one question: **does the compiler need to derive, rename, or +cross-check anything?** + +**Tier 1 — the model reads the keys as-is (most sections).** No compiler change: + +1. read your keys in the model with `configuration_get_value(&config, + "my_section", ...)`; +2. put them under `sections:` in the YAML; +3. document the section and its keys here. + +Optionally, register an **open schema** to enforce required keys while still +allowing anything else through — handy for a section that's still in flux, where +you know a couple of keys are mandatory but the rest are unsettled. Add a row to +`section_schemas[]` in `src/modelconfig/config_compiler.cxx`: + +```cpp +const char* const my_required[] = {"must_have", nullptr}; +const section_schema section_schemas[] = { + {"resource", resource_required}, + {"my_section", my_required}, // required keys checked; unlisted keys pass through +}; +``` + +A missing required key becomes a `config_error` up front (a clearer, earlier +message than a mid-run abort). A section with no registered schema passes through +with no validation at all. + +**Tier 2 — the compiler derives or transforms the config (topology, and later +jobs).** This earns a first-class module: a parser, a compiler that emits the +derived sections, a registry entry, and a `.conf`-equivalence test — the pattern +the `fabric_models[]` table and `compile_fabric()` already follow. Reach for this +only when the compiler is doing real work (shape→counts, friendly→internal names, +cross-section consistency); otherwise Tier 1 is the answer. + +The compiler core is ROSS-free and unit-tested in isolation +(`tests/codes-config-compiler-test.cxx`); add cases there for whatever a new +section or tier introduces. + +--- + +# Reusing config across files: `include:` + +A config can pull in other files with a top-level `include:` — a filename, or a +list of them, resolved relative to the including file's directory: + +```yaml +schema_version: 1 +include: [ common.yaml, sections/storage.yaml ] +topology: { ... } # this file's own keys +``` + +Included files are the **base**; the including file **overrides** them. So you +can factor out a shared piece and vary the rest — define a network once and run +it under different configs, or (as in `lsm-test-include.yaml`) share a model +section across layouts. Merge rules: + +- `components:` and `sections:` merge **by name** — an included set plus your own + additions, with a local entry of the same name winning. +- `topology:` is singular — a local topology replaces the included one, and with + no local one the included topology stands. +- `schema_version:` belongs to the top-level file — the one passed on the + command line — and is required only there. A fragment can simply omit it and + compiles under the top-level file's version; keep reusable fragments + version-free so a version bump never touches them. If a fragment does state + one, it is cross-checked — a version this build doesn't understand is an + error wherever it appears. +- Multiple includes apply in list order; the local file is applied last. + +Includes are resolved when the config is loaded, *before* compilation. The +loader reads each included file with the **same collective MPI read as the main +config** — one collective read per file across the job, not one open per rank — +so the referenced files must be reachable at the same path from every node, and +a missing or unreadable include aborts the whole job with a diagnostic naming +the resolved path. An included file may not itself use `include:` (one level +deep, for now). The pure compiler core does no file I/O — the loader reads the +fragments and hands their bytes in — so `include:` is a loader feature a model +never sees. + +--- + +## Worked examples in the tree + +Each of these YAML files is a twin of the `.conf` beside it, checked in CI to +produce identical results — the authoritative, runnable reference for each model: + +| Model | Example | +|-------|---------| +| simplenet | `tests/conf/modelnet-test-simplenet.yaml` | +| simplep2p | `tests/conf/modelnet-test-simplep2p.yaml` | +| loggp | `tests/conf/modelnet-test-loggp.yaml` | +| dragonfly | `src/network-workloads/conf/modelnet-synthetic-dragonfly.yaml` | +| torus | `tests/conf/modelnet-test-torus.yaml` | +| slimfly | `tests/conf/modelnet-test-slimfly.yaml` | +| express-mesh | `tests/conf/modelnet-test-em.yaml` | +| fattree | `src/network-workloads/conf/modelnet-synthetic-fattree.yaml` | +| dragonfly-dally | `tests/conf/dragonfly-dally/dfdally-72.yaml.in` | +| dragonfly-plus | `tests/conf/dragonfly-plus/dfp-test.yaml.in` | +| dragonfly-custom| `tests/conf/dragonfly-custom/dfcustom-8group.yaml.in` | +| explicit groups (storage/lsm) | `tests/conf/lsm-test.yaml` | +| explicit groups (resource) | `tests/conf/buffer_test.yaml` | +| `include:` composition | `tests/conf/lsm-test-include.yaml` (+ `lsm-workload.yaml`) | + +The `.yaml.in` files are CMake templates (the `@CMAKE_SOURCE_DIR@` in their +connection paths is substituted at configure time); the plain `.yaml` files are +ready to run as-is. diff --git a/doc/example/CMakeLists.txt b/doc/example/CMakeLists.txt index 1b0c5605..ffea52b3 100644 --- a/doc/example/CMakeLists.txt +++ b/doc/example/CMakeLists.txt @@ -11,6 +11,10 @@ endforeach() # Saving default config files to run experiments with configure_file(tutorial-ping-pong.conf.in tutorial-ping-pong.template.conf.in @ONLY) configure_file(tutorial-ping-pong-surrogate.conf.in tutorial-ping-pong-surrogate.template.conf.in @ONLY) +# YAML twins of the templates above (@ONLY so @CMAKE_SOURCE_DIR@ is substituted but +# the ${VAR} tokens stay for envsubst, exactly like the .conf templates). +configure_file(tutorial-ping-pong.yaml.in tutorial-ping-pong.template.yaml.in @ONLY) +configure_file(tutorial-ping-pong-surrogate.yaml.in tutorial-ping-pong-surrogate.template.yaml.in @ONLY) configure_file(kb.dfdally-72-zeromq-director.conf.in kb.dfdally-72-zeromq-director.template.conf.in @ONLY) configure_file(kb.dfdally-72-event-time-director.conf.in kb.dfdally-72-event-time-director.template.conf.in @ONLY) configure_file(kb.dfdally-72-milc-small.workload.conf.in kb.dfdally-72-milc-small.workload.template.conf.in @ONLY) @@ -39,6 +43,9 @@ set(SURROGATE_ENABLED "1") set(TRAINING_ENABLED "1") configure_file(tutorial-ping-pong.conf.in tutorial-ping-pong.conf) configure_file(tutorial-ping-pong-surrogate.conf.in tutorial-ping-pong-surrogate.conf) +# YAML twins, configured for direct build-dir use (all ${VAR} tokens substituted). +configure_file(tutorial-ping-pong.yaml.in tutorial-ping-pong.yaml) +configure_file(tutorial-ping-pong-surrogate.yaml.in tutorial-ping-pong-surrogate.yaml) configure_file(kb.dfdally-72-zeromq-director.conf.in kb.dfdally-72-zeromq-director.conf) configure_file(kb.dfdally-72-event-time-director.conf.in kb.dfdally-72-event-time-director.conf) configure_file(kb.dfdally-72-milc-small.workload.conf.in kb.dfdally-72-milc-small.workload.conf) diff --git a/doc/example/example.yaml b/doc/example/example.yaml new file mode 100644 index 00000000..0287a086 --- /dev/null +++ b/doc/example/example.yaml @@ -0,0 +1,29 @@ +schema_version: 1 + +# YAML twin of example.conf: 16 servers (nw-lp), each point-to-point connected via +# a simplenet NIC. The group name (SERVERS) and the custom server_pings section +# are not what a flat topology would emit, so the layout is written directly with +# the explicit-groups form; PARAMS carried through verbatim, and the model's own +# server_pings section carried through under sections:. + +topology: + format: groups + params: + message_size: 432 + pe_mem_factor: 512 + packet_size: 512 + modelnet_order: [simplenet] + modelnet_scheduler: fcfs + net_startup_ns: 1.5 + net_bw_mbps: 20000 + groups: + SERVERS: + repetitions: 16 + lps: + nw-lp: 1 + modelnet_simplenet: 1 + +sections: + server_pings: + num_reqs: 5 + payload_sz: 4096 diff --git a/doc/example/kb.dfdally-72-event-time-director.yaml.in b/doc/example/kb.dfdally-72-event-time-director.yaml.in new file mode 100644 index 00000000..90dcbb1f --- /dev/null +++ b/doc/example/kb.dfdally-72-event-time-director.yaml.in @@ -0,0 +1,78 @@ +schema_version: 1 + +# YAML twin of kb.dfdally-72-event-time-director.conf.in: a 72-terminal +# dragonfly-dally driven by the ZeroMQ director. The group carries an extra +# director workload LP (dir-nw-lp) alongside nw-lp, which is not the single- +# workload-per-terminal shape the parametric fabric derives, so the layout is +# written directly with the explicit-groups form. The director reads its own +# DIRECTOR section, carried through verbatim (the ${...} tokens are substituted by +# the run environment, and @CMAKE_SOURCE_DIR@ by CMake). + +topology: + format: groups + params: + message_size: 824 + packet_size: 4096 + chunk_size: 64 + num_routers: 4 + num_groups: 9 + num_row_chans: 1 + num_col_chans: 1 + num_cns_per_router: 2 + num_global_channels: 2 + cn_bandwidth: 5.25 + local_bandwidth: 5.25 + global_bandwidth: 5.25 + cn_vc_size: 32768 + local_vc_size: 16384 + global_vc_size: 16384 + cn_delay: 10 + local_delay: 10 + global_delay: 100 + router_delay: 300 + nic_seq_delay: 0 + cn_credit_delay: 10 + local_credit_delay: 10 + global_credit_delay: 100 + num_qos_levels: 1 + bw_reset_window: 250000.0 + max_qos_monitor: 5500000000 + qos_bucket_max: 10 + qos_min_bws: "5,30,20,5" + qos_max_bws: "10,80,60,20" + routing: prog-adaptive + adaptive_threshold: 2560 + route_scoring_metric: alpha + route_scoring_factors: 2 + route_scoring_factors_local_intm: 2 + route_scoring_factors_local_dest: 2 + modelnet_order: [dragonfly_dally, dragonfly_dally_router] + modelnet_scheduler: round-robin + intra-group-connections: "@CMAKE_SOURCE_DIR@/src/network-workloads/conf/dragonfly-dally/dfdally-72-intra" + inter-group-connections: "@CMAKE_SOURCE_DIR@/src/network-workloads/conf/dragonfly-dally/dfdally-72-inter" + groups: + MODELNET_GRP: + repetitions: 36 + lps: + nw-lp: 2 + dir-nw-lp: 2 + modelnet_dragonfly_dally: 2 + modelnet_dragonfly_dally_router: 1 + +sections: + DIRECTOR: + surrogate_backend: "${SURROGATE_BACKEND}" + surrogate_family: "${SURROGATE_FAMILY}" + start_iter: "${START_ITER}" + end_iter: "${END_ITER}" + retrain_enabled: "${RETRAIN_ENABLED}" + retrain_iter: "${RETRAIN_ITER}" + retrain_save_path: "${RETRAIN_SAVE_PATH}" + second_surrogate_enabled: "${SECOND_SURROGATE_ENABLED}" + second_start_iter: "${SECOND_START_ITER}" + second_end_iter: "${SECOND_END_ITER}" + inferencing_enabled: "${INFERENCING_ENABLED}" + surrogate_enabled: "${SURROGATE_ENABLED}" + training_enabled: "${TRAINING_ENABLED}" + debug_prints: "${DIRECTOR_DEBUG_PRINTS}" + shutdown_zmqml_server_on_finalize: "${SHUTDOWN_ZMQML_SERVER_ON_FINALIZE}" diff --git a/doc/example/kb.dfdally-72-zeromq-director.yaml.in b/doc/example/kb.dfdally-72-zeromq-director.yaml.in new file mode 100644 index 00000000..d7a631a9 --- /dev/null +++ b/doc/example/kb.dfdally-72-zeromq-director.yaml.in @@ -0,0 +1,75 @@ +schema_version: 1 + +# YAML twin of kb.dfdally-72-zeromq-director.conf.in: a 72-terminal dragonfly-dally +# driven by the ZeroMQ director. The group carries an extra director workload LP +# (dir-nw-lp) alongside nw-lp, so the layout is written directly with the +# explicit-groups form. The director reads its own DIRECTOR section, carried +# through verbatim (the ${...} tokens are substituted by the run environment, and +# @CMAKE_SOURCE_DIR@ by CMake). + +topology: + format: groups + params: + message_size: 824 + packet_size: "${PACKET_SIZE}" + chunk_size: "${CHUNK_SIZE}" + num_routers: 4 + num_groups: 9 + num_row_chans: 1 + num_col_chans: 1 + num_cns_per_router: 2 + num_global_channels: 2 + cn_bandwidth: 5.25 + local_bandwidth: 5.25 + global_bandwidth: 5.25 + cn_vc_size: 32768 + local_vc_size: 16384 + global_vc_size: 16384 + cn_delay: 10 + local_delay: 10 + global_delay: 100 + router_delay: 300 + nic_seq_delay: 0 + cn_credit_delay: 10 + local_credit_delay: 10 + global_credit_delay: 100 + num_qos_levels: 1 + bw_reset_window: 250000.0 + max_qos_monitor: 5500000000 + qos_bucket_max: 10 + qos_min_bws: "5,30,20,5" + qos_max_bws: "10,80,60,20" + routing: prog-adaptive + adaptive_threshold: 2560 + route_scoring_metric: alpha + route_scoring_factors: 2 + route_scoring_factors_local_intm: 2 + route_scoring_factors_local_dest: 2 + modelnet_order: [dragonfly_dally, dragonfly_dally_router] + modelnet_scheduler: round-robin + intra-group-connections: "@CMAKE_SOURCE_DIR@/src/network-workloads/conf/dragonfly-dally/dfdally-72-intra" + inter-group-connections: "@CMAKE_SOURCE_DIR@/src/network-workloads/conf/dragonfly-dally/dfdally-72-inter" + groups: + MODELNET_GRP: + repetitions: 36 + lps: + nw-lp: 2 + dir-nw-lp: 2 + modelnet_dragonfly_dally: 2 + modelnet_dragonfly_dally_router: 1 + +sections: + DIRECTOR: + start_iter: "${START_ITER}" + end_iter: "${END_ITER}" + retrain_enabled: "${RETRAIN_ENABLED}" + retrain_iter: "${RETRAIN_ITER}" + retrain_save_path: "${RETRAIN_SAVE_PATH}" + second_surrogate_enabled: "${SECOND_SURROGATE_ENABLED}" + second_start_iter: "${SECOND_START_ITER}" + second_end_iter: "${SECOND_END_ITER}" + inferencing_enabled: "${INFERENCING_ENABLED}" + surrogate_enabled: "${SURROGATE_ENABLED}" + training_enabled: "${TRAINING_ENABLED}" + debug_prints: "${DIRECTOR_DEBUG_PRINTS}" + shutdown_zmqml_server_on_finalize: "${SHUTDOWN_ZMQML_SERVER_ON_FINALIZE}" diff --git a/doc/example/tutorial-ping-pong-surrogate.yaml.in b/doc/example/tutorial-ping-pong-surrogate.yaml.in new file mode 100644 index 00000000..1f621040 --- /dev/null +++ b/doc/example/tutorial-ping-pong-surrogate.yaml.in @@ -0,0 +1,99 @@ +# Run this example with: +# > cd path-to-codes/build +# > mpirun -np 3 doc/example/tutorial-synthetic-ping-pong --synch=3 --num_messages=10000 --lp-io-dir=codes-output -- doc/example/tutorial-ping-pong-surrogate.yaml +schema_version: 1 + +# YAML twin of tutorial-ping-pong-surrogate.conf: the same 72-terminal dragonfly- +# dally fabric as tutorial-ping-pong.yaml, plus the NETWORK_SURROGATE section that +# drives the surrogate/high-fidelity director. dragonfly-dally is file-enumerated, +# so the shape counts must match the binary connection files (referenced by +# absolute path; @CMAKE_SOURCE_DIR@ is substituted by CMake). The configure-time +# placeholders are filled in by CMake for the build-dir .yaml, or by envsubst for +# the .template.yaml.in -- exactly as in the .conf. + +components: + compute_host: +# name of this lp changes according to the model + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-dally + shape: +# number of routers in group + num_routers: 4 +# number of groups in the network + num_groups: 9 +# number of compute nodes connected to router, dictated by dragonfly config file + num_cns_per_router: 2 +# number of global channels per router + num_global_channels: 2 + links: +# bandwidth in GiB/s and virtual-channel buffer size in bytes, per link class: +# local + global channels, and the compute node-router channel + local: { bandwidth: 2.0, vc_size: 16384 } + global: { bandwidth: 2.0, vc_size: 16384 } + cn: { bandwidth: 2.0, vc_size: 32768 } + routing: +# routing protocol to be used + algorithm: prog-adaptive + connections: +# network config files for intra-group and inter-group connections + intra: "@CMAKE_SOURCE_DIR@/src/network-workloads/conf/dragonfly-dally/dfdally-72-intra" + inter: "@CMAKE_SOURCE_DIR@/src/network-workloads/conf/dragonfly-dally/dfdally-72-inter" +# packet size in the network + packet_size: ${PACKET_SIZE} +# chunk size in the network (when chunk size = packet size, packets will not be +# divided into chunks) + chunk_size: ${CHUNK_SIZE} +# scheduler options (alternative: round-robin) + modelnet_scheduler: fcfs +# ROSS message size + message_size: 440 +# folder path to store packet latency from terminal to terminal, if no value is given it won't save anything + save_packet_latency_path: "${PACKET_LATENCY_TRACE_PATH}" +# folder path to store router-local timing rows for router queueing-delay training + save_router_timing_trace_path: "${ROUTER_TIMING_TRACE_PATH}" + save_router_timing_trace_stride: "${ROUTER_TIMING_TRACE_STRIDE}" +# router buffer occupancy snapshots + router_buffer_snapshots: [ ${BUFFER_SNAPSHOTS} ] + hosts: + component: compute_host + +sections: +# read verbatim by the surrogate/director stack (section names are case-insensitive, +# so network_surrogate here is the NETWORK_SURROGATE the model reads). + network_surrogate: + enable: 1 # Options: 0 or 1 + +# determines the director switching from surrogate to high-def simulation strategy + director_mode: at-fixed-virtual-times + +# director configuration for: director_mode == "at-fixed-virtual-times" +# timestamps at which to switch to surrogate-mode and back + #fixed_switch_timestamps: [ "100e4", "8900e4" ] # first switch at ~100 ping messages, second at ~9900 pings + #fixed_switch_timestamps: [ "1000e4", "8900e4" ] # first switch at ~1000 ping messages, second at ~9900 pings + fixed_switch_timestamps: [ ${SWITCH_TIMESTAMPS} ] + +# latency predictor to use. Options: average, torch-jit + packet_latency_predictor: "${PREDICTOR_TYPE}" +# some workload models need some time to stabilize, a point where the network behaviour stabilizes. The predictor will ignore all packet latencies that arrive during this period + ignore_until: "${IGNORE_UNTIL}" + +# parameters for torch-jit latency predictor +# accepted modes: +# single-static-model-for-all-terminals: uses torch_jit_model_path, input [1,4], output [1,2] +# lp-aware-single-static-model: uses torch_jit_model_path, input [1,12], output [1,2] +# lp-aware-lp-type-models: uses the terminal/router/default paths below + torch_jit_mode: "${TORCH_JIT_MODE}" + torch_jit_model_path: "${TORCH_JIT_MODEL_PATH}" + torch_jit_terminal_model_path: "${TORCH_JIT_TERMINAL_MODEL_PATH}" + torch_jit_router_timing_model_path: "${TORCH_JIT_ROUTER_TIMING_MODEL_PATH}" + torch_jit_default_model_path: "${TORCH_JIT_DEFAULT_MODEL_PATH}" + +# temporary surrogate debug prints. Options: 0 or 1 + debug_prints: "${SURROGATE_DEBUG_PRINTS}" + +# selecting network treatment on switching to surrogate. Options: freeze, nothing + network_treatment_on_switch: "${NETWORK_TREATMENT}" diff --git a/doc/example/tutorial-ping-pong.yaml.in b/doc/example/tutorial-ping-pong.yaml.in new file mode 100644 index 00000000..ad0dddf9 --- /dev/null +++ b/doc/example/tutorial-ping-pong.yaml.in @@ -0,0 +1,60 @@ +schema_version: 1 + +# YAML twin of tutorial-ping-pong.conf: the 72-terminal dragonfly-dally fabric the +# ping-pong tutorial runs on. dragonfly-dally is file-enumerated, so the shape +# counts are genuine inputs that must match the binary connection files (referenced +# by absolute path; @CMAKE_SOURCE_DIR@ is substituted by CMake so the model resolves +# them from any run directory). The configure-time placeholders are filled in by +# CMake for the build-dir .yaml, or by envsubst for the .template.yaml.in -- exactly +# as in the .conf. The compiler derives repetitions = num_groups * num_routers and +# lays out the [workload, terminal, router] LPs, so nw-lp / modelnet_dragonfly_dally* +# are emitted for you. + +components: + compute_host: +# name of this lp changes according to the model + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-dally + shape: +# number of routers in group + num_routers: 4 +# number of groups in the network + num_groups: 9 +# number of compute nodes connected to router, dictated by dragonfly config file + num_cns_per_router: 2 +# number of global channels per router + num_global_channels: 2 + links: +# bandwidth in GiB/s and virtual-channel buffer size in bytes, per link class: +# local + global channels, and the compute node-router channel + local: { bandwidth: 2.0, vc_size: 16384 } + global: { bandwidth: 2.0, vc_size: 16384 } + cn: { bandwidth: 2.0, vc_size: 32768 } + routing: +# routing protocol to be used + algorithm: prog-adaptive + connections: +# network config files for intra-group and inter-group connections + intra: "@CMAKE_SOURCE_DIR@/src/network-workloads/conf/dragonfly-dally/dfdally-72-intra" + inter: "@CMAKE_SOURCE_DIR@/src/network-workloads/conf/dragonfly-dally/dfdally-72-inter" +# packet size in the network + packet_size: ${PACKET_SIZE} +# chunk size in the network (when chunk size = packet size, packets will not be +# divided into chunks) + chunk_size: ${CHUNK_SIZE} +# scheduler options (alternative: round-robin) + modelnet_scheduler: fcfs +# ROSS message size + message_size: 440 +# router buffer occupancy snapshots + router_buffer_snapshots: [ ${BUFFER_SNAPSHOTS} ] +# folder path to store packet latency from terminal to terminal, if no value is given it won't save anything + save_packet_latency_path: "${PACKET_LATENCY_TRACE_PATH}" + save_router_timing_trace_path: "${ROUTER_TIMING_TRACE_PATH}" + save_router_timing_trace_stride: "${ROUTER_TIMING_TRACE_STRIDE}" + hosts: + component: compute_host diff --git a/doc/example_heterogeneous/example.yaml b/doc/example_heterogeneous/example.yaml new file mode 100644 index 00000000..d0f457d5 --- /dev/null +++ b/doc/example_heterogeneous/example.yaml @@ -0,0 +1,54 @@ +schema_version: 1 + +# YAML twin of example.conf: a heterogeneous setup -- two compute clusters (foo, +# bar) each with its own simplenet parameters, plus forwarder groups bridging +# them over a shared forwarding network. codes_mapping lets the same NIC model +# (modelnet_simplenet) appear per-cluster under an annotation, so this uses the +# explicit-groups form with type@annotation LP entries and matching @-annotated +# PARAMS. The custom run_params section is carried through under sections:. + +topology: + format: groups + params: + message_size: 352 + modelnet_order: [simplenet] + packet_size@foo: 8192 + modelnet_scheduler@foo: fcfs + net_startup_ns@foo: 1.5 + net_bw_mbps@foo: 10000 + packet_size@bar: 2048 + modelnet_scheduler@bar: round-robin + net_startup_ns@bar: 3.0 + net_bw_mbps@bar: 15000 + packet_size: 4096 + modelnet_scheduler: fcfs + net_startup_ns: 8.0 + net_bw_mbps: 5000 + groups: + FOO_CLUSTER: + repetitions: 12 + lps: + node: 1 + modelnet_simplenet@foo: 1 + FOO_FORWARDERS: + repetitions: 4 + lps: + forwarder: 1 + modelnet_simplenet@foo: 1 + modelnet_simplenet: 1 + BAR_CLUSTER: + repetitions: 12 + lps: + node: 1 + modelnet_simplenet@bar: 1 + BAR_FORWARDERS: + repetitions: 4 + lps: + forwarder: 1 + modelnet_simplenet@bar: 1 + modelnet_simplenet: 1 + +sections: + run_params: + num_reqs: 5 + payload_sz: 16384 diff --git a/doc/example_heterogeneous/example_torus.yaml b/doc/example_heterogeneous/example_torus.yaml new file mode 100644 index 00000000..d48dfdd4 --- /dev/null +++ b/doc/example_heterogeneous/example_torus.yaml @@ -0,0 +1,62 @@ +schema_version: 1 + +# YAML twin of example_torus.conf: like example.yaml, but each compute cluster +# (foo, bar) is a torus with its own dimensions, and the forwarders bridge over +# simplenet. The same torus model appears per-cluster under an annotation, so +# this uses the explicit-groups form with type@annotation LP entries and matching +# @-annotated PARAMS. dim_length@foo/@bar are comma-separated strings the model +# parses itself. The custom run_params section is carried through under sections:. + +topology: + format: groups + params: + message_size: 352 + modelnet_order: [torus, simplenet] + packet_size@foo: 8192 + modelnet_scheduler@foo: fcfs + n_dims@foo: 3 + dim_length@foo: "4,2,2" + link_bandwidth@foo: 2.0 + buffer_size@foo: 8192 + num_vc@foo: 1 + chunk_size@foo: 32 + packet_size@bar: 2048 + modelnet_scheduler@bar: round-robin + n_dims@bar: 2 + dim_length@bar: "4,4" + link_bandwidth@bar: 2.0 + buffer_size@bar: 8192 + num_vc@bar: 1 + chunk_size@bar: 32 + packet_size: 4096 + modelnet_scheduler: fcfs + net_startup_ns: 8.0 + net_bw_mbps: 5000 + groups: + FOO_CLUSTER: + repetitions: 12 + lps: + node: 1 + modelnet_torus@foo: 1 + FOO_FORWARDERS: + repetitions: 4 + lps: + forwarder: 1 + modelnet_torus@foo: 1 + modelnet_simplenet: 1 + BAR_CLUSTER: + repetitions: 12 + lps: + node: 1 + modelnet_torus@bar: 1 + BAR_FORWARDERS: + repetitions: 4 + lps: + forwarder: 1 + modelnet_torus@bar: 1 + modelnet_simplenet: 1 + +sections: + run_params: + num_reqs: 5 + payload_sz: 16384 diff --git a/scripts/dragonfly-custom/example/network-model.conf b/scripts/dragonfly-custom/example/network-model.conf index 836d097c..2f7eb4b2 100644 --- a/scripts/dragonfly-custom/example/network-model.conf +++ b/scripts/dragonfly-custom/example/network-model.conf @@ -3,7 +3,7 @@ LPGROUPS MODELNET_GRP { repetitions="768"; - nw-lp="8"; + nw-lp="4"; modelnet_dragonfly_custom="4"; modelnet_dragonfly_custom_router="1"; } @@ -27,8 +27,8 @@ PARAMS cn_bandwidth="12.5"; num_cns_per_router="4"; num_global_channels="4"; - intra-group-connections="/home/noah/Dropbox/RPI/Research/Networks/codes-unified/codes/src/network-workloads/conf/dragonfly-custom/intra-theta-8group"; - inter-group-connections="/home/noah/Dropbox/RPI/Research/Networks/codes-unified/codes/src/network-workloads/conf/dragonfly-custom/inter-theta-8group"; + intra-group-connections="intra-theta-8group"; + inter-group-connections="inter-theta-8group"; routing="adaptive"; num_injection_queues="1"; nic_seq_delay="10"; diff --git a/scripts/dragonfly-custom/example/network-model.yaml b/scripts/dragonfly-custom/example/network-model.yaml new file mode 100644 index 00000000..45d6156d --- /dev/null +++ b/scripts/dragonfly-custom/example/network-model.yaml @@ -0,0 +1,42 @@ +schema_version: 1 + +# YAML twin of network-model.conf: an 8-group Theta-style custom dragonfly whose +# 6x16 router mesh and inter/intra wiring come from hand-authored connection +# files. Rather than re-derive that fabric, the twin is laid out directly with +# the explicit-groups form and its PARAMS are carried through verbatim (including +# the intra/inter connection-file paths, resolved relative to the run directory). + +topology: + format: groups + params: + packet_size: 4096 + message_size: 656 + chunk_size: 4096 + modelnet_scheduler: fcfs + modelnet_order: [dragonfly_custom, dragonfly_custom_router] + num_router_rows: 6 + num_router_cols: 16 + num_groups: 8 + router_delay: 90 + local_vc_size: 65536 + global_vc_size: 65536 + cn_vc_size: 65536 + local_bandwidth: 12.5 + global_bandwidth: 12.5 + cn_bandwidth: 12.5 + num_cns_per_router: 4 + num_global_channels: 4 + intra-group-connections: intra-theta-8group + inter-group-connections: inter-theta-8group + routing: adaptive + num_injection_queues: 1 + nic_seq_delay: 10 + node_copy_queues: 1 + node_eager_limit: 16000 + groups: + MODELNET_GRP: + repetitions: 768 + lps: + nw-lp: 4 + modelnet_dragonfly_custom: 4 + modelnet_dragonfly_custom_router: 1 diff --git a/scripts/dragonfly-plus/example/dfp-test.yaml b/scripts/dragonfly-plus/example/dfp-test.yaml new file mode 100644 index 00000000..ca96b833 --- /dev/null +++ b/scripts/dragonfly-plus/example/dfp-test.yaml @@ -0,0 +1,38 @@ +schema_version: 1 + +# YAML twin of dfp-test.conf: a 5-group dragonfly-plus. It is file-enumerated, so +# the shape counts are genuine inputs that must match the binary connection files +# (referenced by path). The compiler derives repetitions = num_groups, terminals +# per group = num_router_leaf * num_cns_per_router, routers per group = +# num_router_spine + num_router_leaf. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-plus + shape: + num_router_spine: 4 + num_router_leaf: 4 + num_groups: 5 + num_cns_per_router: 4 + links: + local: { bandwidth: 5.25, vc_size: 8192 } + global: { bandwidth: 1.5, vc_size: 16384 } + cn: { bandwidth: 8.0, vc_size: 8192 } + routing: + algorithm: minimal + connections: + intra: "../src/network-workloads/conf/dragonfly-plus/dfp-test-intra" + inter: "../src/network-workloads/conf/dragonfly-plus/dfp-test-inter" + packet_size: 1024 + chunk_size: 1024 + modelnet_scheduler: fcfs + num_level_chans: 1 + message_size: 608 + num_global_connections: 4 + hosts: + component: compute_host diff --git a/scripts/reproducibility-pads23/experiments/conf-files/terminal-dragonfly-72-surrogate-v5.yaml.in b/scripts/reproducibility-pads23/experiments/conf-files/terminal-dragonfly-72-surrogate-v5.yaml.in new file mode 100644 index 00000000..555676dc --- /dev/null +++ b/scripts/reproducibility-pads23/experiments/conf-files/terminal-dragonfly-72-surrogate-v5.yaml.in @@ -0,0 +1,47 @@ +schema_version: 1 + +# YAML twin of terminal-dragonfly-72-surrogate-v5.conf.in: the same 72-terminal +# dragonfly-dally as terminal-dragonfly-72-v5, plus the surrogate switching +# configuration the network surrogate reads from its own SURROGATE section +# (carried through verbatim). The ${...} tokens are substituted by the +# experiment's envsubst step; fixed_switch_timestamps expands to a list, written +# as a YAML sequence. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-dally + shape: + num_routers: 4 + num_groups: 9 + num_cns_per_router: 2 + num_global_channels: 2 + links: + local: { bandwidth: 2.0, vc_size: 16384 } + global: { bandwidth: 2.0, vc_size: 16384 } + cn: { bandwidth: 2.0, vc_size: 32768 } + routing: + algorithm: prog-adaptive + connections: + intra: "${PATH_TO_CODES_SRC}/src/network-workloads/conf/dragonfly-dally/dfdally-72-intra" + inter: "${PATH_TO_CODES_SRC}/src/network-workloads/conf/dragonfly-dally/dfdally-72-inter" + packet_size: 4096 + chunk_size: "${CHUNK_SIZE}" + modelnet_scheduler: fcfs + message_size: 736 + save_packet_latency_path: "${PACKET_LATENCY_PATH}" + router_buffer_snapshots: [ ${BUFFER_SNAPSHOTS} ] + hosts: + component: compute_host + +sections: + SURROGATE: + director_mode: at-fixed-virtual-times + fixed_switch_timestamps: [ ${SWITCH_TIMESTAMPS} ] + packet_latency_predictor: average + ignore_until: "${IGNORE_UNTIL}" + network_treatment_on_switch: "${NETWORK_TREATMENT}" diff --git a/scripts/reproducibility-pads23/experiments/conf-files/terminal-dragonfly-72-v5.yaml.in b/scripts/reproducibility-pads23/experiments/conf-files/terminal-dragonfly-72-v5.yaml.in new file mode 100644 index 00000000..02377007 --- /dev/null +++ b/scripts/reproducibility-pads23/experiments/conf-files/terminal-dragonfly-72-v5.yaml.in @@ -0,0 +1,38 @@ +schema_version: 1 + +# YAML twin of terminal-dragonfly-72-v5.conf.in: a 72-terminal dragonfly-dally. +# The compiler derives repetitions = num_groups * num_routers = 36 and lays out +# the LPs. The ${...} tokens are substituted by the experiment's envsubst step +# before the config is loaded; router_buffer_snapshots expands to a list, so it +# is written as a YAML sequence. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-dally + shape: + num_routers: 4 + num_groups: 9 + num_cns_per_router: 2 + num_global_channels: 2 + links: + local: { bandwidth: 2.0, vc_size: 16384 } + global: { bandwidth: 2.0, vc_size: 16384 } + cn: { bandwidth: 2.0, vc_size: 32768 } + routing: + algorithm: prog-adaptive + connections: + intra: "${PATH_TO_CODES_SRC}/src/network-workloads/conf/dragonfly-dally/dfdally-72-intra" + inter: "${PATH_TO_CODES_SRC}/src/network-workloads/conf/dragonfly-dally/dfdally-72-inter" + packet_size: 4096 + chunk_size: "${CHUNK_SIZE}" + modelnet_scheduler: fcfs + message_size: 736 + save_packet_latency_path: "${PACKET_LATENCY_PATH}" + router_buffer_snapshots: [ ${BUFFER_SNAPSHOTS} ] + hosts: + component: compute_host diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 56e0ddd8..0278e0ec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -92,6 +92,19 @@ list(APPEND SRCS modelconfig/configuration.c modelconfig/txt_configfile.h modelconfig/txt_configfile.c + + # YAML/JSON config front-end: a pure C++ core that compiles the friendly + # topology/component format to the compiled_config IR (config_compiler), a + # dumb emitter that lowers that IR to the same ConfigVTable the .conf parser + # yields (config_emitter), and the thin extern "C" shim configuration.c + # dispatches to (yaml_configfile). Always built -- ryml is vendored, so the + # front-end is core, not an optional feature. + modelconfig/config_compiler.h + modelconfig/config_compiler.cxx + modelconfig/config_emitter.h + modelconfig/config_emitter.cxx + modelconfig/yaml_configfile.h + modelconfig/yaml_configfile.cxx ) list(APPEND LIBS_TO_LINK ROSS::ROSS) @@ -196,6 +209,16 @@ target_include_directories(codes PUBLIC target_link_libraries(codes PUBLIC ${LIBS_TO_LINK}) +# ryml backs the YAML config front-end and is an implementation detail (no public +# codes header includes it). codes is a STATIC library in an install/export set, +# so instead of linking a separate in-tree lib (which the build-tree export() +# would drag into the export set), compile the vendored amalgamation's single +# implementation TU straight into codes and put its headers on codes' PRIVATE +# include path. The ryml objects then live inside codes.a, invisible to the +# installed interface. See thirdparty/rapidyaml/CMakeLists.txt. +target_sources(codes PRIVATE ${CODES_RYML_IMPL_TU}) +target_include_directories(codes SYSTEM PRIVATE ${CODES_RYML_INCLUDE_DIRS}) + add_executable(topology-test networks/model-net/topology-test.c) add_executable(model-net-mpi-replay network-workloads/model-net-mpi-replay.c network-workloads/model-net-mpi-replay-main.c) if(USE_DUMPI) diff --git a/src/modelconfig/config_compiler.cxx b/src/modelconfig/config_compiler.cxx new file mode 100644 index 00000000..d6f27171 --- /dev/null +++ b/src/modelconfig/config_compiler.cxx @@ -0,0 +1,1032 @@ +/* + * Copyright (C) 2013 University of Chicago. + * See COPYRIGHT notice in top-level directory. + * + */ + +#include "config_compiler.h" + +#include + +#include +#include +#include /* LONG_MAX: guard dimension-product overflow */ +#include +#include /* strcasecmp: section names are case-insensitive */ +#include +#include +#include + +namespace codes { +namespace config { + +/* ------------------------------------------------------------------------- + * compiled_config IR builders + * ---------------------------------------------------------------------- */ + +void compiled_section::add_key(std::string key, std::string value) { + keys.push_back(compiled_key{std::move(key), {std::move(value)}}); +} + +void compiled_section::add_key(std::string key, std::vector values) { + keys.push_back(compiled_key{std::move(key), std::move(values)}); +} + +compiled_section& compiled_section::add_subsection(std::string subname) { + subsections.push_back(compiled_section{std::move(subname), {}, {}}); + return subsections.back(); +} + +compiled_section& compiled_config::add_section(std::string name) { + sections.push_back(compiled_section{std::move(name), {}, {}}); + return sections.back(); +} + +namespace { + +/* ------------------------------------------------------------------------- + * ryml plumbing -- route parser errors to config_error, never to ROSS. The + * core throws; the extern "C" shim is the only place tw_error lives. + * ---------------------------------------------------------------------- */ + +[[noreturn]] void ryml_throw(ryml::csubstr msg, ryml::ErrorDataBasic const& ed, void*) { + std::string where = + ed.location.line ? (" at line " + std::to_string(ed.location.line)) : std::string(); + throw config_error("config error: YAML error" + where + ": " + std::string(msg.str, msg.len)); +} + +/* ryml raises *parse* (syntax) errors through the separate parse-error callback, + * whose default implementation prints + aborts; install our own so a malformed + * document throws config_error like everything else. The message ryml builds + * already carries the location, so it is passed through as-is. */ +[[noreturn]] void ryml_throw_parse(ryml::csubstr msg, ryml::ErrorDataParse const& ed, void*) { + std::string where = + ed.ymlloc.line ? (" at line " + std::to_string(ed.ymlloc.line)) : std::string(); + throw config_error("config error: malformed YAML" + where + ": " + + std::string(msg.str, msg.len)); +} + +/* raw scalar text of a node, preserving the exact source spelling */ +std::string scalar(ryml::ConstNodeRef n) { + ryml::csubstr v = n.val(); + return std::string(v.str, v.len); +} + +std::string key_of(ryml::ConstNodeRef n) { + ryml::csubstr k = n.key(); + return std::string(k.str, k.len); +} + +bool has(ryml::ConstNodeRef n, const char* key) { + return n.readable() && n.is_map() && n.has_child(ryml::to_csubstr(key)); +} + +/* Parse a scalar strictly as a base-10 integer (whole string consumed). */ +long parse_int_strict(const std::string& s, const char* what) { + errno = 0; + char* end = nullptr; + long v = std::strtol(s.c_str(), &end, 10); + if (s.empty() || end == s.c_str() || *end != '\0' || errno != 0) + throw config_error(std::string("config error: ") + what + " must be an integer, got \"" + + s + "\""); + return v; +} + +/* ------------------------------------------------------------------------- + * Friendly-format intermediate representation + * ---------------------------------------------------------------------- */ + +using kv_list = std::vector>; + +/* A custom component: a model paired with configured parameters. */ +struct component { + std::string key; /* the components: key referenced by a topology */ + std::string model; /* ComponentModel name (nw-lp, ...) */ + std::string network; /* enumerated flat models: the NIC model a compute node + runs its workload over (added with the flat path) */ + kv_list params; /* scalar model params, raw text, in source order */ +}; + +/* A per-link-class parameter block (e.g. dragonfly local/global/cn). */ +struct link_class { + std::string name; /* class name; combines with each param as _ */ + kv_list params; +}; + +/* A parametric fabric: an HPC topology described by shape parameters. */ +struct fabric { + std::string model; /* network model, e.g. "dragonfly" */ + kv_list shape; /* shape parameters (also drive count derivation) */ + std::vector links; /* per-link-class bandwidth / vc_size */ + kv_list routing; /* routing.* (algorithm maps to PARAMS "routing") */ + kv_list connections; /* connections.{intra,inter}: file-enumerated wiring */ + kv_list extra; /* other scalar fabric keys -> PARAMS verbatim */ + /* list-valued fabric keys (e.g. slimfly generator_set_X) -> multi-value PARAMS */ + std::vector>> extra_lists; + std::string hosts_component; /* hosts.component: the per-terminal workload */ +}; + +struct friendly_config { + std::vector components; + bool parametric = false; + fabric fab; /* parametric topology */ + + bool flat = false; /* flat enumerated topology */ + std::string flat_component; /* the component every node runs */ + long node_count = 0; /* number of nodes = repetitions */ + + /* verbatim `sections:` blocks -- config a model reads directly (DIRECTOR, + * surrogate, storage, ...), passed straight through to the compiled output. + * Emitted after the topology sections. */ + std::vector passthrough; + + /* explicit LP-groups topology (`format: groups`): the user lays out groups, + * LP types, counts and annotations directly. The escape hatch for configs + * that are not a single friendly network. Built during parse. */ + bool explicit_groups = false; + compiled_section explicit_lpgroups{"LPGROUPS", {}, {}}; + compiled_section explicit_params{"PARAMS", {}, {}}; + + const component* find_component(const std::string& k) const { + for (const component& c : components) + if (c.key == k) + return &c; + return nullptr; + } +}; + +/* ------------------------------------------------------------------------- + * Model registry -- maps a friendly fabric model name to its LP-type names, + * modelnet_order method names, and shape->counts derivation. + * ---------------------------------------------------------------------- */ + +/* The LP layout of one repetition. */ +struct layout { + long repetitions; + long terminals_per_rep; /* workload + NIC LP count per repetition */ + long routers_per_rep; /* router/switch LP count per repetition */ +}; + +struct fabric_model { + const char* name; /* friendly name used in fabric.model */ + const char* terminal_lp; /* LPGROUPS lp-type name for the NIC/terminal */ + const char* router_lp; /* LPGROUPS lp-type name for the router/switch */ + const char* term_method; /* modelnet_order method name for the terminal */ + const char* router_method; /* modelnet_order method for the router, or nullptr + if the router is not a separate model-net method */ + layout (*derive)(const kv_list& shape); /* shape -> LP layout */ +}; + +/* Look up a shape value by name, throwing if absent. The value is parsed + * strictly, so a non-integer (num_groups: abc) or trailing garbage (9x) is a + * diagnostic naming the offending key rather than a silent 0/9. */ +long shape_int(const kv_list& shape, const char* key) { + for (const auto& kv : shape) { + if (kv.first == key) { + std::string what = std::string("fabric shape \"") + key + "\""; + return parse_int_strict(kv.second, what.c_str()); + } + } + throw config_error(std::string("config error: fabric shape is missing required key \"") + key + + "\""); +} + +/* Look up a shape value by name, returning a default when absent. Present values + * are parsed strictly (see shape_int). */ +long shape_int_default(const kv_list& shape, const char* key, long dflt) { + for (const auto& kv : shape) { + if (kv.first == key) { + std::string what = std::string("fabric shape \"") + key + "\""; + return parse_int_strict(kv.second, what.c_str()); + } + } + return dflt; +} + +/* Look up a shape value by name as its raw string, throwing if absent. */ +const std::string& shape_str(const kv_list& shape, const char* key) { + for (const auto& kv : shape) + if (kv.first == key) + return kv.second; + throw config_error(std::string("config error: fabric shape is missing required key \"") + key + + "\""); +} + +/* Parse a comma-separated dimension list ("4,2,2" -> {4,2,2}) strictly: a + * non-empty, strictly comma-separated list of positive integers (optional spaces + * around a value), rejecting empty segments ("4,,2"), a trailing comma ("4,2,"), + * and space-separated lists ("4 2"). */ +std::vector parse_dim_lengths(const kv_list& shape, const char* key) { + const std::string& s = shape_str(shape, key); + auto bad = [&]() -> config_error { + return config_error(std::string("config error: fabric shape \"") + key + + "\" must be a comma-separated list of positive integers, got \"" + s + + "\""); + }; + std::vector dims; + const char* p = s.c_str(); + for (;;) { + char* end = nullptr; + errno = 0; + long d = std::strtol(p, &end, 10); /* strtol skips any leading spaces */ + if (end == p || errno != 0 || d <= 0) + throw bad(); + dims.push_back(d); + p = end; + while (*p == ' ') /* trailing spaces after this value */ + ++p; + if (*p == '\0') + break; + if (*p != ',') /* only a comma may separate values (rejects "4 2") */ + throw bad(); + ++p; /* consume the single comma; the next value is now required */ + } + return dims; +} + +/* Node/router count of a mesh-style fabric (torus, express_mesh): the product of + * the per-dimension lengths in dim_length. The entry count is cross-checked + * against the n_dims shape value, and the running product is guarded against + * signed overflow. `model_name` names the offending fabric in diagnostics. */ +long mesh_node_count(const kv_list& shape, const char* model_name) { + long n_dims = shape_int(shape, "n_dims"); + std::vector dims = parse_dim_lengths(shape, "dim_length"); + if (static_cast(dims.size()) != n_dims) + throw config_error(std::string("config error: ") + model_name + " n_dims (" + + std::to_string(n_dims) + + ") does not match the number of dim_length entries (" + + std::to_string(dims.size()) + ")"); + long prod = 1; + for (long d : dims) { + if (prod > LONG_MAX / d) /* d >= 1, so the divide is safe */ + throw config_error(std::string("config error: ") + model_name + + " dim_length product overflows"); + prod *= d; + } + return prod; +} + +/* Regular (Kim-Dally) dragonfly: every count follows from num_routers, the + * routers per group -- the same derivation the model does internally + * (num_cn = num_routers/2, num_groups = num_routers*num_cn + 1). */ +layout derive_dragonfly(const kv_list& shape) { + long num_routers = shape_int(shape, "num_routers"); + if (num_routers <= 0) + throw config_error("config error: dragonfly num_routers must be positive"); + long num_cn = num_routers / 2; + long num_groups = num_routers * num_cn + 1; + return {num_groups * num_routers, num_cn, 1}; +} + +/* Dragonfly-dally (file-enumerated): the shape counts are genuine inputs that + * must match the connection files. total routers = num_groups * num_planes * + * num_routers; each router hosts num_cns_per_router terminals. */ +layout derive_dragonfly_dally(const kv_list& shape) { + long num_routers = shape_int(shape, "num_routers"); + long num_groups = shape_int(shape, "num_groups"); + long num_cns = shape_int(shape, "num_cns_per_router"); + long num_planes = shape_int_default(shape, "num_planes", 1); + /* Not part of the LP-count arithmetic, but required up front: the value must + * match the wiring in the connection files, and a missing key would + * otherwise fall back to the model's default (10) with only a warning -- + * silently contradicting the wiring. */ + shape_int(shape, "num_global_channels"); + return {num_groups * num_planes * num_routers, num_cns, 1}; +} + +/* Fat-tree (internally-generated): one repetition per edge switch, each hosting + * switch_radix/2 terminals, with one switch LP per level. The fabric's switch is + * not a separate model-net method, so only the terminal appears in + * modelnet_order. */ +layout derive_fattree(const kv_list& shape) { + long switch_count = shape_int(shape, "switch_count"); + long switch_radix = shape_int(shape, "switch_radix"); + long num_levels = shape_int(shape, "num_levels"); + /* Each edge switch hosts switch_radix/2 terminals; an odd radix would + * silently truncate that split, so reject it rather than lose a terminal. */ + if (switch_radix % 2 != 0) + throw config_error("config error: fattree switch_radix must be even (each edge switch " + "hosts switch_radix/2 terminals), got " + + std::to_string(switch_radix)); + return {switch_count, switch_radix / 2, num_levels}; +} + +/* Torus (internally-generated): one repetition per torus node, each a single + * terminal, and no separate router LP (the torus node combines routing and the + * terminal). The node count is the product of the per-dimension lengths. */ +layout derive_torus(const kv_list& shape) { + long nodes = mesh_node_count(shape, "torus"); + return {nodes, 1, 0}; +} + +/* Express mesh (internally-generated): one repetition per mesh router, each + * hosting num_cn terminals plus one router LP. The router count is the product + * of the per-dimension lengths. */ +layout derive_express_mesh(const kv_list& shape) { + long routers = mesh_node_count(shape, "express-mesh"); + long num_cn = shape_int(shape, "num_cn"); + return {routers, num_cn, 1}; +} + +/* Slimfly (internally-generated, MMS topology): the two Cayley subgraphs give + * 2 * num_routers^2 routers total, one repetition each, hosting num_terminals + * terminals plus one router LP. */ +layout derive_slimfly(const kv_list& shape) { + long num_routers = shape_int(shape, "num_routers"); + long num_terminals = shape_int(shape, "num_terminals"); + if (num_routers <= 0) + throw config_error("config error: slimfly num_routers must be positive"); + return {2 * num_routers * num_routers, num_terminals, 1}; +} + +/* Dragonfly-plus (file-enumerated): one repetition per group. Each group's + * routers split into a spine and a leaf level (num_router_spine + num_router_leaf + * router LPs); only the leaf routers host terminals, num_cns_per_router each. The + * shape counts are genuine inputs that must match the connection files. */ +layout derive_dragonfly_plus(const kv_list& shape) { + long num_groups = shape_int(shape, "num_groups"); + long spine = shape_int(shape, "num_router_spine"); + long leaf = shape_int(shape, "num_router_leaf"); + long num_cns = shape_int(shape, "num_cns_per_router"); + return {num_groups, leaf * num_cns, spine + leaf}; +} + +/* Dragonfly-custom (file-enumerated): one repetition per router. Each group is a + * num_router_rows x num_router_cols mesh of routers, so total routers = + * num_groups * num_router_rows * num_router_cols; each router hosts + * num_cns_per_router terminals plus one router LP. The shape counts are genuine + * inputs that must match the connection files. */ +layout derive_dragonfly_custom(const kv_list& shape) { + long num_groups = shape_int(shape, "num_groups"); + long rows = shape_int(shape, "num_router_rows"); + long cols = shape_int(shape, "num_router_cols"); + long num_cns = shape_int(shape, "num_cns_per_router"); + /* Not part of the LP-count arithmetic, but required up front: the value must + * match the wiring in the connection files, and a missing key would + * otherwise fall back to the model's default (10) with only a warning -- + * silently contradicting the wiring. */ + shape_int(shape, "num_global_channels"); + return {num_groups * rows * cols, num_cns, 1}; +} + +const fabric_model fabric_models[] = { + {"dragonfly", "modelnet_dragonfly", "modelnet_dragonfly_router", "dragonfly", + "dragonfly_router", derive_dragonfly}, + {"dragonfly-dally", "modelnet_dragonfly_dally", "modelnet_dragonfly_dally_router", + "dragonfly_dally", "dragonfly_dally_router", derive_dragonfly_dally}, + {"fattree", "modelnet_fattree", "fattree_switch", "fattree", nullptr, derive_fattree}, + {"torus", "modelnet_torus", nullptr, "torus", nullptr, derive_torus}, + {"express-mesh", "modelnet_express_mesh", "modelnet_express_mesh_router", "express_mesh", + "express_mesh_router", derive_express_mesh}, + {"slimfly", "modelnet_slimfly", "modelnet_slimfly_router", "slimfly", "slimfly_router", + derive_slimfly}, + {"dragonfly-plus", "modelnet_dragonfly_plus", "modelnet_dragonfly_plus_router", + "dragonfly_plus", "dragonfly_plus_router", derive_dragonfly_plus}, + {"dragonfly-custom", "modelnet_dragonfly_custom", "modelnet_dragonfly_custom_router", + "dragonfly_custom", "dragonfly_custom_router", derive_dragonfly_custom}, +}; + +const fabric_model* find_fabric_model(const std::string& name) { + for (const fabric_model& m : fabric_models) + if (name == m.name) + return &m; + return nullptr; +} + +/* A flat (enumerated) network model: one NIC LP per compute node, all peers. + * Maps a friendly network name to the LPGROUPS lp-type name and the + * modelnet_order method the model registers. */ +struct network_model { + const char* name; /* friendly name used in a component's network: field */ + const char* nic_lp; /* LPGROUPS lp-type name for the NIC */ + const char* method; /* modelnet_order method name */ +}; + +const network_model network_models[] = { + {"simplenet", "modelnet_simplenet", "simplenet"}, + {"simplep2p", "modelnet_simplep2p", "simplep2p"}, + {"loggp", "modelnet_loggp", "loggp"}, +}; + +const network_model* find_network_model(const std::string& name) { + for (const network_model& m : network_models) + if (name == m.name) + return &m; + return nullptr; +} + +/* ------------------------------------------------------------------------- + * Parse: ryml tree -> friendly IR (validating as it goes -- unknown / + * unconsumed keys are errors, not silent drops). + * ---------------------------------------------------------------------- */ + +void parse_components(ryml::ConstNodeRef root, friendly_config& cfg) { + if (!has(root, "components")) + return; + ryml::ConstNodeRef comps = root["components"]; + if (!comps.is_map()) + throw config_error("config error: \"components\" must be a map of name -> component"); + for (ryml::ConstNodeRef cnode : comps.children()) { + component c; + c.key = key_of(cnode); + for (ryml::ConstNodeRef f : cnode.children()) { + std::string k = key_of(f); + if (k == "model") + c.model = scalar(f); + else if (k == "network") + c.network = scalar(f); + else if (k == "type") + /* Reserved for a future schema version. Reject explicitly: a bare + * `type:` scalar would otherwise fall through to the is_keyval() + * branch below and silently become a model param in PARAMS. */ + throw config_error("config error: component \"" + c.key + + "\": key \"type\" is reserved for a future schema version and " + "is not accepted yet; remove it (the model is inferred from " + "\"model:\")"); + else if (f.is_keyval()) + c.params.emplace_back(k, scalar(f)); + else + throw config_error("config error: component \"" + c.key + + "\": unexpected block \"" + k + + "\"; a component takes a model, an optional network, and scalar " + "params (per-node data, edges and inline workloads are not " + "supported)"); + } + /* include-merge: a later document's component overrides an earlier one of + * the same name (within one document, keys are already unique). */ + cfg.components.erase(std::remove_if(cfg.components.begin(), cfg.components.end(), + [&](const component& e) { return e.key == c.key; }), + cfg.components.end()); + cfg.components.push_back(std::move(c)); + } +} + +void parse_fabric(ryml::ConstNodeRef fnode, fabric& fab) { + for (ryml::ConstNodeRef c : fnode.children()) { + std::string k = key_of(c); + if (k == "model") { + fab.model = scalar(c); + } else if (k == "shape") { + for (ryml::ConstNodeRef s : c.children()) + fab.shape.emplace_back(key_of(s), scalar(s)); + } else if (k == "links") { + for (ryml::ConstNodeRef lc : c.children()) { + link_class cls; + cls.name = key_of(lc); + for (ryml::ConstNodeRef p : lc.children()) + cls.params.emplace_back(key_of(p), scalar(p)); + fab.links.push_back(std::move(cls)); + } + } else if (k == "routing") { + for (ryml::ConstNodeRef r : c.children()) + fab.routing.emplace_back(key_of(r), scalar(r)); + } else if (k == "connections") { + /* file-enumerated dragonflies reference the binary connection files + * by path; the compiler maps intra/inter to the model's key names. */ + for (ryml::ConstNodeRef cn : c.children()) + fab.connections.emplace_back(key_of(cn), scalar(cn)); + } else if (c.is_seq()) { + /* a list-valued fabric param (e.g. slimfly generator_set_X: [1, 4]) + * becomes a multi-value PARAMS key. */ + std::vector vals; + for (ryml::ConstNodeRef v : c.children()) + vals.push_back(scalar(v)); + fab.extra_lists.emplace_back(k, std::move(vals)); + } else if (c.is_keyval()) { + fab.extra.emplace_back(k, scalar(c)); + } else { + throw config_error("config error: fabric: unexpected block \"" + k + "\""); + } + } +} + +/* defined with the other pass-through helpers below; used here to build PARAMS + * for the explicit-groups form. */ +compiled_section build_passthrough_section(const std::string& name, ryml::ConstNodeRef node); + +/* Build the LPGROUPS section from an explicit `groups` map. Each group names its + * repetitions and its LP types with counts; an LP-type key may carry an + * annotation as `type@annotation` (the .conf spelling codes_mapping splits on + * '@'). This is the general layout escape hatch for configs that are not a + * single friendly network -- storage clusters, multi-partition, mapping tests -- + * where the compiler derives nothing and the user lays out the LPs directly. */ +void parse_explicit_groups(ryml::ConstNodeRef groups, friendly_config& cfg) { + if (!groups.is_map() || groups.num_children() == 0) + throw config_error("config error: topology.groups must be a non-empty map of " + "group-name -> { repetitions, lps }"); + for (ryml::ConstNodeRef g : groups.children()) { + std::string gname = key_of(g); + if (!g.is_map()) + throw config_error("config error: topology.groups: \"" + gname + + "\" must be a block with repetitions and lps"); + for (ryml::ConstNodeRef c : g.children()) { + std::string k = key_of(c); + if (k != "repetitions" && k != "lps") + throw config_error("config error: topology.groups: \"" + gname + + "\": unexpected key \"" + k + "\" (only repetitions and lps)"); + } + if (!has(g, "repetitions")) + throw config_error("config error: topology.groups: \"" + gname + + "\" needs a repetitions count"); + std::string reps_what = "topology.groups \"" + gname + "\" repetitions"; + long reps = parse_int_strict(scalar(g["repetitions"]), reps_what.c_str()); + if (reps <= 0) + throw config_error("config error: topology.groups: \"" + gname + + "\" repetitions must be positive"); + if (!has(g, "lps") || !g["lps"].is_map() || g["lps"].num_children() == 0) + throw config_error("config error: topology.groups: \"" + gname + + "\" needs a non-empty lps map of lp-type -> count"); + compiled_section& grp = cfg.explicit_lpgroups.add_subsection(gname); + grp.add_key("repetitions", std::to_string(reps)); + for (ryml::ConstNodeRef lp : g["lps"].children()) { + std::string lptype = key_of(lp); + std::string count_what = + "topology.groups \"" + gname + "\" lp \"" + lptype + "\" count"; + long count = parse_int_strict(scalar(lp), count_what.c_str()); + if (count <= 0) + throw config_error("config error: topology.groups: \"" + gname + "\": lp \"" + + lptype + "\" count must be positive"); + grp.add_key(lptype, std::to_string(count)); + } + } +} + +void parse_topology(ryml::ConstNodeRef root, friendly_config& cfg) { + if (!has(root, "topology")) + throw config_error("config error: missing required \"topology\" block"); + ryml::ConstNodeRef topo = root["topology"]; + + std::string format = has(topo, "format") ? scalar(topo["format"]) : std::string(); + + if (format == "parametric") { + cfg.parametric = true; + /* only these keys are consumed for a parametric topology. */ + for (ryml::ConstNodeRef c : topo.children()) { + std::string k = key_of(c); + if (k != "format" && k != "fabric" && k != "hosts") + throw config_error("config error: topology: unexpected key \"" + k + + "\" for a parametric topology"); + } + if (!has(topo, "fabric")) + throw config_error("config error: parametric topology needs a \"fabric\" block"); + parse_fabric(topo["fabric"], cfg.fab); + if (!has(topo, "hosts")) + throw config_error("config error: parametric topology needs hosts.component naming the " + "per-terminal workload"); + ryml::ConstNodeRef hosts = topo["hosts"]; + /* only `component` is consumed under hosts; anything else is an error + * rather than a silent drop. */ + for (ryml::ConstNodeRef h : hosts.children()) { + std::string k = key_of(h); + if (k != "component") + throw config_error("config error: topology.hosts: unexpected key \"" + k + + "\" (only \"component\" is supported)"); + } + if (!has(hosts, "component")) + throw config_error("config error: parametric topology needs hosts.component naming the " + "per-terminal workload"); + cfg.fab.hosts_component = scalar(hosts["component"]); + } else if (format == "flat") { + cfg.flat = true; + /* only these keys are consumed for a flat topology. */ + for (ryml::ConstNodeRef c : topo.children()) { + std::string k = key_of(c); + if (k != "format" && k != "component" && k != "nodes") + throw config_error("config error: topology: unexpected key \"" + k + + "\" for a flat topology"); + } + if (!has(topo, "component")) + throw config_error("config error: flat topology needs a \"component\" naming the " + "compute node (workload + its NIC model)"); + cfg.flat_component = scalar(topo["component"]); + if (!has(topo, "nodes")) + throw config_error("config error: flat topology needs a \"nodes\" count"); + cfg.node_count = parse_int_strict(scalar(topo["nodes"]), "topology.nodes"); + if (cfg.node_count <= 0) + throw config_error("config error: topology.nodes must be positive"); + } else if (format == "groups") { + cfg.explicit_groups = true; + /* only these keys are consumed for an explicit-groups topology. */ + for (ryml::ConstNodeRef c : topo.children()) { + std::string k = key_of(c); + if (k != "format" && k != "groups" && k != "params") + throw config_error("config error: topology: unexpected key \"" + k + + "\" for an explicit-groups topology"); + } + if (!has(topo, "groups")) + throw config_error("config error: explicit-groups topology needs a \"groups\" block"); + parse_explicit_groups(topo["groups"], cfg); + /* PARAMS is written out directly here (the compiler derives nothing for + * this form); a scalar/list/nested map passes through like any section. */ + if (has(topo, "params")) { + if (!topo["params"].is_map()) + throw config_error("config error: topology.params must be a map of key -> value"); + cfg.explicit_params = build_passthrough_section("PARAMS", topo["params"]); + } + } else if (format.empty()) { + throw config_error( + "config error: topology needs a \"format\" (flat, parametric, or groups)"); + } else { + throw config_error("config error: unknown topology format \"" + format + "\""); + } +} + +/* ------------------------------------------------------------------------- + * Pass-through sections (`sections:`) + * + * Config a model reads directly by name (DIRECTOR, NETWORK_SURROGATE, storage, + * resource, ...) is not something the compiler derives or transforms, so it is + * carried through verbatim rather than modeled key-by-key. A new feature can + * add its section here with no compiler change: write the block under + * `sections:` and read it in the model. Section names are case-insensitive + * (matched so at lookup time); a section may optionally register a schema below + * to enforce required keys while still allowing any other key through. + * ---------------------------------------------------------------------- */ + +bool iequals(const std::string& a, const char* b) { + return strcasecmp(a.c_str(), b) == 0; +} + +/* Recursively turn a ryml map node into a compiled_section: scalars become + * single-value keys, sequences multi-value keys, nested maps subsections. */ +compiled_section build_passthrough_section(const std::string& name, ryml::ConstNodeRef node) { + compiled_section sec{name, {}, {}}; + for (ryml::ConstNodeRef c : node.children()) { + std::string k = key_of(c); + if (c.is_seq()) { + std::vector vals; + for (ryml::ConstNodeRef v : c.children()) + vals.push_back(scalar(v)); + sec.add_key(std::move(k), std::move(vals)); + } else if (c.is_map()) { + sec.subsections.push_back(build_passthrough_section(k, c)); + } else if (c.is_keyval()) { + sec.add_key(std::move(k), scalar(c)); + } else { + throw config_error("config error: sections: \"" + name + "\": \"" + k + + "\" must be a scalar, a list, or a nested block"); + } + } + return sec; +} + +/* Optional, open schema for a pass-through section: it enforces required keys + * but does NOT restrict the rest, so a section can carry keys not listed here + * (useful while a feature's config is still in flux). A section with no entry is + * passed through entirely unvalidated. To register one, add a row -- see + * doc/dev/yaml-config.md ("Adding a config section"). */ +struct section_schema { + const char* name; /* section name, matched case-insensitively */ + const char* const* required; /* nullptr-terminated required key names */ +}; + +/* The resource LP aborts at runtime if its `resource` section lacks `available` + * (src/util/resource-lp.c); catch it here with a clearer, earlier diagnostic. */ +const char* const resource_required[] = {"available", nullptr}; + +const section_schema section_schemas[] = { + {"resource", resource_required}, +}; + +const section_schema* find_section_schema(const std::string& name) { + for (const section_schema& s : section_schemas) + if (iequals(name, s.name)) + return &s; + return nullptr; +} + +/* Required-key presence is checked case-sensitively: a model reads its keys by + * exact name, so the required key must be spelled as the model reads it. */ +bool section_has_key(const compiled_section& sec, const char* key) { + for (const compiled_key& k : sec.keys) + if (k.name == key) + return true; + return false; +} + +void validate_section_schema(const compiled_section& sec) { + const section_schema* schema = find_section_schema(sec.name); + if (!schema || !schema->required) + return; + for (const char* const* r = schema->required; *r; ++r) + if (!section_has_key(sec, *r)) + throw config_error("config error: section \"" + sec.name + + "\" is missing required key \"" + std::string(*r) + "\""); +} + +void parse_sections(ryml::ConstNodeRef root, friendly_config& cfg) { + if (!has(root, "sections")) + return; + ryml::ConstNodeRef secs = root["sections"]; + if (!secs.is_map()) + throw config_error("config error: \"sections\" must be a map of section-name -> keys"); + for (ryml::ConstNodeRef s : secs.children()) { + std::string name = key_of(s); + if (!s.is_map()) + throw config_error("config error: sections: \"" + name + "\" must be a block of keys"); + /* the compiler emits LPGROUPS and PARAMS from the topology; a pass-through + * section must not shadow them. */ + if (iequals(name, "LPGROUPS") || iequals(name, "PARAMS")) + throw config_error("config error: sections: \"" + name + + "\" is reserved -- the compiler emits it from the topology; put " + "model parameters on the component or fabric instead"); + compiled_section sec = build_passthrough_section(name, s); + validate_section_schema(sec); + /* include-merge: a later document's section overrides an earlier one of + * the same (case-insensitive) name. */ + cfg.passthrough.erase(std::remove_if(cfg.passthrough.begin(), cfg.passthrough.end(), + [&](const compiled_section& e) { + return iequals(e.name, sec.name.c_str()); + }), + cfg.passthrough.end()); + cfg.passthrough.push_back(std::move(sec)); + } +} + +/* Parse one document with our throwing error handler installed (so ryml's own + * parse errors route through config_error, exactly like our validation errors), + * check it is a top-level map, and hand its root to `fn`. The parser and tree + * live for the duration of `fn`; the friendly IR copies out owned strings, so + * nothing references the tree afterward. */ +template void with_parsed_document(std::string_view text, F&& fn) { + ryml::Callbacks cb; + cb.set_error_basic(ryml_throw); + cb.set_error_parse(ryml_throw_parse); + ryml::EventHandlerTree evt_handler(cb); + ryml::Parser parser(&evt_handler); + ryml::Tree tree = ryml::parse_in_arena(&parser, ryml::to_csubstr(""), + ryml::csubstr(text.data(), text.size())); + ryml::ConstNodeRef root = tree.rootref(); + if (!root.readable() || !root.is_map()) + throw config_error("config error: config must be a YAML/JSON mapping at the top level"); + fn(root); +} + +/* reject unknown top-level keys rather than silently ignoring them. An + * included (base) document may not itself use `include` -- nested includes are + * not supported. */ +void validate_toplevel_keys(ryml::ConstNodeRef root, bool is_base) { + for (ryml::ConstNodeRef c : root.children()) { + std::string k = key_of(c); + if (k != "schema_version" && k != "components" && k != "topology" && k != "sections" && + k != "include") + throw config_error("config error: unexpected top-level key \"" + k + "\""); + if (k == "include" && is_base) + throw config_error("config error: an included file cannot itself use \"include\" " + "(nested includes are not supported)"); + } +} + +/* schema_version is required (on the main document), integer, and any value + * this build doesn't know is a hard error -- a newer config can't be interpreted + * safely. */ +void require_schema_version(ryml::ConstNodeRef root) { + if (!has(root, "schema_version")) + throw config_error( + "config error: missing required top-level \"schema_version\" (this build understands " + "version 1)"); + long v = parse_int_strict(scalar(root["schema_version"]), "schema_version"); + if (v != 1) + throw config_error("config error: unsupported schema_version " + std::to_string(v) + + "; this build understands version 1"); +} + +/* An included document need not restate schema_version, but if it does it must + * agree with this build. */ +void validate_schema_version_if_present(ryml::ConstNodeRef root) { + if (has(root, "schema_version")) + require_schema_version(root); +} + +/* Clear any topology state so a later document's topology fully replaces an + * earlier one (local-overrides-included). */ +void reset_topology(friendly_config& cfg) { + cfg.parametric = false; + cfg.flat = false; + cfg.explicit_groups = false; + cfg.fab = fabric{}; + cfg.flat_component.clear(); + cfg.node_count = 0; + cfg.explicit_lpgroups = compiled_section{"LPGROUPS", {}, {}}; + cfg.explicit_params = compiled_section{"PARAMS", {}, {}}; +} + +/* Merge one document into the accumulating friendly config. Components and + * sections override by name; a topology block replaces any earlier one. */ +void merge_document(ryml::ConstNodeRef root, friendly_config& cfg) { + parse_components(root, cfg); + if (has(root, "topology")) { + reset_topology(cfg); + parse_topology(root, cfg); + } + parse_sections(root, cfg); +} + +/* ------------------------------------------------------------------------- + * Compile: friendly IR -> compiled_config + * ---------------------------------------------------------------------- */ + +/* Compile a parametric fabric into LPGROUPS + PARAMS. */ +void compile_fabric(const friendly_config& cfg, compiled_config& out) { + const fabric& fab = cfg.fab; + + const fabric_model* model = find_fabric_model(fab.model); + if (!model) + throw config_error("config error: unknown fabric model \"" + fab.model + "\""); + + const component* host = cfg.find_component(fab.hosts_component); + if (!host) + throw config_error("config error: hosts.component \"" + fab.hosts_component + + "\" is not defined under components:"); + /* An empty model: would flow into LPGROUPS as an empty LP-type key and fail + * confusingly in codes_mapping later; reject it here with a clear message. */ + if (host->model.empty()) + throw config_error("config error: hosts.component \"" + fab.hosts_component + + "\" has an empty \"model:\"; a component needs a model naming its " + "workload LP type"); + /* A parametric fabric defines the network itself, so a network: on the host + * component is meaningless here (it only applies to flat-topology components) + * and would otherwise be silently ignored. */ + if (!host->network.empty()) + throw config_error("config error: hosts.component \"" + fab.hosts_component + + "\" sets \"network:\", which is only meaningful for a flat-topology " + "component; a parametric fabric defines the network itself"); + + layout lay = model->derive(fab.shape); + + /* A backstop over every model's derivation: a shape that produces a + * degenerate layout (e.g. num_groups: 0) must not reach codes_mapping. Each + * repetition needs at least one terminal; routers may legitimately be 0 for + * mesh-style fabrics that fold routing into the terminal. */ + if (lay.repetitions <= 0 || lay.terminals_per_rep <= 0 || lay.routers_per_rep < 0) + throw config_error( + "config error: fabric model \"" + fab.model + + "\" derived a degenerate layout (repetitions=" + std::to_string(lay.repetitions) + + ", terminals_per_rep=" + std::to_string(lay.terminals_per_rep) + ", routers_per_rep=" + + std::to_string(lay.routers_per_rep) + "); check the shape values"); + + /* --- LPGROUPS: one group of `repetitions` slices, each with the + * per-terminal workload + NIC LPs and the router/switch LPs, emitted in + * [workload, terminal, router] order to match the layout the model + * expects. --- */ + compiled_section& grp = out.add_section("LPGROUPS").add_subsection("MODELNET_GRP"); + grp.add_key("repetitions", std::to_string(lay.repetitions)); + grp.add_key(host->model, std::to_string(lay.terminals_per_rep)); + grp.add_key(model->terminal_lp, std::to_string(lay.terminals_per_rep)); + /* Mesh-style fabrics (torus) fold routing into the terminal node and have no + * separate router LP; skip the router line for them. */ + if (model->router_lp) + grp.add_key(model->router_lp, std::to_string(lay.routers_per_rep)); + + /* --- PARAMS --- */ + compiled_section& params = out.add_section("PARAMS"); + + /* modelnet_order is derived from the fabric model: the terminal, plus the + * router when it is a distinct model-net method. */ + if (model->router_method) + params.add_key("modelnet_order", + std::vector{model->term_method, model->router_method}); + else + params.add_key("modelnet_order", std::vector{model->term_method}); + + /* shape parameters pass straight through (num_routers etc.). */ + for (const auto& kv : fab.shape) + params.add_key(kv.first, kv.second); + + /* per-link-class params become _ (local_bandwidth, ...). */ + for (const link_class& cls : fab.links) + for (const auto& kv : cls.params) + params.add_key(cls.name + "_" + kv.first, kv.second); + + /* routing.algorithm -> "routing"; any other routing.* passes through. */ + for (const auto& kv : fab.routing) + params.add_key(kv.first == "algorithm" ? std::string("routing") : kv.first, kv.second); + + /* connections.{intra,inter} -> the file-enumerated model's connection-file + * keys; paths pass through verbatim (the model reads them relative to the + * working directory). */ + for (const auto& kv : fab.connections) { + if (kv.first == "intra") + params.add_key("intra-group-connections", kv.second); + else if (kv.first == "inter") + params.add_key("inter-group-connections", kv.second); + else + params.add_key(kv.first, kv.second); + } + + /* remaining scalar fabric keys (packet_size, chunk_size, parity pass-through + * knobs) map to PARAMS verbatim. */ + for (const auto& kv : fab.extra) + params.add_key(kv.first, kv.second); + + /* list-valued fabric keys (slimfly's generator_set_X / _X_prime) emit as + * multi-value PARAMS, e.g. generator_set_X=("1","4"). */ + for (const auto& kv : fab.extra_lists) + params.add_key(kv.first, kv.second); + + /* the workload component's own params (if any) also land in PARAMS. */ + for (const auto& kv : host->params) + params.add_key(kv.first, kv.second); +} + +/* Compile a flat (enumerated) network into LPGROUPS + PARAMS: `node_count` + * peer compute nodes, each one repetition running the component's workload LP + * over its NIC LP. simplep2p's link table stays referenced by path in the + * component params; the friendly form supplies only the node count. */ +void compile_flat(const friendly_config& cfg, compiled_config& out) { + const component* comp = cfg.find_component(cfg.flat_component); + if (!comp) + throw config_error("config error: topology.component \"" + cfg.flat_component + + "\" is not defined under components:"); + /* An empty model: would flow into LPGROUPS as an empty LP-type key and fail + * confusingly in codes_mapping later; reject it here with a clear message. */ + if (comp->model.empty()) + throw config_error("config error: topology.component \"" + cfg.flat_component + + "\" has an empty \"model:\"; a component needs a model naming its " + "workload LP type"); + if (comp->network.empty()) + throw config_error("config error: component \"" + cfg.flat_component + + "\" needs a network: field naming its NIC model"); + + const network_model* net = find_network_model(comp->network); + if (!net) + throw config_error("config error: unknown network model \"" + comp->network + "\""); + + /* --- LPGROUPS: one repetition per node, each a workload LP + its NIC LP, + * emitted in [workload, NIC] order to match the model's layout. --- */ + compiled_section& grp = out.add_section("LPGROUPS").add_subsection("MODELNET_GRP"); + grp.add_key("repetitions", std::to_string(cfg.node_count)); + grp.add_key(comp->model, "1"); + grp.add_key(net->nic_lp, "1"); + + /* --- PARAMS: modelnet_order from the network model; the component's params + * (message_size, packet_size, simplep2p's matrix-file references, ...) pass + * straight through. --- */ + compiled_section& params = out.add_section("PARAMS"); + params.add_key("modelnet_order", std::vector{net->method}); + for (const auto& kv : comp->params) + params.add_key(kv.first, kv.second); +} + +} // namespace + +std::vector parse_includes(std::string_view doc) { + std::vector out; + with_parsed_document(doc, [&](ryml::ConstNodeRef root) { + if (!has(root, "include")) + return; + ryml::ConstNodeRef inc = root["include"]; + if (inc.is_seq()) { + for (ryml::ConstNodeRef c : inc.children()) { + if (!c.has_val()) + throw config_error("config error: \"include\" list must contain filenames"); + out.push_back(scalar(c)); + } + } else if (inc.has_val()) { + out.push_back(scalar(inc)); + } else { + throw config_error( + "config error: \"include\" must be a filename or a list of filenames"); + } + }); + return out; +} + +compiled_config compile(std::string_view main_doc, const std::vector& base_docs) { + friendly_config cfg; + + /* Included documents are the base; the main document overrides them. Merge + * the bases first, in listed order, then the main document last. */ + for (const std::string& doc : base_docs) + with_parsed_document(doc, [&](ryml::ConstNodeRef root) { + validate_toplevel_keys(root, /*is_base=*/true); + validate_schema_version_if_present(root); + merge_document(root, cfg); + }); + with_parsed_document(main_doc, [&](ryml::ConstNodeRef root) { + validate_toplevel_keys(root, /*is_base=*/false); + require_schema_version(root); + merge_document(root, cfg); + }); + + if (!cfg.parametric && !cfg.flat && !cfg.explicit_groups) + throw config_error("config error: missing required \"topology\" block"); + + compiled_config out; + if (cfg.explicit_groups) { + /* explicit form: LPGROUPS and PARAMS were built verbatim during parse. */ + out.sections.push_back(std::move(cfg.explicit_lpgroups)); + out.sections.push_back(std::move(cfg.explicit_params)); + } else if (cfg.parametric) + compile_fabric(cfg, out); + else if (cfg.flat) + compile_flat(cfg, out); + + /* verbatim `sections:` blocks follow the compiler-derived topology sections. */ + for (compiled_section& s : cfg.passthrough) + out.sections.push_back(std::move(s)); + return out; +} + +} // namespace config +} // namespace codes diff --git a/src/modelconfig/config_compiler.h b/src/modelconfig/config_compiler.h new file mode 100644 index 00000000..f76d6439 --- /dev/null +++ b/src/modelconfig/config_compiler.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2013 University of Chicago. + * See COPYRIGHT notice in top-level directory. + * + */ + +#ifndef SRC_MODELCONFIG_CONFIG_COMPILER_H +#define SRC_MODELCONFIG_CONFIG_COMPILER_H + +/** + * @file config_compiler.h + * + * Pure C++ core of the YAML/JSON configuration front-end. + * + * codes::config::compile() turns the user-friendly topology + component text + * into a @ref codes::config::compiled_config -- an ordered, plain-data image of + * the LPGROUPS/PARAMS configuration the legacy `.conf` parser produces. It does + * all parsing and validation and has **no** dependency on ROSS, MPI, or + * `abort`: on any invalid input it throws @ref codes::config::config_error. + * + * The dumb emitter (config_emitter.h) turns a compiled_config into a + * ConfigVTable, and a thin `extern "C"` shim (yaml_configfile.h) is the *only* + * place that translates a config_error into ROSS's `tw_error`. Keeping the core + * free of ROSS makes it unit-testable in isolation: tests drive compile() and + * assert on the returned compiled_config, never on simulator behavior. This + * "pure core that throws + boundary shim that owns the tw_error translation" is + * the template for future C++ subsystems in codes. + */ + +#include +#include +#include +#include + +namespace codes { +namespace config { + +/** + * Thrown by compile() on any syntax or validation failure. The message is a + * finished, user-facing diagnostic; the shim hands it verbatim to tw_error. + */ +struct config_error : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +/** One key and its value(s): a scalar carries one value, a list-valued key + * (e.g. `modelnet_order`) several, emitted as `("a","b",...)`. */ +struct compiled_key { + std::string name; ///< the key name, as it is emitted + std::vector values; ///< one value for a scalar; several for a list-valued key +}; + +/** One configuration section: named keys plus nested subsections. LPGROUPS + * holds a MODELNET_GRP subsection; PARAMS is flat. Insertion order is + * preserved throughout, so a compiled config is byte-comparable to the `.conf` + * it replaces. */ +struct compiled_section { + std::string name; ///< section name (e.g. LPGROUPS, PARAMS) + std::vector keys; ///< this section's keys, in insertion order + std::vector subsections; ///< nested subsections, in insertion order + + /** Append a scalar key. */ + void add_key(std::string key, std::string value); + /** Append a list-valued key. */ + void add_key(std::string key, std::vector values); + /** Append and return a nested subsection. */ + compiled_section& add_subsection(std::string subname); +}; + +/** The whole compiled config: the top-level (ROOT) sections, in order. */ +struct compiled_config { + std::vector sections; ///< top-level (ROOT) sections, in order + + /** Append and return a top-level section. */ + compiled_section& add_section(std::string name); +}; + +/** + * Compile YAML/JSON configuration text into the compiled_config IR. + * + * @param main_doc the raw config bytes of the top-level file (JSON is a subset + * of YAML, so one parser handles both). + * @param base_docs the contents of any files named by `main_doc`'s top-level + * `include:` list, already read (the pure core does no file + * I/O), in listed order. They are merged as the base; + * `main_doc` overrides them (components and sections merge by + * name, a topology block replaces any earlier one). + * @throws config_error on malformed YAML or any schema violation. + */ +compiled_config compile(std::string_view main_doc, const std::vector& base_docs = {}); + +/** + * Extract a document's top-level `include:` list (filenames), or an empty vector + * if it has none. The loader boundary uses this to read the referenced files and + * pass them to compile() as base documents, keeping this core free of file I/O. + * + * @throws config_error on malformed YAML or a malformed `include:` value. + */ +std::vector parse_includes(std::string_view doc); + +} // namespace config +} // namespace codes + +#endif diff --git a/src/modelconfig/config_emitter.cxx b/src/modelconfig/config_emitter.cxx new file mode 100644 index 00000000..6da6fd05 --- /dev/null +++ b/src/modelconfig/config_emitter.cxx @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2013 University of Chicago. + * See COPYRIGHT notice in top-level directory. + * + */ + +#include "config_emitter.h" + +#include "configstoreadapter.h" +#include + +#include + +namespace codes { +namespace config { + +namespace { + +void emit_keys(ConfigVTable* cf, SectionHandle sec, const std::vector& keys) { + for (const compiled_key& k : keys) { + std::vector vals; + vals.reserve(k.values.size()); + for (const std::string& v : k.values) + vals.push_back(v.c_str()); + cf_createKey(cf, sec, k.name.c_str(), vals.data(), (unsigned int)vals.size()); + } +} + +void emit_section(ConfigVTable* cf, SectionHandle parent, const compiled_section& s) { + SectionHandle sec; + cf_createSection(cf, parent, s.name.c_str(), &sec); + emit_keys(cf, sec, s.keys); + for (const compiled_section& sub : s.subsections) + emit_section(cf, sec, sub); +} + +} // namespace + +ConfigVTable* emit(const compiled_config& cfg) { + ConfigVTable* cf = cfsa_create_empty(); + for (const compiled_section& s : cfg.sections) + emit_section(cf, ROOT_SECTION, s); + return cf; +} + +} // namespace config +} // namespace codes diff --git a/src/modelconfig/config_emitter.h b/src/modelconfig/config_emitter.h new file mode 100644 index 00000000..535d9561 --- /dev/null +++ b/src/modelconfig/config_emitter.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 University of Chicago. + * See COPYRIGHT notice in top-level directory. + * + */ + +#ifndef SRC_MODELCONFIG_CONFIG_EMITTER_H +#define SRC_MODELCONFIG_CONFIG_EMITTER_H + +/** + * @file config_emitter.h + * + * The dumb second stage of the YAML config front-end: it turns the plain-data + * @ref codes::config::compiled_config the core produced into a ConfigVTable -- + * the same structure the legacy `.conf` text parser yields, so codes_mapping and + * every model read it unchanged through the configuration_get_* accessors. + * + * It is pure data-to-data: it does no validation (compile() already did) and no + * ROSS/tw_error. Its coverage comes from the config-equivalence tests, which + * check a compiled `.yaml` against its golden `.conf`. + */ + +#include "config_compiler.h" + +struct ConfigVTable; + +namespace codes { +namespace config { + +/** + * Emit a compiled_config as a freshly-allocated ConfigVTable (free with + * cf_free). Sections, subsections, keys, and list values are emitted in the + * order the core recorded them. + */ +struct ::ConfigVTable* emit(const compiled_config& cfg); + +} // namespace config +} // namespace codes + +#endif diff --git a/src/modelconfig/configstore.c b/src/modelconfig/configstore.c index 4af9b9e9..06081be8 100644 --- a/src/modelconfig/configstore.c +++ b/src/modelconfig/configstore.c @@ -7,6 +7,7 @@ #include "codes_config.h" #include #include +#include /* strcasecmp: case-insensitive section-name matching */ #include #ifdef HAVE_MALLOC_H #include @@ -273,13 +274,20 @@ static mcs_entry* mcs_findchild(const mcs_entry* e, const char* name) { } +/* Section names are case-insensitive. The all-caps convention (PARAMS, + * LPGROUPS, DIRECTOR, ...) is a historical carryover; the YAML front-end lets a + * config use any case, so section lookups must not depend on it. Keys stay + * case-sensitive -- models read them by exact name -- so only this section-only + * lookup is insensitive, not mcs_findkey. */ mcs_entry* mcs_findsubsection(const mcs_entry* e, const char* name) { - mcs_entry* ret = mcs_findchild(e, name); - if (!ret) - return 0; - if (!ret->is_section) - return 0; - return ret; + mcs_entry* curchild = e->child; + + while (curchild) { + if (curchild->is_section && curchild->name && !strcasecmp(curchild->name, name)) + return curchild; + curchild = curchild->next; + } + return 0; } mcs_entry* mcs_findkey(const mcs_entry* e, const char* name) { diff --git a/src/modelconfig/configstore.h b/src/modelconfig/configstore.h index 262836a6..e4f13f8a 100644 --- a/src/modelconfig/configstore.h +++ b/src/modelconfig/configstore.h @@ -59,7 +59,8 @@ int mcs_getvaluemultiple(const mcs_entry* e, char** buf, unsigned int* maxcount) * value. */ int mcs_getvaluesingle(const mcs_entry* e, char* buf, unsigned int bufsize); -/* Lookup the named subsection in section */ +/* Lookup the named subsection in section; the name match is case-insensitive + * (keys, looked up by mcs_findkey, stay case-sensitive). */ mcs_entry* mcs_findsubsection(const mcs_entry* e, const char* name); /* Lookup the named key in section */ diff --git a/src/modelconfig/configuration.c b/src/modelconfig/configuration.c index 377fe446..5e057a7e 100644 --- a/src/modelconfig/configuration.c +++ b/src/modelconfig/configuration.c @@ -7,6 +7,7 @@ #include #include #include +#include /* strcasecmp: the extension match is case-insensitive */ #include #include #include @@ -15,6 +16,19 @@ #include #include "txt_configfile.h" +#include "yaml_configfile.h" + +/* A .yaml/.yml/.json path selects the YAML/JSON config front-end; anything else + * is parsed by the legacy .conf text parser. The extension is matched + * case-insensitively so .YAML/.Yml/.JSON select the front-end too rather than + * falling through and dying with an unrelated .conf syntax error. */ +static int config_is_yaml(const char* path) { + const char* dot = strrchr(path, '.'); + if (!dot) + return 0; + return strcasecmp(dot, ".yaml") == 0 || strcasecmp(dot, ".yml") == 0 || + strcasecmp(dot, ".json") == 0; +} /* * Global to hold configuration in memory @@ -33,6 +47,12 @@ int configuration_load(const char* filepath, MPI_Comm comm, ConfigHandle* handle char* error = NULL; int rc = 0; char* tmp_path = NULL; + /* Top-level `include:` files: resolved paths, per-file byte buffers, and + * their lengths. All read collectively below and freed at finalize. */ + char** inc_paths = NULL; + char** inc_data = NULL; + size_t* inc_lens = NULL; + size_t n_inc = 0; rc = MPI_File_open(comm, (char*)filepath, MPI_MODE_RDONLY, MPI_INFO_NULL, &fh); if (rc != MPI_SUCCESS) @@ -49,20 +69,67 @@ int configuration_load(const char* filepath, MPI_Comm comm, ConfigHandle* handle if (rc != MPI_SUCCESS) goto finalize; + if (config_is_yaml(filepath)) { + /* the YAML front-end compiles the friendly format into the same + * ConfigVTable the text parser yields, consuming the bytes already + * pulled in by the collective read above. A top-level `include:` list + * names extra config fragments; resolve those paths (relative to the + * config's directory) and read EACH with the same collective MPI pattern + * as the main file, so at scale we do one collective read per file + * rather than one POSIX open per rank per include. */ + inc_paths = yaml_configfile_list_includes(txtdata, txtsize, filepath, &n_inc); + if (n_inc > 0) { + inc_data = (char**)calloc(n_inc, sizeof(*inc_data)); + inc_lens = (size_t*)malloc(n_inc * sizeof(*inc_lens)); + assert(inc_data && inc_lens); + for (size_t i = 0; i < n_inc; i++) { + MPI_File inc_fh = MPI_FILE_NULL; + MPI_Offset inc_size = 0; + + /* A collective failure here (missing/unreadable include) is seen + * by every rank -- the whole job holds identical config bytes -- + * so aborting via tw_error is safe and, unlike the main-file + * open's bare rc, names the resolved path so it's debuggable. */ + rc = MPI_File_open(comm, inc_paths[i], MPI_MODE_RDONLY, MPI_INFO_NULL, &inc_fh); + if (rc != MPI_SUCCESS) + tw_error(TW_LOC, "config error: cannot open included file \"%s\"", + inc_paths[i]); + + rc = MPI_File_get_size(inc_fh, &inc_size); + if (rc != MPI_SUCCESS) + tw_error(TW_LOC, "config error: cannot stat included file \"%s\"", + inc_paths[i]); + + inc_data[i] = (char*)malloc(inc_size ? (size_t)inc_size : 1); + assert(inc_data[i]); + + rc = MPI_File_read_all(inc_fh, inc_data[i], inc_size, MPI_BYTE, &status); + if (rc != MPI_SUCCESS) + tw_error(TW_LOC, "config error: cannot read included file \"%s\"", + inc_paths[i]); + + inc_lens[i] = (size_t)inc_size; + MPI_File_close(&inc_fh); + } + } + *handle = + yaml_configfile_load(txtdata, txtsize, (const char* const*)inc_data, inc_lens, n_inc); + } else { #ifdef __APPLE__ - f = fopen(filepath, "r"); + f = fopen(filepath, "r"); #else - f = fmemopen(txtdata, txtsize, "rb"); + f = fmemopen(txtdata, txtsize, "rb"); #endif - if (!f) { - rc = 1; - goto finalize; - } + if (!f) { + rc = 1; + goto finalize; + } - *handle = txtfile_openStream(f, &error); - if (error) { - rc = 1; - goto finalize; + *handle = txtfile_openStream(f, &error); + if (error) { + rc = 1; + goto finalize; + } } /* NOTE: posix version overwrites argument :(. */ @@ -80,6 +147,13 @@ int configuration_load(const char* filepath, MPI_Comm comm, ConfigHandle* handle fclose(f); free(txtdata); free(tmp_path); + if (inc_data) { + for (size_t i = 0; i < n_inc; i++) + free(inc_data[i]); + free(inc_data); + } + free(inc_lens); + yaml_configfile_free_includes(inc_paths, n_inc); if (error) { fprintf(stderr, "config error: %s\n", error); free(error); diff --git a/src/modelconfig/yaml_configfile.cxx b/src/modelconfig/yaml_configfile.cxx new file mode 100644 index 00000000..56efd4e7 --- /dev/null +++ b/src/modelconfig/yaml_configfile.cxx @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2013 University of Chicago. + * See COPYRIGHT notice in top-level directory. + * + */ + +/* + * Thin extern "C" boundary of the YAML/JSON configuration front-end. It wires + * the pure C++ core (config_compiler.h -- parse + validate, throwing on error) + * to the dumb emitter (config_emitter.h -- IR -> ConfigVTable), and it is the + * ONLY place in the front-end that touches ROSS: a config_error thrown by the + * core is translated here into tw_error. Real-run behavior (abort + diagnostic) + * is therefore unchanged from the .conf path, while "abort" stays a boundary + * policy rather than being baked into every validation site in the core. + * + * Determinism: every rank runs the identical deterministic compile over + * identical bytes -- the top-level config and every included file are read + * collectively by the loader (configuration_load), so malformed input throws on + * every rank and this tw_error fires everywhere at once. The shim itself does no + * file I/O: it consumes the bytes it is handed. It only computes the *names* of + * the include files (yaml_configfile_list_includes), leaving the reads to the + * loader's collective path. + */ + +#include "yaml_configfile.h" + +#include "config_compiler.h" +#include "config_emitter.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +/* Directory portion of a path ("a/b/c.yaml" -> "a/b"), or "." if none. */ +std::string dir_of(const char* path) { + std::string p = path ? path : ""; + std::string::size_type slash = p.find_last_of('/'); + return slash == std::string::npos ? std::string(".") : p.substr(0, slash); +} + +/* Resolve an include path: absolute as-is, otherwise relative to base_dir. */ +std::string resolve_path(const std::string& base_dir, const std::string& rel) { + if (!rel.empty() && rel[0] == '/') + return rel; + return base_dir + "/" + rel; +} + +/* malloc a NUL-terminated copy of s, for handoff to a C caller that frees it. */ +char* dup_cstr(const std::string& s) { + char* out = static_cast(std::malloc(s.size() + 1)); + if (!out) + throw std::bad_alloc(); + std::memcpy(out, s.c_str(), s.size() + 1); + return out; +} + +} // namespace + +char** yaml_configfile_list_includes(const char* data, size_t len, const char* path, + size_t* count) { + try { + std::vector rels = codes::config::parse_includes(std::string_view(data, len)); + *count = rels.size(); + if (rels.empty()) + return nullptr; + std::string base_dir = dir_of(path); + char** paths = static_cast(std::malloc(rels.size() * sizeof(char*))); + if (!paths) + throw std::bad_alloc(); + for (size_t i = 0; i < rels.size(); i++) + paths[i] = dup_cstr(resolve_path(base_dir, rels[i])); + return paths; + } catch (const codes::config::config_error& e) { + tw_error(TW_LOC, "%s", e.what()); + return nullptr; /* unreachable: tw_error aborts */ + } catch (const std::exception& e) { + tw_error(TW_LOC, "config error (internal): %s", e.what()); + return nullptr; /* unreachable: tw_error aborts */ + } +} + +void yaml_configfile_free_includes(char** paths, size_t count) { + if (!paths) + return; + for (size_t i = 0; i < count; i++) + std::free(paths[i]); + std::free(paths); +} + +struct ConfigVTable* yaml_configfile_load(const char* data, size_t len, const char* const* inc_data, + const size_t* inc_lens, size_t n_inc) { + try { + std::string_view main_doc(data, len); + std::vector base_docs; + base_docs.reserve(n_inc); + for (size_t i = 0; i < n_inc; i++) + base_docs.emplace_back(inc_data[i], inc_lens[i]); + codes::config::compiled_config cfg = codes::config::compile(main_doc, base_docs); + return codes::config::emit(cfg); + } catch (const codes::config::config_error& e) { + tw_error(TW_LOC, "%s", e.what()); + return nullptr; /* unreachable: tw_error aborts */ + } catch (const std::exception& e) { + /* Any non-config_error exception (std::bad_alloc, a ryml internal, ...) + * must not escape this extern "C" frame -- that would call + * std::terminate with no diagnostic. Route it to tw_error too. */ + tw_error(TW_LOC, "config error (internal): %s", e.what()); + return nullptr; /* unreachable: tw_error aborts */ + } +} diff --git a/src/modelconfig/yaml_configfile.h b/src/modelconfig/yaml_configfile.h new file mode 100644 index 00000000..f6dc29c8 --- /dev/null +++ b/src/modelconfig/yaml_configfile.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2013 University of Chicago. + * See COPYRIGHT notice in top-level directory. + * + */ + +#ifndef SRC_MODELCONFIG_YAML_CONFIGFILE_H +#define SRC_MODELCONFIG_YAML_CONFIGFILE_H + +/** + * @file yaml_configfile.h + * + * Thin `extern "C"` boundary of the YAML/JSON configuration front-end: the only + * place that runs the pure C++ core (config_compiler.h) plus the emitter + * (config_emitter.h) and translates a compile-time config_error into ROSS's + * `tw_error`. Everything here consumes bytes and does no file I/O of its own -- + * the loader (configuration_load) performs the collective reads and hands the + * bytes in. + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Resolve a config's top-level `include:` list into filesystem paths. + * + * Parses @p data only to discover the include names and joins each with the + * config's directory; it does NO file I/O -- the loader performs the actual + * (collective) reads. A relative include is resolved against the directory of + * @p path; an absolute include passes through unchanged. + * + * @param data the raw bytes of the top-level config file. + * @param len length of @p data in bytes. + * @param path path of the top-level config file, used as the base directory + * for resolving relative includes. + * @param count out: the number of resolved paths returned (0, with a NULL + * return, if the config has no `include:`). + * @return a newly-malloc'd array of @p *count newly-malloc'd, NUL-terminated + * resolved paths, in listed order (NULL when there are none). The + * caller owns the result and must release it with + * yaml_configfile_free_includes (equivalently: free() each element, + * then the array). + * @note On malformed YAML it aborts through `tw_error`, just like + * yaml_configfile_load; because every rank holds the identical config + * bytes from the collective read, that abort fires on every rank at once. + */ +char** yaml_configfile_list_includes(const char* data, size_t len, const char* path, size_t* count); + +/** + * Free an array returned by yaml_configfile_list_includes: frees each element + * and then the array. + * + * @param paths the array to free; a NULL @p paths (as returned for a config + * with no includes) is a no-op. + * @param count the element count that yaml_configfile_list_includes reported + * through its @p count out-parameter. + */ +void yaml_configfile_free_includes(char** paths, size_t count); + +/** + * Compile a YAML/JSON config (the user-friendly topology + component format) + * into the same ConfigVTable the legacy `.conf` text parser produces, so that + * codes_mapping and every model read it unchanged through configuration_get_*. + * + * This is the thin C boundary of the front-end: it runs the pure C++ core + * (compile) and the emitter (emit), and it is the only place that translates a + * compile-time config_error into a ROSS `tw_error`. It consumes bytes and does + * no file I/O of its own; the included files are passed in already read -- e.g. + * via the MPI collective read in configuration_load -- so they must be readable + * by every rank. + * + * @param data the raw bytes of the top-level config file. + * @param len length of @p data in bytes. + * @param inc_data the raw bytes of each of the @p n_inc included files, in the + * order yaml_configfile_list_includes reported them; may be + * NULL when @p n_inc is 0. + * @param inc_lens length of each buffer in @p inc_data; may be NULL when + * @p n_inc is 0. + * @param n_inc number of included files (0 when the config has no + * `include:`). + * @return a newly-allocated ConfigVTable (free with cf_free). + * @note On a syntax or validation error it aborts through `tw_error` with a + * diagnostic, matching how the rest of the configuration front-end reports + * malformed input. + */ +struct ConfigVTable* yaml_configfile_load(const char* data, size_t len, const char* const* inc_data, + const size_t* inc_lens, size_t n_inc); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/src/network-workloads/README_traces.txt b/src/network-workloads/README_traces.txt index ec395b18..401a526f 100644 --- a/src/network-workloads/README_traces.txt +++ b/src/network-workloads/README_traces.txt @@ -74,7 +74,7 @@ src/network-workloads/conf/allocation-rand.conf, allocation-cont.conf. 12- Run the simulation with multiple job allocations ./src/network-workloads//model-net-mpi-replay --sync=1 ---workload_conf_file=../src/network-workloads/workloads.conf +--workload_conf_file=../src/network-workloads/conf/workloads.conf --alloc_file=../src/network-workloads/conf/allocation-rand.conf --workload_type="dumpi" -- ../src/network-workloads/conf/modelnet-mpi-test-dfly-amg-216.conf @@ -82,7 +82,7 @@ src/network-workloads/conf/allocation-rand.conf, allocation-cont.conf. To run in optimistic mode: mpirun -np 4 ./src/network-workloads//model-net-mpi-replay --sync=3 ---workload_conf_file=../src/network-workloads/workloads.conf --alloc_file=allocation.conf +--workload_conf_file=../src/network-workloads/conf/workloads.conf --alloc_file=allocation.conf --workload_type="dumpi" -- ../src/network-workloads/conf/modelnet-mpi-test-dfly-amg-216.conf diff --git a/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-1728-nodes.yaml.in b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-1728-nodes.yaml.in new file mode 100644 index 00000000..d58652f3 --- /dev/null +++ b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-1728-nodes.yaml.in @@ -0,0 +1,36 @@ +schema_version: 1 + +# YAML twin of modelnet-test-dragonfly-1728-nodes.conf.in: a file-enumerated +# dragonfly-custom (6x16 router mesh per group, 9 groups -> 864 routers). +# Connection files are referenced by @abs_srcdir@-relative path (substituted at +# configure time). Shape counts must match those files. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-custom + shape: + num_router_rows: 6 + num_router_cols: 16 + num_groups: 9 + num_cns_per_router: 2 + num_global_channels: 10 + links: + local: { bandwidth: 5.25, vc_size: 8192 } + global: { bandwidth: 18.75, vc_size: 16384 } + cn: { bandwidth: 16.0, vc_size: 8192 } + routing: + algorithm: adaptive + connections: + intra: "@abs_srcdir@/intra-theta" + inter: "@abs_srcdir@/inter-theta" + packet_size: 1024 + chunk_size: 1024 + message_size: 768 + modelnet_scheduler: fcfs + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-custom-768-nodes.yaml.in b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-custom-768-nodes.yaml.in new file mode 100644 index 00000000..ec85fd6e --- /dev/null +++ b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-custom-768-nodes.yaml.in @@ -0,0 +1,36 @@ +schema_version: 1 + +# YAML twin of modelnet-test-dragonfly-custom-768-nodes.conf.in: a file- +# enumerated dragonfly-custom (16x6 router mesh per group, 8 groups -> 768 +# routers). Connection files are referenced by @abs_srcdir@-relative path +# (substituted at configure time). Shape counts must match those files. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-custom + shape: + num_router_rows: 16 + num_router_cols: 6 + num_groups: 8 + num_cns_per_router: 2 + num_global_channels: 10 + links: + local: { bandwidth: 5.25, vc_size: 8192 } + global: { bandwidth: 18.75, vc_size: 16384 } + cn: { bandwidth: 8.0, vc_size: 8192 } + routing: + algorithm: prog-adaptive + connections: + intra: "@abs_srcdir@/intra-theta" + inter: "@abs_srcdir@/inter-theta" + packet_size: 1024 + chunk_size: 1024 + message_size: 592 + modelnet_scheduler: fcfs + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-custom.conf b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-custom.conf index ef313502..bfa65013 100644 --- a/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-custom.conf +++ b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-custom.conf @@ -2,11 +2,11 @@ LPGROUPS { MODELNET_GRP { - repetitions="1520"; + repetitions="1600"; # name of this lp changes according to the model - nw-lp="8"; + nw-lp="4"; # these lp names will be the same for dragonfly-custom model - modelnet_dragonfly_custom="8"; + modelnet_dragonfly_custom="4"; modelnet_dragonfly_custom_router="1"; } } @@ -24,11 +24,11 @@ PARAMS # number of routers within each group # this is dictated by the dragonfly configuration files # intra-group rows for routers - num_router_rows="1"; + num_router_rows="4"; # intra-group columns for routers - num_router_cols="40"; + num_router_cols="20"; # number of groups in the network - num_groups="38"; + num_groups="20"; # buffer size in bytes for local virtual channels local_vc_size="8192"; #buffer size in bytes for global virtual channels @@ -41,21 +41,17 @@ PARAMS global_bandwidth="1.5"; # bandwidth in GiB/s for compute node-router channels cn_bandwidth="8.0"; -# Number of row channels - num_row_chans="2"; -# Number of column channels - num_col_chans="1"; -# ROSS message size +# ROSS message size message_size="608"; # number of compute nodes connected to router, dictated by dragonfly config # file - num_cns_per_router="8"; -# number of global channels per router - num_global_channels="4"; -# network config file for intra-group connections - intra-group-connections="/Users/mmubarak/Documents/software_development/codes/scripts/gen-cray-topo/intratest"; + num_cns_per_router="4"; +# number of global channels per router + num_global_channels="6"; +# network config file for intra-group connections + intra-group-connections="../src/network-workloads/conf/dragonfly-custom/intra-custom"; # network config file for inter-group connections - inter-group-connections="/Users/mmubarak/Documents/software_development/codes/scripts/gen-cray-topo/intertest"; + inter-group-connections="../src/network-workloads/conf/dragonfly-custom/inter-custom"; # routing protocol to be used routing="prog-adaptive"; } diff --git a/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-custom.yaml b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-custom.yaml new file mode 100644 index 00000000..ee49b38c --- /dev/null +++ b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-custom.yaml @@ -0,0 +1,41 @@ +schema_version: 1 + +# YAML twin of modelnet-test-dragonfly-custom.conf: a file-enumerated +# dragonfly-custom (4x20 router mesh per group, 20 groups -> 1600 routers, +# 6400 nodes) -- the fabric the committed intra-custom/inter-custom files encode, +# as documented in README.dragonfly-custom.txt. The compiler derives repetitions +# = num_groups * num_router_rows * num_router_cols, with num_cns_per_router +# terminals plus one router LP per repetition. Shape counts must match the binary +# connection files: num_global_channels is 6 because inter-custom gives some +# routers 6 outgoing global links, and num_row_chans/num_col_chans are left at +# the model defaults (1 and 3), which the intra-custom row/column links match. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-custom + shape: + num_router_rows: 4 + num_router_cols: 20 + num_groups: 20 + num_cns_per_router: 4 + num_global_channels: 6 + links: + local: { bandwidth: 5.25, vc_size: 8192 } + global: { bandwidth: 1.5, vc_size: 16384 } + cn: { bandwidth: 8.0, vc_size: 8192 } + routing: + algorithm: prog-adaptive + connections: + intra: "../src/network-workloads/conf/dragonfly-custom/intra-custom" + inter: "../src/network-workloads/conf/dragonfly-custom/inter-custom" + packet_size: 1024 + chunk_size: 1024 + message_size: 608 + modelnet_scheduler: fcfs + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-edison.yaml b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-edison.yaml new file mode 100644 index 00000000..4032516a --- /dev/null +++ b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-edison.yaml @@ -0,0 +1,35 @@ +schema_version: 1 + +# YAML twin of modelnet-test-dragonfly-edison.conf: a file-enumerated +# dragonfly-custom modeling Edison (6x16 router mesh per group, 15 groups -> +# 1440 routers). Shape counts must match the binary connection files. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-custom + shape: + num_router_rows: 6 + num_router_cols: 16 + num_groups: 15 + num_cns_per_router: 4 + num_global_channels: 10 + links: + local: { bandwidth: 5.25, vc_size: 8192 } + global: { bandwidth: 1.5, vc_size: 16384 } + cn: { bandwidth: 8.0, vc_size: 8192 } + routing: + algorithm: minimal + connections: + intra: "../src/network-workloads/conf/dragonfly-custom/intra-edison" + inter: "../src/network-workloads/conf/dragonfly-custom/inter-edison" + packet_size: 1024 + chunk_size: 1024 + message_size: 608 + modelnet_scheduler: fcfs + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-theta.yaml b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-theta.yaml new file mode 100644 index 00000000..cf892a19 --- /dev/null +++ b/src/network-workloads/conf/dragonfly-custom/modelnet-test-dragonfly-theta.yaml @@ -0,0 +1,35 @@ +schema_version: 1 + +# YAML twin of modelnet-test-dragonfly-theta.conf: a file-enumerated +# dragonfly-custom modeling Theta (6x16 router mesh per group, 9 groups -> 864 +# routers). Shape counts must match the binary connection files. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-custom + shape: + num_router_rows: 6 + num_router_cols: 16 + num_groups: 9 + num_cns_per_router: 4 + num_global_channels: 4 + links: + local: { bandwidth: 5.25, vc_size: 8192 } + global: { bandwidth: 1.5, vc_size: 16384 } + cn: { bandwidth: 16.0, vc_size: 8192 } + routing: + algorithm: adaptive + connections: + intra: "../src/network-workloads/conf/dragonfly-custom/intra-theta" + inter: "../src/network-workloads/conf/dragonfly-custom/inter-theta" + packet_size: 2048 + chunk_size: 2048 + message_size: 624 + modelnet_scheduler: fcfs + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dragonfly-dally/dfdally_3k.yaml b/src/network-workloads/conf/dragonfly-dally/dfdally_3k.yaml new file mode 100644 index 00000000..ee44c213 --- /dev/null +++ b/src/network-workloads/conf/dragonfly-dally/dfdally_3k.yaml @@ -0,0 +1,47 @@ +schema_version: 1 + +# YAML twin of dfdally_3k.conf: a ~3k-terminal dragonfly-dally (repetitions = +# num_groups * num_routers = 342). File-enumerated, so the shape counts must match +# the binary connection files (referenced by path). + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-dally + shape: + num_routers: 18 + num_groups: 19 + num_cns_per_router: 9 + num_global_channels: 9 + links: + local: { bandwidth: 12.5, vc_size: 65536 } + global: { bandwidth: 25.0, vc_size: 65536 } + cn: { bandwidth: 6.25, vc_size: 65536 } + routing: + algorithm: prog-adaptive + connections: + intra: "../src/network-workloads/conf/dragonfly-dally/dfdally-3k-intra" + inter: "../src/network-workloads/conf/dragonfly-dally/dfdally-3k-inter" + packet_size: 4096 + message_size: 736 + chunk_size: 4096 + modelnet_scheduler: fcfs + num_router_rows: 1 + num_router_cols: 18 + router_delay: 90 + adaptive_threshold: 0 + minimal-bias: 0 + num_injection_queues: 1 + nic_seq_delay: 10 + node_copy_queues: 1 + node_eager_limit: 16000 + df-dally-vc: 1 + num_row_chans: 1 + num_col_chans: 1 + auto_credit_delay: 0 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dragonfly-dally/dfdally_72.yaml b/src/network-workloads/conf/dragonfly-dally/dfdally_72.yaml new file mode 100644 index 00000000..314ae388 --- /dev/null +++ b/src/network-workloads/conf/dragonfly-dally/dfdally_72.yaml @@ -0,0 +1,37 @@ +schema_version: 1 + +# YAML twin of dfdally_72.conf: a 72-terminal dragonfly-dally. It is file- +# enumerated, so the shape counts are genuine inputs that must match the binary +# connection files (referenced by path). The compiler derives repetitions = +# num_groups * num_routers = 36 and lays out the LPs. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-dally + shape: + num_routers: 4 + num_groups: 9 + num_cns_per_router: 2 + num_global_channels: 2 + links: + local: { bandwidth: 2.0, vc_size: 16384 } + global: { bandwidth: 2.0, vc_size: 16384 } + cn: { bandwidth: 2.0, vc_size: 32768 } + routing: + algorithm: minimal + connections: + intra: "../src/network-workloads/conf/dragonfly-dally/dfdally-72-intra" + inter: "../src/network-workloads/conf/dragonfly-dally/dfdally-72-inter" + packet_size: 4096 + chunk_size: 4096 + message_size: 736 + modelnet_scheduler: fcfs + minimal-bias: 1 + df-dally-vc: 1 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dragonfly-dally/dfdally_8k.yaml b/src/network-workloads/conf/dragonfly-dally/dfdally_8k.yaml new file mode 100644 index 00000000..64b2a502 --- /dev/null +++ b/src/network-workloads/conf/dragonfly-dally/dfdally_8k.yaml @@ -0,0 +1,38 @@ +schema_version: 1 + +# YAML twin of dfdally_8k.conf: an ~8k-terminal dragonfly-dally (repetitions = +# num_groups * num_routers = 1040). File-enumerated, so the shape counts must +# match the binary connection files (referenced by path). + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-dally + shape: + num_routers: 16 + num_groups: 65 + num_cns_per_router: 8 + num_global_channels: 8 + links: + local: { bandwidth: 2.0, vc_size: 16384 } + global: { bandwidth: 2.0, vc_size: 16384 } + cn: { bandwidth: 2.0, vc_size: 32768 } + routing: + algorithm: prog-adaptive + connections: + intra: "../src/network-workloads/conf/dragonfly-dally/dfdally_8k_intra" + inter: "../src/network-workloads/conf/dragonfly-dally/dfdally_8k_inter" + packet_size: 4096 + chunk_size: 4096 + message_size: 656 + modelnet_scheduler: fcfs + num_row_chans: 1 + num_col_chans: 1 + adaptive_threshold: 16384 + route_scoring_metric: delta + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dragonfly-dally/modelnet-test-dragonfly-dally.yaml.in b/src/network-workloads/conf/dragonfly-dally/modelnet-test-dragonfly-dally.yaml.in new file mode 100644 index 00000000..469d7cb0 --- /dev/null +++ b/src/network-workloads/conf/dragonfly-dally/modelnet-test-dragonfly-dally.yaml.in @@ -0,0 +1,36 @@ +schema_version: 1 + +# YAML twin of modelnet-test-dragonfly-dally.conf.in: a 72-terminal dragonfly- +# dally (repetitions = num_groups * num_routers = 36). File-enumerated; the +# connection-file paths use the @abs_srcdir@ token substituted at configure time. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-dally + shape: + num_routers: 4 + num_groups: 9 + num_cns_per_router: 2 + num_global_channels: 2 + links: + local: { bandwidth: 2.0, vc_size: 16384 } + global: { bandwidth: 2.0, vc_size: 16384 } + cn: { bandwidth: 2.0, vc_size: 32768 } + routing: + algorithm: minimal + connections: + intra: "@abs_srcdir@/dfdally-72-intra" + inter: "@abs_srcdir@/dfdally-72-inter" + packet_size: 4096 + chunk_size: 4096 + message_size: 736 + modelnet_scheduler: fcfs + minimal-bias: 1 + df-dally-vc: 1 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dragonfly-plus/dfp-test.yaml b/src/network-workloads/conf/dragonfly-plus/dfp-test.yaml new file mode 100644 index 00000000..f5e4f9e2 --- /dev/null +++ b/src/network-workloads/conf/dragonfly-plus/dfp-test.yaml @@ -0,0 +1,38 @@ +schema_version: 1 + +# YAML twin of dfp-test.conf: a 5-group dragonfly-plus. File-enumerated, so the +# shape counts must match the binary connection files (referenced by path). The +# compiler derives repetitions = num_groups, terminals per group = num_router_leaf +# * num_cns_per_router, routers per group = num_router_spine + num_router_leaf. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-plus + shape: + num_router_spine: 4 + num_router_leaf: 4 + num_groups: 5 + num_cns_per_router: 4 + links: + local: { bandwidth: 5.25, vc_size: 8192 } + global: { bandwidth: 1.5, vc_size: 16384 } + cn: { bandwidth: 8.0, vc_size: 8192 } + routing: + algorithm: prog-adaptive + connections: + intra: "../src/network-workloads/conf/dragonfly-plus/dfp-test-intra" + inter: "../src/network-workloads/conf/dragonfly-plus/dfp-test-inter" + packet_size: 1024 + chunk_size: 1024 + modelnet_scheduler: fcfs + num_level_chans: 1 + message_size: 608 + num_global_connections: 4 + route_scoring_metric: delta + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dragonfly-plus/dfp_8k.yaml b/src/network-workloads/conf/dragonfly-plus/dfp_8k.yaml new file mode 100644 index 00000000..b0f695ae --- /dev/null +++ b/src/network-workloads/conf/dragonfly-plus/dfp_8k.yaml @@ -0,0 +1,40 @@ +schema_version: 1 + +# YAML twin of dfp_8k.conf: an ~8k-terminal dragonfly-plus. Its LPGROUPS layout +# (repetitions 1056, one router LP per repetition, 8 workload/terminal LPs each) +# is not the per-group shape the parametric dragonfly-plus fabric derives, so the +# layout is written directly with the explicit-groups form and PARAMS are carried +# through verbatim (including the intra/inter connection-file paths). + +topology: + format: groups + params: + packet_size: 4096 + modelnet_order: [dragonfly_plus, dragonfly_plus_router] + modelnet_scheduler: fcfs + chunk_size: 4096 + num_router_spine: 16 + num_router_leaf: 16 + num_level_chans: 1 + num_groups: 33 + local_vc_size: 32768 + global_vc_size: 32768 + cn_vc_size: 32768 + local_bandwidth: 25.0 + global_bandwidth: 25.0 + cn_bandwidth: 25.0 + message_size: 640 + num_cns_per_router: 16 + num_global_connections: 16 + intra-group-connections: "../src/network-workloads/conf/dragonfly-plus/dfp_8k_intra" + inter-group-connections: "../src/network-workloads/conf/dragonfly-plus/dfp_8k_inter" + routing: prog-adaptive + route_scoring_metric: delta + adaptive_threshold: 131072 + groups: + MODELNET_GRP: + repetitions: 1056 + lps: + nw-lp: 8 + modelnet_dragonfly_plus: 8 + modelnet_dragonfly_plus_router: 1 diff --git a/src/network-workloads/conf/dragonfly-plus/modelnet-test-dragonfly-plus.yaml.in b/src/network-workloads/conf/dragonfly-plus/modelnet-test-dragonfly-plus.yaml.in new file mode 100644 index 00000000..b9c0e709 --- /dev/null +++ b/src/network-workloads/conf/dragonfly-plus/modelnet-test-dragonfly-plus.yaml.in @@ -0,0 +1,37 @@ +schema_version: 1 + +# YAML twin of modelnet-test-dragonfly-plus.conf.in: a 5-group dragonfly-plus +# (repetitions = num_groups = 5). File-enumerated; the connection-file paths use +# the @abs_srcdir@ token substituted at configure time. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly-plus + shape: + num_router_spine: 4 + num_router_leaf: 4 + num_groups: 5 + num_cns_per_router: 4 + links: + local: { bandwidth: 5.25, vc_size: 8192 } + global: { bandwidth: 1.5, vc_size: 16384 } + cn: { bandwidth: 8.0, vc_size: 8192 } + routing: + algorithm: prog-adaptive + connections: + intra: "@abs_srcdir@/dfp-test-intra" + inter: "@abs_srcdir@/dfp-test-inter" + packet_size: 1024 + chunk_size: 1024 + modelnet_scheduler: fcfs + num_level_chans: 1 + message_size: 608 + num_global_connections: 4 + route_scoring_metric: delta + hosts: + component: compute_host diff --git a/src/network-workloads/conf/dual-plane-fattree-tapered.yaml b/src/network-workloads/conf/dual-plane-fattree-tapered.yaml new file mode 100644 index 00000000..981c67f1 --- /dev/null +++ b/src/network-workloads/conf/dual-plane-fattree-tapered.yaml @@ -0,0 +1,50 @@ +schema_version: 1 + +# YAML twin of dual-plane-fattree-tapered.conf: a tapered dual-rail (num_rails=2) +# fat-tree. Like dual-plane-fattree, the multi-rail layout (6 switch LPs per +# repetition, 384 workload LPs vs. modelnet_fattree=24 terminals) is not the +# single-plane shape the parametric fattree fabric derives, so the groups are +# laid out directly and PARAMS carried through verbatim. + +topology: + format: groups + params: + ft_type: 0 + packet_size: 8192 + chunk_size: 8192 + message_size: 512 + modelnet_scheduler: fcfs + modelnet_order: [fattree] + num_levels: 3 + tapering: 2 + num_rails: 2 + switch_count: 198 + switch_radix: 36 + router_delay: 90 + soft_delay: 200 + nic_delay: 400 + nic_seq_delay: 100 + num_injection_queues: 2 + link_bandwidth: 11.9 + cn_bandwidth: 24 + vc_size: 65536 + cn_vc_size: 65536 + node_copy_queues: 4 + intra_bandwidth: 30 + rdma_delay: 1000 + eager_limit: 64000 + copy_per_byte: 0.01 + node_eager_limit: 64000 + rail_select: adaptive + rail_select_limit: 8192 + routing: adaptive + routing_folder: taper_routes + dot_file: ftree + dump_topo: 0 + groups: + MODELNET_GRP: + repetitions: 198 + lps: + nw-lp: 384 + modelnet_fattree: 24 + fattree_switch: 6 diff --git a/src/network-workloads/conf/dual-plane-fattree.yaml b/src/network-workloads/conf/dual-plane-fattree.yaml new file mode 100644 index 00000000..bb5d17dc --- /dev/null +++ b/src/network-workloads/conf/dual-plane-fattree.yaml @@ -0,0 +1,50 @@ +schema_version: 1 + +# YAML twin of dual-plane-fattree.conf: a dual-rail (num_rails=2) fat-tree. The +# multi-rail layout is not the single-plane shape the parametric fattree fabric +# derives -- there are 6 switch LPs per repetition (2 planes x 3 levels) and 288 +# workload LPs, distinct from the terminal count modelnet_fattree=18 -- so the +# groups are laid out directly and PARAMS carried through verbatim. + +topology: + format: groups + params: + ft_type: 0 + packet_size: 8192 + chunk_size: 8192 + message_size: 512 + modelnet_scheduler: fcfs + modelnet_order: [fattree] + num_levels: 3 + tapering: 1 + num_rails: 2 + switch_count: 252 + switch_radix: 36 + router_delay: 90 + soft_delay: 200 + nic_delay: 400 + nic_seq_delay: 100 + num_injection_queues: 2 + link_bandwidth: 11.9 + cn_bandwidth: 24 + vc_size: 65536 + cn_vc_size: 65536 + node_copy_queues: 4 + intra_bandwidth: 30 + rdma_delay: 1000 + eager_limit: 64000 + copy_per_byte: 0.01 + node_eager_limit: 64000 + rail_select: adaptive + rail_select_limit: 8192 + routing: adaptive + routing_folder: full_routes + dot_file: ftree + dump_topo: 0 + groups: + MODELNET_GRP: + repetitions: 252 + lps: + nw-lp: 288 + modelnet_fattree: 18 + fattree_switch: 6 diff --git a/src/network-workloads/conf/modelnet-mpi-fattree-summit-k36-n3564.yaml b/src/network-workloads/conf/modelnet-mpi-fattree-summit-k36-n3564.yaml new file mode 100644 index 00000000..b07bdb5a --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-fattree-summit-k36-n3564.yaml @@ -0,0 +1,31 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-fattree-summit-k36-n3564.conf: a 3-level Summit-style +# fat-tree (198 edge switches, switch_radix 36 -> 18 terminals each). No routing +# key is set, so the fabric omits the routing block. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: fattree + shape: + num_levels: 3 + switch_count: 198 + switch_radix: 36 + ft_type: 0 + packet_size: 512 + message_size: 592 + chunk_size: 32 + modelnet_scheduler: fcfs + router_delay: 60 + soft_delay: 1000 + vc_size: 65536 + cn_vc_size: 65536 + link_bandwidth: 12.5 + cn_bandwidth: 12.5 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/modelnet-mpi-test-cry-router.yaml b/src/network-workloads/conf/modelnet-mpi-test-cry-router.yaml new file mode 100644 index 00000000..6246a0fa --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-test-cry-router.yaml @@ -0,0 +1,19 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-test-cry-router.conf: 10 peer compute nodes running a +# workload over a simplenet NIC (flat all-to-all). + +components: + compute_node: + model: nw-lp + network: simplenet + packet_size: 512 + message_size: 296 + modelnet_scheduler: fcfs + net_startup_ns: 1.5 + net_bw_mbps: 20000 + +topology: + format: flat + component: compute_node + nodes: 10 diff --git a/src/network-workloads/conf/modelnet-mpi-test-dfly-amg-1728.yaml b/src/network-workloads/conf/modelnet-mpi-test-dfly-amg-1728.yaml new file mode 100644 index 00000000..e9442077 --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-test-dfly-amg-1728.yaml @@ -0,0 +1,29 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-test-dfly-amg-1728.conf: a regular (Kim-Dally) +# dragonfly with 10 routers per group. The compiler derives the group/repetition +# counts from num_routers (num_cn = num_routers/2, num_groups = num_routers*num_cn +# + 1, so repetitions = 510) and emits the PARAMS the dragonfly model reads. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly + shape: + num_routers: 10 + links: + local: { bandwidth: 5.25, vc_size: 16384 } + global: { bandwidth: 4.7, vc_size: 32768 } + cn: { bandwidth: 5.25, vc_size: 16384 } + routing: + algorithm: adaptive + packet_size: 512 + chunk_size: 256 + modelnet_scheduler: fcfs + message_size: 656 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/modelnet-mpi-test-dfly-amg-216.yaml b/src/network-workloads/conf/modelnet-mpi-test-dfly-amg-216.yaml new file mode 100644 index 00000000..bbea6239 --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-test-dfly-amg-216.yaml @@ -0,0 +1,27 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-test-dfly-amg-216.conf: a regular dragonfly with 8 +# routers per group (num_cn = 4, num_groups = 33, repetitions = 264). + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly + shape: + num_routers: 8 + links: + local: { bandwidth: 5.25, vc_size: 16384 } + global: { bandwidth: 4.7, vc_size: 32768 } + cn: { bandwidth: 5.25, vc_size: 16384 } + routing: + algorithm: adaptive + packet_size: 512 + chunk_size: 256 + modelnet_scheduler: fcfs + message_size: 768 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/modelnet-mpi-test-dfly-mul-cores.yaml b/src/network-workloads/conf/modelnet-mpi-test-dfly-mul-cores.yaml new file mode 100644 index 00000000..c3dc92a9 --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-test-dfly-mul-cores.yaml @@ -0,0 +1,33 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-test-dfly-mul-cores.conf: a regular dragonfly (8 +# routers per group), but with 16 workload LPs per terminal (multiple cores per +# NIC) -- nw-lp (16) differs from the terminal count modelnet_dragonfly (4), so +# the layout is not the single-workload-per-terminal shape the parametric fabric +# derives. It is laid out directly with the explicit-groups form; PARAMS are +# carried through verbatim. + +topology: + format: groups + params: + packet_size: 512 + modelnet_order: [dragonfly, dragonfly_router] + modelnet_scheduler: fcfs + chunk_size: 256 + num_routers: 8 + local_vc_size: 16384 + global_vc_size: 32768 + cn_vc_size: 16384 + local_bandwidth: 5.25 + global_bandwidth: 4.7 + cn_bandwidth: 5.25 + message_size: 592 + routing: adaptive + self_msg_overhead: 20 + groups: + MODELNET_GRP: + repetitions: 264 + lps: + nw-lp: 16 + modelnet_dragonfly: 4 + modelnet_dragonfly_router: 1 diff --git a/src/network-workloads/conf/modelnet-mpi-test-dragonfly.yaml b/src/network-workloads/conf/modelnet-mpi-test-dragonfly.yaml new file mode 100644 index 00000000..371915ca --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-test-dragonfly.yaml @@ -0,0 +1,28 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-test-dragonfly.conf: a regular dragonfly with 8 +# routers per group (repetitions = 264), minimal routing. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly + shape: + num_routers: 8 + links: + local: { bandwidth: 5.25, vc_size: 16384 } + global: { bandwidth: 4.7, vc_size: 32768 } + cn: { bandwidth: 5.25, vc_size: 16384 } + routing: + algorithm: minimal + packet_size: 512 + chunk_size: 512 + modelnet_scheduler: fcfs + message_size: 608 + self_msg_overhead: 10 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/modelnet-mpi-test-fattree.yaml b/src/network-workloads/conf/modelnet-mpi-test-fattree.yaml new file mode 100644 index 00000000..46f6c79b --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-test-fattree.yaml @@ -0,0 +1,36 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-test-fattree.conf: a 3-level fat-tree (32 edge +# switches, switch_radix/2 = 4 terminals each, one switch per level). The +# fattree bandwidth/vc parameters are named flat rather than per link-class, so +# they are carried directly. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: fattree + shape: + num_levels: 3 + switch_count: 32 + switch_radix: 8 + routing: + algorithm: adaptive + ft_type: 0 + packet_size: 512 + message_size: 736 + chunk_size: 512 + modelnet_scheduler: fcfs + router_delay: 90 + terminal_radix: 1 + soft_delay: 1000 + vc_size: 65536 + cn_vc_size: 65536 + link_bandwidth: 12.5 + cn_bandwidth: 12.5 + rail_routing: adaptive + hosts: + component: compute_host diff --git a/src/network-workloads/conf/modelnet-mpi-test-mini-fe.yaml b/src/network-workloads/conf/modelnet-mpi-test-mini-fe.yaml new file mode 100644 index 00000000..54026e1e --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-test-mini-fe.yaml @@ -0,0 +1,19 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-test-mini-fe.conf: 18 peer compute nodes running a +# workload over a simplenet NIC (flat all-to-all). + +components: + compute_node: + model: nw-lp + network: simplenet + packet_size: 512 + message_size: 296 + modelnet_scheduler: fcfs + net_startup_ns: 1.5 + net_bw_mbps: 20000 + +topology: + format: flat + component: compute_node + nodes: 18 diff --git a/src/network-workloads/conf/modelnet-mpi-test-slimfly-min.yaml b/src/network-workloads/conf/modelnet-mpi-test-slimfly-min.yaml new file mode 100644 index 00000000..5f046e61 --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-test-slimfly-min.yaml @@ -0,0 +1,36 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-test-slimfly-min.conf: an MMS slimfly on num_routers=5 +# (repetitions = 2 * 5^2 = 50, num_terminals terminals plus one router LP each). +# The generator sets are list-valued PARAMS written as YAML sequences. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: slimfly + shape: + num_routers: 5 + num_terminals: 3 + links: + local: { bandwidth: 9.0, vc_size: 25600 } + global: { bandwidth: 9.0, vc_size: 25600 } + cn: { bandwidth: 9.0, vc_size: 25600 } + routing: + algorithm: minimal + generator_set_X: [1, 4] + generator_set_X_prime: [2, 3] + packet_size: 256 + chunk_size: 256 + modelnet_scheduler: fcfs + num_vcs: 4 + global_channels: 5 + local_channels: 2 + router_delay: 0 + link_delay: 0 + message_size: 768 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/modelnet-mpi-test-torus.yaml b/src/network-workloads/conf/modelnet-mpi-test-torus.yaml new file mode 100644 index 00000000..c3397ff8 --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-test-torus.yaml @@ -0,0 +1,27 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-test-torus.conf: a 3-dimensional 4x4x2 torus (32 +# nodes). torus folds routing into the terminal node, so there is no separate +# router LP; dim_length is a comma-separated string carried verbatim. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: torus + shape: + n_dims: 3 + dim_length: "4,4,2" + packet_size: 512 + message_size: 768 + modelnet_scheduler: fcfs + net_startup_ns: 1.5 + net_bw_mbps: 20000 + link_bandwidth: 10.0 + buffer_size: 8192 + chunk_size: 256 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/modelnet-mpi-test.yaml b/src/network-workloads/conf/modelnet-mpi-test.yaml new file mode 100644 index 00000000..c0801dc4 --- /dev/null +++ b/src/network-workloads/conf/modelnet-mpi-test.yaml @@ -0,0 +1,20 @@ +schema_version: 1 + +# YAML twin of modelnet-mpi-test.conf: 256 peer compute nodes running a workload +# over a simplenet NIC. A flat topology is just a component (the workload plus its +# NIC model) and a node count; the net params sit directly on the component. + +components: + compute_node: + model: nw-lp + network: simplenet + packet_size: 512 + message_size: 784 + modelnet_scheduler: fcfs + net_startup_ns: 1.5 + net_bw_mbps: 20000 + +topology: + format: flat + component: compute_node + nodes: 256 diff --git a/src/network-workloads/conf/modelnet-synthetic-dragonfly.yaml b/src/network-workloads/conf/modelnet-synthetic-dragonfly.yaml new file mode 100644 index 00000000..63cac09a --- /dev/null +++ b/src/network-workloads/conf/modelnet-synthetic-dragonfly.yaml @@ -0,0 +1,30 @@ +schema_version: 1 + +# YAML twin of modelnet-synthetic-dragonfly.conf: the same 8-router regular +# dragonfly, expressed in the friendly parametric-fabric form. The compiler +# derives the group/repetition counts from the shape and emits the PARAMS the +# dragonfly model reads. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: dragonfly + shape: + num_routers: 8 + links: + local: { bandwidth: 5.25, vc_size: 4096 } + global: { bandwidth: 4.7, vc_size: 8192 } + cn: { bandwidth: 5.25, vc_size: 4096 } + routing: + algorithm: adaptive + packet_size: 512 + chunk_size: 32 + num_vcs: 1 + modelnet_scheduler: fcfs + message_size: 512 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/modelnet-synthetic-fattree-summit-k36-n3564.yaml b/src/network-workloads/conf/modelnet-synthetic-fattree-summit-k36-n3564.yaml new file mode 100644 index 00000000..00e88d90 --- /dev/null +++ b/src/network-workloads/conf/modelnet-synthetic-fattree-summit-k36-n3564.yaml @@ -0,0 +1,31 @@ +schema_version: 1 + +# YAML twin of modelnet-synthetic-fattree-summit-k36-n3564.conf: a 3-level +# Summit-style fat-tree (198 edge switches, switch_radix 36 -> 18 terminals +# each). No routing key is set, so the fabric omits the routing block. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: fattree + shape: + num_levels: 3 + switch_count: 198 + switch_radix: 36 + ft_type: 0 + packet_size: 512 + message_size: 512 + chunk_size: 32 + modelnet_scheduler: fcfs + router_delay: 60 + soft_delay: 1000 + vc_size: 65536 + cn_vc_size: 65536 + link_bandwidth: 12.5 + cn_bandwidth: 12.5 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/modelnet-synthetic-fattree.yaml b/src/network-workloads/conf/modelnet-synthetic-fattree.yaml new file mode 100644 index 00000000..54ac1a75 --- /dev/null +++ b/src/network-workloads/conf/modelnet-synthetic-fattree.yaml @@ -0,0 +1,38 @@ +schema_version: 1 + +# YAML twin of modelnet-synthetic-fattree.conf: a 3-level fat-tree, expressed in +# the parametric-fabric form. The compiler derives the per-edge-switch layout +# from the shape (switch_count edge switches, switch_radix/2 terminals each, one +# switch per level) and emits the PARAMS the fattree model reads. The fattree +# bandwidth/vc parameters are named flat rather than per link-class, so they are +# carried directly. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: fattree + shape: + num_levels: 3 + switch_count: 32 + switch_radix: 8 + routing: + algorithm: adaptive + ft_type: 0 + packet_size: 512 + message_size: 512 + chunk_size: 512 + modelnet_scheduler: fcfs + router_delay: 90 + terminal_radix: 1 + soft_delay: 1000 + vc_size: 65536 + cn_vc_size: 65536 + link_bandwidth: 12.5 + cn_bandwidth: 12.5 + rail_routing: adaptive + hosts: + component: compute_host diff --git a/src/network-workloads/conf/modelnet-synthetic-slimfly-min.yaml b/src/network-workloads/conf/modelnet-synthetic-slimfly-min.yaml new file mode 100644 index 00000000..4f1431e8 --- /dev/null +++ b/src/network-workloads/conf/modelnet-synthetic-slimfly-min.yaml @@ -0,0 +1,36 @@ +schema_version: 1 + +# YAML twin of modelnet-synthetic-slimfly-min.conf: an MMS slimfly on +# num_routers=5 (repetitions = 2 * 5^2 = 50). The generator sets are list-valued +# PARAMS written as YAML sequences. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: slimfly + shape: + num_routers: 5 + num_terminals: 3 + links: + local: { bandwidth: 9.0, vc_size: 25600 } + global: { bandwidth: 9.0, vc_size: 25600 } + cn: { bandwidth: 9.0, vc_size: 25600 } + routing: + algorithm: minimal + generator_set_X: [1, 4] + generator_set_X_prime: [2, 3] + packet_size: 256 + chunk_size: 256 + modelnet_scheduler: fcfs + num_vcs: 4 + global_channels: 5 + local_channels: 2 + router_delay: 0 + link_delay: 0 + message_size: 512 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/slimfly/ffly_3k.yaml b/src/network-workloads/conf/slimfly/ffly_3k.yaml new file mode 100644 index 00000000..c082e151 --- /dev/null +++ b/src/network-workloads/conf/slimfly/ffly_3k.yaml @@ -0,0 +1,47 @@ +schema_version: 1 + +# YAML twin of ffly_3k.conf: a multi-rail (num_rails=2) "Fit Fly" slimfly. It has +# two router LPs per repetition (modelnet_slimfly_router=2), which the parametric +# slimfly fabric does not derive (it always emits one router LP), so the layout is +# written directly with the explicit-groups form and PARAMS carried through +# verbatim (generator sets as YAML sequences). + +topology: + format: groups + params: + sf_type: 1 + num_rails: 2 + rail_select: congestion + packet_size: 4096 + message_size: 736 + chunk_size: 4096 + modelnet_scheduler: fcfs + modelnet_order: [slimfly, slimfly_router] + num_vcs: 4 + num_routers: 13 + num_terminals: 9 + local_channels: 6 + global_channels: 13 + router_delay: 90 + link_delay: 0 + generator_set_X: [1, 10, 9, 12, 3, 4] + generator_set_X_prime: [6, 8, 2, 7, 5, 11] + local_vc_size: 65536 + global_vc_size: 65536 + cn_vc_size: 65536 + local_bandwidth: 12.5 + global_bandwidth: 12.5 + cn_bandwidth: 12.5 + routing: adaptive + csf_ratio: 1 + num_injection_queues: 2 + nic_seq_delay: 10 + node_copy_queues: 2 + node_eager_limit: 16000 + groups: + MODELNET_GRP: + repetitions: 338 + lps: + nw-lp: 9 + modelnet_slimfly: 9 + modelnet_slimfly_router: 2 diff --git a/src/network-workloads/conf/slimfly/sfly_3k.yaml b/src/network-workloads/conf/slimfly/sfly_3k.yaml new file mode 100644 index 00000000..f1ae6888 --- /dev/null +++ b/src/network-workloads/conf/slimfly/sfly_3k.yaml @@ -0,0 +1,42 @@ +schema_version: 1 + +# YAML twin of sfly_3k.conf: an MMS slimfly on num_routers=13 (repetitions = +# 2 * 13^2 = 338), one router LP per repetition. The generator sets are list- +# valued PARAMS written as YAML sequences; the multi-rail knobs (sf_type, +# num_rails, ...) pass through verbatim. + +components: + compute_host: + model: nw-lp + +topology: + format: parametric + fabric: + model: slimfly + shape: + num_routers: 13 + num_terminals: 9 + links: + local: { bandwidth: 12.5, vc_size: 65536 } + global: { bandwidth: 12.5, vc_size: 65536 } + cn: { bandwidth: 12.5, vc_size: 65536 } + routing: + algorithm: adaptive + generator_set_X: [1, 10, 9, 12, 3, 4] + generator_set_X_prime: [6, 8, 2, 7, 5, 11] + message_size: 736 + packet_size: 4096 + chunk_size: 4096 + modelnet_scheduler: fcfs + sf_type: 0 + num_rails: 1 + rail_select: none + num_injection_queues: 1 + node_copy_queues: 1 + local_channels: 6 + global_channels: 13 + router_delay: 90 + csf_ratio: 1 + num_vcs: 4 + hosts: + component: compute_host diff --git a/src/network-workloads/conf/workloads-synthetic.conf b/src/network-workloads/conf/workloads-synthetic.conf deleted file mode 100644 index 9ed7b1c2..00000000 --- a/src/network-workloads/conf/workloads-synthetic.conf +++ /dev/null @@ -1,2 +0,0 @@ -216 synthetic -125 /path/to/Multigrid/Multigrid_125/dumpi-2014.03.06.23.48.13- diff --git a/src/network-workloads/conf/workloads.conf b/src/network-workloads/conf/workloads.conf index 9f2a9cbb..4e1ca0d5 100644 --- a/src/network-workloads/conf/workloads.conf +++ b/src/network-workloads/conf/workloads.conf @@ -1,2 +1,2 @@ 216 synthetic -125 /Users/mmubarak/Documents/software_development/df_traces/Multigrid/MultiGrid_C_n125_dumpi/dumpi-2014.03.06.23.48.13- +125 /path/to/Multigrid/MultiGrid_C_n125_dumpi/dumpi-2014.03.06.23.48.13- diff --git a/src/networks/model-net/dragonfly-custom.cxx b/src/networks/model-net/dragonfly-custom.cxx index ba0aed21..670b080c 100644 --- a/src/networks/model-net/dragonfly-custom.cxx +++ b/src/networks/model-net/dragonfly-custom.cxx @@ -993,7 +993,10 @@ void dragonfly_custom_configure() { anno_map = codes_mapping_get_lp_anno_map(LP_CONFIG_NM_TERM); assert(anno_map); num_params = anno_map->num_annos + (anno_map->has_unanno_lp > 0); - all_params = (dragonfly_param*)malloc(num_params * sizeof(*all_params)); + // calloc (not malloc): dragonfly_read_config leaves optional params + // (e.g. counting_bool) untouched when absent from the config, so the + // struct must start zeroed. dragonfly-dally and dragonfly-plus do the same. + all_params = (dragonfly_param*)calloc(num_params, sizeof(*all_params)); for (int i = 0; i < anno_map->num_annos; i++) { const char* anno = anno_map->annotations[i].ptr; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 294d5c44..9c586328 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,15 +6,21 @@ option(CODES_ENABLE_ZMQML_HYBRID_TESTS "Register ZMQML hybrid workflow tests that require a running zmqmlserver.py" OFF) # --------------------------------------------------------------------------- -# Unit-test smoke test. The GoogleTest framework (GTest::gtest / gtest_main) is -# built by the thirdparty dispatcher when BUILD_TESTING is on — see -# thirdparty/googletest/. This smoke test proves the vendored framework builds, -# links, runs, and reports through CTest. Real unit-test consumers land in -# a future PR. Unit tests run serially (no MPI), distinct from the mpiexec-launched -# model tests below. -add_executable(codes-unit-smoke codes-unit-smoke.cxx) -target_link_libraries(codes-unit-smoke PRIVATE GTest::gtest_main) -add_test(NAME codes-unit-smoke COMMAND codes-unit-smoke) +# Unit tests for the YAML config compiler core. The GoogleTest framework +# (GTest::gtest / gtest_main) is built by the thirdparty dispatcher when +# BUILD_TESTING is on -- see thirdparty/googletest/. The core (config_compiler.cxx) +# is ROSS-free by design, so it compiles straight into the test with ryml -- no +# codes/ROSS/MPI link -- and the tests assert on the returned compiled_config. +# Unit tests run serially (no MPI), distinct from the mpiexec-launched model +# tests below. +add_executable(codes-config-compiler-test + codes-config-compiler-test.cxx + ${CMAKE_SOURCE_DIR}/src/modelconfig/config_compiler.cxx + ${CODES_RYML_IMPL_TU}) +target_include_directories(codes-config-compiler-test SYSTEM PRIVATE ${CODES_RYML_INCLUDE_DIRS}) +target_include_directories(codes-config-compiler-test PRIVATE ${CMAKE_SOURCE_DIR}/src/modelconfig) +target_link_libraries(codes-config-compiler-test PRIVATE GTest::gtest_main) +add_test(NAME codes-config-compiler-test COMMAND codes-config-compiler-test) # --------------------------------------------------------------------------- # MPI launch preflight. Fails loudly if mpiexec can't form a real multi-rank # MPI_COMM_WORLD, guarding against a broken launcher (e.g. Ubuntu 24.04's mpich @@ -183,9 +189,11 @@ endfunction() # CONFIG_B # e.g. new .yaml (or a second .conf) # [NP ] # default 1 # [ARGS ] -# ) +# [CONFIG_FLAG ]) # e.g. "--conf=" for binaries that take +# # the config as a ROSS option instead +# # of the trailing `-- ` positional function(codes_add_lpio_equivalence_test) - cmake_parse_arguments(T "" "NAME;BINARY;NP;CONFIG_A;CONFIG_B" "ARGS" ${ARGN}) + cmake_parse_arguments(T "" "NAME;BINARY;NP;CONFIG_A;CONFIG_B;CONFIG_FLAG" "ARGS" ${ARGN}) if(NOT T_NAME OR NOT T_BINARY OR NOT T_CONFIG_A OR NOT T_CONFIG_B) message(FATAL_ERROR "codes_add_lpio_equivalence_test: NAME, BINARY, CONFIG_A and CONFIG_B are required") endif() @@ -197,12 +205,88 @@ function(codes_add_lpio_equivalence_test) # Each run writes lp-io into a fresh "lp-io" dir inside its own run-N subdir # (lp_io_prepare mkdir's it and aborts if it already exists, so it must be # relative + per-run-isolated, which the runner's subdirs provide). + if(T_CONFIG_FLAG) + # config is a ROSS option (e.g. --conf=): glue it to the prefix and + # pass it among the ROSS args, with no trailing `-- ` positional. + set(_run_a ${_launch} "${_bin}" ${T_ARGS} --lp-io-dir=lp-io "${T_CONFIG_FLAG}${T_CONFIG_A}" ${MPIEXEC_POSTFLAGS}) + set(_run_b ${_launch} "${_bin}" ${T_ARGS} --lp-io-dir=lp-io "${T_CONFIG_FLAG}${T_CONFIG_B}" ${MPIEXEC_POSTFLAGS}) + else() + set(_run_a ${_launch} "${_bin}" ${T_ARGS} --lp-io-dir=lp-io ${MPIEXEC_POSTFLAGS} -- "${T_CONFIG_A}") + set(_run_b ${_launch} "${_bin}" ${T_ARGS} --lp-io-dir=lp-io ${MPIEXEC_POSTFLAGS} -- "${T_CONFIG_B}") + endif() add_test(NAME ${T_NAME} COMMAND "${CMAKE_CURRENT_BINARY_DIR}/run-test.sh" bash "${CMAKE_CURRENT_SOURCE_DIR}/equivalence-run.sh" --marker "Net Events Processed" --lp-io lp-io - @@ ${_launch} "${_bin}" ${T_ARGS} --lp-io-dir=lp-io ${MPIEXEC_POSTFLAGS} -- "${T_CONFIG_A}" - @@ ${_launch} "${_bin}" ${T_ARGS} --lp-io-dir=lp-io ${MPIEXEC_POSTFLAGS} -- "${T_CONFIG_B}" + @@ ${_run_a} + @@ ${_run_b} + WORKING_DIRECTORY "${CODES_BINARY_DIR}") +endfunction() + +# codes_add_config_equivalence_test — assert two configs drive a model to the +# same committed work, comparing a marker line rather than lp-io. The fallback +# for the cases where a per-LP lp-io diff can't be used: the binary emits no +# lp-io at all (e.g. resource-test / buffer_test), or its lp-io is not +# reproducible run to run (e.g. fattree's switch stats). The old .conf vs new +# .yaml check for those. Everything whose lp-io *is* per-run reproducible should +# use codes_add_lpio_equivalence_test instead -- a strictly stronger check. +# +# codes_add_config_equivalence_test( +# NAME BINARY [NP ] +# CONFIG_A # e.g. legacy .conf +# CONFIG_B # e.g. new .yaml +# [MARKER ] [ARGS ...] +# [CONFIG_FLAG ] # e.g. "--conf=" for binaries that take +# # the config as a ROSS option instead +# # of the trailing `-- ` positional +# [SETUP