Initial YAML config with parity to legacy .conf format#259
Open
caitlinross wants to merge 21 commits into
Open
Initial YAML config with parity to legacy .conf format#259caitlinross wants to merge 21 commits into
caitlinross wants to merge 21 commits into
Conversation
Introduce a config compiler that reads the friendly YAML/JSON topology and
component format and lowers it to the same ConfigVTable the .conf text parser
produces, so codes_mapping and the models consume it unchanged through the
configuration_get_* accessors.
The front-end is three layers so the parsing/validation logic is unit-testable
and never leaks ROSS:
- config_compiler: a pure C++ core, compile(text) -> compiled_config, an
ordered plain-data image of the LPGROUPS/PARAMS tree. It does all parsing and
validation with no dependency on ROSS, MPI, or abort: on any invalid input it
throws config_error, and ryml's own parse errors are routed to throw as well.
- config_emitter: a dumb pass that turns the compiled_config into a
ConfigVTable; no validation, its coverage comes from the equivalence tests.
- yaml_configfile: a thin extern "C" shim, the only place ROSS lives -- it runs
compile() + emit() and translates a config_error into tw_error. Real-run
behavior (abort + diagnostic) is unchanged from the .conf path; "abort" is now
a boundary policy rather than being baked into every validation site. Every
rank reads identical bytes and runs the identical deterministic compile, so
malformed input still aborts everywhere at once.
configuration_load dispatches by file extension: a .yaml/.yml/.json path goes
through the front-end, everything else through the existing text parser. The
global config stays a ConfigVTable* either way, so no call site or model changes.
ryml is vendored and always built, so there is no feature guard.
Validation the throwing core enforces now: schema_version is required, integer,
and any value this build doesn't know is a hard error (a newer config can't be
read safely); unknown top-level keys, unknown topology keys, and nested blocks a
component/fabric doesn't consume are errors rather than silent drops.
This first cut compiles the parametric-fabric source for the regular dragonfly:
the group/repetition and per-router LP counts are derived from the fabric shape
(the same num_routers-driven math the model does internally), per-link-class
bandwidth/vc_size and routing map to the model's PARAMS, and modelnet_order is
derived from the fabric model. Values are carried through as their raw scalar
text so a compiled config is byte-comparable to the .conf it replaces.
Verified against modelnet-synthetic-dragonfly.conf: model-net-synthetic produces
byte-identical per-LP lp-io output from the .yaml twin.
Point the synthetic-dragonfly lp-io equivalence test at the YAML config instead of running the .conf twice, so it now asserts the YAML front-end and the legacy text parser drive the model to byte-identical per-LP output.
Extend the YAML front-end to the flat all-to-all network models, simplenet and
simplep2p. A flat topology is expressed as one component -- the workload model
(nw-lp) bundled with the NIC model it runs over (network: simplenet|simplep2p)
plus that model's params -- and a scalar node count:
topology:
format: flat
component: compute_node
nodes: 16
The compiler lays out one repetition per node (the workload LP and its NIC LP),
derives modelnet_order from the network model, and passes the component params
(message_size, packet_size, and simplep2p's matrix-file references) straight
through to PARAMS. simplep2p's link table stays referenced by path to its
existing matrix files; the friendly form supplies only the node count.
This is deliberately not a node/edge graph: the Cytoscape element form (per-node
data, per-edge bandwidth/latency) is a new representation with no consuming model
yet, so it waits for the WAN model.
Adds a marker-based config-equivalence helper (the flat-model test binaries write
lp-io to a fixed path, so committed-event counts are compared instead of lp-io
dirs) and registers both twins. Verified: modelnet-simplep2p-test and
modelnet-test produce identical event counts from the .yaml and the .conf.
The fabric model descriptor already varies the per-repetition router/switch LP
count and whether the router is a distinct model-net method, so add the two
families as registry entries plus their shape->counts derivations:
- fattree (internally generated): one repetition per edge switch, switch_radix/2
terminals each, one switch LP per level, and only the terminal in
modelnet_order.
- dragonfly-dally (file enumerated): repetitions = num_groups * num_routers with
the shape counts passed through as genuine inputs, and connections.{intra,inter}
mapped to the model's connection-file keys and referenced by path.
Verified: model-net-synthetic-dragonfly-all produces byte-identical per-LP lp-io
from the dally YAML and its .conf; model-net-synthetic-fattree produces matching
committed-event counts from the fattree YAML and its .conf (fattree's lp-io
switch stats are not reproducible run to run, so event counts are the signal).
The dally test configures both configs with an absolute path to the shared
connection files so the runner's per-run subdirs resolve them.
Add YAML config support for five more model-net networks, each with a
.conf-equivalence test proving the compiled config drives the model to
identical results:
- loggp (flat/p2p; one-line registry entry)
- torus (internally-generated; no separate router LP)
- express-mesh (internally-generated)
- slimfly (internally-generated; list-valued generator sets)
- dragonfly-plus (file-enumerated; per-LP lp-io equivalence)
Two small additions to the compiler core support these:
- null router LP: torus folds routing into the terminal node, so the
LPGROUPS router line is skipped when a fabric has no router method.
- list-valued fabric params: slimfly's generator_set_X / _X_prime are
written as YAML sequences and emitted as multi-value PARAMS (the
emitter already handled multi-value keys).
This brings YAML coverage to 10 of 11 network models. dragonfly-custom
remains.
Complete model-net YAML parity: dragonfly-custom is the last topology to land. Like dragonfly-dally/plus it is file-enumerated, so the shape counts (num_router_rows x num_router_cols routers per group, num_groups groups, num_cns_per_router terminals per router) are genuine inputs the compiler derives the LP layout from and that must match the binary connection files. The golden fixture is the 8-group Theta-style custom dragonfly from scripts/dragonfly-custom/example (6x16 router mesh -> 768 routers). The older in-tree custom configs were unusable as a baseline (dead hard-coded paths, and a transposed rows/cols shape that segfaulted the model against the available connection files); the equivalence test uses the corrected shape and passes at per-LP lp-io. Every model-net topology can now be configured from YAML.
…ames Add a top-level `sections:` block to the YAML config front-end for config a model reads directly by name (DIRECTOR, NETWORK_SURROGATE, resource, storage, ...). Each entry is emitted verbatim as a top-level section: scalars become single-value keys, lists multi-value keys, nested maps subsections. This restores the .conf ergonomics for such config -- a new feature adds its section with no compiler change -- while topology stays friendly and strictly validated. A section may optionally register an *open* schema (section_schemas[]): required keys are enforced while any other key still passes through, which suits a feature whose config is still in flux. `resource` is registered (its `available` key is mandatory -- resource-lp.c aborts without it), so a missing key is now a clear compile-time error. LPGROUPS/PARAMS are reserved (emitted from the topology). Section names are case-insensitive: the all-caps convention (PARAMS, DIRECTOR, ...) is a historical carryover, so mcs_findsubsection matches case-insensitively. Keys stay case-sensitive -- models read them by exact name. Verified end to end: a model reads `params` written lowercase. Also lands the first real unit test of the ROSS-free compiler core (tests/codes-config-compiler-test.cxx), asserting on compiled_config for the pass-through, open-schema, reserved-name, and case-preservation behavior -- with no codes/ROSS/MPI link, as the layered design intends. Documents the feature and a two-tier "adding a config section" process in doc/dev/yaml-config.md.
Add an explicit LP-layout form to the YAML front-end for configs that are not a
single friendly network -- storage clusters, resource/buffer tests, multi-
partition and mapping layouts -- where the compiler derives nothing and the user
lays out the groups directly:
topology:
format: groups
params: { message_size: 512 }
groups:
TRITON_GRP:
repetitions: 1
lps: { nw-lp: 1, lsm: 1 }
Each group's `repetitions` and `lps` (lp-type -> count) transcribe a .conf
LPGROUPS directly, validated (positive counts, known keys). An LP-type key may
carry an annotation as `type@annotation` -- the spelling codes_mapping splits on
'@' -- so the same type can appear more than once per group. `params` becomes
PARAMS (scalars/lists/nested pass through). Composes with the `sections:`
pass-through for a model that also reads its own section.
Verified end to end: lsm-test (TRITON_GRP + lsm section) and resource-test (BUF +
resource section) run byte-identically from their .conf and their new
explicit-groups .yaml twins. The config-equivalence harness gains a CONFIG_FLAG
option so binaries that take the config as a ROSS option (--conf=,
--codes-config=) rather than a trailing positional can be equivalence-tested.
Unit tests (codes-config-compiler-test) cover the groups/params structure,
annotations, composition with sections, and the validation errors. Documented in
doc/dev/yaml-config.md.
…iles A YAML config can now pull in other files with a top-level `include:` (a filename or a list), so a shared fragment -- a network, a set of components, a model section -- can be factored out and reused: define it once, vary the rest (e.g. the same section across different layouts). Included files are the base; the including file overrides them. `components` and `sections` merge by name (local wins on a clash); `topology`/`schema_version` are singular (local replaces). Multiple includes apply in list order, local last. One level deep for now -- an included file may not itself `include:`. The compiler core stays I/O-free: include resolution is a loader-boundary concern. compile() now takes the main document plus already-read base documents and merges them; a new parse_includes() extracts a document's include list. The extern "C" shim (yaml_configfile_load) gains the config path, reads the referenced files relative to its directory, and hands their contents to the core. configuration_load threads the path through. Included files must be readable by every rank (documented). Tests: unit tests for the multi-document merge (component/section merge and override, topology-from-base, nested-include rejection, parse_includes) and an end-to-end equivalence test -- lsm-test-include.yaml includes a shared lsm-workload fragment and runs byte-identical to lsm-test.conf. Documented in doc/dev/yaml-config.md.
Close the validation gaps found in review of the config compiler core: - shape values parse strictly: a non-integer (num_groups: abc) or trailing garbage (9x) is a diagnostic naming the key, not a silent 0/9; a backstop after every fabric derivation rejects degenerate layouts (repetitions/terminals <= 0) before they reach codes_mapping - dim_length: reject empty segments, trailing commas, and space-separated lists; guard product overflow; cross-check the entry count against n_dims (torus, express-mesh) - reject an empty component model: (was an empty LP-type key in LPGROUPS), unexpected keys under hosts:, the component type: key (reserved; would otherwise leak into PARAMS), network: on a parametric host component, and an odd fattree switch_radix - replace the deprecated 4-arg ryml::Callbacks constructor with the default constructor + setters (silences the only warning in new code) - catch std::exception at the extern "C" boundary so bad_alloc & co. reach tw_error with a message instead of std::terminate - match the config extension case-insensitively (.YAML, .Yml, .JSON) Adds 13 unit tests (first parametric-path coverage plus each new error path) and the matching yaml-config.md updates.
The YAML shim used to read include: fragments itself with std::ifstream -- one POSIX open per rank per include (a metadata storm on parallel filesystems at scale) and a hang risk: a file readable on some nodes but not others makes those ranks tw_error while the rest proceed into the next collective. Includes are now read in configuration_load with the same collective MPI_File_read_all pattern as the main config -- one collective read per file across the job. The shim gains yaml_configfile_list_includes() (resolves include paths from the main document's bytes, no file I/O) plus a matching free helper, and yaml_configfile_load() now consumes the include bytes directly, making it the pure bytes -> ConfigVTable function its header always described. A missing or unreadable include aborts the job with a diagnostic naming the resolved path.
The simplep2p / simplenet / loggp / torus / express-mesh / slimfly and
both lsm config-equivalence tests only compared a marker line ("Net
Events Processed") -- not because their models cannot be diffed per-LP,
but because their test binaries wrote lp-io to a fixed, non-per-run path,
so two runs' output could not coexist.
Give modelnet-test, modelnet-simplep2p-test and lsm-test the same
configurable lp-io options already used by model-net-synthetic
(--lp-io-dir / --lp-io-use-suffix; no output when the flag is unset), then
upgrade those eight registrations to codes_add_lpio_equivalence_test,
which diffs the per-LP lp-io output (send/recv counts, timings, torus link
stats, ...) on top of the marker -- a strictly stronger check. lsm-test
takes its config via --conf=, so add a CONFIG_FLAG option to
codes_add_lpio_equivalence_test mirroring the one already on
codes_add_config_equivalence_test.
Nothing consumed these binaries' previous fixed-path lp-io output (checked
tests/*.sh, tests/expected/ and CI), so emitting no lp-io when the flag is
unset is safe; the plain run-tests for the touched binaries still pass.
Left marker-based, with corrected comments:
- resource-test (buffer_test) emits no lp-io at all.
- fattree: its network switch/msg stats ARE reproducible run to run, but
model-net-synthetic-fattree also dumps a PARAMS_LOG sim_log.txt into the
lp-io dir with wall-clock-derived ROSS columns (running_time,
event_rate, ...) that vary every run, and the binary hardcodes its lp-io
path, so a whole-dir lp-io diff would spuriously fail. The old comment
blaming the switch stats was inaccurate and is fixed.
modelnet-p2p-bw likewise hardcodes lp_io_prepare but only backs a single-
run smoke test, so no existing comparison would benefit; left unchanged.
Add yaml configs to ping-pong tutorial and tests proving the YAML front-end drives the model identically to the legacy .conf. To cover the surrogate, teach codes_add_config_equivalence_test the SETUP and REQUIRE options codes_add_equivalence_test already has: with SETUP, CONFIG_A/B are the bare names a per-run setup script generates, so one script generating both the .conf and its .yaml twin lets the two formats be compared on the run- time-assembled config; REQUIRE asserts the surrogate actually switched. The new surrogate-config-equivalence-setup.sh generates both from their templates with the same env, so they differ only in format.
Every LPGROUPS-format .conf (and .conf.in template) now has a YAML twin stored beside it with the same basename, so each config is usable and readable in either format regardless of whether a test runs it. Fabric form is used where a supported family's derivation reproduces the LPGROUPS/PARAMS exactly; the explicit groups form transcribes the rest (extra LP types like dir-nw-lp, custom group names, per-cluster annotations, and shapes the parametric families don't derive). Each twin was verified mechanically: the YAML compiled through the front-end and the .conf parsed by the legacy parser produce structurally identical configurations (sections matched case-insensitively, group order preserved, key values compared exactly; templates compared with identical placeholder substitutions on both sides). The one exclusion is tests/workload/codes-workload-test.conf: it is PARAMS-only (no LPGROUPS), and the YAML front-end requires a topology block, so it has no expressible twin.
also removes the placeholder unit smoke test that isn't a real codes test.
…rrors Cover the three untested validation areas of the YAML config compiler core: - schema_version: missing, unsupported (2), and non-integer values are rejected on the main document, and an included base document that restates a disagreeing schema_version is rejected too - flat-topology validation: every error path now has the coverage its parametric mirror image already had -- an unexpected topology key (the documented edges/graph rejection), non-integer / zero / negative / missing nodes, missing component, a reference to an undefined component, an empty component model:, a missing network:, and an unknown network model - malformed / non-map input: a YAML syntax error throws config_error through the installed ryml parse callback (never print-and-abort, ryml's default), and a sequence or scalar top-level document is rejected with the top-level-mapping diagnostic
workloads.conf and workloads-synthetic.conf were near-identical example workload lists (line 1 "216 synthetic" identical; line 2 a placeholder dumpi trace path). workloads.conf still carried an absolute trace path on a specific machine, while the synthetic copy already used a /path/to/ placeholder.
1413197 to
1cddeec
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds a YAML front-end for CODES configuration. A
.yaml/.ymlconfigcompiles into exactly the same in-memory configuration (
ConfigVTable) thatthe legacy
.confparser produces, so models and the mapping layer arecompletely unaware of which format was used — no model code changes, and
.conffiles continue to work unchanged. The format you pick is dispatched onfile extension.
Why
The legacy
.confformat is a custom grammar with a flex/bison parser: opaqueto newcomers, unfriendly to tooling, and easy to get subtly wrong. YAML gives
us a standard, readable format that editors, linters, and scripts already
understand, and it lets the config say what you mean (a dragonfly of a given
shape) instead of how the simulator wants it spelled (LP counts per repetition).
The format
Every config declares
schema_version: 1and expresses its topology one ofthree ways, from most to least abstract:
fabricblocks for known network families — dragonfly(including dally, plus, and custom variants), fat-tree, slim fly, torus, and
express mesh. You state the fabric parameters; the compiler derives the LP
groups and PARAMS the models expect.
loggp).
format: groups— a general escape hatch that can expressanything the legacy format can, including custom group names, extra LP
types, and annotations.
Beyond topology: a pass-through
sections:block carries model-specificsections verbatim, annotated keys (
key@cluster) are supported throughout,and
include:lets configs be split and reused across files (included filesare read with collective MPI I/O, same as the main config).
Internally the compiler is a pure C++ core with no ROSS or MPI dependency —
errors are exceptions translated to
tw_errorat a single C boundary — whichis what makes it unit-testable in isolation.
Parity
Pretty much everything expressible in
.conftoday is expressible in YAML,and the tree proves it: every legacy config file now has a YAML twin sitting
next to it, and each twin was mechanically verified to compile to the same
parsed configuration as its
.conf. The one deliberate exception istests/workload/codes-workload-test.conf, a PARAMS-only file with noLPGROUPS block — a degenerate shape the YAML format intentionally does not
express (documented in the tests README).
Testing
MPI): fabric derivations, validation and error paths, includes, and
malformed input.
formats and diff the per-LP
lp-iooutput byte for byte wherever themodel's output is reproducible, falling back to event-count markers where
it isn't (this required un-hardcoding lp-io paths in a few test binaries,
which strengthened the pre-existing equivalence tests too).
developers — are covered in both formats.
doc/dev/yaml-config.mdis a complete standalone reference for the format,and the new public API carries doxygen comments. Creating the yaml twins also
flushed out some long-standing config rot (hardcoded paths to
contributors' machines, configs inconsistent with their committed topology
files), which is cleaned up here.