Host-side tracking and artifacts

Prepared simulations can run without MLflow and without putting tracking clients, paths, or credentials inside the JAX program. run_prepared owns the host boundary: it executes and synchronizes the numerical result, calls the analyzer, records metrics, verifies artifacts, and only then marks the run finished.

Untracked and directory-backed runs

NullTracker disables telemetry. DirectoryArtifactSink retains analyzer artifacts under a directory named for the explicit run ID:

import jax

from adept import (
    DirectoryArtifactSink,
    NullTracker,
    RunRequest,
    SimulationSpec,
    run_prepared,
    solver_registry,
)

prepared = solver_registry.prepare(
    SimulationSpec.from_legacy_config(config),
    key=42,
)
completed = run_prepared(
    prepared,
    key=jax.random.key(42),
    request=RunRequest(experiment="untracked", run_id="local-example"),
    tracker=NullTracker(),
    artifact_sink=DirectoryArtifactSink("./adept-results"),
)

raw_result = completed.raw_result
host_result = completed.materialized_result
report = completed.report

When the builder supplies an ObservationPlan, raw_result retains device arrays for transformed numerical use and host_result contains explicitly materialized NumPy arrays. The analyzer receives host_result; it never decides implicitly when to gather a device or sharded value.

This path does not import or contact MLflow. Artifact destinations are preflighted before numerical execution. Files and directories are copied through a staging path, hashed, and read back for verification before the run can finish successfully.

Returning metrics and artifacts from an analyzer

Analyzers return a Report; they do not call MLflow or upload files themselves:

from dataclasses import replace

from adept import Artifact, MetricEvent, Report


class EnergyAnalyzer:
    def analyze(self, result, manifest):
        del manifest
        energy = float(result.observations["energy"][-1])
        return Report(
            result={"final_energy": energy},
            metrics=(MetricEvent({"final_energy": energy}),),
            artifacts=(Artifact("./plots/energy.png", artifact_path="plots"),),
        )


prepared = replace(prepared, analyzer=EnergyAnalyzer())

Artifacts are local file or directory references. The selected ArtifactSink decides where they are stored. Upload or verification failures are strict: the tracker is told that the run failed, and the exception is returned to the caller.

MLflow without active-run state

The MLflow adapters use MlflowClient and pass the RunHandle to every operation. They never use mlflow.set_experiment, mlflow.start_run, or the process-global active run. This makes concurrent parent and child runs independent:

from adept import (
    MLflowArtifactSink,
    MLflowTracker,
    RunRequest,
    run_prepared,
)

tracker = MLflowTracker(tracking_uri="https://tracking.example")
artifacts = MLflowArtifactSink(tracking_uri="https://tracking.example")

completed = run_prepared(
    prepared,
    key=key,
    request=RunRequest(experiment="tpd-scan", name="angle-18"),
    tracker=tracker,
    artifact_sink=artifacts,
)

Experiment creation is race-tolerant. When another worker creates the experiment first, ADEPT resolves its explicit ID and creates the run there; it never silently falls back to MLflow’s Default experiment.

To resume an existing run, pass its ID in RunRequest(run_id=...). The adapter validates that the run is active (not deleted), preserves its existing parent, and explicitly transitions it back to RUNNING before returning the handle. A process failure during resumed execution therefore leaves the run visibly incomplete rather than retaining a stale terminal status.

For an MLflow service behind ADEPT’s /ajax-api/2.0 reverse-proxy route, configure the compatibility behavior only on the adapter:

tracker = MLflowTracker(
    tracking_uri="https://tracking.example",
    rest_api_path_prefix="/ajax-api/2.0",
)

The prefixed route table belongs only to that adapter instance. Ordinary MLflow clients and adapters using another prefix in the same process keep their own routes.

Credentials remain in the environment or normal MLflow/AWS configuration. They must not be placed in RunRequest, RunManifest, or a serialized simulation specification.

Failure policy

Tracking is strict by default. For disposable progress telemetry, use FailurePolicy.BEST_EFFORT; tracking errors are then returned in HostRunResult.tracking_errors while a successful numerical result is preserved:

from adept import FailurePolicy

completed = run_prepared(
    prepared,
    key=key,
    tracker=tracker,
    artifact_sink=DirectoryArtifactSink("./adept-results"),
    tracking_failure_policy=FailurePolicy.BEST_EFFORT,
)

Best-effort applies only to tracker telemetry. Artifact writes and verification remain strict because a run with missing declared outputs must not appear successfully archived.

The existing ergoExo and ADEPTModule entry points retain their current MLflow behavior during this phase. Eligible TF1D and PIC1D forward solves now use this host runtime underneath the compatibility façade; legacy MLflow and post-processing remain the public boundary. See Legacy API compatibility.