Skip to content

docs: notebook metrics integration (house format first, TensorBoard as an adapter) - #285

Open
Fan Shangxiang (MartinForReal) wants to merge 2 commits into
Azure:mainfrom
MartinForReal:design/notebook-tensorboard-integration
Open

Fan Shangxiang (MartinForReal) wants to merge 2 commits into
Azure:mainfrom
MartinForReal:design/notebook-tensorboard-integration

Conversation

@MartinForReal

@MartinForReal Fan Shangxiang (MartinForReal) commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Design review for how a researcher sees training metrics from a notebook-submitted run.

Recommendation: converge on the metrics path TauGrid already has, and treat TensorBoard's
tfevents as an input adapter.
This revision replaces the original proposal to add a TensorBoard
sidecar service; that option is preserved in the document as a documented alternative, deferred
rather than rejected.

What is in it

A single document, docs/design/notebook-tensorboard-integration.md, containing:

  • The requested format analysis: the house W&B-style JSONL contract (_step, _timestamp, tag
    keys, scalar import, closed immutable chunks) beside tfevents (TFRecord framing with masked
    CRC32C, protobuf Event, wall_time, simple_value/tensor, append-only). Verdict: different
    encodings, isomorphic scalar semantics, so a converter is the right integration and a parallel UI
    is not.
  • Decisions, a cost comparison against the deferred service, risks, open questions, and alternatives.

Related issue

Closes #

Validation

Docs-only change; no code or tests changed.

Findings were checked against main rather than a working tree, which is what the earlier
mistake came from:

git ls-tree -r --name-only origin/main -- metrics/experiment-metrics-collector
git show origin/main:scripts/lib/image-specs.sh | grep taugrid-metrics-collector
git ls-tree -r --name-only origin/main -- portal/internal/expimport
git grep -n "tfevents\|tensorboard" origin/main -- portal/internal/expimport
git show origin/main:portal/go.mod | grep protobuf

Compatibility and operational impact

None. No implementation is proposed for merge in this change.

For reviewers

Two repository inconsistencies, both re-verified against main, are recorded in the document:
portal/internal/expimport is described as a TensorBoard importer but decodes no event files (its
only TensorBoard tie is tag/card naming semantics), and monitoring/gpu-metrics-collector writes
Node conditions rather than reading training metrics.

The document also records one correction to its own earlier revision. It claimed
portal/README.md names a taugrid-metrics-collector that does not exist in-tree. That was wrong:
the component is real, at metrics/experiment-metrics-collector/ (a Go module with
cmd/taugrid-metrics-collector) plus images/taugrid-metrics-collector/Dockerfile, listed as a
first-party image in scripts/lib/image-specs.sh. The mistake came from searching a checkout that
was stale relative to main. This settles the open question about which component should accept a
tfevents adapter: it is the training-metrics sidecar TauGrid already ships, so that question is
closed in the document rather than left for a maintainer.

Checklist

  • The change is focused and does not include unrelated edits.
  • Tests cover the changed behavior, or I explained why tests are not needed.
  • Documentation and examples are updated when public behavior changes.
  • Generated files are updated from their source and included in this pull request.
  • New dependencies are publicly available and their license and provenance were reviewed.
  • The diff contains no secrets, private endpoints, customer data, or Microsoft-internal information.
  • Fork-based CI can validate the change without repository secrets, or maintainer-only validation is clearly identified.

@feiskyer Pengfei Ni (feiskyer) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@MartinForReal

Copy link
Copy Markdown
Contributor Author

Kevin Cho (@chokevin) Could you please take a look?


## 1. Problem

