Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
a9d14cc
support large scale embedding inference, split model and export embed…
eric-gecheng Dec 18, 2025
a2b6779
export model of distributed embedding mode
eric-gecheng Feb 3, 2026
b354d6e
merge master
eric-gecheng Feb 3, 2026
10d027c
add support of tiling to dense subgraph
eric-gecheng Feb 10, 2026
d0fc488
Merge branch 'master' into feature/export_split
eric-gecheng Apr 28, 2026
c8d3a9d
dynemb supports input_tile=3
eric-gecheng May 5, 2026
6441395
Merge branch 'bugfix/dynemb_input_tile_3' into feature/export_split
eric-gecheng May 5, 2026
e27ac7d
dynamic emb supports distributed export
eric-gecheng May 7, 2026
cca0f6c
fix input_tile 3 on dynemb
eric-gecheng May 13, 2026
a4a18a2
Merge branch 'master' into feature/export_split
eric-gecheng May 13, 2026
c45c386
warmup model to avoid lazy components when exporting
eric-gecheng May 15, 2026
4f7df8b
build INPUT_TILE=3 dynamicemb user-side aliases in a per-rank temp vi…
eric-gecheng May 21, 2026
6233314
input_tile default to 3 when using distributed export
eric-gecheng May 21, 2026
5b68cab
Merge branch 'master' into feature/export_split
eric-gecheng May 24, 2026
6eff2ff
fix model._features
eric-gecheng May 28, 2026
5ee85b3
merge master
eric-gecheng May 28, 2026
0d6815f
Merge branch 'master' into feature/export_split
eric-gecheng Jun 1, 2026
414ec7d
merge master
eric-gecheng Jun 1, 2026
376c320
merge branch feature/export_split
eric-gecheng Jun 1, 2026
f347a21
fix batch_size missing in dense subgraph
eric-gecheng Jun 1, 2026
29fdabd
Merge branch 'master' into feature/export_split
eric-gecheng Jun 2, 2026
74a86c5
Merge branch 'feature/export_split' of github.com:eric-gecheng/TorchE…
eric-gecheng Jun 3, 2026
7cb07cd
ensure input tile for distributed embedding
eric-gecheng Jun 3, 2026
6874133
fix input tile
eric-gecheng Jun 4, 2026
cb885a9
export dymemb scores
eric-gecheng Jun 7, 2026
26beb62
Merge branch 'master' into feature/export_split
eric-gecheng Jun 7, 2026
57ee9c8
distributed export support multiple gpu checkpoint
eric-gecheng Jun 8, 2026
ff32763
ensure rank0 for distributed export
eric-gecheng Jun 9, 2026
f92da02
fix stale emb_config
eric-gecheng Jun 9, 2026
deb62f9
raw/shared key mismatch
eric-gecheng Jun 9, 2026
67d3e36
deepcopy pipeline_config
eric-gecheng Jun 9, 2026
813f4fb
use set instead of list for better performance
eric-gecheng Jun 9, 2026
a4e6ec3
export_distributed_embedding return none
eric-gecheng Jun 9, 2026
1e15c81
use capital letter for USE_DISTRIBUTED_EMBEDDING
eric-gecheng Jun 9, 2026
4382b4e
update document
eric-gecheng Jun 9, 2026
cf1bafd
Merge branch 'feature/export_split' of github.com:eric-gecheng/TorchE…
eric-gecheng Jun 10, 2026
1125716
code format
eric-gecheng Jun 10, 2026
fbab7ea
fix(export): disambiguate EC/EBC sparse embeddings with shared names
eric-gecheng Jun 10, 2026
1931e1b
Merge branch 'feature/export_split' of github.com:eric-gecheng/TorchE…
eric-gecheng Jun 10, 2026
84f242c
add delta_embedding_dump
eric-gecheng Jun 16, 2026
55c430c
add delta_embedding_dump test case
eric-gecheng Jun 16, 2026
b8d7f6f
delta dump supports multi gpu training
eric-gecheng Jun 17, 2026
3f8e672
fix: eval may pollute delta tracker
eric-gecheng Jun 18, 2026
e9f4482
fix: zch logic in delta dump has dead code
eric-gecheng Jun 18, 2026
fcd3c92
fix:enable auto-compact for delta tracker
eric-gecheng Jun 18, 2026
96d0b0d
fix: performance tune for delta dump python dict
eric-gecheng Jun 18, 2026
3c72721
add integration test
eric-gecheng Jun 18, 2026
addb17c
fix comments
eric-gecheng Jun 18, 2026
004e7f1
fix: tail flush delta export
eric-gecheng Jun 18, 2026
996071c
fix: flush trailing delta embeddings without ragged or overwritten sh…
eric-gecheng Jun 18, 2026
203ba55
test: add multi-GPU dynamicemb integration test for delta embedding dump
eric-gecheng Jun 18, 2026
ff0aa39
fix: sync final delta dump step across ranks
eric-gecheng Jun 22, 2026
6a1b381
perf: flush each dynamic emb module once per delta dump
eric-gecheng Jun 22, 2026
305fed7
fix: atomically write delta dump shards via temp + os.replace
eric-gecheng Jun 22, 2026
a925103
fix: remove enable field from DeltaEmbeddingDumpConfig protobuf
eric-gecheng Jun 22, 2026
5029e00
perf: cache table shard infos in DeltaEmbeddingDumper to avoid recomp…
eric-gecheng Jun 23, 2026
676fd8a
fix: avoid NCCL hang in final_dump when rank has no training data
eric-gecheng Jun 23, 2026
3118469
fix: prevent ragged shard dirs when synced final step is a dump boundary
eric-gecheng Jun 23, 2026
158710c
test: add coverage for out-of-range and empty ids in _lookup_embeddings
eric-gecheng Jun 23, 2026
49405b9
docs: add argument docstrings to public APIs in delta_embedding_dump
eric-gecheng Jun 23, 2026
c854723
refactor: move delta embedding dump validation into DeltaEmbeddingDum…
eric-gecheng Jun 23, 2026
0dea6b1
Merge branch 'master' into feat/delta_export
eric-gecheng Jun 23, 2026
d9c2658
Merge branch 'feature/export_split' into feat/dense_export
eric-gecheng Jun 23, 2026
742450b
initial implementation of dense hot export
eric-gecheng Jun 23, 2026
8801160
add online_dense_export_util.py
eric-gecheng Jun 23, 2026
7998052
Optimize online dense export with CPU-only path
eric-gecheng Jul 2, 2026
078bd64
bug fix
eric-gecheng Jul 2, 2026
0d3ef05
merge master
eric-gecheng Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions tzrec/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
)
from tzrec.utils.filesystem_util import url_to_fs
from tzrec.utils.logging_util import ProgressLogger, logger
from tzrec.utils.online_dense_export_util import OnlineDenseExportManager
from tzrec.utils.plan_util import create_planner, get_default_sharders
from tzrec.version import __version__ as tzrec_version

