Zilong Zheng commited on
Commit
5c8dfd6
·
1 Parent(s): e30263d

Update README

Browse files
Files changed (12) hide show
  1. LICENSE +1 -1
  2. NOTICE +0 -6
  3. README.md +4 -4
  4. README.zh-CN.md +0 -111
  5. assets/gs1_pareto_frontier.svg +53 -53
  6. assets/inference-latency.svg +1354 -0
  7. chat_template.jinja +52 -154
  8. compiler.py +85 -44
  9. config.py +0 -46
  10. modular_gs1.py +166 -8
  11. schema.py +1 -1
  12. util.py +0 -202
LICENSE CHANGED
@@ -187,7 +187,7 @@
187
  same "printed page" as the copyright notice for easier
188
  identification within third-party archives.
189
 
190
- Copyright [yyyy] [name of copyright owner]
191
 
192
  Licensed under the Apache License, Version 2.0 (the "License");
193
  you may not use this file except in compliance with the License.
 
187
  same "printed page" as the copyright notice for easier
188
  identification within third-party archives.
189
 
190
+ Copyright 2026 Beijing Institute for General Artificial Intelligence
191
 
192
  Licensed under the Apache License, Version 2.0 (the "License");
193
  you may not use this file except in compliance with the License.
NOTICE DELETED
@@ -1,6 +0,0 @@
1
- GS1
2
- Copyright 2026 BIGAI.
3
-
4
- GS1 uses Qwen3.5 components from Hugging Face Transformers under Apache-2.0.
5
- Third-party libraries retain their respective copyright and licenses.
6
- Model weights are distributed under the license stated in each model card.
 
 
 
 
 
 
 
README.md CHANGED
@@ -12,15 +12,13 @@ base_model:
12
  ---
13
  # GS1-0.8B
14
 
15
- [English](README.md) · [简体中文](README.zh-CN.md)
16
-
17
  ![GS1 — Fast, Faithful, Frontier](assets/banner.svg)
18
 
19
  > ***Fast, Faithful, Frontier System One Intelligence.***
20
 
21
  [Code](https://github.com/bigai-nlco/GS1) · [Model](https://huggingface.co/collections/bigai/gs1) · [Documentation](https://github.com/bigai-nlco/GS1/blob/main/docs/README.md) · [Playground](https://github.com/bigai-nlco/GS1/blob/main/playground/README.md)
22
 
23
- **GS1-0.8B** is an open-source decision model developed by **BIGAI** (Beijing Institute for General Artificial Intelligence). It computes native probability distributions over runtime-defined candidates for agent routing, verification, extraction and control.
24
 
25
  ## GS1 Highlights
26
 
@@ -216,6 +214,8 @@ print(result["usage"])
216
 
217
  The response contains `model`, `answers` and `usage`. Choice preserves candidate IDs and returns every candidate probability; Noul returns `p(true)`; Score returns the expected zero-based rubric level and its full probability distribution. Generated `output_tokens` is zero.
218
 
 
 
219
  ### GS1 Runtime
220
 
221
  The GS1 runtime adds shared-state reuse, compatible batching, HTTP serving and qualified CUDA Graph replay:
@@ -277,4 +277,4 @@ If you find our work helpful, feel free to give us a cite.
277
  institution={BIGAI},
278
  year={2026}
279
  }