Ray workloads commonly produce TensorBoard-compatible outputs, but TauGrid does

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm a bit conflicted here. While I'm ok with supporting tensorboard it does seem to be a bit orthogonal to how we want to display per job metrics? I also think its not the same format as offline W&B metrics which is what the industry seems to use today.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right, and the document no longer treats TensorBoard as the output. House W&B-style
JSONL is the target; TensorBoard becomes an optional input. Worth noting the tag vocabulary is
already compatible -- the house format uses slash-delimited tags like train/loss, and
portal/internal/expimport/expimport_test.go asserts "TensorBoard card semantics" for them -- so
what is missing is transport, not modeling.


## 2. Decision

**TensorBoard runs as a sidecar container in the RayJob head pod, and its UI is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do already have a metrics offload sidecar we should configure that container to handle the different metrics logging but again we should make sure it works as broadly as possible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and it is now the primary recommendation. Broadening the existing metrics path beats adding
a service on every axis we compared: no Service per job, no new image to pin and review, no new proxy
route, and no GOVERNANCE design gate for a new required service. The notebook loss pane already reads
the house format, so the same rows serve it and Stellar with nothing to reconcile.

@@ -0,0 +1,281 @@
# TauGrid Notebook TensorBoard Integration - Design

Status: **proposal, awaiting review.** No implementation code is written yet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would need some analysis between the tensorboard format and the existing format we are exporting for taugrid metrics. I haven't looked closely at this recently but I feel like if they are similar this should be a simple integration but if they are too different we should try to coincide the logging as much as possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delivered, in the document's section 2. Short version: the two formats are not similar in encoding
(UTF-8 JSON lines versus TFRecord-framed protobuf) but they are isomorphic in scalar semantics --
Event.step -> _step, Event.wall_time -> _timestamp, Value.tag -> the JSON key,
simple_value/tensor -> the number. So a converter into the house format is small and bounded:
one reader, one direction, scalars only, no UI and no service. Two things make it cheaper than it
looks -- google.golang.org/protobuf is already a direct dependency of portal/go.mod, and CRC32C
is stdlib -- so no new module is needed. A parallel TensorBoard UI would duplicate Stellar and leave
two pipelines to keep in sync, which is what we want to avoid.

@chokevin

Copy link
Copy Markdown
Contributor

Overally I'm totally onboard with an interactive notebook. Whether tensorboard is the right output or another alternative or maybe even local taugrid without a backing adx cluster could be something to consider

@MartinForReal
Fan Shangxiang (MartinForReal) force-pushed the design/notebook-tensorboard-integration branch from c49c92c to 377ae60 Compare October 8, 2026 08:04
@MartinForReal Fan Shangxiang (MartinForReal) changed the title docs: notebook TensorBoard integration (sidecar + proxy) docs: notebook metrics integration (house format first, TensorBoard as an adapter) Oct 8, 2026
remote-write path — and **no `tfevents` decoding**. The only TensorBoard tie is
*tag/card naming semantics* (`expimport_test.go:91`). The README line should say
the importer consumes TensorBoard-*shaped tags*, not event files.
2. **`portal/README.md` names `taugrid-metrics-collector`, which does not exist in

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The standalone collector already exists under metrics/experiment-metrics-collector, is packaged under images/taugrid-metrics-collector, and is what workload sidecars invoke. It owns source checkpoints, typed projection, durable spooling and queued ADX delivery, rather than the remote-write path shown in §5. Base the adapter ownership and architecture analysis on those boundaries, and assess protobuf dependencies in the collector module rather than portal/go.mod, so the proposal preserves one collection and delivery pipeline.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct on both counts, and the boundary correction changed the analysis rather than just the wording.

§5 no longer draws the collector as a remote-write forwarder. It now shows what the module actually owns -- source checkpoints, typed projection, durable spooling (spool.go) and queued ADX delivery (adx_queued.go) -- with the tfevents adapter as an additional source at the typed-projection boundary. That placement is the argument: a decoder that emits rows through the same projection inherits retry, ordering and restart safety, while one that writes to ADX directly would bypass all three.

