[bugfix] run inline SID features through FG and export the LM at the export precision - #687
Conversation
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
32e5d05 to
e12f299
Compare
| 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) |
There was a problem hiding this comment.
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 不处理该序列特征,这个取值下每个编码原样输出 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
Review summary — five-area static reviewNo blocking issues. The fix is correct and well-verified end to end. Invariants confirmed beyond the diff:
Three low-severity, optional notes posted inline:
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
2a21603 to
6ba8b99
Compare
Summary
An inline SID history is a
sequence_id_feature, or a groupedsequence_feature { id_feature }, with noembedding_dimandvalue_dimequal to the number of SID levels. It was added in #685 and worked only underfg_mode: FG_NONE, for two reasons that together left no valid config:feature <hist__sid> no bucketize config, and the parse fails withsparse sequence feature internal error.compile_promptrejected 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_memberacceptsnum_bucketson an inline SID member when it equalssum(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 othernum_buckets,vocab_*,zch,dynamicemb.docs/source/feature/feature.mdsays that value is required whenever FG runs: training withfg_modeother than FG_NONE, or a processor running FG on the exportedfg.json.genrec_causal_lm_model_mock.confignow runs underfg_mode: FG_DAG, and itshist__sidsetsnum_buckets: 12. To go with it:create_mock_prompt_datawrites raw FG inputs:hist__sid_codes,tags__tag_a_id,tags__tag_b_ids,tags__text_raw, and word text for the tokenize features.DataParser, which is what a processor hands the front-end.Exported LM dtype
A second export bug, found while serving the same re-export.
config.jsonhad the wrong dtype in two places:text_configcarried the backbone's parameter dtype: FP32, whichlm_parameter_dtypekeeps so Adam's small updates don't underflow.transformers>=4.56serializes the field asdtype, but the top-level copy looked fortorch_dtype.sglang takes the dtype from
text_configand, under--dtype auto, turns a float32 config into fp16. So a BF16-trained LM was served in fp16 unless--dtype bfloat16was passed by hand.The fix:
main.exportsets the backbone'sdtypefromexport_config.mixed_precisionwhen it is set. That field is the repo's existing statement of inference precision;mixed_precision_for_exportdeliberately doesn't fall back totrain_configand warns when the two differ. On the scripted front-end path the field changes nothing else.dtype.The top level still copies only
vocab_size,hidden_size,num_hidden_layersanddtype, for loaders that read only the outer config. Copying the whole backbone would clobber the composite's ownarchitectures,model_typeandeos_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
num_bucketsfromsid_spaceinload_pipeline_config, the wayvocab_fileis 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.num_bucketsfromIdFeature's fg json. Not possible: a feature never seesprompt_config.sid_space.train_config.mixed_precision. Not taken: export already takes its inference precision fromexport_config.mixed_precisionalone, on purpose. The mock config now setsexport_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):num_buckets: 12compiles to an INLINE slot.list<list<int64>>offset codes through pyfg and checks that values, lengths and key lengths come out unchanged.num_buckets, both cases fail withsparse sequence feature internal error.test_inline_id_feature_may_not_declare_an_id_spacestill rejects any othernum_buckets.genrec_integration_testnow trains, evaluates, exports and predicts end to end under FG_DAG. That covers: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_testasserts thatconfig.jsonhasdtype: bfloat16at the top level and intext_config. With the fix reverted, it fails withKeyError: '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_testandtzrec.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 runandpyrefly checkare clean.Production-shaped data:
num_buckets: 768;🤖 Generated with Claude Code
https://claude.ai/code/session_016SW7gfb1MeLZsu5RFrrQZV