Skip to content

[bugfix] run inline SID features through FG and export the LM at the export precision - #687

Merged
tiankongdeguiji merged 2 commits into
alibaba:masterfrom
tiankongdeguiji:fix/inline-sid-fg
Sep 24, 2026
Merged

tiankongdeguiji merged 2 commits into
alibaba:masterfrom
tiankongdeguiji:fix/inline-sid-fg

Conversation

@tiankongdeguiji

@tiankongdeguiji tiankongdeguiji commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

An inline SID history is a sequence_id_feature, or a grouped sequence_feature { id_feature }, with no embedding_dim and value_dim equal to the number of SID levels. It was added in #685 and worked only under fg_mode: FG_NONE, for two reasons that together left no valid config:

  • pyfg will not run a sequence id feature that has no bucketize config. It logs feature <hist__sid> no bucketize config, and the parse fails with sparse sequence feature internal error.
  • compile_prompt rejected any id space on an inline member.

The same failure hits an FG_NONE-trained model at serving time: the export always writes fg.json, and a processor running FG on it fails the same way.

Changes:

  • _check_inline_member accepts num_buckets on an inline SID member when it equals sum(sid_space.codebook), the offset-code space. Bucketizing over exactly that space maps every code to itself. Every other id space is still rejected: hash_bucket_size, any other num_buckets, vocab_*, zch, dynamicemb.
  • docs/source/feature/feature.md says that value is required whenever FG runs: training with fg_mode other than FG_NONE, or a processor running FG on the exported fg.json.
  • genrec_causal_lm_model_mock.config now runs under fg_mode: FG_DAG, and its hist__sid sets num_buckets: 12. To go with it:
    • create_mock_prompt_data writes raw FG inputs: hist__sid_codes, tags__tag_a_id, tags__tag_b_ids, tags__text_raw, and word text for the tokenize features.
    • The integration test builds the served front-end's request by running those rows through DataParser, which is what a processor hands the front-end.
  • Version: bump to 1.4.15.

Exported LM dtype

A second export bug, found while serving the same re-export. config.json had the wrong dtype in two places:

  • text_config carried the backbone's parameter dtype: FP32, which lm_parameter_dtype keeps so Adam's small updates don't underflow.
  • The top level had no dtype at all. transformers>=4.56 serializes the field as dtype, but the top-level copy looked for torch_dtype.

sglang takes the dtype from text_config and, under --dtype auto, turns a float32 config into fp16. So a BF16-trained LM was served in fp16 unless --dtype bfloat16 was passed by hand.

The fix:

  • main.export sets the backbone's dtype from export_config.mixed_precision when it is set. That field is the repo's existing statement of inference precision; mixed_precision_for_export deliberately doesn't fall back to train_config and warns when the two differ. On the scripted front-end path the field changes nothing else.
  • The top-level copy reads dtype.

The top level still copies only vocab_size, hidden_size, num_hidden_layers and dtype, for loaders that read only the outer config. Copying the whole backbone would clobber the composite's own architectures, model_type and eos_token_id/pad_token_id (for example, the backbone's eos 151643 vs the extended tokenizer's 151645), and would leave two copies of every field to drift apart.

Alternatives considered

  • Fill num_buckets from sid_space in load_pipeline_config, the way vocab_file is filled for tokenize features. Not taken: at load time the SID slot member cannot be told apart from other id features without an embedding, such as FG DAG stub features, and changing their bucketization would be wrong. A config usually has only one SID feature, so declaring the value there is simple and explicit.
  • Emit num_buckets from IdFeature's fg json. Not possible: a feature never sees prompt_config.sid_space.
  • Serve at train_config.mixed_precision. Not taken: export already takes its inference precision from export_config.mixed_precision alone, on purpose. The mock config now sets export_config { mixed_precision: "BF16" } to match its training.

