Skip to content

k4bench.runner.k4run

k4bench.runner.k4run

Execute a single k4run job and return a :class:RunResult.

Design principle

Like :mod:k4bench.runner.ddsim, this module owns instrumentation and not the job's configuration, and here k4Bench knows even less about the job: it is any set of Gaudi options files. The runner manages only --num-events, to compute events/sec, and appends the k4BenchAuditor's options file after the job's own. Input files, geometry and the job's own switches are passed through verbatim via extra_args.

Per-event and per-component costs

When available, the k4BenchAuditor is enabled on every algorithm and service. It writes the per-event JSON in the same format as the DDG4 timing plugin, the per-component JSON, and the resolved job options, all inside the log directory. The run-level virtual-memory peak is copied into :class:RunResult. Given a heap path, the job also records which component allocated each heap block, and the auditor writes what each held there.

After a successful run, what each file the job created holds on disk, collection by collection for its EDM4hep files, is written to <label>_event_size.json in the log directory (:mod:k4bench.analysis.event_size), and what each event holds too when the caller asks for it.

run_k4run

run_k4run(*, options: list[str], label: str, n_events: int, work_dir: Path, log_dir: Path, setup_script: Path | None = None, extra_args: list[str] | None = None, verbose: bool = False, heap_json_path: Path | None = None, per_event_content: bool = False) -> RunResult

Run one k4run job in work_dir and return collected metrics.

The runner injects --num-events and, when the auditor is available, appends its options file after options. All other job arguments should be supplied via extra_args.

Parameters:

Name Type Description Default
options list[str]

Gaudi options files, loaded in order. Relative paths are resolved by k4run against work_dir.

required
label str

Human-readable name for this run. Used as the stem of the log and of the auditor's output files, and stored in :attr:RunResult.label.

required
n_events int

Number of events; passed to --num-events and used to compute :attr:RunResult.events_per_sec.

required
work_dir Path

Directory the job runs in. The job's own outputs are written there; :attr:RunResult.output_size_mb is the size of every file it created.

required
log_dir Path

Directory where <label>.log and the auditor's files are written.

required
setup_script Path | None

Optional shell script sourced before k4run.

None
extra_args list[str] | None

Additional k4run arguments passed through verbatim, after the options files, so the options files' own argument parsers read them.

None
verbose bool

Stream k4run output live to stdout.

False
heap_json_path Path | None

Where the auditor writes the heap each component held. Given one, the job records which component allocated every heap block, which makes it slower: a run with one is never a timed run.

None
per_event_content bool

Also record what each event of the job's EDM4hep files holds, collection by collection, in its event size record.

False

Returns:

Type Description
RunResult

Process-level timing, memory, and throughput metrics.