280
- ```
 
12
  ---
13
  # GS1-0.8B
14
 
 
 
15
  ![GS1 — Fast, Faithful, Frontier](assets/banner.svg)
16
 
17
  > ***Fast, Faithful, Frontier System One Intelligence.***
18
 
19
  [Code](https://github.com/bigai-nlco/GS1) · [Model](https://huggingface.co/collections/bigai/gs1) · [Documentation](https://github.com/bigai-nlco/GS1/blob/main/docs/README.md) · [Playground](https://github.com/bigai-nlco/GS1/blob/main/playground/README.md)
20
 
21
+ **GS1** is a family of open-source decision models developed by **BIGAI** (Beijing Institute for General Artificial Intelligence). It computes native probability distributions over runtime-defined candidates for agent routing, verification, extraction and control.
22
 
23
  ## GS1 Highlights
24
 
 
214
 
215
  The response contains `model`, `answers` and `usage`. Choice preserves candidate IDs and returns every candidate probability; Noul returns `p(true)`; Score returns the expected zero-based rubric level and its full probability distribution. Generated `output_tokens` is zero.
216
 
217
+ `chat_template.jinja` renders the native GS1 state, question and options, including XML escaping, the trained user/assistant boundaries, the empty thinking prefix and `<|decide|>`. It is exported from the same serializer in `compiler.py`. `model.decide` prepares typed candidates, branch spans, masks, positions and budget checks. [Native template usage](https://github.com/bigai-nlco/GS1/blob/main/docs/inference.md#native-decision-template)
218
+
219
  ### GS1 Runtime
220
 
221
  The GS1 runtime adds shared-state reuse, compatible batching, HTTP serving and qualified CUDA Graph replay:
 
277
  institution={BIGAI},
278
  year={2026}
279
  }
280
+ ```
README.zh-CN.md DELETED
@@ -1,111 +0,0 @@
1
- # GS1-0.8B
2
-
3
- [English](README.md) · [简体中文](README.zh-CN.md)
4
-
5
- ![GS1](assets/banner.svg)
6
-
7
- **Fast, Faithful, Frontier.** GS1-0.8B 是原生类型化决策模型。输入状态、问题和运行时定义的候选含义,直接获得 **Choice、Noul、Score** 分布,生成输出 token 数为零。
8
-
9
- [代码与优化推理](https://github.com/bigai-nlco/GS1) · [模型合集](https://huggingface.co/collections/bigai/gs1) · [完整结果](https://github.com/bigai-nlco/GS1/blob/main/benchmark/results.md)
10
-
11
- | 指标 | GS1-0.8B |
12
- |---|---:|
13
- | 实际存储参数量 | 879,765,056 |
14
- | 37 项公开 Decision Index | **56.12** |
15
- | JevBench 公开任务准确率,231 个决策 | **77.92%** |
16
- | AG News 测试准确率,7,600 个决策 | **90.99%** |
17
- | ToolACE 选择准确率,640 个决策 | **93.28%** |
18
- | CUDA Graph 完整热决策 P50 / P95 | **12.51 / 13.22 ms** |
19
- | 上述延迟设置的峰值已分配显存 | **4.07 GiB** |
20
-
21
- Decision Index 来自公开 edition 0.3 的完整评测:115,479 个唯一请求、279,333 个决策、最终覆盖率 100%,采用 BF16 原生参考计算;该分数与私有 Full 分数分开。三项常规榜单于 2026 年 11 月 9 日完成,均无失败、无重试。JevBench 公开任务准确率不等同于密封榜单综合分;ToolACE 是固定哈希子集的首个工具/不调用工具选择,不评估参数生成。
22
-
23
- 延迟于 2026 年 11 月 9 日测量:报告 48 GiB 的 RTX 4090、BF16 计算、最长分支 256 token、单问题、单 GPU,预热五次、测量 30 次。进程内完整计时包含校验、编译、模型计算和概率,排除加载、图捕获、HTTP 与 SDK 传输。[决策指标](assets/results/public-evaluation.json) · [原始延迟样本](assets/results/inference-latency.json) · [测量方法和全部型号](https://github.com/bigai-nlco/GS1/blob/main/benchmark/latency.md)
24
-
25
- ## Hugging Face 默认推理
26
-
27
- **不需要安装或导入 GS1 仓库。** 使用 Python 3.11+,安装本目录的依赖:
28
-
29
- ```bash
30
- python -m pip install -r requirements.txt
31
- ```
32
-
33
- ```python
34
- from transformers import AutoModel
35
-
36
- model = AutoModel.from_pretrained(
37
- "bigai/GS1-0.8B", # a local GS1-0.8B directory also works
38
- trust_remote_code=True,
39
- ).eval().to("cuda")
40
-
41
- result = model.decide(
42
- state="A customer was charged twice and asks for a refund.",
43
- questions={
44
- "team": {
45
- "type": "choice",
46
- "instructions": "Choose the team that should handle this ticket.",
47
- "criteria": {"billing": "Payments and refunds", "technical": "Software problems"},
48
- },
49
- "refund": {"type": "noul", "instructions": "Does the customer request a refund?"},
50
- "urgency": {
51
- "type": "score",
52
- "instructions": "Rate the urgency of handling this ticket.",
53
- "criteria": ["Routine", "Soon", "Immediate"],
54
- },
55
- },
56
- )
57
- print(result["answers"])
58
- ```
59
-
60
- 上传前也可以将模型 ID 替换为本地完整目录。`trust_remote_code=True` 加载本目录提供的模型代码。默认保留原始混合存储精度,复现测量时不要整体转换权重 dtype。兼容的 CPU PyTorch 环境可使用 `.to("cpu")` 和 `decide(..., precision="fp32")`。默认 eager 路径可选安装上游 CUDA 算子;实测环境使用 `flash-linear-attention[cuda]==0.5.2` 和 `causal-conv1d==1.7.0`。
61
-
62
- 返回值包含 `model`、`answers` 和 `usage`。Choice 返回候选 ID、所有候选概率及 confidence;Noul 返回 `p(true)`;Score 返回从零开始的量表等级期望及各等级概率。输出 token 用量为零。默认实现逐问题执行完整分支,不依赖运行时资格文件或图捕获。
63
-
64
- ## GS1 优化推理
65
-
66
- GS1 仓库为相同权重提供源码绑定的 CUDA Graph、兼容批处理、状态共享、HTTP 服务和 playground。
67
-
68
- ```bash
69
- git clone https://github.com/bigai-nlco/GS1.git
70
- cd GS1
71
- uv sync --locked --extra serve --extra cuda
72
- uv run gs1 serve --model bigai/GS1-0.8B --model-name GS1
73
- ```
74
-
75
- 在源码目录中额外安装 `--extra eval`,再针对本机模型、实现、硬件和请求形状执行图资格验证:
76
-
77
- ```bash
78
- uv run python -m benchmark.inference --model bigai/GS1-0.8B \
79
- --out runs/runtime.json --runtime-profile-out runs/profile.json \
80
- --graph-cache-mb 16384 --graph-cache-entries 128
81
-
82
- uv run gs1 serve --model bigai/GS1-0.8B --model-name GS1 \
83
- --runtime-profile runs/profile.json --experimental-cuda-graphs \
84
- --graph-warmup-requests runs/profile-warmup.json \
85
- --graph-cache-mb 4096 --graph-cache-entries 128
86
- ```
87
-
88
- benchmark 同时生成 `profile-warmup.json`。服务启动时预热已知图;图未命中回退原生 eager,未验证形状回退单分支参考计算。代码、模型、精度或硬件改变后需重新验证;资格文件留在本地,不作为权重上传。[完整推理指南](https://github.com/bigai-nlco/GS1/blob/main/docs/inference.zh-CN.md)
89
-
90
- ## 架构与数值策略
91
-
92
- ![架构](assets/architecture.svg)
93
-
94
- GS1-0.8B 的隐藏宽度为 1,024,包含 24 个解码层��� 4 个决策注意力头。架构结合 Qwen3.5 混合 GDN/注意力骨干、分段双向全注意力、保持残差累加的 Attention Residuals,以及两层候选路由、256 维最终比较空间的原生交叉注意力决策头。不包含语言生成输出头,候选含义在推理时提供,无固定任务分类词表。
95
-
96
- 文本和决策头持久权重为 FP32,保留的视觉塔为 BF16;配置指定决策头采用 BF16 计算,最终 logits 和概率归约使用 FP32。完整分支上限为 65,536 token,同时受准入和显存预算约束;默认 HTTP 服务使用更紧的 4,096-token 上限。公开决策结果均为纯文本评测。`config.json` 保存原生定义、token ID 和 `inference_limits`;safetensors 参数键和 dtype 均完整保留。
97
-
98
- ## 适用范围与许可
99
-
100
- 适合路由、分类、工具选择、有限选项判断等类型化决策。概率不保证正确,校准效果依赖任务分布。公开结果也完整展示限制:HLE 准确率较低,未启用置信拒答时较大模型的 AA-Omniscience 非幻觉率为零;详见[完整组件结果](assets/results/public-evaluation.json)。
101
-
102
- 代码与发布权重采用 [Apache-2.0](LICENSE),[上游署名](NOTICE) 保留。本目录包含模型架构和推理依赖;评测输入与本地运行时资格文件另行保存。
103
-
104
- ```bibtex
105
- @techreport{zheng2026gs1,
106
- title={GS1: Fast, Faithful, Frontier System One Intelligence},
107
- author={Zheng, Zilong and Liu, Yang and Qin, Shuhan and Lin, Ziyong and Li, Jiaqi and Jia, Zixia and Zhu, Song-Chun},
108
- institution={BIGAI},
109
- year={2026}
110
- }
111
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/gs1_pareto_frontier.svg CHANGED
assets/inference-latency.svg ADDED
chat_template.jinja CHANGED
@@ -1,154 +1,52 @@
1
- {%- set image_count = namespace(value=0) %}
2
- {%- set video_count = namespace(value=0) %}
3
- {%- macro render_content(content, do_vision_count, is_system_content=false) %}
4
- {%- if content is string %}
5
- {{- content }}
6
- {%- elif content is iterable and content is not mapping %}
7
- {%- for item in content %}
8
- {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
9
- {%- if is_system_content %}
10
- {{- raise_exception('System message cannot contain images.') }}
11
- {%- endif %}
12
- {%- if do_vision_count %}
13
- {%- set image_count.value = image_count.value + 1 %}
14
- {%- endif %}
15
- {%- if add_vision_id %}
16
- {{- 'Picture ' ~ image_count.value ~ ': ' }}
17
- {%- endif %}
18
- {{- '<|vision_start|><|image_pad|><|vision_end|>' }}
19
- {%- elif 'video' in item or item.type == 'video' %}
20
- {%- if is_system_content %}
21
- {{- raise_exception('System message cannot contain videos.') }}
22
- {%- endif %}
23
- {%- if do_vision_count %}
24
- {%- set video_count.value = video_count.value + 1 %}
25
- {%- endif %}
26
- {%- if add_vision_id %}
27
- {{- 'Video ' ~ video_count.value ~ ': ' }}
28
- {%- endif %}
29
- {{- '<|vision_start|><|video_pad|><|vision_end|>' }}
30
- {%- elif 'text' in item %}
31
- {{- item.text }}
32
- {%- else %}
33
- {{- raise_exception('Unexpected item type in content.') }}
34
- {%- endif %}
35
- {%- endfor %}
36
- {%- elif content is none or content is undefined %}
37
- {{- '' }}
38
- {%- else %}
39
- {{- raise_exception('Unexpected content type.') }}
40
- {%- endif %}
41
- {%- endmacro %}
42
- {%- if not messages %}
43
- {{- raise_exception('No messages provided.') }}
44
- {%- endif %}
45
- {%- if tools and tools is iterable and tools is not mapping %}
46
- {{- '<|im_start|>system\n' }}
47
- {{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
48
- {%- for tool in tools %}
49
- {{- "\n" }}
50
- {{- tool | tojson }}
51
- {%- endfor %}
52
- {{- "\n</tools>" }}
53
- {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
54
- {%- if messages[0].role == 'system' %}
55
- {%- set content = render_content(messages[0].content, false, true)|trim %}
56
- {%- if content %}
57
- {{- '\n\n' + content }}
58
- {%- endif %}
59
- {%- endif %}
60
- {{- '<|im_end|>\n' }}
61
- {%- else %}
62
- {%- if messages[0].role == 'system' %}
63
- {%- set content = render_content(messages[0].content, false, true)|trim %}
64
- {{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
65
- {%- endif %}
66
- {%- endif %}
67
- {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
68
- {%- for message in messages[::-1] %}
69
- {%- set index = (messages|length - 1) - loop.index0 %}
70
- {%- if ns.multi_step_tool and message.role == "user" %}
71
- {%- set content = render_content(message.content, false)|trim %}
72
- {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
73
- {%- set ns.multi_step_tool = false %}
74
- {%- set ns.last_query_index = index %}
75
- {%- endif %}
76
- {%- endif %}
77
- {%- endfor %}
78
- {%- if ns.multi_step_tool %}
79
- {{- raise_exception('No user query found in messages.') }}
80
- {%- endif %}
81
- {%- for message in messages %}
82
- {%- set content = render_content(message.content, true)|trim %}
83
- {%- if message.role == "system" %}
84
- {%- if not loop.first %}
85
- {{- raise_exception('System message must be at the beginning.') }}
86
- {%- endif %}
87
- {%- elif message.role == "user" %}
88
- {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
89
- {%- elif message.role == "assistant" %}
90
- {%- set reasoning_content = '' %}
91
- {%- if message.reasoning_content is string %}
92
- {%- set reasoning_content = message.reasoning_content %}
93
- {%- else %}
94
- {%- if '</think>' in content %}
95
- {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
96
- {%- set content = content.split('</think>')[-1].lstrip('\n') %}
97
- {%- endif %}
98
- {%- endif %}
99
- {%- set reasoning_content = reasoning_content|trim %}
100
- {%- if loop.index0 > ns.last_query_index %}
101
- {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
102
- {%- else %}
103
- {{- '<|im_start|>' + message.role + '\n' + content }}
104
- {%- endif %}
105
- {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
106
- {%- for tool_call in message.tool_calls %}
107
- {%- if tool_call.function is defined %}
108
- {%- set tool_call = tool_call.function %}
109
- {%- endif %}
110
- {%- if loop.first %}
111
- {%- if content|trim %}
112
- {{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
113
- {%- else %}
114
- {{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
115
- {%- endif %}
116
- {%- else %}
117
- {{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
118
- {%- endif %}
119
- {%- if tool_call.arguments is defined %}
120
- {%- for args_name, args_value in tool_call.arguments|items %}
121
- {{- '<parameter=' + args_name + '>\n' }}
122
- {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
123
- {{- args_value }}
124
- {{- '\n</parameter>\n' }}
125
- {%- endfor %}
126
- {%- endif %}
127
- {{- '</function>\n</tool_call>' }}
128
- {%- endfor %}
129
- {%- endif %}
130
- {{- '<|im_end|>\n' }}
131
- {%- elif message.role == "tool" %}
132
- {%- if loop.previtem and loop.previtem.role != "tool" %}
133
- {{- '<|im_start|>user' }}
134
- {%- endif %}
135
- {{- '\n<tool_response>\n' }}
136
- {{- content }}
137
- {{- '\n</tool_response>' }}
138
- {%- if not loop.last and loop.nextitem.role != "tool" %}
139
- {{- '<|im_end|>\n' }}
140
- {%- elif loop.last %}
141
- {{- '<|im_end|>\n' }}
142
- {%- endif %}
143
- {%- else %}
144
- {{- raise_exception('Unexpected message role.') }}
145
- {%- endif %}
146
- {%- endfor %}
147
- {%- if add_generation_prompt %}
148
- {{- '<|im_start|>assistant\n' }}
149
- {%- if enable_thinking is defined and enable_thinking is true %}
150
- {{- '<think>\n' }}
151
- {%- else %}
152
- {{- '<think>\n\n</think>\n\n' }}
153
- {%- endif %}
154
- {%- endif %}
 
1
+ {# GS1 typed decision prompt. question/options are normalized by Compiler. #}
2
+ {%- macro xml_text(value) -%}
3
+ {%- set text = value if value is string else value | tojson(ensure_ascii=false, separators=(',', ':'), sort_keys=true) -%}
4
+ {{- text | replace('&', '&amp;') | replace('<', '&lt;') | replace('>', '&gt;') -}}
5
+ {%- endmacro -%}
6
+ {%- if messages | length != 1 or messages[0]['role'] != 'user' -%}
7
+ {{- raise_exception('GS1 requires exactly one user message containing the state.') -}}
8
+ {%- endif -%}
9
+ {%- if question is undefined or options is undefined -%}
10
+ {{- raise_exception('GS1 requires question and options; use model.decide for typed decisions.') -}}
11
+ {%- endif -%}
12
+ {%- if options is string or options is mapping or options is not sequence or not options -%}
13
+ {{- raise_exception('GS1 options must be a nonempty sequence of candidate descriptions.') -}}
14
+ {%- endif -%}
15
+ {%- if tools or enable_thinking -%}
16
+ {{- raise_exception('GS1 uses native decisions with the no-thinking prefix.') -}}
17
+ {%- endif -%}
18
+ {%- set state = messages[0]['content'] -%}
19
+ {%- set visual = namespace(enabled=false) -%}
20
+ {%- if state is sequence and state is not string and state is not mapping -%}
21
+ {%- for item in state -%}
22
+ {%- if item is mapping and item['type'] == 'image' -%}
23
+ {%- set visual.enabled = true -%}
24
+ {%- endif -%}
25
+ {%- endfor -%}
26
+ {%- endif -%}
27
+ {{- '<|im_start|>user\n<state>\n' -}}
28
+ {%- if visual.enabled -%}
29
+ {%- for item in state -%}
30
+ {%- if item is mapping and item['type'] == 'text' and item['text'] is string -%}
31
+ {{- xml_text(item['text']) -}}
32
+ {%- elif item is mapping and item | length == 2 and item['type'] == 'image' and item['image'] is string and item['image'].startswith('data:image/') and ';base64,' in item['image'] -%}
33
+ {{- '<|vision_start|><|image_pad|><|vision_end|>' -}}
34
+ {%- else -%}
35
+ {{- raise_exception('Vision state requires text and inline base64 images.') -}}
36
+ {%- endif -%}
37
+ {%- endfor -%}
38
+ {%- else -%}
39
+ {{- xml_text(state) -}}
40
+ {%- endif -%}
41
+ {{- '\n</state>\n<question>\n' -}}
42
+ {{- xml_text(question) -}}
43
+ {{- '\n</question>' -}}
44
+ {%- for option in options -%}
45
+ {{- '\n<option>\n' -}}
46
+ {{- xml_text(option) -}}
47
+ {{- '\n</option>' -}}
48
+ {%- endfor -%}
49
+ {{- '<|im_end|>\n' -}}
50
+ {%- if add_generation_prompt -%}
51
+ {{- '<|im_start|>assistant\n<think>\n\n</think>\n\n<|decide|>' -}}
52
+ {%- endif -%}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
compiler.py CHANGED
@@ -1,14 +1,14 @@
1
  """Compile typed decisions into XML prompts and execute native decision branches."""
2
 
 
3
  from dataclasses import dataclass, field
4
- from html import escape
5
  from pathlib import Path
6
 
7
  import torch
 
8
 
9
- from .config import INFERENCE_LIMITS, DecisionConfig
10
  from .schema import DecisionRequest, candidate_ids, noul_descriptions
11
- from .util import canonical
12
 
13
  XML_STATE, XML_STATE_END = "<state>", "</state>"
14
  XML_QUESTION, XML_QUESTION_END = "<question>", "</question>"
@@ -19,6 +19,71 @@ NO_THINKING_PREFIX = "<think>\n\n</think>\n\n"
19
  COMPILER = "xml_decision"
20
  POSITIONS = "branch_local"
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  def structural_tokens():
24
  return [*XML_TAGS, DECISION_TOKEN]
@@ -94,6 +159,8 @@ class Compiler:
94
  processor=None,
95
  ):
96
  self.tokenizer = tokenizer
 
 
97
  self.decision = DecisionConfig.model_validate(decision or {})
98
  self.max_context = max_context
99
  self.max_questions = max_questions
@@ -111,10 +178,6 @@ class Compiler:
111
  self.xml_newline = tuple(tokenizer.encode("\n", add_special_tokens=False))
112
  self.no_thinking_ids = no_thinking_token_ids(tokenizer)
113
 
114
- @staticmethod
115
- def _xml_text(value):
116
- return escape(value if isinstance(value, str) else canonical(value), quote=False)
117
-
118
  @staticmethod
119
  def _visual_state(state):
120
  return isinstance(state, list) and any(
@@ -178,29 +241,10 @@ class Compiler:
178
  self.processor.tokenizer = self.tokenizer
179
  return self.processor
180
 
181
- def _contents(self, state):
182
- if not self._visual_state(state):
183
- return f"{XML_STATE}\n{self._xml_text(state)}\n{XML_STATE_END}"
184
- contents = [{"type": "text", "text": f"{XML_STATE}\n"}]
185
- for item in state:
186
- if item.get("type") == "text" and isinstance(item.get("text"), str):
187
- contents.append({"type": "text", "text": self._xml_text(item["text"])})
188
- elif (
189
- set(item) == {"type", "image"}
190
- and isinstance(item.get("image"), str)
191
- and item["image"].startswith("data:image/")
192
- and ";base64," in item["image"]
193
- ):
194
- contents.append(item)
195
- else:
196
- raise ValueError("Vision state requires text and inline base64 images")
197
- return [*contents, {"type": "text", "text": f"\n{XML_STATE_END}"}]
198
-
199
  def compile(self, request: DecisionRequest):
200
  if len(request.questions) > self.max_questions:
201
  raise ValueError(f"questions exceeds {self.max_questions}")
202
  visual = self._visual_state(request.state)
203
- contents = self._contents(request.state)
204
  template = self._processor() if visual else self.tokenizer
205
  branches, candidates, state = [], {}, None
206
  for qid, question in request.questions.items():
@@ -212,17 +256,18 @@ class Compiler:
212
  options = list(noul_descriptions(question))
213
  else:
214
  options = list(question.criteria)
215
- prompt = (
216
- f"\n{XML_QUESTION}\n{self._xml_text(question.instructions)}\n{XML_QUESTION_END}"
217
- + "".join(f"\n{XML_OPTION}\n{self._xml_text(o)}\n{XML_OPTION_END}" for o in options)
218
- )
219
- content = [*contents, {"type": "text", "text": prompt}] if visual else contents + prompt
220
- rendered = template.apply_chat_template(
221
- [{"role": "user", "content": content}],
222
- add_generation_prompt=True,
223
- enable_thinking=False,
224
- tokenize=False,
225
- )
 
226
  payload = None
227
  if visual:
228
  encoded = template(
@@ -233,9 +278,9 @@ class Compiler:
233
  token_ids = encoded["input_ids"][0].tolist()
234
  else:
235
  token_ids = self.tokenizer.encode(rendered, add_special_tokens=False)
236
- if tuple(token_ids[-len(self.no_thinking_ids) :]) != self.no_thinking_ids:
237
- raise ValueError("Chat template changed the no-thinking assistant prefix")
238
- token_ids.append(self.controls[DECISION_TOKEN])
239
  markers = self._xml_positions(token_ids, len(ids))
240
  prefix = markers.pop("prefix_end")
241
  first_option = markers.pop("first_option")
@@ -253,10 +298,6 @@ class Compiler:
253
  in {"input_ids", "attention_mask", "pixel_values", "image_grid_thw", "mm_token_type_ids"}
254
  }
255
  payload["input_ids"] = torch.tensor([token_ids], dtype=torch.long)
256
- for key, fill in (("attention_mask", 1), ("mm_token_type_ids", 0)):
257
- if key in payload:
258
- value = payload[key]
259
- payload[key] = torch.cat((value, torch.full((1, 1), fill, dtype=value.dtype)), dim=-1)
260
  branches.append(
261
  Branch(
262
  qid,
 
1
  """Compile typed decisions into XML prompts and execute native decision branches."""
2
 
3
+ import json
4
  from dataclasses import dataclass, field
 
5
  from pathlib import Path
6
 
7
  import torch
8
+ from jinja2 import TemplateError
9
 
10
+ from .modular_gs1 import INFERENCE_LIMITS, DecisionConfig
11
  from .schema import DecisionRequest, candidate_ids, noul_descriptions
 
12
 
13
  XML_STATE, XML_STATE_END = "<state>", "</state>"
14
  XML_QUESTION, XML_QUESTION_END = "<question>", "</question>"
 
19
  COMPILER = "xml_decision"
20
  POSITIONS = "branch_local"
21
 
22
+ CHAT_TEMPLATE = r"""{# GS1 typed decision prompt. question/options are normalized by Compiler. #}
23
+ {%- macro xml_text(value) -%}
24
+ {%- set text = value if value is string else value | tojson(ensure_ascii=false, separators=(',', ':'), sort_keys=true) -%}
25
+ {{- text | replace('&', '&amp;') | replace('<', '&lt;') | replace('>', '&gt;') -}}
26
+ {%- endmacro -%}
27
+ {%- if messages | length != 1 or messages[0]['role'] != 'user' -%}
28
+ {{- raise_exception('GS1 requires exactly one user message containing the state.') -}}
29
+ {%- endif -%}
30
+ {%- if question is undefined or options is undefined -%}
31
+ {{- raise_exception('GS1 requires question and options; use model.decide for typed decisions.') -}}
32
+ {%- endif -%}
33
+ {%- if options is string or options is mapping or options is not sequence or not options -%}
34
+ {{- raise_exception('GS1 options must be a nonempty sequence of candidate descriptions.') -}}
35
+ {%- endif -%}
36
+ {%- if tools or enable_thinking -%}
37
+ {{- raise_exception('GS1 uses native decisions with the no-thinking prefix.') -}}
38
+ {%- endif -%}
39
+ {%- set state = messages[0]['content'] -%}
40
+ {%- set visual = namespace(enabled=false) -%}
41
+ {%- if state is sequence and state is not string and state is not mapping -%}
42
+ {%- for item in state -%}
43
+ {%- if item is mapping and item['type'] == 'image' -%}
44
+ {%- set visual.enabled = true -%}
45
+ {%- endif -%}
46
+ {%- endfor -%}
47
+ {%- endif -%}
48
+ {{- '<|im_start|>user\n<state>\n' -}}
49
+ {%- if visual.enabled -%}
50
+ {%- for item in state -%}
51
+ {%- if item is mapping and item['type'] == 'text' and item['text'] is string -%}
52
+ {{- xml_text(item['text']) -}}
53
+ {%- elif item is mapping and item | length == 2 and item['type'] == 'image' and item['image'] is string and item['image'].startswith('data:image/') and ';base64,' in item['image'] -%}
54
+ {{- '<|vision_start|><|image_pad|><|vision_end|>' -}}
55
+ {%- else -%}
56
+ {{- raise_exception('Vision state requires text and inline base64 images.') -}}
57
+ {%- endif -%}
58
+ {%- endfor -%}
59
+ {%- else -%}
60
+ {{- xml_text(state) -}}
61
+ {%- endif -%}
62
+ {{- '\n</state>\n<question>\n' -}}
63
+ {{- xml_text(question) -}}
64
+ {{- '\n</question>' -}}
65
+ {%- for option in options -%}
66
+ {{- '\n<option>\n' -}}
67
+ {{- xml_text(option) -}}
68
+ {{- '\n</option>' -}}
69
+ {%- endfor -%}
70
+ {{- '<|im_end|>\n' -}}
71
+ {%- if add_generation_prompt -%}
72
+ {{- '<|im_start|>assistant\n<think>\n\n</think>\n\n<|decide|>' -}}
73
+ {%- endif -%}
74
+ """
75
+
76
+
77
+ def canonical(value) -> str:
78
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True, allow_nan=False)
79
+
80
+
81
+ def save_chat_template(directory):
82
+ """Keep the standalone HF asset identical to the runtime serializer."""
83
+ directory = Path(directory)
84
+ directory.mkdir(parents=True, exist_ok=True)
85
+ (directory / "chat_template.jinja").write_text(CHAT_TEMPLATE, encoding="utf-8")
86
+
87
 
88
  def structural_tokens():
89
  return [*XML_TAGS, DECISION_TOKEN]
 
159
  processor=None,
160
  ):
161
  self.tokenizer = tokenizer
162
+ # Tokenizer exports and direct rendering use the same native GS1 format.
163
+ self.tokenizer.chat_template = CHAT_TEMPLATE
164
  self.decision = DecisionConfig.model_validate(decision or {})
165
  self.max_context = max_context
166
  self.max_questions = max_questions
 
178
  self.xml_newline = tuple(tokenizer.encode("\n", add_special_tokens=False))
179
  self.no_thinking_ids = no_thinking_token_ids(tokenizer)
180
 
 
 
 
 
181
  @staticmethod
182
  def _visual_state(state):
183
  return isinstance(state, list) and any(
 
241
  self.processor.tokenizer = self.tokenizer
242
  return self.processor
243
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  def compile(self, request: DecisionRequest):
245
  if len(request.questions) > self.max_questions:
246
  raise ValueError(f"questions exceeds {self.max_questions}")
247
  visual = self._visual_state(request.state)
 
248
  template = self._processor() if visual else self.tokenizer
249
  branches, candidates, state = [], {}, None
250
  for qid, question in request.questions.items():
 
256
  options = list(noul_descriptions(question))
257
  else:
258
  options = list(question.criteria)
259
+ try:
260
+ rendered = template.apply_chat_template(
261
+ [{"role": "user", "content": request.state}],
262
+ chat_template=CHAT_TEMPLATE,
263
+ question=question.instructions,
264
+ options=options,
265
+ add_generation_prompt=True,
266
+ enable_thinking=False,
267
+ tokenize=False,
268
+ )
269
+ except TemplateError as error:
270
+ raise ValueError(str(error)) from error
271
  payload = None
272
  if visual:
273
  encoded = template(
 
278
  token_ids = encoded["input_ids"][0].tolist()
279
  else:
280
  token_ids = self.tokenizer.encode(rendered, add_special_tokens=False)
281
+ terminal = (*self.no_thinking_ids, self.controls[DECISION_TOKEN])
282
+ if tuple(token_ids[-len(terminal) :]) != terminal:
283
+ raise ValueError("Chat template changed the no-thinking decision prefix")
284
  markers = self._xml_positions(token_ids, len(ids))
285
  prefix = markers.pop("prefix_end")
286
  first_option = markers.pop("first_option")
 
298
  in {"input_ids", "attention_mask", "pixel_values", "image_grid_thw", "mm_token_type_ids"}
299
  }
300
  payload["input_ids"] = torch.tensor([token_ids], dtype=torch.long)
 
 
 
 
301
  branches.append(
302
  Branch(
303
  qid,
config.py DELETED
@@ -1,46 +0,0 @@
1
- """Semantic architecture and scalar calibration settings for GS1."""
2
-
3
- import math
4
- from typing import Literal
5
-
6
- from pydantic import BaseModel, ConfigDict, Field, field_validator
7
-
8
- INFERENCE_LIMITS = dict(
9
- max_context=65536,
10
- max_questions=512,
11
- max_request_tokens=131072,
12
- max_branches=5120,
13
- batch_tokens=16384,
14
- max_attention_bytes=1073741824,
15
- max_logit_elements=16777216,
16
- )
17
-
18
-
19
- class ModelConfig(BaseModel):
20
- model_config = ConfigDict(extra="forbid", strict=True, allow_inf_nan=False)
21
-
22
-
23
- class DecisionConfig(ModelConfig):
24
- attnres: bool = True
25
- attnres_layers: list[int] | None = None
26
- attnres_eps: float = Field(default=1e-6, gt=0)
27
- bidirectional: bool = True
28
- bidirectional_layers: list[int] | None = None
29
- cross_attention_heads: int = Field(default=8, ge=1)
30
- cross_attention_layers: int = Field(default=2, ge=1)
31
- projection_size: int = Field(default=256, ge=2)
32
- norm_eps: float = Field(default=1e-5, gt=0)
33
-
34
- @field_validator("attnres_layers", "bidirectional_layers")
35
- @classmethod
36
- def selected_layers(cls, value):
37
- if value is not None:
38
- if not value or any(i < 0 for i in value) or len(set(value)) != len(value):
39
- raise ValueError("Layer selection must contain distinct nonnegative indices")
40
- return sorted(value)
41
- return value
42
-
43
-
44
- class CalibrationConfig(ModelConfig):
45
- mode: Literal["identity", "scalar_temperature"] = "identity"
46
- log_temperature_bound: float = Field(default=math.log(20), gt=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modular_gs1.py CHANGED
@@ -1,9 +1,16 @@
1
  """GS1: native hybrid text/vision decision model with attention residuals."""
2
 
 
 
3
  import math
4
  from dataclasses import dataclass
 
 
5
 
6
  import torch
 
 
 
7
  from torch import nn
8
  from torch.nn import functional as F
9
  from torch.nn.attention import SDPBackend, sdpa_kernel
@@ -22,8 +29,76 @@ from transformers.models.qwen3_5.modeling_qwen3_5 import (
22
  )
23
  from transformers.utils import ModelOutput
24
 
25
- from .config import INFERENCE_LIMITS, CalibrationConfig, DecisionConfig
26
- from .util import named_seed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
 
29
  class GS1Config(Qwen3_5Config):
@@ -499,7 +574,7 @@ class GS1PretrainedModel(Qwen3_5PreTrainedModel):
499
  module.gradient_checkpointing = False
500
 
501
  def save_pretrained(self, save_directory, *, state_dict=None, **kwargs):
502
- from .util import export_state
503
 
504
  self.config.decision = self.decision_config.model_dump()
505
  if hasattr(self, "calibrator"):
@@ -507,18 +582,101 @@ class GS1PretrainedModel(Qwen3_5PreTrainedModel):
507
  if hasattr(self, "decision_head"):
508
  self.config.head_compute_dtype = "bfloat16" if self.decision_head.bf16_compute else "float32"
509
  kwargs.pop("safe_serialization", None)
 
 
510
  return super().save_pretrained(
511
  save_directory,
512
- state_dict=export_state(self, state_dict),
513
  safe_serialization=True,
514
  **kwargs,
515
  )
516
 
517
  @classmethod
518
- def from_pretrained(cls, source, *, device="cpu", dtype=None, **kwargs):
519
- from .util import load_native
520
-
521
- return load_native(source, device=device, dtype=dtype, model_class=cls, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
 
523
 
524
  class GS1TextModel(GS1PretrainedModel):
 
1
  """GS1: native hybrid text/vision decision model with attention residuals."""
2
 
3
+ import hashlib
4
+ import json
5
  import math
6
  from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Literal
9
 
10
  import torch
11
+ from accelerate import init_empty_weights
12
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
13
+ from safetensors.torch import load_file
14
  from torch import nn
15
  from torch.nn import functional as F
16
  from torch.nn.attention import SDPBackend, sdpa_kernel
 
29
  )
30
  from transformers.utils import ModelOutput
31
 
32
+ INFERENCE_LIMITS = dict(
33
+ max_context=65536,
34
+ max_questions=512,
35
+ max_request_tokens=131072,
36
+ max_branches=5120,
37
+ batch_tokens=16384,
38
+ max_attention_bytes=1073741824,
39
+ max_logit_elements=16777216,
40
+ )
41
+
42
+
43
+ class ModelConfig(BaseModel):
44
+ model_config = ConfigDict(extra="forbid", strict=True, allow_inf_nan=False)
45
+
46
+
47
+ class DecisionConfig(ModelConfig):
48
+ attnres: bool = True
49
+ attnres_layers: list[int] | None = None
50
+ attnres_eps: float = Field(default=1e-6, gt=0)
51
+ bidirectional: bool = True
52
+ bidirectional_layers: list[int] | None = None
53
+ cross_attention_heads: int = Field(default=8, ge=1)
54
+ cross_attention_layers: int = Field(default=2, ge=1)
55
+ projection_size: int = Field(default=256, ge=2)
56
+ norm_eps: float = Field(default=1e-5, gt=0)
57
+
58
+ @field_validator("attnres_layers", "bidirectional_layers")
59
+ @classmethod
60
+ def selected_layers(cls, value):
61
+ if value is not None:
62
+ if not value or any(i < 0 for i in value) or len(set(value)) != len(value):
63
+ raise ValueError("Layer selection must contain distinct nonnegative indices")
64
+ return sorted(value)
65
+ return value
66
+
67
+
68
+ class CalibrationConfig(ModelConfig):
69
+ mode: Literal["identity", "scalar_temperature"] = "identity"
70
+ log_temperature_bound: float = Field(default=math.log(20), gt=0)
71
+
72
+
73
+ def named_seed(seed, name):
74
+ payload = json.dumps(
75
+ ["gs1.rng", seed, name], ensure_ascii=False, separators=(",", ":"), sort_keys=True, allow_nan=False
76
+ )
77
+ return int.from_bytes(hashlib.sha256(payload.encode()).digest()[:8], "big") % (2**63)
78
+
79
+
80
+ def _read_weights(directory):
81
+ index = directory / "model.safetensors.index.json"
82
+ if index.is_file():
83
+ weight_map = json.loads(index.read_text())["weight_map"]
84
+ files = sorted(set(weight_map.values()))
85
+ else:
86
+ files = sorted(p.name for p in directory.glob("*.safetensors"))
87
+ weight_map = None
88
+ if not files:
89
+ raise ValueError("Checkpoint contains no safetensors weights")
90
+ if any(Path(name).is_absolute() or ".." in Path(name).parts for name in files):
91
+ raise ValueError("Checkpoint shard is outside its directory")
92
+ # Hub snapshots can link to cache blobs outside their snapshot directory.
93
+ state = {}
94
+ for name in files:
95
+ shard = load_file(directory / name, device="cpu")
96
+ if state.keys() & shard.keys():
97
+ raise ValueError("Duplicate checkpoint parameter")
98
+ state.update(shard)
99
+ if weight_map is not None and state.keys() != weight_map.keys():
100
+ raise ValueError("Checkpoint index differs from its parameter contents")
101
+ return state
102
 
103
 
104
  class GS1Config(Qwen3_5Config):
 
574
  module.gradient_checkpointing = False
575
 
576
  def save_pretrained(self, save_directory, *, state_dict=None, **kwargs):
577
+ from .compiler import save_chat_template
578
 
579
  self.config.decision = self.decision_config.model_dump()
580
  if hasattr(self, "calibrator"):
 
582
  if hasattr(self, "decision_head"):
583
  self.config.head_compute_dtype = "bfloat16" if self.decision_head.bf16_compute else "float32"
584
  kwargs.pop("safe_serialization", None)
585
+ save_chat_template(save_directory)
586
+ state = self.state_dict() if state_dict is None else state_dict
587
  return super().save_pretrained(
588
  save_directory,
589
+ state_dict={name: value.detach().contiguous() for name, value in state.items()},
590
  safe_serialization=True,
591
  **kwargs,
592
  )
593
 
594
  @classmethod
595
+ def from_pretrained(
596
+ cls,
597
+ source,
598
+ *,
599
+ device="cpu",
600
+ dtype=None,
601
+ config=None,
602
+ revision=None,
603
+ local_files_only=False,
604
+ cache_dir=None,
605
+ token=None,
606
+ force_download=False,
607
+ **kwargs,
608
+ ):
609
+ for name in (
610
+ "_from_auto",
611
+ "_from_pipeline",
612
+ "_commit_hash",
613
+ "trust_remote_code",
614
+ "code_revision",
615
+ "weights_only",
616
+ "use_safetensors",
617
+ "adapter_kwargs",
618
+ ):
619
+ kwargs.pop(name, None)
620
+ if "torch_dtype" in kwargs:
621
+ if dtype is not None:
622
+ raise ValueError("Use only dtype or torch_dtype")
623
+ dtype = kwargs.pop("torch_dtype")
624
+ device_map = kwargs.pop("device_map", None)
625
+ if device_map is not None:
626
+ if isinstance(device_map, dict) and set(device_map) == {""}:
627
+ device = device_map[""]
628
+ elif isinstance(device_map, str) and (
629
+ device_map in {"cpu", "cuda"} or device_map.startswith("cuda:")
630
+ ):
631
+ device = device_map
632
+ else:
633
+ raise ValueError(
634
+ "GS1 supports a single device; use .to(device) instead of automatic device mapping"
635
+ )
636
+ if kwargs:
637
+ raise TypeError(f"Unsupported GS1 loading options: {sorted(kwargs)}")
638
+ directory = Path(source).expanduser()
639
+ if directory.is_dir():
640
+ directory = directory.resolve()
641
+ else:
642
+ from huggingface_hub import snapshot_download
643
+
644
+ directory = Path(
645
+ snapshot_download(
646
+ str(source),
647
+ revision=revision,
648
+ local_files_only=local_files_only,
649
+ cache_dir=cache_dir,
650
+ token=token,
651
+ force_download=force_download,
652
+ allow_patterns=["*.json", "*.safetensors", "*.jinja", "*.py", "*.model", "*.txt"],
653
+ )
654
+ )
655
+ raw = json.loads((directory / "config.json").read_text())
656
+ if raw.get("model_type") != "gs1":
657
+ raise ValueError("Expected native GS1 weights")
658
+ config = config or GS1Config(**raw)
659
+ config._name_or_path = str(source)
660
+ # Keep nonpersistent rotary buffers real while constructing empty parameters.
661
+ with init_empty_weights(include_buffers=False):
662
+ model = cls(config)
663
+ state = _read_weights(directory)
664
+ expected = model.state_dict()
665
+ if state.keys() != expected.keys():
666
+ raise ValueError(
667
+ f"Incomplete GS1 weights: missing={sorted(expected.keys() - state.keys())}, "
668
+ f"unexpected={sorted(state.keys() - expected.keys())}"
669
+ )
670
+ for name, value in state.items():
671
+ if value.shape != expected[name].shape:
672
+ raise ValueError(f"GS1 parameter shape mismatch: {name}")
673
+ model.load_state_dict(state, strict=True, assign=True)
674
+ model._processor_source = directory
675
+ if dtype is not None and dtype != "auto":
676
+ if isinstance(dtype, str):
677
+ dtype = getattr(torch, dtype)
678
+ model.to(dtype=dtype)
679
+ return model.to(device).eval()
680
 
681
 
682
  class GS1TextModel(GS1PretrainedModel):
schema.py CHANGED
@@ -13,7 +13,7 @@ from typesafe_sdk import Noul as Noul
13
  from typesafe_sdk import Question as Question
14
  from typesafe_sdk import Score as Score
15
 
16
- from .config import CalibrationConfig
17
 
18
  Description = str | dict[str, JsonValue] | list[JsonValue]
19
  CONFIDENCE_VERSION = "typesafe_v1"
 
13
  from typesafe_sdk import Question as Question
14
  from typesafe_sdk import Score as Score
15
 
16
+ from .modular_gs1 import CalibrationConfig
17
 
18
  Description = str | dict[str, JsonValue] | list[JsonValue]
19
  CONFIDENCE_VERSION = "typesafe_v1"
util.py DELETED
@@ -1,202 +0,0 @@
1
- """Native checkpoint loading, serialization and deterministic identities."""
2
-
3
- import hashlib
4
- import json
5
- from pathlib import Path
6
-
7
- import torch
8
- from accelerate import init_empty_weights
9
- from safetensors.torch import load_file
10
-
11
-
12
- def canonical(value) -> str:
13
- return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True, allow_nan=False)
14
-
15
-
16
- def digest(value) -> str:
17
- return hashlib.sha256(canonical(value).encode()).hexdigest()
18
-
19
-
20
- def file_hash(path: Path) -> str:
21
- h = hashlib.sha256()
22
- with path.open("rb") as stream:
23
- for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b""):
24
- h.update(chunk)
25
- return h.hexdigest()
26
-
27
-
28
- def named_seed(seed, name):
29
- return int.from_bytes(hashlib.sha256(canonical(["gs1.rng", seed, name]).encode()).digest()[:8], "big") % (
30
- 2**63
31
- )
32
-
33
-
34
- def _weight_files(directory):
35
- directory = Path(directory)
36
- index = directory / "model.safetensors.index.json"
37
- if index.is_file():
38
- weight_map = json.loads(index.read_text())["weight_map"]
39
- files = sorted(set(weight_map.values()))
40
- else:
41
- files = sorted(p.name for p in directory.glob("*.safetensors"))
42
- weight_map = None
43
- if not files:
44
- raise ValueError("Checkpoint contains no safetensors weights")
45
- if any(Path(name).is_absolute() or ".." in Path(name).parts for name in files):
46
- raise ValueError("Checkpoint shard is outside its directory")
47
- # Hub snapshots link to shared cache blobs; validate shard names rather
48
- # than rejecting those normal cache symlinks after resolving their targets.
49
- return [directory / name for name in files], weight_map
50
-
51
-
52
- def checkpoint_files(directory):
53
- """Files that define the weights, native configuration and input encoding."""
54
- directory = Path(directory)
55
- names = (
56
- "config.json",
57
- "model.safetensors.index.json",
58
- "tokenizer.json",
59
- "tokenizer_config.json",
60
- "special_tokens_map.json",
61
- "added_tokens.json",
62
- "vocab.json",
63
- "vocab.txt",
64
- "merges.txt",
65
- "tokenizer.model",
66
- "chat_template.jinja",
67
- "preprocessor_config.json",
68
- "video_preprocessor_config.json",
69
- )
70
- weights, _ = _weight_files(directory)
71
- return sorted({*weights, *(directory / name for name in names if (directory / name).is_file())})
72
-
73
-
74
- def read_weights(directory):
75
- files, weight_map = _weight_files(directory)
76
- state = {}
77
- for path in files:
78
- shard = load_file(path, device="cpu")
79
- if state.keys() & shard.keys():
80
- raise ValueError("Duplicate checkpoint parameter")
81
- state.update(shard)
82
- if weight_map is not None and state.keys() != weight_map.keys():
83
- raise ValueError("Checkpoint index differs from its parameter contents")
84
- return state
85
-
86
-
87
- def _construct(config, model_class=None):
88
- from .modular_gs1 import GS1Model
89
-
90
- # Nonpersistent rotary buffers must remain real tensors, not meta placeholders.
91
- with init_empty_weights(include_buffers=False):
92
- return (model_class or GS1Model)(config)
93
-
94
-
95
- def _assign(model, state):
96
- expected = model.state_dict()
97
- if state.keys() != expected.keys():
98
- raise ValueError(
99
- f"Incomplete GS1 weights: missing={sorted(expected.keys() - state.keys())}, "
100
- f"unexpected={sorted(state.keys() - expected.keys())}"
101
- )
102
- for name, value in state.items():
103
- if value.shape != expected[name].shape:
104
- raise ValueError(f"GS1 parameter shape mismatch: {name}")
105
- model.load_state_dict(state, strict=True, assign=True)
106
- return model
107
-
108
-
109
- def resolve_source(
110
- source, *, revision=None, local_files_only=False, cache_dir=None, token=None, force_download=False
111
- ):
112
- """Resolve a native directory or an immutable Hub snapshot."""
113
- path = Path(source).expanduser()
114
- if path.is_dir():
115
- return path.resolve()
116
- from huggingface_hub import snapshot_download
117
-
118
- return Path(
119
- snapshot_download(
120
- str(source),
121
- revision=revision,
122
- local_files_only=local_files_only,
123
- cache_dir=cache_dir,
124
- token=token,
125
- force_download=force_download,
126
- allow_patterns=["*.json", "*.safetensors", "*.jinja", "*.py", "*.model", "*.txt"],
127
- )
128
- )
129
-
130
-
131
- def load_native(
132
- source,
133
- *,
134
- device="cpu",
135
- dtype=None,
136
- model_class=None,
137
- config=None,
138
- revision=None,
139
- local_files_only=False,
140
- cache_dir=None,
141
- token=None,
142
- force_download=False,
143
- **kwargs,
144
- ):
145
- from .modular_gs1 import GS1Config
146
-
147
- for name in (
148
- "_from_auto",
149
- "_from_pipeline",
150
- "_commit_hash",
151
- "trust_remote_code",
152
- "code_revision",
153
- "weights_only",
154
- "use_safetensors",
155
- "adapter_kwargs",
156
- ):
157
- kwargs.pop(name, None)
158
- if "torch_dtype" in kwargs:
159
- if dtype is not None:
160
- raise ValueError("Use only dtype or torch_dtype")
161
- dtype = kwargs.pop("torch_dtype")
162
- device_map = kwargs.pop("device_map", None)
163
- if device_map is not None:
164
- if isinstance(device_map, dict) and set(device_map) == {""}:
165
- device = device_map[""]
166
- elif isinstance(device_map, str) and (
167
- device_map in {"cpu", "cuda"} or device_map.startswith("cuda:")
168
- ):
169
- device = device_map
170
- else:
171
- raise ValueError(
172
- "GS1 supports a single device; use .to(device) instead of automatic device mapping"
173
- )
174
- if kwargs:
175
- raise TypeError(f"Unsupported GS1 loading options: {sorted(kwargs)}")
176
- directory = resolve_source(
177
- source,
178
- revision=revision,
179
- local_files_only=local_files_only,
180
- cache_dir=cache_dir,
181
- token=token,
182
- force_download=force_download,
183
- )
184
- raw = json.loads((directory / "config.json").read_text())
185
- if raw.get("model_type") != "gs1":
186
- raise ValueError("Expected native GS1 weights")
187
- config = config or GS1Config(**raw)
188
- config._name_or_path = str(source)
189
- model = _construct(config, model_class)
190
- model = _assign(model, read_weights(directory))
191
- model._processor_source = directory
192
- if dtype is not None and dtype != "auto":
193
- if isinstance(dtype, str):
194
- dtype = getattr(torch, dtype)
195
- model.to(dtype=dtype)
196
- return model.to(device).eval()
197
-
198
-
199
- def export_state(model, state=None):
200
- """Save the native inference parameter tree without dtype conversion."""
201
- state = model.state_dict() if state is None else state
202
- return {name: value.detach().contiguous() for name, value in state.items()}