Test Plan

  • compile_test.test_inline_id_feature_may_bucketize_over_the_code_space (FG_NORMAL and FG_DAG):

    • It checks that a grouped SID member with num_buckets: 12 compiles to an INLINE slot.
    • It parses list<list<int64>> offset codes through pyfg and checks that values, lengths and key lengths come out unchanged.
    • Without num_buckets, both cases fail with sparse sequence feature internal error.
    • test_inline_id_feature_may_not_declare_an_id_space still rejects any other num_buckets.
  • genrec_integration_test now trains, evaluates, exports and predicts end to end under FG_DAG. That covers:

    • FG tokenization of the title and tag texts;
    • the grouped SID codes passing through FG;
    • the scripted front-end matching the eager assembler on FG output.

    I checked by hand that the mock text tokenizes to word ids, not <unk>, and that the SID codes come out unchanged.

  • Exported dtype: genrec_integration_test asserts that config.json has dtype: bfloat16 at the top level and in text_config. With the fix reverted, it fails with KeyError: 'dtype'.

  • Test run in the torch 2.13 env covered tzrec.utils.config_util_test, tzrec.utils.hf_export_util_test, tzrec.prompt.{compile,assembler,hole_keys}_test, tzrec.models.genrec_model_test, tzrec.models.genrec_causal_lm_model_test and tzrec.tests.genrec_integration_test, including the GPU distributed-embedding export. Result: 114 tests OK, 5 skipped. All 5 skips need flash-attn, which has no torch 2.13 wheel.

  • Lint and types: pre-commit run and pyrefly check are clean.

  • Production-shaped data:

    • table: a grouped history table under FG_DAG, with up to 300 items per row, 3 SID levels of 256 codes, and ten per-item attribute sub-features;
    • config: num_buckets: 768;
    • result: one batch loaded, and every offset code came out unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_016SW7gfb1MeLZsu5RFrrQZV

pyfg refuses to run a sequence id feature without a bucketize config, and
compile_prompt rejected any id space on an inline SID member (a sequence id
feature without embedding_dim). Such a member therefore only loaded under
FG_NONE, and its exported fg.json failed in any processor running FG.

compile now accepts num_buckets on that member when it equals
sum(sid_space.codebook), the offset-code space, over which bucketizing is the
identity; any other id space is still rejected. The genrec mock config runs
under FG_DAG with that num_buckets, on raw FG inputs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SW7gfb1MeLZsu5RFrrQZV
@tiankongdeguiji tiankongdeguiji added the claude-review Let Claude Review label Sep 23, 2026
@github-actions github-actions Bot removed the claude-review Let Claude Review label Sep 23, 2026
Comment thread tzrec/prompt/compile.py
name
for name in ("hash_bucket_size", "num_buckets", "zch", "dynamicemb")
if member.config.HasField(name)
and not (name == "num_buckets" and member.config.num_buckets == num_codes)

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.

Low / optional follow-up: this makes a bucketize config permitted on an inline SID member but never required — a member with no bucketize config at all still compiles cleanly under FG_DAG/FG_NORMAL and only fails deep inside pyfg on the first parse (or as the missing fg_output[feat_name] in DataParser._parse_feature_fg_handler, data_parser.py:384), far from the config that caused it, while the doc bullet added in this PR says num_buckets is 必须 whenever FG runs.

member.fg_mode is available here (feature.py:419), and _validate already uses logger.warning for risks that can't be proven at compile time (compile.py:598, 609, 624), so a warning when an inline SID member carries no bucketize config would surface this at compile time. A warning rather than an error also covers the serving angle: an FG_NONE-trained model exports its fg.json without a bucketizer for this feature, and a processor running FG on it hits the same failure.

- **value_dim**: 默认值是0,可以设置1,value_dim=0时支持多值ID输出

- **embedding_dim** 可省略:省略时该特征不建 embedding 表,只能作为 prompt_config 中的 inline slot,此时 `value_dim` 需等于 `sid_space.codebook` 的层数,每个序列元素携带每层一个 offset SID 编码,且不能再声明 `num_buckets`/`hash_bucket_size`/`vocab_*`/`zch`/`dynamicemb`
- **embedding_dim** 可省略:省略时该特征不建 embedding 表,只能作为 prompt_config 中的 inline slot,此时 `value_dim` 需等于 `sid_space.codebook` 的层数,每个序列元素携带每层一个 offset SID 编码,且不能再声明 `hash_bucket_size`/`vocab_*`/`zch`/`dynamicemb`;`num_buckets` 只能等于 `sid_space.codebook` 之和(各层编码总数)。需要 FG 时(训练时 fg_mode 不是 FG_NONE,或线上 Processor 按导出的 fg.json 做 FG)必须这样配置,否则 FG 不处理该序列特征,这个取值下每个编码原样输出

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.