Expand Down Expand Up @@ -336,6 +337,7 @@ def _train_and_evaluate(
ignore_restore_optimizer: bool = False,
dataloader_state: Optional[Dict[str, Any]] = None,
delta_embedding_dumper: Optional[DeltaEmbeddingDumper] = None,
pipeline_config_path: Optional[str] = None,
) -> None:
"""Train and evaluate the model."""
is_rank_zero = int(os.environ.get("RANK", 0)) == 0
Expand Down Expand Up @@ -450,6 +452,16 @@ def run_eval(step: int, epoch: int) -> None:
)
model.train()

online_dense_exporter = OnlineDenseExportManager(
model_dir,
pipeline_config_path or os.path.join(model_dir, "pipeline.config"),
ckpt_manager,
)

def after_checkpoint_saved(step: int, data_ts: float) -> None:
checkpoint_path = os.path.join(model_dir, f"model.ckpt-{step}")
online_dense_exporter.submit(step, checkpoint_path, data_ts)

# this rank's last consumed event-time, reused by the epoch / final saves
data_timestamp = -1.0
for i_epoch in epoch_iter:
Expand Down Expand Up @@ -530,6 +542,7 @@ def run_eval(step: int, epoch: int) -> None:
dataloader_state,
data_timestamp=data_timestamp,
):
after_checkpoint_saved(i_step, data_timestamp)
run_eval(i_step, i_epoch)
if train_config.is_profiling:
prof.step()
Expand All @@ -542,6 +555,7 @@ def run_eval(step: int, epoch: int) -> None:
epoch=i_epoch,
data_timestamp=data_timestamp,
):
after_checkpoint_saved(i_step, data_timestamp)
run_eval(i_step, i_epoch)

