Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
ad3aef7
[feat] qwen2 generative-rec LM: HF-Qwen2 backbone + SID vocab
WhiteSwan1 Jun 8, 2026
c825f1d
Merge remote-tracking branch 'upstream/master' into support_qwen
WhiteSwan1 Jun 8, 2026
a8eeb12
[refactor] generative-rec LM: base/subclass split + vectorized splice
WhiteSwan1 Jun 9, 2026
21c344a
[test] generative-rec LM: base + Qwen2 splice/tokenize unit tests
WhiteSwan1 Jun 9, 2026
bfe657f
[feat] generative-rec LM: branch predict into train/eval loss vs infe…
WhiteSwan1 Jun 9, 2026
9acfd86
[refactor] generative-rec LM: register families directly, drop class_…
WhiteSwan1 Jun 9, 2026
632fce4
[feat] generative-rec LM: pre-allocate activation pool to fix per-ran…
WhiteSwan1 Jun 10, 2026
b2f1890
[config] generative-rec LM example: set user_sequence sequence_length…
WhiteSwan1 Jun 10, 2026
e51651d
[feat] generative-rec LM: save HF-format checkpoints alongside DCP + …
WhiteSwan1 Jun 12, 2026
59b3c5b
[refactor] generative-rec LM export: reuse model.export_hf in the off…
WhiteSwan1 Jun 12, 2026
679aa17
[refactor] generative-rec LM: fold HF export into main.export, delete…
WhiteSwan1 Jun 12, 2026
8fff846
[refactor] generative-rec LM: pipeline-driven HF import/export + firs…
WhiteSwan1 Jun 16, 2026
9b43143
[refactor] generative-rec LM: encapsulate __init__ into helpers, trim…
WhiteSwan1 Jun 16, 2026
d4e87ff
Merge upstream/master (alibaba) into support_qwen
WhiteSwan1 Jun 16, 2026
5526dc9
[fix] generative-rec LM: build LM in fp32 (master weights) — fixes lr…
WhiteSwan1 Jun 17, 2026
d72436a
[fix] generative-rec LM: SID validity gate + code-review fixes
WhiteSwan1 Jun 17, 2026
97390b2
[refactor] _validate_sid_candidates: F.pad + masked_fill
WhiteSwan1 Jun 17, 2026
17d4350
[refactor] generative-rec LM: trim over-detailed comments
WhiteSwan1 Jun 17, 2026
1871227
[refactor] generative-rec LM: extract _detokenize_sids
WhiteSwan1 Jun 17, 2026
59312bf
[docs] generative-rec LM: fix misleading "reader caps" comments
WhiteSwan1 Jun 17, 2026
0487481
[feat] genrec LM: ALGR-style escalating-beam decode (dynamic_beam)
WhiteSwan1 Jun 18, 2026
af87888
[examples] genrec LM: data converters + configs + smoke/train scaffol…
WhiteSwan1 Jun 18, 2026
446d857
[feat] generative-rec LM: feature retrieval via init_input/build_inpu…
WhiteSwan1 Jun 24, 2026
7c888a0
[feat] generative-rec LM: make generated_sids_key + param_dtype proto…
WhiteSwan1 Jun 24, 2026
bc7c3f9
[refactor] generative-rec LM: drop now-redundant GENERATED_SIDS_KEY c…
WhiteSwan1 Jun 24, 2026
214d44d
[refactor] generative-rec LM: collapse param_dtype double-lookup to o…
WhiteSwan1 Jun 24, 2026
b71efce
[feat] generative-rec LM: max_sequence_length as a model-config knob …
WhiteSwan1 Jun 24, 2026
b8f20cb
[refactor] generative-rec LM: drop _input_sequence_length; max_sequen…
WhiteSwan1 Jun 24, 2026
15bc2c0
[feat] generative-rec LM: make max_sequence_length required; migrate …
WhiteSwan1 Jun 24, 2026
b373e9e
[refactor] generative-rec LM: answer is a data_config.label_field, no…
WhiteSwan1 Jun 25, 2026
0730645
[refactor] genreclm: derive SID-column names instead of restating them
WhiteSwan1 Jun 25, 2026
d68f594
[refactor] genreclm proto: drop reserved markers for removed fields
WhiteSwan1 Jun 25, 2026
af2d7bb
[refactor] genreclm: drop history_group_name; use the single feature_…
WhiteSwan1 Jun 25, 2026
c168cc3
[refactor] genreclm: trim redundant inline comments
WhiteSwan1 Jun 25, 2026
9e82c8b
Merge remote-tracking branch 'upstream/master' into support_qwen
WhiteSwan1 Jun 29, 2026
ead0932
Merge remote-tracking branch 'upstream/master' into support_qwen
WhiteSwan1 Jul 20, 2026
0e639fb
Merge remote-tracking branch 'upstream/master' into support_qwen
WhiteSwan1 Jul 27, 2026
2d97b5c
[fix] GenerativeRecLM: apply per-level SID offsets
WhiteSwan1 Jul 27, 2026
230dd06
[bugfix] genrec LM: fix beam OOM, dropped ignore_index, and export bloat
WhiteSwan1 Jul 27, 2026
848ed88
[refactor] genrec LM: test the beam kernel directly and drop test dup…
WhiteSwan1 Jul 27, 2026
4496bf1
[refactor] genrec LM: import transformers at module scope
WhiteSwan1 Jul 28, 2026
ed12b4f
[refactor] LinearDecayLR: rename total_size to num_training_steps
WhiteSwan1 Jul 28, 2026
9731ac5
[feat] genrec LM: add a mock config and an integration test
WhiteSwan1 Jul 28, 2026
7cba6a4
[feat] genrec LM: declare the SID space and prompt text on a SID feature
WhiteSwan1 Jul 28, 2026
f34975a
[bugfix] genrec: reject a SID feature knob that silently does nothing
WhiteSwan1 Jul 28, 2026
1576c16
[refactor] genrec: name the LM family after the SID family convention
WhiteSwan1 Jul 28, 2026
c349e04
[refactor] genrec: state the beam schedule as a list, drop the second…
WhiteSwan1 Jul 28, 2026
798e28b
[bugfix] genrec: keep tzrec.predict working by hiding the decode from FX
WhiteSwan1 Jul 28, 2026
d2c6d97
[ci] pin transformers to the OSS mirror instead of PyPI
WhiteSwan1 Jul 29, 2026
47d3356
[bugfix] genrec: let a SID feature live in an fg pipeline
WhiteSwan1 Jul 29, 2026
ccbbd35
Merge remote-tracking branch 'upstream/master' into support_qwen
WhiteSwan1 Jul 29, 2026
8c5621f
[ci] drop the inline comment from the transformers requirement
WhiteSwan1 Jul 29, 2026
cf32708
[bugfix] genrec: bound the hf_backbone walk so a wrapper cycle cannot…
WhiteSwan1 Jul 29, 2026
759bb98
[refactor] genrec: cut the explanatory prose down to what is load-bea…
WhiteSwan1 Jul 29, 2026
27fd5ba
[refactor] genrec: own the beam config in the base and name the kerne…
WhiteSwan1 Jul 29, 2026
712c863
[refactor] genrec: drop a redundant test and two dead SidFeature fields
WhiteSwan1 Jul 29, 2026
a3ff479
[refactor] genrec: test base behaviour on the base class
WhiteSwan1 Jul 29, 2026
dedea7a
[refactor] genrec: make the beam-width default a private class attribute
WhiteSwan1 Jul 29, 2026
386b6d6
[refactor] genrec: set the beam-width default in __init__
WhiteSwan1 Jul 29, 2026
d72c429
[refactor] genrec: require the beam schedule and the SID codebook
WhiteSwan1 Jul 29, 2026
07443e3
[bugfix] genrec: refuse dense EMA in HF export
WhiteSwan1 Jul 29, 2026
17a4631
[refactor] genrec: pin SidFeature value_dim and correct its fg comment
WhiteSwan1 Jul 29, 2026
ebcefab
[chore] bump version to 1.3.9
WhiteSwan1 Jul 29, 2026
9664eca
[feat] prompt: add prompt_config and the prompt compiler
WhiteSwan1 Aug 5, 2026
434f555
[feat] prompt: add the varlen prompt assembler
WhiteSwan1 Aug 5, 2026
c61003c
[feat] prompt: add the slot projection module
WhiteSwan1 Aug 5, 2026
04154f0
[feat] prompt: add the Qwen model core
WhiteSwan1 Aug 5, 2026
330167a
[feat] prompt: decode SIDs from the assembled prompt
WhiteSwan1 Aug 5, 2026
f631ea8
[feat] prompt: wire the compiler into the entry points
WhiteSwan1 Aug 5, 2026
a7dad47
[feat] prompt: add an end-to-end stack test
WhiteSwan1 Aug 5, 2026
336cf7c
[refactor] prompt: remove the pre-prompt generative stack
WhiteSwan1 Aug 5, 2026
fb7b3c6
[feat] prompt: persist the prompt contract and check it on restore
WhiteSwan1 Aug 5, 2026
c6ec0e2
[bugfix] prompt: fix four gaps a real training run exposed
WhiteSwan1 Aug 5, 2026
a589b7f
[bugfix] prompt: derive the feature groups a projected slot needs
WhiteSwan1 Aug 5, 2026
8bcf3d6
[bugfix] prompt: make predict work
WhiteSwan1 Aug 5, 2026
6149ffb
[bugfix] prompt: make export self-describing and refuse TorchScript
WhiteSwan1 Aug 5, 2026
21c7030
[doc] add the prompt-native generative recommendation manual
WhiteSwan1 Aug 5, 2026
a1d58ae
Merge remote-tracking branch 'upstream/master' into feat/prompt_genre…
WhiteSwan1 Aug 5, 2026
dce8ed1
[bugfix] prompt: write checkpoint assets from rank 0 only
WhiteSwan1 Aug 5, 2026
53e1062
[bugfix] prompt: bound the supervised logits window
WhiteSwan1 Aug 5, 2026
ea45b36
[refactor] prompt: pass the compiled prompt directly
WhiteSwan1 Aug 6, 2026
16e4a5c
[refactor] prompt: split out BasePromptGenerativeModel
WhiteSwan1 Aug 6, 2026
f9b1af6
[refactor] prompt: cleanup pass over the new stack
WhiteSwan1 Aug 6, 2026
c58c490
[bugfix] resolve slot width and shield the padded forward from FX
WhiteSwan1 Aug 6, 2026
108054a
[bugfix] left-pad the packed batch so short rows keep their answer
WhiteSwan1 Aug 10, 2026
7c77611
[ci] cover BasePromptGenerativeModel directly
WhiteSwan1 Aug 10, 2026
cc75e08
[ci] sort imports in the base prompt model test
WhiteSwan1 Aug 10, 2026
9bf8b58
[perf] build the prompt assembler once per dataset
WhiteSwan1 Aug 11, 2026
9906dae
[refactor] state the prompt export guard as one condition
WhiteSwan1 Aug 12, 2026
501e0df
[refactor] rename prompt-stack names that collide or under-describe
WhiteSwan1 Aug 12, 2026
11a980f
[perf] skip mean resizing when the backbone is still empty
WhiteSwan1 Aug 12, 2026
b098dae
Merge remote-tracking branch 'upstream/master' into feat/prompt_genre…
WhiteSwan1 Aug 13, 2026
ebac64c
[chore] bump version to 1.3.15
WhiteSwan1 Aug 13, 2026
6c64e36
[refactor] simplify prompt assembly tests
WhiteSwan1 Aug 13, 2026
15563a5
[bugfix] align prompt assembly with Qwen execution
WhiteSwan1 Aug 13, 2026
b156f80
[refactor] clarify prompt compilation flow
WhiteSwan1 Aug 13, 2026
a734fd2
[refactor] build prompt labels in model
WhiteSwan1 Aug 13, 2026
5346daa
[refactor] clarify prompt model naming
WhiteSwan1 Aug 14, 2026
029bd95
[refactor] encapsulate Qwen input padding
WhiteSwan1 Aug 14, 2026
fc9c3c2
[bugfix] give spliced projections the LM dtype
WhiteSwan1 Aug 17, 2026
595c226
[bugfix] require sid_space and response, and reject a placeholderless…
WhiteSwan1 Aug 17, 2026
47b7844
[bugfix] hash projection bodies in plan_hash
WhiteSwan1 Aug 17, 2026
ac05d04
[bugfix] close two holes in the prompt checkpoint guard
WhiteSwan1 Aug 17, 2026
2a8a085
[bugfix] build prompt embeddings outside both FX leaves
WhiteSwan1 Aug 17, 2026
2b530c0
[chore] cut the added inline comments to one line each
WhiteSwan1 Aug 17, 2026
a22d440
[refactor] drop the prompt proto reserved markers and close the tag gaps
WhiteSwan1 Aug 17, 2026
ab99b1c
[refactor] mark prompt sid_space required
WhiteSwan1 Aug 18, 2026
aa804e1
Merge remote-tracking branch 'upstream/master' into feat/prompt_genre…
WhiteSwan1 Aug 19, 2026
2704d1d
Merge remote-tracking branch 'upstream/master' into feat/prompt_genre…
WhiteSwan1 Aug 24, 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
1 change: 1 addition & 0 deletions requirements/runtime.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ tensorboard
torch==2.12.1
torchmetrics==1.0.3
torchrec==1.7.0
transformers @ https://tzrec.oss-accelerate.aliyuncs.com/third_party/transformers/transformers-4.51.2-py3-none-any.whl
47 changes: 46 additions & 1 deletion tzrec/datasets/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import numpy as np
import pyarrow as pa
import pyarrow.compute as pc
import torch
from torch import distributed as dist
from torch.utils.data import DataLoader, IterableDataset, get_worker_info

