yiqichen01 commited on
Commit
5137deb
·
verified ·
1 Parent(s): 3917583

Upload folder using huggingface_hub

Browse files
Files changed (34) hide show
  1. .gitattributes +2 -0
  2. LICENSE +210 -0
  3. assets/benchmark-appendix.png +3 -0
  4. assets/benchmark.png +3 -0
  5. assets/logo-en.png +0 -0
  6. assets/logo-zh.png +0 -0
  7. finetune/README.md +416 -0
  8. finetune/README_CN.md +416 -0
  9. finetune/data/example_data.jsonl +8 -0
  10. finetune/deepspeed_support/ds_zero2_no_offload.json +25 -0
  11. finetune/deepspeed_support/ds_zero2_offload.json +34 -0
  12. finetune/deepspeed_support/ds_zero3_no_offload.json +41 -0
  13. finetune/deepspeed_support/ds_zero3_offload.json +41 -0
  14. finetune/deepspeed_support/ds_zero3_optimizer_offload.json +37 -0
  15. finetune/deepspeed_support/ds_zero3_param_offload.json +37 -0
  16. finetune/deepspeed_support/merge_lora_weight.py +54 -0
  17. finetune/deepspeed_support/merge_lora_weight.sh +1 -0
  18. finetune/deepspeed_support/train.py +903 -0
  19. finetune/deepspeed_support/train.sh +118 -0
  20. finetune/deepspeed_support/train_lora.sh +123 -0
  21. finetune/llama_factory_support/dataset_info.json +16 -0
  22. finetune/llama_factory_support/hy_v3_full_sft.yaml +57 -0
  23. finetune/llama_factory_support/hy_v3_lora_sft.yaml +56 -0
  24. finetune/llama_factory_support/hy_v3_patches.py +621 -0
  25. finetune/llama_factory_support/hy_v3_template.py +46 -0
  26. finetune/llama_factory_support/train_hy_v3.py +72 -0
  27. finetune/llama_factory_support/train_lf.sh +114 -0
  28. finetune/ms_swift_support/hy_v3_full_sft.yaml +115 -0
  29. finetune/ms_swift_support/hy_v3_lora_sft.yaml +104 -0
  30. finetune/ms_swift_support/hy_v3_swift_patches.py +309 -0
  31. finetune/ms_swift_support/sft_train.sh +205 -0
  32. finetune/requirements.txt +12 -0
  33. finetune/tools/check_converted.py +455 -0
  34. finetune/tools/convert_ckpt_to_outer.py +641 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/benchmark-appendix.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/benchmark.png filter=lfs diff=lfs merge=lfs -text
