Repository navigation
[feat] retire a stream lineage once its successor catches up - #712
tiankongdeguiji merged 27 commits into
Conversation
Add ONLINE_LEARNING_SUCCESSOR_DIR so a daily-based streaming job ends itself when the next day's lineage has published data at least as fresh, letting DataWorks deploy the successor after the predecessor exits. Adds a best-practice doc for the DataWorks + PAI-DLC + PAI-EAS workflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Lets the online-learning full export set the per-lineage FeatureDB version from the command line instead of a placeholder in the config file; the doc also renames the stream/export nodes to train_stream/full_export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
The training-side hand-over already checks successor freshness, so the deploy node no longer repeats the comparison, which could block the switch when the predecessor publishes once more while finalizing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
…ghtly in DLC nodes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
…over Once both lineages consume at the Kafka head, each one's published data_timestamp only advances at its own publish, so with a fixed phase offset between the two publish cadences the strict successor >= own comparison could never pass, leaving the old job running until its max runtime and blocking every later deploy. The check now accepts a successor trailing by up to one publish interval plus a 5-minute margin (15 minutes when the cadence is step-based), overridable via ONLINE_LEARNING_HANDOVER_TOLERANCE_SECS, and logs the observed lag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Lineage hand-over no longer requires online dense export. Each lineage writes its quorum consumed event-time to <model_dir>/progress.json at the log cadence, and the predecessor retires once the successor trails by at most ONLINE_LEARNING_HANDOVER_TOLERANCE_SECS (default 300s); ONLINE_LEARNING_SUCCESSOR_DIR now names the successor's model_dir. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
| data_ts = _valid_data_ts( | ||
| checkpoint_util.quorum_event_time(worker_ts, self._quorum) | ||
| ) | ||
| decision = [False] |
There was a problem hiding this comment.
These object collectives run on the default process group, which is NCCL on GPU training. all_gather_object/broadcast_object_list then pickle to CPU tensors, copy them H2D/D2H through torch.cuda.current_device(), and force a device sync in the train loop — on the same communicator the forward/backward collectives use. The sibling collectives in this same loop deliberately avoid that: CheckpointManager gathers event-times on a dedicated gloo group (checkpoint_util.py:463) and OnlineDenseExportManager does the same (online_dense_export_util.py:359), and dist_util.get_dist_object_pg() exists for exactly this. Worth reusing one of those patterns here (e.g. create a gloo side group in __init__ and pass group= to both calls). Amortized cost is small at the default log_step_count_steps, but it scales linearly if the log cadence is lowered.
| class OnlineLearningHandover: | ||
| """Retire a stream lineage once the next one has consumed data as fresh. | ||
|
|
||
| Every lineage writes its quorum consumed event-time to |
There was a problem hiding this comment.
Minor: a JSON integer too large for a float (e.g. a 400-digit data_ts written by hand or by a foreign writer on the shared mount) makes math.isfinite(ts) raise OverflowError (it converts the int to float), which is not caught by except (OSError, ValueError) in _read_successor_ts — so instead of degrading to "no hand-over" like the other corrupt-file cases, rank 0 dies between the gather and the broadcast and the job goes down with a confusing traceback. Converting inside a try: float(ts) except (TypeError, ValueError, OverflowError): return None (or adding OverflowError to the read's except tuple) closes it in one line.
| def setUp(self): | ||
| self.test_dir = make_test_dir() | ||
| self.addCleanup(shutil.rmtree, self.test_dir) | ||
|
|
There was a problem hiding this comment.
Nice coverage of the decision logic and corrupt-file cases. One gap worth closing: the distributed branch of update_and_check (dist.is_initialized() path — worker_ts sizing, all_gather_object, and broadcast_object_list) and the rank≠0 side of the RANK gate are never executed by these tests, and that's the code whose failure mode is a deadlock or rank desync in real multi-worker jobs. Both are cheap to cover without GPUs by mocking dist.is_initialized/get_world_size/all_gather_object/broadcast_object_list (e.g. an all_gather_object side_effect filling [1000.0, -1.0] and asserting rank 0 writes the quorum value; a broadcast_object_list side_effect setting the decision and asserting a RANK=1 process returns the broadcast result without touching the filesystem). Non-blocking suggestion.
Review summaryReviewed across code quality, performance, test coverage, documentation accuracy, and robustness. Overall this is a well-built feature: the hand-over decision is broadcast so all ranks exit the loops together (keeping the every-step collectives matched), the quorum math is reused from Findings (inline, all non-blocking):
Two smaller observations not worth inline threads: rank 0 does two remote-filesystem round trips per check (write own progress + open a usually-absent successor file) while the other ranks block in the broadcast — fine at the default 🤖 Generated with Claude Code |
FeatureDB now holds only each lineage's streaming deltas, which EAS applies on top of the daily full export it loads from OSS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH
|
|
||
| ### full_export | ||
|
|
||
| 将天级模型的 embedding 全量上传到本链路的 FeatureDB 版本,详见[dynemb 流式导出与 FeatureDB 全量上传](../usage/export.md)。天级配置中 `export_config.dynamic_embedding.feature_store_config` 不写 `version`,导出时通过 `--edit_config_json` 按业务日期指定: |
There was a problem hiding this comment.
full_export 只需要导出到oss即可, 不需要将embedding 上传featureDB, 只有增量训练才需要上传featureDB
|
|
||
| deploy 使用 PyODPS 3 节点,调度资源组选择 `dataworks_pairec_task_pod`(其中预置了 `eascmd64`),调度参数同上,节点内通过 `args` 读取。 | ||
|
|
||
| 服务挂载两个 OSS 目录: |
There was a problem hiding this comment.
只要挂载一个oss 目录, 就是T 天的全量导出目录。
增量训练 stream_T会直接在此目录中创建dense_hot_export/current.json
| }, | ||
| "dense_hot_load": { | ||
| "enable": True, | ||
| "current_json": "/mnt/serving/dense_hot_export/current.json", |
There was a problem hiding this comment.
无需指定current_json 字段。
|
|
||
| 在线学习相关的 Processor 参数: | ||
|
|
||
| - `distributed_embedding`:需为 `true`,增量 embedding 轮询依赖分布式 embedding。 |
There was a problem hiding this comment.
processor 无需配置distributed_embedding, processor 会自动判断。
| 在线学习相关的 Processor 参数: | ||
|
|
||
| - `distributed_embedding`:需为 `true`,增量 embedding 轮询依赖分布式 embedding。 | ||
| - `feature_store_embedding_polling`:从 FeatureDB 轮询增量 embedding。配置该块且不写 `mode` 时即为 `apply` 模式:服务启动时先从头扫描版本 `version` 中的全部数据,再增量更新。`feature_view_name` 与 `version` 必填,且需与训练写入的 FeatureView 和版本一致;同时需要 `access_key_id`、`access_key_secret`、`region`、`fs_project`、`featuredb_username`、`featuredb_password`。 |
There was a problem hiding this comment.
改为:
feature_store_embedding_polling: 从FeatureDB 轮询增量embedding的配置。 必填项为feature_view_name和version, 且需与训练时写入的feature view和version一致。 选填参数sparse_poll_interval_secs, 不填时默认为 0,表示由processor按同频率轮询dense和sparse, 若设置>0, 则可以以更高的频率拉取embedding并更新。
|
|
||
| - `distributed_embedding`:需为 `true`,增量 embedding 轮询依赖分布式 embedding。 | ||
| - `feature_store_embedding_polling`:从 FeatureDB 轮询增量 embedding。配置该块且不写 `mode` 时即为 `apply` 模式:服务启动时先从头扫描版本 `version` 中的全部数据,再增量更新。`feature_view_name` 与 `version` 必填,且需与训练写入的 FeatureView 和版本一致;同时需要 `access_key_id`、`access_key_secret`、`region`、`fs_project`、`featuredb_username`、`featuredb_password`。 | ||
| - `sparse_poll_interval_secs`(位于 `feature_store_embedding_polling` 内):默认 0,即不做周期扫描,只在加载新的 dense 版本前拉取一次增量,sparse 随 dense 按 `paired_dump_interval_*` 的节奏更新。若训练配置了更细的 `sparse_dump_interval_*`,希望线上 embedding 更快生效,需将其设为正数(秒)。 |
There was a problem hiding this comment.
改为:
sparse_poll_interval_secs(位于feature_store_embedding_polling内):默认 0,即不做周期扫描,只在加载新的 dense 版本同时拉取一次增量embedding。 拉取的频率由dense_hot_load.dense_poll_interval_minutes控制。 若配置sparse_poll_interval_secs>0, 则可以让embedding更快的生效。
|
|
||
| 此外,全量导出与流式训练的产物需要相互兼容: | ||
|
|
||
| - 两者需使用相同的模型配置与导出相关环境变量(如 `INPUT_TILE`)。Processor 会比较热加载版本与初始模型的 `dense_meta.json`。 |
There was a problem hiding this comment.
如 INPUT_TILE, 这个可以删除, 因为input_tile 在distributed export 下默认就是 3。
| "model_config": { | ||
| "outputs": "<输出名,逗号分隔>", | ||
| "fg_mode": "normal", | ||
| "distributed_embedding": True, |
There was a problem hiding this comment.
"distributed_embedding": True,
这个无需配置
| "feature_view_name": "<与训练 feature_store_config 相同的 feature_view_name>", | ||
| "version": f"ol_{bizdate}", | ||
| }, | ||
| "dense_hot_load": { |
There was a problem hiding this comment.
dense_hot_load 里可以增加配置这个配置作为示例:
"dense_poll_interval_minutes": 1,
| --pipeline_config_path /mnt/data/online/stream.config \ | ||
| --continue_train \ | ||
| --fine_tune_checkpoint /mnt/data/online/daily/${bizdate} \ | ||
| --model_dir /mnt/data/online/stream/${bizdate}/model \ |
There was a problem hiding this comment.
增量训练的model_dir 应该配置成T 日的模型全量导出目录
… online learning doc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011fSGWxBXJPmjjvt7LVaXNp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011fSGWxBXJPmjjvt7LVaXNp
…ning doc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011fSGWxBXJPmjjvt7LVaXNp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011fSGWxBXJPmjjvt7LVaXNp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011fSGWxBXJPmjjvt7LVaXNp
Summary
Supports an online-learning setup where a daily incremental job is the base and a streaming (Kafka) job continues from each daily checkpoint. Each day the streaming job built on day T's checkpoint starts alongside the one built on day T-1. Once the new one has caught up, the old one should retire and EAS should switch over.
ONLINE_LEARNING_SUCCESSOR_DIR(the next lineage'smodel_dir). Everylog_step_count_steps, all ranks gather their consumed event-times and reconcile them with the same quorum as checkpointmetadata_ts. Rank 0 writes the result to<model_dir>/progress.json, reads the successor's, and broadcasts the decision. When the successor trails by no more thanONLINE_LEARNING_HANDOVER_TOLERANCE_SECS(default 300 s), all ranks leave both training loops at the same step.finalize_publishstill run. Other termination paths are unchanged.model_direquals this run's own, or if the tolerance is not a finite non-negative number. Both are checked on every rank, so a bad value can't strand the other ranks.tzrec.exportgains--edit_config_json(same semantics astrain_eval/predict), so the full export can set the per-lineage FeatureDB version without a config placeholder.docs/source/best_practices/online_learning.md. It covers the DataWorks DAG (train_daily -> full_export -> train_stream -> deploy) withdlc submitnode commands, a timeline diagram, per-lineage isolation (Kafkagroup.id, FeatureDB version, model and serving dirs), and the PyODPS 3 deploy node (eascmd modify).Design choices
progress.jsonin each lineage'smodel_dir, independent of online dense export and of the dataset type (any source that sets the data timestamp). An earlier revision compared the publisheddense_hot_export/current.json. That tied hand-over toONLINE_DENSE_EXPORT=1, and its value only moved once per publish interval.OnlineLearningHandoverandcreate_online_learning_handoverintzrec/utils/online_learning_util.py(progress file, quorum, tolerance, env parsing).main.pyonly creates it, callsupdate_and_check()at the log cadence, and acts on the result by exiting both loops and running the existing shutdown. This matches howDeltaEmbeddingDumper/OnlineDenseExportManagerare wired. Neither the checkpoint nor the export manager is involved. A source without timestamps never hands over, and rank 0 warns once.>=could fail indefinitely. That would keep the old job running until its max runtime and block every later deploy. A fixed 300 s default covers this, because progress now updates every check rather than every publish. Rank 0 logs the observed lag at each check.num_epochs: 1and nonum_steps, since any normal exit triggers deploy. A self-dependency on the deploy node keeps switches in date order.The deploy template's Processor keys were verified against the TorchRecProcessor source. The service mounts the daily full export as the model dir and the lineage serving root separately, and sets
dense_hot_load.current_jsonexplicitly, because the default looks under the model dir. It setsfeature_store_embedding_polling.{feature_view_name, version: ol_<bizdate>}, which defaults to apply mode, anddistributed_embedding: true.Test Plan
python -m unittest tzrec.utils.online_learning_util_test tzrec.main_test: 15 + 38 tests pass. New cases cover:model_dirrejectedcaught_upis called at the log cadence with the step and batch timestamp, and hand-over skips the epoch-end and final saves while dump, drain and finalize still runtzrec.exportforwarding--edit_config_jsontorchrunon 2x A10 (NCCL), ranks with different timestamps:pre-commit run,pyrefly check: clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01FouSwvnwPn2Z61mLnv4vsH