Expand All @@ -40,6 +41,8 @@
remove_nullable,
)
from tzrec.features.feature import BaseFeature
from tzrec.prompt.assembler import PromptAssembler
from tzrec.prompt.plan import CompiledPrompt, SlotSeg
from tzrec.protos import data_pb2
from tzrec.utils import config_util
from tzrec.utils.load_class import get_register_class_meta
Expand Down Expand Up @@ -97,6 +100,7 @@ class BaseDataset(IterableDataset, metaclass=_dataset_meta_cls):
mode (Mode): train or eval or predict.
debug_level (int): dataset debug level, when mode=predict and
debug_level > 0, will dump fg encoded data to debug_str
compiled_prompt (CompiledPrompt, optional): compiled prompt assembly contract.
"""

def __init__(
Expand All @@ -107,8 +111,18 @@ def __init__(
reserved_columns: Optional[List[str]] = None,
mode: Mode = Mode.EVAL,
debug_level: int = 0,
compiled_prompt: Optional[CompiledPrompt] = None,
) -> None:
super(BaseDataset, self).__init__()
self._prompt_assembler = (
PromptAssembler(
compiled_prompt.prompt_plan,
compiled_prompt.sid_space,
include_response=mode != Mode.PREDICT,
)
if compiled_prompt is not None
else None
)
self._data_config = data_config
self._features = features
self._input_path = input_path
Expand All @@ -121,8 +135,25 @@ def __init__(
else None
)

parser_features = features

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.

parser_features should not exist — the answer is a label, not a feature

tzrec/datasets/dataset.py:138-156 makes the parsed feature set mode-dependent: at
PREDICT it walks the compiled plan, subtracts the response slots' members from the
prompt slots', and hands DataParser a reduced list. It is there because the
supervised answer is declared in feature_configs, and a feature is an input column
the parser unconditionally requires — while at predict there is no answer column. The
filter is a patch undoing a declaration that shouldn't be made.

Three costs:

  1. One fact, two derivations. PromptAssembler(include_response=mode != PREDICT)
    (prompt/assembler.py:79-85) decides what the assembler reads; the set arithmetic
    in the dataset decides what the parser produces. Nothing checks the pair, and they
    are computed by different code. Drift is a KeyError in assemble_batch, or a
    reader demanding a column that isn't there.
  2. Prompt semantics in the generic dataset. BaseDataset now imports SlotSeg and
    walks segments / response_segments. It is the only place in dataset.py that
    knows what a model's target looks like.
  3. It doesn't stop here. Under FG_DAG the FG graph DataParser builds
    (data_parser.py:177) now differs between train and predict, and everything derived
    from the full feature list still advertises the answer as an input:
    create_fg_json(features) in export_util.py, and the feature_configs rewritten
    into the exported pipeline.config. RFC 0002 §5.1 has serving run FG from that
    published fg_json, so a serving host would be told to produce an item:answer
    input that no online request can supply — a third place the same subtraction has to
    be re-derived.

The response feature contributes nothing to the compiled plan. Of what a SlotSeg
derives (prompt/compile.py:319-336):

derived for a response slot source
width STATIC(num_levels) sid_space, never the feature (compile.py:93-94)
fill INLINE forced — PROJECTED is rejected at compile.py:284-288
group_type JAGGED_SEQUENCE forced — a scalar member is DEEP → PROJECTED → rejected
shift base_vocab_size applied unconditionally in assembler.py:_inline_tokens

The feature declaration's only remaining job is to satisfy "every feature_names entry
exists in feature_configs" — which then forces the filter that cancels the parse it
caused.

And the column is already declared twice. prompt_generative_qwen_mock.config has
both label_fields: "answer" (:31) and sequence_raw_feature { feature_name: "answer" }
(:42). DataParser.parse writes features first (data_parser.py:289-321, keys
answer.values / answer.lengths) and labels second (data_parser.py:221-250, the
same keys
), so the label pass already overwrites the feature pass. At train the
assembler is reading label-parsed tensors; the feature pass produces a dense
(total, value_dim) float array that is then silently replaced by flat int64.

Proposal: a response placeholder resolves to a data_config.label_fields column

compile_prompt(cfg, features, label_fields, ...) keeps resolving body placeholders
against features, and resolves response placeholders against label_fields, building
those SlotSegs from the constants in the table above. Everything else already exists:

  • DataParser(labels=... if mode != PREDICT else None) is the mode gate, in one place.
  • Labels already emit {name}.values / {name}.lengths for list<int> columns into
    the same output_data the assembler consumes, so the assembler needs no change
    (no key_lengthsper_row = lengths → reshape (-1, num_levels) → band-check).
  • _selected_input_names already excludes label_fields at predict.

Deleted: dataset.py:138-156 and its SlotSeg import; the duplicate feature_configs
block; _slot_width's answer_levels parameter; the response-is-PROJECTED check at
compile.py:284-288, i.e. RFC 0001 §5.3 rule 6, which becomes structurally impossible
rather than enforced. Added: one check that every response placeholder names a declared
label field.

Precedent in this branch's own history: b373e9e5 "[refactor] generative-rec LM: answer
is a data_config.label_field, not a feature"
made exactly this move in the pre-prompt
stack, for the same reason — it "lets the label be absent at inference without the
EmbeddingGroup requiring it". The prompt rewrite reverted it.

On a future text response: tokenize it with the extended tokenizer compile_prompt
already builds and persists (covered by vocab_hash) — not through FG. FG is the
feature extraction layer; routing a label through it means RFC 0001 §3.4's vocab_file
injection, i.e. a second tokenizer instance, configured by path, that can disagree with
the vocabulary lm_head generates into. Using the same in-process Tokenizer the
compiler extended makes that disagreement unrepresentable. The label route is not a
trade against text responses; it is the right shape for them too.

Related, same code: the answer-width check at compile.py:515-526 cannot fire — a
response slot's width is STATIC(num_levels) by construction, so the comparison is
tautological. The check that is missing is on the data: _inline_tokens only asserts
values.size % num_levels == 0 (assembler.py:110), so a label row carrying two items
passes, and logits_suffix_len — derived from the declared width — then opens a
supervised window shorter than what was emitted, dropping part of the loss silently.
With the answer as a label, the exact width is known at compile, so this becomes
values.size == num_levels.

if compiled_prompt is not None and mode == Mode.PREDICT:
prompt_feature_names = {
feature_name
for segment in compiled_prompt.prompt_plan.segments
if isinstance(segment, SlotSeg)
for feature_name in segment.feature_names
}
response_feature_names = {
feature_name
for segment in compiled_prompt.prompt_plan.response_segments
if isinstance(segment, SlotSeg)
for feature_name in segment.feature_names
}
response_only = response_feature_names - prompt_feature_names
parser_features = [f for f in features if f.name not in response_only]

self._data_parser = DataParser(
features=features,
features=parser_features,
labels=list(data_config.label_fields)
if self._mode != Mode.PREDICT
else None,
Expand Down Expand Up @@ -385,6 +416,16 @@ def _build_batch(self, input_data: Dict[str, pa.Array]) -> Batch:
else:
batch = self._data_parser.to_batch(output_data)

if self._prompt_assembler is not None:
batch.additional_infos.update(
{
k: torch.from_numpy(np.asarray(v))
for k, v in self._prompt_assembler.assemble_batch(
output_data
).items()
}
)

# Set checkpoint info on batch
batch.checkpoint_info = checkpoint_info
batch.data_timestamp = data_timestamp
Expand Down Expand Up @@ -762,6 +803,7 @@ def create_dataloader(
gl_cluster: Optional[Dict[str, Union[int, str]]] = None,
debug_level: int = 0,
checkpoint_state: Optional[Dict[str, Any]] = None,
compiled_prompt: Optional[CompiledPrompt] = None,
) -> DataLoader:
"""Build dataloader.

