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.
The two execution paths reach the same result through different code:
| Path | Who 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 intocontentis readable by any principal withget configmapsin the benchmark namespace, and it is echoed into the harvested artifacts. Pass credentials throughpodTemplate.envFromSecretsinstead; see Configuration.
variables:
isl: 1024
osl: 512
benchmark:
artifacts:
user_files:
- path: input_config.json
format: json # optional; inferred from content type
content:
isl: "{{ isl }}"
osl: "{{ osl }}"
endpoint: "{{ endpoint_url }}"
model: "{{ model }}"
- path: meta/notes.md # subdirectories allowed
content: |
Run {{ job_name }} started at {{ epoch }}.
Targeting {{ model }} @ {{ endpoint_url }}.
models:
- my-org/my-model
endpoint:
type: chat
urls: ["http://my-frontend:8000"]
datasets:
- name: main
type: synthetic
phases:
- name: profiling
type: concurrency
concurrency: 10
requests: 100Result 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.
Each entry is:
| Field | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Output path relative to the run directory. Subdirectories OK. Absolute paths and any segment equal to .. are rejected. |
format |
json | yaml | text |
no | Serialization format. If omitted: text when content is a string, json otherwise. |
content |
structured or string | yes | Templated value. Dict/list/scalar for json/yaml; string for text. Jinja2 expressions in any string leaf are rendered. |
Format/content compatibility:
format: jsonorformat: yamlrequires structuredcontent(dict/list/scalar).format: textrequires stringcontent.
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.
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):
| Name | Type | Meaning |
|---|---|---|
epoch |
str | Run 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_name |
str | Locally: run-directory basename (or the parent directory name when epoch came from the basename). Under the operator: the AIPerfJob name. |
namespace |
str | Locally: value of AIPERF_NAMESPACE, empty string when unset. Under the operator: the CR's namespace. |
model |
str | First model name from benchmark.models / benchmark.model; empty string when none. |
endpoint_url |
str | First entry of benchmark.endpoint.urls; empty string when none. |
artifact_dir |
str | Absolute 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.
These are all fatal — the benchmark does not start.
| Failure | Cause | Where you see it |
|---|---|---|
| Path validation | Absolute path, .. segment, empty path, control chars |
Config load (pydantic ValidationError) |
| Format/content mismatch | e.g. format: json with content: "string" |
Config load (pydantic ValidationError) |
| Undefined variable | Template references a name not in context | Run start (UserFileError); message names the file path and the variable |
| Path escape | Resolved path is not inside the run directory | Run start (UserFileError) |
| Write failure | Disk 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.
- Sidecar metadata — produce an
input_config.jsonfor downstream tooling that expects the dynamo-style deployment-shape file. - Run notes — write a
notes.mdsummarizing what this run is for, who triggered it. - Manifests — emit a manifest a downstream pipeline will read.
- 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: falsesafety 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.