The dependency claim was assessed against the wrong module, and correcting it flips the conclusion. google.golang.org/protobuf is a direct dependency of portal/go.mod, which is what I cited, but metrics/experiment-metrics-collector/go.mod pulls only azkustodata, azkustoingest, azcore and azidentity -- no protobuf. So an adapter inside the collector does add a dependency to that module, while one beside expimport does not. The earlier "no new module needed" line was wrong for the collector placement; §6.1 and §9 Q2 now record the real trade-off instead of asserting it away.

@MartinForReal
Fan Shangxiang (MartinForReal) force-pushed the design/notebook-tensorboard-integration branch 2 times, most recently from 429e2dd to e9948be Compare October 8, 2026 11:15

```
Event.step -> _step (int64 -> int)
Event.wall_time -> _timestamp (double -> int seconds)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please preserve fractional epoch seconds instead of narrowing wall_time to integer seconds. With the existing projection, two observations of the same tag/step in the same source file within one second would receive the same event ID: value and history-line tags are excluded from that identity. The ADX dedup view would then hide one observation. Keep _timestamp as numeric seconds with its fractional part and require a regression case for this pair.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. _timestamp now keeps the fractional part; the mapping line reads Event.wall_time -> _timestamp (double -> numeric seconds, fractional part kept) and no longer narrows to integer seconds.

The document now records the mechanism rather than just the rule: the projection identities an observation by source file, tag and step, and value and history-line tags are excluded from that identity, so a floored timestamp collapses two same-tag, same-step observations inside one second onto one event ID and the ADX dedup view shows only one of them. I also noted that a training step normally emits its metrics in a single burst, which makes this routine rather than exotic, and that the loss is silent. A regression case for the same-second pair is required in §2.4, and the correction is recorded as R7 in the revision summary.

|- metrics-offload / collector (experiment-metrics-collector)
|
| source checkpoints (what has already been read from each source)
| typed projection (source records -> house JSONL rows)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The typed projection does not emit the _step/_timestamp house rows defined above: it turns them into canonical MetricEvent NDJSON, which both spool replay and queued delivery validate. Labeling its output and ADX input as house JSONL hides the adapter boundary and would direct an implementation toward a payload those consumers reject. Keep the existing pipeline, but show history-shaped scalar records before projection and canonical metric events afterward.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and this was a regression from my own previous revision. I re-based the architecture on the collector's boundaries and then labelled the projection's output as house _step/_timestamp JSONL - precisely wrong, which is worse than the vagueness it replaced, because it would send an implementer toward a payload the consumers reject.

Verified against main rather than a working tree. The real boundaries:

  • source -> checkpoints: raw history bytes + SourceCheckpoint (metrics/experiment-metrics-collector/history.go:20)
  • source record -> projection input: history-shaped scalar records (collector.go:456; core/exptelemetry/history.go:52 ProjectJSONLHistoryRow)
  • projection output: []exptelemetry.MetricEvent + canonical NDJSON (collector.go:446; core/exptelemetry/metric.go:46, schema tau.experiment.metric.v1 at :18, MarshalNDJSON at :177)
  • spool replay validates canonical and requires a byte-for-byte round-trip (spool.go:481, called at :388)
  • queued delivery validates again and ingests as JSON (adx_queued.go:284, :300, :426; MetricEventChunk{Events []MetricEvent; NDJSON []byte} at types.go:20/29/30)

So ADX receives canonical MetricEvent NDJSON. Section 5 now shows history-shaped records before the projection and canonical metric events after it, with a boundary table carrying the file:line for each hop, and states outright that the projection output is not house JSONL. The adapter stops at the projection's input. I corrected the same error in §2.1, §2.5, §3 D2, §3.1, §4.1, §8 and §10.5 rather than only the line you flagged.

lost from TauGrid's model: `tfevents` files are already classified as artifacts
by the store and UI (`portal/internal/expstore/store_test.go:722`,
`portal/frontend/src/stellar/evidence-helpers.ts:85`), so a histogram or image
remains reachable as run evidence without being a metric row.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cited store fixture explicitly registers an event-file artifact; it does not establish automatic retention of files consumed by the proposed adapter. The collector persists scalar chunks/checkpoints, and artifact offload processes files already registered for the run. Dropping non-scalars therefore does not itself leave their evidence reachable. Qualify this promise on durable retention and run-associated registration of the original event files, rather than treating artifact representation as preservation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - "representable as an artifact" is not "durably retained and registered", and the document treated those as the same thing.

What the cited fixture actually does: portal/internal/expstore/store_test.go:875-887 registers an ArtifactRecord{Type:"tensorboard", URI:"artifacts/seed-1/events.out.tfevents.test"} inside RecordRunData for run seed-1 (same fixture at adx_export_test.go:347-358). It establishes that an event file can be registered, not that anything registers it automatically. And offload only uploads what is already registered: store.ArtifactsForRun(ctx, opts.RunID) at portal/internal/artifactoffload/offload.go:129.

Section 2.4 now makes preservation conditional on durable retention and run-associated registration, states that dropping non-scalars is otherwise unrecoverable, and records that the adapter registers nothing itself. I qualified the same claim in §8 and §10.2, and added §9 Q6 plus a §11 item for the retention/registration dependency, since it is now an explicit assumption rather than an implied guarantee.

Two incidental fixes while verifying: a stale line reference (store_test.go:722 -> 875) and a citation to portal/frontend/src/stellar/evidence-helpers.ts, which does not exist on main - I had cited it without checking. Removed.

@MartinForReal
Fan Shangxiang (MartinForReal) force-pushed the design/notebook-tensorboard-integration branch from 0ed5e47 to e208a6f Compare October 9, 2026 05:47
Placement therefore matters more than "add a converter". A `tfevents` reader is an
additional **source** that emits history-shaped scalar records, so it enters at the
typed projection's input rather than at its output. The existing checkpoints, spool
and queued delivery keep working unchanged, and the decoder inherits retry, ordering

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The source checkpoint layer cannot remain unchanged here. readSource currently truncates at the last newline and tracks line counts (metrics/experiment-metrics-collector/history.go:123-148), while Runner.project splits those bytes on newlines and decodes JSON objects (collector.go:449-463). A protobuf payload may contain 0x0a, and a live event file may end midway through a TFRecord, so reusing this reader can checkpoint in the middle of a frame and make restart permanently fail or misdecode. Please make a framing-aware source/checkpoint abstraction part of the design: advance only through a complete length/CRC/payload/CRC record and define truncated-tail retry behavior. The spool and ADX delivery can be reused, but checkpointing and ordering are not inherited for free.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and this invalidated the document's load-bearing claim rather than a detail. The "source checkpoint layer is reused unchanged" statement is withdrawn.

The review's reading checks out against main: readSource truncates at the last newline and tracks line counts (metrics/experiment-metrics-collector/history.go:118-129, 144-150), and Runner.project splits those bytes on newlines and JSON-decodes them (collector.go:449-463). A protobuf payload containing 0x0a therefore splits a frame, and a file that ends mid-record checkpoints inside it - which makes restart fail or misdecode rather than merely lose a record.

New §5.1 defines a framing-aware source/checkpoint abstraction:

  • advance only across a complete TFRecord frame - uint64 length, masked CRC32C of length, payload, masked CRC32C of payload
  • a CRC mismatch is a hard error that does not advance the checkpoint
  • a truncated tail stops at the last verified frame, is retained by the checkpoint, and is retried on the next pass - never checkpointed past
  • ordering is per logical run (strict file order within a source, prefix-digest gate, segments in write order), explicitly not a global total order

The spool and queued ADX delivery are still reused. §5's boundary table, §8 risk 3 and §9 Q2 are updated to match, and §0 R10 records the correction. I also annotated the older R6 row, which had claimed the source checkpoints were reused - it now notes that R10 narrows the source half of that row, rather than leaving two rows contradicting each other.

```