if use_step and i_step >= train_config.num_steps - 1:
Expand Down Expand Up @@ -586,7 +600,9 @@ def run_eval(step: int, epoch: int) -> None:
data_timestamp=data_timestamp,
final=True,
):
after_checkpoint_saved(i_step, data_timestamp)
run_eval(i_step, i_epoch)
online_dense_exporter.close()
ckpt_manager.close()


Expand Down Expand Up @@ -851,6 +867,7 @@ def train_and_evaluate(
ignore_restore_optimizer=ignore_restore_optimizer,
dataloader_state=dataloader_state,
delta_embedding_dumper=delta_embedding_dumper,
pipeline_config_path=os.path.join(pipeline_config.model_dir, "pipeline.config"),
)
if is_local_rank_zero:
logger.info("Train and Evaluate Finished.")
Expand Down
154 changes: 154 additions & 0 deletions tzrec/tools/online_dense_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Copyright (c) 2025, Alibaba Group;
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Copyright (c) 2026, Alibaba Group;
# Licensed under the Apache License, Version 2.0 (the "License");

import argparse
import datetime
import json
import os
import shutil
from typing import Any, Dict, Optional

from tzrec.main import _create_features, _create_model
from tzrec.models.model import ScriptWrapper
from tzrec.utils import config_util, env_util
from tzrec.utils.export_util import (
ensure_input_tile_for_distributed_embedding,
export_dense_model_cpu,
)
from tzrec.utils.logging_util import logger
from tzrec.utils.online_dense_export_util import make_version

DENSE_HOT_EXPORT_DIR = "dense_hot_export"
VERSIONS_DIR = "versions"
CURRENT_JSON = "current.json"


def _utc_now() -> str:
return datetime.datetime.now(datetime.timezone.utc).isoformat()


def _atomic_write_json(path: str, data: Dict[str, Any]) -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
tmp_path = f"{path}.tmp.{os.getpid()}"
with open(tmp_path, "w") as f:
json.dump(data, f, indent=2, sort_keys=True)
f.write("\n")
os.replace(tmp_path, path)


def _publish_current(current_path: str, payload: Dict[str, Any]) -> None:
_atomic_write_json(current_path, payload)


def export_online_dense_model(
pipeline_config_path: str,
checkpoint_path: str,
model_dir: str,
version: Optional[str] = None,
checkpoint_step: Optional[int] = None,
data_timestamp: Optional[float] = None,
) -> Dict[str, Any]:
"""Export and publish one online-learning dense model version."""
if not env_util.use_distributed_embedding():
raise RuntimeError("ONLINE_DENSE_EXPORT requires USE_DISTRIBUTED_EMBEDDING=1.")

ensure_input_tile_for_distributed_embedding()

version = version or make_version()
export_root = os.path.join(model_dir, DENSE_HOT_EXPORT_DIR)
versions_root = os.path.join(export_root, VERSIONS_DIR)
version_dir = os.path.join(versions_root, version)
tmp_dir = f"{version_dir}.tmp.{os.getpid()}"

if os.path.exists(version_dir):
raise RuntimeError(f"dense version already exists: {version_dir}")
if os.path.exists(tmp_dir):
shutil.rmtree(tmp_dir)
os.makedirs(versions_root, exist_ok=True)

pipeline_config = config_util.load_pipeline_config(pipeline_config_path)
features = _create_features(
list(pipeline_config.feature_configs), pipeline_config.data_config
)
model = _create_model(
pipeline_config.model_config,
features,
list(pipeline_config.data_config.label_fields),
sampler_type=None,
)
model.set_is_inference(True)
scripted_model = ScriptWrapper(model)

try:
export_dense_model_cpu(
pipeline_config=pipeline_config,
model=scripted_model,
checkpoint_path=checkpoint_path,
save_dir=tmp_dir,
)

required_files = ["scripted_model.pt", "dense_meta.json"]
for file_name in required_files:
file_path = os.path.join(tmp_dir, file_name)
if not os.path.exists(file_path):
raise RuntimeError(f"missing dense export artifact: {file_path}")

ready_path = os.path.join(tmp_dir, "READY")
with open(ready_path, "w") as f:
f.write(_utc_now())
f.write("\n")

os.rename(tmp_dir, version_dir)
except BaseException:
if os.path.exists(tmp_dir):
shutil.rmtree(tmp_dir)
raise

current_payload: Dict[str, Any] = {
"version": version,
"checkpoint_path": os.path.abspath(checkpoint_path),
"created_at": _utc_now(),
}

