Logging and metrics
StableTested and in active use. Breaking changes will be called out.
Training runs emit metrics through a Logger. Which logger you attach decides
where they go — nowhere, a CSV file, or a live dashboard.
from noeira.core.logger import NoOpLogger, CsvLogger, RemoteLogger, CompositeLogger| Logger | Destination |
|---|---|
NoOpLogger | discards — the default |
CsvLogger | a local CSV file |
RemoteLogger | HTTP POST to noeira cloud |
CompositeLogger | several loggers at once |
Attaching one
Section titled “Attaching one”Loggers are passed to train as a pointer plus a compile-time type parameter:
var logger = CsvLogger("/tmp/run_metrics.csv", buffer_size=64)logger.set_config("algo", "sac")logger.set_config("env", "HalfCheetah")
_ = agent.train[L=CsvLogger]( env, NUM_STEPS, logger=Pointer(to=logger).as_unsafe_any_origin(), print_every=50_000, diag_every=1_000,)logger.close()_ = logger # keep it alive past the pointerThe _ = logger at the end is not decoration — it extends the lifetime past
the raw pointer handed to the driver. Dropping it is a use-after-free.
Two cadences
Section titled “Two cadences”The drivers emit at two rates, and separating them is what keeps a long run’s log both useful and small.
Driver cadence (print_every) — the coarse progress signal: avg_reward,
episodes. Every 50,000 steps is typical for a 600,000-step run.
Diagnostic cadence (diag_every) — the full metric bundle for the
algorithm: policy_loss, critic_loss, alpha, mean_q, train_steps. Every
1,000 steps.
Reward alone tells you that something is wrong. The bundle tells you what:
a collapsing return with rising mean_q is value explosion; with flat mean_q
it is a policy problem.
RemoteLogger
Section titled “RemoteLogger”Streams to noeira cloud over HTTP as batched JSON POSTs.
The run is announced by register() — call it through register_run with a
RunContext, before step 0 — and ended by close(), which sends the finish.
from noeira.core.dotenv import load_dotenv
var env_vars = load_dotenv()var logger = RemoteLogger( server_url=env_vars.get("NOEIRA_CLOUD_URL", ""), run_name="SAC HalfCheetah (GPU)", buffer_size=64, api_key=env_vars.get("NOEIRA_CLOUD_API_KEY", ""),)logger.set_config("algorithm", "SAC")logger.set_config("env", "HalfCheetah")logger.set_config("n_envs", String(N_ENVS))logger.register() # or register_run(run, logger) — see belowA logger that is never registered still registers lazily on its first flush,
so older drivers keep working — but a run that dies before that flush never
appears on the dashboard. New drivers should use a RunContext and
register_run; see registering a run.
Configuration comes from a .env file at the repo root, read by
noeira.core.dotenv:
NOEIRA_CLOUD_URL=https://cloud.noeira.aiNOEIRA_CLOUD_API_KEY=...buffer_size controls batching (default 200). Smaller means fresher charts
and more requests; 64–200 is the usual range. logger.total_logged() reports
how many points were logged.
With no .env, or an empty URL, the logger stays inert and training runs
unaffected.
Registering a run
Section titled “Registering a run”A driver makes a RunContext — which creates the run’s directory and writes
run.kv before any training — then a logger, and announces the run with
register_run before step 0:
from noeira.core.dotenv import load_dotenvfrom noeira.core.logger import RemoteLoggerfrom noeira.core.run import RunContext, register_run
var env_vars = load_dotenv()var run = RunContext( project="so101", driver="examples/so101/act_so101_train_gpu.mojo", slug="act-so101",)var logger = RemoteLogger( server_url=env_vars.get("NOEIRA_CLOUD_URL", ""), run_name=run.name(), run_id=run.id, buffer_size=64, api_key=env_vars.get("NOEIRA_CLOUD_API_KEY", ""),)logger.set_config("algorithm", "ACT")register_run(run, logger) # seeds run_id, project, driver, seed, host... then POSTs /runs
# ... training: logger.log_scalar(...), run.checkpoint_path("best") ...
run.close() # run.kv: status=done, finished=<now>logger.close() # flushes, sends the finish, drains the queueregister_run is what makes a run that dies at step 0 visible at all. The
task-family SAC driver (noeira/tasks/sac_family_driver.mojo), ACT on the
SO-101 and on LIBERO, Forward-Backward and BFM-Zero on the G1 all do this.
While the run is alive the logger also sends a heartbeat, which is how a run
that was killed can be told from one that is simply quiet.
The POST never happens on the training thread. flush serialises the batch
and queues it; a background thread in noeira/io/http_sink.mojo owns the
connection, sends in order, and sends a heartbeat when the run has been quiet
for a minute. There is no Python in this path.
set_config
Section titled “set_config”Configuration keys are recorded once with the run, not per point:
logger.set_config("algorithm", "SAC")logger.set_config("hidden", String(HIDDEN))logger.set_config("batch", String(BATCH))logger.set_config("buffer_capacity", String(REPLAY_CAPACITY))Worth being generous here. Comparing two runs six weeks apart is only possible if the thing that differed was recorded.
Metrics per algorithm
Section titled “Metrics per algorithm”Each algorithm defines a metrics struct. Representative fields:
| Algorithm | Metrics |
|---|---|
| DQN | loss, mean_q, explore_rate, td_error_abs_mean, train_steps |
| SAC | policy_loss, critic_loss, alpha, mean_q, mean_next_q, reward_mean |
| PPO | policy_loss, critic_loss, approx_kl, clip_fraction, entropy |
| MuZero | policy_loss, value_loss, reward_loss, avg_reward, buffer_size |
| FB / FB-CPR | fb_measure_loss, fb_ortho_loss, policy_loss, disc_expert_loss, disc_policy_loss |
| ACT | l1_loss, kl_loss, val_l1_loss, grad_norm, step_s |
The shared training drivers additionally emit avg_reward and episodes at the driver cadence.
Naming
Section titled “Naming”Metric names are flat snake_case, with no / namespaces, because the
dashboard groups charts by exact name:
- a loss ends in
_loss(policy_loss,value_loss,val_l1_loss); - a statistic is
<quantity>_<stat>(reward_mean,action_abs_mean); - an evaluation starts with
eval_(eval_return,eval_success_rate); - a per-task series appends the task name (
eval_success_rate_<task>).
Settings are not metrics. A value that does not change during the run goes
through set_config, which writes it to metrics.config.kv beside
metrics.csv.
What to watch
Section titled “What to watch”Drawn from the failures documented across these docs:
mean_q climbing without bound — value explosion. LayerNorm the critic. Hit
by SAC on Humanoid, MBPO, and REDQ at high UTD.
Return collapsing after early progress — often a target-network problem. If BatchNorm is involved, check that the hard copy carried the running statistics.
Loss descending while return stays flat — the model is learning something, just not the task. Classic symptom of a misaligned target: prev-action off-by-one in a world model, or termination/truncation collapsed into one flag.
train_steps not advancing as expected — check updates_per_step. At the
default of 1 with 32 environments you are training 32× less than intended.
CSV for offline runs
Section titled “CSV for offline runs”var logger = CsvLogger("/tmp/muzero_cartpole_metrics_gpu.csv", buffer_size=64)Right for headless boxes, CI and anything you want to plot yourself. Several examples default to it.
- noeira cloud — where
RemoteLoggersends - Checkpoints — persisting the run itself