**Do not floor `wall_time` to whole seconds.** The projection identifies an
observation by the source file, the tag and the step; value and history-line tags

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mapping is missing TensorBoard’s logical-run identity. The relative event-file directory/logdir hierarchy distinguishes Tune trials and TensorBoard runs; the tag only identifies a metric within that run. TauGrid groups a series by its Tau run_id and metric name, while its deterministic event ID also includes MetricFileID (core/exptelemetry/metric.go:89-117). Without an explicit mapping, multiple trial directories can be merged into one Tau series with overlapping steps, while rotation/replay of one logical run can receive different file identities and evade deduplication. Please define how the TensorBoard logical run maps to Tau run/group/tag/prefix identity and how source identity remains stable across event-file rotation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct - the mapping was missing, and the consequence is worse than losing a label: trial directories merge into one series, and rotation evades deduplication.

Verified against main: the deterministic event ID folds in metric_file_id alongside run_id and run_group_id (core/exptelemetry/metric.go:91-122 - the review cited 89-117, the function runs to 122), collector.go:471 sets MetricFileID from the reading path, and history.go:205-208 hashes the cleaned physical path - which is why rotation changes the event ID and defeats dedup.

New §2.4.1 defines the mapping: trial directory -> run_group_id (distinct per trial), run directory -> run_id (a rotation segment is NOT a new run), tag -> metric_name, with a run-scoped prefix applied by the adapter and project/experiment from the §3.2 submission contract.