Low / optional: "这个取值下每个编码原样输出" holds exactly for codes in [0, sum(codebook)). The general num_buckets contract above (line 97: 取值不在[0, num_buckets)范围内时,分箱结果为0) clamps an out-of-range integer to 0 — and 0 is a valid level-0 code, so a history row written against a different/larger codebook silently becomes a real SID token in the LM instead of erroring (the assembler shifts inline values with no range check, assembler.py:252–256). The config side is fully closed by _check_inline_member, so this is purely a data-side edge, but a one-clause note that out-of-range codes follow FG's clamp-to-0 rule would make the bullet precise.

batch_size: 4
dataset_type: ParquetDataset
fg_mode: FG_NONE
fg_mode: FG_DAG

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.

Question, non-blocking: this is the only prompt-enabled integration config, so flipping the lane FG_NONE → FG_DAG drops end-to-end train/eval/export/predict coverage of the configuration that worked before this PR (inline SID with no num_buckets under FG_NONE); it remains covered only at compile level in compile_test.py. If that offline path from #685 is meant to stay guaranteed, a parameterized fg_mode on _prepare_config (or a second config) would keep both lanes — otherwise, was dropping it the intended CI-cost tradeoff?

@github-actions

Copy link
Copy Markdown
Contributor

Review summary — five-area static review

No blocking issues. The fix is correct and well-verified end to end. Invariants confirmed beyond the diff:

  • The exemption cannot hit a degenerate space: num_buckets is optional uint64 (so HasField + equality is safe), and _build_sid_space rejects empty/non-positive codebooks before _check_inline_member runs, so num_codes >= 1 always and num_buckets: 0 is still rejected.
  • No shadowing: IdFeature._fg_json emits num_buckets only when zch/dynamicemb/hash_bucket_size/vocab_* are all absent — exactly the set the comprehension still rejects — so an accepted config can never silently hash instead of identity-map.
  • Serving claim holds: export calls create_fg_json without remove_bucketizer, so the exported fg.json carries the bucketize config to a processor.
  • _request faithfully replaces the hand-built dict, member by member: dense score → .values only, multi-value grouped members keep .key_lengths, title/tags__tag_a get .values/.lengths — matching what the assembler, HoleKeyBuilder, and to_batch read.
  • Config ↔ mock-data mapping is complete: every expression in the mock config resolves to a column the regenerated create_mock_prompt_data writes under FG_DAG naming; generated codes are in range by construction and the rng stays locally seeded.
  • Version bump is complete: no 1.4.14 remains anywhere; tzrec/version.py is the single source.

Three low-severity, optional notes posted inline:

  1. compile.py — a bucketize config is now permitted but never required on an inline SID member; a compile-time logger.warning when one is absent would surface the failure near its cause instead of deep inside pyfg.
  2. feature.md — "每个编码原样输出" holds only for in-range codes; FG's general num_buckets contract clamps out-of-range integers to 0, which for inline SIDs becomes a valid LM token silently. Worth one clause in the doc bullet.
  3. Mock config — flipping the only prompt-enabled integration lane FG_NONE → FG_DAG drops end-to-end coverage of the previously-working FG_NONE configuration (now compile-level only); asked whether that tradeoff was intended.

Areas: code quality, performance (nothing noteworthy — the FG_NONE→FG_DAG switch is negligible at 8 rows), test coverage (accept/reject pair interacts correctly; conventions followed), documentation accuracy (clean), security (validation-bypass paths closed). All five reviews completed.

The exported config.json carried the backbone's parameter dtype (FP32, what
lm_parameter_dtype keeps for Adam) in text_config, and no top-level dtype at
all: transformers>=4.56 serializes the field as dtype, while the top-level copy
still looked for torch_dtype. An engine that reads text_config, as sglang does,
therefore ran a BF16-trained LM in fp16 under --dtype auto.

main.export now sets the backbone's dtype from export_config.mixed_precision
when it is set, and the top-level copy reads dtype.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SW7gfb1MeLZsu5RFrrQZV
@tiankongdeguiji tiankongdeguiji changed the title [bugfix] run inline SID sequence features through FG [bugfix] run inline SID features through FG and export the LM at the export precision Sep 23, 2026
@tiankongdeguiji
tiankongdeguiji merged commit 3d6f6b4 into alibaba:master Sep 24, 2026
13 checks passed
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