Skip to content

[feat]online export of dense models#604

Open
eric-gecheng wants to merge 85 commits into
alibaba:masterfrom
eric-gecheng:feat/dense_export
Open

[feat]online export of dense models#604
eric-gecheng wants to merge 85 commits into
alibaba:masterfrom
eric-gecheng:feat/dense_export

Conversation

@eric-gecheng

Copy link
Copy Markdown
Collaborator

No description provided.

The full export special-cases MatchModel (per-tower user/item dirs) and
TDM (separate embedding/ and model/ dirs), but the online dense export
unconditionally wrapped the whole model and published one
scripted_model.pt at the version root, so a hot swap for such a
deployment would load an artifact whose layout doesn't match what the
processor deployed from the full export. Reject these model families
with a clear error right after model build instead of silently
producing an incompatible monolithic export.

Co-Authored-By: Claude <noreply@anthropic.com>
@eric-gecheng eric-gecheng added the codex-review Let Codex Review label Jul 20, 2026
@github-actions github-actions Bot removed the codex-review Let Codex Review label Jul 20, 2026
Comment thread tzrec/utils/export_util.py
Comment thread tzrec/utils/export_util.py
Comment thread tzrec/tools/online_dense_export.py
Comment thread tzrec/utils/online_dense_export_util.py Outdated
Comment thread tzrec/main.py Outdated
Comment thread tzrec/utils/online_dense_export_util.py
@github-actions

Copy link
Copy Markdown
Contributor

Follow-up static review on the latest head: I left six inline comments covering meta-parameter warm-up failure, Kafka consumer-group interference, retention deleting the published target, two exporter shutdown/lifecycle races, and unbounded export logs.

The meta warm-up, Kafka input reuse, publication retention, and shutdown issues are blockers because they can prevent exports, disrupt live training, or publish after cleanup. Static review only; tests and builds were not run as requested.

eric-gecheng and others added 6 commits July 21, 2026 11:24
export_dense_model_cpu's pre-trace warm-up runs a forward to materialize
lazy dense submodules, but it ran before embedding params were
materialized: models default to meta device and restore_model happens
only later, so the warm-up's sparse lookup executed on meta tensors and
crashed. The distributed path avoids this by restoring a materialized
DMP first; the E2E test hid the bug by calling init_parameters(..., cpu)
on the model before export, so real online dense exports (which never
init_parameters) failed at warm-up. Call init_parameters(model, device)
right before the warm-up to allocate meta embeddings -- the warm-up only
needs shapes to flow through lazy modules, and real weights still come
from restore_model(..., error_on_missing_keys=True) below. Rework the
E2E test to export a meta model (no init_parameters) so it guards the
fix instead of masking it.

Co-Authored-By: Claude <noreply@anthropic.com>
export_dense_model_cpu falls back to pipeline_config.train_input_path
when data_input_path is unset. For Kafka training that path carries the
live training group.id, so the export's predict dataloader subscribed
with it, joined the training consumer group, and held partitions through
the slow tracing/restoring/scripting -- rebalancing training on every
checkpoint. Rewrite group.id to an isolated <orig>__dense_export group
when the fallback input is a kafka URI, so the export consumer never
joins the training group; non-kafka inputs and an explicit
data_input_path are left untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
_prune_old_dense_versions keeps the lexicographically-newest K versions
and deletes the rest, but the just-published version (what current.json
points at) can sort outside the newest K: an explicit --version with an
older timestamp, or clock rollback after a restart -- the worker's
_last_version starts empty and is not seeded from disk, so
_make_monotonic_version degrades to the raw clock. Deleting that version
left current.json referencing a missing directory. Read the version
current.json points at and always spare it from deletion, even when it
sorts outside the newest K; a missing or corrupt current.json degrades
to pure newest-K retention.

Co-Authored-By: Claude <noreply@anthropic.com>
… backstop