New §2.4.2 makes source identity stable: metric_file_id derives from a run-scoped logical source key rather than the physical path, and the physical segment (basename plus dev/inode) lives in the checkpoint. This requires the projection to take metric_file_id from the source rather than re-hashing read.path, which §2.4.2 states as a required change.

One dependency I could not verify and have recorded as §9 Q7 instead of asserting: whether the submission convention actually guarantees the logdir hierarchy this mapping needs. I could not find anything in-tree that enforces or documents it.

| **D2** | Treat `tfevents` as an **input adapter** that normalizes into history-shaped house JSONL rows, and stop it there. | Scalar loss curves from TensorBoard-producing frameworks (Ray Tune, Lightning, HF, Keras) become available to every existing consumer. The adapter emits the projection's *input*; it does not emit canonical `MetricEvent` NDJSON. No new service. |
| **D3** | **Defer** the per-job TensorBoard sidecar + proxy service. | Avoids a new required service, a new Service per job, and a new proxy route. See §4. |
| **D4** | Treat "local TauGrid without ADX" as a **separate design**, not part of this one. | Keeps this proposal reviewable; see §10.5 for why it interacts with D1. |
| **D5** | Keep the notebook loss pane as the **primary** surface. | No iframe/subpath problem, no extra container, works with no opt-in. |

@chokevin Kevin Cho (chokevin) Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not currently work without opt-in. metrics.offload.enabled defaults to false and the collector requires a history path, project/experiment identity, durable storage, and ADX configuration (core/runconfig/schema.go:146-154, core/runconfig/config.go:703-711). The companion notebook proposal also emits {"step":3,"loss":1.25} (notebook-plugin.md:268), but the supported online row contract requires _step and _timestamp, and its panel API does not specify experiment/offload identity. Please add the submission contract that enables and mounts the collector, resolves/stamps project/experiment/group/run identity, and either emits the existing house row shape or introduces an explicit normalizer. Otherwise the promised live/durable loss pane will have no accepted data.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct - the live/durable loss pane assumed an opt-in that is off by default, so it had no accepted data path.