LICENSE ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Tencent is pleased to support the open source community by making Tencent Hy3 available.
2
+
3
+ Copyright (C) 2026 Tencent. All rights reserved.
4
+
5
+ Tencent Hy3 is licensed under the Apache-2.0.
6
+
7
+
8
+ Terms of the Apache-2.0:
9
+ --------------------------------------------------------------------
10
+ Apache License
11
+ Version 2.0, January 2004
12
+ http://www.apache.org/licenses/
13
+
14
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
15
+
16
+ 1. Definitions.
17
+
18
+ "License" shall mean the terms and conditions for use, reproduction,
19
+ and distribution as defined by Sections 1 through 9 of this document.
20
+
21
+ "Licensor" shall mean the copyright owner or entity authorized by
22
+ the copyright owner that is granting the License.
23
+
24
+ "Legal Entity" shall mean the union of the acting entity and all
25
+ other entities that control, are controlled by, or are under common
26
+ control with that entity. For the purposes of this definition,
27
+ "control" means (i) the power, direct or indirect, to cause the
28
+ direction or management of such entity, whether by contract or
29
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
30
+ outstanding shares, or (iii) beneficial ownership of such entity.
31
+
32
+ "You" (or "Your") shall mean an individual or Legal Entity
33
+ exercising permissions granted by this License.
34
+
35
+ "Source" form shall mean the preferred form for making modifications,
36
+ including but not limited to software source code, documentation
37
+ source, and configuration files.
38
+
39
+ "Object" form shall mean any form resulting from mechanical
40
+ transformation or translation of a Source form, including but
41
+ not limited to compiled object code, generated documentation,
42
+ and conversions to other media types.
43
+
44
+ "Work" shall mean the work of authorship, whether in Source or
45
+ Object form, made available under the License, as indicated by a
46
+ copyright notice that is included in or attached to the work
47
+ (an example is provided in the Appendix below).
48
+
49
+ "Derivative Works" shall mean any work, whether in Source or Object
50
+ form, that is based on (or derived from) the Work and for which the
51
+ editorial revisions, annotations, elaborations, or other modifications
52
+ represent, as a whole, an original work of authorship. For the purposes
53
+ of this License, Derivative Works shall not include works that remain
54
+ separable from, or merely link (or bind by name) to the interfaces of,
55
+ the Work and Derivative Works thereof.
56
+
57
+ "Contribution" shall mean any work of authorship, including
58
+ the original version of the Work and any modifications or additions
59
+ to that Work or Derivative Works thereof, that is intentionally
60
+ submitted to Licensor for inclusion in the Work by the copyright owner
61
+ or by an individual or Legal Entity authorized to submit on behalf of
62
+ the copyright owner. For the purposes of this definition, "submitted"
63
+ means any form of electronic, verbal, or written communication sent
64
+ to the Licensor or its representatives, including but not limited to
65
+ communication on electronic mailing lists, source code control systems,
66
+ and issue tracking systems that are managed by, or on behalf of, the
67
+ Licensor for the purpose of discussing and improving the Work, but
68
+ excluding communication that is conspicuously marked or otherwise
69
+ designated in writing by the copyright owner as "Not a Contribution."
70
+
71
+ "Contributor" shall mean Licensor and any individual or Legal Entity
72
+ on behalf of whom a Contribution has been received by Licensor and
73
+ subsequently incorporated within the Work.
74
+
75
+ 2. Grant of Copyright License. Subject to the terms and conditions of
76
+ this License, each Contributor hereby grants to You a perpetual,
77
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
78
+ copyright license to reproduce, prepare Derivative Works of,
79
+ publicly display, publicly perform, sublicense, and distribute the
80
+ Work and such Derivative Works in Source or Object form.
81
+
82
+ 3. Grant of Patent License. Subject to the terms and conditions of
83
+ this License, each Contributor hereby grants to You a perpetual,
84
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
85
+ (except as stated in this section) patent license to make, have made,
86
+ use, offer to sell, sell, import, and otherwise transfer the Work,
87
+ where such license applies only to those patent claims licensable
88
+ by such Contributor that are necessarily infringed by their
89
+ Contribution(s) alone or by combination of their Contribution(s)
90
+ with the Work to which such Contribution(s) was submitted. If You
91
+ institute patent litigation against any entity (including a
92
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
93
+ or a Contribution incorporated within the Work constitutes direct
94
+ or contributory patent infringement, then any patent licenses
95
+ granted to You under this License for that Work shall terminate
96
+ as of the date such litigation is filed.
97
+
98
+ 4. Redistribution. You may reproduce and distribute copies of the
99
+ Work or Derivative Works thereof in any medium, with or without
100
+ modifications, and in Source or Object form, provided that You
101
+ meet the following conditions:
102
+
103
+ (a) You must give any other recipients of the Work or
104
+ Derivative Works a copy of this License; and
105
+
106
+ (b) You must cause any modified files to carry prominent notices
107
+ stating that You changed the files; and
108
+
109
+ (c) You must retain, in the Source form of any Derivative Works
110
+ that You distribute, all copyright, patent, trademark, and
111
+ attribution notices from the Source form of the Work,
112
+ excluding those notices that do not pertain to any part of
113
+ the Derivative Works; and
114
+
115
+ (d) If the Work includes a "NOTICE" text file as part of its
116
+ distribution, then any Derivative Works that You distribute must
117
+ include a readable copy of the attribution notices contained
118
+ within such NOTICE file, excluding those notices that do not
119
+ pertain to any part of the Derivative Works, in at least one
120
+ of the following places: within a NOTICE text file distributed
121
+ as part of the Derivative Works; within the Source form or
122
+ documentation, if provided along with the Derivative Works; or,
123
+ within a display generated by the Derivative Works, if and
124
+ wherever such third-party notices normally appear. The contents
125
+ of the NOTICE file are for informational purposes only and
126
+ do not modify the License. You may add Your own attribution
127
+ notices within Derivative Works that You distribute, alongside
128
+ or as an addendum to the NOTICE text from the Work, provided
129
+ that such additional attribution notices cannot be construed
130
+ as modifying the License.
131
+
132
+ You may add Your own copyright statement to Your modifications and
133
+ may provide additional or different license terms and conditions
134
+ for use, reproduction, or distribution of Your modifications, or
135
+ for any such Derivative Works as a whole, provided Your use,
136
+ reproduction, and distribution of the Work otherwise complies with
137
+ the conditions stated in this License.
138
+
139
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
140
+ any Contribution intentionally submitted for inclusion in the Work
141
+ by You to the Licensor shall be under the terms and conditions of
142
+ this License, without any additional terms or conditions.
143
+ Notwithstanding the above, nothing herein shall supersede or modify
144
+ the terms of any separate license agreement you may have executed
145
+ with Licensor regarding such Contributions.
146
+
147
+ 6. Trademarks. This License does not grant permission to use the trade
148
+ names, trademarks, service marks, or product names of the Licensor,
149
+ except as required for reasonable and customary use in describing the
150
+ origin of the Work and reproducing the content of the NOTICE file.
151
+
152
+ 7. Disclaimer of Warranty. Unless required by applicable law or
153
+ agreed to in writing, Licensor provides the Work (and each
154
+ Contributor provides its Contributions) on an "AS IS" BASIS,
155
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
156
+ implied, including, without limitation, any warranties or conditions
157
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
158
+ PARTICULAR PURPOSE. You are solely responsible for determining the
159
+ appropriateness of using or redistributing the Work and assume any
160
+ risks associated with Your exercise of permissions under this License.
161
+
162
+ 8. Limitation of Liability. In no event and under no legal theory,
163
+ whether in tort (including negligence), contract, or otherwise,
164
+ unless required by applicable law (such as deliberate and grossly
165
+ negligent acts) or agreed to in writing, shall any Contributor be
166
+ liable to You for damages, including any direct, indirect, special,
167
+ incidental, or consequential damages of any character arising as a
168
+ result of this License or out of the use or inability to use the
169
+ Work (including but not limited to damages for loss of goodwill,
170
+ work stoppage, computer failure or malfunction, or any and all
171
+ other commercial damages or losses), even if such Contributor
172
+ has been advised of the possibility of such damages.
173
+
174
+ 9. Accepting Warranty or Additional Liability. While redistributing
175
+ the Work or Derivative Works thereof, You may choose to offer,
176
+ and charge a fee for, acceptance of support, warranty, indemnity,
177
+ or other liability obligations and/or rights consistent with this
178
+ License. However, in accepting such obligations, You may act only
179
+ on Your own behalf and on Your sole responsibility, not on behalf
180
+ of any other Contributor, and only if You agree to indemnify,
181
+ defend, and hold each Contributor harmless for any liability
182
+ incurred by, or claims asserted against, such Contributor by reason
183
+ of your accepting any such warranty or additional liability.
184
+
185
+ END OF TERMS AND CONDITIONS
186
+
187
+ APPENDIX: How to apply the Apache License to your work.
188
+
189
+ To apply the Apache License to your work, attach the following
190
+ boilerplate notice, with the fields enclosed by brackets "[]"
191
+ replaced with your own identifying information. (Don't include
192
+ the brackets!) The text should be enclosed in the appropriate
193
+ comment syntax for the file format. We also recommend that a
194
+ file or class name and description of purpose be included on the
195
+ same "printed page" as the copyright notice for easier
196
+ identification within third-party archives.
197
+
198
+ Copyright [yyyy] [name of copyright owner]
199
+
200
+ Licensed under the Apache License, Version 2.0 (the "License");
201
+ you may not use this file except in compliance with the License.
202
+ You may obtain a copy of the License at
203
+
204
+ http://www.apache.org/licenses/LICENSE-2.0
205
+
206
+ Unless required by applicable law or agreed to in writing, software
207
+ distributed under the License is distributed on an "AS IS" BASIS,
208
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
209
+ See the License for the specific language governing permissions and
210
+ limitations under the License.
assets/benchmark-appendix.png ADDED

Git LFS Details

  • SHA256: c2b5092a0dd1207a6b356fed2047956bbce13e8441943c19b266ef9bbca3d174
  • Pointer size: 132 Bytes
  • Size of remote file: 4.71 MB
assets/benchmark.png ADDED

Git LFS Details

  • SHA256: 4e85c1ec7d769ec1211a479338ab8a5f6ca8d03eb791a4664cef030e162c027b
  • Pointer size: 132 Bytes
  • Size of remote file: 1.07 MB
assets/logo-en.png ADDED
assets/logo-zh.png ADDED
finetune/README.md ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="left">
2
+ <a href="README_CN.md">中文</a> | English
3
+ </p>
4
+
5
+ # Model Training
6
+
7
+ Hy3 provides processes related to model training. This section details how to process training data for model training purposes.
8
+
9
+ ## Training Data Format and Processing
10
+
11
+ **Hy3 supports both "slow thinking" and "fast thinking" modes. By default, the model outputs in slow thinking mode. If you wish the model to use fast thinking, you can control it via the `reasoning_effort` parameter (options: `high`, `low`, `no_think`).**
12
+
13
+ The training data should be formatted as a list of messages. By default, the system prompt for both training and inference is empty, but you may customize it as needed.
14
+
15
+ ```python
16
+ # Fast thinking pattern (no_think)
17
+ {"reasoning_effort": "no_think", "messages": [{"content": "You are a helpful assistant.\nThe current time is 2026-01-01 13:26:12 Thursday", "role": "system"}, {"content": "1+1=?", "role": "user"}, {"role": "assistant", "content": "1+1=2"}]}
18
+
19
+ # Slow thinking pattern (high)
20
+ {"reasoning_effort": "high", "messages": [{"content": "You are a helpful assistant.\nThe current time is 2026-01-01 13:26:12 Thursday", "role": "system"}, {"content": "1+1=?", "role": "user"}, {"role": "assistant", "content": "1+1=2", "reasoning_content": "The user is asking for the result of 1 + 1. In basic decimal arithmetic, 1 + 1 equals 2."}]}
21
+
22
+ from transformers import AutoTokenizer
23
+ tokenizer = AutoTokenizer.from_pretrained("./models", use_fast=False, trust_remote_code=True)
24
+ ids = tokenizer.apply_chat_template(messages, is_training=True)
25
+ ```
26
+
27
+ ## Checkpoint Format Conversion
28
+
29
+ The original Hy3 checkpoint stores each expert's weights independently. Before training, it is recommended to convert it to the HuggingFace-compatible format (fusing multiple experts per layer into 3D tensors with unified key naming) to improve loading and training speed. You can also train directly with the original format without conversion, but the loading speed will be slower. We provide a conversion script `convert_ckpt_to_outer.py` and a validation script `check_converted.py`, located in the `train/tools` directory.
30
+
31
+ ### Conversion
32
+
33
+ ```sh
34
+ python convert_ckpt_to_outer.py \
35
+ --input_dir <original_checkpoint_dir> \
36
+ --output_dir <output_dir> \
37
+ --workers 8
38
+ ```
39
+
40
+ **Parameters:**
41
+
42
+ - `--input_dir`: Path to the original checkpoint directory (required)
43
+ - `--output_dir`: Path to the converted checkpoint output directory (required)
44
+ - `--workers`: Number of parallel worker processes, default is 8 (optional)
45
+
46
+ The conversion script performs the following steps:
47
+ 1. Pre-scans `model.safetensors.index.json` to detect cross-shard expert groups
48
+ 2. Converts weights shard-by-shard in parallel (key renaming + expert fusion)
49
+ 3. Post-processes cross-shard expert groups (merges data from multiple shards)
50
+ 4. Copies `config.json`, tokenizer, and other files
51
+ 5. Rebuilds `model.safetensors.index.json`
52
+
53
+ ### Validation
54
+
55
+ After conversion, it is recommended to validate the result using the validation script:
56
+
57
+ ```sh
58
+ python check_converted.py <converted_checkpoint_dir> --spot-check 3
59
+ ```
60
+
61
+ **Parameters:**
62
+
63
+ - First argument: Path to the converted checkpoint directory (required)
64
+ - `--spot-check`: Number of shard files to spot-check by loading tensors and verifying shape, dtype, NaN/Inf, etc. Default is 3 (optional)
65
+
66
+ The validation script checks the following:
67
+ 1. Completeness of `config.json`
68
+ 2. Whether all expected keys in `model.safetensors.index.json` are present (including regular layers and MTP layers)
69
+ 3. Whether all referenced shard files exist and are non-empty
70
+ 4. Spot-checks tensor shape, dtype, and NaN/Inf in selected shard files
71
+ 5. Detects orphan empty shard files (cross-shard merge residues, safe to delete)
72
+
73
+ ## Quick Start
74
+
75
+ You can quickly get started by following the instructions in the Quick Start Guide.
76
+
77
+ ## Model Training
78
+
79
+ ### Hardware Requirements
80
+
81
+ Based on testing, when `make_moe_param_leaf_module` and `zero3+offload` are disabled and `max_seq_length` is set to 4096:
82
+
83
+ - **LoRA Fine-tuning**: Requires at least a single machine with 8 GPUs (at least 80GB memory each).
84
+ - **Full Fine-tuning**: Requires at least 4 machines with 32 GPUs (at least 80GB memory each).
85
+
86
+ ### Configure Passwordless SSH Login Between Machines (Multi-Machine Training)
87
+
88
+ > If you only use single-machine training, you can skip this section.
89
+
90
+ The following instructions use two machines as an example, with their IPs denoted as `${ip1}` and `${ip2}`. All steps should be performed inside the Docker container.
91
+
92
+ First, configure passwordless SSH for each container on every machine:
93
+
94
+ ```sh
95
+ ssh-keygen # Generate id_rsa and id_rsa.pub for passwordless login
96
+ ssh-keygen -t rsa -A # Generate /etc/ssh/ssh_host_rsa_key and ssh_host_ecdsa_key for SSH listening
97
+ /usr/sbin/sshd -p 36005 -o ListenAddress=0.0.0.0 # Start SSH listening
98
+ echo "Port 36005" > ~/.ssh/config # Set SSH connection port to 36005
99
+ passwd root # Set the root password to avoid monitoring platform alerts
100
+ ```
101
+
102
+ Note: `36005` is an example port. You may use any available port, but ensure it is **open** and **not occupied by other processes**.
103
+
104
+ Next, in each machine's container, execute:
105
+
106
+ ```sh
107
+ cat ~/.ssh/id_rsa.pub
108
+ ```
109
+
110
+ **Copy the output SSH public key and paste it into the `~/.ssh/authorized_keys` file, one key per line. This must be done on every machine.** In the end, the `~/.ssh/authorized_keys` file on each machine should be identical and contain the public keys of all machines.
111
+
112
+ Please note that for multi-node training, the code executed on each node must be identical. It is recommended to mount a shared network drive. If this is not possible, you must manually copy the dataset, scripts, and code to the same directory on each machine.
113
+
114
+ ### Launch Methods
115
+
116
+ This project provides three training methods. You can choose based on your needs:
117
+
118
+ - **DeepSpeed Native Training** (based on HuggingFace Transformers Trainer): Located in the `train/deepspeed_support` directory
119
+ - **LLaMA-Factory Training**: Located in the `train/llama_factory_support` directory
120
+ - **ms-swift Training**: Located in the `train/ms_swift_support` directory
121
+
122
+ #### DeepSpeed Native Training
123
+
124
+ Reference: [HuggingFace Transformers Trainer](https://huggingface.co/docs/transformers/main/en/main_classes/trainer)
125
+
126
+ ##### Single-Machine Training
127
+
128
+ In the `train/deepspeed_support` directory, execute:
129
+
130
+ ```sh
131
+ pip install -r requirements.txt
132
+ bash train.sh
133
+ ```
134
+
135
+ ##### Multi-Machine Training
136
+
137
+ To launch training across multiple machines, please first complete the configuration in [Configure Passwordless SSH Login Between Machines](#configure-passwordless-ssh-login-between-machines-multi-machine-training), and ensure all machines are within the same cluster.
138
+
139
+ Confirm that dependencies are installed (if not, run `pip install -r requirements.txt`), then add the following configuration at the beginning of `train.sh`:
140
+
141
+ ```shell
142
+ export HOST_GPU_NUM=8
143
+ # IP list, comma separated. e.g. "192.168.1.1,192.168.1.2" or single node "192.168.1.1"
144
+ IP_LIST=${IP_LIST:-"127.0.0.1"}
145
+ ```
146
+
147
+ Note: If the `IP_LIST` environment variable is not set, replace `IP_LIST` with the IP list! The format is:
148
+ ```
149
+ For a single IP:
150
+ IP_LIST=${ip_1}
151
+
152
+ For multiple IPs:
153
+ IP_LIST=${ip_1},${ip_2}
154
+
155
+ ```
156
+
157
+ Replace `${ip_1}` and `${ip_2}` with the actual IP addresses.
158
+
159
+ Then, on the machine with `${ip1}`, execute `bash train.sh` in the `train/deepspeed_support/` directory. On first launch, you may see the following output:
160
+
161
+ ```ssh
162
+ The authenticity of host '[ip]:36005 ([ip]:36005)' can't be established.
163
+ ECDSA key fingerprint is xxxxxx.
164
+ ECDSA key fingerprint is MD5:xxxxxx.
165
+ Are you sure you want to continue connecting (yes/no)?
166
+ ```
167
+
168
+ Type `yes` to continue.
169
+
170
+ ##### Key Parameters
171
+
172
+ The key parameters in the script are as follows:
173
+
174
+ - `--deepspeed`: Path to the DeepSpeed configuration file. Three default DeepSpeed configuration files are provided in the `train/deepspeed_support` folder: `ds_zero2_no_offload.json`, `ds_zero3_no_offload.json`, and `ds_zero3_offload.json`, with decreasing memory requirements in that order.
175
+ - `--model_name_or_path`: Path to the Hy3 HF pre-trained model weights to load, otherwise loading will fail.
176
+ - `--tokenizer_name_or_path`: Path to the tokenizer folder, otherwise loading will fail.
177
+ - `--train_data_file`: Path to the training file, which should be a jsonl file.
178
+ - `--output_dir`: Output directory where logs, tensorboard files, and model weights will be stored.
179
+ - `--per_device_train_batch_size`: Batch size per GPU.
180
+ - `--gradient_accumulation_steps`: Number of gradient accumulation steps. The global batch size is `per_device_train_batch_size * gradient_accumulation_steps * dp_size`.
181
+ - `--max_steps`: Total number of training steps.
182
+ - `--save_steps`: Number of steps between saving checkpoints.
183
+ - `--use_lora`: Whether to use LoRA training. Also accepts `--lora_rank`, `--lora_alpha`, and `--lora_dropout` parameters. By default, LoRA is applied to "q_proj", "k_proj", "v_proj", and "o_proj". To change this, modify the code. Note: **When using LoRA training, only the LoRA weights are saved, not the base model weights.** To merge LoRA weights, see the "LoRA Weight Merging" section below.
184
+ - `--make_moe_param_leaf_module`: When using ZeRO-3 with MoE training, treat the MoE module as a leaf module, i.e., its parameters are not partitioned by ZeRO-3. This option is expected to significantly increase memory usage.
185
+ - `--gradient_checkpointing`: Enable gradient checkpointing.
186
+ - `--train_attention_params_only`: Whether to train only attention parameters.
187
+ - `--learning_rate`: Maximum learning rate during training.
188
+ - `--min_lr`: Minimum learning rate during training.
189
+ - `--use_flash_attn`: Enable flash-attention for accelerated training.
190
+
191
+ **Notes:**
192
+
193
+ - To resume training from a previously saved checkpoint rather than loading pre-trained weights, specify `--resume_from_checkpoint` with the path to the checkpoint. Do not specify `--model_name_or_path`; this will load only the weights without the training state.
194
+ - When resuming from a checkpoint, there may be minor differences in loss due to the randomness of some non-deterministic algorithms. This is normal. See: [HuggingFace Transformers Trainer Randomness](https://huggingface.co/docs/transformers/main/en/main_classes/trainer#randomness)
195
+ - When `--model_name_or_path` is specified, all model-related parameters will be ignored.
196
+ - Samples within a batch are padded to the length of the longest sample in the batch, but the maximum length of each sample is `max_seq_length`. Any excess will be truncated.
197
+ - If you see a warning about bias weights not being loaded, you can ignore it. Hunyuan-Large does not use bias.
198
+
199
+ ##### What if GPU Memory is Insufficient?
200
+
201
+ Reference: [DeepSpeed Configuration](https://www.deepspeed.ai/docs/config-json/)
202
+
203
+ You can try modifying the DeepSpeed configuration by removing the `auto` attribute from the following parameters and reducing their values:
204
+
205
+ - `stage3_param_persistence_threshold`
206
+ - `stage3_prefetch_bucket_size`
207
+ - `stage3_max_reuse_distance`
208
+
209
+ ##### LoRA Weight Merging
210
+
211
+ LoRA weights saved during training cannot be merged into the ZeRO-3 model at runtime, as ZeRO-3 partitions model weights across data parallel ranks. To merge LoRA weights into the base model, you can do so offline to obtain a merged weight file. Run `merge_lora_weight.sh` to merge the LoRA and base model weights. The parameters are:
212
+
213
+ - `--base_model_path`: Directory of the base model weights
214
+ - `--adapter_model_path`: Directory of the LoRA weights
215
+ - `--output_path`: Directory to save the merged weights
216
+ - `--save_dtype`: Data type for saving the merged weights; options are: fp16, bf16, fp32
217
+
218
+ #### LLaMA-Factory Training
219
+
220
+ If you are familiar with LLaMA-Factory, you may use it for fine-tuning. All scripts, code, and configuration files are archived in the `train/llama_factory_support` directory. Unless otherwise specified, all files mentioned below are located in this directory.
221
+
222
+ ##### Installation
223
+
224
+ You can install LLaMA-Factory by downloading the source code from https://github.com/hiyouga/LLaMA-Factory/tree/main and following the instructions on the website.
225
+
226
+ ##### Configuration Files
227
+
228
+ We provide sample LLaMA-Factory training configuration files: `hy_v3_lora_sft.yaml` and `hy_v3_full_sft.yaml`, corresponding to LoRA training and full fine-tuning respectively.
229
+
230
+ Key parameters in the configuration files are as follows:
231
+
232
+ **Model:**
233
+
234
+ - `model_name_or_path`: Path to the Hy3 HF format pre-trained model weights
235
+ - `trust_remote_code`: Whether to trust remote code; Hy3 requires this to be set to `true`
236
+
237
+ **Training Method:**
238
+
239
+ - `stage`: Training stage, currently `sft` (supervised fine-tuning)
240
+ - `finetuning_type`: Fine-tuning type, either `full` (full fine-tuning) or `lora` (LoRA fine-tuning)
241
+ - `deepspeed`: DeepSpeed configuration file path; `ds_zero3_offload.json` is recommended for full fine-tuning, `ds_zero2_offload_lora.json` for LoRA fine-tuning
242
+
243
+ **LoRA Parameters (only effective during LoRA fine-tuning):**
244
+
245
+ - `lora_rank`: LoRA rank, default `64`
246
+ - `lora_alpha`: LoRA alpha coefficient, default `128`
247
+ - `lora_dropout`: LoRA dropout ratio, default `0.05`
248
+ - `lora_target`: Target modules for LoRA, default `q_proj,k_proj,v_proj,o_proj`
249
+
250
+ **Dataset:**
251
+
252
+ - `dataset_dir`: Dataset directory path
253
+ - `dataset`: Dataset name, must be registered in `dataset_info.json` under `dataset_dir`
254
+ - `template`: Chat template; Hy3 uses `hy_v3`
255
+ - `cutoff_len`: Maximum sequence length; sequences exceeding this will be truncated. For full fine-tuning, can be set to `262144` (262K); for LoRA fine-tuning, `8192` is recommended to save memory
256
+ - `max_samples`: Maximum number of samples per dataset
257
+ - `overwrite_cache`: Whether to overwrite cached preprocessed datasets
258
+
259
+ **Output:**
260
+
261
+ - `output_dir`: Output directory where logs, TensorBoard files, and weights will be stored
262
+ - `logging_steps`: Number of steps between logging
263
+ - `save_steps`: Number of steps between saving checkpoints
264
+ - `plot_loss`: Whether to plot the training loss curve
265
+ - `overwrite_output_dir`: Whether to overwrite the existing output directory
266
+ - `save_only_model`: Whether to save only model weights (excluding optimizer states, etc.)
267
+ - `report_to`: Logging tool, options: `none`, `wandb`, `tensorboard`, `swanlab`, `mlflow`
268
+
269
+ **Training Hyperparameters:**
270
+
271
+ - `per_device_train_batch_size`: Batch size per GPU
272
+ - `gradient_accumulation_steps`: Gradient accumulation steps; `per_device_train_batch_size * gradient_accumulation_steps * dp_size` equals the global batch size
273
+ - `learning_rate`: Maximum learning rate; `1.0e-5` recommended for full fine-tuning, `2.0e-4` for LoRA fine-tuning
274
+ - `num_train_epochs`: Number of training epochs
275
+ - `lr_scheduler_type`: Learning rate scheduler type; `cosine_with_min_lr` is recommended
276
+ - `lr_scheduler_kwargs.min_lr_rate`: Ratio of minimum to maximum learning rate; e.g., `0.1` means the minimum learning rate is 10% of the maximum
277
+ - `warmup_ratio`: Proportion of total training steps used for warmup
278
+ - `bf16`: Whether to use BFloat16 mixed precision training
279
+ - `gradient_checkpointing`: Whether to enable gradient checkpointing to save memory
280
+ - `ddp_timeout`: Distributed training timeout (milliseconds)
281
+ - `flash_attn`: Attention implementation; `fa2` (FlashAttention-2) is recommended, `sdpa` is also available; using `fa2` requires the flash-attn package
282
+ - `resume_from_checkpoint`: Resume training from a specified checkpoint path; set to `null` to start from scratch
283
+
284
+ ##### Launch Training
285
+
286
+ For multi-machine training, please first complete the configuration in [Configure Passwordless SSH Login Between Machines](#configure-passwordless-ssh-login-between-machines-multi-machine-training) (single-machine training can skip this step).
287
+
288
+ Modify the following configuration at the beginning of `train_lf.sh`:
289
+
290
+ ```shell
291
+ export HOST_GPU_NUM=8
292
+ # IP list, comma separated. e.g. "192.168.1.1,192.168.1.2" or single node "192.168.1.1"
293
+ export IP_LIST=${IP_LIST:-"127.0.0.1"}
294
+ ```
295
+
296
+ Note: If the `IP_LIST` environment variable is not set, replace `IP_LIST` with the IP list! The format is:
297
+ ```
298
+ For a single IP:
299
+ IP_LIST=${ip_1}
300
+
301
+ For multiple IPs:
302
+ IP_LIST=${ip_1},${ip_2}
303
+
304
+ ```
305
+
306
+ Replace `${ip_1}` and `${ip_2}` with the actual IP addresses.
307
+
308
+ Then, on each machine, run `bash train_lf.sh` in the `train/llama_factory_support/` directory.
309
+
310
+ #### ms-swift Training
311
+
312
+ If you are familiar with ms-swift, you may use it for fine-tuning. All scripts, code, and configuration files are archived in the `train/ms_swift_support` directory. Unless otherwise specified, all files mentioned below are located in this directory.
313
+
314
+ ##### Installation
315
+
316
+ You can install ms-swift via pip:
317
+
318
+ ```sh
319
+ pip install ms-swift==4.2.2
320
+ ```
321
+
322
+ Or install from source: https://github.com/modelscope/ms-swift
323
+
324
+ ##### Training Scripts and Configuration Files
325
+
326
+ | Training Method | Configuration File | Launch Script |
327
+ |----------------|-------------------|---------------|
328
+ | Full Fine-tuning | `hy_v3_full_sft.yaml` | `bash sft_train.sh` |
329
+ | LoRA Fine-tuning | `hy_v3_lora_sft.yaml` | `bash sft_train.sh` |
330
+
331
+ ##### About the eos_token_id Patch
332
+
333
+ The `hy_v3_swift_patches.py` file in the directory fixes an issue with the eos token in ms-swift's default template. The default template uses the `<|hy_eos|>` string as `chat_sep` and `suffix`, which gets tokenized into multiple token IDs, causing `model.generate()` to fail to stop correctly during inference.
334
+
335
+ The patch re-registers the template using the `[['eos_token_id']]` syntax, allowing ms-swift to dynamically resolve `tokenizer.eos_token_id` at runtime and generate the correct single token.
336
+
337
+ The launch script automatically loads this patch via `--custom_register_path hy_v3_swift_patches.py`, requiring no additional action.
338
+
339
+ ##### Key Parameters
340
+
341
+ Key parameters in the configuration files are as follows:
342
+
343
+ **Model:**
344
+
345
+ - `model`: Model path, can be a HuggingFace Hub ID or a local path
346
+ - `model_type`: Model type, set to `hy_v3`
347
+ - `template`: Chat template, set to `hy_v3`
348
+ - `torch_dtype`: Data type, `bfloat16` is recommended
349
+ - `attn_impl`: Attention implementation, `flash_attn` is recommended
350
+
351
+ **Training Method:**
352
+
353
+ - `tuner_type`: Fine-tuning type; set to `full` for full fine-tuning, `lora` for LoRA fine-tuning
354
+ - `tuner_backend`: LoRA backend, set to `peft`
355
+ - `lora_rank`: LoRA rank, default `8`
356
+ - `lora_alpha`: LoRA alpha coefficient, default `16`
357
+ - `lora_dropout`: LoRA dropout ratio, default `0.05`
358
+
359
+ **Dataset:**
360
+
361
+ - `dataset`: Dataset path, supports local jsonl files (sharegpt format)
362
+ - `max_length`: Maximum sequence length; sequences exceeding this will be truncated
363
+ - `truncation_strategy`: Truncation strategy, options: `delete` (discard overlong samples) or `truncation_left`
364
+ - `lazy_tokenize`: Whether to use lazy tokenization, `true` is recommended
365
+
366
+ **Output:**
367
+
368
+ - `output_dir`: Output directory
369
+ - `save_steps`: Number of steps between saving checkpoints
370
+ - `save_total_limit`: Maximum number of checkpoints to keep
371
+ - `logging_steps`: Number of steps between logging
372
+ - `report_to`: Logging tool, options: `none`, `wandb`, `tensorboard`, `swanlab`, `mlflow`
373
+
374
+ **Training Hyperparameters:**
375
+
376
+ - `per_device_train_batch_size`: Batch size per GPU
377
+ - `gradient_accumulation_steps`: Gradient accumulation steps
378
+ - `learning_rate`: Maximum learning rate; `1.0e-5` recommended for full fine-tuning, `3.0e-4` for LoRA fine-tuning
379
+ - `num_train_epochs`: Number of training epochs
380
+ - `lr_scheduler_type`: Learning rate scheduler type, `cosine` is recommended
381
+ - `warmup_ratio`: Proportion of total training steps used for warmup
382
+ - `bf16`: Whether to use BFloat16 mixed precision training
383
+
384
+ **DeepSpeed / Optimization:**
385
+
386
+ - `deepspeed`: DeepSpeed strategy, options: `zero0`, `zero2`, `zero2_offload`, `zero3`, `zero3_offload`; `zero3_offload` recommended for full fine-tuning, `zero2_offload` for LoRA fine-tuning
387
+ - `gradient_checkpointing`: Whether to enable gradient checkpointing
388
+ - `max_grad_norm`: Gradient clipping threshold
389
+
390
+ **Other:**
391
+
392
+ - `ddp_timeout`: Distributed training timeout (milliseconds)
393
+ - `seed`: Random seed
394
+ - `resume_from_checkpoint`: Resume training from a specified checkpoint path
395
+
396
+ ##### Launch Training
397
+
398
+ For multi-machine training, please first complete the configuration in [Configure Passwordless SSH Login Between Machines](#configure-passwordless-ssh-login-between-machines-multi-machine-training) (single-machine training can skip this step).
399
+
400
+ Modify the following configuration in the `sft_train.sh` script:
401
+
402
+ ```shell
403
+ export HOST_GPU_NUM=8
404
+ # IP list, comma separated. e.g. "10.0.0.1,10.0.0.2" or single node "127.0.0.1"
405
+ export IP_LIST=${IP_LIST:-"127.0.0.1"}
406
+ ```
407
+
408
+ Then, on each machine, execute the launch script in the `train/ms_swift_support/` directory:
409
+
410
+ ```sh
411
+ # Single-machine training
412
+ bash sft_train.sh
413
+
414
+ # Multi-machine training (execute on each machine)
415
+ IP_LIST="10.0.0.1,10.0.0.2" bash sft_train.sh
416
+ ```
finetune/README_CN.md ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="left">
2
+ <a href="README.md">English</a> | 中文
3
+ </p>
4
+
5
+ # 模型训练
6
+
7
+ Hy3 提供了模型训练相关流程,您可以在此章节对训练数据格式进行处理以供模型训练使用。
8
+
9
+ ## 训练数据格式及处理
10
+
11
+ **Hy3 同时支持慢思考与快思考两种模式,模型的默认输出是慢思考模式,若想让模型进行快思考,可通过 `reasoning_effort` 参数控制(可选值:`high`、`low`、`no_think`)。**
12
+
13
+ 训练数据按照以下形式处理为 messages 格式,训练和推理的默认 system prompt 为空,可以根据自己的需求进行设定。
14
+
15
+ ```python
16
+ # Fast thinking pattern (no_think)
17
+ {"reasoning_effort": "no_think", "messages": [{"content": "你是一个有用的人工智能助手。\n现在的时间是2026-01-01 13:26:12 周四", "role": "system"}, {"content": "1+1=?", "role": "user"}, {"role": "assistant", "content": "1+1=2"}]}
18
+
19
+ # Slow thinking pattern (high)
20
+ {"reasoning_effort": "high", "messages": [{"content": "你是一个有用的人工智能助手。\n现在的时间是2026-01-01 13:26:12 周四", "role": "system"}, {"content": "1+1=?", "role": "user"}, {"role": "assistant", "content": "1+1=2", "reasoning_content": "用户问的是1+1等于多少。在基本的十进制算术中,1+1等于2。"}]}
21
+
22
+ from transformers import AutoTokenizer
23
+ tokenizer = AutoTokenizer.from_pretrained("./models", use_fast=False, trust_remote_code=True)
24
+ ids = tokenizer.apply_chat_template(messages, is_training=True)
25
+ ```
26
+
27
+ ## 权重格式转换
28
+
29
+ Hy3 的原始 checkpoint 采用每个 expert 独立存储的格式,在训练前建议转换为 expert 融合后的 HuggingFace 标准格式(将同一层的多个 expert 权重融合为 3D 张量,并统一 key 命名),用于提高加载和训练的速率。不进行转换也可以直接使用原始格式进行训练,但加载速度会较慢。我们提供了转换脚本 `convert_ckpt_to_outer.py` 和校验脚本 `check_converted.py`,位于 `train/tools` 目录下。
30
+
31
+ ### 转换
32
+
33
+ ```sh
34
+ python convert_ckpt_to_outer.py \
35
+ --input_dir <原始checkpoint目录> \
36
+ --output_dir <输出目录> \
37
+ --workers 8
38
+ ```
39
+
40
+ **参数说明:**
41
+
42
+ - `--input_dir`:原始 checkpoint 目录路径(必选)
43
+ - `--output_dir`:转换后的 checkpoint 输出目录路径(必选)
44
+ - `--workers`:并行转换的进程数,默认为 8(可选)
45
+
46
+ 转换脚本会执行以下步骤:
47
+ 1. 预扫描 `model.safetensors.index.json`,检测跨 shard 的 expert 分组
48
+ 2. 逐 shard 并行转换权重(key 重命名 + expert 融合)
49
+ 3. 后处理跨 shard 的 expert 分组(合并来自多个 shard 的数据)
50
+ 4. 复制 `config.json`、tokenizer 等其他文件
51
+ 5. 重建 `model.safetensors.index.json`
52
+
53
+ ### 校验
54
+
55
+ 转换完成后,建议使用校验脚本验证转换结果的完整性:
56
+
57
+ ```sh
58
+ python check_converted.py <转换后的checkpoint目录> --spot-check 3
59
+ ```
60
+
61
+ **参数说明:**
62
+
63
+ - 第一个参数:转换后的 checkpoint 目录路径(必选)
64
+ - `--spot-check`:随机抽检的 shard 文件数量,会加载 tensor 并检查 shape、dtype、NaN/Inf 等,默认为 3(可选)
65
+
66
+ 校验脚本会检查以下内容:
67
+ 1. `config.json` 的完整性
68
+ 2. `model.safetensors.index.json` 中所有预期 key 是否齐全(包括常规层和 MTP 层)
69
+ 3. 所有引用的 shard 文件是否存在且非空
70
+ 4. 抽检 shard 文件中 tensor 的 shape、dtype 是否正确,是否存在 NaN/Inf
71
+ 5. 检测孤立的空 shard 文件(跨 shard 合并残留,可安全删除)
72
+
73
+ ## 快速开始
74
+
75
+ 您可以参照快速开始文档中的内容进行快速上手。
76
+
77
+ ## 模型训练
78
+
79
+ ### 硬件需求
80
+
81
+ 经过测试,不开 make_moe_param_leaf_module 以及 zero3+offload,max_seq_length 为 4096:
82
+
83
+ - **LoRA 微调**:最少需要单机 8 卡(显存至少 80GB)。
84
+ - **全量微调**:最少需要 4 机 32 卡(显存至少 80GB)。
85
+
86
+ ### 配置机器间免密 ssh 登录(多机训练)
87
+
88
+ > 如果只使用单机训练,可跳过本节。
89
+
90
+ 以下操作以两个机器为例,两台机器的 ip 分别以`${ip1}`和`${ip2}`标识,以下操作均在 docker container 内执行。
91
+
92
+ 首先,配置多机container免密,在每台机器上执行。
93
+
94
+ ```sh
95
+ ssh-keygen # 生成id_rsa和id_rsa.pub,用于免密登录
96
+ ssh-keygen -t rsa -A # 生成/etc/ssh/ssh_host_rsa_key和ssh_host_ecdsa_key, 用于后面启动ssh listen
97
+ /usr/sbin/sshd -p 36005 -o ListenAddress=0.0.0.0 # 启动 SSH 监听
98
+ echo "Port 36005" > ~/.ssh/config # ssh 连接端口修改为 36005
99
+ passwd root # 需要配置root密码,否则监测平台会报警
100
+ ```
101
+
102
+ 注意:这里的`36005`是一个示例端口,可以选用任意端口,但需要保证使用的端口**开放**且**不被其他的进程占用**。
103
+
104
+ 接下来,在每台机器的 container 内,执行:
105
+
106
+ ```sh
107
+ cat ~/.ssh/id_rsa.pub
108
+ ```
109
+
110
+ **将输出的 ssh 公钥复制并粘贴到`~/.ssh/authorized_keys`文件中,每行一个公钥,每台机器上都要做这个操作**。最终每台机器上的`~/.ssh/authorized_keys`文件内容应当是一致的,并且包含了所有机器的公钥。
111
+
112
+ 需要注意,多节点训练时,每个节点上执行的代码都得一致,建议挂载一个共享的网络盘,如果无法挂载共享网盘,则需要手动将数据集、脚本、代码复制在多台机器的相同目录下。
113
+
114
+ ### 启动方式
115
+
116
+ 本项目提供三种训练方式,您可以根据需求选择:
117
+
118
+ - **DeepSpeed 原生训练**(基于 HuggingFace Transformers Trainer):位于 `train/deepspeed_support` 目录下
119
+ - **LLaMA-Factory 训练**:位于 `train/llama_factory_support` 目录下
120
+ - **ms-swift 训练**:位于 `train/ms_swift_support` 目录下
121
+
122
+ #### DeepSpeed 原生训练
123
+
124
+ 参考:[HuggingFace Transformers Trainer](https://huggingface.co/docs/transformers/main/en/main_classes/trainer)
125
+
126
+ ##### 单机启动训练
127
+
128
+ 在 `train/deepspeed_support` 目录下,执行:
129
+
130
+ ```sh
131
+ pip install -r requirements.txt
132
+ bash train.sh
133
+ ```
134
+
135
+ ##### 多机启动训练
136
+
137
+ 如果要用多台机器启动训练,请先完成 [配置机器间免密 ssh 登录](#配置机器间免密-ssh-登录多机训练) 中的配置,并保证多台机器在一个集群内。
138
+
139
+ 确认依赖已经安装完成(如未安装,请执行`pip install -r requirements.txt`安装),然后在`train.sh`中的开头增加以下配置:
140
+
141
+ ```shell
142
+ export HOST_GPU_NUM=8
143
+ # IP list, comma separated. e.g. "192.168.1.1,192.168.1.2" or single node "192.168.1.1"
144
+ IP_LIST=${IP_LIST:-"127.0.0.1"}
145
+ ```
146
+
147
+ 注意:如果`IP_LIST`环境变量未设置,则将`IP_LIST`替换为IP列表!格式为:
148
+ ```
149
+ 如果只有一个IP:
150
+ IP_LIST=${ip_1}
151
+
152
+ 如果有多个IP:
153
+ IP_LIST=${ip_1},${ip_2}
154
+
155
+ ```
156
+
157
+ 请将`${ip_1}`和`${ip_2}`替换为真实的IP地址。
158
+
159
+ 然后,在`${ip1}`的机器上,在`train/deepspeed_support/`目录下,执行`bash train.sh`即可,注意第一次启动时可能会看见以下的输出:
160
+
161
+ ```ssh
162
+ The authenticity of host '[ip]:36005 ([ip]:36005)' can't be established.
163
+ ECDSA key fingerprint is xxxxxx.
164
+ ECDSA key fingerprint is MD5:xxxxxx.
165
+ Are you sure you want to continue connecting (yes/no)?
166
+ ```
167
+
168
+ 此时输入`yes`即可继续。
169
+
170
+ ##### 关键参数
171
+
172
+ 脚本中的关键参数如下:
173
+
174
+ - `--deepspeed`: 此参数应当指向一个 deepspeed 的配置文件,`train/deepspeed_support`文件夹下提供了三种 DeepSpeed 的默认配置文件:`ds_zero2_no_offload.json`, `ds_zero3_no_offload.json`, `ds_zero3_offload.json`,这三个配置文件所需显存依次减少
175
+ - `--model_name_or_path`: 要加载的 Hy3 的 HF 预训练模型权重,否则无法加载
176
+ - `--tokenizer_name_or_path`: tokenizer 文件夹路径, 否则无法加载
177
+ - `--train_data_file`: 训练文件路径,应该为一个 jsonl 文件
178
+ - `--output_dir`: 输出文件夹,log、tensorboard 和权重都会存储在这个路径下
179
+ - `--per_device_train_batch_size`: 每张卡上的 batch size
180
+ - `--gradient_accumulation_steps`: 梯度累计次数,`per_device_train_batch_size * gradient_accumulation_steps * dp_size`为 global_batch_size
181
+ - `--max_steps`: 训练的总步数
182
+ - `--save_steps`: 每多少个 step 存储一个 checkpoint
183
+ - `--use_lora`: 是否用 lora 训练,同时接收`--lora_rank`,`--lora_alpha`和`--lora_dropout`参数。lora 默认应用于 "q_proj", "k_proj", "v_proj", "o_proj" 四个参数,如果需要改变的话在代码中修改即可。注意:**使用 lora 训练时,只会保存 lora 的权重,而不会保存 base 模型的权重**,如果需要合并 lora 权重,看下面的"Lora 权重合并"一节
184
+ - `--make_moe_param_leaf_module`:当用 zero3 以及 MoE 训练时,将 MoE 模块视作一个 leaf module,即它的参数不进行 zero3 切分,这个选项预计会显著增加显存占用
185
+ - `--gradient_checkpointing`:开启梯度重计算
186
+ - `--train_attention_params_only`: 是否只训练 attention 参数
187
+ - `--learning_rate`: 训练时的最大学习率
188
+ - `--min_lr`: 训练时的最小学习率
189
+ - `--use_flash_attn`: 开启 flash-attention 进行训练加速
190
+
191
+ **注意:**
192
+
193
+ - 如果想从一个中途保存的 ckpt 继续训练,而不是加载一个预训练的权重,直接指定`--resume_from_checkpoint`为之前训练保存的 ckpt 路径,不要指定`--model_name_or_path`,这样只会加载权重,而不会加载训练状态
194
+ - 从 ckpt 继续训练时,loss 可能会有微小的偏差,这是由一些非确定性算法带来的随机性,是正常现象。参考:[HuggingFace Transformers Trainer Randomness](https://huggingface.co/docs/transformers/main/en/main_classes/trainer#randomness)
195
+ - 当 `--model_name_or_path` 有效时,所有模型相关的参数都会被忽略
196
+ - 一个 batch 内的样本会通过 padding 对齐 batch 内最长的样本,而每条样本的长度最长为 max_seq_length,超出的部分会被裁剪
197
+ - 如果报出 bias 权重没有 load 的 warning,忽略即可,Hunyuan-Large 中不会用到 bias
198
+
199
+ ##### 显存不足怎么办?
200
+
201
+ 参考:[DeepSpeed Configuration](https://www.deepspeed.ai/docs/config-json/)
202
+
203
+ 可以尝试修改 ds config,去掉这几个参数的 auto 属性,改小试试看:
204
+
205
+ - `stage3_param_persistence_threshold`
206
+ - `stage3_prefetch_bucket_size`
207
+ - `stage3_max_reuse_distance`
208
+
209
+ ##### Lora 模型合并
210
+
211
+ 保存下来的 lora 权重没法在训练运行时合并到 zero3 模型中,因为 zero3 开启时模型权重会切分到各 dp rank 上。因此如果想把 lora 权重合并到 base 模型上,可以通过离线的方式合并后得到权重文件。执行`merge_lora_weight.sh`即可完成 lora 权重和 base 模型权重的合并,其中的参数有:
212
+
213
+ - `--base_model_path`:base 模型的权重目录
214
+ - `--adapter_model_path`:lora 权重目录
215
+ - `--output_path`:合并后的权重保存目录
216
+ - `--save_dtype`: 以什么数据格式存储合并后的权重,可选值:fp16,bf16,fp32
217
+
218
+ #### LLaMA-Factory 训练
219
+
220
+ 如果对 LLaMA-Factory 较为熟悉,可使用 LLaMA-Factory 进行微调。脚本、代码以及配置文件都归档在 `train/llama_factory_support` 目录下。如果没有特别说明,接下来我们提到的文件都是该目录下的文件。
221
+
222
+ ##### 安装
223
+
224
+ 可以通过下载源码 https://github.com/hiyouga/LLaMA-Factory/tree/main ,根据网站的指引进行安装。
225
+
226
+ ##### 配置文件
227
+
228
+ 我们提供了 llama-factory 的训练示例配置文件 `hy_v3_lora_sft.yaml`和`hy_v3_full_sft.yaml`文件,分别对应 LoRA 训练和全量微调。
229
+
230
+ 脚本中的关键参数如下:
231
+
232
+ **模型相关:**
233
+
234
+ - `model_name_or_path`: Hy3 HF 格式预训练模型权重路径
235
+ - `trust_remote_code`: 是否信任远程代码, Hy3 需要设置为 `true`
236
+
237
+ **训练方法:**
238
+
239
+ - `stage`: 训练阶段, 当前为 `sft`(监督微调)
240
+ - `finetuning_type`: 微调类型, 可选 `full`(全量微调) 或 `lora`(LoRA 微调)
241
+ - `deepspeed`: DeepSpeed 配置文件路径, 全量微调推荐 `ds_zero3_offload.json`, LoRA 微调推荐 `ds_zero2_offload_lora.json`
242
+
243
+ **LoRA 参数(仅 LoRA 微调时生效):**
244
+
245
+ - `lora_rank`: LoRA 秩, 默认 `64`
246
+ - `lora_alpha`: LoRA alpha 系数, 默认 `128`
247
+ - `lora_dropout`: LoRA dropout 比率, 默认 `0.05`
248
+ - `lora_target`: LoRA 应用的目标模块, 默认为 `q_proj,k_proj,v_proj,o_proj`
249
+
250
+ **数据集:**
251
+
252
+ - `dataset_dir`: 数据集目录路径
253
+ - `dataset`: 数据集名称, 需要在 `dataset_dir` 下的 `dataset_info.json` 中注册
254
+ - `template`: 对话模板, Hy3 使用 `hy_v3`
255
+ - `cutoff_len`: 最大序列长度, 超出部分会被截断; 全量微调可设为 `262144`(262K), LoRA 微调建议设为 `8192` 以节省显存
256
+ - `max_samples`: 每个数据集最多使用的样本数
257
+ - `overwrite_cache`: 是否覆盖已缓存的预处理数据集
258
+
259
+ **输出:**
260
+
261
+ - `output_dir`: 输出目录, 日志、TensorBoard 和权重都会存储在此路径下
262
+ - `logging_steps`: 每多少步记录一次日志
263
+ - `save_steps`: 每多少步保存一次 checkpoint
264
+ - `plot_loss`: 是否绘制训练 loss 曲线
265
+ - `overwrite_output_dir`: 是否覆盖已有的输出目录
266
+ - `save_only_model`: 是否只保存模型权重(不保存优化器状态等)
267
+ - `report_to`: 日志上报工具, 可选 `none`, `wandb`, `tensorboard`, `swanlab`, `mlflow`
268
+
269
+ **训练超参数:**
270
+
271
+ - `per_device_train_batch_size`: 每张卡上的 batch size
272
+ - `gradient_accumulation_steps`: 梯度累积步数, `per_device_train_batch_size * gradient_accumulation_steps * dp_size` 为 global batch size
273
+ - `learning_rate`: 最大学习率, 全量微调推荐 `1.0e-5`, LoRA 微调推荐 `2.0e-4`
274
+ - `num_train_epochs`: 训练轮数
275
+ - `lr_scheduler_type`: 学习率调度器类型, 推荐使用 `cosine_with_min_lr`
276
+ - `lr_scheduler_kwargs.min_lr_rate`: 最小学习率与最大学习率的比值, 例如 `0.1` 表示最小学习率为最大学习率的 10%
277
+ - `warmup_ratio`: 预热阶段占总训练步数的比例
278
+ - `bf16`: 是否使用 BFloat16 混合精度训练
279
+ - `gradient_checkpointing`: 是否开启梯度重计算以节省显存
280
+ - `ddp_timeout`: 分布式训练超时时间(毫秒)
281
+ - `flash_attn`: 注意力实现方式, 推荐 `fa2`(FlashAttention-2), 也可选 `sdpa`; 使用 `fa2` 需要安装 flash-attn 包
282
+ - `resume_from_checkpoint`: 从指定 checkpoint 路径恢复训练, 设为 `null` 表示从头开始训练
283
+
284
+ ##### 启动训练
285
+
286
+ 如需多机训练,请先完成 [配置机器间免密 ssh 登录](#配置机器间免密-ssh-登录多机训练) 中的配置(单机训练可跳过此步骤)。
287
+
288
+ 修改`train_lf.sh`中开头的以下配置:
289
+
290
+ ```shell
291
+ export HOST_GPU_NUM=8
292
+ # IP list, comma separated. e.g. "192.168.1.1,192.168.1.2" or single node "192.168.1.1"
293
+ export IP_LIST=${IP_LIST:-"127.0.0.1"}
294
+ ```
295
+
296
+ 注意:如果`IP_LIST`环境变量未设置,则将`IP_LIST`替换为IP列表!格式为:
297
+ ```
298
+ 如果只有一个IP:
299
+ IP_LIST=${ip_1}
300
+
301
+ 如果有多个IP:
302
+ IP_LIST=${ip_1},${ip_2}
303
+
304
+ ```
305
+
306
+ 请将`${ip_1}`和`${ip_2}`替换为真实的IP地址。
307
+
308
+ 然后,在每一台机器上,在`train/llama_factory_support/`目录下执行`bash train_lf.sh`。
309
+
310
+ #### ms-swift 训练
311
+
312
+ 如果对 ms-swift 较为熟悉,可使用 ms-swift 进行微调。脚本、代码以及配置文件都归档在 `train/ms_swift_support` 目录下。如果没有特别说明,接下来我们提到的文件都是该目录下的文件。
313
+
314
+ ##### 安装
315
+
316
+ 可以通过 pip 安装 ms-swift:
317
+
318
+ ```sh
319
+ pip install ms-swift==4.2.2
320
+ ```
321
+
322
+ 或从源码安装:https://github.com/modelscope/ms-swift
323
+
324
+ ##### 训练脚本与配置文件
325
+
326
+ | 训练方式 | 配置文件 | 启动脚本 |
327
+ |---------|---------|---------|
328
+ | 全量微调 | `hy_v3_full_sft.yaml` | `bash sft_train.sh` |
329
+ | LoRA 微调 | `hy_v3_lora_sft.yaml` | `bash sft_train.sh` |
330
+
331
+ ##### 关于 eos_token_id Patch
332
+
333
+ 目录下的 `hy_v3_swift_patches.py` 文件用于修复 ms-swift 默认模板中 eos token 的问题。默认模板将 `<|hy_eos|>` 字符串作为 `chat_sep` 和 `suffix`,该字符串会被 tokenize 为多个 token ID,导致推理时 `model.generate()` 无法正确停止。
334
+
335
+ Patch 通过 `[['eos_token_id']]` 语法重新注册模板,使 ms-swift 在运行时动态解析 `tokenizer.eos_token_id`,生成正确的单个 token。
336
+
337
+ 启动脚本已通过 `--custom_register_path hy_v3_swift_patches.py` 自动加载此 patch,无需额外操作。
338
+
339
+ ##### 关键参数
340
+
341
+ 配置文件中的关键参数如下:
342
+
343
+ **模型相关:**
344
+
345
+ - `model`: 模型路径,可以是 HuggingFace Hub ID 或本地路径
346
+ - `model_type`: 模型类型,设为 `hy_v3`
347
+ - `template`: 对话模板,设为 `hy_v3`
348
+ - `torch_dtype`: 数据类型,推荐 `bfloat16`
349
+ - `attn_impl`: 注意力实现,推荐 `flash_attn`
350
+
351
+ **训练方法:**
352
+
353
+ - `tuner_type`: 微调类型,全量微调设为 `full`,LoRA 微调设为 `lora`
354
+ - `tuner_backend`: LoRA 后端,设为 `peft`
355
+ - `lora_rank`: LoRA 秩,默认 `8`
356
+ - `lora_alpha`: LoRA alpha 系数,默认 `16`
357
+ - `lora_dropout`: LoRA dropout 比率,默认 `0.05`
358
+
359
+ **数据集:**
360
+
361
+ - `dataset`: 数据集路径,支持本地 jsonl 文件(sharegpt 格式)
362
+ - `max_length`: 最大序列长度,超出部分会被截断
363
+ - `truncation_strategy`: 截断策略,可选 `delete`(丢弃超长样本)或 `truncation_left`
364
+ - `lazy_tokenize`: 是否延迟 tokenize,推荐 `true`
365
+
366
+ **输出:**
367
+
368
+ - `output_dir`: 输出目录
369
+ - `save_steps`: 每多少步保存一次 checkpoint
370
+ - `save_total_limit`: 最多保留的 checkpoint 数量
371
+ - `logging_steps`: 每多少步记录一次日志
372
+ - `report_to`: 日志上报工具,可选 `none`, `wandb`, `tensorboard`, `swanlab`, `mlflow`
373
+
374
+ **训练超参数:**
375
+
376
+ - `per_device_train_batch_size`: 每张卡上的 batch size
377
+ - `gradient_accumulation_steps`: 梯度累积步数
378
+ - `learning_rate`: 最大学习率,全量微调推荐 `1.0e-5`,LoRA 微调推荐 `3.0e-4`
379
+ - `num_train_epochs`: 训练轮数
380
+ - `lr_scheduler_type`: 学习率调度器类型,推荐 `cosine`
381
+ - `warmup_ratio`: 预热阶段占总训练步数的比例
382
+ - `bf16`: 是否使用 BFloat16 混合精度训练
383
+
384
+ **DeepSpeed / 优化:**
385
+
386
+ - `deepspeed`: DeepSpeed 策略,可选 `zero0`, `zero2`, `zero2_offload`, `zero3`, `zero3_offload`;全量微调推荐 `zero3_offload`,LoRA 微调推荐 `zero2_offload`
387
+ - `gradient_checkpointing`: 是否开启梯度重计算
388
+ - `max_grad_norm`: 梯度裁剪阈值
389
+
390
+ **其他:**
391
+
392
+ - `ddp_timeout`: 分布式训练超时时间(毫秒)
393
+ - `seed`: 随机种子
394
+ - `resume_from_checkpoint`: 从指定 checkpoint 路径恢复训练
395
+
396
+ ##### 启动训练
397
+
398
+ 如需多机训练,请先完成 [配置机器间免密 ssh 登录](#配置机器间免密-ssh-登录多机训练) 中的配置(单机训练可跳过此步骤)。
399
+
400
+ 修改 `sft_train.sh` 脚本中的以下配置:
401
+
402
+ ```shell
403
+ export HOST_GPU_NUM=8
404
+ # IP list, comma separated. e.g. "10.0.0.1,10.0.0.2" or single node "127.0.0.1"
405
+ export IP_LIST=${IP_LIST:-"127.0.0.1"}
406
+ ```
407
+
408
+ 然后,在每一台机器上,在 `train/ms_swift_support/` 目录下执行启动脚本:
409
+
410
+ ```sh
411
+ # 单机训练
412
+ bash sft_train.sh
413
+
414
+ # 多机训练(在每台机器上执行)
415
+ IP_LIST="10.0.0.1,10.0.0.2" bash sft_train.sh
416
+ ```
finetune/data/example_data.jsonl ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "能否帮我撰写一个关于环保的议论文开头部分?\n主题:环保\n立场:重要性和我们的责任"}, {"role": "assistant", "content": "在我们的生活中,环保已经成为一个不可忽视的议题。随着科技的发展和人口的增长,我们的环境正在遭受前所未有的压力。森林被砍伐,河流被污染,动植物种类正在消失,全球气候正在发生变化。这些都是我们无法忽视的事实。然而,环保并不仅仅是一个环境问题,它更是一个关乎我们生存和发展的问题。环保的重要性已经超越了任何一个单一的领域,它关乎我们的健康、经济、社会和文化。因此,我们每一个人都有责任和义务去保护我们的环境,这不仅是为了我们自己,也是为了我们的后代和整个地球。"}]}
2
+ {"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "请描述小红书APP的用户画像,并用表格的形式展示结果。"}, {"role": "assistant", "content": "| 用户特征 | 描述 |\n| -------- | ------------------------------ |\n| 年龄段 | 18-35岁 |\n| 性别 | 女性用户较多(约占70%) |\n| 地域 | 一线城市和二线城市用户占比较高 |\n| 职业 | 白领、创业者、自由职业者等 |\n| 爱好 | 美容、时尚、旅行、美食、健康、生活方式等 |\n| 消费能力 | 中高消费能力用户 |\n| 使用目的 | 阅读内容,分享购物心得,结识相似兴趣的朋友 |\n\n注:以上用户画像仅供参考,实际情况可能会有所不同。"}]}
3
+ {"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "请根据以下两个句子的描述,判断它们是否对同一情况进行了描述。句子1:“外面下着大雨,人们纷纷打起了伞。” 句子2:“由于雨量较大,人们都撑起了雨伞。”"}, {"role": "assistant", "content": "是的,这两个句子描述的是同一情况。"}]}
4
+ {"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "你认为游戏开发者应该如何处理游戏中的社交网络集成?"}, {"role": "assistant", "content": "游戏开发者在处理游戏中的社交网络集成时可以考虑以下几点:\n\n1. 以用户为中心:确保集成社交网络功能是为了提高用户体验,让玩家更容易地与朋友互动、分享成就和游戏进度。\n\n2. 保护隐私:在集成社交网络时,务必保护用户的隐私。在收集和使用用户数据时,需要遵循相关法规和用户协议,同时提供明确的隐私政策。\n\n3. 适度推送:在推送游戏动态、邀请和通知时要适度,避免过度打扰用户或侵犯用户隐私。可提供自定义设置,让玩家选择性地接收通知。\n\n4. 社交功能设计:在游戏中加入社交功能,如聊天、组队、比赛等,以增进玩家之间的互动。同时,确保这些功能易用且不影响游戏体验。\n\n5. 社区建设:鼓励玩家在社交平台上建立游戏相关的社区,分享游戏心得、攻略和经验。这将有助于增加游戏的知名度和吸引力。\n\n6. 奖励机制:设定奖励机制,鼓励玩家在社交网络上分享游戏内容、邀请好友等。但要确保奖励机制不会破坏游戏平衡。\n\n7. 持续更新和优化:根据玩家的反馈,不断优化社交网络集成功能,确保其与游戏的其他方面保持一致。\n\n总之,游戏开发者应该在保护用户隐私的前提下,提供便捷的社交网络集成功能,以提高游戏的互动性和吸引力。同时,要关注玩家的反馈,不断优化功能。"}]}
5
+ {"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "将这段过去进行时的文字改为现在完成进行时:“她们在公园里跳舞。”"}, {"role": "assistant", "content": "她们已经在公园里跳舞了。"}]}
6
+ {"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "在godaddy上买了一台服务器怎么更改root账户"}, {"role": "assistant", "content": "在GoDaddy上购买的服务器可能是Linux系统的VPS或者是专用服务器。要更改root账户,你可以通过SSH连接到服务器并修改密码。以下是具体步骤:\n\n1. 打开终端(Terminal)或使用SSH客户端(如PuTTY)。\n2. 使用SSH连接到服务器。在终端中输入以下命令:\n\n ```\n ssh root@your_server_ip_address\n ```\n\n 将`your_server_ip_address`替换为你的服务器IP地址。如果需要,你可能还需要输入服务器的SSH端口号。\n \n3. 当系统提示输入密码时,输入你的当前root密码。如果连接成功,你将看到服务器的命令提示符。\n\n4. 要更改root密码,请输入以下命令:\n\n ```\n passwd\n ```\n\n5. 系统将提示你输入新的UNIX密码。输入你想要设置的新密码并按回车键。\n\n6. 系统将要求你重新输入新密码以进行确认。再次输入新密码并按回车键。\n\n7. 如果密码更改成功,系统将显示“密码已成功更新”的消息。\n\n8. 使用`exit`命令断开与服务器的连接。\n\n现在你已经成功更改了root账户的密码。下次使用SSH连接到服务器时,请使用新的密码进行身份验证。"}]}
7
+ {"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "PHP算法逻辑:如何计算年龄?"}, {"role": "assistant", "content": "要计算年龄,您可以使用以下PHP算法。此算法考虑了生日、当前日期以及闰年的计算。\n\n```php\nfunction calculateAge($birthdate) {\n // 格式为YYYY-MM-DD的生日\n $birthdate = strtotime($birthdate);\n \n // 获取当前日期\n $now = time();\n\n // 计算年龄\n $age = date('Y', $now) - date('Y', $birthdate);\n \n // 如果还没有过生日,减去一年\n if (date('m-d', $now) < date('m-d', $birthdate)) {\n $age--;\n }\n\n return $age;\n}\n\n// 使用\n$birthdate = \"2000-01-01\";\necho \"年龄是: \" . calculateAge($birthdate) . \" 岁\";\n```\n\n这个函数将生日作为输入,输出用户的年龄。首先,它将生日转换为时间戳,然后获取当前的时间戳。接下来,它计算年份之间的差异。如果当前日期仍未超过这一年的生日,这个函数将年龄减少1。\n\n在使用函数时,只需传入出生日期即可,函数会返回相应的年龄。"}]}
8
+ {"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "给我解释一下什么是胰岛素抵抗? 请详细描述胰岛素抵抗的症状、原因与治疗方法。 胰岛素抵抗会影响一个人的日常生活吗? 有哪些生活习惯改变可以帮助改善胰岛素抵抗?"}, {"role": "assistant", "content": "胰岛素抵抗是指身体对胰岛素的敏感性降低,胰岛素不能有效地发挥作用,导致血糖无法正常进入身体细胞。胰岛素是一种激素,由胰腺产生,其主要作用是帮助身体细胞吸收血糖并将其转化为能量。当胰岛素抵抗发生时,胰腺会分泌更多的胰岛素以维持正常的血糖水平,但长期的胰岛素抵抗可能导致2型糖尿病和其他健康问题的发生。\n\n胰岛素抵抗的症状可能包括:\n\n1. 高血糖:由于胰岛素不能正常工作,血糖无法进入细胞,导致血糖升高。\n2. 疲劳:由于身体无法有效利用血糖产生能量,患者可能会感到疲劳无力。\n3. 肥胖:胰岛素抵抗可能导致过量的胰岛素分泌,使得脂肪存储增加,进而导致体重增加。\n4. 高血压:胰岛素抵抗与高血压之间存在关联,可能导致血压升高。\n5. 黑棘皮病:皮肤出现黑色、粗糙的皮疹,通常出现在颈部、腋窝等皮肤皱褶处。\n\n胰岛素抵抗的原因可能包括:\n\n1. 遗传因素:遗传易感基因可能会增加一个人患胰岛素抵抗的风险。\n2. 肥胖:过重或肥胖会增加患胰岛素抵抗的风险。\n3. 缺乏运动:缺乏运动可能会导致肌肉对胰岛素的敏感性降低。\n4. 高糖饮食:长期摄入高糖食品可能导致胰岛素抵抗。\n5. 睡眠不足:长期睡眠不足可能会影响胰岛素的正常功能。\n\n治疗胰岛素抵抗的方法主要包括生活方式的改变和药物治疗:\n\n1. 减轻体重:通过改善饮食和增加运动来减轻体重,有助于提高身体对胰岛素的敏感性。\n2. 增加运动:增加有氧运动和力量训练,有助于提高肌肉对胰岛素的敏感性。\n3. 改善饮食:摄入低糖、高纤维、富含全谷物和蔬菜水果的食物,有助于控制血糖和减轻体重。\n4. 药物治疗:在必要时,医生可能会开处降糖药物,如二甲双胍,以帮助提高胰岛素敏感性。\n\n胰岛素抵抗会影响一个人的日常生活,包括疲劳、体重增加、血糖波动等问题。通过改变生活习惯,如减轻体重、增加运动、改善饮食和保证充足睡眠,可以帮助改善胰岛素抵抗,降低患2型糖尿病的风险。"}]}
finetune/deepspeed_support/ds_zero2_no_offload.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fp16": {
3
+ "enabled": "auto",
4
+ "loss_scale": 0,
5
+ "loss_scale_window": 100,
6
+ "initial_scale_power": 16,
7
+ "hysteresis": 2,
8
+ "min_loss_scale": 1e-10
9
+ },
10
+ "zero_optimization": {
11
+ "stage": 2,
12
+ "allgather_partitions": true,
13
+ "allgather_bucket_size": 1e8,
14
+ "overlap_comm": true,
15
+ "reduce_scatter": true,
16
+ "reduce_bucket_size": 1e8,
17
+ "contiguous_gradients": true
18
+ },
19
+ "gradient_accumulation_steps": "auto",
20
+ "gradient_clipping": "auto",
21
+ "steps_per_print": 10,
22
+ "train_batch_size": "auto",
23
+ "train_micro_batch_size_per_gpu": "auto",
24
+ "wall_clock_breakdown": false
25
+ }
finetune/deepspeed_support/ds_zero2_offload.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fp16": {
3
+ "enabled": "auto",
4
+ "loss_scale": 0,
5
+ "loss_scale_window": 1000,
6
+ "initial_scale_power": 16,
7
+ "hysteresis": 2,
8
+ "min_loss_scale": 1
9
+ },
10
+ "bf16": {
11
+ "enabled": "auto"
12
+ },
13
+
14
+ "zero_optimization": {
15
+ "stage": 2,
16
+ "offload_optimizer": {
17
+ "device": "cpu",
18
+ "pin_memory": true
19
+ },
20
+ "allgather_partitions": true,
21
+ "allgather_bucket_size": 5e8,
22
+ "overlap_comm": true,
23
+ "reduce_scatter": true,
24
+ "reduce_bucket_size": 5e8,
25
+ "contiguous_gradients": true
26
+ },
27
+
28
+ "gradient_accumulation_steps": "auto",
29
+ "gradient_clipping": "auto",
30
+ "steps_per_print": 10,
31
+ "train_batch_size": "auto",
32
+ "train_micro_batch_size_per_gpu": "auto",
33
+ "wall_clock_breakdown": false
34
+ }
finetune/deepspeed_support/ds_zero3_no_offload.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fp16": {
3
+ "enabled": "auto",
4
+ "loss_scale": 0,
5
+ "loss_scale_window": 1000,
6
+ "initial_scale_power": 16,
7
+ "hysteresis": 2,
8
+ "min_loss_scale": 1
9
+ },
10
+ "bf16": {
11
+ "enabled": "auto"
12
+ },
13
+
14
+ "zero_optimization": {
15
+ "stage": 3,
16
+ "offload_optimizer": {
17
+ "device": "none",
18
+ "pin_memory": true
19
+ },
20
+ "offload_param": {
21
+ "device": "none",
22
+ "pin_memory": true
23
+ },
24
+ "overlap_comm": true,
25
+ "contiguous_gradients": true,
26
+ "sub_group_size": 1e9,
27
+ "reduce_bucket_size": "auto",
28
+ "stage3_prefetch_bucket_size": "auto",
29
+ "stage3_param_persistence_threshold": "auto",
30
+ "stage3_max_live_parameters": 1e9,
31
+ "stage3_max_reuse_distance": 1e9,
32
+ "stage3_gather_16bit_weights_on_model_save": true
33
+ },
34
+
35
+ "gradient_accumulation_steps": "auto",
36
+ "gradient_clipping": "auto",
37
+ "steps_per_print": 10,
38
+ "train_batch_size": "auto",
39
+ "train_micro_batch_size_per_gpu": "auto",
40
+ "wall_clock_breakdown": false
41
+ }
finetune/deepspeed_support/ds_zero3_offload.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fp16": {
3
+ "enabled": "auto",
4
+ "loss_scale": 0,
5
+ "loss_scale_window": 1000,
6
+ "initial_scale_power": 16,
7
+ "hysteresis": 2,
8
+ "min_loss_scale": 1
9
+ },
10
+ "bf16": {
11
+ "enabled": "auto"
12
+ },
13
+
14
+ "zero_optimization": {
15
+ "stage": 3,
16
+ "offload_optimizer": {
17
+ "device": "cpu",
18
+ "pin_memory": true
19
+ },
20
+ "offload_param": {
21
+ "device": "cpu",
22
+ "pin_memory": true
23
+ },
24
+ "overlap_comm": true,
25
+ "contiguous_gradients": true,
26
+ "sub_group_size": 1e9,
27
+ "reduce_bucket_size": "auto",
28
+ "stage3_prefetch_bucket_size": "auto",
29
+ "stage3_param_persistence_threshold": "auto",
30
+ "stage3_max_live_parameters": 1e9,
31
+ "stage3_max_reuse_distance": 1e9,
32
+ "stage3_gather_16bit_weights_on_model_save": false
33
+ },
34
+
35
+ "gradient_accumulation_steps": "auto",
36
+ "gradient_clipping": "auto",
37
+ "steps_per_print": 10,
38
+ "train_batch_size": "auto",
39
+ "train_micro_batch_size_per_gpu": "auto",
40
+ "wall_clock_breakdown": false
41
+ }
finetune/deepspeed_support/ds_zero3_optimizer_offload.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fp16": {
3
+ "enabled": "auto",
4
+ "loss_scale": 0,
5
+ "loss_scale_window": 1000,
6
+ "initial_scale_power": 16,
7
+ "hysteresis": 2,
8
+ "min_loss_scale": 1
9
+ },
10
+ "bf16": {
11
+ "enabled": "auto"
12
+ },
13
+
14
+ "zero_optimization": {
15
+ "stage": 3,
16
+ "offload_optimizer": {
17
+ "device": "cpu",
18
+ "pin_memory": true
19
+ },
20
+ "overlap_comm": true,
21
+ "contiguous_gradients": true,
22
+ "sub_group_size": 1e9,
23
+ "reduce_bucket_size": "auto",
24
+ "stage3_prefetch_bucket_size": "auto",
25
+ "stage3_param_persistence_threshold": "auto",
26
+ "stage3_max_live_parameters": 1e9,
27
+ "stage3_max_reuse_distance": 1e9,
28
+ "stage3_gather_16bit_weights_on_model_save": true
29
+ },
30
+
31
+ "gradient_accumulation_steps": "auto",
32
+ "gradient_clipping": "auto",
33
+ "steps_per_print": 10,
34
+ "train_batch_size": "auto",
35
+ "train_micro_batch_size_per_gpu": "auto",
36
+ "wall_clock_breakdown": false
37
+ }
finetune/deepspeed_support/ds_zero3_param_offload.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fp16": {
3
+ "enabled": "auto",
4
+ "loss_scale": 0,
5
+ "loss_scale_window": 1000,
6
+ "initial_scale_power": 16,
7
+ "hysteresis": 2,
8
+ "min_loss_scale": 1
9
+ },
10
+ "bf16": {
11
+ "enabled": "auto"
12
+ },
13
+
14
+ "zero_optimization": {
15
+ "stage": 3,
16
+ "offload_param": {
17
+ "device": "cpu",
18
+ "pin_memory": true
19
+ },
20
+ "overlap_comm": true,
21
+ "contiguous_gradients": true,
22
+ "sub_group_size": 1e9,
23
+ "reduce_bucket_size": "auto",
24
+ "stage3_prefetch_bucket_size": "auto",
25
+ "stage3_param_persistence_threshold": "auto",
26
+ "stage3_max_live_parameters": 1e9,
27
+ "stage3_max_reuse_distance": 1e9,
28
+ "stage3_gather_16bit_weights_on_model_save": true
29
+ },
30
+
31
+ "gradient_accumulation_steps": "auto",
32
+ "gradient_clipping": "auto",
33
+ "steps_per_print": 10,
34
+ "train_batch_size": "auto",
35
+ "train_micro_batch_size_per_gpu": "auto",
36
+ "wall_clock_breakdown": false
37
+ }
finetune/deepspeed_support/merge_lora_weight.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 导入所需的库
2
+ from transformers import AutoModelForCausalLM # 用于加载预训练的语言模型
3
+ from peft import LoraConfig, get_peft_model, PeftModel # 用于处理LoRA权重
4
+ import argparse # 用于解析命令行参数
5
+ import shutil # 用于文件操作,如复制
6
+ import os # 用于文件路径操作
7
+ import torch # 用于深度学习操作
8
+
9
+ def main():
10
+ # 创建参数解析器
11
+ parser = argparse.ArgumentParser()
12
+ # 添加命令行参数
13
+ parser.add_argument("--base_model_path", type=str, required=True,
14
+ help="Path to pretrained model or model identifier from huggingface.co/models")
15
+ parser.add_argument("--adapter_model_path", type=str, required=True, help="Path to adapter model")
16
+ parser.add_argument("--output_path", type=str, required=True, help="Path to save the output model")
17
+ parser.add_argument("--save_dtype", type=str, choices=['bf16', 'fp32', 'fp16'],
18
+ default='fp32', help="In which dtype to save, fp32, bf16 or fp16.")
19
+ # 解析命令行参数
20
+ args = parser.parse_args()
21
+
22
+ name2dtype = {'bf16': torch.bfloat16, 'fp32': torch.float32, 'fp16': torch.float16}
23
+ # 加载基座模型
24
+ model = AutoModelForCausalLM.from_pretrained(
25
+ args.base_model_path, device_map='cpu',
26
+ trust_remote_code=True, torch_dtype=name2dtype[args.save_dtype]
27
+ )
28
+ # 在基座模型的基础上加载 adapter 权重
29
+ model = PeftModel.from_pretrained(model, args.adapter_model_path, trust_remote_code=True)
30
+ # 融合模型和 adapter
31
+ model = model.merge_and_unload()
32
+ # 保存融合后的模型权重
33
+ model.save_pretrained(args.output_path, safe_serialization=False)
34
+
35
+ # Copy tokenizer, config and other non-weight files from base model
36
+ # Skip model weight files (.safetensors, .bin, .pt) and index files
37
+ _SKIP_SUFFIXES = ('.safetensors', '.bin', '.pt', '.pth')
38
+ _SKIP_NAMES = {'model.safetensors.index.json', 'pytorch_model.bin.index.json'}
39
+
40
+ for fname in os.listdir(args.base_model_path):
41
+ src = os.path.join(args.base_model_path, fname)
42
+ if not os.path.isfile(src):
43
+ continue
44
+ if fname in _SKIP_NAMES or fname.endswith(_SKIP_SUFFIXES):
45
+ continue
46
+ dst = os.path.join(args.output_path, fname)
47
+ if not os.path.exists(dst):
48
+ shutil.copy(src, dst)
49
+ print(f'Copied {fname}')
50
+
51
+ print(f'Merged model weight is saved to {args.output_path}')
52
+
53
+ if __name__ == "__main__":
54
+ main()
finetune/deepspeed_support/merge_lora_weight.sh ADDED
@@ -0,0 +1 @@
 
 
1
+ python3 merge_lora_weight.py --base_model_path /xxx/hy_l_train/checkpoint-200 --adapter_model_path /xxx/runs/hy_l_lora_train/checkpoint-200 --output_path /xxx/ckpts/merged_hy_lora_weight --save_dtype bf16
finetune/deepspeed_support/train.py ADDED
@@ -0,0 +1,903 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Tencent Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
16
+ #
17
+ # This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
18
+ # and OPT implementations in this library. It has been modified from its
19
+ # original forms to accommodate minor architectural differences compared
20
+ # to GPT-NeoX and OPT used by the Meta AI team that trained the model.
21
+ #
22
+ # Licensed under the Apache License, Version 2.0 (the "License");
23
+ # you may not use this file except in compliance with the License.
24
+ # You may obtain a copy of the License at
25
+ #
26
+ # http://www.apache.org/licenses/LICENSE-2.0
27
+ #
28
+ # Unless required by applicable law or agreed to in writing, software
29
+ # distributed under the License is distributed on an "AS IS" BASIS,
30
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31
+ # See the License for the specific language governing permissions and
32
+ # limitations under the License.
33
+
34
+
35
+ import os
36
+ import re
37
+ import sys
38
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
39
+ import json
40
+ import torch
41
+ import shutil
42
+ import logging
43
+ from dataclasses import dataclass, field
44
+ import deepspeed
45
+ from typing import Optional, Dict
46
+
47
+ import transformers
48
+ from torch.utils.data import Dataset
49
+ from transformers import Trainer, TrainerCallback
50
+ from peft import LoraConfig, get_peft_model, PeftModel
51
+ from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR
52
+ from transformers.modeling_utils import unwrap_model
53
+
54
+
55
+ def print_args(args, name='arguments'):
56
+ """Print arguments."""
57
+ if torch.distributed.get_rank() == 0:
58
+ print(f'------------------------ {name} ------------------------', flush=True)
59
+ str_list = []
60
+ for arg in vars(args):
61
+ dots = '.' * (48 - len(arg))
62
+ str_list.append(' {} {} {}'.format(arg, dots, getattr(args, arg)))
63
+ for arg in sorted(str_list, key=lambda x: x.lower()):
64
+ print(arg, flush=True)
65
+ print(f'-------------------- end of {name} ---------------------', flush=True)
66
+
67
+
68
+ @dataclass
69
+ class ModelArguments:
70
+ use_flash_attn: bool = field(
71
+ default=False,
72
+ metadata={"help": "Enable FlashAttention-2 for faster training."}
73
+ )
74
+ use_lora: bool = field(default=False, metadata={"help": "Enable Lora for faster training."})
75
+ hidden_size: int = field(default=2048, metadata={"help": "The hidden size of the model."})
76
+ num_layers: int = field(default=24, metadata={"help": "The number of layers of the model."})
77
+ num_attention_heads: int = field(default=16, metadata={"help": "The number of attention heads of the model."})
78
+ intermediate_size: int = field(default=8192, metadata={"help": "The intermediate size of the model."})
79
+ max_position_embeddings: int = field(
80
+ default=2048,
81
+ metadata={"help": "The maximum sequence length that this model might ever be used with."}
82
+ )
83
+ vocab_size: int = field(default=50257, metadata={"help": "The vocabulary size of the model."})
84
+ type_vocab_size: int = field(default=1, metadata={"help": "The vocabulary size of the model."})
85
+ layer_norm_eps: float = field(
86
+ default=1e-5,
87
+ metadata={"help": "The epsilon used by the layer normalization layers of the model."}
88
+ )
89
+ moe_topk: int = field(default=4, metadata={"help": "The topk for MOE."})
90
+ num_experts: int = field(default=8, metadata={"help": "The number of experts for MOE."})
91
+ num_key_value_heads: int = field(default=16, metadata={"help": "The number of key-value heads in GQA."})
92
+ moe_intermediate_size: int = field(default=1536, metadata={"help": "The intermediate size of each MoE expert."})
93
+ use_mixed_mlp_moe: bool = field(
94
+ default=False,
95
+ metadata={"help": "Whether to use mixed MoE with shared expert."}
96
+ )
97
+ num_shared_expert: int = field(default=1, metadata={"help": "Number of shared experts."})
98
+ use_qk_norm: bool = field(default=False, metadata={"help": "Whether to use qk norm."})
99
+ moe_layer_num_skipped: int = field(default=1, metadata={"help": "Number of initial dense layers before MoE layers."})
100
+ tie_word_embeddings: bool = field(
101
+ default=True,
102
+ metadata={"help": "Whether to tie the word embeddings of the encoder and the decoder."}
103
+ )
104
+ lora_rank: int = field(default=64, metadata={"help": "The rank of lora."})
105
+ lora_alpha: int = field(default=8, metadata={"help": "Lora alpha"})
106
+ lora_dropout: float = field(default=0.0, metadata={"help": "Lora dropout"})
107
+ train_attention_params_only: bool = field(default=False, metadata={
108
+ "help": "Whether to train attention parameters only."}
109
+ )
110
+
111
+
112
+ @dataclass
113
+ class DataArguments:
114
+ train_data_file: str = field(default=None, metadata={"help": "Path to the training data."})
115
+ max_seq_length: int = field(
116
+ default=2048,
117
+ metadata={"help": "The max sequence length of the model inputs after tokenization."}
118
+ )
119
+ complex_data: Optional[str] = field(default=None)
120
+ use_dummy_data: bool = field(default=False, metadata={"help": "Use dummy data."})
121
+
122
+
123
+ @dataclass
124
+ class TrainingArguments(transformers.TrainingArguments):
125
+ cache_dir: Optional[str] = field(default=None)
126
+ optim: str = field(default="adamw_torch")
127
+ model_max_length: int = field(
128
+ default=2048,
129
+ metadata={"help": "Maximum sequence length. Sequences will be right padded (and possibly truncated)."},
130
+ )
131
+ tokenizer_name_or_path: Optional[str] = field(default=None)
132
+ model_name_or_path: Optional[str] = field(default=None)
133
+ min_lr: float = field(
134
+ default=0.01,
135
+ metadata={"help": "The final learning rate at the end of the decay will be learning_rate * min_lr"}
136
+ )
137
+
138
+
139
+ IGNORE_INDEX = -100
140
+
141
+
142
+ class DummyDataset(Dataset):
143
+ def __init__(self, tokenizer, max_seq_length=512, length=1000):
144
+ self.tokenizer = tokenizer
145
+ self.max_seq_length = max_seq_length
146
+ self.length = length
147
+
148
+ def __len__(self):
149
+ return self.length
150
+
151
+ def __getitem__(self, index):
152
+ tokens = torch.randint(0, self.tokenizer.vocab_size, (self.max_seq_length, ))
153
+ return {'input_ids': tokens, 'labels': tokens}
154
+
155
+
156
+ class SFTDataset(Dataset):
157
+ def __init__(self, data_file, tokenizer, max_seq_length = 2048, prompt_format = 'mplus'):
158
+ self.tokenizer = tokenizer
159
+ self.prompt_format = prompt_format
160
+ self.max_seq_length = max_seq_length
161
+
162
+ self.data_list = self.load_data(data_file)
163
+
164
+ def __len__(self):
165
+ return len(self.data_list)
166
+
167
+ def load_data(self, data_file):
168
+ logging.info('Loading data: {}'.format(data_file))
169
+ with open(data_file, 'r', encoding='utf8') as f:
170
+ data_list = f.readlines()
171
+ logging.info("there are {} data in dataset".format(len(data_list)))
172
+ return data_list
173
+
174
+ def encode_data(self, data_dict):
175
+ model_inputs = {}
176
+ reasoning_effort = data_dict.get('reasoning_effort', None)
177
+ if reasoning_effort is None:
178
+ reasoning_effort = 'no_think'
179
+ try:
180
+ template_output = self.tokenizer.apply_chat_template(data_dict['messages'], tokenize=True, return_dict=False, is_training=True, reasoning_effort=reasoning_effort)
181
+ except Exception as e:
182
+ print(f"[ERROR] apply_chat_template failed: {e}")
183
+ print(f"[ERROR] messages: {data_dict['messages']}")
184
+ print(f"[ERROR] reasoning_effort: {reasoning_effort}")
185
+ template_output = []
186
+
187
+ # Debug: Check template_output type and content
188
+ if isinstance(template_output, bool):
189
+ print(f"[WARNING] apply_chat_template returned bool: {template_output}")
190
+ print(f"[WARNING] messages: {data_dict['messages']}")
191
+ print(f"[WARNING] reasoning_effort: {reasoning_effort}")
192
+ # Return empty tensor to avoid crash
193
+ template_output = []
194
+
195
+ if isinstance(template_output, list) and len(template_output) > 0 and isinstance(template_output[0], list):
196
+ template_output = template_output[0]
197
+
198
+ # Ensure template_output is a list of integers
199
+ if not isinstance(template_output, list) or not all(isinstance(x, int) for x in template_output):
200
+ print(f"[WARNING] Invalid template_output format: {type(template_output)}, content: {template_output}")
201
+ print(f"[WARNING] messages: {data_dict['messages']}")
202
+ template_output = []
203
+
204
+ message_tokens = torch.tensor(template_output, dtype=torch.long)
205
+
206
+ # Use new HunYuan tokenizer special tokens
207
+ # Get assistant_token from tokenizer attribute (dynamic, not hardcoded)
208
+ assistant_token = getattr(self.tokenizer, 'assistant_token', None)
209
+ if assistant_token is None:
210
+ # Fallback: try to get from tokenizer_config
211
+ assistant_token = '<|hy_Assistant:6124c78e|>'
212
+ assistant_token_id = self.tokenizer.convert_tokens_to_ids(assistant_token)
213
+
214
+ # Safety check: ensure assistant_token_id is valid
215
+ if assistant_token_id is None or assistant_token_id == self.tokenizer.unk_token_id:
216
+ print(f"[WARNING] assistant_token_id is invalid: {assistant_token_id}, assistant_token: {assistant_token}")
217
+ print(f"[WARNING] Using fallback token ID")
218
+ # Use a fallback: try to find the token in vocab
219
+ assistant_token_id = self.tokenizer.convert_tokens_to_ids('<|hy_Assistant:6124c78e|>')
220
+
221
+ eos_token_id = self.tokenizer.convert_tokens_to_ids(self.tokenizer.eos_token)
222
+ pad_token_id = self.tokenizer.pad_token_id
223
+
224
+ # Find assistant reply boundaries: starts at assistant_token, ends at eos_token
225
+ # Handle empty message_tokens case
226
+ if message_tokens.numel() == 0:
227
+ print(f"[WARNING] Empty message_tokens, skipping data sample")
228
+ # Return empty tensors with proper shape
229
+ input_ids = torch.tensor([], dtype=torch.long)
230
+ labels = torch.tensor([], dtype=torch.long)
231
+ attention_mask = torch.tensor([], dtype=torch.bool)
232
+ else:
233
+ loss_token_begins = (message_tokens == assistant_token_id).nonzero(as_tuple=True)[0].tolist()
234
+ loss_token_ends = (message_tokens == eos_token_id).nonzero(as_tuple=True)[0].tolist()
235
+ message_labels = torch.tensor([IGNORE_INDEX] * message_tokens.shape[0])
236
+ for begin_idx, end_idx in zip(loss_token_begins, loss_token_ends):
237
+ # Compute loss from the token after <|hy_Assistant|> to eos_token (inclusive)
238
+ message_labels[begin_idx + 1:end_idx + 1] = message_tokens[begin_idx + 1:end_idx + 1]
239
+ input_ids = message_tokens.to(torch.long)
240
+ labels = message_labels.to(torch.long)
241
+
242
+ input_ids = input_ids[:self.max_seq_length]
243
+ labels = labels[:self.max_seq_length]
244
+ attention_mask = [1 if val != pad_token_id else 0 for val in input_ids]
245
+ attention_mask = torch.tensor(attention_mask, dtype=torch.bool)
246
+
247
+ model_inputs["input_ids"] = input_ids
248
+ model_inputs["attention_mask"] = attention_mask
249
+ model_inputs["labels"] = labels
250
+
251
+ return model_inputs
252
+
253
+ def __getitem__(self, index):
254
+ data = self.data_list[index]
255
+ data = json.loads(data)
256
+ model_inputs = self.encode_data(data)
257
+
258
+ # Check if the encoded data is empty (due to tokenization failure)
259
+ if model_inputs["input_ids"].numel() == 0:
260
+ # Return a valid placeholder sample to avoid crash
261
+ # Use a minimal valid sequence with special tokens
262
+ assistant_token_id = self.tokenizer.convert_tokens_to_ids('<|hy_Assistant|>')
263
+ eos_token_id = self.tokenizer.convert_tokens_to_ids(self.tokenizer.eos_token)
264
+ pad_token_id = self.tokenizer.pad_token_id
265
+
266
+ # Create a minimal valid sequence: <|hy_Assistant|> + eos
267
+ placeholder_tokens = [assistant_token_id, eos_token_id]
268
+ placeholder_tokens = placeholder_tokens[:self.max_seq_length]
269
+
270
+ input_ids = torch.tensor(placeholder_tokens, dtype=torch.long)
271
+ labels = torch.tensor([IGNORE_INDEX, eos_token_id], dtype=torch.long)[:self.max_seq_length]
272
+ attention_mask = torch.tensor([1, 1], dtype=torch.bool)[:self.max_seq_length]
273
+
274
+ # Pad to max_seq_length if needed
275
+ if len(placeholder_tokens) < self.max_seq_length:
276
+ padding_length = self.max_seq_length - len(placeholder_tokens)
277
+ input_ids = torch.cat([input_ids, torch.full((padding_length,), pad_token_id, dtype=torch.long)])
278
+ labels = torch.cat([labels, torch.full((padding_length,), IGNORE_INDEX, dtype=torch.long)])
279
+ attention_mask = torch.cat([attention_mask, torch.zeros(padding_length, dtype=torch.bool)])
280
+
281
+ model_inputs = {
282
+ "input_ids": input_ids,
283
+ "attention_mask": attention_mask,
284
+ "labels": labels
285
+ }
286
+
287
+ return model_inputs
288
+
289
+
290
+ @dataclass
291
+ class DataCollatorForSupervisedDataset(object):
292
+ """Collate examples for supervised fine-tuning."""
293
+
294
+ tokenizer: transformers.PreTrainedTokenizer
295
+
296
+ def __call__(self, instances):
297
+ input_ids = [instance['input_ids'] for instance in instances]
298
+ labels = [instance['labels'] for instance in instances]
299
+ pad_token_id = self.tokenizer.pad_token_id
300
+ input_ids = torch.nn.utils.rnn.pad_sequence(input_ids, batch_first=True, padding_value=pad_token_id)
301
+ labels = torch.nn.utils.rnn.pad_sequence(labels, batch_first=True, padding_value=IGNORE_INDEX)
302
+ return dict(
303
+ input_ids=input_ids,
304
+ labels=labels,
305
+ attention_mask=input_ids.ne(pad_token_id),
306
+ )
307
+
308
+
309
+ def make_supervised_data_module(tokenizer, data_args) -> Dict:
310
+ """Make dataset and collator for supervised fine-tuning."""
311
+ if data_args.use_dummy_data:
312
+ train_dataset = DummyDataset(tokenizer, data_args.max_seq_length)
313
+ else:
314
+ train_dataset = SFTDataset(
315
+ tokenizer=tokenizer,
316
+ data_file=data_args.train_data_file,
317
+ max_seq_length=data_args.max_seq_length
318
+ )
319
+ data_collator = DataCollatorForSupervisedDataset(tokenizer=tokenizer)
320
+ return dict(train_dataset=train_dataset, eval_dataset=None, data_collator=data_collator)
321
+
322
+
323
+ # for full model training, change the config.json, copy the model and configuration to support Auto load
324
+ class CustomSaveCallback(TrainerCallback):
325
+ def on_save(self, args, state, control, **kwargs):
326
+ if torch.distributed.get_rank() == 0:
327
+ output_dir = os.path.join(args.output_dir, f"{PREFIX_CHECKPOINT_DIR}-{state.global_step}")
328
+
329
+ # Copy tokenizer files to checkpoint directory
330
+ tokenizer_files = [
331
+ 'generation_config.json',
332
+ 'hy.tiktoken',
333
+ 'tokenizer_config.json',
334
+ 'tokenization_hy.py',
335
+ 'tokenizer.json',
336
+ 'special_tokens_map.json',
337
+ 'chat_template.jinja',
338
+ ]
339
+ for fname in tokenizer_files:
340
+ src = os.path.join(args.tokenizer_name_or_path, fname)
341
+ if os.path.isfile(src):
342
+ shutil.copy(src, os.path.join(output_dir, fname))
343
+
344
+ return control
345
+
346
+
347
+ def train():
348
+ parser = transformers.HfArgumentParser((ModelArguments, DataArguments, TrainingArguments))
349
+ model_args, data_args, training_args = parser.parse_args_into_dataclasses()
350
+ print_args(model_args, 'model arguments')
351
+ print_args(data_args, 'data arguments')
352
+ print_args(training_args, 'training arguments')
353
+
354
+ tokenizer = transformers.AutoTokenizer.from_pretrained(
355
+ training_args.tokenizer_name_or_path,
356
+ trust_remote_code = True
357
+ )
358
+
359
+ init_kwargs = {}
360
+ if model_args.use_flash_attn:
361
+ init_kwargs["attn_implementation"] = "flash_attention_2"
362
+ # Workaround: transformers >= 5.x uses importlib.metadata.packages_distributions()
363
+ # to verify flash-attn package name, which fails when the package is installed under
364
+ # a custom distribution name (e.g. ptm-flash-attn). Patch the check to skip it.
365
+ try:
366
+ from transformers.modeling_flash_attention_utils import FLASH_ATTENTION_COMPATIBILITY_MATRIX
367
+ _orig_pkg_check = FLASH_ATTENTION_COMPATIBILITY_MATRIX[2]["pkg_availability_check"]
368
+ FLASH_ATTENTION_COMPATIBILITY_MATRIX[2]["pkg_availability_check"] = lambda *a, **kw: True
369
+ print("[Patch] Bypassed flash_attn package distribution name check for FA2.")
370
+ except Exception as e:
371
+ print(f"[Patch] Could not patch FA2 pkg check (non-fatal): {e}")
372
+ if training_args.bf16:
373
+ init_kwargs["dtype"] = torch.bfloat16
374
+ elif training_args.fp16:
375
+ init_kwargs["dtype"] = torch.float16
376
+
377
+ # Check if model weights exist (not just the directory)
378
+ _has_weights = (
379
+ training_args.model_name_or_path is not None
380
+ and os.path.isdir(training_args.model_name_or_path)
381
+ and any(
382
+ os.path.isfile(os.path.join(training_args.model_name_or_path, f))
383
+ for f in ("model.safetensors", "pytorch_model.bin", "model.safetensors.index.json", "pytorch_model.bin.index.json")
384
+ )
385
+ )
386
+
387
+ # -----------------------------------------------------------------------
388
+ # Fix: Rename checkpoint keys so that old-style weight names (e.g.
389
+ # self_attn.q_norm) are mapped to the current model attribute names
390
+ # (e.g. self_attn.query_layernorm). The model's
391
+ # _fix_state_dict_key_on_load hook is NOT invoked on the DeepSpeed
392
+ # ZeRO-3 loading path, so we monkey-patch the ZeRO-3 loader instead.
393
+ # -----------------------------------------------------------------------
394
+ # Key renames: checkpoint format -> installed transformers 5.8.1 model format
395
+ # Checkpoint uses: mlp.router.gate, mlp.expert_bias, mlp.shared_mlp
396
+ # Model uses: mlp.gate, mlp.e_score_correction_bias, mlp.shared_experts
397
+ _CKPT_KEY_RENAMES = [
398
+ ("mlp.router.gate.", "mlp.gate."),
399
+ ("mlp.expert_bias", "mlp.e_score_correction_bias"),
400
+ ("mlp.shared_mlp.", "mlp.shared_experts."),
401
+ # Also handle even older checkpoints that use mlp.gate.wg
402
+ ("mlp.gate.wg.", "mlp.gate."),
403
+ ]
404
+
405
+ # Regex to match per-expert keys in checkpoint
406
+ # e.g. model.layers.10.mlp.experts.5.gate_proj.weight
407
+ _EXPERT_KEY_RE = re.compile(
408
+ r"^(.*\.mlp\.experts\.)(\d+)\.(gate_proj|up_proj|down_proj)\.weight$"
409
+ )
410
+
411
+ from transformers.integrations.deepspeed import (
412
+ _load_state_dict_into_zero3_model as _orig_load_zero3,
413
+ )
414
+ import transformers.integrations.deepspeed as _ds_mod
415
+ import transformers.modeling_utils as _mu_mod
416
+
417
+ def _patched_load_zero3(model_to_load, state_dict, load_config=None):
418
+ new_sd = {}
419
+ expert_groups = {} # prefix -> {expert_idx -> {proj_name -> tensor}}
420
+
421
+ for k, v in state_dict.items():
422
+ m = _EXPERT_KEY_RE.match(k)
423
+ if m:
424
+ # Per-expert key: collect for fusion
425
+ prefix = m.group(1)
426
+ expert_idx = int(m.group(2))
427
+ proj_name = m.group(3)
428
+ if prefix not in expert_groups:
429
+ expert_groups[prefix] = {}
430
+ if expert_idx not in expert_groups[prefix]:
431
+ expert_groups[prefix][expert_idx] = {}
432
+ expert_groups[prefix][expert_idx][proj_name] = v
433
+ else:
434
+ # Non-expert key: apply simple renames
435
+ new_k = k
436
+ for old_sub, new_sub in _CKPT_KEY_RENAMES:
437
+ if old_sub in new_k:
438
+ new_k = new_k.replace(old_sub, new_sub)
439
+ break
440
+ new_sd[new_k] = v
441
+
442
+ # Fuse expert groups into 3D tensors
443
+ for prefix in sorted(expert_groups.keys()):
444
+ experts = expert_groups[prefix]
445
+ num_experts = max(experts.keys()) + 1
446
+ gate_up_list = []
447
+ down_list = []
448
+ for i in range(num_experts):
449
+ if i not in experts:
450
+ continue
451
+ exp = experts[i]
452
+ if "gate_proj" in exp and "up_proj" in exp:
453
+ gate_up_list.append(torch.cat([exp["gate_proj"], exp["up_proj"]], dim=0))
454
+ if "down_proj" in exp:
455
+ down_list.append(exp["down_proj"])
456
+ if gate_up_list:
457
+ new_sd[f"{prefix}gate_up_proj"] = torch.stack(gate_up_list, dim=0)
458
+ if down_list:
459
+ new_sd[f"{prefix}down_proj"] = torch.stack(down_list, dim=0)
460
+ del expert_groups
461
+
462
+ # Call original ZeRO-3 loader for parameters
463
+ result = _orig_load_zero3(model_to_load, new_sd, load_config)
464
+
465
+ # -------------------------------------------------------------------
466
+ # Patch: Manually load buffers (e.g. e_score_correction_bias).
467
+ # ZeRO-3's loader only handles named_parameters, not named_buffers.
468
+ # -------------------------------------------------------------------
469
+ buffers_loaded = 0
470
+ for name, buf in model_to_load.named_buffers():
471
+ if name in new_sd:
472
+ src_tensor = new_sd[name]
473
+ if isinstance(src_tensor, torch.Tensor):
474
+ buf.data.copy_(src_tensor.to(buf.dtype))
475
+ buffers_loaded += 1
476
+ # Remove from unexpected keys if tracked
477
+ if isinstance(result, tuple) and len(result) >= 2:
478
+ if isinstance(result[1], set):
479
+ result[1].discard(name)
480
+ if buffers_loaded > 0:
481
+ print(f"[HYV3 Patch] Manually loaded {buffers_loaded} buffers "
482
+ f"(e.g. e_score_correction_bias) into model.")
483
+
484
+ return result
485
+
486
+ _ds_mod._load_state_dict_into_zero3_model = _patched_load_zero3
487
+ _mu_mod._load_state_dict_into_zero3_model = _patched_load_zero3
488
+ # -----------------------------------------------------------------------
489
+
490
+ # -------------------------------------------------------------------
491
+ # Patch: Save-time reverse key rename + 3D -> per-expert unfuse.
492
+ #
493
+ # When saving checkpoints, the model state_dict uses 3D fused experts
494
+ # and new naming. We reverse both for old checkpoint compatibility:
495
+ # - mlp.gate. -> mlp.router.gate.
496
+ # - mlp.e_score_correction_bias -> mlp.expert_bias
497
+ # - mlp.shared_experts. -> mlp.shared_mlp.
498
+ # - experts.gate_up_proj -> experts.{N}.gate_proj.weight + up_proj
499
+ # - experts.down_proj -> experts.{N}.down_proj.weight
500
+ # -------------------------------------------------------------------
501
+ _SAVE_KEY_RENAMES = [
502
+ ("mlp.gate.", "mlp.router.gate."),
503
+ ("mlp.e_score_correction_bias", "mlp.expert_bias"),
504
+ ("mlp.shared_experts.", "mlp.shared_mlp."),
505
+ ]
506
+ _FUSED_EXPERT_KEY_RE = re.compile(
507
+ r"^(.*\.mlp\.experts\.)(gate_up_proj|down_proj)$"
508
+ )
509
+
510
+ def _apply_save_reverse_rename_patch():
511
+ try:
512
+ from transformers.models.hy_v3.modeling_hy_v3 import HYV3ForCausalLM
513
+ except ImportError:
514
+ try:
515
+ from transformers.hy_v3.modeling_hy_v3 import HYV3ForCausalLM
516
+ except ImportError:
517
+ print("[HYV3 Patch] Could not import HYV3ForCausalLM; "
518
+ "save reverse rename patch NOT applied.")
519
+ return
520
+
521
+ _orig_save_pretrained = HYV3ForCausalLM.save_pretrained
522
+
523
+ def _patched_save_pretrained(self, *args, **kwargs):
524
+ state_dict = kwargs.get("state_dict", None)
525
+ if state_dict is not None:
526
+ reversed_sd = {}
527
+ for k, v in state_dict.items():
528
+ new_k = k
529
+ # Apply simple key renames
530
+ for new_sub, old_sub in _SAVE_KEY_RENAMES:
531
+ if new_sub in new_k:
532
+ new_k = new_k.replace(new_sub, old_sub)
533
+ break
534
+
535
+ # Check if this is a fused 3D expert key
536
+ m = _FUSED_EXPERT_KEY_RE.match(new_k)
537
+ if m:
538
+ prefix = m.group(1) # e.g. "model.layers.1.mlp.experts."
539
+ proj_type = m.group(2) # "gate_up_proj" or "down_proj"
540
+
541
+ if proj_type == "gate_up_proj":
542
+ # v shape: [num_experts, 2*intermediate, hidden]
543
+ num_experts = v.shape[0]
544
+ intermediate = v.shape[1] // 2
545
+ for i in range(num_experts):
546
+ gate = v[i, :intermediate, :]
547
+ up = v[i, intermediate:, :]
548
+ reversed_sd[f"{prefix}{i}.gate_proj.weight"] = gate
549
+ reversed_sd[f"{prefix}{i}.up_proj.weight"] = up
550
+ elif proj_type == "down_proj":
551
+ # v shape: [num_experts, hidden, intermediate]
552
+ num_experts = v.shape[0]
553
+ for i in range(num_experts):
554
+ reversed_sd[f"{prefix}{i}.down_proj.weight"] = v[i]
555
+ else:
556
+ reversed_sd[new_k] = v
557
+
558
+ kwargs["state_dict"] = reversed_sd
559
+ print(f"[HYV3 Patch] Reverse-renamed and unfused "
560
+ f"{len(state_dict)} -> {len(reversed_sd)} "
561
+ f"state_dict keys for old checkpoint compatibility.")
562
+ return _orig_save_pretrained(self, *args, **kwargs)
563
+
564
+ HYV3ForCausalLM.save_pretrained = _patched_save_pretrained
565
+ print("[HYV3 Patch] Applied: save-time reverse key rename + "
566
+ "3D -> per-expert unfuse for old ckpt compatibility.")
567
+
568
+ _apply_save_reverse_rename_patch()
569
+ # -------------------------------------------------------------------
570
+
571
+ if _has_weights:
572
+ print(f"Initializing model from local file: {training_args.model_name_or_path}")
573
+ # ---------------------------------------------------------------
574
+ # Memory-efficient loading: Instead of from_pretrained's default
575
+ # ZeRO-3 path (which merges ALL shards into one huge dict in CPU
576
+ # memory), we:
577
+ # 1. Create the model skeleton under deepspeed.zero.Init (meta)
578
+ # 2. Load each safetensors shard one at a time
579
+ # 3. Scatter each shard's weights into ZeRO-3 partitions
580
+ # 4. Free the shard immediately
581
+ # This reduces per-rank CPU memory from ~670GB to ~7GB (1 shard).
582
+ # ---------------------------------------------------------------
583
+ import json as _json
584
+ from safetensors import safe_open
585
+
586
+ ds_config = training_args.deepspeed
587
+ if isinstance(ds_config, str):
588
+ with open(ds_config, "r") as f:
589
+ ds_config = _json.load(f)
590
+ # Replace "auto" values that deepspeed.zero.Init cannot resolve
591
+ _auto_defaults = {
592
+ "train_batch_size": training_args.per_device_train_batch_size
593
+ * training_args.gradient_accumulation_steps
594
+ * training_args.world_size,
595
+ "train_micro_batch_size_per_gpu": training_args.per_device_train_batch_size,
596
+ "gradient_accumulation_steps": training_args.gradient_accumulation_steps,
597
+ "gradient_clipping": training_args.max_grad_norm,
598
+ }
599
+ for k, v in _auto_defaults.items():
600
+ if k in ds_config and ds_config[k] == "auto":
601
+ ds_config[k] = v
602
+
603
+ # Step 1: Create model with empty (meta) weights under ZeRO-3 Init
604
+ model_path = training_args.model_name_or_path
605
+ config = transformers.AutoConfig.from_pretrained(
606
+ model_path, trust_remote_code=True
607
+ )
608
+ with deepspeed.zero.Init(dtype=torch.bfloat16,
609
+ config_dict_or_path=ds_config):
610
+ model = transformers.AutoModelForCausalLM.from_config(
611
+ config, trust_remote_code=True,
612
+ torch_dtype=init_kwargs.get("dtype", torch.bfloat16),
613
+ attn_implementation=init_kwargs.get("attn_implementation", None),
614
+ )
615
+ print(f"[HYV3] Model skeleton created under ZeRO-3 Init.")
616
+
617
+ # Step 2: Determine shard files from index
618
+ index_file = os.path.join(model_path, "model.safetensors.index.json")
619
+ if os.path.isfile(index_file):
620
+ with open(index_file, "r") as f:
621
+ index_data = _json.load(f)
622
+ # Get unique shard filenames in order
623
+ shard_files = list(dict.fromkeys(index_data["weight_map"].values()))
624
+ else:
625
+ # Single shard model
626
+ shard_files = ["model.safetensors"]
627
+
628
+ # Step 3: Load each shard and scatter into ZeRO-3 model
629
+ # For per-expert keys, we need to collect them per-layer and fuse
630
+ # into 3D tensors (gate_up_proj, down_proj) before scattering.
631
+ total_shards = len(shard_files)
632
+ all_loaded_keys = set()
633
+ # Buffer for cross-shard expert accumulation:
634
+ # prefix -> {expert_idx -> {proj_name -> tensor}}
635
+ pending_experts = {}
636
+
637
+ for shard_idx, shard_name in enumerate(shard_files, 1):
638
+ shard_path = os.path.join(model_path, shard_name)
639
+ print(f"[HYV3] Loading shard {shard_idx}/{total_shards}: {shard_name}")
640
+
641
+ # Load shard into CPU memory
642
+ shard_sd = {}
643
+ with safe_open(shard_path, framework="pt", device="cpu") as f:
644
+ for key in f.keys():
645
+ shard_sd[key] = f.get_tensor(key)
646
+
647
+ # Separate expert keys from non-expert keys, apply renames
648
+ renamed_sd = {}
649
+ expert_keys_in_shard = {} # prefix -> {expert_idx -> {proj_name -> tensor}}
650
+
651
+ for k, v in shard_sd.items():
652
+ m = _EXPERT_KEY_RE.match(k)
653
+ if m:
654
+ # Per-expert key: collect for fusion
655
+ prefix = m.group(1) # e.g. "model.layers.10.mlp.experts."
656
+ expert_idx = int(m.group(2))
657
+ proj_name = m.group(3) # gate_proj, up_proj, or down_proj
658
+ if prefix not in expert_keys_in_shard:
659
+ expert_keys_in_shard[prefix] = {}
660
+ if expert_idx not in expert_keys_in_shard[prefix]:
661
+ expert_keys_in_shard[prefix][expert_idx] = {}
662
+ expert_keys_in_shard[prefix][expert_idx][proj_name] = v
663
+ else:
664
+ # Non-expert key: apply simple renames
665
+ new_k = k
666
+ for old_sub, new_sub in _CKPT_KEY_RENAMES:
667
+ if old_sub in new_k:
668
+ new_k = new_k.replace(old_sub, new_sub)
669
+ break
670
+ renamed_sd[new_k] = v
671
+ del shard_sd
672
+
673
+ # Merge expert keys from this shard into pending_experts
674
+ for prefix, experts in expert_keys_in_shard.items():
675
+ if prefix not in pending_experts:
676
+ pending_experts[prefix] = {}
677
+ for idx, projs in experts.items():
678
+ if idx not in pending_experts[prefix]:
679
+ pending_experts[prefix][idx] = {}
680
+ pending_experts[prefix][idx].update(projs)
681
+ del expert_keys_in_shard
682
+
683
+ # Check if any pending expert groups are now complete
684
+ # (all 3 projections for all experts in the layer)
685
+ # We detect completeness by checking if we have gate_proj, up_proj,
686
+ # and down_proj for a contiguous range of expert indices.
687
+ completed_prefixes = []
688
+ for prefix, experts in pending_experts.items():
689
+ # Check if all experts have all 3 projections
690
+ if not experts:
691
+ continue
692
+ max_idx = max(experts.keys())
693
+ num_experts_found = len(experts)
694
+ # A layer is complete if we have a contiguous range and all have 3 projs
695
+ all_complete = all(
696
+ len(projs) == 3 for projs in experts.values()
697
+ )
698
+ # Heuristic: if we have 192 experts (or max_idx+1 == num found)
699
+ # and all have 3 projections, consider it complete
700
+ if all_complete and num_experts_found == (max_idx + 1):
701
+ completed_prefixes.append(prefix)
702
+
703
+ # Fuse completed expert groups and add to renamed_sd
704
+ for prefix in completed_prefixes:
705
+ experts = pending_experts.pop(prefix)
706
+ num_experts_layer = max(experts.keys()) + 1
707
+ gate_up_list = []
708
+ down_list = []
709
+ for i in range(num_experts_layer):
710
+ exp = experts[i]
711
+ gate_up = torch.cat([exp["gate_proj"], exp["up_proj"]], dim=0)
712
+ gate_up_list.append(gate_up)
713
+ down_list.append(exp["down_proj"])
714
+ fused_gate_up = torch.stack(gate_up_list, dim=0)
715
+ fused_down = torch.stack(down_list, dim=0)
716
+ del gate_up_list, down_list, experts
717
+
718
+ # Model key format: model.layers.X.mlp.experts.gate_up_proj
719
+ renamed_sd[f"{prefix}gate_up_proj"] = fused_gate_up
720
+ renamed_sd[f"{prefix}down_proj"] = fused_down
721
+ print(f"[HYV3] Fused {num_experts_layer} experts for {prefix}")
722
+
723
+ # Scatter this shard's weights into ZeRO-3 partitioned model
724
+ if renamed_sd:
725
+ _orig_load_zero3(model, renamed_sd)
726
+
727
+ # Also load buffers (e.g. e_score_correction_bias)
728
+ for name, buf in model.named_buffers():
729
+ if name in renamed_sd:
730
+ src_tensor = renamed_sd[name]
731
+ if isinstance(src_tensor, torch.Tensor):
732
+ buf.data.copy_(src_tensor.to(buf.dtype))
733
+
734
+ all_loaded_keys.update(renamed_sd.keys())
735
+ del renamed_sd
736
+ import gc; gc.collect()
737
+
738
+ # Flush any remaining pending experts (cross-shard edge case)
739
+ if pending_experts:
740
+ print(f"[HYV3] Flushing {len(pending_experts)} remaining expert group(s)...")
741
+ flush_sd = {}
742
+ for prefix, experts in pending_experts.items():
743
+ num_experts_layer = max(experts.keys()) + 1
744
+ gate_up_list = []
745
+ down_list = []
746
+ for i in range(num_experts_layer):
747
+ if i not in experts:
748
+ print(f"[HYV3] Warning: Missing expert {i} in {prefix}")
749
+ continue
750
+ exp = experts[i]
751
+ gate_up = torch.cat([exp["gate_proj"], exp["up_proj"]], dim=0)
752
+ gate_up_list.append(gate_up)
753
+ down_list.append(exp["down_proj"])
754
+ if gate_up_list:
755
+ fused_gate_up = torch.stack(gate_up_list, dim=0)
756
+ fused_down = torch.stack(down_list, dim=0)
757
+ flush_sd[f"{prefix}gate_up_proj"] = fused_gate_up
758
+ flush_sd[f"{prefix}down_proj"] = fused_down
759
+ print(f"[HYV3] Fused {len(gate_up_list)} experts for {prefix}")
760
+ del gate_up_list, down_list
761
+ del pending_experts
762
+
763
+ if flush_sd:
764
+ _orig_load_zero3(model, flush_sd)
765
+ for name, buf in model.named_buffers():
766
+ if name in flush_sd:
767
+ src_tensor = flush_sd[name]
768
+ if isinstance(src_tensor, torch.Tensor):
769
+ buf.data.copy_(src_tensor.to(buf.dtype))
770
+ all_loaded_keys.update(flush_sd.keys())
771
+ del flush_sd
772
+ import gc; gc.collect()
773
+
774
+ # Step 4: Report any missing/unexpected keys
775
+ model_keys = set(n for n, _ in model.named_parameters())
776
+ model_keys.update(n for n, _ in model.named_buffers())
777
+ missing = model_keys - all_loaded_keys
778
+ unexpected = all_loaded_keys - model_keys
779
+ if missing:
780
+ # Filter out keys that are expected to be missing (e.g. lm_head with tied embeddings)
781
+ real_missing = {k for k in missing if "lm_head" not in k}
782
+ if real_missing:
783
+ print(f"[HYV3] Warning: {len(real_missing)} keys not found in checkpoint "
784
+ f"(first 10): {list(real_missing)[:10]}")
785
+ if unexpected:
786
+ print(f"[HYV3] Warning: {len(unexpected)} unexpected keys in checkpoint "
787
+ f"(first 10): {list(unexpected)[:10]}")
788
+ print(f"[HYV3] Shard-by-shard loading complete. "
789
+ f"Loaded {len(all_loaded_keys)} keys from {total_shards} shards.")
790
+ else:
791
+ from transformers import HYV3Config
792
+ from transformers import HYV3ForCausalLM
793
+ print(f"Model weights not found at: {training_args.model_name_or_path}, "
794
+ f"using random initialized HYV3 model instead.")
795
+ # Use len(tokenizer) to include added special tokens; tokenizer.vocab_size
796
+ # may only return the base vocabulary size and miss special tokens whose
797
+ # IDs exceed that range, causing index-out-of-bounds in the embedding layer.
798
+ config = HYV3Config(
799
+ vocab_size=len(tokenizer),
800
+ hidden_size=model_args.hidden_size,
801
+ intermediate_size=model_args.intermediate_size,
802
+ max_position_embeddings=training_args.model_max_length,
803
+ moe_topk=model_args.moe_topk,
804
+ num_experts=model_args.num_experts,
805
+ num_attention_heads=model_args.num_attention_heads,
806
+ num_key_value_heads=model_args.num_key_value_heads,
807
+ num_hidden_layers=model_args.num_layers,
808
+ moe_intermediate_size=model_args.moe_intermediate_size,
809
+ use_mixed_mlp_moe=model_args.use_mixed_mlp_moe,
810
+ num_shared_expert=model_args.num_shared_expert,
811
+ use_qk_norm=model_args.use_qk_norm,
812
+ moe_layer_num_skipped=model_args.moe_layer_num_skipped,
813
+ tie_word_embeddings=model_args.tie_word_embeddings,
814
+ )
815
+ with deepspeed.zero.Init(dtype=init_kwargs.get("torch_dtype", torch.bfloat16), config_dict_or_path=training_args.deepspeed):
816
+ model = HYV3ForCausalLM(config)
817
+
818
+ if model_args.train_attention_params_only:
819
+ for name, param in model.named_parameters():
820
+ if 'self_attn' not in name:
821
+ param.requires_grad = False
822
+
823
+ if model_args.use_lora:
824
+ # define Lora configuration
825
+ lora_config = LoraConfig(
826
+ r=model_args.lora_rank,
827
+ lora_alpha=model_args.lora_alpha,
828
+ lora_dropout=model_args.lora_dropout,
829
+ target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
830
+ bias="none",
831
+ task_type="CAUSAL_LM",
832
+ )
833
+ model = get_peft_model(model, lora_config)
834
+
835
+ data_module = make_supervised_data_module(tokenizer=tokenizer, data_args=data_args)
836
+ # Tell Trainer not to attempt DataParallel
837
+ model.is_parallelizable = True
838
+ model.model_parallel = True
839
+
840
+ training_args.lr_scheduler_kwargs = {
841
+ 'min_lr_rate': training_args.min_lr / training_args.learning_rate,
842
+ }
843
+
844
+ # -----------------------------------------------------------------------
845
+ # Fix: DeepSpeed ZeRO-3 + gradient checkpointing compatibility.
846
+ #
847
+ # PyTorch's torch.utils.checkpoint with use_reentrant=False (the default
848
+ # in transformers) performs strict metadata checks on recomputed tensors
849
+ # during backward. Under ZeRO-3, parameters are all-gathered during the
850
+ # first forward pass (shape=[full_size]) but may be partitioned back
851
+ # (shape=[0]) when the checkpoint recomputes, causing a CheckpointError.
852
+ #
853
+ # Setting use_reentrant=True avoids this strict metadata check.
854
+ # -----------------------------------------------------------------------
855
+ if training_args.gradient_checkpointing and training_args.deepspeed:
856
+ training_args.gradient_checkpointing_kwargs = {"use_reentrant": True}
857
+
858
+ trainer = Trainer(
859
+ model=model,
860
+ processing_class=tokenizer,
861
+ args=training_args,
862
+ callbacks=[CustomSaveCallback],
863
+ **data_module
864
+ )
865
+ model.config.use_cache = False
866
+
867
+ # -----------------------------------------------------------------------
868
+ # Monkey-patch: fix dtype mismatch in DeepSpeed ZeRO-3 linear wrapper.
869
+ #
870
+ # By this point the DeepSpeed engine has been initialised by the Trainer
871
+ # and torch.nn.functional.linear has been replaced with
872
+ # zero3_linear_wrap. That wrapper does NOT auto-align input/weight
873
+ # dtypes before the matmul, causing "expected mat1 and mat2 to have the
874
+ # same dtype" errors in mixed-precision paths (MoE router gate in fp32
875
+ # with bf16 weights, expert FFN receiving fp32 routing-weighted input
876
+ # with bf16 weights, etc.).
877
+ #
878
+ # We wrap F.linear HERE (after DeepSpeed init) so that:
879
+ # 1. We are sure to capture the already-replaced function.
880
+ # 2. The dtype cast happens *outside* the autograd.Function, so
881
+ # gradient-checkpointing recompute sees identical tensor metadata.
882
+ # -----------------------------------------------------------------------
883
+ import torch.nn.functional as _F
884
+ _orig_F_linear = _F.linear
885
+
886
+ def _dtype_safe_linear(input, weight, bias=None):
887
+ if input.dtype != weight.dtype:
888
+ input = input.to(weight.dtype)
889
+ return _orig_F_linear(input, weight, bias)
890
+
891
+ _F.linear = _dtype_safe_linear
892
+ # -----------------------------------------------------------------------
893
+
894
+ trainer.train(resume_from_checkpoint=training_args.resume_from_checkpoint)
895
+
896
+ # Synchronize all processes before exit to avoid "Connection reset by peer"
897
+ # warnings caused by timing differences in multi-node shutdown.
898
+ if torch.distributed.is_initialized():
899
+ torch.distributed.barrier()
900
+
901
+
902
+ if __name__ == "__main__":
903
+ train()
finetune/deepspeed_support/train.sh ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ NET_TYPE="high"
4
+ export NCCL_DEBUG=WARN
5
+ export NCCL_P2P_LEVEL=NVL
6
+ export NCCL_IB_TIMEOUT=24
7
+ export NCCL_NVLS_ENABLE=0
8
+ export NCCL_MPI_PROFILE_PRIMS_ENABLE=0
9
+ export CUDA_DEVICE_MAX_CONNECTIONS=1
10
+ export TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC=3600
11
+ if [[ "${NET_TYPE}" = "low" ]]; then
12
+ export NCCL_SOCKET_IFNAME=eth1
13
+ export NCCL_IB_GID_INDEX=3
14
+ export NCCL_IB_HCA=mlx5_2:1
15
+ export NCCL_IB_SL=3
16
+ export NCCL_CHECK_DISABLE=1
17
+ export NCCL_P2P_DISABLE=0
18
+ export NCCL_LL_THRESHOLD=16384
19
+ export NCCL_IB_CUDA_SUPPORT=1
20
+ else
21
+ export NCCL_IB_GID_INDEX=3
22
+ export NCCL_IB_SL=3
23
+ export NCCL_CHECK_DISABLE=1
24
+ export NCCL_P2P_DISABLE=0
25
+ export NCCL_IB_DISABLE=0
26
+ export NCCL_LL_THRESHOLD=16384
27
+ export NCCL_IB_CUDA_SUPPORT=1
28
+ export NCCL_SOCKET_IFNAME=bond1
29
+ export UCX_NET_DEVICES=bond1
30
+ export NCCL_IB_HCA=mlx5_bond_1,mlx5_bond_5,mlx5_bond_3,mlx5_bond_7,mlx5_bond_4,mlx5_bond_8,mlx5_bond_2,mlx5_bond_6
31
+ export NCCL_COLLNET_ENABLE=0
32
+ export SHARP_COLL_ENABLE_SAT=0
33
+ export NCCL_NET_GDR_LEVEL=2
34
+ export NCCL_IB_QPS_PER_CONNECTION=4
35
+ export NCCL_IB_TC=160
36
+ export NCCL_PXN_DISABLE=1
37
+ fi
38
+
39
+ export HOST_GPU_NUM=8
40
+ # IP list, comma separated. e.g. "192.168.1.1,192.168.1.2" or single node "192.168.1.1"
41
+ IP_LIST=${IP_LIST:-"127.0.0.1"}
42
+
43
+ IFS=',' read -ra IP_ARRAY <<< "$IP_LIST"
44
+ export NODES=${#IP_ARRAY[@]}
45
+ export LOCAL_IP=${IP_ARRAY[0]}
46
+ NODE_IP_LIST=""
47
+ for ip in "${IP_ARRAY[@]}"; do
48
+ if [ -n "$NODE_IP_LIST" ]; then
49
+ NODE_IP_LIST="${NODE_IP_LIST},"
50
+ fi
51
+ NODE_IP_LIST="${NODE_IP_LIST}${ip}:${HOST_GPU_NUM}"
52
+ done
53
+ export NODE_IP_LIST
54
+ export NODE_NUM=$((${NODES} * ${HOST_GPU_NUM}))
55
+
56
+ model_path=path_to_model_weight
57
+ tokenizer_path=../../models
58
+ train_data_file=../data/example_data.jsonl
59
+
60
+ # ds_config_file=ds_zero2_no_offload.json
61
+ # ds_config_file=ds_zero3_no_offload.json
62
+ ds_config_file=ds_zero3_offload.json
63
+
64
+ output_path=/root/hf_train_output_full
65
+
66
+ mkdir -p ${output_path}
67
+
68
+ current_time=$(date "+%Y.%m.%d-%H.%M.%S")
69
+ log_file=${output_path}/"log_${current_time}.txt"
70
+
71
+ echo $NODE_IP_LIST > env.txt 2>&1
72
+ sed "s/:/ slots=/g" env.txt | sed "s/,/\n/g" > "hostfile"
73
+ sed "s/:.//g" env.txt | sed "s/,/\n/g" > "pssh.hosts"
74
+ export CHIEF_IP=$LOCAL_IP
75
+
76
+ if [ ${NODES} -gt 1 ]; then
77
+ HOST_PATH=hostfile
78
+ DS_ARGS="--hostfile=${HOST_PATH} --master_addr ${CHIEF_IP}"
79
+ else
80
+ DS_ARGS=""
81
+ fi
82
+
83
+ echo "NODES: ${NODES}, LOCAL_IP: ${LOCAL_IP}, NODE_IP_LIST: ${NODE_IP_LIST}"
84
+
85
+ deepspeed ${DS_ARGS} \
86
+ train.py \
87
+ --do_train \
88
+ --model_name_or_path ${model_path} \
89
+ --tokenizer_name_or_path ${tokenizer_path} \
90
+ --train_data_file ${train_data_file} \
91
+ --deepspeed ${ds_config_file} \
92
+ --output_dir ${output_path} \
93
+ --per_device_train_batch_size 1 \
94
+ --gradient_accumulation_steps 1 \
95
+ --gradient_checkpointing \
96
+ --lr_scheduler_type cosine_with_min_lr \
97
+ --logging_steps 1 \
98
+ --max_steps 200 \
99
+ --save_steps 100 \
100
+ --learning_rate 1e-5 \
101
+ --min_lr 1e-6 \
102
+ --warmup_ratio 0.01 \
103
+ --save_strategy steps \
104
+ --bf16 \
105
+ --hidden_size 4096 \
106
+ --intermediate_size 13312 \
107
+ --model_max_length 4096 \
108
+ --max_seq_length 4096 \
109
+ --moe_topk 8 \
110
+ --num_experts 192 \
111
+ --moe_intermediate_size 1536 \
112
+ --moe_layer_num_skipped 1 \
113
+ --num_attention_heads 64 \
114
+ --num_key_value_heads 8 \
115
+ --num_layers 80 \
116
+ --use_mixed_mlp_moe \
117
+ --num_shared_expert 1 \
118
+ --use_qk_norm | tee ${log_file}
finetune/deepspeed_support/train_lora.sh ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ NET_TYPE="high"
4
+ export NCCL_DEBUG=WARN
5
+ export NCCL_P2P_LEVEL=NVL
6
+ export NCCL_IB_TIMEOUT=24
7
+ export NCCL_NVLS_ENABLE=0
8
+ export NCCL_MPI_PROFILE_PRIMS_ENABLE=0
9
+ export CUDA_DEVICE_MAX_CONNECTIONS=1
10
+ export TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC=3600
11
+ if [[ "${NET_TYPE}" = "low" ]]; then
12
+ export NCCL_SOCKET_IFNAME=eth1
13
+ export NCCL_IB_GID_INDEX=3
14
+ export NCCL_IB_HCA=mlx5_2:1
15
+ export NCCL_IB_SL=3
16
+ export NCCL_CHECK_DISABLE=1
17
+ export NCCL_P2P_DISABLE=0
18
+ export NCCL_LL_THRESHOLD=16384
19
+ export NCCL_IB_CUDA_SUPPORT=1
20
+ else
21
+ export NCCL_IB_GID_INDEX=3
22
+ export NCCL_IB_SL=3
23
+ export NCCL_CHECK_DISABLE=1
24
+ export NCCL_P2P_DISABLE=0
25
+ export NCCL_IB_DISABLE=0
26
+ export NCCL_LL_THRESHOLD=16384
27
+ export NCCL_IB_CUDA_SUPPORT=1
28
+ export NCCL_SOCKET_IFNAME=bond1
29
+ export UCX_NET_DEVICES=bond1
30
+ export NCCL_IB_HCA=mlx5_bond_1,mlx5_bond_5,mlx5_bond_3,mlx5_bond_7,mlx5_bond_4,mlx5_bond_8,mlx5_bond_2,mlx5_bond_6
31
+ export NCCL_COLLNET_ENABLE=0
32
+ export SHARP_COLL_ENABLE_SAT=0
33
+ export NCCL_NET_GDR_LEVEL=2
34
+ export NCCL_IB_QPS_PER_CONNECTION=4
35
+ export NCCL_IB_TC=160
36
+ export NCCL_PXN_DISABLE=1
37
+ fi
38
+
39
+ export HOST_GPU_NUM=8
40
+ # IP list, comma separated. e.g. "192.168.1.1,192.168.1.2" or single node "192.168.1.1"
41
+ IP_LIST=${IP_LIST:-"127.0.0.1"}
42
+
43
+ IFS=',' read -ra IP_ARRAY <<< "$IP_LIST"
44
+ export NODES=${#IP_ARRAY[@]}
45
+ export LOCAL_IP=${IP_ARRAY[0]}
46
+ NODE_IP_LIST=""
47
+ for ip in "${IP_ARRAY[@]}"; do
48
+ if [ -n "$NODE_IP_LIST" ]; then
49
+ NODE_IP_LIST="${NODE_IP_LIST},"
50
+ fi
51
+ NODE_IP_LIST="${NODE_IP_LIST}${ip}:${HOST_GPU_NUM}"
52
+ done
53
+ export NODE_IP_LIST
54
+ export NODE_NUM=$((${NODES} * ${HOST_GPU_NUM}))
55
+
56
+
57
+ model_path=path_to_model_weight
58
+ tokenizer_path=../../models
59
+ train_data_file=../data/example_data.jsonl
60
+
61
+ # ds_config_file=ds_zero2_no_offload.json
62
+ # ds_config_file=ds_zero3_no_offload.json
63
+ ds_config_file=ds_zero3_offload.json
64
+
65
+ output_path=/root/hf_train_output
66
+
67
+ mkdir -p ${output_path}
68
+
69
+ current_time=$(date "+%Y.%m.%d-%H.%M.%S")
70
+ log_file=${output_path}/"log_${current_time}.txt"
71
+
72
+ echo $NODE_IP_LIST > env.txt 2>&1
73
+ sed "s/:/ slots=/g" env.txt | sed "s/,/\n/g" > "hostfile"
74
+ sed "s/:.//g" env.txt | sed "s/,/\n/g" > "pssh.hosts"
75
+ export CHIEF_IP=$LOCAL_IP
76
+
77
+ if [ ${NODES} -gt 1 ]; then
78
+ HOST_PATH=hostfile
79
+ DS_ARGS="--hostfile=${HOST_PATH} --master_addr ${CHIEF_IP}"
80
+ else
81
+ DS_ARGS=""
82
+ fi
83
+
84
+ echo "NODES: ${NODES}, LOCAL_IP: ${LOCAL_IP}, NODE_IP_LIST: ${NODE_IP_LIST}"
85
+
86
+ deepspeed ${DS_ARGS} \
87
+ train.py \
88
+ --do_train \
89
+ --model_name_or_path ${model_path} \
90
+ --tokenizer_name_or_path ${tokenizer_path} \
91
+ --train_data_file ${train_data_file} \
92
+ --deepspeed ${ds_config_file} \
93
+ --output_dir ${output_path} \
94
+ --per_device_train_batch_size 1 \
95
+ --gradient_accumulation_steps 1 \
96
+ --gradient_checkpointing \
97
+ --lr_scheduler_type cosine_with_min_lr \
98
+ --logging_steps 1 \
99
+ --max_steps 200 \
100
+ --save_steps 100 \
101
+ --learning_rate 1e-5 \
102
+ --min_lr 1e-6 \
103
+ --warmup_ratio 0.01 \
104
+ --save_strategy steps \
105
+ --bf16 \
106
+ --use_lora \
107
+ --lora_rank 64 \
108
+ --lora_alpha 128 \
109
+ --lora_dropout 0.1 \
110
+ --hidden_size 4096 \
111
+ --intermediate_size 13312 \
112
+ --model_max_length 4096 \
113
+ --max_seq_length 4096 \
114
+ --moe_topk 8 \
115
+ --num_experts 192 \
116
+ --moe_intermediate_size 1536 \
117
+ --moe_layer_num_skipped 1 \
118
+ --num_attention_heads 64 \
119
+ --num_key_value_heads 8 \
120
+ --num_layers 80 \
121
+ --use_mixed_mlp_moe \
122
+ --num_shared_expert 1 \
123
+ --use_qk_norm | tee ${log_file}
finetune/llama_factory_support/dataset_info.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "hy_v3_demo": {
3
+ "file_name": "../data/example_data.jsonl",
4
+ "formatting": "sharegpt",
5
+ "columns": {
6
+ "messages": "messages"
7
+ },
8
+ "tags": {
9
+ "role_tag": "role",
10
+ "content_tag": "content",
11
+ "user_tag": "user",
12
+ "assistant_tag": "assistant",
13
+ "system_tag": "system"
14
+ }
15
+ }
16
+ }
finetune/llama_factory_support/hy_v3_full_sft.yaml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### model
2
+ model_name_or_path: ../hf
3
+ trust_remote_code: true
4
+
5
+ ### method
6
+ stage: sft
7
+ do_train: true
8
+ finetuning_type: full
9
+ deepspeed: ../deepspeed_support/ds_zero3_offload.json
10
+
11
+ ### dataset
12
+ dataset_dir: .
13
+ dataset: hy_v3_demo
14
+ template: hy_v3
15
+ cutoff_len: 4096 # HYV3 supports 262k context length
16
+ max_samples: 1000
17
+ overwrite_cache: true
18
+
19
+ ### output
20
+ output_dir: saves/hy_v3/full/sft
21
+ logging_steps: 1
22
+ save_steps: 500
23
+ plot_loss: true
24
+ overwrite_output_dir: true
25
+ save_only_model: false
26
+ report_to: none # choices: [none, wandb, tensorboard, swanlab, mlflow]
27
+
28
+ ### train
29
+ per_device_train_batch_size: 1
30
+ gradient_accumulation_steps: 1
31
+ learning_rate: 1.0e-5
32
+ num_train_epochs: 100.0
33
+ lr_scheduler_type: cosine_with_min_lr
34
+ lr_scheduler_kwargs:
35
+ min_lr_rate: 0.1 # min_lr / learning_rate = 1e-6 / 1e-5 = 0.1
36
+ warmup_ratio: 0.1
37
+ bf16: true
38
+ gradient_checkpointing: true
39
+ gradient_checkpointing_kwargs:
40
+ use_reentrant: true
41
+ ddp_timeout: 180000000
42
+ flash_attn: fa2
43
+ resume_from_checkpoint: null
44
+
45
+ ### eval
46
+ # eval_dataset: alpaca_en_demo
47
+ # val_size: 0.1
48
+ # per_device_eval_batch_size: 1
49
+ # eval_strategy: steps
50
+ # eval_steps: 500
51
+
52
+ ### HYV3 specific parameters (MoE architecture)
53
+ # moe_num_experts: 192
54
+ # moe_topk: 8
55
+ # moe_intermediate_size: 1536
56
+ # moe_layer_num_skipped: 1
57
+ # output_router_logits: true
finetune/llama_factory_support/hy_v3_lora_sft.yaml ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### model
2
+ model_name_or_path: ../hf
3
+ trust_remote_code: true
4
+
5
+ ### method
6
+ stage: sft
7
+ do_train: true
8
+ finetuning_type: lora
9
+ deepspeed: ../deepspeed_support/ds_zero2_offload.json
10
+
11
+ ### LoRA parameters
12
+ lora_rank: 64
13
+ lora_alpha: 128
14
+ lora_dropout: 0.05
15
+ lora_target: q_proj,k_proj,v_proj,o_proj
16
+
17
+ ### dataset
18
+ dataset_dir: .
19
+ dataset: hy_v3_demo
20
+ template: hy_v3
21
+ cutoff_len: 4096 # Use shorter context for LoRA to save memory; increase if needed
22
+ max_samples: 1000
23
+ overwrite_cache: true
24
+
25
+ ### output
26
+ output_dir: saves/hy_v3/lora/sft
27
+ logging_steps: 10
28
+ save_steps: 500
29
+ plot_loss: true
30
+ overwrite_output_dir: true
31
+ save_only_model: false
32
+ report_to: none # choices: [none, wandb, tensorboard, swanlab, mlflow]
33
+
34
+ ### train
35
+ per_device_train_batch_size: 1
36
+ gradient_accumulation_steps: 4
37
+ learning_rate: 2.0e-4
38
+ num_train_epochs: 3.0
39
+ lr_scheduler_type: cosine_with_min_lr
40
+ lr_scheduler_kwargs:
41
+ min_lr_rate: 0.1 # min_lr / learning_rate = 2e-5 / 2e-4 = 0.1
42
+ warmup_ratio: 0.1
43
+ bf16: true
44
+ gradient_checkpointing: true
45
+ gradient_checkpointing_kwargs:
46
+ use_reentrant: true
47
+ ddp_timeout: 180000000
48
+ flash_attn: fa2
49
+ resume_from_checkpoint: null
50
+
51
+ ### eval
52
+ # eval_dataset: alpaca_en_demo
53
+ # val_size: 0.1
54
+ # per_device_eval_batch_size: 1
55
+ # eval_strategy: steps
56
+ # eval_steps: 500
finetune/llama_factory_support/hy_v3_patches.py ADDED
@@ -0,0 +1,621 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HYV3 monkey-patches for LLaMA Factory + DeepSpeed ZeRO-3 training.
3
+
4
+ This module applies all necessary runtime patches so that HYV3 (MoE)
5
+ can be trained correctly under LLaMA Factory with DeepSpeed ZeRO-3.
6
+
7
+ Usage:
8
+ Import this module **before** calling `llamafactory-cli train`:
9
+
10
+ import hy_v3_patches # applies patches on import
11
+ # ... then start training
12
+
13
+ Or add to the LLaMA Factory YAML via a custom entry-point wrapper.
14
+
15
+ Patches applied:
16
+ 1. ZeRO-3 key rename + expert fuse + buffer loading
17
+ Handles both inner format (original) and outer format (pre-converted) checkpoints.
18
+ - Key renaming: mlp.router.gate -> mlp.gate, etc.
19
+ - Per-expert -> 3D fuse: experts.N.gate_proj -> experts.gate_up_proj
20
+ - Buffer loading: e_score_correction_bias (ZeRO-3 only handles parameters)
21
+ 2. Tokenizer file copy (CustomSaveCallback)
22
+ Ensures each checkpoint directory is self-contained for inference.
23
+ """
24
+
25
+ import os
26
+ import re
27
+ import logging
28
+ import shutil
29
+ from typing import Optional
30
+
31
+ import torch
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # ============================================================================
36
+ # Patch 1: Key rename + expert fuse + buffer loading for ZeRO-3
37
+ #
38
+ # The checkpoint may be in either inner format (original) or outer format
39
+ # (pre-converted by convert_ckpt_to_outer.py). This patch handles both:
40
+ # - Key renaming: mlp.router.gate -> mlp.gate, etc.
41
+ # - Per-expert -> 3D fuse: experts.N.gate_proj -> experts.gate_up_proj
42
+ # - Buffer loading: e_score_correction_bias (ZeRO-3 only handles parameters)
43
+ #
44
+ # If the checkpoint is already in outer format, the rename/fuse logic is
45
+ # effectively a no-op (no matching keys to transform).
46
+ # ============================================================================
47
+
48
+ # Key renames: checkpoint inner format -> model format
49
+ _CKPT_KEY_RENAMES = [
50
+ ("mlp.router.gate.", "mlp.gate."),
51
+ ("mlp.expert_bias", "mlp.e_score_correction_bias"),
52
+ ("mlp.shared_mlp.", "mlp.shared_experts."),
53
+ # Also handle even older checkpoints that use mlp.gate.wg
54
+ ("mlp.gate.wg.", "mlp.gate."),
55
+ ]
56
+
57
+ # Regex to match per-expert keys in checkpoint
58
+ # e.g. model.layers.10.mlp.experts.5.gate_proj.weight
59
+ _EXPERT_KEY_RE = re.compile(
60
+ r"^(.*\.mlp\.experts\.)(\d+)\.(gate_proj|up_proj|down_proj)\.weight$"
61
+ )
62
+
63
+
64
+ def _apply_buffer_loading_patch():
65
+ """Patch the DeepSpeed ZeRO-3 state_dict loader to handle:
66
+ 1. Key renaming (inner checkpoint format -> model format)
67
+ 2. Per-expert -> 3D fused expert tensors
68
+ 3. Manual buffer loading (e_score_correction_bias etc.)
69
+
70
+ ZeRO-3's _load_state_dict_into_zero3_model only handles named_parameters.
71
+ Buffers like e_score_correction_bias must be loaded manually.
72
+ """
73
+ try:
74
+ from transformers.integrations.deepspeed import (
75
+ _load_state_dict_into_zero3_model as _orig_load_zero3,
76
+ )
77
+ import transformers.integrations.deepspeed as _ds_mod
78
+ import transformers.modeling_utils as _mu_mod
79
+ except ImportError:
80
+ logger.warning(
81
+ "Could not import transformers.integrations.deepspeed; "
82
+ "buffer loading patch NOT applied."
83
+ )
84
+ return
85
+
86
+ def _patched_load_zero3(model_to_load, state_dict, *args, **kwargs):
87
+ # Step 1: Key rename + per-expert collection
88
+ new_sd = {}
89
+ expert_groups = {} # prefix -> {expert_idx -> {proj_name -> tensor}}
90
+
91
+ for k, v in state_dict.items():
92
+ m = _EXPERT_KEY_RE.match(k)
93
+ if m:
94
+ # Per-expert key: collect for fusion
95
+ prefix = m.group(1)
96
+ expert_idx = int(m.group(2))
97
+ proj_name = m.group(3)
98
+ if prefix not in expert_groups:
99
+ expert_groups[prefix] = {}
100
+ if expert_idx not in expert_groups[prefix]:
101
+ expert_groups[prefix][expert_idx] = {}
102
+ expert_groups[prefix][expert_idx][proj_name] = v
103
+ else:
104
+ # Non-expert key: apply simple renames
105
+ new_k = k
106
+ for old_sub, new_sub in _CKPT_KEY_RENAMES:
107
+ if old_sub in new_k:
108
+ new_k = new_k.replace(old_sub, new_sub)
109
+ break
110
+ new_sd[new_k] = v
111
+
112
+ # Step 2: Fuse expert groups into 3D tensors
113
+ if expert_groups:
114
+ for prefix in sorted(expert_groups.keys()):
115
+ experts = expert_groups[prefix]
116
+ num_experts = max(experts.keys()) + 1
117
+ gate_up_list = []
118
+ down_list = []
119
+ for i in range(num_experts):
120
+ if i not in experts:
121
+ logger.warning(
122
+ "HYV3 Patch 1: Missing expert %d in %s", i, prefix
123
+ )
124
+ continue
125
+ exp = experts[i]
126
+ if "gate_proj" in exp and "up_proj" in exp:
127
+ gate_up_list.append(
128
+ torch.cat([exp["gate_proj"], exp["up_proj"]], dim=0)
129
+ )
130
+ if "down_proj" in exp:
131
+ down_list.append(exp["down_proj"])
132
+ if gate_up_list:
133
+ new_sd[f"{prefix}gate_up_proj"] = torch.stack(gate_up_list, dim=0)
134
+ if down_list:
135
+ new_sd[f"{prefix}down_proj"] = torch.stack(down_list, dim=0)
136
+ logger.info(
137
+ "HYV3 Patch 1: Fused %d expert groups from per-expert to 3D format.",
138
+ len(expert_groups)
139
+ )
140
+ del expert_groups
141
+
142
+ # Step 3: Load parameters via original ZeRO-3 loader
143
+ result = _orig_load_zero3(model_to_load, new_sd, *args, **kwargs)
144
+
145
+ # Step 4: Manually load buffers (e.g. e_score_correction_bias)
146
+ # ZeRO-3's loader only handles named_parameters, not named_buffers.
147
+ buffers_loaded = 0
148
+ for name, buf in model_to_load.named_buffers():
149
+ if name in new_sd:
150
+ src_tensor = new_sd[name]
151
+ if isinstance(src_tensor, torch.Tensor):
152
+ buf.data.copy_(src_tensor.to(buf.dtype))
153
+ buffers_loaded += 1
154
+ if isinstance(result, tuple) and len(result) >= 2:
155
+ if isinstance(result[1], set):
156
+ result[1].discard(name)
157
+ if buffers_loaded > 0:
158
+ logger.info(
159
+ "HYV3 Patch 1: Manually loaded %d buffers into model.",
160
+ buffers_loaded
161
+ )
162
+
163
+ del new_sd
164
+ return result
165
+
166
+ _ds_mod._load_state_dict_into_zero3_model = _patched_load_zero3
167
+ _mu_mod._load_state_dict_into_zero3_model = _patched_load_zero3
168
+ logger.info(
169
+ "HYV3 patch applied: ZeRO-3 key rename + expert fuse + buffer loading."
170
+ )
171
+
172
+ # ============================================================================
173
+ # Patch 2: Tokenizer file copy callback
174
+ #
175
+ # Ensures each checkpoint directory is self-contained for inference by
176
+ # copying all tokenizer-related files from the original tokenizer path.
177
+ # ============================================================================
178
+
179
+ # Tokenizer files that should be copied to each checkpoint
180
+ _TOKENIZER_FILES = [
181
+ "generation_config.json",
182
+ "hy.tiktoken",
183
+ "tokenizer_config.json",
184
+ "tokenization_hy.py",
185
+ "tokenizer.json",
186
+ "special_tokens_map.json",
187
+ "chat_template.jinja",
188
+ ]
189
+
190
+ def _copy_tokenizer_to_checkpoint(tokenizer_dir: str, checkpoint_dir: str):
191
+ """Copy tokenizer files from tokenizer_dir to checkpoint_dir."""
192
+ for fname in _TOKENIZER_FILES:
193
+ src = os.path.join(tokenizer_dir, fname)
194
+ if os.path.isfile(src):
195
+ shutil.copy(src, os.path.join(checkpoint_dir, fname))
196
+
197
+ # ============================================================================
198
+ # LLaMA Factory Callback: integrates patch 2 (tokenizer copy) into the
199
+ # training loop.
200
+ # ============================================================================
201
+
202
+ try:
203
+ from transformers import TrainerCallback
204
+ from transformers.trainer_utils import PREFIX_CHECKPOINT_DIR
205
+
206
+ class HYV3PatchCallback(TrainerCallback):
207
+ """
208
+ LLaMA Factory compatible callback that applies HYV3-specific patches.
209
+
210
+ Add to your YAML or pass to Trainer:
211
+ callbacks: [hy_v3_patches.HYV3PatchCallback]
212
+ """
213
+
214
+ def __init__(self, tokenizer_dir: Optional[str] = None):
215
+ """
216
+ Args:
217
+ tokenizer_dir: Path to the original tokenizer directory.
218
+ If None, will try to use model_name_or_path from training args.
219
+ """
220
+ self._tokenizer_dir = tokenizer_dir
221
+
222
+ def on_save(self, args, state, control, **kwargs):
223
+ # --- Patch 2: Copy tokenizer files ---
224
+ if torch.distributed.is_initialized() and torch.distributed.get_rank() != 0:
225
+ return control
226
+
227
+ checkpoint_dir = os.path.join(
228
+ args.output_dir,
229
+ f"{PREFIX_CHECKPOINT_DIR}-{state.global_step}",
230
+ )
231
+
232
+ # Determine tokenizer directory
233
+ tokenizer_dir = self._tokenizer_dir
234
+ if tokenizer_dir is None:
235
+ # Try common locations
236
+ tokenizer_dir = getattr(args, "tokenizer_name_or_path", None)
237
+ if tokenizer_dir is None:
238
+ tokenizer_dir = getattr(args, "model_name_or_path", None)
239
+
240
+ if tokenizer_dir and os.path.isdir(tokenizer_dir):
241
+ _copy_tokenizer_to_checkpoint(tokenizer_dir, checkpoint_dir)
242
+ logger.info(
243
+ "HYV3: Copied tokenizer files from %s to %s",
244
+ tokenizer_dir, checkpoint_dir
245
+ )
246
+
247
+ return control
248
+
249
+ except ImportError:
250
+ logger.warning(
251
+ "transformers not available; HYV3PatchCallback not defined."
252
+ )
253
+
254
+ # ============================================================================
255
+ # Patch 3: Memory-efficient shard-by-shard model loading for ZeRO-3
256
+ #
257
+ # The default transformers from_pretrained + ZeRO-3 path loads ALL shards
258
+ # into a single state_dict in CPU memory before distributing. For a ~670GB
259
+ # model with 8 processes per node, this causes CPU OOM.
260
+ #
261
+ # This patch replaces from_pretrained with a shard-by-shard loader that:
262
+ # 1. Creates the model skeleton under deepspeed.zero.Init (meta tensors)
263
+ # 2. Loads each safetensors shard one at a time (~7GB each)
264
+ # 3. Applies key renames + expert fusion per shard
265
+ # 4. Scatters into ZeRO-3 partitions immediately
266
+ # 5. Frees the shard before loading the next one
267
+ #
268
+ # This reduces per-rank CPU memory from ~670GB to ~7GB.
269
+ # ============================================================================
270
+
271
+ def _apply_shard_loading_patch():
272
+ """Monkey-patch AutoModelForCausalLM.from_pretrained to use shard-by-shard
273
+ loading when DeepSpeed ZeRO-3 is active."""
274
+ import gc
275
+ import json as _json
276
+ import transformers
277
+
278
+ _orig_from_pretrained = transformers.AutoModelForCausalLM.from_pretrained
279
+
280
+ def _shard_loading_from_pretrained(pretrained_model_name_or_path, *args, **kwargs):
281
+ """Memory-efficient from_pretrained that loads shards one at a time."""
282
+ import deepspeed
283
+
284
+ model_path = pretrained_model_name_or_path
285
+
286
+ # Only apply shard loading if:
287
+ # 1. It's a local directory with safetensors
288
+ # 2. DeepSpeed ZeRO-3 is being used
289
+ if not (isinstance(model_path, str) and os.path.isdir(model_path)):
290
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
291
+
292
+ index_file = os.path.join(model_path, "model.safetensors.index.json")
293
+ single_file = os.path.join(model_path, "model.safetensors")
294
+ if not (os.path.isfile(index_file) or os.path.isfile(single_file)):
295
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
296
+
297
+ # Try to determine the DeepSpeed config
298
+ ds_config = None
299
+
300
+ # Check if there's a deepspeed config in the HfTrainerDeepSpeedConfig
301
+ try:
302
+ from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled
303
+ if not is_deepspeed_zero3_enabled():
304
+ logger.info(
305
+ "[HYV3 Patch 3] ZeRO-3 not enabled, using default loader."
306
+ )
307
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
308
+ except (ImportError, Exception):
309
+ # If we can't determine, try to proceed anyway
310
+ pass
311
+
312
+ # Get the deepspeed config from HF's global state
313
+ try:
314
+ from transformers.integrations.deepspeed import deepspeed_config as _get_ds_config
315
+ ds_config = _get_ds_config()
316
+ except (ImportError, Exception):
317
+ ds_config = None
318
+
319
+ if ds_config is None:
320
+ # Fallback: try weak ref approach
321
+ try:
322
+ from transformers.integrations import deepspeed as _hf_ds
323
+ if hasattr(_hf_ds, '_hf_deepspeed_config_weak_ref'):
324
+ _weak_ref = _hf_ds._hf_deepspeed_config_weak_ref
325
+ if _weak_ref is not None:
326
+ ds_obj = _weak_ref()
327
+ if ds_obj is not None:
328
+ ds_config = ds_obj.config
329
+ except (ImportError, AttributeError, Exception):
330
+ pass
331
+
332
+ if ds_config is None:
333
+ # Last resort: look for the config file path in environment
334
+ ds_config_path = os.environ.get("DEEPSPEED_CONFIG_FILE", None)
335
+ if ds_config_path is None:
336
+ ds_config_path = os.environ.get("DEEPSPEED_CONFIG", None)
337
+ if ds_config_path and os.path.isfile(ds_config_path):
338
+ with open(ds_config_path, "r") as f:
339
+ ds_config = _json.load(f)
340
+
341
+ if ds_config is None:
342
+ logger.warning(
343
+ "[HYV3 Patch 3] Cannot determine DeepSpeed config, "
344
+ "falling back to default from_pretrained."
345
+ )
346
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
347
+
348
+ # Ensure ds_config is a dict
349
+ if hasattr(ds_config, 'config'):
350
+ ds_config = ds_config.config
351
+ if not isinstance(ds_config, dict):
352
+ logger.warning(
353
+ "[HYV3 Patch 3] ds_config is not a dict (%s), falling back.",
354
+ type(ds_config)
355
+ )
356
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
357
+
358
+ # Check if it's actually ZeRO stage 3
359
+ zero_stage = ds_config.get("zero_optimization", {}).get("stage", 0)
360
+ if zero_stage != 3:
361
+ logger.info(
362
+ "[HYV3 Patch 3] Not ZeRO-3 (stage=%d), using default loader.",
363
+ zero_stage
364
+ )
365
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
366
+
367
+ logger.info(
368
+ "[HYV3 Patch 3] Using shard-by-shard loading for model at: %s",
369
+ model_path
370
+ )
371
+
372
+ try:
373
+ from safetensors import safe_open
374
+ from transformers.integrations.deepspeed import (
375
+ _load_state_dict_into_zero3_model as _load_zero3,
376
+ )
377
+ except ImportError as e:
378
+ logger.warning(
379
+ "[HYV3 Patch 3] Required imports not available (%s), "
380
+ "falling back to default from_pretrained.", e
381
+ )
382
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
383
+
384
+ # Replace "auto" values that deepspeed.zero.Init cannot resolve
385
+ ds_config_copy = _json.loads(_json.dumps(ds_config))
386
+ _auto_defaults = {
387
+ "train_batch_size": 32,
388
+ "train_micro_batch_size_per_gpu": 1,
389
+ "gradient_accumulation_steps": 1,
390
+ "gradient_clipping": 1.0,
391
+ }
392
+ for k, v in _auto_defaults.items():
393
+ if k in ds_config_copy and ds_config_copy[k] == "auto":
394
+ ds_config_copy[k] = v
395
+
396
+ # Determine dtype
397
+ torch_dtype = kwargs.pop("torch_dtype", torch.bfloat16)
398
+ if torch_dtype is None or torch_dtype == "auto":
399
+ torch_dtype = torch.bfloat16
400
+ trust_remote_code = kwargs.pop("trust_remote_code", True)
401
+ attn_implementation = kwargs.pop("attn_implementation", None)
402
+ # Pop config if already provided by caller (e.g. LLaMA Factory)
403
+ config = kwargs.pop("config", None)
404
+
405
+ # Step 1: Create model skeleton under ZeRO-3 Init (meta tensors)
406
+ if config is None:
407
+ config = transformers.AutoConfig.from_pretrained(
408
+ model_path, trust_remote_code=trust_remote_code
409
+ )
410
+ with deepspeed.zero.Init(
411
+ dtype=torch_dtype, config_dict_or_path=ds_config_copy
412
+ ):
413
+ model = transformers.AutoModelForCausalLM.from_config(
414
+ config,
415
+ trust_remote_code=trust_remote_code,
416
+ torch_dtype=torch_dtype,
417
+ attn_implementation=attn_implementation,
418
+ )
419
+ logger.info("[HYV3 Patch 3] Model skeleton created under ZeRO-3 Init.")
420
+
421
+ # Step 2: Determine shard files
422
+ if os.path.isfile(index_file):
423
+ with open(index_file, "r") as f:
424
+ index_data = _json.load(f)
425
+ shard_files = list(dict.fromkeys(index_data["weight_map"].values()))
426
+ else:
427
+ shard_files = ["model.safetensors"]
428
+
429
+ # Step 3: Load each shard and scatter into ZeRO-3 model
430
+ total_shards = len(shard_files)
431
+ all_loaded_keys = set()
432
+ pending_experts = {} # prefix -> {expert_idx -> {proj_name -> tensor}}
433
+
434
+ for shard_idx, shard_name in enumerate(shard_files, 1):
435
+ shard_path = os.path.join(model_path, shard_name)
436
+ logger.info(
437
+ "[HYV3 Patch 3] Loading shard %d/%d: %s",
438
+ shard_idx, total_shards, shard_name
439
+ )
440
+
441
+ # Load shard into CPU memory
442
+ shard_sd = {}
443
+ with safe_open(shard_path, framework="pt", device="cpu") as f:
444
+ for key in f.keys():
445
+ shard_sd[key] = f.get_tensor(key)
446
+
447
+ # Separate expert keys from non-expert keys, apply renames
448
+ renamed_sd = {}
449
+ expert_keys_in_shard = {}
450
+
451
+ for k, v in shard_sd.items():
452
+ m = _EXPERT_KEY_RE.match(k)
453
+ if m:
454
+ prefix = m.group(1)
455
+ expert_idx = int(m.group(2))
456
+ proj_name = m.group(3)
457
+ if prefix not in expert_keys_in_shard:
458
+ expert_keys_in_shard[prefix] = {}
459
+ if expert_idx not in expert_keys_in_shard[prefix]:
460
+ expert_keys_in_shard[prefix][expert_idx] = {}
461
+ expert_keys_in_shard[prefix][expert_idx][proj_name] = v
462
+ else:
463
+ new_k = k
464
+ for old_sub, new_sub in _CKPT_KEY_RENAMES:
465
+ if old_sub in new_k:
466
+ new_k = new_k.replace(old_sub, new_sub)
467
+ break
468
+ renamed_sd[new_k] = v
469
+ del shard_sd
470
+
471
+ # Merge expert keys into pending_experts
472
+ for prefix, experts in expert_keys_in_shard.items():
473
+ if prefix not in pending_experts:
474
+ pending_experts[prefix] = {}
475
+ for idx, projs in experts.items():
476
+ if idx not in pending_experts[prefix]:
477
+ pending_experts[prefix][idx] = {}
478
+ pending_experts[prefix][idx].update(projs)
479
+ del expert_keys_in_shard
480
+
481
+ # Check for completed expert groups
482
+ completed_prefixes = []
483
+ for prefix, experts in pending_experts.items():
484
+ if not experts:
485
+ continue
486
+ max_idx = max(experts.keys())
487
+ num_experts_found = len(experts)
488
+ all_complete = all(
489
+ len(projs) == 3 for projs in experts.values()
490
+ )
491
+ if all_complete and num_experts_found == (max_idx + 1):
492
+ completed_prefixes.append(prefix)
493
+
494
+ # Fuse completed expert groups
495
+ for prefix in completed_prefixes:
496
+ experts = pending_experts.pop(prefix)
497
+ num_experts_layer = max(experts.keys()) + 1
498
+ gate_up_list = []
499
+ down_list = []
500
+ for i in range(num_experts_layer):
501
+ exp = experts[i]
502
+ gate_up = torch.cat([exp["gate_proj"], exp["up_proj"]], dim=0)
503
+ gate_up_list.append(gate_up)
504
+ down_list.append(exp["down_proj"])
505
+ fused_gate_up = torch.stack(gate_up_list, dim=0)
506
+ fused_down = torch.stack(down_list, dim=0)
507
+ del gate_up_list, down_list, experts
508
+ renamed_sd[f"{prefix}gate_up_proj"] = fused_gate_up
509
+ renamed_sd[f"{prefix}down_proj"] = fused_down
510
+ logger.info(
511
+ "[HYV3 Patch 3] Fused %d experts for %s",
512
+ num_experts_layer, prefix
513
+ )
514
+
515
+ # Scatter this shard's weights into ZeRO-3 model
516
+ if renamed_sd:
517
+ _load_zero3(model, renamed_sd)
518
+ # Also load buffers
519
+ for name, buf in model.named_buffers():
520
+ if name in renamed_sd:
521
+ src_tensor = renamed_sd[name]
522
+ if isinstance(src_tensor, torch.Tensor):
523
+ buf.data.copy_(src_tensor.to(buf.dtype))
524
+ all_loaded_keys.update(renamed_sd.keys())
525
+ del renamed_sd
526
+ gc.collect()
527
+
528
+ # Flush remaining pending experts
529
+ if pending_experts:
530
+ logger.info(
531
+ "[HYV3 Patch 3] Flushing %d remaining expert group(s)...",
532
+ len(pending_experts)
533
+ )
534
+ flush_sd = {}
535
+ for prefix, experts in pending_experts.items():
536
+ num_experts_layer = max(experts.keys()) + 1
537
+ gate_up_list = []
538
+ down_list = []
539
+ for i in range(num_experts_layer):
540
+ if i not in experts:
541
+ logger.warning(
542
+ "[HYV3 Patch 3] Missing expert %d in %s", i, prefix
543
+ )
544
+ continue
545
+ exp = experts[i]
546
+ gate_up = torch.cat([exp["gate_proj"], exp["up_proj"]], dim=0)
547
+ gate_up_list.append(gate_up)
548
+ down_list.append(exp["down_proj"])
549
+ if gate_up_list:
550
+ fused_gate_up = torch.stack(gate_up_list, dim=0)
551
+ fused_down = torch.stack(down_list, dim=0)
552
+ flush_sd[f"{prefix}gate_up_proj"] = fused_gate_up
553
+ flush_sd[f"{prefix}down_proj"] = fused_down
554
+ logger.info(
555
+ "[HYV3 Patch 3] Fused %d experts for %s",
556
+ len(gate_up_list), prefix
557
+ )
558
+ del gate_up_list, down_list
559
+ del pending_experts
560
+
561
+ if flush_sd:
562
+ _load_zero3(model, flush_sd)
563
+ for name, buf in model.named_buffers():
564
+ if name in flush_sd:
565
+ src_tensor = flush_sd[name]
566
+ if isinstance(src_tensor, torch.Tensor):
567
+ buf.data.copy_(src_tensor.to(buf.dtype))
568
+ all_loaded_keys.update(flush_sd.keys())
569
+ del flush_sd
570
+ gc.collect()
571
+
572
+ # Report missing/unexpected keys
573
+ model_keys = set(n for n, _ in model.named_parameters())
574
+ model_keys.update(n for n, _ in model.named_buffers())
575
+ missing = model_keys - all_loaded_keys
576
+ unexpected = all_loaded_keys - model_keys
577
+ if missing:
578
+ real_missing = {k for k in missing if "lm_head" not in k}
579
+ if real_missing:
580
+ logger.warning(
581
+ "[HYV3 Patch 3] %d keys not found in checkpoint (first 10): %s",
582
+ len(real_missing), list(real_missing)[:10]
583
+ )
584
+ if unexpected:
585
+ logger.warning(
586
+ "[HYV3 Patch 3] %d unexpected keys (first 10): %s",
587
+ len(unexpected), list(unexpected)[:10]
588
+ )
589
+ logger.info(
590
+ "[HYV3 Patch 3] Shard-by-shard loading complete. "
591
+ "Loaded %d keys from %d shards.",
592
+ len(all_loaded_keys), total_shards
593
+ )
594
+
595
+ return model
596
+
597
+ # Apply the monkey-patch
598
+ transformers.AutoModelForCausalLM.from_pretrained = staticmethod(_shard_loading_from_pretrained)
599
+ logger.info(
600
+ "HYV3 patch applied: shard-by-shard model loading for ZeRO-3 "
601
+ "(reduces CPU memory from ~670GB to ~7GB per rank)."
602
+ )
603
+
604
+
605
+ # ============================================================================
606
+ # Auto-apply patches on import
607
+ # ============================================================================
608
+
609
+ # Patch 1: ZeRO-3 key rename + expert fuse + buffer loading
610
+ _apply_buffer_loading_patch()
611
+
612
+ # Patch 3: Memory-efficient shard-by-shard loading
613
+ _apply_shard_loading_patch()
614
+
615
+ # Patch 2 (tokenizer copy) is applied via HYV3PatchCallback during training.
616
+ # Users should add HYV3PatchCallback to their Trainer callbacks.
617
+
618
+ logger.info(
619
+ "HYV3 patches module loaded. Remember to add HYV3PatchCallback to "
620
+ "your Trainer callbacks for tokenizer file copy on save."
621
+ )
finetune/llama_factory_support/hy_v3_template.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HYV3 chat template registration for LLaMA Factory.
3
+
4
+ Usage:
5
+ 1. Copy this file's register_template block into LLaMA Factory's
6
+ src/llamafactory/data/template.py (for upstream MR).
7
+ 2. Or import this module before training to register at runtime:
8
+ import hy_v3_template
9
+ """
10
+
11
+ from llamafactory.data.template import ReasoningTemplate, register_template
12
+ from llamafactory.data.formatter import EmptyFormatter, StringFormatter
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # HYV3 (MoE, pure text) chat template
17
+ #
18
+ # Token format (from chat_template.jinja & tokenizer_config.json):
19
+ # BOS: <|hy_begin▁of▁sentence|>
20
+ # System: {system_content} (directly after BOS, no role tag)
21
+ # User: <|hy_User|>{user_content}
22
+ # Assistant: <|hy_Assistant|>{assistant_content}<|hy_eos|>
23
+ # EOS: <|hy_eos|>
24
+ #
25
+ # Loss mask: only compute loss on assistant content (including <|hy_eos|>).
26
+ #
27
+ # Note: The system message has NO explicit role token -- it is placed right
28
+ # after BOS. The eos_token is <|hy_eos|>.
29
+ #
30
+ # Reasoning: Supports think tags via ReasoningTemplate.
31
+ # - thought_words: ("<think>", "</think>") matching jinja template
32
+ # - enable_thinking: set globally via data_args.enable_thinking (default True)
33
+ # - Training data always includes think tags (empty or with content)
34
+ # ---------------------------------------------------------------------------
35
+
36
+ register_template(
37
+ name="hy_v3",
38
+ template_class=ReasoningTemplate,
39
+ format_user=StringFormatter(slots=["<|hy_User|>{{content}}"]),
40
+ format_assistant=StringFormatter(slots=["<|hy_Assistant|>{{content}}", {"eos_token"}]),
41
+ format_system=StringFormatter(slots=["{{content}}"]),
42
+ format_prefix=EmptyFormatter(slots=[{"bos_token"}]),
43
+ thought_words=("<think>", "</think>"),
44
+ stop_words=["<|hy_eos|>"],
45
+ efficient_eos=True,
46
+ )
finetune/llama_factory_support/train_hy_v3.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLaMA Factory training entry-point wrapper for HYV3.
3
+
4
+ This script:
5
+ 1. Registers the hy_v3 chat template
6
+ 2. Applies all HYV3 monkey-patches (checkpoint key rename, dtype fix, etc.)
7
+ 3. Injects HYV3PatchCallback into the training loop
8
+ 4. Calls run_exp() to start LLaMA Factory training
9
+
10
+ How it works:
11
+ - train_lf.sh launches this script via torchrun directly:
12
+ torchrun ... train_hy_v3.py hy_v3_full_sft.yaml
13
+ - Each torchrun worker executes this script, so all patches are applied
14
+ in every worker process before training begins.
15
+ - We call run_exp() directly (not the CLI launcher) to avoid the
16
+ launcher re-spawning workers and losing our patches.
17
+
18
+ Usage:
19
+ # Via launch script (recommended):
20
+ bash train_lf.sh
21
+
22
+ # Direct single-node (8 GPUs):
23
+ torchrun --nproc_per_node 8 train_hy_v3.py hy_v3_full_sft.yaml
24
+ """
25
+
26
+ import sys
27
+ import os
28
+
29
+ # Add current directory to path so patches can be imported
30
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
31
+
32
+ # Step 1: Register HYV3 template (must be before training starts)
33
+ import hy_v3_template # noqa: F401
34
+
35
+ # Step 2: Apply checkpoint key rename patch (must be before model loading)
36
+ import hy_v3_patches # noqa: F401
37
+
38
+ # Step 3: Inject HYV3PatchCallback into LLaMA Factory's training flow
39
+ from llamafactory.train.sft.workflow import run_sft as _orig_run_sft
40
+
41
+
42
+ def _patched_run_sft(model_args, data_args, training_args, finetuning_args, generating_args, callbacks=None):
43
+ """Wrap run_sft to inject HYV3PatchCallback."""
44
+ if callbacks is None:
45
+ callbacks = []
46
+
47
+ # Determine tokenizer directory for the save callback
48
+ tokenizer_dir = getattr(model_args, "model_name_or_path", None)
49
+ callbacks.append(hy_v3_patches.HYV3PatchCallback(tokenizer_dir=tokenizer_dir))
50
+
51
+ return _orig_run_sft(model_args, data_args, training_args, finetuning_args, generating_args, callbacks=callbacks)
52
+
53
+
54
+ # Monkey-patch the SFT workflow
55
+ import llamafactory.train.sft.workflow as _sft_wf
56
+ _sft_wf.run_sft = _patched_run_sft
57
+
58
+
59
+ def main():
60
+ """Entry point: called by torchrun in each worker process.
61
+
62
+ Since train_lf.sh launches us via torchrun directly, all patches
63
+ (template registration, checkpoint key rename, SFT callback injection)
64
+ are already applied in this process. We just call run_exp() to start
65
+ training — no need to go through the CLI launcher.
66
+ """
67
+ from llamafactory.train.tuner import run_exp
68
+ run_exp()
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
finetune/llama_factory_support/train_lf.sh ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # ============================================================================
3
+ # LLaMA Factory training launch script for HYV3
4
+ #
5
+ # This script sets up the environment and launches training via torchrun.
6
+ #
7
+ # We use train_hy_v3.py as the entry point (not llamafactory-cli)
8
+ # because we need to inject HYV3-specific monkey-patches and register
9
+ # the hy_v3 chat template BEFORE LLaMA Factory starts.
10
+ # train_hy_v3.py directly calls run_exp() in each torchrun worker,
11
+ # ensuring all patches are active.
12
+ #
13
+ # Usage:
14
+ # Single node: bash train_lf.sh
15
+ # Multi-node: Run this script on EACH node with the same IP_LIST.
16
+ # IP_LIST="10.0.0.1,10.0.0.2" bash train_lf.sh
17
+ # ============================================================================
18
+
19
+ set -euo pipefail
20
+
21
+ # -------------------- Network Configuration --------------------
22
+ NET_TYPE="high"
23
+ export NCCL_DEBUG=WARN
24
+ export NCCL_P2P_LEVEL=NVL
25
+ export NCCL_IB_TIMEOUT=24
26
+ export NCCL_NVLS_ENABLE=0
27
+ export NCCL_MPI_PROFILE_PRIMS_ENABLE=0
28
+ export CUDA_DEVICE_MAX_CONNECTIONS=1
29
+ export TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC=3600
30
+ if [[ "${NET_TYPE}" = "low" ]]; then
31
+ export NCCL_SOCKET_IFNAME=eth1
32
+ export NCCL_IB_GID_INDEX=3
33
+ export NCCL_IB_HCA=mlx5_2:1
34
+ export NCCL_IB_SL=3
35
+ export NCCL_CHECK_DISABLE=1
36
+ export NCCL_P2P_DISABLE=0
37
+ export NCCL_LL_THRESHOLD=16384
38
+ export NCCL_IB_CUDA_SUPPORT=1
39
+ else
40
+ export NCCL_IB_GID_INDEX=3
41
+ export NCCL_IB_SL=3
42
+ export NCCL_CHECK_DISABLE=1
43
+ export NCCL_P2P_DISABLE=0
44
+ export NCCL_IB_DISABLE=0
45
+ export NCCL_LL_THRESHOLD=16384
46
+ export NCCL_IB_CUDA_SUPPORT=1
47
+ export NCCL_SOCKET_IFNAME=bond1
48
+ export UCX_NET_DEVICES=bond1
49
+ export NCCL_IB_HCA=mlx5_bond_1,mlx5_bond_5,mlx5_bond_3,mlx5_bond_7,mlx5_bond_4,mlx5_bond_8,mlx5_bond_2,mlx5_bond_6
50
+ export NCCL_COLLNET_ENABLE=0
51
+ export SHARP_COLL_ENABLE_SAT=0
52
+ export NCCL_NET_GDR_LEVEL=2
53
+ export NCCL_IB_QPS_PER_CONNECTION=4
54
+ export NCCL_IB_TC=160
55
+ export NCCL_PXN_DISABLE=1
56
+ fi
57
+
58
+ # Skip LLaMA Factory version check (we use a newer transformers branch)
59
+ export DISABLE_VERSION_CHECK=1
60
+
61
+ # -------------------- Node Configuration --------------------
62
+ export HOST_GPU_NUM=8
63
+ # IP list, comma separated. e.g. "10.0.0.1,10.0.0.2" or single node "127.0.0.1"
64
+ export IP_LIST=${IP_LIST:-"127.0.0.1"}
65
+
66
+ MASTER_PORT=${MASTER_PORT:-29500}
67
+
68
+ IFS=',' read -ra IP_ARRAY <<< "$IP_LIST"
69
+ NODES=${#IP_ARRAY[@]}
70
+ MASTER_ADDR=${IP_ARRAY[0]}
71
+
72
+ # -------------------- Paths --------------------
73
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
74
+ YAML_FILE="${SCRIPT_DIR}/hy_v3_full_sft.yaml"
75
+ ENTRY_SCRIPT="${SCRIPT_DIR}/train_hy_v3.py"
76
+
77
+ # -------------------- Distributed Environment --------------------
78
+ export MASTER_ADDR="${MASTER_ADDR}"
79
+ export MASTER_PORT="${MASTER_PORT}"
80
+ export NNODES="${NODES}"
81
+
82
+ if [ ${NODES} -gt 1 ]; then
83
+ # Determine local node rank by matching local IP against IP_LIST
84
+ LOCAL_IP=$(hostname -i | awk '{print $1}')
85
+ NODE_RANK=0
86
+ for i in "${!IP_ARRAY[@]}"; do
87
+ if [[ "${IP_ARRAY[$i]}" == "${LOCAL_IP}" ]]; then
88
+ NODE_RANK=$i
89
+ break
90
+ fi
91
+ done
92
+ export RANK="${NODE_RANK}"
93
+ else
94
+ export RANK=0
95
+ fi
96
+
97
+ echo "============================================"
98
+ echo " HYV3 LLaMA Factory Training"
99
+ echo " Nodes: ${NNODES}, Rank: ${RANK}"
100
+ echo " Master: ${MASTER_ADDR}:${MASTER_PORT}"
101
+ echo " GPUs per node: ${HOST_GPU_NUM}"
102
+ echo " Total GPUs: $((NODES * HOST_GPU_NUM))"
103
+ echo "============================================"
104
+
105
+ # -------------------- Launch --------------------
106
+ # We launch torchrun directly (instead of FORCE_TORCHRUN) so that each
107
+ # worker process runs train_hy_v3.py with all HYV3 patches applied.
108
+ torchrun \
109
+ --nnodes "${NNODES}" \
110
+ --node_rank "${RANK}" \
111
+ --nproc_per_node "${HOST_GPU_NUM}" \
112
+ --master_addr "${MASTER_ADDR}" \
113
+ --master_port "${MASTER_PORT}" \
114
+ "${ENTRY_SCRIPT}" "${YAML_FILE}"
finetune/ms_swift_support/hy_v3_full_sft.yaml ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================================
2
+ # ms-swift SFT config for HYV3 Full-parameter Fine-tuning
3
+ #
4
+ # Usage:
5
+ # YAML_FILE=hy_v3_full_sft.yaml bash sft_train.sh
6
+ #
7
+ # or directly:
8
+ # swift sft --config hy_v3_full_sft.yaml
9
+ #
10
+ # Model: Tencent-Hunyuan/Hy3
11
+ # Template: hy_v3 (natively supported in ms-swift 4.2.2)
12
+ # ============================================================================
13
+
14
+ # ---- Model Settings ----
15
+ model: Tencent-Hunyuan/Hy3
16
+ # model: /path/to/local/Hy3 # use local path if needed
17
+ model_type: hy_v3
18
+ template: hy_v3
19
+ torch_dtype: bfloat16
20
+ attn_impl: flash_attn
21
+ # experts_impl: auto # let ms-swift choose the best MoE expert implementation
22
+
23
+ # ---- Training Method ----
24
+ # Full-parameter SFT: no LoRA settings needed
25
+ # For LoRA, uncomment below:
26
+ # tuner_backend: peft
27
+ # tuner_type: lora
28
+ # lora_rank: 8
29
+ # lora_alpha: 16
30
+ # lora_dropout: 0.05
31
+ # target_modules:
32
+ # - q_proj
33
+ # - k_proj
34
+ # - v_proj
35
+ # - o_proj
36
+ # - gate_proj
37
+ # - up_proj
38
+ # - down_proj
39
+
40
+ # ---- Dataset Settings ----
41
+ # Option 1: Use dataset registered in dataset_info.json
42
+ # dataset:
43
+ # - hy_v3_demo
44
+ # dataset_dir: . # path to dataset_info.json
45
+
46
+ # Option 2: Use local jsonl file directly (RECOMMENDED)
47
+ # ms-swift supports sharegpt-format jsonl directly:
48
+ dataset:
49
+ - ../data/example_data.jsonl
50
+
51
+ max_length: 4096 # HYV3 supports up to 262k context
52
+ truncation_strategy: delete # or "truncation_left"
53
+ lazy_tokenize: true
54
+ preprocess_num_proc: 4
55
+
56
+ # ---- Output Settings ----
57
+ output_dir: saves/hy_v3/full/sft
58
+ save_steps: 500
59
+ save_strategy: steps
60
+ save_total_limit: 3
61
+ save_only_model: false
62
+ logging_steps: 10
63
+ report_to: none # choices: [none, wandb, tensorboard, swanlab, mlflow]
64
+
65
+ # ---- Training Hyperparameters ----
66
+ per_device_train_batch_size: 1
67
+ gradient_accumulation_steps: 8
68
+ learning_rate: 1.0e-5
69
+ num_train_epochs: 3.0
70
+ max_steps: -1 # set to >0 to override num_train_epochs
71
+ warmup_ratio: 0.1
72
+ lr_scheduler_type: cosine
73
+ # lr_scheduler_kwargs:
74
+ # min_lr_rate: 0.1 # min_lr / learning_rate
75
+ bf16: true
76
+ # fp16: false
77
+
78
+ # ---- DeepSpeed / Optimization ----
79
+ deepspeed: zero3_offload # options: zero0, zero2, zero2_offload, zero3, zero3_offload
80
+ # You can also specify a custom deepspeed config file:
81
+ # deepspeed: /path/to/ds_config.json
82
+ gradient_checkpointing: true
83
+ # gradient_checkpointing_kwargs:
84
+ # use_reentrant: true # may be needed for ZeRO-3, test first
85
+ max_grad_norm: 1.0
86
+ weight_decay: 0.1
87
+ adam_beta1: 0.9
88
+ adam_beta2: 0.95
89
+ optim: adamw_torch
90
+
91
+ # ---- Distributed Training ----
92
+ ddp_timeout: 180000000
93
+ # packing: true # enable sequence packing for efficiency
94
+ # packing_length: 4096
95
+
96
+ # ---- Evaluation (optional) ----
97
+ # eval_strategy: steps
98
+ # eval_steps: 500
99
+ # eval_dataset:
100
+ # - hy_v3_eval
101
+ # per_device_eval_batch_size: 1
102
+ # eval_use_evalscope: false
103
+
104
+ # ---- Generation Settings (for inference during/after training) ----
105
+ max_new_tokens: 2048
106
+ temperature: 0.7
107
+ top_p: 0.9
108
+ # stop_words:
109
+ # - "</s>"
110
+ # - "<|im_end|>"
111
+
112
+ # ---- Misc ----
113
+ seed: 42
114
+ ignore_data_skip: true
115
+ resume_from_checkpoint: null # set to checkpoint path to resume
finetune/ms_swift_support/hy_v3_lora_sft.yaml ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================================
2
+ # ms-swift SFT config for HYV3 LoRA Fine-tuning
3
+ #
4
+ # Usage:
5
+ # YAML_FILE=hy_v3_lora_sft.yaml bash sft_train.sh
6
+ #
7
+ # or directly:
8
+ # swift sft --config hy_v3_lora_sft.yaml
9
+ #
10
+ # Model: Tencent-Hunyuan/Hy3
11
+ # Template: hy_v3 (natively supported in ms-swift 4.2.2)
12
+ # Training method: LoRA (Low-Rank Adaptation)
13
+ # ============================================================================
14
+
15
+ # ---- Model Settings ----
16
+ model: Tencent-Hunyuan/Hy3
17
+ # model: /path/to/local/Hy3 # use local path if needed
18
+ model_type: hy_v3
19
+ template: hy_v3
20
+ torch_dtype: bfloat16
21
+ attn_impl: flash_attn
22
+ # experts_impl: auto
23
+
24
+ # ---- Training Method: LoRA ----
25
+ tuner_backend: peft
26
+ tuner_type: lora
27
+ lora_rank: 8
28
+ lora_alpha: 16
29
+ lora_dropout: 0.05
30
+ # target_modules: # auto-detected if not specified
31
+ # - q_proj
32
+ # - k_proj
33
+ # - v_proj
34
+ # - o_proj
35
+ # - gate_proj
36
+ # - up_proj
37
+ # - down_proj
38
+ # use_rslora: false
39
+ # use_dora: false
40
+
41
+ # ---- Dataset Settings ----
42
+ # Option 1: Use dataset registered in dataset_info.json
43
+ # dataset:
44
+ # - hy_v3_demo
45
+ # dataset_dir: .
46
+
47
+ # Option 2: Use local jsonl file directly (RECOMMENDED)
48
+ dataset:
49
+ - ../data/example_data.jsonl
50
+
51
+ max_length: 4096
52
+ truncation_strategy: delete
53
+ lazy_tokenize: true
54
+ preprocess_num_proc: 4
55
+
56
+ # ---- Output Settings ----
57
+ output_dir: saves/hy_v3/lora/sft
58
+ save_steps: 500
59
+ save_strategy: steps
60
+ save_total_limit: 3
61
+ save_only_model: false
62
+ logging_steps: 10
63
+ report_to: none
64
+
65
+ # ---- Training Hyperparameters ----
66
+ per_device_train_batch_size: 1
67
+ gradient_accumulation_steps: 8
68
+ learning_rate: 3.0e-4 # LoRA typically uses higher LR
69
+ num_train_epochs: 3.0
70
+ max_steps: -1
71
+ warmup_ratio: 0.1
72
+ lr_scheduler_type: cosine
73
+ bf16: true
74
+
75
+ # ---- DeepSpeed / Optimization ----
76
+ deepspeed: ../deepspeed_support/ds_zero3_offload.json # Use custom ZeRO-3 offload config
77
+ gradient_checkpointing: true
78
+ max_grad_norm: 1.0
79
+ weight_decay: 0.1
80
+ adam_beta1: 0.9
81
+ adam_beta2: 0.95
82
+ optim: adamw_torch
83
+
84
+ # ---- Distributed Training ----
85
+ ddp_timeout: 180000000
86
+ # packing: true
87
+ # packing_length: 4096
88
+
89
+ # ---- Evaluation (optional) ----
90
+ # eval_strategy: steps
91
+ # eval_steps: 500
92
+ # eval_dataset:
93
+ # - hy_v3_eval
94
+ # per_device_eval_batch_size: 1
95
+
96
+ # ---- Generation Settings ----
97
+ max_new_tokens: 2048
98
+ temperature: 0.7
99
+ top_p: 0.9
100
+
101
+ # ---- Misc ----
102
+ seed: 42
103
+ ignore_data_skip: true
104
+ resume_from_checkpoint: null
finetune/ms_swift_support/hy_v3_swift_patches.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HYV3 patches for ms-swift + DeepSpeed ZeRO-3 training.
3
+
4
+ This module applies necessary runtime patches so that HYV3 (MoE) can be
5
+ trained correctly under ms-swift with DeepSpeed ZeRO-3.
6
+
7
+ Patches applied:
8
+ 1. Template fix: Re-register hy_v3 template with dynamic [['eos_token_id']]
9
+ for chat_sep and suffix (fixes inference stop token issue).
10
+ 2. Shard-by-shard model loading (Patch 3): Replaces the default
11
+ from_pretrained which loads ALL shards into CPU memory at once,
12
+ causing OOM for large models (~670GB). Instead, loads one shard
13
+ at a time (~7GB each), leveraging transformers 5.8.1's built-in
14
+ conversion_mapping for key rename + expert fusion.
15
+
16
+ Usage:
17
+ swift sft --custom_register_path hy_v3_swift_patches.py ...
18
+ """
19
+
20
+ import os
21
+ import gc
22
+ import json as _json
23
+ import logging
24
+ from typing import Optional
25
+
26
+ import torch
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # ============================================================================
31
+ # Patch 1: Template fix
32
+ # ============================================================================
33
+
34
+ from swift.template.register import TemplateMeta, register_template
35
+ from swift.template.constant import LLMTemplateType
36
+ from swift.template.templates.llm import HyV3Template
37
+
38
+ register_template(
39
+ TemplateMeta(
40
+ LLMTemplateType.hy_v3,
41
+ prefix=['<|hy_begin▁of▁sentence|>'],
42
+ system_prefix=['<|hy_begin▁of▁sentence|>{{SYSTEM}}'],
43
+ prompt=['<|hy_User|>{{QUERY}}<|hy_Assistant|>'],
44
+ chat_sep=[['eos_token_id']],
45
+ suffix=[['eos_token_id']],
46
+ template_cls=HyV3Template,
47
+ is_thinking=True,
48
+ thinking_prefix='',
49
+ non_thinking_prefix='',
50
+ history_thinking_prefix='',
51
+ agent_template='hy_v3',
52
+ ),
53
+ exist_ok=True,
54
+ )
55
+
56
+ logger.info(
57
+ "HYV3 template patch applied: hy_v3 template re-registered with "
58
+ "dynamic [['eos_token_id']] for chat_sep and suffix."
59
+ )
60
+
61
+ # ============================================================================
62
+ # Patch 3: Memory-efficient shard-by-shard model loading for ZeRO-3
63
+ #
64
+ # The default transformers 5.8.1 from_pretrained + ZeRO-3 path loads ALL
65
+ # shards into a single merged_state_dict in CPU memory before distributing.
66
+ # For a ~670GB model with 8 processes per node, this causes CPU OOM.
67
+ #
68
+ # This patch replaces from_pretrained with a shard-by-shard loader that:
69
+ # 1. Creates the model skeleton under deepspeed.zero.Init (meta tensors)
70
+ # 2. Loads each safetensors shard one at a time (~7GB each)
71
+ # 3. Passes each shard through _load_state_dict_into_zero3_model which
72
+ # internally applies the conversion_mapping (key rename + expert fusion)
73
+ # 4. Frees the shard before loading the next one
74
+ #
75
+ # This reduces per-rank CPU memory from ~670GB to ~7GB.
76
+ #
77
+ # Note: Unlike the LLaMA-Factory version, we do NOT need to manually handle
78
+ # key renames or expert fusion here, because transformers 5.8.1's
79
+ # _load_state_dict_into_zero3_model already applies weight_mapping
80
+ # (conversion_mapping) internally.
81
+ # ============================================================================
82
+
83
+ def _apply_shard_loading_patch():
84
+ """Monkey-patch AutoModelForCausalLM.from_pretrained to use shard-by-shard
85
+ loading when DeepSpeed ZeRO-3 is active."""
86
+ import transformers
87
+
88
+ _orig_from_pretrained = transformers.AutoModelForCausalLM.from_pretrained
89
+
90
+ def _shard_loading_from_pretrained(pretrained_model_name_or_path, *args, **kwargs):
91
+ """Memory-efficient from_pretrained that loads shards one at a time."""
92
+ import deepspeed
93
+
94
+ model_path = pretrained_model_name_or_path
95
+
96
+ # Only apply shard loading if:
97
+ # 1. It's a local directory with safetensors
98
+ # 2. DeepSpeed ZeRO-3 is being used
99
+ if not (isinstance(model_path, str) and os.path.isdir(model_path)):
100
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
101
+
102
+ index_file = os.path.join(model_path, "model.safetensors.index.json")
103
+ single_file = os.path.join(model_path, "model.safetensors")
104
+ if not (os.path.isfile(index_file) or os.path.isfile(single_file)):
105
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
106
+
107
+ # Check if ZeRO-3 is enabled
108
+ try:
109
+ from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled
110
+ if not is_deepspeed_zero3_enabled():
111
+ logger.info(
112
+ "[HYV3 Patch 3] ZeRO-3 not enabled, using default loader."
113
+ )
114
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
115
+ except (ImportError, Exception):
116
+ pass
117
+
118
+ # Get the deepspeed config
119
+ ds_config = None
120
+ try:
121
+ from transformers.integrations.deepspeed import deepspeed_config as _get_ds_config
122
+ ds_config = _get_ds_config()
123
+ except (ImportError, Exception):
124
+ ds_config = None
125
+
126
+ if ds_config is None:
127
+ try:
128
+ from transformers.integrations import deepspeed as _hf_ds
129
+ if hasattr(_hf_ds, '_hf_deepspeed_config_weak_ref'):
130
+ _weak_ref = _hf_ds._hf_deepspeed_config_weak_ref
131
+ if _weak_ref is not None:
132
+ ds_obj = _weak_ref()
133
+ if ds_obj is not None:
134
+ ds_config = ds_obj.config
135
+ except (ImportError, AttributeError, Exception):
136
+ pass
137
+
138
+ if ds_config is None:
139
+ ds_config_path = os.environ.get("DEEPSPEED_CONFIG_FILE", None)
140
+ if ds_config_path is None:
141
+ ds_config_path = os.environ.get("DEEPSPEED_CONFIG", None)
142
+ if ds_config_path and os.path.isfile(ds_config_path):
143
+ with open(ds_config_path, "r") as f:
144
+ ds_config = _json.load(f)
145
+
146
+ if ds_config is None:
147
+ logger.warning(
148
+ "[HYV3 Patch 3] Cannot determine DeepSpeed config, "
149
+ "falling back to default from_pretrained."
150
+ )
151
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
152
+
153
+ # Ensure ds_config is a dict
154
+ if hasattr(ds_config, 'config'):
155
+ ds_config = ds_config.config
156
+ if not isinstance(ds_config, dict):
157
+ logger.warning(
158
+ "[HYV3 Patch 3] ds_config is not a dict (%s), falling back.",
159
+ type(ds_config)
160
+ )
161
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
162
+
163
+ # Check if it's actually ZeRO stage 3
164
+ zero_stage = ds_config.get("zero_optimization", {}).get("stage", 0)
165
+ if zero_stage != 3:
166
+ logger.info(
167
+ "[HYV3 Patch 3] Not ZeRO-3 (stage=%d), using default loader.",
168
+ zero_stage
169
+ )
170
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
171
+
172
+ logger.info(
173
+ "[HYV3 Patch 3] Using shard-by-shard loading for model at: %s",
174
+ model_path
175
+ )
176
+
177
+ try:
178
+ from safetensors import safe_open
179
+ from transformers.integrations.deepspeed import (
180
+ _load_state_dict_into_zero3_model as _load_zero3,
181
+ )
182
+ from transformers.conversion_mapping import get_model_conversion_mapping
183
+ from transformers.modeling_utils import LoadStateDictConfig
184
+ except ImportError as e:
185
+ logger.warning(
186
+ "[HYV3 Patch 3] Required imports not available (%s), "
187
+ "falling back to default from_pretrained.", e
188
+ )
189
+ return _orig_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
190
+
191
+ # Replace "auto" values that deepspeed.zero.Init cannot resolve
192
+ ds_config_copy = _json.loads(_json.dumps(ds_config))
193
+ _auto_defaults = {
194
+ "train_batch_size": 32,
195
+ "train_micro_batch_size_per_gpu": 1,
196
+ "gradient_accumulation_steps": 1,
197
+ "gradient_clipping": 1.0,
198
+ }
199
+ for k, v in _auto_defaults.items():
200
+ if k in ds_config_copy and ds_config_copy[k] == "auto":
201
+ ds_config_copy[k] = v
202
+
203
+ # Determine dtype - handle both torch_dtype (old) and dtype (new, transformers >= 4.56)
204
+ torch_dtype = kwargs.pop("torch_dtype", None)
205
+ if torch_dtype is None:
206
+ torch_dtype = kwargs.pop("dtype", torch.bfloat16)
207
+ if torch_dtype is None or torch_dtype == "auto":
208
+ torch_dtype = torch.bfloat16
209
+ if isinstance(torch_dtype, str):
210
+ torch_dtype = getattr(torch, torch_dtype, torch.bfloat16)
211
+
212
+ trust_remote_code = kwargs.pop("trust_remote_code", True)
213
+ attn_implementation = kwargs.pop("attn_implementation", None)
214
+ config = kwargs.pop("config", None)
215
+
216
+ # Step 1: Create model skeleton under ZeRO-3 Init (meta tensors)
217
+ if config is None:
218
+ config = transformers.AutoConfig.from_pretrained(
219
+ model_path, trust_remote_code=trust_remote_code
220
+ )
221
+
222
+ with deepspeed.zero.Init(
223
+ dtype=torch_dtype, config_dict_or_path=ds_config_copy
224
+ ):
225
+ model = transformers.AutoModelForCausalLM.from_config(
226
+ config,
227
+ trust_remote_code=trust_remote_code,
228
+ torch_dtype=torch_dtype,
229
+ attn_implementation=attn_implementation,
230
+ )
231
+ logger.info("[HYV3 Patch 3] Model skeleton created under ZeRO-3 Init.")
232
+
233
+ # Step 2: Get weight conversion mapping (key rename + expert fusion)
234
+ # transformers 5.8.1 has built-in conversion_mapping for hy_v3
235
+ weight_conversions = get_model_conversion_mapping(model, None, None)
236
+
237
+ # Create a minimal load_config with weight_mapping
238
+ load_config = LoadStateDictConfig(
239
+ pretrained_model_name_or_path=model_path,
240
+ weight_mapping=weight_conversions,
241
+ )
242
+
243
+ # Step 3: Determine shard files
244
+ if os.path.isfile(index_file):
245
+ with open(index_file, "r") as f:
246
+ index_data = _json.load(f)
247
+ shard_files = list(dict.fromkeys(index_data["weight_map"].values()))
248
+ else:
249
+ shard_files = ["model.safetensors"]
250
+
251
+ # Step 4: Load each shard and scatter into ZeRO-3 model
252
+ total_shards = len(shard_files)
253
+
254
+ for shard_idx, shard_name in enumerate(shard_files, 1):
255
+ shard_path = os.path.join(model_path, shard_name)
256
+ logger.info(
257
+ "[HYV3 Patch 3] Loading shard %d/%d: %s",
258
+ shard_idx, total_shards, shard_name
259
+ )
260
+
261
+ # Load shard into CPU memory
262
+ shard_sd = {}
263
+ with safe_open(shard_path, framework="pt", device="cpu") as f:
264
+ for key in f.keys():
265
+ shard_sd[key] = f.get_tensor(key)
266
+
267
+ # Use transformers' built-in ZeRO-3 loader which handles:
268
+ # - weight_mapping (key rename + expert fusion via conversion_mapping)
269
+ # - buffer loading
270
+ # - parameter scattering into ZeRO-3 partitions
271
+ _load_zero3(model, shard_sd, load_config)
272
+
273
+ del shard_sd
274
+ gc.collect()
275
+
276
+ logger.info(
277
+ "[HYV3 Patch 3] Shard-by-shard loading complete. "
278
+ "Loaded %d shards.", total_shards
279
+ )
280
+
281
+ # Patch G: Disable output_router_logits to save CPU memory during training.
282
+ # When output_router_logits=True, all 79 MoE layers accumulate router logits
283
+ # tensors throughout forward pass, causing significant memory growth under
284
+ # ZeRO-3 offload. Since router_aux_loss_coef=0.0 (no aux loss), these logits
285
+ # are not needed for training.
286
+ if hasattr(model, 'config') and getattr(model.config, 'output_router_logits', False):
287
+ model.config.output_router_logits = False
288
+ logger.info(
289
+ "[HYV3 Patch G] Disabled output_router_logits to reduce "
290
+ "CPU memory usage during ZeRO-3 offload training."
291
+ )
292
+
293
+ return model
294
+
295
+ # Apply the monkey-patch
296
+ transformers.AutoModelForCausalLM.from_pretrained = staticmethod(_shard_loading_from_pretrained)
297
+ logger.info(
298
+ "HYV3 Patch 3 applied: shard-by-shard model loading for ZeRO-3 "
299
+ "(reduces CPU memory from ~670GB to ~7GB per rank)."
300
+ )
301
+
302
+
303
+ # ============================================================================
304
+ # Auto-apply patches on import
305
+ # ============================================================================
306
+
307
+ _apply_shard_loading_patch()
308
+
309
+ logger.info("HYV3 ms-swift patches loaded successfully.")
finetune/ms_swift_support/sft_train.sh ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # ============================================================================
3
+ # ms-swift SFT training launch script for HYV3
4
+ #
5
+ # ms-swift 4.2.2 has native HYV3 support:
6
+ # - Model registered: LLMModelType.hy_v3
7
+ # - Template registered: TemplateType.hy_v3
8
+ # - Agent template: HyV3AgentTemplate
9
+ # - No monkey-patches needed for basic full-parameter or LoRA SFT.
10
+ #
11
+ # Usage:
12
+ # Single node: bash sft_train.sh
13
+ # Multi-node: Run this script on EACH node with the same IP_LIST.
14
+ # IP_LIST="10.0.0.1,10.0.0.2" bash sft_train.sh
15
+ #
16
+ # Note: ms-swift does NOT support --config parameter.
17
+ # All parameters must be passed directly via command line.
18
+ # ============================================================================
19
+
20
+ set -euo pipefail
21
+
22
+ # -------------------- Network Configuration --------------------
23
+ NET_TYPE="high"
24
+ export NCCL_DEBUG=WARN
25
+ export NCCL_P2P_LEVEL=NVL
26
+ export NCCL_IB_TIMEOUT=24
27
+ export NCCL_NVLS_ENABLE=0
28
+ export NCCL_MPI_PROFILE_PRIMS_ENABLE=0
29
+ export CUDA_DEVICE_MAX_CONNECTIONS=1
30
+ export TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC=3600
31
+ if [[ "${NET_TYPE}" = "low" ]]; then
32
+ export NCCL_SOCKET_IFNAME=eth1
33
+ export NCCL_IB_GID_INDEX=3
34
+ export NCCL_IB_HCA=mlx5_2:1
35
+ export NCCL_IB_SL=3
36
+ export NCCL_CHECK_DISABLE=1
37
+ export NCCL_P2P_DISABLE=0
38
+ export NCCL_LL_THRESHOLD=16384
39
+ export NCCL_IB_CUDA_SUPPORT=1
40
+ else
41
+ export NCCL_IB_GID_INDEX=3
42
+ export NCCL_IB_SL=3
43
+ export NCCL_CHECK_DISABLE=1
44
+ export NCCL_P2P_DISABLE=0
45
+ export NCCL_IB_DISABLE=0
46
+ export NCCL_LL_THRESHOLD=16384
47
+ export NCCL_IB_CUDA_SUPPORT=1
48
+ export NCCL_SOCKET_IFNAME=bond1
49
+ export UCX_NET_DEVICES=bond1
50
+ export NCCL_IB_HCA=mlx5_bond_1,mlx5_bond_5,mlx5_bond_3,mlx5_bond_7,mlx5_bond_4,mlx5_bond_8,mlx5_bond_2,mlx5_bond_6
51
+ export NCCL_COLLNET_ENABLE=0
52
+ export SHARP_COLL_ENABLE_SAT=0
53
+ export NCCL_NET_GDR_LEVEL=2
54
+ export NCCL_IB_QPS_PER_CONNECTION=4
55
+ export NCCL_IB_TC=160
56
+ export NCCL_PXN_DISABLE=1
57
+ fi
58
+
59
+ # -------------------- Node Configuration --------------------
60
+ export HOST_GPU_NUM=8
61
+ # IP list, comma separated. e.g. "10.0.0.1,10.0.0.2" or single node "127.0.0.1"
62
+ export IP_LIST=${IP_LIST:-"127.0.0.1"}
63
+
64
+ MASTER_PORT=${MASTER_PORT:-29500}
65
+
66
+ IFS=',' read -ra IP_ARRAY <<< "$IP_LIST"
67
+ NODES=${#IP_ARRAY[@]}
68
+ MASTER_ADDR=${IP_ARRAY[0]}
69
+
70
+ # -------------------- Distributed Environment --------------------
71
+ export MASTER_ADDR="${MASTER_ADDR}"
72
+ export MASTER_PORT="${MASTER_PORT}"
73
+ export NNODES="${NODES}"
74
+
75
+ if [ ${NODES} -gt 1 ]; then
76
+ # Determine local node rank by matching local IP against IP_LIST
77
+ LOCAL_IP=$(hostname -i | awk '{print $1}')
78
+ NODE_RANK=0
79
+ for i in "${!IP_ARRAY[@]}"; do
80
+ if [[ "${IP_ARRAY[$i]}" == "${LOCAL_IP}" ]]; then
81
+ NODE_RANK=$i
82
+ break
83
+ fi
84
+ done
85
+ export RANK="${NODE_RANK}"
86
+ else
87
+ export RANK=0
88
+ fi
89
+
90
+ echo "============================================"
91
+ echo " HYV3 ms-swift SFT Training"
92
+ echo " Nodes: ${NNODES}, Rank: ${RANK}"
93
+ echo " Master: ${MASTER_ADDR}:${MASTER_PORT}"
94
+ echo " GPUs per node: ${HOST_GPU_NUM}"
95
+ echo " Total GPUs: $((NODES * HOST_GPU_NUM))"
96
+ echo "============================================"
97
+
98
+ # -------------------- Launch --------------------
99
+ # ms-swift does NOT support --config parameter.
100
+ # All parameters must be passed directly via command line.
101
+ # For multi-node, we need to set the distributed env vars and let swift handle it.
102
+
103
+ # Common SFT parameters from hy_v3_full_sft.yaml
104
+ SFT_PARAMS=(
105
+ # ---- Model Settings ----
106
+ --model /path/to/Hy3
107
+ --model_type hy_v3
108
+ --template hy_v3
109
+ --torch_dtype bfloat16
110
+ --tuner_type full
111
+ --attn_impl flash_attn
112
+
113
+ # ---- Dataset Settings ----
114
+ --dataset ../data/example_data.jsonl
115
+ --max_length 4096
116
+ --truncation_strategy delete
117
+ --lazy_tokenize true
118
+ --dataset_num_proc 4
119
+
120
+ # ---- Output Settings ----
121
+ --output_dir saves/hy_v3/full/sft
122
+ --save_steps 500
123
+ --save_strategy steps
124
+ --save_total_limit 3
125
+ --save_only_model false
126
+ --logging_steps 10
127
+ --report_to none
128
+
129
+ # ---- Training Hyperparameters ----
130
+ --per_device_train_batch_size 1
131
+ --gradient_accumulation_steps 1
132
+ --learning_rate 1.0e-5
133
+ --num_train_epochs 3.0
134
+ --max_steps -1
135
+ --warmup_ratio 0.1
136
+ --lr_scheduler_type cosine
137
+ --bf16 true
138
+
139
+ # ---- DeepSpeed / Optimization ----
140
+ --deepspeed zero3_offload
141
+ --gradient_checkpointing true
142
+ --max_grad_norm 1.0
143
+ --weight_decay 0.1
144
+ --adam_beta1 0.9
145
+ --adam_beta2 0.95
146
+ --optim adamw_torch
147
+
148
+ # ---- Distributed Training ----
149
+ --ddp_timeout 180000000
150
+
151
+ # ---- Generation Settings ----
152
+ --max_new_tokens 2048
153
+ --temperature 0.7
154
+ --top_p 0.9
155
+
156
+ # ---- Misc ----
157
+ --seed 42
158
+ --ignore_data_skip true
159
+ )
160
+
161
+ if [ ${NODES} -eq 1 ]; then
162
+ # Single-node: use torchrun to ensure local_world_size is set correctly
163
+ # This avoids the DeepSpeed + device_map compatibility error
164
+ export NODE_RANK=0
165
+ export NNODES=1
166
+
167
+ # Add current directory to PYTHONPATH so hy_v3_swift_patches can be imported
168
+ export PYTHONPATH="${PYTHONPATH:+${PYTHONPATH}:}$(pwd)"
169
+
170
+ torchrun \
171
+ --nproc_per_node "${HOST_GPU_NUM}" \
172
+ --master_port "${MASTER_PORT}" \
173
+ -m swift.cli.sft \
174
+ --custom_register_path hy_v3_swift_patches.py \
175
+ "${SFT_PARAMS[@]}"
176
+ else
177
+ # Multi-node: use torchrun
178
+ # Determine local node rank
179
+ LOCAL_IP=$(hostname -i 2>/dev/null || hostname -I | awk '{print $1}')
180
+ NODE_RANK=0
181
+ for i in "${!IP_ARRAY[@]}"; do
182
+ if [[ "${IP_ARRAY[$i]}" == "${LOCAL_IP}" ]]; then
183
+ NODE_RANK=$i
184
+ break
185
+ fi
186
+ done
187
+
188
+ export NODE_RANK="${NODE_RANK}"
189
+ export NNODES="${NODES}"
190
+ export MASTER_ADDR="${MASTER_ADDR}"
191
+ export MASTER_PORT="${MASTER_PORT}"
192
+
193
+ # Add current directory to PYTHONPATH so hy_v3_swift_patches can be imported
194
+ export PYTHONPATH="${PYTHONPATH:+${PYTHONPATH}:}$(pwd)"
195
+
196
+ torchrun \
197
+ --nnodes "${NNODES}" \
198
+ --node_rank "${NODE_RANK}" \
199
+ --nproc_per_node "${HOST_GPU_NUM}" \
200
+ --master_addr "${MASTER_ADDR}" \
201
+ --master_port "${MASTER_PORT}" \
202
+ -m swift.cli.sft \
203
+ --custom_register_path hy_v3_swift_patches.py \
204
+ "${SFT_PARAMS[@]}"
205
+ fi
finetune/requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ transformers>=5.6.0
2
+ torch>=2.10.0
3
+ torchvision
4
+ torchaudio
5
+ accelerate>=1.11.0
6
+ peft>=0.18.1
7
+ deepspeed>=0.18.7
8
+ sentencepiece
9
+ protobuf
10
+ ninja
11
+ flash-attn
12
+ tensorboard
finetune/tools/check_converted.py ADDED
@@ -0,0 +1,455 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Quick validation script for converted HYV3 outer-format checkpoint.
4
+
5
+ Checks:
6
+ 1. model.safetensors.index.json structure and completeness
7
+ 2. All expected weight keys exist (dense layer 0, MoE layers 1-79)
8
+ 3. Expert tensor shapes (fused 3D format)
9
+ 4. All referenced shard files exist and are non-empty
10
+ 5. Spot-check: load a few shards and verify tensor shapes/dtypes
11
+ 6. No duplicate or orphan keys
12
+
13
+ Usage:
14
+ python check_converted.py <output_dir> [--spot-check N]
15
+
16
+ Example:
17
+ python check_converted.py pretrain_base/hf_outer
18
+ python check_converted.py pretrain_base/hf_outer --spot-check 5
19
+ """
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import sys
25
+ import time
26
+ from collections import defaultdict
27
+
28
+ # ============================================================================
29
+ # Expected key patterns for HYV3 outer format
30
+ # ============================================================================
31
+
32
+ # Dense layer (layer 0) expected suffixes
33
+ DENSE_SUFFIXES = [
34
+ "input_layernorm.weight",
35
+ "post_attention_layernorm.weight",
36
+ "self_attn.q_proj.weight",
37
+ "self_attn.k_proj.weight",
38
+ "self_attn.v_proj.weight",
39
+ "self_attn.o_proj.weight",
40
+ "self_attn.q_norm.weight",
41
+ "self_attn.k_norm.weight",
42
+ "mlp.gate_proj.weight",
43
+ "mlp.up_proj.weight",
44
+ "mlp.down_proj.weight",
45
+ ]
46
+
47
+ # MoE layer (layers 1-79) expected suffixes
48
+ MOE_SUFFIXES = [
49
+ "input_layernorm.weight",
50
+ "post_attention_layernorm.weight",
51
+ "self_attn.q_proj.weight",
52
+ "self_attn.k_proj.weight",
53
+ "self_attn.v_proj.weight",
54
+ "self_attn.o_proj.weight",
55
+ "self_attn.q_norm.weight",
56
+ "self_attn.k_norm.weight",
57
+ # MoE-specific
58
+ "mlp.gate.weight",
59
+ "mlp.e_score_correction_bias",
60
+ "mlp.experts.gate_up_proj",
61
+ "mlp.experts.down_proj",
62
+ "mlp.shared_experts.gate_proj.weight",
63
+ "mlp.shared_experts.up_proj.weight",
64
+ "mlp.shared_experts.down_proj.weight",
65
+ ]
66
+
67
+ # MTP (Multi-Token Prediction) layer expected suffixes
68
+ # MTP layers share MoE structure but have additional projection/norm keys
69
+ MTP_EXTRA_SUFFIXES = [
70
+ "eh_proj.weight",
71
+ "enorm.weight",
72
+ "final_layernorm.weight",
73
+ "hnorm.weight",
74
+ ]
75
+
76
+ # Global keys (not per-layer)
77
+ GLOBAL_KEYS = [
78
+ "model.embed_tokens.weight",
79
+ "model.norm.weight",
80
+ "lm_head.weight",
81
+ ]
82
+
83
+
84
+ def load_config(output_dir):
85
+ """Load config.json and extract model parameters."""
86
+ config_path = os.path.join(output_dir, "config.json")
87
+ if not os.path.exists(config_path):
88
+ print(f"[ERROR] config.json not found in {output_dir}")
89
+ return None
90
+ with open(config_path) as f:
91
+ return json.load(f)
92
+
93
+
94
+ def check_index_json(output_dir):
95
+ """Check model.safetensors.index.json for structure and completeness."""
96
+ index_path = os.path.join(output_dir, "model.safetensors.index.json")
97
+ if not os.path.exists(index_path):
98
+ print(f"[ERROR] model.safetensors.index.json not found")
99
+ return None, []
100
+
101
+ with open(index_path) as f:
102
+ index = json.load(f)
103
+
104
+ errors = []
105
+
106
+ # Check structure
107
+ if "metadata" not in index:
108
+ errors.append("Missing 'metadata' in index.json")
109
+ elif "total_size" not in index["metadata"]:
110
+ errors.append("Missing 'total_size' in metadata")
111
+
112
+ if "weight_map" not in index:
113
+ errors.append("Missing 'weight_map' in index.json")
114
+ return index, errors
115
+
116
+ weight_map = index["weight_map"]
117
+ total_size = index.get("metadata", {}).get("total_size", 0)
118
+
119
+ print(f" Index keys : {len(weight_map)}")
120
+ print(f" Total size : {total_size / 1e9:.2f} GB")
121
+
122
+ # Check for empty weight_map
123
+ if len(weight_map) == 0:
124
+ errors.append("weight_map is empty")
125
+
126
+ return index, errors
127
+
128
+
129
+ def check_expected_keys(weight_map, config):
130
+ """Check that all expected keys exist in the weight_map."""
131
+ errors = []
132
+ warnings = []
133
+
134
+ num_layers = config.get("num_hidden_layers", 80)
135
+ first_k_dense = config.get("first_k_dense_replace", 1)
136
+ num_experts = config.get("num_experts", 192)
137
+ num_mtp_layers = config.get("num_nextn_predict_layers", 0)
138
+
139
+ # Check global keys
140
+ for key in GLOBAL_KEYS:
141
+ if key not in weight_map:
142
+ errors.append(f"Missing global key: {key}")
143
+
144
+ # Check per-layer keys (regular layers)
145
+ missing_by_type = defaultdict(list)
146
+ for layer_idx in range(num_layers):
147
+ prefix = f"model.layers.{layer_idx}."
148
+ if layer_idx < first_k_dense:
149
+ # Dense layer
150
+ suffixes = DENSE_SUFFIXES
151
+ else:
152
+ # MoE layer
153
+ suffixes = MOE_SUFFIXES
154
+
155
+ for suffix in suffixes:
156
+ full_key = prefix + suffix
157
+ if full_key not in weight_map:
158
+ missing_by_type[suffix].append(layer_idx)
159
+
160
+ # Check MTP layers (layer num_layers .. num_layers + num_mtp_layers - 1)
161
+ mtp_missing_by_type = defaultdict(list)
162
+ for mtp_idx in range(num_mtp_layers):
163
+ layer_idx = num_layers + mtp_idx
164
+ prefix = f"model.layers.{layer_idx}."
165
+ # MTP layers use MoE structure + extra projection/norm keys
166
+ mtp_suffixes = MOE_SUFFIXES + MTP_EXTRA_SUFFIXES
167
+ for suffix in mtp_suffixes:
168
+ full_key = prefix + suffix
169
+ if full_key not in weight_map:
170
+ mtp_missing_by_type[suffix].append(layer_idx)
171
+
172
+ for suffix, layers in sorted(mtp_missing_by_type.items()):
173
+ layer_str = str(layers)
174
+ errors.append(f"Missing MTP key '{suffix}' in layers: {layer_str}")
175
+
176
+ for suffix, layers in sorted(missing_by_type.items()):
177
+ if len(layers) <= 5:
178
+ layer_str = str(layers)
179
+ else:
180
+ layer_str = f"{layers[:3]}...({len(layers)} total)"
181
+ errors.append(f"Missing '{suffix}' in layers: {layer_str}")
182
+
183
+ # Check for unexpected keys (not matching any known pattern)
184
+ known_prefixes = set()
185
+ # Regular layers + MTP layers
186
+ for layer_idx in range(num_layers + num_mtp_layers):
187
+ known_prefixes.add(f"model.layers.{layer_idx}.")
188
+ known_prefixes.add("model.embed_tokens.")
189
+ known_prefixes.add("model.norm.")
190
+ known_prefixes.add("lm_head.")
191
+ # Alternative MTP prefix (some models use this)
192
+ known_prefixes.add("model.mtp_layers.")
193
+
194
+ unexpected = []
195
+ for key in weight_map:
196
+ if not any(key.startswith(p) for p in known_prefixes):
197
+ unexpected.append(key)
198
+
199
+ if unexpected:
200
+ if len(unexpected) <= 5:
201
+ for k in unexpected:
202
+ warnings.append(f"Unexpected key: {k}")
203
+ else:
204
+ warnings.append(f"{len(unexpected)} unexpected keys found (first 3: {unexpected[:3]})")
205
+
206
+ return errors, warnings
207
+
208
+
209
+ def check_shard_files(output_dir, weight_map):
210
+ """Check that all referenced shard files exist and are non-empty."""
211
+ errors = []
212
+ warnings = []
213
+
214
+ # Get unique shard files
215
+ shard_files = sorted(set(weight_map.values()))
216
+ print(f" Shard files : {len(shard_files)}")
217
+
218
+ missing = []
219
+ empty = []
220
+ total_disk_size = 0
221
+
222
+ for sf in shard_files:
223
+ path = os.path.join(output_dir, sf)
224
+ if not os.path.exists(path):
225
+ missing.append(sf)
226
+ else:
227
+ size = os.path.getsize(path)
228
+ if size == 0:
229
+ empty.append(sf)
230
+ total_disk_size += size
231
+
232
+ print(f" Disk size : {total_disk_size / 1e9:.2f} GB")
233
+
234
+ if missing:
235
+ errors.append(f"Missing shard files ({len(missing)}): {missing[:5]}")
236
+ if empty:
237
+ errors.append(f"Empty shard files ({len(empty)}): {empty[:5]}")
238
+
239
+ # Check for orphan shard files (exist on disk but not in index)
240
+ all_safetensors = set(
241
+ f for f in os.listdir(output_dir)
242
+ if f.endswith(".safetensors")
243
+ )
244
+ referenced = set(shard_files)
245
+ orphans = all_safetensors - referenced
246
+ if orphans:
247
+ # Distinguish between empty residue files (cross-shard merge artifacts)
248
+ # and real orphan files with actual data
249
+ EMPTY_SHARD_THRESHOLD = 128 # bytes; empty safetensors header is ~16 bytes
250
+ residue_orphans = []
251
+ real_orphans = []
252
+ for o in sorted(orphans):
253
+ sz = os.path.getsize(os.path.join(output_dir, o))
254
+ if sz <= EMPTY_SHARD_THRESHOLD:
255
+ residue_orphans.append(o)
256
+ else:
257
+ real_orphans.append(o)
258
+
259
+ if residue_orphans:
260
+ warnings.append(
261
+ f"{len(residue_orphans)} empty residue shard(s) from cross-shard merge "
262
+ f"(<=128 bytes each, safe to delete)"
263
+ )
264
+ if real_orphans:
265
+ errors.append(
266
+ f"Orphan shard files with data (not in index): {real_orphans[:5]}"
267
+ )
268
+
269
+ return errors, warnings
270
+
271
+
272
+ def check_key_distribution(weight_map):
273
+ """Check the distribution of keys across shards."""
274
+ shard_key_count = defaultdict(int)
275
+ for key, shard in weight_map.items():
276
+ shard_key_count[shard] += 1
277
+
278
+ counts = sorted(shard_key_count.values())
279
+ print(f" Keys/shard : min={counts[0]}, max={counts[-1]}, "
280
+ f"median={counts[len(counts)//2]}")
281
+
282
+ # Check for shards with 0 keys (should not happen if they are in weight_map)
283
+ zero_shards = [s for s, c in shard_key_count.items() if c == 0]
284
+ if zero_shards:
285
+ return [f"Shards with 0 keys: {zero_shards}"]
286
+ return []
287
+
288
+
289
+ def spot_check_shards(output_dir, weight_map, config, num_checks=3):
290
+ """Spot-check a few shards by loading and verifying tensor shapes."""
291
+ errors = []
292
+
293
+ try:
294
+ from safetensors import safe_open
295
+ except ImportError:
296
+ print(" [SKIP] safetensors not installed, skipping spot-check")
297
+ return errors
298
+
299
+ num_experts = config.get("num_experts", 192)
300
+ expert_hidden = config.get("expert_hidden_dim", config.get("moe_intermediate_size", 1536))
301
+ hidden_size = config.get("hidden_size", 4096)
302
+
303
+ # Find shards that contain expert tensors (most interesting to check)
304
+ expert_shards = set()
305
+ for key, shard in weight_map.items():
306
+ if "experts.gate_up_proj" in key or "experts.down_proj" in key:
307
+ expert_shards.add(shard)
308
+
309
+ # Pick a few shards to check
310
+ check_shards = sorted(expert_shards)[:num_checks]
311
+ if not check_shards:
312
+ check_shards = sorted(set(weight_map.values()))[:num_checks]
313
+
314
+ print(f"\n Spot-checking {len(check_shards)} shard(s)...")
315
+
316
+ for shard_file in check_shards:
317
+ shard_path = os.path.join(output_dir, shard_file)
318
+ t0 = time.time()
319
+
320
+ try:
321
+ with safe_open(shard_path, framework="pt", device="cpu") as f:
322
+ keys_in_shard = list(f.keys())
323
+ for key in keys_in_shard:
324
+ tensor = f.get_tensor(key)
325
+
326
+ # Check expert shapes
327
+ if key.endswith("experts.gate_up_proj"):
328
+ expected_shape = (num_experts, expert_hidden * 2, hidden_size)
329
+ if tuple(tensor.shape) != expected_shape:
330
+ errors.append(
331
+ f"{shard_file}/{key}: shape {tuple(tensor.shape)} "
332
+ f"!= expected {expected_shape}"
333
+ )
334
+
335
+ elif key.endswith("experts.down_proj"):
336
+ expected_shape = (num_experts, hidden_size, expert_hidden)
337
+ if tuple(tensor.shape) != expected_shape:
338
+ errors.append(
339
+ f"{shard_file}/{key}: shape {tuple(tensor.shape)} "
340
+ f"!= expected {expected_shape}"
341
+ )
342
+
343
+ # Check for NaN/Inf
344
+ if tensor.is_floating_point():
345
+ if tensor.isnan().any():
346
+ errors.append(f"{shard_file}/{key}: contains NaN values")
347
+ if tensor.isinf().any():
348
+ errors.append(f"{shard_file}/{key}: contains Inf values")
349
+
350
+ elapsed = time.time() - t0
351
+ print(f" {shard_file}: {len(keys_in_shard)} keys, OK ({elapsed:.1f}s)")
352
+
353
+ except Exception as e:
354
+ errors.append(f"Failed to load {shard_file}: {e}")
355
+
356
+ return errors
357
+
358
+
359
+ def main():
360
+ parser = argparse.ArgumentParser(
361
+ description="Validate converted HYV3 outer-format checkpoint."
362
+ )
363
+ parser.add_argument(
364
+ "output_dir", type=str,
365
+ help="Path to the converted outer-format checkpoint directory.",
366
+ )
367
+ parser.add_argument(
368
+ "--spot-check", type=int, default=3, dest="spot_check",
369
+ help="Number of shards to spot-check by loading tensors (default: 3).",
370
+ )
371
+ args = parser.parse_args()
372
+
373
+ output_dir = os.path.abspath(args.output_dir)
374
+ print(f"Validating: {output_dir}\n")
375
+
376
+ if not os.path.isdir(output_dir):
377
+ print(f"[ERROR] Directory not found: {output_dir}")
378
+ sys.exit(1)
379
+
380
+ all_errors = []
381
+ all_warnings = []
382
+
383
+ # 1. Load config
384
+ print("[1/5] Loading config.json...")
385
+ config = load_config(output_dir)
386
+ if config is None:
387
+ print("[ERROR] Cannot proceed without config.json")
388
+ sys.exit(1)
389
+
390
+ num_layers = config.get("num_hidden_layers", 0)
391
+ num_experts = config.get("num_experts", 0)
392
+ first_k_dense = config.get("first_k_dense_replace", 0)
393
+ num_mtp = config.get("num_nextn_predict_layers", 0)
394
+ print(f" Layers : {num_layers} ({first_k_dense} dense, {num_layers - first_k_dense} MoE)")
395
+ print(f" MTP layers : {num_mtp}")
396
+ print(f" Experts/layer : {num_experts}")
397
+ print(f" Hidden size : {config.get('hidden_size', '?')}")
398
+ print(f" Expert hidden : {config.get('expert_hidden_dim', config.get('moe_intermediate_size', '?'))}")
399
+
400
+ # 2. Check index.json
401
+ print("\n[2/5] Checking model.safetensors.index.json...")
402
+ index, idx_errors = check_index_json(output_dir)
403
+ all_errors.extend(idx_errors)
404
+
405
+ if index is None or "weight_map" not in index:
406
+ print("[ERROR] Cannot proceed without valid index.json")
407
+ sys.exit(1)
408
+
409
+ weight_map = index["weight_map"]
410
+
411
+ # 3. Check expected keys
412
+ print("\n[3/5] Checking expected keys...")
413
+ key_errors, key_warnings = check_expected_keys(weight_map, config)
414
+ all_errors.extend(key_errors)
415
+ all_warnings.extend(key_warnings)
416
+
417
+ # Also check key distribution
418
+ dist_errors = check_key_distribution(weight_map)
419
+ all_errors.extend(dist_errors)
420
+
421
+ # 4. Check shard files
422
+ print("\n[4/5] Checking shard files on disk...")
423
+ shard_errors, shard_warnings = check_shard_files(output_dir, weight_map)
424
+ all_errors.extend(shard_errors)
425
+ all_warnings.extend(shard_warnings)
426
+
427
+ # 5. Spot-check
428
+ if args.spot_check > 0:
429
+ print(f"\n[5/5] Spot-checking tensors (loading {args.spot_check} shard(s))...")
430
+ spot_errors = spot_check_shards(output_dir, weight_map, config, args.spot_check)
431
+ all_errors.extend(spot_errors)
432
+ else:
433
+ print("\n[5/5] Spot-check skipped (--spot-check 0)")
434
+
435
+ # Summary
436
+ print(f"\n{'=' * 60}")
437
+ if all_warnings:
438
+ print(f"WARNINGS ({len(all_warnings)}):")
439
+ for w in all_warnings:
440
+ print(f" [WARN] {w}")
441
+
442
+ if all_errors:
443
+ print(f"ERRORS ({len(all_errors)}):")
444
+ for e in all_errors:
445
+ print(f" [ERROR] {e}")
446
+ print(f"\nResult: FAILED ({len(all_errors)} error(s), {len(all_warnings)} warning(s))")
447
+ sys.exit(1)
448
+ else:
449
+ print(f"Result: PASSED (0 errors, {len(all_warnings)} warning(s))")
450
+ print(f"{'=' * 60}")
451
+ sys.exit(0)
452
+
453
+
454
+ if __name__ == "__main__":
455
+ main()
finetune/tools/convert_ckpt_to_outer.py ADDED
@@ -0,0 +1,641 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Memory-friendly checkpoint converter: inner -> outer format (v2).
4
+
5
+ Converts the HYV3 checkpoint from inner format (per-expert keys, old naming)
6
+ to outer format (fused 3D experts, new naming) shard by shard.
7
+
8
+ Handles the case where a single layer's experts may be split across
9
+ multiple shards (cross-shard experts) by deferring their fusion to a
10
+ post-processing step.
11
+
12
+ v2 improvements over v1:
13
+ - Post-processing is shard-centric (each shard read/written only once)
14
+ instead of prefix-centric (same shard read/written multiple times).
15
+ This fixes Bus error (core dump) when there are many cross-shard groups.
16
+ - Explicit memory management with gc.collect() to prevent memory bloat.
17
+ - Better progress reporting during post-processing.
18
+
19
+ Supports multi-process parallelism for faster conversion.
20
+
21
+ Usage:
22
+ # Default 8 workers
23
+ python convert_ckpt_to_outer.py \\
24
+ --input_dir pretrain_base/hf \\
25
+ --output_dir pretrain_base/hf_outer
26
+
27
+ # Custom worker count
28
+ python convert_ckpt_to_outer.py \\
29
+ --input_dir pretrain_base/hf \\
30
+ --output_dir pretrain_base/hf_outer \\
31
+ --workers 16
32
+
33
+ The script will:
34
+ 1. Pre-scan index.json to detect cross-shard expert groups
35
+ 2. Convert weights shard-by-shard in parallel (key rename + expert fuse)
36
+ 3. Post-process cross-shard expert groups (merge from multiple shards)
37
+ - v2: shard-centric approach, each shard read/written only once
38
+ 4. Copy config.json as-is (already in outer format)
39
+ 5. Copy all other files (tokenizer, etc.)
40
+ 6. Rebuild model.safetensors.index.json
41
+ """
42
+
43
+ import argparse
44
+ import gc
45
+ import json
46
+ import os
47
+ import re
48
+ import signal
49
+ import shutil
50
+ import sys
51
+ import time
52
+ import traceback
53
+ from collections import OrderedDict, defaultdict
54
+ from multiprocessing import Pool
55
+
56
+ import torch
57
+
58
+ try:
59
+ from safetensors import safe_open
60
+ from safetensors.torch import save_file
61
+ except ImportError:
62
+ raise ImportError("Please install safetensors: pip install safetensors")
63
+
64
+ # ============================================================================
65
+ # Signal handling for Bus error (SIGBUS) and other fatal signals
66
+ # ============================================================================
67
+
68
+ def _fatal_signal_handler(signum, frame):
69
+ """Handle fatal signals (SIGBUS, SIGSEGV) by logging before exit.
70
+
71
+ These signals cannot be caught by try/except. This handler ensures
72
+ the error message is written to stderr (captured by nohup redirection)
73
+ before the process terminates.
74
+ """
75
+ sig_name = signal.Signals(signum).name if hasattr(signal, 'Signals') else str(signum)
76
+ pid = os.getpid()
77
+ msg = (
78
+ f"\n[FATAL] Process {pid} received {sig_name} (signal {signum}).\n"
79
+ f"This typically indicates an out-of-memory condition during mmap I/O.\n"
80
+ f"Stack trace at time of signal:\n"
81
+ )
82
+ sys.stderr.write(msg)
83
+ traceback.print_stack(frame, file=sys.stderr)
84
+ sys.stderr.flush()
85
+ # Re-raise with default handler to get proper exit code
86
+ signal.signal(signum, signal.SIG_DFL)
87
+ os.kill(pid, signum)
88
+
89
+
90
+ def _install_signal_handlers():
91
+ """Install handlers for SIGBUS and SIGSEGV in the current process."""
92
+ for sig in (signal.SIGBUS, signal.SIGSEGV):
93
+ try:
94
+ signal.signal(sig, _fatal_signal_handler)
95
+ except (OSError, ValueError):
96
+ # Some signals may not be available on all platforms
97
+ pass
98
+
99
+
100
+ def _pool_worker_init():
101
+ """Initializer for multiprocessing pool workers.
102
+
103
+ Installs signal handlers so that Bus errors in worker processes
104
+ are also logged before the process dies.
105
+ """
106
+ _install_signal_handlers()
107
+
108
+
109
+ # ============================================================================
110
+ # Key rename mapping (inner -> outer)
111
+ # ============================================================================
112
+
113
+ _KEY_RENAMES = [
114
+ ("mlp.router.gate.", "mlp.gate."),
115
+ ("mlp.expert_bias", "mlp.e_score_correction_bias"),
116
+ ("mlp.shared_mlp.", "mlp.shared_experts."),
117
+ ]
118
+
119
+ # Regex to match per-expert keys
120
+ _EXPERT_KEY_RE = re.compile(
121
+ r"^(.*\.mlp\.experts\.)(\d+)\.(gate_proj|up_proj|down_proj)\.weight$"
122
+ )
123
+
124
+ def rename_key(key: str) -> str:
125
+ """Rename a single key from inner to outer format."""
126
+ for old_sub, new_sub in _KEY_RENAMES:
127
+ if old_sub in key:
128
+ key = key.replace(old_sub, new_sub)
129
+ break
130
+ return key
131
+
132
+ def scan_cross_shard_experts(index_path: str):
133
+ """Pre-scan index.json to find expert groups that span multiple shards.
134
+
135
+ Returns:
136
+ cross_shard_prefixes: set of expert prefixes that span multiple shards
137
+ e.g. {"model.layers.80.mlp.experts."}
138
+ """
139
+ with open(index_path) as f:
140
+ index = json.load(f)
141
+ wm = index["weight_map"]
142
+
143
+ # prefix -> set of shards
144
+ prefix_shards = defaultdict(set)
145
+ for key in wm:
146
+ m = _EXPERT_KEY_RE.match(key)
147
+ if m:
148
+ prefix = m.group(1)
149
+ prefix_shards[prefix].add(wm[key])
150
+
151
+ cross_shard_prefixes = set()
152
+ for prefix, shards in prefix_shards.items():
153
+ if len(shards) > 1:
154
+ cross_shard_prefixes.add(prefix)
155
+
156
+ return cross_shard_prefixes
157
+
158
+ def convert_shard(shard_path: str, cross_shard_prefixes: set = None):
159
+ """Load a single shard, rename keys, and fuse experts.
160
+
161
+ For expert groups in cross_shard_prefixes, the per-expert keys are
162
+ kept as-is (just renamed) and returned separately as deferred items,
163
+ to be merged later in a post-processing step.
164
+
165
+ Returns:
166
+ result: OrderedDict of converted tensors (ready to save)
167
+ deferred_expert_keys: list of original expert keys that were deferred
168
+ (these are kept in result with their original per-expert naming
169
+ but with the outer rename applied, to be post-processed later)
170
+ """
171
+ if cross_shard_prefixes is None:
172
+ cross_shard_prefixes = set()
173
+
174
+ tensors = OrderedDict()
175
+ with safe_open(shard_path, framework="pt", device="cpu") as f:
176
+ for key in f.keys():
177
+ tensors[key] = f.get_tensor(key)
178
+
179
+ # Separate expert keys from non-expert keys
180
+ expert_groups = {} # prefix -> {expert_idx -> {proj_name -> tensor}}
181
+ deferred_expert_keys = [] # keys that belong to cross-shard experts
182
+ result = OrderedDict()
183
+
184
+ for key, tensor in tensors.items():
185
+ m = _EXPERT_KEY_RE.match(key)
186
+ if m:
187
+ prefix = m.group(1)
188
+ expert_idx = int(m.group(2))
189
+ proj_name = m.group(3)
190
+
191
+ if prefix in cross_shard_prefixes:
192
+ # Defer: keep the key as-is (with rename) for post-processing
193
+ new_key = rename_key(key)
194
+ result[new_key] = tensor
195
+ deferred_expert_keys.append(new_key)
196
+ else:
197
+ # Normal: collect for fusion within this shard
198
+ if prefix not in expert_groups:
199
+ expert_groups[prefix] = {}
200
+ if expert_idx not in expert_groups[prefix]:
201
+ expert_groups[prefix][expert_idx] = {}
202
+ expert_groups[prefix][expert_idx][proj_name] = tensor
203
+ else:
204
+ # Non-expert key: just rename
205
+ new_key = rename_key(key)
206
+ result[new_key] = tensor
207
+
208
+ # Fuse expert weights for each non-cross-shard layer prefix
209
+ for prefix in sorted(expert_groups.keys()):
210
+ experts = expert_groups[prefix]
211
+ num_experts = max(experts.keys()) + 1
212
+
213
+ gate_up_list = []
214
+ down_list = []
215
+ for i in range(num_experts):
216
+ if i not in experts:
217
+ raise ValueError(
218
+ f"Missing expert {i} in {prefix}. "
219
+ f"Found: {sorted(experts.keys())}"
220
+ )
221
+ exp = experts[i]
222
+ gate_up = torch.cat([exp["gate_proj"], exp["up_proj"]], dim=0)
223
+ gate_up_list.append(gate_up)
224
+ down_list.append(exp["down_proj"])
225
+
226
+ fused_gate_up = torch.stack(gate_up_list, dim=0)
227
+ fused_down = torch.stack(down_list, dim=0)
228
+
229
+ for exp in experts.values():
230
+ exp.clear()
231
+ gate_up_list.clear()
232
+ down_list.clear()
233
+
234
+ result[f"{prefix}gate_up_proj"] = fused_gate_up
235
+ result[f"{prefix}down_proj"] = fused_down
236
+
237
+ return result, deferred_expert_keys
238
+
239
+ def _process_one_shard(args_tuple):
240
+ """Worker function: convert a single shard and save to output dir.
241
+
242
+ Args:
243
+ args_tuple: (idx, num_shards, shard_file, input_dir, output_dir, cross_shard_prefixes)
244
+
245
+ Returns:
246
+ (shard_file, key_list, shard_size, elapsed, deferred_keys)
247
+ """
248
+ idx, num_shards, shard_file, input_dir, output_dir, cross_shard_prefixes = args_tuple
249
+ shard_path = os.path.join(input_dir, shard_file)
250
+ t0 = time.time()
251
+
252
+ converted, deferred_keys = convert_shard(shard_path, cross_shard_prefixes)
253
+
254
+ shard_size = sum(t.numel() * t.element_size() for t in converted.values())
255
+
256
+ out_shard_path = os.path.join(output_dir, shard_file)
257
+ save_file(converted, out_shard_path)
258
+
259
+ elapsed = time.time() - t0
260
+ num_keys = len(converted)
261
+ key_list = list(converted.keys())
262
+
263
+ del converted
264
+
265
+ deferred_info = ""
266
+ if deferred_keys:
267
+ deferred_info = f", Deferred={len(deferred_keys)}"
268
+
269
+ print(
270
+ f" [{idx + 1}/{num_shards}] {shard_file}: "
271
+ f"Keys={num_keys}, Size={shard_size / 1e9:.2f} GB, "
272
+ f"Time={elapsed:.1f}s{deferred_info}",
273
+ flush=True,
274
+ )
275
+
276
+ return shard_file, key_list, shard_size, elapsed, deferred_keys
277
+
278
+
279
+ def post_process_cross_shard_experts(output_dir, cross_shard_prefixes, all_deferred):
280
+ """Merge cross-shard expert groups (v2: shard-centric approach).
281
+
282
+ Instead of iterating per-prefix (which causes the same shard to be
283
+ loaded/saved multiple times), this v2 approach:
284
+ 1. Builds a mapping of which prefixes each shard is involved in
285
+ 2. Collects all expert tensors from all involved shards in ONE pass
286
+ 3. Fuses all prefixes
287
+ 4. Writes each shard only ONCE with all its updates applied
288
+
289
+ This avoids the Bus error (core dump) caused by repeated mmap of
290
+ large files and memory bloat.
291
+
292
+ Args:
293
+ output_dir: path to output directory
294
+ cross_shard_prefixes: set of expert prefixes that span multiple shards
295
+ all_deferred: dict of {shard_file: [deferred_key, ...]}
296
+
297
+ Returns:
298
+ updated_shards: dict of {shard_file: (key_list, shard_size)} for updated shards
299
+ """
300
+ if not cross_shard_prefixes:
301
+ return {}
302
+
303
+ print(f"\n Post-processing {len(cross_shard_prefixes)} cross-shard expert group(s)...",
304
+ flush=True)
305
+
306
+ # ----------------------------------------------------------------
307
+ # Step 1: Build mappings
308
+ # ----------------------------------------------------------------
309
+ # prefix -> ordered list of shards that contain its experts
310
+ prefix_to_shards = defaultdict(set)
311
+ # shard -> set of prefixes it is involved in
312
+ shard_to_prefixes = defaultdict(set)
313
+
314
+ for shard_file, deferred_keys in all_deferred.items():
315
+ for key in deferred_keys:
316
+ m = _EXPERT_KEY_RE.match(key)
317
+ if m:
318
+ prefix = m.group(1)
319
+ if prefix in cross_shard_prefixes:
320
+ prefix_to_shards[prefix].add(shard_file)
321
+ shard_to_prefixes[shard_file].add(prefix)
322
+
323
+ # For each prefix, decide which shard will hold the fused result
324
+ # (use the first shard alphabetically)
325
+ prefix_to_target_shard = {}
326
+ for prefix in sorted(prefix_to_shards.keys()):
327
+ target = sorted(prefix_to_shards[prefix])[0]
328
+ prefix_to_target_shard[prefix] = target
329
+
330
+ # All shards that need to be updated
331
+ all_involved_shards = set()
332
+ for shards in prefix_to_shards.values():
333
+ all_involved_shards.update(shards)
334
+
335
+ print(f" Involved shards: {len(all_involved_shards)}", flush=True)
336
+ print(f" Expert groups: {len(prefix_to_shards)}", flush=True)
337
+
338
+ # ----------------------------------------------------------------
339
+ # Step 2: Collect all expert tensors from all involved shards
340
+ # (one pass per shard)
341
+ # ----------------------------------------------------------------
342
+ # prefix -> {expert_idx -> {proj_name -> tensor}}
343
+ all_expert_data = defaultdict(dict)
344
+ # shard -> OrderedDict of non-expert keys (to be re-saved)
345
+ shard_non_expert = {}
346
+
347
+ sorted_involved = sorted(all_involved_shards)
348
+ for si, shard_file in enumerate(sorted_involved):
349
+ shard_path = os.path.join(output_dir, shard_file)
350
+ prefixes_in_shard = shard_to_prefixes[shard_file]
351
+
352
+ print(f" [{si+1}/{len(sorted_involved)}] Reading {shard_file} "
353
+ f"({len(prefixes_in_shard)} prefix(es))...", flush=True)
354
+
355
+ non_expert = OrderedDict()
356
+ with safe_open(shard_path, framework="pt", device="cpu") as f:
357
+ for key in f.keys():
358
+ m = _EXPERT_KEY_RE.match(key)
359
+ if m and m.group(1) in prefixes_in_shard:
360
+ # This is a deferred expert key
361
+ prefix = m.group(1)
362
+ expert_idx = int(m.group(2))
363
+ proj_name = m.group(3)
364
+ if expert_idx not in all_expert_data[prefix]:
365
+ all_expert_data[prefix][expert_idx] = {}
366
+ all_expert_data[prefix][expert_idx][proj_name] = f.get_tensor(key)
367
+ else:
368
+ # Non-expert key: keep as-is
369
+ non_expert[key] = f.get_tensor(key)
370
+
371
+ shard_non_expert[shard_file] = non_expert
372
+ gc.collect()
373
+
374
+ # ----------------------------------------------------------------
375
+ # Step 3: Fuse all expert groups
376
+ # ----------------------------------------------------------------
377
+ # prefix -> {"gate_up_proj": tensor, "down_proj": tensor}
378
+ fused_results = {}
379
+
380
+ for pi, prefix in enumerate(sorted(all_expert_data.keys())):
381
+ expert_data = all_expert_data[prefix]
382
+ num_experts = max(expert_data.keys()) + 1
383
+
384
+ print(f" Fusing {prefix} ({num_experts} experts)...", flush=True)
385
+
386
+ gate_up_list = []
387
+ down_list = []
388
+ for i in range(num_experts):
389
+ if i not in expert_data:
390
+ raise ValueError(
391
+ f"Missing expert {i} in {prefix} after cross-shard merge. "
392
+ f"Found: {sorted(expert_data.keys())}"
393
+ )
394
+ exp = expert_data[i]
395
+ if "gate_proj" not in exp or "up_proj" not in exp:
396
+ raise ValueError(
397
+ f"Expert {i} in {prefix} missing gate_proj/up_proj. "
398
+ f"Has: {sorted(exp.keys())}"
399
+ )
400
+ if "down_proj" not in exp:
401
+ raise ValueError(
402
+ f"Expert {i} in {prefix} missing down_proj. "
403
+ f"Has: {sorted(exp.keys())}"
404
+ )
405
+ gate_up = torch.cat([exp["gate_proj"], exp["up_proj"]], dim=0)
406
+ gate_up_list.append(gate_up)
407
+ down_list.append(exp["down_proj"])
408
+
409
+ fused_gate_up = torch.stack(gate_up_list, dim=0)
410
+ fused_down = torch.stack(down_list, dim=0)
411
+
412
+ fused_results[prefix] = {
413
+ "gate_up_proj": fused_gate_up,
414
+ "down_proj": fused_down,
415
+ }
416
+
417
+ # Free per-expert data for this prefix
418
+ del gate_up_list, down_list
419
+ for exp in expert_data.values():
420
+ exp.clear()
421
+ del all_expert_data[prefix]
422
+ gc.collect()
423
+
424
+ del all_expert_data
425
+ gc.collect()
426
+
427
+ # ----------------------------------------------------------------
428
+ # Step 4: Write each involved shard ONCE with all updates applied
429
+ # ----------------------------------------------------------------
430
+ updated_shards = {}
431
+
432
+ for si, shard_file in enumerate(sorted_involved):
433
+ shard_path = os.path.join(output_dir, shard_file)
434
+ non_expert = shard_non_expert[shard_file]
435
+
436
+ # Add fused tensors for prefixes that target this shard
437
+ fused_added = []
438
+ for prefix, target_shard in prefix_to_target_shard.items():
439
+ if target_shard == shard_file and prefix in fused_results:
440
+ non_expert[f"{prefix}gate_up_proj"] = fused_results[prefix]["gate_up_proj"]
441
+ non_expert[f"{prefix}down_proj"] = fused_results[prefix]["down_proj"]
442
+ fused_added.append(prefix)
443
+
444
+ save_file(non_expert, shard_path)
445
+ shard_size = sum(t.numel() * t.element_size() for t in non_expert.values())
446
+ updated_shards[shard_file] = (list(non_expert.keys()), shard_size)
447
+
448
+ fused_info = ""
449
+ if fused_added:
450
+ fused_info = f", Fused {len(fused_added)} group(s)"
451
+
452
+ print(f" [{si+1}/{len(sorted_involved)}] Wrote {shard_file}: "
453
+ f"{len(non_expert)} keys, {shard_size / 1e9:.2f} GB{fused_info}",
454
+ flush=True)
455
+
456
+ # Free memory for this shard
457
+ del shard_non_expert[shard_file]
458
+ for prefix in fused_added:
459
+ del fused_results[prefix]
460
+ del non_expert
461
+ gc.collect()
462
+
463
+ return updated_shards
464
+
465
+
466
+ def main():
467
+ parser = argparse.ArgumentParser(
468
+ description="Convert HYV3 checkpoint from inner to outer format (v2, shard-centric post-processing)."
469
+ )
470
+ parser.add_argument(
471
+ "--input_dir", type=str, required=True,
472
+ help="Path to the inner-format checkpoint directory.",
473
+ )
474
+ parser.add_argument(
475
+ "--output_dir", type=str, required=True,
476
+ help="Path to the output outer-format checkpoint directory.",
477
+ )
478
+ parser.add_argument(
479
+ "--workers", type=int, default=8,
480
+ help="Number of parallel worker processes (default: 8).",
481
+ )
482
+ args = parser.parse_args()
483
+
484
+ input_dir = os.path.abspath(args.input_dir)
485
+ output_dir = os.path.abspath(args.output_dir)
486
+ num_workers = args.workers
487
+
488
+ if not os.path.isdir(input_dir):
489
+ raise FileNotFoundError(f"Input directory not found: {input_dir}")
490
+
491
+ os.makedirs(output_dir, exist_ok=True)
492
+
493
+ # Pre-scan for cross-shard expert groups
494
+ index_path = os.path.join(input_dir, "model.safetensors.index.json")
495
+ cross_shard_prefixes = set()
496
+ if os.path.exists(index_path):
497
+ cross_shard_prefixes = scan_cross_shard_experts(index_path)
498
+ if cross_shard_prefixes:
499
+ print(f"Detected {len(cross_shard_prefixes)} cross-shard expert group(s):")
500
+ for p in sorted(cross_shard_prefixes):
501
+ print(f" - {p}")
502
+ print()
503
+
504
+ # Get all safetensors files
505
+ shard_files = sorted(
506
+ f for f in os.listdir(input_dir) if f.endswith(".safetensors")
507
+ )
508
+ if not shard_files:
509
+ raise FileNotFoundError(f"No .safetensors files found in {input_dir}")
510
+
511
+ # Skip already-converted shards (for resumability)
512
+ # NOTE: if there are cross-shard experts, we cannot skip shards that
513
+ # contain deferred keys (they need post-processing). For simplicity,
514
+ # when cross-shard experts exist, we re-process all shards.
515
+ remaining = []
516
+ skipped = []
517
+ if cross_shard_prefixes:
518
+ # Re-process all shards when cross-shard experts exist
519
+ remaining = list(shard_files)
520
+ else:
521
+ for sf in shard_files:
522
+ out_path = os.path.join(output_dir, sf)
523
+ if os.path.exists(out_path) and os.path.getsize(out_path) > 0:
524
+ skipped.append(sf)
525
+ else:
526
+ remaining.append(sf)
527
+
528
+ num_shards = len(shard_files)
529
+ num_workers = min(num_workers, len(remaining)) if remaining else 1
530
+
531
+ print(f"=" * 60)
532
+ print(f"HYV3 Checkpoint Converter (inner -> outer, v2)")
533
+ print(f" Input : {input_dir}")
534
+ print(f" Output : {output_dir}")
535
+ print(f" Shards : {num_shards} total, {len(skipped)} already done, {len(remaining)} to process")
536
+ print(f" Workers: {num_workers}")
537
+ if cross_shard_prefixes:
538
+ print(f" Cross-shard experts: {len(cross_shard_prefixes)} group(s) (will post-process)")
539
+ print(f"=" * 60)
540
+
541
+ t_start = time.time()
542
+
543
+ # Build task list for remaining shards
544
+ tasks = [
545
+ (i, len(remaining), sf, input_dir, output_dir, cross_shard_prefixes)
546
+ for i, sf in enumerate(remaining)
547
+ ]
548
+
549
+ # Process in parallel
550
+ results = []
551
+ if tasks:
552
+ with Pool(processes=num_workers, initializer=_pool_worker_init) as pool:
553
+ results = pool.map(_process_one_shard, tasks)
554
+
555
+ # Collect deferred keys info
556
+ all_deferred = {} # shard_file -> [deferred_keys]
557
+ for shard_file, key_list, shard_size, elapsed, deferred_keys in results:
558
+ if deferred_keys:
559
+ all_deferred[shard_file] = deferred_keys
560
+
561
+ # Post-process cross-shard expert groups (v2: shard-centric)
562
+ updated_shards = {}
563
+ if cross_shard_prefixes and all_deferred:
564
+ updated_shards = post_process_cross_shard_experts(
565
+ output_dir, cross_shard_prefixes, all_deferred
566
+ )
567
+
568
+ # Build weight_map and total_size
569
+ weight_map = OrderedDict()
570
+ total_size = 0
571
+
572
+ # For skipped shards, read their keys from the output files
573
+ for sf in skipped:
574
+ out_path = os.path.join(output_dir, sf)
575
+ with safe_open(out_path, framework="pt", device="cpu") as f:
576
+ keys = list(f.keys())
577
+ for key in keys:
578
+ weight_map[key] = sf
579
+ t = f.get_tensor(key)
580
+ total_size += t.numel() * t.element_size()
581
+
582
+ # Collect results from newly converted shards
583
+ for shard_file, key_list, shard_size, elapsed, deferred_keys in results:
584
+ if shard_file in updated_shards:
585
+ # This shard was updated by post-processing
586
+ updated_key_list, updated_size = updated_shards[shard_file]
587
+ for key in updated_key_list:
588
+ weight_map[key] = shard_file
589
+ total_size += updated_size
590
+ else:
591
+ for key in key_list:
592
+ weight_map[key] = shard_file
593
+ total_size += shard_size
594
+
595
+ # Build and save index
596
+ sorted_weight_map = OrderedDict(sorted(weight_map.items()))
597
+ index = {
598
+ "metadata": {"total_size": total_size},
599
+ "weight_map": sorted_weight_map,
600
+ }
601
+ index_path_out = os.path.join(output_dir, "model.safetensors.index.json")
602
+ with open(index_path_out, "w") as f:
603
+ json.dump(index, f, indent=2)
604
+ f.write("\n")
605
+ print(f"\nSaved {index_path_out}")
606
+
607
+ # Copy non-safetensors files (config, tokenizer, etc.)
608
+ skip_suffixes = {".safetensors"}
609
+ skip_names = {"model.safetensors.index.json"}
610
+ copied = []
611
+ for fname in os.listdir(input_dir):
612
+ if fname in skip_names:
613
+ continue
614
+ if any(fname.endswith(s) for s in skip_suffixes):
615
+ continue
616
+ src = os.path.join(input_dir, fname)
617
+ dst = os.path.join(output_dir, fname)
618
+ if os.path.isfile(src):
619
+ shutil.copy2(src, dst)
620
+ copied.append(fname)
621
+ elif os.path.isdir(src):
622
+ if os.path.exists(dst):
623
+ shutil.rmtree(dst)
624
+ shutil.copytree(src, dst)
625
+ copied.append(fname + "/")
626
+
627
+ if copied:
628
+ print(f"\nCopied files: {', '.join(copied)}")
629
+
630
+ t_total = time.time() - t_start
631
+ print(f"\n{'=' * 60}")
632
+ print(f"Conversion complete!")
633
+ print(f" Total keys : {len(weight_map)}")
634
+ print(f" Total size : {total_size / 1e9:.2f} GB")
635
+ print(f" Total time : {t_total:.1f}s ({t_total / 60:.1f} min)")
636
+ print(f" Output dir : {output_dir}")
637
+ print(f"{'=' * 60}")
638
+
639
+ if __name__ == "__main__":
640
+ _install_signal_handlers()
641
+ main()