# Keep the service-facing pointer beside the immutable dense export versions.
_publish_current(os.path.join(export_root, CURRENT_JSON), current_payload)
logger.info("published online dense export version %s to %s", version, version_dir)
return current_payload


def main() -> None:
"""Run the online dense export command-line entrypoint."""
parser = argparse.ArgumentParser(
description="Export one online-learning dense model version."
)
parser.add_argument("--pipeline_config_path", required=True)
parser.add_argument("--checkpoint_path", required=True)
parser.add_argument("--model_dir", required=True)
parser.add_argument("--version", default=None)
parser.add_argument("--checkpoint_step", type=int, default=None)
parser.add_argument("--data_timestamp", type=float, default=None)
args = parser.parse_args()

export_online_dense_model(
pipeline_config_path=args.pipeline_config_path,
checkpoint_path=args.checkpoint_path,
model_dir=args.model_dir,
version=args.version,
checkpoint_step=args.checkpoint_step,
data_timestamp=args.data_timestamp,
)


if __name__ == "__main__":
main()
116 changes: 116 additions & 0 deletions tzrec/tools/online_dense_export_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Copyright (c) 2025, Alibaba Group;
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Copyright (c) 2026, Alibaba Group;
# Licensed under the Apache License, Version 2.0 (the "License");

import json
import os
import tempfile
import unittest
from types import SimpleNamespace
from unittest import mock

from tzrec.tools.online_dense_export import export_online_dense_model


class OnlineDenseExportTest(unittest.TestCase):
def test_export_online_dense_model_publishes_ready_version(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
checkpoint_path = os.path.join(tmp_dir, "model.ckpt-10")
os.makedirs(checkpoint_path)
pipeline_config_path = os.path.join(tmp_dir, "pipeline.config")
open(pipeline_config_path, "w").close()

def fake_export_dense_model_cpu(**kwargs):
self.assertNotIn("dense_only", kwargs)
save_dir = kwargs["save_dir"]
os.makedirs(save_dir, exist_ok=True)
with open(os.path.join(save_dir, "scripted_model.pt"), "w") as f:
f.write("pt")
with open(os.path.join(save_dir, "dense_meta.json"), "w") as f:
json.dump({"group": ["feature__ebc"]}, f)

dummy_config = SimpleNamespace(
feature_configs=[],
data_config=SimpleNamespace(label_fields=[]),
model_config=SimpleNamespace(),
)
dummy_model = mock.Mock()

with (
mock.patch.dict(
os.environ,
{"USE_DISTRIBUTED_EMBEDDING": "1"},
clear=False,
),
mock.patch(
"tzrec.tools.online_dense_export.config_util.load_pipeline_config",
return_value=dummy_config,
),
mock.patch(
"tzrec.tools.online_dense_export._create_features",
return_value=[],
),
mock.patch(
"tzrec.tools.online_dense_export._create_model",
return_value=dummy_model,
),
mock.patch(
"tzrec.tools.online_dense_export.ScriptWrapper",
side_effect=lambda model: model,
),
mock.patch(
"tzrec.tools.online_dense_export.export_dense_model_cpu",
side_effect=fake_export_dense_model_cpu,
),
):
payload = export_online_dense_model(
pipeline_config_path=pipeline_config_path,
checkpoint_path=checkpoint_path,
model_dir=tmp_dir,
version="20260623174703",
checkpoint_step=10,
data_timestamp=42.0,
)

version_dir = os.path.join(
tmp_dir, "dense_hot_export", "versions", "20260623174703"
)
self.assertTrue(os.path.exists(os.path.join(version_dir, "READY")))
self.assertTrue(
os.path.exists(os.path.join(version_dir, "scripted_model.pt"))
)
self.assertTrue(
os.path.exists(os.path.join(version_dir, "dense_meta.json"))
)

current_path = os.path.join(tmp_dir, "dense_hot_export", "current.json")
with open(current_path) as f:
current = json.load(f)
self.assertEqual(
set(current.keys()), {"checkpoint_path", "created_at", "version"}
)
self.assertEqual(current["version"], "20260623174703")
self.assertEqual(
current["checkpoint_path"], os.path.abspath(checkpoint_path)
)
self.assertTrue(current["created_at"])
self.assertFalse(os.path.exists(os.path.join(tmp_dir, "dense_hot_update")))

self.assertEqual(
set(payload.keys()), {"checkpoint_path", "created_at", "version"}
)
self.assertEqual(payload["version"], "20260623174703")


if __name__ == "__main__":
unittest.main()
Loading
Loading