Verified on main: metrics.offload.enabled defaults false (core/runconfig/schema.go:148); it needs a pinned image (run_job.go:556-558) and a writable storage.output PVC under /data (storage/paths.go:12, run_job.go:515-521); metrics.history must be non-empty (config.go:702-704) and resolve under the durable root (run_job.go:563-573); identity requires experiment.project/name (config.go:705-712) with group passed through (719-723); and the collector requires run/project/experiment/group/source (collector.go:60-68).

New §3.2 is the submission contract: what enables and mounts the sidecar, what identity is resolved and stamped, ADX-only delivery (schema.go:152, metricsoffload/options.go:131-139), and an explicit NORMALIZER for the row-shape gap the review identified - the supported online contract needs _step/_timestamp (schema.go:146) while the companion proposal emits {"step":3,"loss":1.25} (notebook-plugin.md:268), which fails because historyStep returns nil for _step (core/exptelemetry/history.go:71-74, 185-188). The normalizer table covers both the metric-file line and the stdout path.

§3's D5 row, which claimed this "works with no opt-in", is corrected. The MetricEvent boundary is preserved - the adapter emits history-shaped rows and the projection still assigns event_id; no "house JSONL output" wording was reintroduced.

kept in sync with the first — which is precisely the outcome the review asked to
avoid.
- "Coincide the logging as much as possible" is therefore **already true at the
schema level**. The remaining work is transport, not modeling.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conclusion overstates compatibility. W&B-shaped JSONL is a supported source shape, but the canonical exchanged/stored contract is tau.experiment.metric.v1 MetricEvent. The cited “TensorBoard card semantics” test only verifies Tau’s card classification for train/loss; it does not cover TensorBoard run identity, dtype conversion, duplicates, ordering, or non-scalars. The projection normalizes values to finite float64, timestamps to microseconds, requires nonnegative int64 steps, and drops non-scalars, so this requires modeling/normalization decisions in addition to transport. Please narrow the claim to “TensorBoard scalar events can be normalized into the existing canonical MetricEvent pipeline” and specify those normalization rules.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and the claim is narrowed to "TensorBoard scalar events can be normalized into the existing canonical MetricEvent pipeline".

The rules are verified on main and now stated in a new §2.4.3 table: finite float64 values (history.go:91-94, 211-250), timestamps truncated to microseconds via time.UnixMicro in UTC (203-208), nonnegative int64 steps (144-201), non-scalars dropped (95-97), _-prefixed keys treated as metadata (82). And the cited test does only pin card classification - expimport_test.go:91 asserts tau.metric.card == "World model" for train/loss, with ResearchMetricCard a name-to-card switch (mapping.go:53-62).

One refinement to your list, which I think matters for an implementation: it conflates two different behaviours. Non-finite numeric values are fatal - rejected, not skipped - while non-scalar values are dropped. §2.4.3 states them separately rather than as one "normalizes and drops" rule, because an implementer who treats NaN as droppable would silently lose data the pipeline is supposed to refuse.

ADX (canonical MetricEvent NDJSON, schema tau.experiment.metric.v1)
|
v
Stellar / expstore --> Portal /api/stellar/series

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For collector-only runs, this endpoint reads legacy metric projections rather than the canonical MetricEvent rows delivered to ADX, so the notebook can miss successfully ingested metrics. Please use the existing /api/v2/stellar/runs/{run_id}/series API and document its scoped request and points response instead of adding a parallel legacy export.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and stronger than the review states: the legacy route is not merely less canonical, it is deprecated.

Verified on main: portal/README.md:134 lists /api/v2/stellar/runs/{run_id}/series; README:138-142 notes canonical typed reads use TauExpRunCatalogRows/TauExpMetricEventRows while raw ExperimentMetrics is legacy-only and not a v2 fallback; the route is wired at server.go:311 and handled at v2.go:502-559 with the structs at v2.go:128-145. The old /api/stellar/series is served with Deprecation: true and a successor-version link (server.go:314-328, 971) - so a notebook reading it can miss successfully ingested metrics AND is building on a route already marked for removal.