close() detached the finalizer before joining the worker, and
_close_timeout covered only a single task timeout. At close the worker
can have an in-flight export plus one pending (latest-wins coalescing
caps it at one), so draining could need two task timeouts while
_close_timeout allowed one: the join timed out with the worker alive,
and the already-detached finalizer left no atexit backstop, so close()
returned with a live subprocess publisher after ckpt_manager.close().
Make _close_timeout cover an in-flight plus one pending task, and
detach the finalizer only after the worker actually stops so a worker
that outlives the close timeout keeps the drain backstop.

Co-Authored-By: Claude <noreply@anthropic.com>
…eption

_train_and_evaluate only called online_dense_exporter.close() and
ckpt_manager.close() on the happy path, after the final save and eval.
Any raise in the training loop, on_train_end, final save, or eval
skipped them; and the bound self._worker_loop thread target keeps the
exporter reachable, so its weakref.finalize backstop does not fire when
the function unwinds -- a caller that catches the error leaks the
daemon worker and the protected checkpoint and may see a late
current.json publish. Wrap the training body (the epoch loop through
the final run_eval) in try/finally so both close() calls always run.

Co-Authored-By: Claude <noreply@anthropic.com>
Each export attempt writes one <version>.log under dense_hot_export/logs/,
but version retention only prunes version dirs (and only on successful
exports), never these logs -- a long-running online-learning job
accumulates one file/inode per checkpoint including failed exports, and
failed attempts never reach the version-retention call at all. Prune
logs in the worker's _run_task finally (runs for success, timeout, and
failure), keeping the newest K (ONLINE_DENSE_EXPORT_KEEP_LOGS, default
3); version names are timestamps, so name order is chronological.

Co-Authored-By: Claude <noreply@anthropic.com>
@alibaba alibaba deleted a comment from github-actions Bot Jul 21, 2026
@alibaba alibaba deleted a comment from github-actions Bot Jul 21, 2026
@alibaba alibaba deleted a comment from github-actions Bot Jul 21, 2026
@alibaba alibaba deleted a comment from github-actions Bot Jul 21, 2026
@alibaba alibaba deleted a comment from github-actions Bot Jul 21, 2026
@alibaba alibaba deleted a comment from github-actions Bot Jul 21, 2026
@alibaba alibaba deleted a comment from github-actions Bot Jul 21, 2026
@eric-gecheng eric-gecheng added the codex-review Let Codex Review label Jul 21, 2026
@github-actions github-actions Bot removed the codex-review Let Codex Review label Jul 21, 2026
@eric-gecheng eric-gecheng changed the title [WIP] [feat]online export of dense models [feat]online export of dense models Jul 22, 2026
eric-gecheng and others added 3 commits July 22, 2026 16:49
dynamicemb features are FG-hashed into a 2**63-1 bucket, so warm-up
batches carry ~1e18 ids. On the GPU/DMP path the dynamicemb backend
remaps them into [0, max_capacity), but export_dense_model_cpu runs the
raw model on CPU where that backend is not active, so ebc_user degrades
to F.embedding_bag and its strict range-check rejects the id. The warm-up
only materializes lazy-module shapes for FX tracing -- its lookup values
are discarded (real weights come from restore_model; the trace uses
symbolic proxies) -- so zero sparse_feature values in-place before the
warm-up forward; 0 is unconditionally in range. The GPU distributed
export path is unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
…dense_hot_export

ONLINE_DENSE_EXPORT_DIR was returned verbatim as the export root, so the
publish tree landed directly at the serving root without the dense_hot_export
leaf the inference processor reads from. resolve_dense_export_root now always
appends dense_hot_export, treating the env var as the serving root in both the
override and default branches. The manager passes the pre-suffix serving root
to the export subprocess so it re-resolves to the same leaf instead of
double-nesting dense_hot_export.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread tzrec/utils/env_util.py
return flag


def use_distributed_embedding() -> bool:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

use_distributed_embedding already in acc/utils.py

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.

2 participants