Source code in k4bench/runner/k4run.py
def run_k4run(
    *,
    options: list[str],
    label: str,
    n_events: int,
    work_dir: Path,
    log_dir: Path,
    setup_script: Path | None = None,
    extra_args: list[str] | None = None,
    verbose: bool = False,
    heap_json_path: Path | None = None,
    per_event_content: bool = False,
) -> RunResult:
    """Run one k4run job in *work_dir* and return collected metrics.

    The runner injects ``--num-events`` and, when the auditor is available,
    appends its options file after *options*. All other job arguments should
    be supplied via *extra_args*.

    Parameters
    ----------
    options:
        Gaudi options files, loaded in order. Relative paths are resolved by
        k4run against *work_dir*.

    label:
        Human-readable name for this run. Used as the stem of the log and of
        the auditor's output files, and stored in :attr:`RunResult.label`.

    n_events:
        Number of events; passed to ``--num-events`` and used to compute
        :attr:`RunResult.events_per_sec`.

    work_dir:
        Directory the job runs in. The job's own outputs are written there;
        :attr:`RunResult.output_size_mb` is the size of every file it created.

    log_dir:
        Directory where ``<label>.log`` and the auditor's files are written.

    setup_script:
        Optional shell script sourced before k4run.

    extra_args:
        Additional k4run arguments passed through verbatim, after the options
        files, so the options files' own argument parsers read them.

    verbose:
        Stream k4run output live to stdout.

    heap_json_path:
        Where the auditor writes the heap each component held. Given one, the
        job records which component allocated every heap block, which makes
        it slower: a run with one is never a timed run.

    per_event_content:
        Also record what each event of the job's EDM4hep files holds,
        collection by collection, in its event size record.

    Returns
    -------
    RunResult
        Process-level timing, memory, and throughput metrics.
    """
    log_dir.mkdir(parents=True, exist_ok=True)

    # Auditor output artifacts. Removed first so a run that fails to write its
    # own is never credited with a previous run's.
    event_json_path = label_path(log_dir, label, EVENTS_SUFFIX)
    components_json_path = label_path(log_dir, label, COMPONENTS_SUFFIX)
    joboptions_path = label_path(log_dir, label, JOBOPTIONS_SUFFIX)
    event_size_path = label_path(log_dir, label, EVENT_SIZE_SUFFIX)
    for path in (
        event_json_path, components_json_path, joboptions_path, event_size_path, heap_json_path,
    ):
        if path is not None:
            path.unlink(missing_ok=True)

    env = os.environ.copy()

    auditor_available = setup_auditor_environment(
        env=env,
        components_json_path=components_json_path,
        event_json_path=event_json_path,
        joboptions_path=joboptions_path,
        setup_script=setup_script,
        heap_json_path=heap_json_path,
    )

    cmd = timed_shell_command(
        [
            "k4run",
            *_k4run_args(
                options=options,
                n_events=n_events,
                extra_args=extra_args or [],
                auditor_options=auditor_options_file() if auditor_available else None,
            ),
        ],
        setup_script,
        # The setup script may set LD_PRELOAD, so the counter is preloaded after it.
        after_setup=ALLOC_COUNTER_PRELOAD if auditor_available and setup_script else "",
    )

    existing = _files_in(work_dir)
    proc = run_timed(
        cmd,
        env=env,
        log_path=label_path(log_dir, label, LOG_SUFFIX),
        verbose=verbose,
        cwd=work_dir,
    )
    created = _files_in(work_dir) - existing
    if proc.returncode == 0 and created:
        record_event_size(
            event_size_path, {str(path.relative_to(work_dir)): path for path in sorted(created)},
            per_event=per_event_content,
        )

    return run_result(
        label=label,
        n_events=n_events,
        proc=proc,
        event_json_path=event_json_path,
        output_size_mb=sum(path.stat().st_size for path in created) / 1024**2,
        instrumentation="auditor" if auditor_available else None,
    )

marlin_processor_dump

marlin_processor_dump(setup_script: Path | None = None) -> str | None

Marlin -x in the jobs' environment: every Marlin processor type the stack's MARLIN_DLL libraries register, with its parameters, as steering XML. None when Marlin cannot be run there.

Source code in k4bench/runner/k4run.py
def marlin_processor_dump(setup_script: Path | None = None) -> str | None:
    """``Marlin -x`` in the jobs' environment: every Marlin processor type the
    stack's ``MARLIN_DLL`` libraries register, with its parameters, as steering
    XML. ``None`` when Marlin cannot be run there.
    """
    # A setup script that fails must not leave another Marlin on PATH to run.
    source = f"source {shlex.quote(str(setup_script))} >/dev/null 2>&1 && " if setup_script else ""
    # Marlin leaves files (cdb.log) in the directory it runs in.
    with tempfile.TemporaryDirectory(prefix="k4bench-marlin-") as cwd:
        try:
            proc = subprocess.run(
                ["bash", "-c", f"{source}exec Marlin -x"],
                cwd=cwd, capture_output=True, timeout=600,
                # Processor descriptions are free text, in no promised encoding.
                encoding="utf-8", errors="replace",
            )
        except (OSError, subprocess.SubprocessError):
            return None
    start = proc.stdout.find("<?xml")
    if proc.returncode != 0 or start < 0 or "<marlin" not in proc.stdout:
        return None
    return proc.stdout[start:]