Additional YAML Features: units, run settings, and workloads#266
Open
caitlinross wants to merge 11 commits into
Open
Additional YAML Features: units, run settings, and workloads#266caitlinross wants to merge 11 commits into
caitlinross wants to merge 11 commits into
Conversation
The compiler emits modelnet_order (derived from the fabric/network model) as the first PARAMS key, then appends the user's fabric and component params. The config store returns the first match for a key name, so a user-supplied param of the same name would land after the derived value and be silently ignored -- violating the front-end's rule that it never silently drops a key. Route every user-supplied PARAMS key in compile_fabric and compile_flat through a new add_user_param() helper that rejects any name the compiler derives itself, with a diagnostic telling the user to remove it. modelnet_order is the only such key today; it is kept in a small array so more can be added without touching the call sites. The explicit-groups form derives no PARAMS and writes modelnet_order itself, so it builds PARAMS directly and is intentionally not guarded. Covered by unit tests for each reachable path (flat component param, fabric pass-through key, host-component param), that a prefix-sharing key (modelnet_scheduler) is still accepted, and that the explicit-groups form still allows a hand-written modelnet_order.
Add a standalone harness (config-tree-equivalence-test) that loads two config files through the production loader -- a .conf via the legacy text parser, a .yaml via the YAML front-end, including collective include resolution -- and asserts the two compiled trees are equal with cf_equal. It runs no model, so unlike the lp-io / marker equivalence tests it compares every section, key and value between a twin pair, catching defaulting or derivation drift a short behavioral run can mask, at a fraction of the cost. It is registered once per twin (data-driven, taking the two paths as arguments) for the same set the behavioral equivalence tests use; the surrogate twin is excluded because its config is assembled per run from a template by envsubst, so there is no static file pair to diff. To make this usable, cf_equal is reimplemented as an order-insensitive structural comparison: the config store keys every lookup by name (sections case-insensitively, keys case-sensitively) and never by position, so two trees are equal when they hold the same named entries with the same values regardless of the order entries were written -- while the order of values *within* a key (e.g. modelnet_order) is still significant and is checked. The previous implementation compared entries positionally by type, so a .conf and its twin that list PARAMS keys in a different order (every twin does -- the compiler emits modelnet_order first) compared unequal despite being identical to every model. It also mishandled empty-valued keys; comparing whole value lists via getMultiKey fixes that uniformly. cf_equal had no callers, so nothing depended on the old behavior. A new cf_equal_report(h1, h2, FILE*) writes each divergence under its section/key path (missing on one side, key-vs-section, or differing values) so a failure names exactly what diverged rather than only that the trees differ; cf_equal is now a thin wrapper over it with no report. All twins compile to identical trees -- no compiler emission or twin fixes were needed.
Add an environment-variable hook, CODES_RESOLVED_CONFIG_DUMP, checked in configuration_load(). When set, rank 0 writes the fully-resolved config tree (every compiler-derived and defaulted value, in .conf text form) so a run is traceable to one complete configuration. The value is a file path, or "-"/"stdout" for standard output; the dump reuses cf_dump for stdout and the same underlying writer for a file. It is opt-in and default-off, so it does not perturb existing tests or workflows (several diff lp-io output or grep stdout). It works identically for a legacy .conf run and a compiled YAML config, since both produce the same in-memory tree -- so it is about run traceability, not a YAML feature. A destination that can't be opened is a non-fatal warning; the run continues. The config-tree harness gains a single-config load-only mode (a config linter), which combined with the dump env var prints a resolved tree without running a model; the new resolved-config-dump test uses it to assert the dump carries a derived key (modelnet_order). Documented in doc/dev/yaml-config.md, noting it works for .conf runs too.
A dimensioned config value can now be written with a unit suffix and is converted to the target model's internal unit before it reaches PARAMS, instead of every value being a bare number in whatever internal unit the model happens to use. Add a small pure module (unit_convert) that classifies a raw value string against a fixed, case-sensitive suffix grammar and normalizes a recognized unit to a canonical base unit: time -> ns, size -> bytes, bandwidth -> bytes/second. It also formats a converted number as a plain decimal a C reader parses (integers without a decimal point, fractions in shortest round-tripping fixed notation, never an exponent). The module is unit-tested. Extend the model registry with per-parameter unit metadata: each dimensioned PARAMS key carries its quantity and the model's internal unit as a scale factor, so conversion is table-driven and per-model. This matters because the models disagree on units -- link_bandwidth is GiB/s in torus but bytes/ns in fattree, net_bw_mbps is really MiB/s, and the dragonfly QoS counting windows are microseconds. A unit-bearing value is converted; a bare number is emitted verbatim, so it already means the internal unit and every existing config compiles to the identical tree. A unit of the wrong quantity, a negative magnitude, or unknown trailing text is rejected with a diagnostic naming the parameter. A unit suffix on a knob the compiler cannot classify is also rejected: the model would atof() it and silently keep the bare number. Bare bandwidth numbers stay pass-through (there is no safe universal default) and units are recommended for them.
Introduce a run-level simulation: block for settings that belong to the run rather than the topology or a model: end_time and pe_mem_factor. Each is resolved to the PARAMS key the simulator already reads (PARAMS/end_time in nanoseconds, PARAMS/pe_mem_factor), so nothing downstream changes and the values appear in the resolved-config dump naturally. end_time is time-valued and reuses the units subsystem (bare number = ns, or a time unit like "100us"); a size/bandwidth unit, a non-positive value, or garbage is rejected. pe_mem_factor must be a positive integer. Setting one of these keys both in simulation: and as a pass-through model parameter would let the config store's first-match lookup silently drop one value, so the collision is rejected instead. Unknown keys in the block are rejected like everywhere else in the front-end.
Read PARAMS/end_time (nanoseconds) in codes_mapping_setup and use it for ROSS's g_tw_ts_end, but only when the command line did not set one. ROSS's --end option writes g_tw_ts_end directly and records no "was set" flag, so we compare against ROSS's compiled-in default (100000.0): an unchanged value means no --end was given and the config value applies; any other value is a command-line override and wins. Passing --end=100000 explicitly is therefore indistinguishable from omitting it. codes_mapping_setup is the chokepoint every model calls between loading the config and running, so no per-model main() changes. A model that hardcodes g_tw_ts_end after this call (several synthetic drivers do) keeps its value; a model that never sets it honors the config. The key rides in PARAMS, so a legacy .conf can carry it too. Adds two integration tests on model-net-synthetic-fattree (which never sets its own end time): one asserting a YAML end_time terminates the run at the configured time via ROSS's "Simulation End Time" banner, one asserting a command-line --end overrides it.
Add a workload front-end to the YAML config compiler: a node's workload (what
it runs) is now configured separately from the node model (what it is).
Two spellings compile to the same in-memory tree:
- an inline `workload:` on the topology's compute component -- the common
single-workload shortcut -- lowers to a dedicated WORKLOAD section carrying
the synthetic params (traffic, num_messages, payload_size, arrival_time);
- a top-level `jobs:` block places one or more workloads on the topology's
node slots, each with a rank count and a placement (contiguous policy or an
explicit node list). A single synthetic job covering every slot contiguously
desugars to exactly the WORKLOAD section the inline shortcut produces; any
richer placement emits a JOBS section, one subsection per job with its
resolved workload, rank count, and node allocation.
Size/time params are unit-resolved at parse time (payload_size -> bytes,
arrival_time -> nanoseconds), matching the rest of the front-end; traffic stays
a verbatim pattern name the model main maps to its own enum, since the name ->
value meaning is model-specific. Only the `synthetic` workload type is wired;
other types (dumpi, ...) and a job `qos:` key are recognized but rejected with a
clear "not yet configurable from this format" diagnostic, so a user keeps the
legacy path for them. Placement is validated against the topology's slot count
(in range, no double-booking, ranks consistent with the placement, total ranks
within the available slots). Unknown keys are rejected like everywhere else.
The two forms are mutually exclusive, a workload: may live only on the compute
component, and both are rejected for an explicit-groups topology (which has no
single compute-component notion). A config with neither emits no new section, so
a legacy config's tree is unchanged.
Make the synthetic-traffic knobs (traffic, num_messages, payload_size, arrival_time) settable from a YAML workload:/jobs: config instead of only from the command line, so a parameter sweep no longer needs a near-identical config copy per point. Add codes-workload-config, a small helper a synthetic main calls once after codes_mapping_setup(): for each option the main registered, it applies the value from the compiled WORKLOAD section unless the command line already set it. CLI detection follows the end_time pattern -- the global is compared to the option's registered default (captured before tw_init parses argv), so the precedence is command line > config > built-in default. Traffic is mapped from a friendly pattern name to the model's own enum through a per-main table, since the name -> value meaning differs per model. A legacy .conf carries no WORKLOAD section, so every apply is a no-op and .conf behavior is byte-identical. Wire all four synthetic mains (model-net-synthetic, -dragonfly-all, -fattree, -slimfly), each passing the knobs it actually registers. Fix an end-time clobber while here: model-net-synthetic and model-net-synthetic-dragonfly-all unconditionally set g_tw_ts_end to a 5-day default after codes_mapping_setup(), which silently discarded both a config-provided end_time and a --end on the command line. The hardcoded value is now a fallback applied only when neither the config nor the command line set an end time (g_tw_ts_end still at ROSS's 100000.0 default). This also makes --end take effect on those two mains, which it silently did not before. Add two integration tests on a cheap dragonfly synthetic run: one proving a config-provided num_messages takes effect (2 vs the model's default 20), one proving --num_messages=1 overrides the config.
An explicit multi-job jobs: config compiles to a JOBS section (one subsection per job with its resolved workload and node allocation), but the synthetic mains generate a single global traffic pattern and have no per-job destination dispatch, so they cannot execute a multi-job placement. Running such a config would silently ignore the placement. Emit a num_jobs marker on the JOBS section and add codes_workload_config_check_unsupported_jobs(), which each synthetic main calls after applying its workload params: if a JOBS section is present it aborts with a clear diagnostic (job count plus guidance to use a single workload:, a one-job all-nodes jobs: entry, or model-net-mpi-replay), rather than misrunning. A single-workload (WORKLOAD-section) or legacy config is unaffected. This keeps multi-job as a validated compile only: the JOBS allocation is the inspectable input a future job-aware synthetic executor (or an in-memory codes-jobmap list constructor feeding one) would consume, but wiring that execution needs job-aware destination selection in a main's core loop, which is out of scope here.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
The synthetic mains' only runtime behavior for a multi-job config -- the codes_workload_config_check_unsupported_jobs() abort -- had no test: the compiler side (JOBS emission, placement validation) is unit-tested, but nothing verified a main actually refuses to run one. Add a two-job dragonfly config and a test feeding it to model-net-synthetic, gated on the guard's diagnostic with PASS_REGULAR_EXPRESSION (a match passes and the abort's non-zero exit is ignored), proving the run stops with guidance instead of silently running one global pattern over the placement. The guard fires before tw_run, so the aborted run leaves no output files behind.
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.
With #259, the YAML config front-end could describe a machine (the topology and its model parameters) but everything else about an experiment (how long to run, what traffic to generate, how many messages, where jobs land) lived on the command line or was hardcoded in the model mains. This branch moves a substantial piece of that into the config: run-level simulation settings and synthetic workload definition can now be expressed in YAML.
This is a step toward a config that describes the full experiment: ROSS runtime options, trace-based workloads (dumpi and friends are recognized but rejected with a clear diagnostic), multi-job execution, and QoS remain outside the format for now.
What's new
25GiB/s,100us) and are converted to each model's internal unit via table-driven per-parameter metadata. This matters because the models quietly disagree, e.g., link_bandwidth is GiB/s in torus but bytes/ns in fattree. Until now the user had to know each model's convention. Bare numbers still pass through verbatim, so every existing config compiles to the identical tree.simulation:block for settings that belong to the run rather than the topology (end_time,pe_mem_factor). The end time is applied centrally incodes_mapping_setup, so every model honors it without per-main changes, and an explicit--endon the command line still wins.workload:shortcut on the compute component or a top-leveljobs:block that places one or more workloads onto the topology's node slots with validated allocations (in-range, no double-booking, rank counts consistent). Both spellings lower to the same internal representation.--endvalues.Scope note: multi-job is compile-and-validate only
A multi-job
jobs:block compiles to a validated JOBS section (job count plus each job's resolved workload and node allocation) but nothing executes it yet. The synthetic mains generate one global traffic pattern with no per-job destination dispatch, so they call a guard that aborts with guidance on a multi-job config rather than silently running global traffic over the placement. The compiled JOBS section is the deliberate seam for the follow-up. It doesn't really change much code to take that support out, so I decided to leave it in even though it isn't used yet. What you get today is early validation and lintability: placement errors surface at yaml read time, and the resolved allocation is inspectable via the config dump.Guardrails
The front-end's rule is that nothing is ever silently dropped or misinterpreted, and this branch extends that in several directions:
PARAMSkeys are rejected instead of silently ignored.Traceability and testing
CODES_RESOLVED_CONFIG_DUMPhook writes the fully-resolved config tree (every derived and defaulted value) for any run, whether legacy .conf or YAML, so a result is traceable to one complete configuration. Combined with the harness's new load-only mode it doubles as a config linter.cf_equalwas reimplemented as an order-insensitive structural comparison to make this possible; it previously had no callers.)Legacy .conf behavior is unchanged throughout: a .conf carries none of the new sections, so every apply path is a no-op and existing runs are byte-identical.