User-Defined Output Files (artifacts.user_files)

View as Markdown

artifacts.user_files lets you declare arbitrary templated output files that are materialized into the run directory before the benchmark begins. Files are rendered with jinja2 against the user variables: block plus a small set of system-injected names.

AIPerfJob accepts the same block — artifacts is part of the shared benchmark: config envelope. aiperf kube profile --config serializes the spec with camelCase aliases before submitting it, so snake_case authoring in an AIPerf YAML is fine; in hand-written CR YAML use artifacts.userFiles, because the apiserver prunes keys the CRD schema does not advertise. content is a typeless x-kubernetes-preserve-unknown-fields node in both CRDs, so every legal shape — including a bare string for format: text — is accepted at admission, and the operator’s Pydantic validators enforce the format/content pairing on reconcile.

Where the files are written

The two execution paths reach the same result through different code:

PathWho writes the files
Local (aiperf profile, local multi-run subprocess runner)The config resolver chain (ArtifactDirResolver), which derives the render identity from the resolved artifact dir.
Kubernetes (AIPerfJob, AIPerfSweep children)The controller pod’s system_controller container, before the benchmark starts, via aiperf.kubernetes.user_files.materialize_serialized_run_user_files.

Kubernetes service containers boot from the controller-rendered BenchmarkRun (aiperf service --benchmark-run) and deliberately skip the resolver chain, so that seeds, synthesized defaults, and artifact identity stay identical across pods. The declared entries therefore travel inside the serialized run itself and are rendered from that data — nothing is re-resolved in-pod. Only the system_controller container writes them: it owns the run directory that the results sidecar serves and the operator harvests, while worker pods mount their own private /results volume.

Do not put secrets in content. The serialized run is stored in a Kubernetes ConfigMap, so anything you write into content is readable by any principal with get configmaps in the benchmark namespace, and it is echoed into the harvested artifacts. Pass credentials through podTemplate.envFromSecrets instead; see Configuration.

Quickstart

1variables:
2 isl: 1024
3 osl: 512
4
5benchmark:
6 artifacts:
7 user_files:
8 - path: input_config.json
9 format: json # optional; inferred from content type
10 content:
11 isl: "{{ isl }}"
12 osl: "{{ osl }}"
13 endpoint: "{{ endpoint_url }}"
14 model: "{{ model }}"
15
16 - path: meta/notes.md # subdirectories allowed
17 content: |
18 Run {{ job_name }} started at {{ epoch }}.
19 Targeting {{ model }} @ {{ endpoint_url }}.
20
21 models:
22 - my-org/my-model
23 endpoint:
24 type: chat
25 urls: ["http://my-frontend:8000"]
26 datasets:
27 - name: main
28 type: synthetic
29 phases:
30 - name: profiling
31 type: concurrency
32 concurrency: 10
33 requests: 100

Result in the run directory:

{artifact_dir}/ # the resolved artifacts.dir for this run
├── input_config.json
├── meta/
│ └── notes.md
└── ... (standard AIPerf artifacts)

Files land directly in the run directory — there is no extra per-run wrapper. The run directory is whatever artifacts.dir resolves to: the explicit --artifact-dir / artifacts.dir value, or ./artifacts/<auto-generated-name> when neither was set. In-cluster the operator pins artifacts.dir to the /results container mount, so the files are harvested alongside the standard exports and appear under /api/v1/results/<namespace>/<name>/runs/<epoch>/ and in aiperf kube results.

Schema

Each entry is:

FieldTypeRequiredDescription
pathstringyesOutput path relative to the run directory. Subdirectories OK. Absolute paths and any segment equal to .. are rejected.
formatjson | yaml | textnoSerialization format. If omitted: text when content is a string, json otherwise.
contentstructured or stringyesTemplated value. Dict/list/scalar for json/yaml; string for text. Jinja2 expressions in any string leaf are rendered.

Format/content compatibility:

  • format: json or format: yaml requires structured content (dict/list/scalar).
  • format: text requires string content.

Rendered scalars are coerced in json/yaml output. A leaf like "{{ isl }}" with isl: 1024 is written as the number 1024, not the string "1024"; true/false become booleans. Strings containing no Jinja2 markers are passed through untouched, so a hand-written "1024" stays a string. format: text never coerces.

Dict keys are not rendered. Jinja2 expressions only resolve in string values, not in dict keys. content: {"{{ model }}": "x"} writes a file with the literal key "{{ model }}", not the resolved model name. Put templated values where they belong — in values — or pre-flatten the dict before passing it to AIPerf.

Templating context

Inside content, you can reference:

1. User-declared variables — anything you put in the top-level variables: block of your config. Variables may reference each other (in any YAML order); cross-references are resolved in dependency order (a Kahn-style topological pass) at config-load time, so a derived variable like total_concurrency: "{{ concurrency_per_gpu * deployment_gpu_count }}" already holds its computed value by the time user_files rendering runs. Cycles raise ConfigurationError naming the participating variables.

2. System-injected names (stable API):

NameTypeMeaning
epochstrRun epoch identifier (e.g. "1714000000"). Locally: wall-clock seconds captured at run start, unless the run-directory basename is itself epoch-shaped (9-10 digits, optionally with a 6-digit uid suffix) or the literal legacy, in which case that basename is used. Under the operator: the same epoch key the run’s PVC directory is named after, so {{ epoch }} matches the /api/v1/results/<ns>/<name>/runs/<epoch>/ path the artifacts land under.
job_namestrLocally: run-directory basename (or the parent directory name when epoch came from the basename). Under the operator: the AIPerfJob name.
namespacestrLocally: value of AIPERF_NAMESPACE, empty string when unset. Under the operator: the CR’s namespace.
modelstrFirst model name from benchmark.models / benchmark.model; empty string when none.
endpoint_urlstrFirst entry of benchmark.endpoint.urls; empty string when none.
artifact_dirstrAbsolute path to the run directory.

Collision rule: if a user variables: key shadows an injected name, the injected name wins and a WARNING is logged when the render context is built at run start. Rename your variable.

Errors

These are all fatal — the benchmark does not start.

FailureCauseWhere you see it
Path validationAbsolute path, .. segment, empty path, control charsConfig load (pydantic ValidationError)
Format/content mismatche.g. format: json with content: "string"Config load (pydantic ValidationError)
Undefined variableTemplate references a name not in contextRun start (UserFileError); message names the file path and the variable
Path escapeResolved path is not inside the run directoryRun start (UserFileError)
Write failureDisk full, permission denied, etc.Run start (UserFileError); message includes resolved path and OS error

Config-load failures reject the YAML (and, for an AIPerfJob, the CR submitted by aiperf kube profile) before anything reaches the cluster. Run-start failures are raised by the process executing the benchmark: locally that aborts aiperf profile, and in-cluster it aborts the system_controller container before the benchmark begins, which the operator surfaces as status.phase=Failed.

Use cases

  • Sidecar metadata — produce an input_config.json for downstream tooling that expects the dynamo-style deployment-shape file.
  • Run notes — write a notes.md summarizing what this run is for, who triggered it.
  • Manifests — emit a manifest a downstream pipeline will read.

Limitations (v1)

  • Pre-run only. Files render before the benchmark starts. Post-run files that include results are tracked as a future extension.
  • Files always overwrite. No overwrite: false safety net.
  • No required: false. Every declared file must materialize successfully or the run aborts.
  • Strict undefined. A typo in {{ varaibles_name }} is a hard error, not a silent empty string.