Expand All @@ -776,6 +818,8 @@ def create_dataloader(
debug_level > 0, will dump fg encoded data to debug_str
checkpoint_state (dict, optional): resume state, applied before the
eager ``iter()`` forks workers so it reaches them.
compiled_prompt (CompiledPrompt, optional): when set, each batch carries
the assembled prompt streams in ``additional_infos``.

Return:
dataloader (dataloader): a DataLoader.
Expand All @@ -790,6 +834,7 @@ def create_dataloader(
reserved_columns=reserved_columns,
mode=mode,
debug_level=debug_level,
compiled_prompt=compiled_prompt,
)
if checkpoint_state:
dataset.load_state_dict(dict(checkpoint_state))
Expand Down
98 changes: 86 additions & 12 deletions tzrec/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,17 @@
from tzrec.optim.ema import DenseEMA, EMAOptimizer
from tzrec.optim.lr_scheduler import BaseLR
from tzrec.optim.optimizer import TZRecOptimizer
from tzrec.prompt.compile import compile_prompt
from tzrec.prompt.persist import check_prompt_assets, copy_prompt_assets
from tzrec.prompt.plan import CompiledPrompt
from tzrec.protos import export_pb2
from tzrec.protos.data_pb2 import DataConfig, DatasetType
from tzrec.protos.eval_pb2 import EvalConfig
from tzrec.protos.export_pb2 import ExportConfig
from tzrec.protos.feature_pb2 import FeatureConfig
from tzrec.protos.model_pb2 import Kernel as KernelProto
from tzrec.protos.model_pb2 import ModelConfig
from tzrec.protos.pipeline_pb2 import EasyRecConfig
from tzrec.protos.train_pb2 import TrainConfig
from tzrec.utils import checkpoint_util, config_util, predict_util
from tzrec.utils.delta_embedding_dump import DeltaEmbeddingDumper
Expand Down Expand Up @@ -119,6 +124,17 @@ def _create_features(
return features


def _compile_prompt(
pipeline_config: EasyRecConfig, features: List[BaseFeature]
) -> Optional[CompiledPrompt]:
"""Compile prompt_config for entry points that build prompt-aware objects."""
if not pipeline_config.HasField("prompt_config"):
return None
return compile_prompt(
pipeline_config.prompt_config, features, model_dir=pipeline_config.model_dir
)


def _get_sampler_type(data_config: DataConfig) -> Optional[str]:
try:
sampler_type = (
Expand All @@ -137,6 +153,7 @@ def _create_model(
labels: List[str],
sample_weights: Optional[List[str]] = None,
sampler_type: Optional[str] = None,
compiled_prompt: Optional[CompiledPrompt] = None,
) -> BaseModel:
"""Build model.

Expand All @@ -146,6 +163,8 @@ def _create_model(
labels (list): list of label names.
sample_weights (list): list of sample weight names.
sampler_type (str): negative sampler type
compiled_prompt (CompiledPrompt, optional): forwarded to prompt-native models.

Return:
model: a EasyRec Model.
"""
Expand All @@ -159,6 +178,7 @@ def _create_model(
labels,
sample_weights=sample_weights,
sampler_type=sampler_type,
compiled_prompt=compiled_prompt,
)

kernel = Kernel[KernelProto.Name(model_config.kernel)]
Expand Down Expand Up @@ -693,6 +713,7 @@ def train_and_evaluate(
data_config = pipeline_config.data_config
# Build feature
features = _create_features(list(pipeline_config.feature_configs), data_config)
compiled_prompt = _compile_prompt(pipeline_config, features)

ckpt_manager = checkpoint_util.CheckpointManager(
pipeline_config.model_dir,
Expand Down Expand Up @@ -731,6 +752,8 @@ def train_and_evaluate(

# Restore dataloader state before create_dataloader starts its workers
dataloader_state: Optional[Dict[str, Any]] = None
if ckpt_path:
check_prompt_assets(compiled_prompt, ckpt_path)
if ckpt_path and continue_train:
dataloader_state = ckpt_manager.restore_dataloader_state(ckpt_path)
if dataloader_state and not restore_from_model_dir:
Expand All @@ -743,6 +766,7 @@ def train_and_evaluate(
features,
pipeline_config.train_input_path,
mode=Mode.TRAIN,
compiled_prompt=compiled_prompt,
checkpoint_state=dataloader_state,
)
eval_dataloader = None
Expand All @@ -754,6 +778,7 @@ def train_and_evaluate(
features,
pipeline_config.eval_input_path,
mode=Mode.EVAL,
compiled_prompt=compiled_prompt,
gl_cluster=gl_cluster,
)

Expand All @@ -766,7 +791,11 @@ def train_and_evaluate(
list(data_config.label_fields),
sample_weights=list(data_config.sample_weight_fields),
sampler_type=sampler_type,
compiled_prompt=compiled_prompt,
)
# Cold start only; a resumed or fine-tuned run gets its weights from DCP.
if ckpt_path is None:
model.init_from_pretrained()
model = TrainWrapper(
model, device=device, mixed_precision=train_config.mixed_precision
)
Expand Down Expand Up @@ -957,12 +986,14 @@ def evaluate(
data_config = pipeline_config.data_config
# Build feature
features = _create_features(list(pipeline_config.feature_configs), data_config)
compiled_prompt = _compile_prompt(pipeline_config, features)

eval_dataloader = create_dataloader(
data_config,
features,
eval_input_path or pipeline_config.eval_input_path,
mode=Mode.EVAL,
compiled_prompt=compiled_prompt,
)

sampler_type = _get_sampler_type(data_config)
Expand All @@ -974,6 +1005,7 @@ def evaluate(
list(data_config.label_fields),
sample_weights=list(data_config.sample_weight_fields),
sampler_type=sampler_type,
compiled_prompt=compiled_prompt,
)
model = TrainWrapper(
model, device=device, mixed_precision=train_config.mixed_precision
Expand Down Expand Up @@ -1005,6 +1037,7 @@ def evaluate(
)

if checkpoint_path:
check_prompt_assets(compiled_prompt, checkpoint_path)
ckpt_manager.restore(
checkpoint_path,
model,
Expand Down Expand Up @@ -1077,6 +1110,53 @@ def export(
if asset_files:
assets = asset_files.split(",")

ckpt_manager = checkpoint_util.CheckpointManager(
pipeline_config.model_dir, export_config=pipeline_config.export_config
)
if not checkpoint_path:
if (
pipeline_config.HasField("export_config")
and pipeline_config.export_config.exporter_type == "best"
):
checkpoint_path, _ = ckpt_manager.best_checkpoint()
else:
checkpoint_path, _ = ckpt_manager.latest_checkpoint()

# HF export converts the checkpoint dir directly -- no model build, no DCP restore.
if pipeline_config.export_config.export_format == export_pb2.ExportFormat.HF:

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.

The HF export path should still build and restore the full model. Direct DCP-to-HF conversion only exports the backbone and drops sparse embeddings and projection parameters, which must be included in the scripted model. Therefore, storing HF assets in every checkpoint is redundant; generate them from the restored model during export instead.

if config_util.use_dense_ema(
pipeline_config.export_config, pipeline_config.train_config
):
raise ValueError(
"HF export: dcp_to_hf reads <checkpoint>/model, so it cannot "
"serve Dense EMA parameters. Set export_config.use_dense_ema to "
"false to export the raw weights."
)
if not checkpoint_path:
raise ValueError("HF export: no checkpoint found to convert.")
if not os.path.exists(os.path.join(checkpoint_path, "config.json")):
raise ValueError(
f"HF export: {checkpoint_path} has no co-located HF assets; it "
f"was not written by an HF-backed model."
)
if assets:
logger.warning(f"HF export ignores asset_files: {assets}.")
if is_rank_zero:
from tzrec.utils.hf_export_util import dcp_to_hf

dcp_to_hf(checkpoint_path, export_dir)
# Carry the prompt contract saved alongside the checkpoint.
copy_prompt_assets(checkpoint_path, export_dir)

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.

Generate the actual serving contract during export. Export the resized HF model, extended tokenizer, scripted GenRec front-end, and embedded identity digests from the compiled pipeline. Do not copy prompt JSON files from a training checkpoint.

return

if pipeline_config.HasField("prompt_config"):
raise ValueError(
"a prompt-native model exports to a HuggingFace directory, not "
"TorchScript: its input is an assembled token stream the dataloader "
"builds, which an export-time dummy batch cannot supply. Set "
"export_config.export_format to HF."
)

data_config = pipeline_config.data_config

# Build feature
Expand All @@ -1096,18 +1176,6 @@ def export(
model.set_is_inference(True)
model = InferWrapper(model)

if not checkpoint_path:
ckpt_manager = checkpoint_util.CheckpointManager(
pipeline_config.model_dir, export_config=pipeline_config.export_config
)
if (
pipeline_config.HasField("export_config")
and pipeline_config.export_config.exporter_type == "best"
):
checkpoint_path, _ = ckpt_manager.best_checkpoint()
else:
checkpoint_path, _ = ckpt_manager.latest_checkpoint()

if isinstance(model.model, MatchModel):
for name, module in model.model.named_children():
if isinstance(module, MatchTower) or isinstance(module, MatchTowerWoEG):
Expand Down Expand Up @@ -1284,13 +1352,15 @@ def predict(
data_config.drop_remainder = False
# Build feature
features = _create_features(list(pipeline_config.feature_configs), data_config)
compiled_prompt = _compile_prompt(pipeline_config, features)

infer_dataloader = create_dataloader(
data_config,
features,
predict_input_path,
reserved_columns=reserved_cols,
mode=Mode.PREDICT,
compiled_prompt=compiled_prompt,
debug_level=debug_level,
)
infer_iterator = infer_dataloader.get_iterator() # pyre-ignore[16]
Expand Down Expand Up @@ -1557,6 +1627,7 @@ def predict_checkpoint(
data_config = pipeline_config.data_config
# Build feature
features = _create_features(list(pipeline_config.feature_configs), data_config)
compiled_prompt = _compile_prompt(pipeline_config, features)

# Build dataloader
predict_dataloader = create_dataloader(
Expand All @@ -1565,6 +1636,7 @@ def predict_checkpoint(
predict_input_path,
reserved_columns=reserved_cols,
mode=Mode.PREDICT,
compiled_prompt=compiled_prompt,
debug_level=debug_level,
)

Expand All @@ -1584,6 +1656,7 @@ def predict_checkpoint(
pipeline_config.model_config,
features,
[],
compiled_prompt=compiled_prompt,
)
model.set_is_inference(True)
model = PredictWrapper(
Expand Down Expand Up @@ -1619,6 +1692,7 @@ def predict_checkpoint(
model.eval()

if checkpoint_path:
check_prompt_assets(compiled_prompt, checkpoint_path)
ckpt_manager.restore(
checkpoint_path,
model,
Expand Down
Loading
Loading