New §5.2 documents the read API: the scoped request ({run_id} path, required target, required metric, workspace/project, start_step/end_step/max_points) and the points response (v2SeriesPoint {int64 step, float64 value} inside v2SeriesResponse.points). §5's diagram, §7 item 4 and §9/§11 are updated, and §0 records it as R14.

One related gap this surfaced that you did not raise: the companion document docs/design/notebook-plugin.md:257 also uses the deprecated /api/stellar/series. It is out of scope for this one-file change, so I recorded it in §7.4 and asked it as §11 item 7 rather than silently leaving it or widening this PR.

…s an adapter)

Design review for how a researcher sees training metrics from a notebook-submitted
run, replacing the original per-job TensorBoard sidecar proposal.

Recommendation: converge on the metrics path TauGrid already has and treat
TensorBoard's tfevents as an input adapter. The document provides the requested
format analysis (the house W&B-style JSONL contract beside tfevents), records the
decisions, compares costs against the deferred service, and preserves the original
sidecar design as a documented alternative that is deferred rather than rejected.

The adapter is placed at the collector's typed-projection boundary, so the existing
source checkpoints, durable spool and queued ADX delivery are reused instead of
bypassed. That boundary is stated precisely: the projection consumes history-shaped
scalar records and emits canonical MetricEvent NDJSON, which spool replay and queued
delivery validate before ADX ingests it. Non-scalar preservation is conditional on
durable retention and run-associated registration of the original event files, since
representing them as artifacts does not by itself keep them reachable.

Also records what verification found: portal/internal/expimport decodes no event
files despite its README wording, monitoring/gpu-metrics-collector writes Node
conditions rather than reading training metrics, and an earlier revision's claim
that taugrid-metrics-collector does not exist was wrong - the component is real at
metrics/experiment-metrics-collector.
@MartinForReal
Fan Shangxiang (MartinForReal) force-pushed the design/notebook-tensorboard-integration branch from e208a6f to 4ea2008 Compare October 11, 2026 13:12
…contract

Five review corrections, all changes to the document rather than replies.

The load-bearing claim in this design was wrong: the source checkpoint layer
cannot be reused unchanged. `readSource` truncates at the last newline and tracks
line counts, while `Runner.project` splits those bytes on newlines and decodes JSON
objects - so a framed tfevents source, whose protobuf payload may contain 0x0a and
whose file may end mid-record, can checkpoint inside a frame and make restart fail
permanently or misdecode. The new framing-aware abstraction advances only across a
complete TFRecord frame (length, masked CRC32C of length, payload, masked CRC32C of
payload), treats a CRC mismatch as a hard error that does not advance, and retains a
truncated tail for the next pass rather than checkpointing past it. Ordering is per
logical run, explicitly not a global total order.

Run identity was also undefined. TensorBoard's logical run lives in the event-file
directory hierarchy while Tau's deterministic event ID folds in the metric file, so
Tune trial directories could merge into one series and event-file rotation could
evade deduplication. The document now maps the relative hierarchy onto Tau
project/experiment/group/run and the metric prefix, and derives the file identity
from a run-scoped logical source key rather than the physical path, with the segment
in the checkpoint.

The live/durable loss pane also assumed an opt-in that is off by default. A
submission contract now states what enables the collector, what identity is resolved
and stamped, and - because the supported online row contract needs `_step` and
`_timestamp` while the companion proposal emits `step` and `loss` - an explicit
normalizer for both the metric-file and stdout paths.

The compatibility claim is narrowed to what the evidence supports: TensorBoard
scalar events can be normalized into the canonical MetricEvent pipeline. The
normalization rules are stated and separately: non-finite numerics are rejected as
fatal while non-scalar values are dropped, which the review conflated.

The notebook read API now uses the existing /api/v2/stellar/runs/{run_id}/series
rather than a parallel legacy export - and the review understated it, since the
legacy route is served with `Deprecation: true` and a successor-version link.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants