zhan1206 commited on
Commit
3ab86b9
·
1 Parent(s): 201e778

fix: v3 comprehensive defect repair

Browse files

Fixes from AI audit report (20 defects, 14 fixed):

F1: Unified GQA config - num_key_value_heads=8 in config.json (was 32)
F3: Added missing sbla_mode, hidden_act, attention_dropout to config.json
S1: Removed redundant fusion-config-8b.json (3 configs -> 1)
S3: Moved debug/script files to scripts/, out of tests/ and root/
S5: README 256K -> 32K (expandable to 256K with RoPE scaling)
M1: Fixed mini_data.json think_rank distribution (was all 0, now 0:89,1:3,2:8,3:8)
M3: Added train_tokenizer.py script for SentencePiece .model generation
M5: Renamed test_sbbla -> test_sbla, moved fix_* from tests/ to scripts/
L1: Added bitsandbytes, ollama, scipy, scikit-learn to requirements.txt
L2: Removed Push-ToGitHub.ps1 and push_to_github.py (duplicate tooling)
L5: Fixed mini config block_size==window_size (32 vs 128), sbla_mode=mixed

New modules:
- models/tokenizer.py: Unified tokenizer management (GPT2 placeholder + Fusion SP)
- scripts/train_tokenizer.py: SentencePiece tokenizer training
- scripts/fix_mini_data.py: Fix think_rank distribution
- scripts/add_depth3_samples.py: Add depth=3 samples

Also:
- Deleted ollama_deploy_fixed.py, fusion_model_fixed.py (intermediate artifacts)
- Added M4 TODO in FusionAttention (unify with sbla_attention.py)
- Updated train/full_finetune.py to use models.tokenizer module

Push-ToGitHub.ps1 DELETED
@@ -1,149 +0,0 @@
1
- # Fusion 项目 GitHub 推送脚本(本地执行)
2
- # 作者:朱子瞻
3
- # 项目:Fusion - 六边形开源大模型
4
-
5
- Write-Host "=" * 60 -ForegroundColor Cyan
6
- Write-Host "Fusion 项目 GitHub 推送脚本" -ForegroundColor Cyan
7
- Write-Host "=" * 60
8
-
9
- # 1. 检查 Git
10
- Write-Host "`n🔍 检查 Git..." -ForegroundColor Yellow
11
- try {
12
- $gitVersion = git --version
13
- Write-Host "✅ Git 已安装:$gitVersion" -ForegroundColor Green
14
- } catch {
15
- Write-Host "❌ Git 未安装,请先安装 Git" -ForegroundColor Red
16
- exit 1
17
- }
18
-
19
- # 2. 进入项目目录
20
- $projectDir = Split-Path -Parent $MyInvocation.MyCommand.Path
21
- Set-Location $projectDir
22
- Write-Host "`n📂 项目目录:$projectDir" -ForegroundColor Yellow
23
-
24
- # 3. 检查 Git 状态
25
- Write-Host "`n🔍 检查 Git 状态..." -ForegroundColor Yellow
26
- $status = git status --porcelain
27
- if ($status) {
28
- Write-Host "⚠️ 有未提交的更改" -ForegroundColor Yellow
29
- git status
30
-
31
- $commit = Read-Host "`n是否提交更改?(Y/N)"
32
- if ($commit -eq 'Y' -or $commit -eq 'y') {
33
- $msg = Read-Host "输入提交信息(默认:Update)"
34
- if (-not $msg) { $msg = "Update" }
35
-
36
- git add .
37
- git commit -m $msg
38
- Write-Host "✅ 已提交" -ForegroundColor Green
39
- }
40
- }
41
-
42
- # 4. 创建 GitHub 仓库
43
- Write-Host "`n📦 创建 GitHub 仓库..." -ForegroundColor Yellow
44
- Write-Host " 仓库名:fusion-llm"
45
- Write-Host " 描述:Fusion - 六边形开源大模型"
46
- Write-Host "`n🔐 请输入 GitHub Personal Access Token"
47
- Write-Host " 创建地址:<ADDRESS_REMOVED>
48
- Write-Host " 需要权限:repo(全选)`n"
49
-
50
- $token = Read-Host "Token" -AsSecureString
51
- $tokenPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
52
- [Runtime.InteropServices.Marshal]::SecureStringToBSTR($token)
53
- )
54
-
55
- if (-not $tokenPlain) {
56
- Write-Host "❌ Token 不能为空" -ForegroundColor Red
57
- exit 1
58
- }
59
-
60
- # 调用 GitHub API 创建仓库
61
- $headers = @{
62
- "Authorization" = "token $tokenPlain"
63
- "Accept" = "application/vnd.github.v3+json"
64
- }
65
-
66
- $body = @{
67
- "name" = "fusion-llm"
68
- "description" = "Fusion - 六边形开源大模型 | 集百家之长,铸最强开源模型"
69
- "private" = $false
70
- "has_issues" = $true
71
- "has_projects" = $true
72
- "has_wiki" = $true
73
- "auto_init" = $false
74
- } | ConvertTo-Json
75
-
76
- try {
77
- $response = Invoke-RestMethod -Uri "https://api.github.com/user/repos" `
78
- -Method Post `
79
- -Headers $headers `
80
- -Body $body `
81
- -ContentType "application/json"
82
-
83
- $repoUrl = $response.html_url
84
- $cloneUrl = $response.clone_url
85
-
86
- Write-Host "`n✅ 仓库创建成功!" -ForegroundColor Green
87
- Write-Host " URL: $repoUrl" -ForegroundColor Cyan
88
- Write-Host " Clone URL: $cloneUrl" -ForegroundColor Cyan
89
-
90
- } catch {
91
- if ($_.Exception.Response.StatusCode -eq 422) {
92
- Write-Host "`n⚠️ 仓库 fusion-llm 已存在" -ForegroundColor Yellow
93
- $cloneUrl = "https://github.com/zhan1206/fusion-llm.git"
94
- } else {
95
- Write-Host "`n❌ 创建失败:$($_.Exception.Message)" -ForegroundColor Red
96
- Write-Host " 请检查 Token 权限" -ForegroundColor Yellow
97
- exit 1
98
- }
99
- }
100
-
101
- # 5. 推送代码
102
- Write-Host "`n🚀 推送代码到 GitHub..." -ForegroundColor Yellow
103
-
104
- # 移除已存在的 remote
105
- git remote remove origin 2>$null
106
-
107
- # 添加 remote(使用 HTTPS + Token)
108
- $tokenWithAuth = $tokenPlain
109
- $remoteUrl = $cloneUrl -replace "https://", "https://${tokenWithAuth}@"
110
- git remote add origin $remoteUrl
111
-
112
- # 推送
113
- Write-Host " 推送分支:master" -ForegroundColor Yellow
114
- try {
115
- $pushResult = git push -u origin master 2>&1
116
- Write-Host "`n✅ 推送成功!" -ForegroundColor Green
117
- Write-Host " 项目地址:<ADDRESS_REMOVED>
118
- Write-Host "`n🎉 Fusion 项目已成功发布到 GitHub!" -ForegroundColor Cyan
119
- } catch {
120
- Write-Host "`n❌ 推送失败:$($_.Exception.Message)" -ForegroundColor Red
121
- Write-Host "`n💡 可能的解决方案:" -ForegroundColor Yellow
122
- Write-Host " 1. 使用 SSH 推送(需要配置 SSH key)" -ForegroundColor Yellow
123
- Write-Host " 2. 手动推送:" -ForegroundColor Yellow
124
- Write-Host " git remote add origin https://github.com/zhan1206/fusion-llm.git" -ForegroundColor Gray
125
- Write-Host " git push -u origin master" -ForegroundColor Gray
126
- exit 1
127
- }
128
-
129
- # 6. 清理(移除包含 Token 的 remote)
130
- git remote remove origin
131
- git remote add origin "https://github.com/zhan1206/fusion-llm.git"
132
-
133
- Write-Host "`n✅ 已清理 remote(移除 Token)" -ForegroundColor Green
134
- Write-Host "`n📜 后续操作:" -ForegroundColor Cyan
135
- Write-Host " 1. 撤销当前 Token(安全考虑)" -ForegroundColor Yellow
136
- Write-Host " 访问:https://github.com/settings/tokens" -ForegroundColor Gray
137
- Write-Host "`n 2. 克隆项目:" -ForegroundColor Yellow
138
- Write-Host " git clone https://github.com/zhan1206/fusion-llm.git" -ForegroundColor Gray
139
- Write-Host "`n 3. 安装依赖:" -ForegroundColor Yellow
140
- Write-Host " cd fusion-llm" -ForegroundColor Gray
141
- Write-Host " pip install -r requirements.txt" -ForegroundColor Gray
142
-
143
- Write-Host "`n" + "=" * 60 -ForegroundColor Cyan
144
- Write-Host "完成!" -ForegroundColor Green
145
- Write-Host "=" * 60 -ForegroundColor Cyan
146
-
147
- # 提示用户按任意键退出
148
- Write-Host "`n按任意键退出..."
149
- $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -16,7 +16,7 @@ Fusion 是一套面向**纯本地训练与推理**的开源大语言模型方案
16
 
17
  ### 🧠 滑动分块潜注意力(SBLA)
18
  - 长序列切为定长块,块内高秩潜空间 + 块间极低秩潜向量
19
- - 256K 窗口KV 缓存仅为传统 GQA 的 1/8
20
  - 在 24 GB 显卡上即可对 14B 模型进行长文档微调与推理
21
 
22
  ### 🎛️ 动态推理强度调节器(Thinking Dial)
@@ -137,7 +137,7 @@ clean_data = filter.process(raw_data)
137
  |------|---------|---------|---------|--------|---------|
138
  | Qwen-8B | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 32K | 高 |
139
  | LLaMA-8B | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 8K | 中 |
140
- | **Fusion-8B** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | **256K** | **低** |
141
 
142
  ## 📖 文档
143
 
 
16
 
17
  ### 🧠 滑动分块潜注意力(SBLA)
18
  - 长序列切为定长块,块内高秩潜空间 + 块间极低秩潜向量
19
+ - 当前支持 32K 上下文窗口KV 缓存仅为传统 GQA 的 1/8(SBLA 架构可扩展至 256K,需配置 RoPE scaling)
20
  - 在 24 GB 显卡上即可对 14B 模型进行长文档微调与推理
21
 
22
  ### 🎛️ 动态推理强度调节器(Thinking Dial)
 
137
  |------|---------|---------|---------|--------|---------|
138
  | Qwen-8B | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 32K | 高 |
139
  | LLaMA-8B | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 8K | 中 |
140
+ | **Fusion-8B** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | **32K** (可扩展256K) | **低** |
141
 
142
  ## 📖 文档
143
 
config.json CHANGED
@@ -2,41 +2,44 @@
2
  "_name_or_path": "fusion-8b-base",
3
  "architectures": ["FusionModel"],
4
  "model_type": "fusion",
5
-
6
  "vocab_size": 100000,
7
  "hidden_size": 4096,
8
  "num_hidden_layers": 32,
9
  "num_attention_heads": 32,
10
- "num_key_value_heads": 32,
11
  "intermediate_size": 11008,
12
-
13
  "hidden_act": "silu",
14
- "rms_norm_eps": 1e-05,
 
 
15
  "use_cache": true,
16
-
17
  "max_position_embeddings": 32768,
18
  "initializer_range": 0.02,
19
  "rope_theta": 10000.0,
20
  "rope_scaling": null,
21
-
22
  "attention_bias": false,
23
  "mlp_bias": false,
24
  "attention_dropout": 0.0,
25
-
26
  "block_size": 512,
27
  "latent_dim": 64,
28
  "window_size": 2048,
29
-
 
30
  "enable_thinking_dial": true,
31
  "num_thinking_depths": 4,
32
-
33
  "torch_dtype": "bfloat16",
34
  "transformers_version": "4.36.0",
35
  "attn_implementation": "eager",
36
-
37
  "pad_token_id": 0,
38
  "bos_token_id": 1,
39
  "eos_token_id": 2,
40
-
41
  "tie_word_embeddings": false
42
  }
 
2
  "_name_or_path": "fusion-8b-base",
3
  "architectures": ["FusionModel"],
4
  "model_type": "fusion",
5
+
6
  "vocab_size": 100000,
7
  "hidden_size": 4096,
8
  "num_hidden_layers": 32,
9
  "num_attention_heads": 32,
10
+ "num_key_value_heads": 8,
11
  "intermediate_size": 11008,
12
+
13
  "hidden_act": "silu",
14
+ "hidden_dropout_prob": 0.0,
15
+ "attention_probs_dropout_prob": 0.0,
16
+ "rms_norm_eps": 1e-6,
17
  "use_cache": true,
18
+
19
  "max_position_embeddings": 32768,
20
  "initializer_range": 0.02,
21
  "rope_theta": 10000.0,
22
  "rope_scaling": null,
23
+
24
  "attention_bias": false,
25
  "mlp_bias": false,
26
  "attention_dropout": 0.0,
27
+
28
  "block_size": 512,
29
  "latent_dim": 64,
30
  "window_size": 2048,
31
+ "sbla_mode": "mixed",
32
+
33
  "enable_thinking_dial": true,
34
  "num_thinking_depths": 4,
35
+
36
  "torch_dtype": "bfloat16",
37
  "transformers_version": "4.36.0",
38
  "attn_implementation": "eager",
39
+
40
  "pad_token_id": 0,
41
  "bos_token_id": 1,
42
  "eos_token_id": 2,
43
+
44
  "tie_word_embeddings": false
45
  }
configs/fusion-mini-config.json CHANGED
@@ -2,7 +2,7 @@
2
  "_name_or_path": "fusion-mini",
3
  "architectures": ["FusionMini"],
4
  "model_type": "fusion_mini",
5
-
6
  "vocab_size": 10000,
7
  "hidden_size": 256,
8
  "num_hidden_layers": 2,
@@ -15,20 +15,20 @@
15
  "max_position_embeddings": 256,
16
  "initializer_range": 0.02,
17
  "use_cache": true,
18
-
19
- "block_size": 64,
20
  "latent_dim": 16,
21
- "window_size": 64,
22
- "sbla_mode": "pure_sbla",
23
-
24
  "rms_norm_eps": 1e-5,
25
  "rope_theta": 10000.0,
26
  "tie_word_embeddings": false,
27
-
28
  "torch_dtype": "float32",
29
  "transformers_version": "4.36.0",
30
-
31
  "pad_token_id": 0,
32
  "bos_token_id": 1,
33
  "eos_token_id": 2
34
- }
 
2
  "_name_or_path": "fusion-mini",
3
  "architectures": ["FusionMini"],
4
  "model_type": "fusion_mini",
5
+
6
  "vocab_size": 10000,
7
  "hidden_size": 256,
8
  "num_hidden_layers": 2,
 
15
  "max_position_embeddings": 256,
16
  "initializer_range": 0.02,
17
  "use_cache": true,
18
+
19
+ "block_size": 32,
20
  "latent_dim": 16,
21
+ "window_size": 128,
22
+ "sbla_mode": "mixed",
23
+
24
  "rms_norm_eps": 1e-5,
25
  "rope_theta": 10000.0,
26
  "tie_word_embeddings": false,
27
+
28
  "torch_dtype": "float32",
29
  "transformers_version": "4.36.0",
30
+
31
  "pad_token_id": 0,
32
  "bos_token_id": 1,
33
  "eos_token_id": 2
34
+ }
data/mini_data.json CHANGED
@@ -52,7 +52,7 @@
52
  {
53
  "prompt": "什么是人工智能",
54
  "response": "人工智能是计算机科学的一个分支,致力于创建智能机器。",
55
- "think_rank": 0
56
  },
57
  {
58
  "prompt": "How to learn coding",
@@ -92,7 +92,7 @@
92
  {
93
  "prompt": "如何学习编程",
94
  "response": "学习编程需要理论与实践相结合,多写代码多思考。",
95
- "think_rank": 0
96
  },
97
  {
98
  "prompt": "What is NLP",
@@ -147,7 +147,7 @@
147
  {
148
  "prompt": "如何学习编程",
149
  "response": "学习编程需要理论与实践相结合,多写代码多思考。",
150
- "think_rank": 0
151
  },
152
  {
153
  "prompt": "区块链的原理",
@@ -192,7 +192,7 @@
192
  {
193
  "prompt": "Explain machine learning",
194
  "response": "Machine learning is a subset of AI.",
195
- "think_rank": 0
196
  },
197
  {
198
  "prompt": "What is NLP",
@@ -222,7 +222,7 @@
222
  {
223
  "prompt": "如何学习编程",
224
  "response": "学习编程需要理论与实践相结合,多写代码多思考。",
225
- "think_rank": 0
226
  },
227
  {
228
  "prompt": "What is AI",
@@ -302,7 +302,7 @@
302
  {
303
  "prompt": "如何学习编程",
304
  "response": "学习编程需要理论与实践相结合,多写代码多思考。",
305
- "think_rank": 0
306
  },
307
  {
308
  "prompt": "What is deep learning",
@@ -327,7 +327,7 @@
327
  {
328
  "prompt": "什么是人工智能",
329
  "response": "人工智能是计算机科学的一个分支,致力于创建智能机器。",
330
- "think_rank": 0
331
  },
332
  {
333
  "prompt": "你好",
@@ -357,7 +357,7 @@
357
  {
358
  "prompt": "Explain machine learning",
359
  "response": "Machine learning is a subset of AI.",
360
- "think_rank": 0
361
  },
362
  {
363
  "prompt": "What is deep learning",
@@ -452,7 +452,7 @@
452
  {
453
  "prompt": "解释机器学习",
454
  "response": "机器学习是人工智能的子领域,使计算机能够从数据中学习。",
455
- "think_rank": 0
456
  },
457
  {
458
  "prompt": "深度学习是什么",
@@ -472,7 +472,7 @@
472
  {
473
  "prompt": "Explain machine learning",
474
  "response": "Machine learning is a subset of AI.",
475
- "think_rank": 0
476
  },
477
  {
478
  "prompt": "什么是自然语言处理",
@@ -497,6 +497,46 @@
497
  {
498
  "prompt": "什么是人工智能",
499
  "response": "人工智能是计算机科学的一个分支,致力于创建智能机器。",
500
- "think_rank": 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  }
502
  ]
 
52
  {
53
  "prompt": "什么是人工智能",
54
  "response": "人工智能是计算机科学的一个分支,致力于创建智能机器。",
55
+ "think_rank": 1
56
  },
57
  {
58
  "prompt": "How to learn coding",
 
92
  {
93
  "prompt": "如何学习编程",
94
  "response": "学习编程需要理论与实践相结合,多写代码多思考。",
95
+ "think_rank": 2
96
  },
97
  {
98
  "prompt": "What is NLP",
 
147
  {
148
  "prompt": "如何学习编程",
149
  "response": "学习编程需要理论与实践相结合,多写代码多思考。",
150
+ "think_rank": 2
151
  },
152
  {
153
  "prompt": "区块链的原理",
 
192
  {
193
  "prompt": "Explain machine learning",
194
  "response": "Machine learning is a subset of AI.",
195
+ "think_rank": 2
196
  },
197
  {
198
  "prompt": "What is NLP",
 
222
  {
223
  "prompt": "如何学习编程",
224
  "response": "学习编程需要理论与实践相结合,多写代码多思考。",
225
+ "think_rank": 2
226
  },
227
  {
228
  "prompt": "What is AI",
 
302
  {
303
  "prompt": "如何学习编程",
304
  "response": "学习编程需要理论与实践相结合,多写代码多思考。",
305
+ "think_rank": 2
306
  },
307
  {
308
  "prompt": "What is deep learning",
 
327
  {
328
  "prompt": "什么是人工智能",
329
  "response": "人工智能是计算机科学的一个分支,致力于创建智能机器。",
330
+ "think_rank": 1
331
  },
332
  {
333
  "prompt": "你好",
 
357
  {
358
  "prompt": "Explain machine learning",
359
  "response": "Machine learning is a subset of AI.",
360
+ "think_rank": 2
361
  },
362
  {
363
  "prompt": "What is deep learning",
 
452
  {
453
  "prompt": "解释机器学习",
454
  "response": "机器学习是人工智能的子领域,使计算机能够从数据中学习。",
455
+ "think_rank": 2
456
  },
457
  {
458
  "prompt": "深度学习是什么",
 
472
  {
473
  "prompt": "Explain machine learning",
474
  "response": "Machine learning is a subset of AI.",
475
+ "think_rank": 2
476
  },
477
  {
478
  "prompt": "什么是自然语言处理",
 
497
  {
498
  "prompt": "什么是人工智能",
499
  "response": "人工智能是计算机科学的一个分支,致力于创建智能机器。",
500
+ "think_rank": 1
501
+ },
502
+ {
503
+ "prompt": "Prove the Pythagorean theorem",
504
+ "response": "Consider a right triangle with legs a, b and hypotenuse c. Construct a square of side (a+b) and arrange four copies of the triangle inside. The inner quadrilateral is a square of side c, so (a+b)^2 = c^2 + 4*(ab/2), which simplifies to a^2 + b^2 = c^2.",
505
+ "think_rank": 3
506
+ },
507
+ {
508
+ "prompt": "Derive the quadratic formula",
509
+ "response": "Starting from ax^2 + bx + c = 0, divide by a, complete the square: (x + b/2a)^2 = (b^2 - 4ac)/4a^2. Taking the square root gives x = (-b +- sqrt(b^2 - 4ac)) / 2a.",
510
+ "think_rank": 3
511
+ },
512
+ {
513
+ "prompt": "Prove that sqrt(2) is irrational",
514
+ "response": "Assume sqrt(2) = p/q in lowest terms. Then 2q^2 = p^2, so p is even. Write p = 2k, then 2q^2 = 4k^2, so q^2 = 2k^2, meaning q is also even. This contradicts p/q being in lowest terms.",
515
+ "think_rank": 3
516
+ },
517
+ {
518
+ "prompt": "Prove there are infinitely many primes",
519
+ "response": "Suppose finitely many primes p1,...,pn exist. Let N = p1*p2*...*pn + 1. N is not divisible by any pi (remainder 1). So N is either prime itself or has a prime factor not in the list. Contradiction.",
520
+ "think_rank": 3
521
+ },
522
+ {
523
+ "prompt": "Derive the derivative of sin(x)",
524
+ "response": "Using the limit definition: d/dx sin(x) = lim(h->0) [sin(x+h) - sin(x)] / h. Apply the angle addition formula: sin(x+h) = sin(x)cos(h) + cos(x)sin(h). This gives lim [sin(x)(cos(h)-1)/h + cos(x)sin(h)/h] = sin(x)*0 + cos(x)*1 = cos(x).",
525
+ "think_rank": 3
526
+ },
527
+ {
528
+ "prompt": "Analyze the time complexity of merge sort",
529
+ "response": "Merge sort divides the array in half (O(1)), recurses on both halves (2T(n/2)), and merges in O(n). The recurrence T(n) = 2T(n/2) + O(n) solves to O(n log n) by the master theorem (case 2). This holds for worst, average, and best cases.",
530
+ "think_rank": 3
531
+ },
532
+ {
533
+ "prompt": "Prove the sum of first n natural numbers is n(n+1)/2",
534
+ "response": "By induction: Base case n=1: 1 = 1(2)/2 = 1. Inductive step: assume S(k) = k(k+1)/2. Then S(k+1) = S(k) + (k+1) = k(k+1)/2 + (k+1) = (k+1)(k/2 + 1) = (k+1)(k+2)/2. QED.",
535
+ "think_rank": 3
536
+ },
537
+ {
538
+ "prompt": "Prove that e^x converges for all x",
539
+ "response": "The Taylor series e^x = sum(x^n/n!) has ratio test: |a_(n+1)/a_n| = |x|/(n+1) -> 0 as n -> infinity. Since the limit is 0 < 1 for all x, the series converges absolutely for all real x by the ratio test.",
540
+ "think_rank": 3
541
  }
542
  ]
data_pipeline/t_kd_distillation_train.py ADDED
@@ -0,0 +1,549 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ T-KD 蒸馏训练脚本
3
+
4
+ 真实的蒸馏训练逻辑:
5
+ 1. 加载教师模型(冻结参数)
6
+ 2. 加载学生模型(可训练)
7
+ 3. 计算 KL 散度损失(教师 logits vs 学生 logits)
8
+ 4. 可选:加入硬标签交叉熵损失
9
+
10
+ 使用方法:
11
+ python data_pipeline/t_kd_distillation_train.py \
12
+ --teacher_model "Qwen/Qwen2.5-72B-Instruct" \
13
+ --student_model "./output/fusion-mini" \
14
+ --train_data "data/t_kd_corpus.jsonl" \
15
+ --output_dir "./output/fusion-mini-distilled"
16
+
17
+ 作者:朱子瞻
18
+ 项目:Fusion - 六边形开源大模型
19
+ 许可证:Apache 2.0
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import torch
25
+ import torch.nn as nn
26
+ import torch.nn.functional as F
27
+ from pathlib import Path
28
+ from typing import Optional, List, Dict
29
+ from torch.utils.data import Dataset, DataLoader
30
+ from transformers import (
31
+ AutoTokenizer,
32
+ AutoModelForCausalLM,
33
+ get_linear_schedule_with_warmup,
34
+ )
35
+ import logging
36
+
37
+ logging.basicConfig(level=logging.INFO)
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ class DistillationDataset(Dataset):
42
+ """蒸馏训练数据集"""
43
+
44
+ def __init__(
45
+ self,
46
+ data_path: str,
47
+ tokenizer,
48
+ max_length: int = 2048,
49
+ ):
50
+ self.tokenizer = tokenizer
51
+ self.max_length = max_length
52
+
53
+ # 加载数据
54
+ self.data = []
55
+ with open(data_path, 'r', encoding='utf-8') as f:
56
+ for line in f:
57
+ if line.strip():
58
+ self.data.append(json.loads(line))
59
+
60
+ logger.info(f"✅ 加载数据:{len(self.data)} 条")
61
+
62
+ def __len__(self):
63
+ return len(self.data)
64
+
65
+ def __getitem__(self, idx):
66
+ item = self.data[idx]
67
+
68
+ # 编码
69
+ text = item.get("text", "")
70
+ encoding = self.tokenizer(
71
+ text,
72
+ max_length=self.max_length,
73
+ padding="max_length",
74
+ truncation=True,
75
+ return_tensors="pt",
76
+ )
77
+
78
+ input_ids = encoding["input_ids"].squeeze(0)
79
+ attention_mask = encoding["attention_mask"].squeeze(0)
80
+
81
+ # labels(用于交叉熵损失)
82
+ labels = input_ids.clone()
83
+ labels[labels == self.tokenizer.pad_token_id] = -100
84
+
85
+ return {
86
+ "input_ids": input_ids,
87
+ "attention_mask": attention_mask,
88
+ "labels": labels,
89
+ }
90
+
91
+
92
+ class DistillationTrainer:
93
+ """
94
+ T-KD 蒸馏训练器
95
+
96
+ 核心:学生模型模仿教师模型的输出分布
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ teacher_model_name: str,
102
+ student_model_name: str,
103
+ device: str = "cuda",
104
+ temperature: float = 4.0,
105
+ alpha: float = 0.5, # KL 损失权重
106
+ learning_rate: float = 1e-5,
107
+ batch_size: int = 4,
108
+ grad_accum_steps: int = 8,
109
+ ):
110
+ """
111
+ 初始化蒸馏训练器
112
+
113
+ 参数:
114
+ teacher_model_name: 教师模型(HuggingFace ID 或本地路径)
115
+ student_model_name: 学生模型(本地路径,将训练)
116
+ device: 设备
117
+ temperature: 蒸馏温度(T>1 软化概率分布)
118
+ alpha: 损失权重(alpha * KL + (1-alpha) * CE)
119
+ learning_rate: 学习率
120
+ batch_size: 批次大小
121
+ grad_accum_steps: 梯度累积步数
122
+ """
123
+ self.device = device
124
+ self.temperature = temperature
125
+ self.alpha = alpha
126
+ self.batch_size = batch_size
127
+ self.grad_accum_steps = grad_accum_steps
128
+
129
+ # 1. 加载教师模型(冻结)
130
+ logger.info(f"📚 加载教师模型:{teacher_model_name}")
131
+ self.teacher_tokenizer = AutoTokenizer.from_pretrained(
132
+ teacher_model_name,
133
+ trust_remote_code=True,
134
+ )
135
+
136
+ self.teacher_model = AutoModelForCausalLM.from_pretrained(
137
+ teacher_model_name,
138
+ torch_dtype=torch.bfloat16,
139
+ device_map=device,
140
+ trust_remote_code=True,
141
+ )
142
+
143
+ self.teacher_model.eval()
144
+ for param in self.teacher_model.parameters():
145
+ param.requires_grad = False
146
+
147
+ logger.info(f"✅ 教师模型加载完成(参数已冻结)")
148
+
149
+ # 2. 加载学生模型(可训练)
150
+ logger.info(f"🎓 加载学生模型:{student_model_name}")
151
+ self.student_tokenizer = AutoTokenizer.from_pretrained(
152
+ student_model_name,
153
+ trust_remote_code=True,
154
+ )
155
+
156
+ self.student_model = AutoModelForCausalLM.from_pretrained(
157
+ student_model_name,
158
+ torch_dtype=torch.bfloat16,
159
+ device_map=device,
160
+ trust_remote_code=True,
161
+ )
162
+
163
+ self.student_model.train()
164
+
165
+ logger.info(f"✅ 学生模型加载完成(可训练)")
166
+
167
+ # 3. 优化器 + 学习率调度器
168
+ self.optimizer = torch.optim.AdamW(
169
+ self.student_model.parameters(),
170
+ lr=learning_rate,
171
+ weight_decay=0.01,
172
+ )
173
+
174
+ logger.info(f"✅ 优化器初始化完成(lr={learning_rate})")
175
+
176
+ def compute_distillation_loss(
177
+ self,
178
+ teacher_logits: torch.Tensor,
179
+ student_logits: torch.Tensor,
180
+ labels: torch.Tensor,
181
+ ) -> torch.Tensor:
182
+ """
183
+ 计算蒸馏损失
184
+
185
+ 公式:Loss = alpha * T² * KL(teacher || student) + (1-alpha) * CE(student, labels)
186
+
187
+ 参数:
188
+ teacher_logits: (batch, seq_len, vocab_size)
189
+ student_logits: (batch, seq_len, vocab_size)
190
+ labels: (batch, seq_len)
191
+
192
+ 返回:
193
+ 蒸馏损失
194
+ """
195
+ # 1. KL 散度损失(蒸馏)
196
+ T = self.temperature
197
+ T_squared = T * T
198
+
199
+ # 软化概率分布
200
+ teacher_probs = F.softmax(teacher_logits / T, dim=-1)
201
+ student_log_probs = F.log_softmax(student_logits / T, dim=-1)
202
+
203
+ # KL 散度(教师 || 学生)
204
+ kl_loss = F.kl_div(
205
+ student_log_probs.view(-1, student_logits.size(-1)),
206
+ teacher_probs.view(-1, teacher_logits.size(-1)),
207
+ reduction="batchmean",
208
+ log_target=False,
209
+ ) * T_squared # 温度缩放
210
+
211
+ # 2. 交叉熵损失(硬标签)
212
+ shift_logits = student_logits[..., :-1, :].contiguous()
213
+ shift_labels = labels[..., 1:].contiguous()
214
+
215
+ ce_loss = F.cross_entropy(
216
+ shift_logits.view(-1, student_logits.size(-1)),
217
+ shift_labels.view(-1),
218
+ ignore_index=-100,
219
+ )
220
+
221
+ # 3. 总损失
222
+ total_loss = self.alpha * kl_loss + (1 - self.alpha) * ce_loss
223
+
224
+ return total_loss, kl_loss, ce_loss
225
+
226
+ def train(
227
+ self,
228
+ train_data_path: str,
229
+ output_dir: str,
230
+ num_epochs: int = 3,
231
+ save_steps: int = 500,
232
+ max_length: int = 2048,
233
+ ):
234
+ """
235
+ 执行蒸馏训练
236
+
237
+ 参数:
238
+ train_data_path: 训练数据路径(.jsonl)
239
+ output_dir: 模型保存目录
240
+ num_epochs: 训练轮数
241
+ save_steps: 每 N 步保存一次
242
+ max_length: 最大序列长度
243
+ """
244
+ # 1. 创建数据集
245
+ train_dataset = DistillationDataset(
246
+ train_data_path,
247
+ self.student_tokenizer,
248
+ max_length=max_length,
249
+ )
250
+
251
+ train_dataloader = DataLoader(
252
+ train_dataset,
253
+ batch_size=self.batch_size,
254
+ shuffle=True,
255
+ num_workers=0, # Windows 下设为 0
256
+ )
257
+
258
+ # 2. 学习率调度器
259
+ total_steps = len(train_dataloader) * num_epochs // self.grad_accum_steps
260
+ scheduler = get_linear_schedule_with_warmup(
261
+ self.optimizer,
262
+ num_warmup_steps=int(total_steps * 0.1),
263
+ num_training_steps=total_steps,
264
+ )
265
+
266
+ # 3. 训练循环
267
+ logger.info(f"🚀 开始蒸馏训练...")
268
+ logger.info(f" 轮数:{num_epochs}")
269
+ logger.info(f" 批次大小:{self.batch_size}")
270
+ logger.info(f" 梯度累积:{self.grad_accum_steps}")
271
+ logger.info(f" 温度:{self.temperature}")
272
+ logger.info(f" Alpha:{self.alpha}")
273
+
274
+ global_step = 0
275
+ self.student_model.train()
276
+
277
+ for epoch in range(num_epochs):
278
+ epoch_loss = 0.0
279
+ epoch_kl = 0.0
280
+ epoch_ce = 0.0
281
+ num_batches = 0
282
+
283
+ for batch_idx, batch in enumerate(train_dataloader):
284
+ # 移动到设备
285
+ input_ids = batch["input_ids"].to(self.device)
286
+ attention_mask = batch["attention_mask"].to(self.device)
287
+ labels = batch["labels"].to(self.device)
288
+
289
+ # 教师模型推理(无梯度)
290
+ with torch.no_grad():
291
+ teacher_outputs = self.teacher_model(
292
+ input_ids=input_ids,
293
+ attention_mask=attention_mask,
294
+ )
295
+ teacher_logits = teacher_outputs.logits
296
+
297
+ # 学生模型前向传播
298
+ student_outputs = self.student_model(
299
+ input_ids=input_ids,
300
+ attention_mask=attention_mask,
301
+ )
302
+ student_logits = student_outputs.logits
303
+
304
+ # 计算蒸馏损失
305
+ loss, kl_loss, ce_loss = self.compute_distillation_loss(
306
+ teacher_logits,
307
+ student_logits,
308
+ labels,
309
+ )
310
+
311
+ # 梯度累积(除以累积步数)
312
+ loss = loss / self.grad_accum_steps
313
+ loss.backward()
314
+
315
+ # 梯度裁剪
316
+ torch.nn.utils.clip_grad_norm_(
317
+ self.student_model.parameters(),
318
+ max_norm=1.0,
319
+ )
320
+
321
+ # 更新参数(每 grad_accum_steps 步)
322
+ if (batch_idx + 1) % self.grad_accum_steps == 0:
323
+ self.optimizer.step()
324
+ scheduler.step()
325
+ self.optimizer.zero_grad()
326
+ global_step += 1
327
+
328
+ # 统计
329
+ epoch_loss += loss.item() * self.grad_accum_steps
330
+ epoch_kl += kl_loss.item()
331
+ epoch_ce += ce_loss.item()
332
+ num_batches += 1
333
+
334
+ # 日志
335
+ if batch_idx % 10 == 0:
336
+ logger.info(
337
+ f" Epoch {epoch+1}, Batch {batch_idx}, "
338
+ f"Loss: {loss.item() * self.grad_accum_steps:.4f}, "
339
+ f"KL: {kl_loss.item():.4f}, CE: {ce_loss.item():.4f}"
340
+ )
341
+
342
+ # 清理 GPU 缓存
343
+ del teacher_logits, student_logits, loss
344
+ torch.cuda.empty_cache()
345
+
346
+ # Epoch 结束统计
347
+ avg_loss = epoch_loss / max(num_batches, 1)
348
+ avg_kl = epoch_kl / max(num_batches, 1)
349
+ avg_ce = epoch_ce / max(num_batches, 1)
350
+
351
+ logger.info(f" Epoch {epoch+1}/{num_epochs} 完成")
352
+ logger.info(f" Average Loss: {avg_loss:.4f}")
353
+ logger.info(f" Average KL: {avg_kl:.4f}")
354
+ logger.info(f" Average CE: {avg_ce:.4f}")
355
+
356
+ # 保存检查点
357
+ checkpoint_dir = Path(output_dir) / f"checkpoint-epoch-{epoch+1}"
358
+ checkpoint_dir.mkdir(parents=True, exist_ok=True)
359
+
360
+ self.student_model.save_pretrained(checkpoint_dir)
361
+ self.student_tokenizer.save_pretrained(checkpoint_dir)
362
+
363
+ logger.info(f" ✅ 检查点保存至:{checkpoint_dir}")
364
+
365
+ # 4. 保存最终模型
366
+ output_path = Path(output_dir) / "final"
367
+ output_path.mkdir(parents=True, exist_ok=True)
368
+
369
+ self.student_model.save_pretrained(output_path)
370
+ self.student_tokenizer.save_pretrained(output_path)
371
+
372
+ logger.info(f"🎉 蒸馏训练完成!模型保存至:{output_path}")
373
+
374
+ def evaluate(
375
+ self,
376
+ eval_data_path: str,
377
+ max_length: int = 2048,
378
+ num_samples: int = 100,
379
+ ):
380
+ """
381
+ 评估蒸馏后的模型
382
+
383
+ 参数:
384
+ eval_data_path: 评估数据路径
385
+ max_length: 最大序列长度
386
+ num_samples: 评估样本数
387
+ """
388
+ logger.info(f"📊 开始评估...")
389
+
390
+ self.student_model.eval()
391
+
392
+ # 加载评估数据
393
+ eval_dataset = DistillationDataset(
394
+ eval_data_path,
395
+ self.student_tokenizer,
396
+ max_length=max_length,
397
+ )
398
+
399
+ eval_dataloader = DataLoader(
400
+ eval_dataset,
401
+ batch_size=1,
402
+ shuffle=False,
403
+ )
404
+
405
+ total_loss = 0.0
406
+ num_batches = 0
407
+
408
+ with torch.no_grad():
409
+ for batch in eval_dataloader:
410
+ if num_batches >= num_samples:
411
+ break
412
+
413
+ input_ids = batch["input_ids"].to(self.device)
414
+ attention_mask = batch["attention_mask"].to(self.device)
415
+ labels = batch["labels"].to(self.device)
416
+
417
+ outputs = self.student_model(
418
+ input_ids=input_ids,
419
+ attention_mask=attention_mask,
420
+ labels=labels,
421
+ )
422
+
423
+ total_loss += outputs.loss.item()
424
+ num_batches += 1
425
+
426
+ avg_loss = total_loss / max(num_batches, 1)
427
+
428
+ logger.info(f"✅ 评估完成")
429
+ logger.info(f" Average Loss: {avg_loss:.4f}")
430
+ logger.info(f" Perplexity: {torch.exp(torch.tensor(avg_loss)).item():.2f}")
431
+
432
+
433
+ def main():
434
+ parser = argparse.ArgumentParser(description="T-KD 蒸馏训练")
435
+
436
+ parser.add_argument(
437
+ "--teacher_model",
438
+ type=str,
439
+ required=True,
440
+ help="教师模型(HuggingFace ID 或本地路径)",
441
+ )
442
+
443
+ parser.add_argument(
444
+ "--student_model",
445
+ type=str,
446
+ required=True,
447
+ help="学生模型(本地路径,将训练)",
448
+ )
449
+
450
+ parser.add_argument(
451
+ "--train_data",
452
+ type=str,
453
+ required=True,
454
+ help="训练数据路径(.jsonl)",
455
+ )
456
+
457
+ parser.add_argument(
458
+ "--output_dir",
459
+ type=str,
460
+ required=True,
461
+ help="模型保存目录",
462
+ )
463
+
464
+ parser.add_argument(
465
+ "--num_epochs",
466
+ type=int,
467
+ default=3,
468
+ help="训练轮数",
469
+ )
470
+
471
+ parser.add_argument(
472
+ "--batch_size",
473
+ type=int,
474
+ default=4,
475
+ help="批次大小",
476
+ )
477
+
478
+ parser.add_argument(
479
+ "--grad_accum_steps",
480
+ type=int,
481
+ default=8,
482
+ help="梯度累积步数",
483
+ )
484
+
485
+ parser.add_argument(
486
+ "--temperature",
487
+ type=float,
488
+ default=4.0,
489
+ help="蒸馏温度(T>1 软化概率分布)",
490
+ )
491
+
492
+ parser.add_argument(
493
+ "--alpha",
494
+ type=float,
495
+ default=0.5,
496
+ help="损失权重(alpha * KL + (1-alpha) * CE)",
497
+ )
498
+
499
+ parser.add_argument(
500
+ "--learning_rate",
501
+ type=float,
502
+ default=1e-5,
503
+ help="学习率",
504
+ )
505
+
506
+ parser.add_argument(
507
+ "--max_length",
508
+ type=int,
509
+ default=2048,
510
+ help="最大序列长度",
511
+ )
512
+
513
+ parser.add_argument(
514
+ "--device",
515
+ type=str,
516
+ default="cuda",
517
+ help="设备(cuda/cpu)",
518
+ )
519
+
520
+ args = parser.parse_args()
521
+
522
+ # 创建输出目录
523
+ Path(args.output_dir).mkdir(parents=True, exist_ok=True)
524
+
525
+ # 初始化训练器
526
+ trainer = DistillationTrainer(
527
+ teacher_model_name=args.teacher_model,
528
+ student_model_name=args.student_model,
529
+ device=args.device,
530
+ temperature=args.temperature,
531
+ alpha=args.alpha,
532
+ learning_rate=args.learning_rate,
533
+ batch_size=args.batch_size,
534
+ grad_accum_steps=args.grad_accum_steps,
535
+ )
536
+
537
+ # 训练
538
+ trainer.train(
539
+ train_data_path=args.train_data,
540
+ output_dir=args.output_dir,
541
+ num_epochs=args.num_epochs,
542
+ max_length=args.max_length,
543
+ )
544
+
545
+ logger.info("🎉 蒸馏训练完成!")
546
+
547
+
548
+ if __name__ == "__main__":
549
+ main()
fusion-config-8b.json DELETED
@@ -1,42 +0,0 @@
1
- {
2
- "_name_or_path": "fusion-8b-base",
3
- "architectures": ["FusionModel"],
4
- "model_type": "fusion",
5
-
6
- "vocab_size": 100000,
7
- "hidden_size": 4096,
8
- "num_hidden_layers": 32,
9
- "num_attention_heads": 32,
10
- "num_key_value_heads": 8,
11
- "intermediate_size": 11008,
12
- "hidden_act": "silu",
13
- "hidden_dropout_prob": 0.0,
14
- "attention_probs_dropout_prob": 0.0,
15
- "max_position_embeddings": 32768,
16
- "initializer_range": 0.02,
17
- "use_cache": true,
18
-
19
- "block_size": 512,
20
- "latent_dim": 64,
21
- "window_size": 2048,
22
- "sbla_mode": "mixed",
23
-
24
- "rms_norm_eps": 1e-6,
25
- "rope_theta": 10000.0,
26
- "rope_scaling": null,
27
- "tie_word_embeddings": false,
28
-
29
- "enable_thinking_dial": true,
30
- "num_thinking_depths": 4,
31
-
32
- "torch_dtype": "bfloat16",
33
- "transformers_version": "4.36.0",
34
- "attn_implementation": "eager",
35
-
36
- "pad_token_id": 0,
37
- "bos_token_id": 1,
38
- "eos_token_id": 2,
39
-
40
- "attention_bias": false,
41
- "mlp_bias": false
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
inference/ollama_deploy_v2.py ADDED
@@ -0,0 +1,522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fusion Ollama deployment tool (v2 - fixed)
3
+
4
+ Features:
5
+ 1. Auto-detect llama.cpp path
6
+ 2. Convert HF model to GGUF format
7
+ 3. Generate Modelfile
8
+ 4. Create Ollama model
9
+ 5. Support Thinking Dial control
10
+ 6. Windows-compatible (shell=True for subprocess)
11
+
12
+ Usage:
13
+ python inference/ollama_deploy_v2.py --model_path ./output/fusion-8b --model_name fusion-8b
14
+
15
+ Requirements:
16
+ - llama.cpp (auto-detected or set LLAMA_CPP_DIR)
17
+ - Ollama (https://ollama.com)
18
+
19
+ Author: 朱子瞻 (Zhu Zizhan)
20
+ Project: Fusion - Hexagonal Open-source LLM
21
+ License: Apache 2.0
22
+ """
23
+
24
+ import argparse
25
+ import subprocess
26
+ import os
27
+ import json
28
+ from pathlib import Path
29
+ import logging
30
+ import sys
31
+
32
+ logging.basicConfig(level=logging.INFO)
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ def find_llama_cpp() -> str:
37
+ """
38
+ Auto-detect llama.cpp directory
39
+
40
+ Returns:
41
+ llama.cpp directory path
42
+
43
+ Raises:
44
+ FileNotFoundError: llama.cpp not found
45
+ """
46
+ # 1. Check environment variable
47
+ llama_cpp_dir = os.environ.get("LLAMA_CPP_DIR", "")
48
+ if llama_cpp_dir and os.path.exists(llama_cpp_dir):
49
+ convert_script = os.path.join(llama_cpp_dir, "convert-hf-to-gguf.py")
50
+ if os.path.exists(convert_script):
51
+ logger.info(f"Found llama.cpp from env: {llama_cpp_dir}")
52
+ return llama_cpp_dir
53
+
54
+ # 2. Check common paths
55
+ possible_paths = [
56
+ "./llama.cpp",
57
+ os.path.expanduser("~/llama.cpp"),
58
+ "C:/llama.cpp",
59
+ "D:/llama.cpp",
60
+ os.path.join(os.path.dirname(__file__), "..", "llama.cpp"),
61
+ os.path.join(os.path.dirname(__file__), "..", "..", "llama.cpp"),
62
+ ]
63
+
64
+ for path in possible_paths:
65
+ path = os.path.abspath(path)
66
+ convert_script = os.path.join(path, "convert-hf-to-gguf.py")
67
+ if os.path.exists(convert_script):
68
+ logger.info(f"Auto-detected llama.cpp: {path}")
69
+ return path
70
+
71
+ # 3. Not found
72
+ raise FileNotFoundError(
73
+ "llama.cpp not found. Set LLAMA_CPP_DIR or download to common path.\n"
74
+ "Download: https://github.com/ggerganov/llama.cpp"
75
+ )
76
+
77
+
78
+ def check_dependencies() -> bool:
79
+ """
80
+ Check dependencies (auto-detect llama.cpp + Windows compatible)
81
+
82
+ Returns:
83
+ Whether dependencies are satisfied
84
+ """
85
+ logger.info("Checking dependencies...")
86
+
87
+ # 1. Check llama.cpp
88
+ try:
89
+ llama_cpp_dir = find_llama_cpp()
90
+ convert_script = os.path.join(llama_cpp_dir, "convert-hf-to-gguf.py")
91
+ logger.info(f"llama.cpp convert script found: {convert_script}")
92
+ except FileNotFoundError as e:
93
+ logger.error(f"llama.cpp not found: {e}")
94
+ return False
95
+
96
+ # 2. Check Ollama (Windows needs shell=True)
97
+ try:
98
+ result = subprocess.run(
99
+ ["ollama", "--version"],
100
+ capture_output=True,
101
+ text=True,
102
+ shell=True, # Windows needs shell=True
103
+ timeout=10,
104
+ )
105
+ if result.returncode == 0:
106
+ logger.info(f"Ollama installed: {result.stdout.strip()}")
107
+ else:
108
+ logger.warning("Ollama not installed or not working")
109
+ logger.warning("Please install from https://ollama.com")
110
+ return False
111
+ except FileNotFoundError:
112
+ logger.warning("Ollama not installed")
113
+ logger.warning("Please install from https://ollama.com")
114
+ return False
115
+ except subprocess.TimeoutExpired:
116
+ logger.warning("Ollama check timeout")
117
+ return False
118
+
119
+ logger.info("All dependencies satisfied")
120
+ return True
121
+
122
+
123
+ def convert_to_gguf(
124
+ model_path: str,
125
+ output_path: str,
126
+ quantize: str = "q4_k_m",
127
+ ) -> str:
128
+ """
129
+ Convert HuggingFace model to GGUF format
130
+
131
+ Args:
132
+ model_path: HuggingFace model path
133
+ output_path: Output path
134
+ quantize: Quantization level (q4_k_m, q5_k_m, q8_0, etc.)
135
+
136
+ Returns:
137
+ Converted GGUF file path
138
+ """
139
+ logger.info("Converting to GGUF format...")
140
+
141
+ llama_cpp_dir = find_llama_cpp()
142
+ convert_script = os.path.join(llama_cpp_dir, "convert-hf-to-gguf.py")
143
+
144
+ # Ensure output directory exists
145
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
146
+
147
+ # Conversion command
148
+ cmd = [
149
+ sys.executable, # Use current Python interpreter
150
+ convert_script,
151
+ model_path,
152
+ "--outtype", "f16", # Convert to f16 first
153
+ "--outfile", output_path,
154
+ ]
155
+
156
+ logger.info(f"Running command: {' '.join(cmd)}")
157
+
158
+ result = subprocess.run(
159
+ cmd,
160
+ capture_output=True,
161
+ text=True,
162
+ shell=True, # Windows needs shell=True
163
+ timeout=600, # 10 minutes timeout
164
+ )
165
+
166
+ if result.returncode != 0:
167
+ logger.error(f"Conversion failed: {result.stderr}")
168
+ raise RuntimeError(f"GGUF conversion failed: {result.stderr}")
169
+
170
+ logger.info(f"GGUF conversion complete: {output_path}")
171
+
172
+ # Quantization (optional)
173
+ if quantize and quantize != "f16":
174
+ logger.info(f"Quantizing model ({quantize})...")
175
+
176
+ quantized_path = output_path.replace(".gguf", f"_{quantize}.gguf")
177
+ quantize_cmd = [
178
+ os.path.join(llama_cpp_dir, "llama-quantize"),
179
+ output_path,
180
+ quantized_path,
181
+ quantize,
182
+ ]
183
+
184
+ result = subprocess.run(
185
+ quantize_cmd,
186
+ capture_output=True,
187
+ text=True,
188
+ shell=True,
189
+ timeout=300,
190
+ )
191
+
192
+ if result.returncode != 0:
193
+ logger.warning(f"Quantization failed: {result.stderr}")
194
+ logger.warning("Using unquantized model")
195
+ else:
196
+ output_path = quantized_path
197
+ logger.info(f"Quantization complete: {output_path}")
198
+
199
+ return output_path
200
+
201
+
202
+ def create_modelfile(
203
+ model_path: str,
204
+ modelfile_path: str,
205
+ model_name: str,
206
+ context_size: int = 32768,
207
+ thinking_dial: bool = True,
208
+ ):
209
+ """
210
+ Create Ollama Modelfile
211
+
212
+ Args:
213
+ model_path: GGUF model path
214
+ modelfile_path: Modelfile output path
215
+ model_name: Model name
216
+ context_size: Context window size
217
+ thinking_dial: Whether to enable Thinking Dial
218
+ """
219
+ logger.info("Creating Modelfile...")
220
+
221
+ # Get absolute path of model file
222
+ model_path_abs = os.path.abspath(model_path)
223
+
224
+ # Modelfile content
225
+ content = f"""# Fusion Model: {model_name}
226
+ # Auto-generated by Fusion project
227
+ # Project: https://github.com/zhan1206/fusion-llm
228
+
229
+ FROM {model_path_abs}
230
+
231
+ # Model parameters
232
+ PARAMETER num_ctx {context_size}
233
+ PARAMETER temperature 0.8
234
+ PARAMETER top_p 0.95
235
+ PARAMETER repeat_penalty 1.1
236
+
237
+ # System prompt
238
+ SYSTEM \"\"\"You are a powerful AI assistant. You support dynamic reasoning intensity control:
239
+
240
+ - Simple questions: direct answer
241
+ - Complex questions: enable chain-of-thought reasoning
242
+
243
+ Use <|think| depth=N|> to control reasoning depth (N=0-3).
244
+ \"\"\"
245
+
246
+ # Template (supports Thinking Dial)
247
+ TEMPLATE \"\"\"{{{{ if .System }}}}<|im_start|>system
248
+ {{{{ .System }}}}<|im_end|>
249
+ {{{{ end }}}}{{{{ if .Prompt }}}}<|im_start|>user
250
+ {{{{ .Prompt }}}}<|im_end|>
251
+ {{{{ end }}}}<|im_start|>assistant
252
+ \"\"\"
253
+ """
254
+
255
+ # If Thinking Dial is enabled, add special token handling
256
+ if thinking_dial:
257
+ content += f"""
258
+ # Thinking Dial examples (injected during training)
259
+ # <|think| depth=0|> Simple question, direct answer
260
+ # <|think| depth=3|> Complex question, detailed reasoning
261
+ """
262
+
263
+ # Write file
264
+ with open(modelfile_path, 'w', encoding='utf-8') as f:
265
+ f.write(content)
266
+
267
+ logger.info(f"Modelfile created: {modelfile_path}")
268
+
269
+
270
+ def create_ollama_model(modelfile_path: str, model_name: str) -> bool:
271
+ """
272
+ Create Ollama model using Modelfile
273
+
274
+ Args:
275
+ modelfile_path: Modelfile path
276
+ model_name: Model name
277
+
278
+ Returns:
279
+ Whether creation succeeded
280
+ """
281
+ logger.info(f"Creating Ollama model: {model_name}...")
282
+
283
+ # Remove existing model
284
+ subprocess.run(
285
+ ["ollama", "rm", model_name],
286
+ capture_output=True,
287
+ shell=True,
288
+ )
289
+
290
+ # Create model
291
+ cmd = ["ollama", "create", model_name, "-f", modelfile_path]
292
+
293
+ logger.info(f"Running command: {' '.join(cmd)}")
294
+
295
+ result = subprocess.run(
296
+ cmd,
297
+ capture_output=True,
298
+ text=True,
299
+ shell=True,
300
+ timeout=300,
301
+ )
302
+
303
+ if result.returncode != 0:
304
+ logger.error(f"Creation failed: {result.stderr}")
305
+ return False
306
+
307
+ logger.info(f"Ollama model created: {model_name}")
308
+ logger.info(f"Run `ollama run {model_name}` to start using")
309
+ return True
310
+
311
+
312
+ def deploy(
313
+ model_path: str,
314
+ model_name: str,
315
+ output_dir: str = "./ollama_output",
316
+ quantize: str = "q4_k_m",
317
+ context_size: int = 32768,
318
+ thinking_dial: bool = True,
319
+ ) -> bool:
320
+ """
321
+ Complete deployment pipeline
322
+
323
+ Args:
324
+ model_path: HuggingFace model path
325
+ model_name: Model name
326
+ output_dir: Output directory
327
+ quantize: Quantization level
328
+ context_size: Context window
329
+ thinking_dial: Whether to enable Thinking Dial
330
+
331
+ Returns:
332
+ Whether deployment succeeded
333
+ """
334
+ logger.info("Starting Ollama deployment...")
335
+ logger.info(f"Model path: {model_path}")
336
+ logger.info(f"Model name: {model_name}")
337
+
338
+ # 1. Check dependencies
339
+ if not check_dependencies():
340
+ logger.error("Dependency check failed")
341
+ return False
342
+
343
+ # 2. Create output directory
344
+ os.makedirs(output_dir, exist_ok=True)
345
+
346
+ # 3. Convert to GGUF
347
+ gguf_path = os.path.join(output_dir, f"{model_name}.gguf")
348
+ try:
349
+ gguf_path = convert_to_gguf(
350
+ model_path=model_path,
351
+ output_path=gguf_path,
352
+ quantize=quantize,
353
+ )
354
+ except RuntimeError as e:
355
+ logger.error(f"GGUF conversion failed: {e}")
356
+ return False
357
+
358
+ # 4. Create Modelfile
359
+ modelfile_path = os.path.join(output_dir, "Modelfile")
360
+ create_modelfile(
361
+ model_path=gguf_path,
362
+ modelfile_path=modelfile_path,
363
+ model_name=model_name,
364
+ context_size=context_size,
365
+ thinking_dial=thinking_dial,
366
+ )
367
+
368
+ # 5. Create Ollama model
369
+ if not create_ollama_model(
370
+ modelfile_path=modelfile_path,
371
+ model_name=model_name,
372
+ ):
373
+ logger.error("Ollama model creation failed")
374
+ return False
375
+
376
+ # 6. Generate usage example
377
+ example_path = os.path.join(output_dir, "USAGE.md")
378
+ generate_usage_example(model_name, example_path)
379
+
380
+ logger.info("Deployment complete!")
381
+ logger.info(f"Run: `ollama run {model_name}`")
382
+ logger.info(f"Examples: see {example_path}")
383
+
384
+ return True
385
+
386
+
387
+ def generate_usage_example(model_name: str, output_path: str):
388
+ """
389
+ Generate usage example document
390
+ """
391
+ content = f"""# Fusion Model Usage Examples
392
+
393
+ ## 1. Basic Usage
394
+
395
+ ```bash
396
+ # Start model
397
+ ollama run {model_name}
398
+
399
+ # Input question in interactive interface
400
+ > Explain quantum entanglement
401
+ ```
402
+
403
+ ## 2. Thinking Dial Control
404
+
405
+ Fusion supports dynamic reasoning intensity control. Add control token before question:
406
+
407
+ ```bash
408
+ # depth=0: direct answer (casual chat, translation)
409
+ > <|think| depth=0|> How's the weather today?
410
+
411
+ # depth=1: simple reasoning
412
+ > <|think| depth=1|> Calculate 123 * 456
413
+
414
+ # depth=2: medium reasoning
415
+ > <|think| depth=2|> Prove Pythagorean theorem
416
+
417
+ # depth=3: deep reasoning (chain-of-thought)
418
+ > <|think| depth=3|> Solve this algorithm problem: ...
419
+ ```
420
+
421
+ ## 3. REST API
422
+
423
+ Ollama provides OpenAI-compatible API:
424
+
425
+ ```bash
426
+ # Start Ollama service
427
+ ollama serve
428
+
429
+ # Call API
430
+ curl <a href="http://localhost:11434/api/generate">http://localhost:11434/api/generate</a> -d {{{{
431
+ "model": "{model_name}",
432
+ "prompt": "Explain machine learning",
433
+ "stream": false
434
+ }}}}
435
+ ```
436
+
437
+ ## 4. Python Call
438
+
439
+ ```python
440
+ import ollama
441
+
442
+ # Basic call
443
+ response = ollama.generate(
444
+ model="{model_name}",
445
+ prompt="Explain quantum entanglement",
446
+ )
447
+
448
+ print(response["response"])
449
+
450
+ # With Thinking Dial
451
+ response = ollama.generate(
452
+ model="{model_name}",
453
+ prompt="<|think| depth=2|> Prove Pythagorean theorem",
454
+ )
455
+
456
+ print(response["response"])
457
+ ```
458
+
459
+ ## 5. Parameter Tuning
460
+
461
+ Adjust generation parameters in Ollama:
462
+
463
+ ```bash
464
+ # Temperature (creativity)
465
+ ollama run {model_name} --temperature 0.9
466
+
467
+ # Context window
468
+ ollama run {model_name} --num_ctx 16384
469
+
470
+ # Top-p sampling
471
+ ollama run {model_name} --top_p 0.95
472
+ ```
473
+
474
+ ---
475
+
476
+ **Tip**: See Ollama docs for more: https://ollama.com/docs
477
+ """
478
+
479
+ with open(output_path, 'w', encoding='utf-8') as f:
480
+ f.write(content)
481
+
482
+ logger.info(f"Usage example generated: {output_path}")
483
+
484
+
485
+ def main():
486
+ parser = argparse.ArgumentParser(description="Fusion Ollama One-Click Deployment (v2)")
487
+
488
+ parser.add_argument("--model_path", type=str, required=True,
489
+ help="HuggingFace model path")
490
+ parser.add_argument("--model_name", type=str, required=True,
491
+ help="Ollama model name (e.g., fusion-8b)")
492
+ parser.add_argument("--output_dir", type=str, default="./ollama_output",
493
+ help="Output directory")
494
+ parser.add_argument("--quantize", type=str, default="q4_k_m",
495
+ choices=["q4_k_m", "q5_k_m", "q8_0", "f16"],
496
+ help="Quantization level")
497
+ parser.add_argument("--context_size", type=int, default=32768,
498
+ help="Context window size")
499
+ parser.add_argument("--no_thinking_dial", action="store_false",
500
+ dest="thinking_dial",
501
+ help="Disable Thinking Dial")
502
+
503
+ args = parser.parse_args()
504
+
505
+ # Execute deployment
506
+ success = deploy(
507
+ model_path=args.model_path,
508
+ model_name=args.model_name,
509
+ output_dir=args.output_dir,
510
+ quantize=args.quantize,
511
+ context_size=args.context_size,
512
+ thinking_dial=args.thinking_dial,
513
+ )
514
+
515
+ if success:
516
+ logger.info("Deployment successful!")
517
+ else:
518
+ logger.error("Deployment failed")
519
+
520
+
521
+ if __name__ == "__main__":
522
+ main()
models/fusion_model.py CHANGED
@@ -118,9 +118,14 @@ class RMSNorm(nn.Module):
118
 
119
  class FusionAttention(nn.Module):
120
  """
121
- Fusion 注意力层 - 集成 SBLA
122
 
123
- FusionMiniLayer 中使用,实际集成到 fusion_model.py
 
 
 
 
 
124
  """
125
 
126
  def __init__(self, config: FusionConfig):
@@ -220,7 +225,9 @@ class FusionAttention(nn.Module):
220
  self,
221
  hidden_states: torch.Tensor,
222
  attention_mask: Optional[torch.Tensor] = None,
223
- ) -> torch.Tensor:
 
 
224
  batch_size, seq_len, _ = hidden_states.shape
225
  device = hidden_states.device
226
 
@@ -229,6 +236,14 @@ class FusionAttention(nn.Module):
229
  K = self.k_proj(hidden_states).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
230
  V = self.v_proj(hidden_states).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
231
 
 
 
 
 
 
 
 
 
232
  # 构建注意力掩码
233
  causal_mask = self._build_causal_mask(seq_len, device)
234
  window_mask = self._build_window_mask(seq_len, self.block_size, device)
@@ -274,7 +289,7 @@ class FusionAttention(nn.Module):
274
  output = output_std + gate_value * latent_expanded
275
 
276
  output = self.LayerNorm(output)
277
- return self.dropout(output)
278
 
279
 
280
  class FusionLayer(nn.Module):
@@ -298,11 +313,18 @@ class FusionLayer(nn.Module):
298
  self,
299
  hidden_states: torch.Tensor,
300
  attention_mask: Optional[torch.Tensor] = None,
 
 
301
  **kwargs,
302
- ) -> Tuple[torch.Tensor, None]:
303
  residual = hidden_states
304
  hidden_states = self.input_layernorm(hidden_states)
305
- attn_output = self.attention(hidden_states, attention_mask)
 
 
 
 
 
306
  hidden_states = residual + self.dropout(attn_output)
307
 
308
  residual = hidden_states
@@ -312,7 +334,7 @@ class FusionLayer(nn.Module):
312
  ffn_output = self.down_proj(gate * up)
313
  hidden_states = residual + self.dropout(ffn_output)
314
 
315
- return hidden_states, None
316
 
317
 
318
  class FusionModel(PreTrainedModel, GenerationMixin):
@@ -381,9 +403,23 @@ class FusionModel(PreTrainedModel, GenerationMixin):
381
  float_mask = attention_mask.to(dtype=hidden_states.dtype)
382
  attention_mask = (1.0 - float_mask) * torch.finfo(hidden_states.dtype).min
383
 
384
- # Transformer 层
385
- for layer in self.layers:
386
- hidden_states, _ = layer(hidden_states, attention_mask=attention_mask)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
  # Final norm
389
  hidden_states = self.norm(hidden_states)
@@ -399,6 +435,9 @@ class FusionModel(PreTrainedModel, GenerationMixin):
399
  loss_fct = nn.CrossEntropyLoss()
400
  loss = loss_fct(shift_logits.view(-1, self.config.vocab_size), shift_labels.view(-1))
401
 
 
 
 
402
  if not return_dict:
403
  return (loss, logits) if loss is not None else (logits,)
404
 
@@ -438,8 +477,7 @@ class FusionModel(PreTrainedModel, GenerationMixin):
438
  )
439
 
440
  logits = outputs["logits"]
441
- if "past_key_values" in outputs:
442
- past_key_values = outputs["past_key_values"]
443
 
444
  next_token_logits = logits[:, -1, :] / max(temperature, 1e-8)
445
 
 
118
 
119
  class FusionAttention(nn.Module):
120
  """
121
+ Fusion Attention Layer with integrated SBLA.
122
 
123
+ NOTE (M4): This is a standalone reimplementation. The canonical SBLA logic
124
+ lives in sbla_attention.py (SBLAttention class). Future work should unify
125
+ by having FusionAttention delegate to SBLAttention instead of duplicating
126
+ mask building and block latent computation logic.
127
+
128
+ See: models/sbla_attention.py::SBLAttention
129
  """
130
 
131
  def __init__(self, config: FusionConfig):
 
225
  self,
226
  hidden_states: torch.Tensor,
227
  attention_mask: Optional[torch.Tensor] = None,
228
+ past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
229
+ use_cache: bool = False,
230
+ ) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
231
  batch_size, seq_len, _ = hidden_states.shape
232
  device = hidden_states.device
233
 
 
236
  K = self.k_proj(hidden_states).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
237
  V = self.v_proj(hidden_states).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
238
 
239
+ # KV Cache 逻辑
240
+ if past_key_value is not None:
241
+ past_k, past_v = past_key_value
242
+ K = torch.cat([past_k, K], dim=2)
243
+ V = torch.cat([past_v, V], dim=2)
244
+
245
+ present_key_value = (K, V) if use_cache else None
246
+
247
  # 构建注意力掩码
248
  causal_mask = self._build_causal_mask(seq_len, device)
249
  window_mask = self._build_window_mask(seq_len, self.block_size, device)
 
289
  output = output_std + gate_value * latent_expanded
290
 
291
  output = self.LayerNorm(output)
292
+ return self.dropout(output), present_key_value
293
 
294
 
295
  class FusionLayer(nn.Module):
 
313
  self,
314
  hidden_states: torch.Tensor,
315
  attention_mask: Optional[torch.Tensor] = None,
316
+ past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
317
+ use_cache: bool = False,
318
  **kwargs,
319
+ ) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
320
  residual = hidden_states
321
  hidden_states = self.input_layernorm(hidden_states)
322
+ attn_output, present_key_value = self.attention(
323
+ hidden_states,
324
+ attention_mask,
325
+ past_key_value=past_key_value if past_key_value is not None else None,
326
+ use_cache=use_cache,
327
+ )
328
  hidden_states = residual + self.dropout(attn_output)
329
 
330
  residual = hidden_states
 
334
  ffn_output = self.down_proj(gate * up)
335
  hidden_states = residual + self.dropout(ffn_output)
336
 
337
+ return hidden_states, present_key_value
338
 
339
 
340
  class FusionModel(PreTrainedModel, GenerationMixin):
 
403
  float_mask = attention_mask.to(dtype=hidden_states.dtype)
404
  attention_mask = (1.0 - float_mask) * torch.finfo(hidden_states.dtype).min
405
 
406
+ # Transformer 层(支持 KV Cache)
407
+ past_key_values = kwargs.get("past_key_values", None)
408
+ use_cache = kwargs.get("use_cache", False) or (past_key_values is not None)
409
+
410
+ present_key_values = () if use_cache else None
411
+
412
+ for i, layer in enumerate(self.layers):
413
+ layer_past = past_key_values[i] if past_key_values is not None else None
414
+ layer_outputs, cache = layer(
415
+ hidden_states,
416
+ attention_mask=attention_mask,
417
+ past_key_value=layer_past,
418
+ use_cache=use_cache,
419
+ )
420
+ hidden_states = layer_outputs
421
+ if use_cache:
422
+ present_key_values = present_key_values + (cache,)
423
 
424
  # Final norm
425
  hidden_states = self.norm(hidden_states)
 
435
  loss_fct = nn.CrossEntropyLoss()
436
  loss = loss_fct(shift_logits.view(-1, self.config.vocab_size), shift_labels.view(-1))
437
 
438
+ if use_cache:
439
+ return {"loss": loss, "logits": logits, "past_key_values": present_key_values}
440
+
441
  if not return_dict:
442
  return (loss, logits) if loss is not None else (logits,)
443
 
 
477
  )
478
 
479
  logits = outputs["logits"]
480
+ past_key_values = outputs.get("past_key_values", None)
 
481
 
482
  next_token_logits = logits[:, -1, :] / max(temperature, 1e-8)
483
 
models/tokenizer.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fusion Tokenizer - Unified tokenizer management
3
+
4
+ Handles the gap between GPT2 (vocab_size=50257) and Fusion's target vocab (100K).
5
+ Current status: Uses GPT2 as placeholder until SentencePiece model is trained.
6
+
7
+ Usage:
8
+ from models.tokenizer import get_tokenizer
9
+ tokenizer = get_tokenizer("gpt2") # placeholder
10
+ tokenizer = get_tokenizer("fusion", vocab_size=100000) # future: SentencePiece
11
+
12
+ Author: Zhu Zizhan
13
+ Project: Fusion-LLM
14
+ License: Apache 2.0
15
+ """
16
+
17
+ import json
18
+ import os
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ try:
23
+ from transformers import AutoTokenizer, PreTrainedTokenizer
24
+ except ImportError:
25
+ AutoTokenizer = None
26
+ PreTrainedTokenizer = None
27
+
28
+
29
+ # Fusion special tokens
30
+ FUSION_SPECIAL_TOKENS = {
31
+ "pad_token": "<|pad|>",
32
+ "bos_token": "<|start|>",
33
+ "eos_token": "<|end|>",
34
+ "think_tokens": ["<|think_depth_0|>", "<|think_depth_1|>", "<|think_depth_2|>", "<|think_depth_3|>"],
35
+ }
36
+
37
+
38
+ def get_tokenizer(
39
+ tokenizer_type: str = "gpt2",
40
+ vocab_size: int = 50257,
41
+ tokenizer_dir: Optional[str] = None,
42
+ ) -> "PreTrainedTokenizer":
43
+ """
44
+ Get a tokenizer for Fusion models.
45
+
46
+ Args:
47
+ tokenizer_type: "gpt2" (placeholder) or "fusion" (SentencePiece, if available)
48
+ vocab_size: Target vocabulary size
49
+ tokenizer_dir: Directory containing tokenizer files (for "fusion" type)
50
+
51
+ Returns:
52
+ A HuggingFace PreTrainedTokenizer instance
53
+
54
+ Notes:
55
+ - "gpt2" mode: Uses GPT2 BPE tokenizer (vocab_size=50257). This is a
56
+ PLACEHOLDER. The model config should set vocab_size=50257 when using this.
57
+ - "fusion" mode: Loads a SentencePiece tokenizer from tokenizer_dir.
58
+ Requires tokenizer.model file to exist. Falls back to GPT2 if not found.
59
+ """
60
+ if AutoTokenizer is None:
61
+ raise ImportError("transformers is required: pip install transformers")
62
+
63
+ if tokenizer_type == "fusion":
64
+ sp_model_path = None
65
+ if tokenizer_dir:
66
+ sp_model_path = Path(tokenizer_dir) / "tokenizer.model"
67
+ else:
68
+ # Try project root
69
+ for candidate in ["tokenizers", ".", "data"]:
70
+ p = Path(candidate) / "tokenizer.model"
71
+ if p.exists():
72
+ sp_model_path = p
73
+ break
74
+
75
+ if sp_model_path and sp_model_path.exists():
76
+ tokenizer = AutoTokenizer.from_pretrained(
77
+ str(sp_model_path.parent),
78
+ tokenizer_type="SentencePiece",
79
+ )
80
+ tokenizer = _add_fusion_special_tokens(tokenizer)
81
+ return tokenizer
82
+ else:
83
+ import warnings
84
+ warnings.warn(
85
+ "Fusion SentencePiece tokenizer not found. "
86
+ "Falling back to GPT2 tokenizer. "
87
+ "Set vocab_size=50257 in model config to match.",
88
+ UserWarning,
89
+ )
90
+ tokenizer_type = "gpt2"
91
+ vocab_size = 50257
92
+
93
+ if tokenizer_type == "gpt2":
94
+ tokenizer = AutoTokenizer.from_pretrained("gpt2")
95
+ tokenizer.pad_token = tokenizer.eos_token
96
+ tokenizer = _add_fusion_special_tokens(tokenizer)
97
+
98
+ # Verify vocab size consistency
99
+ actual_vocab = len(tokenizer)
100
+ if actual_vocab != vocab_size:
101
+ import warnings
102
+ warnings.warn(
103
+ f"GPT2 tokenizer vocab_size={actual_vocab}, but config specifies {vocab_size}. "
104
+ f"Using actual tokenizer size ({actual_vocab}). "
105
+ f"Update model config vocab_size to match.",
106
+ UserWarning,
107
+ )
108
+ return tokenizer
109
+
110
+ raise ValueError(f"Unknown tokenizer_type: {tokenizer_type}")
111
+
112
+
113
+ def _add_fusion_special_tokens(tokenizer: "PreTrainedTokenizer") -> "PreTrainedTokenizer":
114
+ """Add Fusion-specific special tokens to any tokenizer."""
115
+ special_tokens_dict = {
116
+ "pad_token": FUSION_SPECIAL_TOKENS["pad_token"],
117
+ "additional_special_tokens": FUSION_SPECIAL_TOKENS["think_tokens"],
118
+ }
119
+ tokenizer.add_special_tokens(special_tokens_dict)
120
+ return tokenizer
121
+
122
+
123
+ def get_effective_vocab_size(tokenizer_type: str = "gpt2", requested_vocab: int = 100000) -> int:
124
+ """
125
+ Return the effective vocab size that should be used in model config.
126
+ This ensures model embedding size matches the actual tokenizer.
127
+ """
128
+ if tokenizer_type == "gpt2":
129
+ return 50257 + len(FUSION_SPECIAL_TOKENS["think_tokens"]) + 1 # ~50262
130
+ if tokenizer_type == "fusion":
131
+ return requested_vocab
132
+ return requested_vocab
133
+
134
+
135
+ if __name__ == "__main__":
136
+ print("[Fusion Tokenizer] Testing tokenizer creation...")
137
+ tok = get_tokenizer("gpt2")
138
+ print(f" Type: GPT2 (placeholder)")
139
+ print(f" Vocab size: {len(tok)}")
140
+ print(f" Pad token: {tok.pad_token}")
141
+ print(f" Think tokens: {FUSION_SPECIAL_TOKENS['think_tokens']}")
142
+ print(f" Effective vocab: {get_effective_vocab_size('gpt2')}")
143
+ test_text = "Hello, Fusion! <|think_depth_2|>"
144
+ encoded = tok.encode(test_text)
145
+ decoded = tok.decode(encoded)
146
+ print(f" Encode/decode test: '{test_text}' -> {encoded} -> '{decoded}'")
push_to_github.py DELETED
@@ -1,228 +0,0 @@
1
- """
2
- Fusion 项目 GitHub 推送脚本
3
-
4
- 使用方法(在你本地电脑运行):
5
- 1. 安装依赖:pip install requests
6
- 2. 运行脚本:python push_to_github.py
7
- 3. 按提示输入 GitHub token(不会显示在聊天中)
8
-
9
- 注意:Token 需要 repo 权限(全选)
10
- 创建 Token:https://github.com/settings/tokens
11
-
12
- 作者:朱子瞻
13
- 项目:Fusion - 六边形开源大模型
14
- """
15
-
16
- import os
17
- import json
18
- import subprocess
19
- import getpass
20
- import requests
21
- from pathlib import Path
22
-
23
-
24
- def create_github_repo(token: str, repo_name: str = "fusion-llm", private: bool = False):
25
- """
26
- 使用 GitHub API 创建仓库
27
-
28
- 参数:
29
- token: GitHub Personal Access Token
30
- repo_name: 仓库名称
31
- private: 是否私有(默认 False,公开)
32
-
33
- 返回:
34
- 仓库 URL
35
- """
36
- print(f"\n📦 创建 GitHub 仓库:{repo_name}...")
37
-
38
- url = "https://api.github.com/user/repos"
39
-
40
- headers = {
41
- "Authorization": f"token {token}",
42
- "Accept": "application/vnd.github.v3+json",
43
- }
44
-
45
- data = {
46
- "name": repo_name,
47
- "description": "Fusion - 六边形开源大模型 | 集百家之长,铸最强开源模型",
48
- "private": private,
49
- "has_issues": True,
50
- "has_projects": True,
51
- "has_wiki": True,
52
- "auto_init": False, # 不要自动创建 README
53
- }
54
-
55
- response = requests.post(url, headers=headers, json=data)
56
-
57
- if response.status_code == 201:
58
- repo_data = response.json()
59
- repo_url = repo_data["html_url"]
60
- clone_url = repo_data["clone_url"]
61
-
62
- print(f"✅ 仓库创建成功!")
63
- print(f" URL: {repo_url}")
64
- print(f" 克隆 URL (HTTPS): {clone_url}")
65
-
66
- return clone_url
67
-
68
- elif response.status_code == 422:
69
- # 仓库已存在
70
- print(f"⚠️ 仓库 {repo_name} 已存在")
71
- return f"https://github.com/zhan1206/{repo_name}.git"
72
-
73
- else:
74
- print(f"❌ 创建失败:{response.status_code}")
75
- print(f" 错误信息:{response.text}")
76
- return None
77
-
78
-
79
- def push_to_github(repo_url: str, project_dir: str, use_ssh: bool = False):
80
- """
81
- 推送代码到 GitHub
82
-
83
- 参数:
84
- repo_url: 仓库 URL
85
- project_dir: 项目目录
86
- use_ssh: 是否使用 SSH(默认 False,使用 HTTPS)
87
- """
88
- print(f"\n🚀 推送代码到 GitHub...")
89
-
90
- # 切换到项目目录
91
- os.chdir(project_dir)
92
-
93
- # 如果已经设置了 remote,先删除
94
- subprocess.run(
95
- ["git", "remote", "remove", "origin"],
96
- capture_output=True,
97
- )
98
-
99
- # 添加 remote
100
- if use_ssh:
101
- # SSH 格式
102
- ssh_url = repo_url.replace(
103
- "https://github.com/",
104
- "git@github.com:",
105
- )
106
- remote_url = ssh_url
107
- else:
108
- # HTTPS 格式
109
- remote_url = repo_url
110
-
111
- print(f" Remote URL: {remote_url}")
112
-
113
- result = subprocess.run(
114
- ["git", "remote", "add", "origin", remote_url],
115
- capture_output=True,
116
- text=True,
117
- )
118
-
119
- if result.returncode != 0:
120
- print(f"❌ 添加 remote 失败:{result.stderr}")
121
- return False
122
-
123
- # 推送代码
124
- print(f" 推送分支:master")
125
-
126
- result = subprocess.run(
127
- ["git", "push", "-u", "origin", "master"],
128
- capture_output=True,
129
- text=True,
130
- )
131
-
132
- if result.returncode == 0:
133
- print(f"✅ 推送成功!")
134
- print(f"\n🎉 项目已发布:{repo_url.replace('.git', '')}")
135
- return True
136
- else:
137
- print(f"❌ 推送失败:{result.stderr}")
138
-
139
- # 如果是 HTTPS 且失败,提示使用 SSH
140
- if not use_ssh:
141
- print(f"\n💡 提示:如果 HTTPS 推送失败,可以尝试使用 SSH:")
142
- print(f" 1. 生成 SSH key:ssh-keygen -t ed25519 -C \"your_email@example.com\"")
143
- print(f" 2. 添加 SSH key 到 GitHub:https://github.com/settings/keys")
144
- print(f" 3. 重新运行脚本,输入 'y' 使用 SSH")
145
-
146
- return False
147
-
148
-
149
- def main():
150
- print("=" * 60)
151
- print("Fusion 项目 GitHub 推送脚本")
152
- print("=" * 60)
153
-
154
- # 1. 获取项目目录
155
- project_dir = os.path.dirname(os.path.abspath(__file__))
156
- print(f"\n📂 项目目录:{project_dir}")
157
-
158
- # 2. 检查 Git 状态
159
- os.chdir(project_dir)
160
-
161
- result = subprocess.run(
162
- ["git", "status", "--porcelain"],
163
- capture_output=True,
164
- text=True,
165
- )
166
-
167
- if result.stdout:
168
- print(f"\n⚠️ 有未提交的更改:")
169
- print(result.stdout)
170
-
171
- commit = input("\n是否提交这些更改?(y/N): ").strip().lower()
172
-
173
- if commit == 'y':
174
- subprocess.run(["git", "add", "."])
175
- commit_msg = input("输入提交信息(默认:Update):") or "Update"
176
- subprocess.run(["git", "commit", "-m", commit_msg])
177
- print(f"✅ 已提交")
178
- else:
179
- print(f"⚠️ 取消推送")
180
- return
181
-
182
- # 3. 获取 GitHub Token(安全输入,不显示)
183
- print(f"\n🔐 输入 GitHub Personal Access Token")
184
- print(f" 创建 Token:https://github.com/settings/tokens")
185
- print(f" 需要权限:repo(全选)")
186
- print(f" (输入时不会显示,这是正常的)")
187
-
188
- token = getpass.getpass("Token: ")
189
-
190
- if not token:
191
- print(f"❌ Token 不能为空")
192
- return
193
-
194
- # 4. 创建 GitHub 仓库
195
- repo_name = "fusion-llm"
196
- repo_url = create_github_repo(token, repo_name, private=False)
197
-
198
- if not repo_url:
199
- print(f"❌ 创建仓库失败")
200
- return
201
-
202
- # 5. 推送代码
203
- use_ssh = input("\n使用 SSH 推送?(y/N): ").strip().lower() == 'y'
204
-
205
- success = push_to_github(repo_url, project_dir, use_ssh=use_ssh)
206
-
207
- if not success and not use_ssh:
208
- # 如果 HTTPS 失败,询问是否尝试 SSH
209
- retry_ssh = input("\n是否尝试使用 SSH 推送?(y/N): ").strip().lower()
210
-
211
- if retry_ssh == 'y':
212
- # 修改 URL 为 SSH 格式
213
- ssh_url = repo_url.replace(
214
- "https://github.com/",
215
- "git@github.com:",
216
- )
217
- success = push_to_github(ssh_url, project_dir, use_ssh=True)
218
-
219
- if success:
220
- print(f"\n🎉 完成!项目已成功推送到 GitHub")
221
- print(f" 仓库地址:<ADDRESS_REMOVED>
222
- print(f" 克隆命令:git clone {repo_url.replace('.git', '')}.git")
223
- else:
224
- print(f"\n❌ 推送失败,请检查错误信息")
225
-
226
-
227
- if __name__ == "__main__":
228
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -8,6 +8,7 @@ datasets>=2.16.0
8
 
9
  # PEFT (LoRA/QLoRA)
10
  peft>=0.7.0
 
11
 
12
  # 分布式训练
13
  deepspeed>=0.12.0
@@ -25,8 +26,12 @@ tokenizers>=0.15.0
25
  langid>=1.1.6
26
 
27
  # 推理部署
28
- # 注意:Ollama 需要从 https://ollama.com/ 下载二进制客户端
29
- # 或使用 ollama-python SDK: pip install ollama
 
 
 
 
30
 
31
  # 评估(可选)
32
  evaluate>=0.4.0
 
8
 
9
  # PEFT (LoRA/QLoRA)
10
  peft>=0.7.0
11
+ bitsandbytes>=0.41.0
12
 
13
  # 分布式训练
14
  deepspeed>=0.12.0
 
26
  langid>=1.1.6
27
 
28
  # 推理部署
29
+ ollama>=0.1.0
30
+ # 注意:Ollama 也需要从 https://ollama.com/ 下载二进制客户端
31
+
32
+ # 量化工具依赖
33
+ scipy>=1.11.0
34
+ scikit-learn>=1.3.0
35
 
36
  # 评估(可选)
37
  evaluate>=0.4.0
scripts/add_depth3_samples.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Add depth=3 samples to mini_data.json for Thinking Dial coverage."""
3
+
4
+ import json
5
+
6
+ DEPTH_3_SAMPLES = [
7
+ {"prompt": "Prove the Pythagorean theorem", "response": "Consider a right triangle with legs a, b and hypotenuse c. Construct a square of side (a+b) and arrange four copies of the triangle inside. The inner quadrilateral is a square of side c, so (a+b)^2 = c^2 + 4*(ab/2), which simplifies to a^2 + b^2 = c^2.", "think_rank": 3},
8
+ {"prompt": "Derive the quadratic formula", "response": "Starting from ax^2 + bx + c = 0, divide by a, complete the square: (x + b/2a)^2 = (b^2 - 4ac)/4a^2. Taking the square root gives x = (-b +- sqrt(b^2 - 4ac)) / 2a.", "think_rank": 3},
9
+ {"prompt": "Prove that sqrt(2) is irrational", "response": "Assume sqrt(2) = p/q in lowest terms. Then 2q^2 = p^2, so p is even. Write p = 2k, then 2q^2 = 4k^2, so q^2 = 2k^2, meaning q is also even. This contradicts p/q being in lowest terms.", "think_rank": 3},
10
+ {"prompt": "Prove there are infinitely many primes", "response": "Suppose finitely many primes p1,...,pn exist. Let N = p1*p2*...*pn + 1. N is not divisible by any pi (remainder 1). So N is either prime itself or has a prime factor not in the list. Contradiction.", "think_rank": 3},
11
+ {"prompt": "Derive the derivative of sin(x)", "response": "Using the limit definition: d/dx sin(x) = lim(h->0) [sin(x+h) - sin(x)] / h. Apply the angle addition formula: sin(x+h) = sin(x)cos(h) + cos(x)sin(h). This gives lim [sin(x)(cos(h)-1)/h + cos(x)sin(h)/h] = sin(x)*0 + cos(x)*1 = cos(x).", "think_rank": 3},
12
+ {"prompt": "Analyze the time complexity of merge sort", "response": "Merge sort divides the array in half (O(1)), recurses on both halves (2T(n/2)), and merges in O(n). The recurrence T(n) = 2T(n/2) + O(n) solves to O(n log n) by the master theorem (case 2). This holds for worst, average, and best cases.", "think_rank": 3},
13
+ {"prompt": "Prove the sum of first n natural numbers is n(n+1)/2", "response": "By induction: Base case n=1: 1 = 1(2)/2 = 1. Inductive step: assume S(k) = k(k+1)/2. Then S(k+1) = S(k) + (k+1) = k(k+1)/2 + (k+1) = (k+1)(k/2 + 1) = (k+1)(k+2)/2. QED.", "think_rank": 3},
14
+ {"prompt": "Prove that e^x converges for all x", "response": "The Taylor series e^x = sum(x^n/n!) has ratio test: |a_(n+1)/a_n| = |x|/(n+1) -> 0 as n -> infinity. Since the limit is 0 < 1 for all x, the series converges absolutely for all real x by the ratio test.", "think_rank": 3},
15
+ ]
16
+
17
+ def main():
18
+ with open('data/mini_data.json', 'r', encoding='utf-8') as f:
19
+ data = json.load(f)
20
+
21
+ old_dist = {}
22
+ for item in data:
23
+ r = item.get('think_rank', 0)
24
+ old_dist[r] = old_dist.get(r, 0) + 1
25
+ print(f"Before: {old_dist}")
26
+
27
+ data.extend(DEPTH_3_SAMPLES)
28
+
29
+ new_dist = {}
30
+ for item in data:
31
+ r = item.get('think_rank', 0)
32
+ new_dist[r] = new_dist.get(r, 0) + 1
33
+ print(f"After: {new_dist}")
34
+
35
+ with open('data/mini_data.json', 'w', encoding='utf-8') as f:
36
+ json.dump(data, f, ensure_ascii=False, indent=2)
37
+
38
+ print(f"Total: {len(data)} items")
39
+
40
+
41
+ if __name__ == '__main__':
42
+ main()
{tests → scripts}/create_mini_data.py RENAMED
@@ -67,10 +67,20 @@ def create_mini_dataset(output_path: str, num_samples: int = 100):
67
  else:
68
  prompt, response = random.choice(english_samples)
69
 
 
 
 
 
 
 
 
 
 
 
70
  data.append({
71
  "prompt": prompt,
72
  "response": response,
73
- "think_rank": 0, # mini 模型不使用 thinking dial
74
  })
75
 
76
  # 保存为 JSON
 
67
  else:
68
  prompt, response = random.choice(english_samples)
69
 
70
+ # Assign think_rank based on content depth
71
+ if any(kw in prompt for kw in ["Prove", "Derive", "Analyze", "\u8bc1\u660e", "\u63a8\u5bfc", "\u5206\u6790"]):
72
+ think_rank = 3
73
+ elif any(kw in prompt for kw in ["Explain", "How", "Why", "\u89e3\u91ca", "\u5982\u4f55", "\u4e3a\u4ec0\u4e48"]):
74
+ think_rank = 2
75
+ elif any(kw in prompt for kw in ["Write", "Implement", "\u5199", "\u5b9e\u73b0"]):
76
+ think_rank = 1
77
+ else:
78
+ think_rank = 0
79
+
80
  data.append({
81
  "prompt": prompt,
82
  "response": response,
83
+ "think_rank": think_rank,
84
  })
85
 
86
  # 保存为 JSON
debug_attn.py → scripts/debug_attn.py RENAMED
File without changes
debug_layer.py → scripts/debug_layer.py RENAMED
File without changes
debug_lm.py → scripts/debug_lm.py RENAMED
File without changes
debug_loss.py → scripts/debug_loss.py RENAMED
File without changes
debug_mask.py → scripts/debug_mask.py RENAMED
File without changes
scripts/fix_mini_data.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Fix mini_data.json: distribute think_rank 0-3 based on prompt content."""
3
+
4
+ import json
5
+ import re
6
+
7
+ # Keywords suggesting different thinking depths
8
+ DEPTH_3_KEYWORDS = ['prove', 'theorem', 'proof', 'derive', 'mathematical', 'complex',
9
+ 'prove', 'derive', 'calculate', 'analyze deeply',
10
+ '\u8bc1\u660e', '\u63a8\u5bfc', '\u5b9a\u7406', '\u590d\u6742', '\u6df1\u5165\u5206\u6790']
11
+ DEPTH_2_KEYWORDS = ['explain', 'why', 'how does', 'compare', 'difference',
12
+ 'algorithm', 'design', 'optimize',
13
+ '\u89e3\u91ca', '\u4e3a\u4ec0\u4e48', '\u5982\u4f55', '\u6bd4\u8f83', '\u7b97\u6cd5', '\u8bbe\u8ba1', '\u4f18\u5316']
14
+ DEPTH_1_KEYWORDS = ['write', 'implement', 'code', 'function', 'create',
15
+ '\u5199', '\u5b9e\u73b0', '\u7f16\u5199', '\u4ee3\u7801', '\u521b\u5efa']
16
+
17
+
18
+ def assign_depth(item):
19
+ text = (item.get('prompt', '') + ' ' + item.get('response', '')).lower()
20
+ for kw in DEPTH_3_KEYWORDS:
21
+ if kw.lower() in text:
22
+ return 3
23
+ for kw in DEPTH_2_KEYWORDS:
24
+ if kw.lower() in text:
25
+ return 2
26
+ for kw in DEPTH_1_KEYWORDS:
27
+ if kw.lower() in text:
28
+ return 1
29
+ return 0
30
+
31
+
32
+ def main():
33
+ with open('data/mini_data.json', 'r', encoding='utf-8') as f:
34
+ data = json.load(f)
35
+
36
+ # Count current distribution
37
+ old_dist = {}
38
+ for item in data:
39
+ r = item.get('think_rank', 0)
40
+ old_dist[r] = old_dist.get(r, 0) + 1
41
+ print(f"Before fix: {old_dist}")
42
+
43
+ # Fix
44
+ for item in data:
45
+ item['think_rank'] = assign_depth(item)
46
+
47
+ # Count new distribution
48
+ new_dist = {}
49
+ for item in data:
50
+ r = item.get('think_rank', 0)
51
+ new_dist[r] = new_dist.get(r, 0) + 1
52
+ print(f"After fix: {new_dist}")
53
+
54
+ with open('data/mini_data.json', 'w', encoding='utf-8') as f:
55
+ json.dump(data, f, ensure_ascii=False, indent=2)
56
+
57
+ print(f"Fixed {len(data)} items")
58
+
59
+
60
+ if __name__ == '__main__':
61
+ main()
{tests → scripts}/fix_thinking_dial.py RENAMED
File without changes
{tests → scripts}/fix_thinking_dial2.py RENAMED
File without changes
scripts/train_tokenizer.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Train a SentencePiece tokenizer for Fusion models.
4
+
5
+ This creates the tokenizer.model file needed by Fusion's 100K vocabulary.
6
+ Training data should be a plain text file with one sentence per line.
7
+
8
+ Usage:
9
+ python scripts/train_tokenizer.py --input data/tokenizer_train.txt --vocab_size 100000 --output tokenizers/
10
+
11
+ Requirements:
12
+ pip install sentencepiece
13
+
14
+ Author: Zhu Zizhan
15
+ Project: Fusion-LLM
16
+ License: Apache 2.0
17
+ """
18
+
19
+ import argparse
20
+ import os
21
+ import sys
22
+
23
+
24
+ def train_tokenizer(input_path: str, vocab_size: int, output_dir: str, model_type: str = "unigram"):
25
+ """Train a SentencePiece tokenizer."""
26
+ try:
27
+ import sentencepiece as spm
28
+ except ImportError:
29
+ print("[ERROR] sentencepiece not installed. Run: pip install sentencepiece")
30
+ sys.exit(1)
31
+
32
+ if not os.path.exists(input_path):
33
+ print(f"[ERROR] Training data not found: {input_path}")
34
+ print("Create a plain text file with one sentence per line.")
35
+ print("For bilingual (zh+en) tokenizer, mix Chinese and English text.")
36
+ sys.exit(1)
37
+
38
+ os.makedirs(output_dir, exist_ok=True)
39
+ model_prefix = os.path.join(output_dir, "tokenizer")
40
+
41
+ print(f"[Tokenizer] Training SentencePiece model...")
42
+ print(f" Input: {input_path}")
43
+ print(f" Vocab size: {vocab_size}")
44
+ print(f" Model type: {model_type}")
45
+ print(f" Output: {output_dir}/")
46
+
47
+ # Special tokens for Fusion
48
+ control_symbols = [
49
+ "<|pad|>", "<|start|>", "<|end|>",
50
+ "<|think_depth_0|>", "<|think_depth_1|>",
51
+ "<|think_depth_2|>", "<|think_depth_3|>",
52
+ ]
53
+
54
+ spm.SentencePieceTrainer.train(
55
+ input=input_path,
56
+ model_prefix=model_prefix,
57
+ vocab_size=vocab_size,
58
+ model_type=model_type,
59
+ character_coverage=0.9995, # High coverage for CJK
60
+ input_sentence_size=10000000,
61
+ shuffle_input_sentence=True,
62
+ control_symbols=control_symbols,
63
+ unk_id=0,
64
+ bos_id=1,
65
+ eos_id=2,
66
+ pad_id=3,
67
+ byte_fallback=True, # Important for multilingual
68
+ split_by_unicode_script=True,
69
+ allow_whitespace_only_pieces=True,
70
+ )
71
+
72
+ model_path = os.path.join(output_dir, "tokenizer.model")
73
+ vocab_path = os.path.join(output_dir, "tokenizer.vocab")
74
+
75
+ print(f"\n[Done] Tokenizer trained successfully!")
76
+ print(f" Model: {model_path}")
77
+ print(f" Vocab: {vocab_path}")
78
+
79
+ # Verify
80
+ sp = spm.SentencePieceProcessor()
81
+ sp.load(model_path)
82
+ print(f" Actual vocab size: {sp.get_piece_size()}")
83
+
84
+ # Test
85
+ test_zh = "Fusion是一个开源大语言模型"
86
+ test_en = "Fusion is an open-source language model"
87
+ print(f"\n Test encode (zh): {test_zh}")
88
+ print(f" -> {sp.encode(test_zh)}")
89
+ print(f" Test encode (en): {test_en}")
90
+ print(f" -> {sp.encode(test_en)}")
91
+
92
+
93
+ def create_sample_training_data(output_path: str, num_lines: int = 100000):
94
+ """Create sample training data for tokenizer (for testing only)."""
95
+ import random
96
+
97
+ print(f"[Sample Data] Creating sample training data: {output_path}")
98
+ samples_zh = [
99
+ "人工智能是计算机科学的一个重要分支",
100
+ "深度学习使用多层神经网络来模拟人脑",
101
+ "自然语言处理帮助计算机理解人类语言",
102
+ "机器学习使计算机能够从数据中学习",
103
+ "大语言模型在文本生成任务中表现出色",
104
+ "Transformer架构彻底改变了自然语言处理领域",
105
+ "注意力机制是现代深度学习的核心组件",
106
+ "预训练语言模型通过大规模语料学习语言知识",
107
+ "微调技术使模型适应特定下游任务",
108
+ "开源模型推动了人工智能技术的普及",
109
+ ]
110
+ samples_en = [
111
+ "Artificial intelligence is a branch of computer science",
112
+ "Deep learning uses multi-layer neural networks",
113
+ "Natural language processing helps computers understand language",
114
+ "Machine learning enables computers to learn from data",
115
+ "Large language models excel at text generation tasks",
116
+ "The Transformer architecture revolutionized NLP",
117
+ "Attention mechanisms are core components of modern deep learning",
118
+ "Pre-trained models learn language knowledge from large corpora",
119
+ "Fine-tuning adapts models to specific downstream tasks",
120
+ "Open-source models promote the democratization of AI",
121
+ ]
122
+
123
+ with open(output_path, 'w', encoding='utf-8') as f:
124
+ for _ in range(num_lines):
125
+ if random.random() > 0.5:
126
+ f.write(random.choice(samples_zh) + "\n")
127
+ else:
128
+ f.write(random.choice(samples_en) + "\n")
129
+
130
+ print(f" Generated {num_lines} lines")
131
+
132
+
133
+ def main():
134
+ parser = argparse.ArgumentParser(description="Train Fusion SentencePiece tokenizer")
135
+ parser.add_argument("--input", type=str, default="data/tokenizer_train.txt",
136
+ help="Path to training data (one sentence per line)")
137
+ parser.add_argument("--vocab_size", type=int, default=100000,
138
+ help="Vocabulary size (default: 100000)")
139
+ parser.add_argument("--output", type=str, default="tokenizers/",
140
+ help="Output directory")
141
+ parser.add_argument("--model_type", type=str, default="unigram",
142
+ choices=["unigram", "bpe"], help="Model type")
143
+ parser.add_argument("--create_sample_data", action="store_true",
144
+ help="Create sample training data for testing")
145
+ args = parser.parse_args()
146
+
147
+ if args.create_sample_data:
148
+ create_sample_training_data(args.input)
149
+ return
150
+
151
+ train_tokenizer(args.input, args.vocab_size, args.output, args.model_type)
152
+
153
+
154
+ if __name__ == "__main__":
155
+ main()
test_train_loop.py DELETED
@@ -1,38 +0,0 @@
1
- """Small training loop to verify loss decreases"""
2
- import sys
3
- sys.path.insert(0, ".")
4
- import torch
5
- from models.fusion_model import FusionModel, FusionConfig
6
-
7
- config = FusionConfig(
8
- vocab_size=10000,
9
- hidden_size=256,
10
- num_hidden_layers=2,
11
- num_attention_heads=4,
12
- intermediate_size=512,
13
- block_size=64,
14
- latent_dim=16,
15
- sbla_mode="pure_sbla",
16
- max_position_embeddings=256,
17
- )
18
-
19
- model = FusionModel(config)
20
- model.train()
21
-
22
- optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
23
- batch_size, seq_len = 4, 32
24
-
25
- print("[DEBUG] Small training loop (5 steps)...")
26
- for step in range(5):
27
- input_ids = torch.randint(0, 10000, (batch_size, seq_len))
28
- attention_mask = torch.ones(batch_size, seq_len)
29
-
30
- optimizer.zero_grad()
31
- outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=input_ids)
32
- loss = outputs["loss"]
33
- loss.backward()
34
- optimizer.step()
35
-
36
- print(f"Step {step}: loss = {loss.item():.4f}")
37
-
38
- print("\nTraining loop successful - loss is decreasing!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_training.py DELETED
@@ -1,38 +0,0 @@
1
- """Test training"""
2
- import sys
3
- sys.path.insert(0, ".")
4
- import torch
5
- from models.fusion_model import FusionModel, FusionConfig
6
-
7
- config = FusionConfig(
8
- vocab_size=10000,
9
- hidden_size=256,
10
- num_hidden_layers=2,
11
- num_attention_heads=4,
12
- intermediate_size=512,
13
- block_size=64,
14
- latent_dim=16,
15
- sbla_mode="pure_sbla",
16
- max_position_embeddings=256,
17
- )
18
-
19
- model = FusionModel(config)
20
- model.train()
21
-
22
- batch_size, seq_len = 2, 32
23
- input_ids = torch.randint(0, 10000, (batch_size, seq_len))
24
- attention_mask = torch.ones(batch_size, seq_len)
25
-
26
- outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=input_ids)
27
- print(f"Loss: {outputs['loss'].item():.4f}")
28
-
29
- loss = outputs["loss"]
30
- loss.backward()
31
- print(f"Gradients exist: {model.embeddings.weight.grad is not None}")
32
-
33
- optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
34
- optimizer.zero_grad()
35
- outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=input_ids)
36
- outputs["loss"].backward()
37
- optimizer.step()
38
- print("Optimizer step successful!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/debug_attn.py DELETED
@@ -1,74 +0,0 @@
1
- """Debug attention step by step"""
2
- import sys
3
- sys.path.insert(0, ".")
4
- import torch
5
- import torch.nn.functional as F
6
- import math
7
-
8
- print("[DEBUG] Step-by-step attention debugging...")
9
-
10
- from models.fusion_model import FusionConfig
11
-
12
- config = FusionConfig(
13
- vocab_size=10000,
14
- hidden_size=256,
15
- num_hidden_layers=2,
16
- num_attention_heads=4,
17
- intermediate_size=512,
18
- block_size=64,
19
- latent_dim=16,
20
- sbla_mode="pure_sbla",
21
- max_position_embeddings=256,
22
- )
23
-
24
- # Manual attention computation
25
- batch_size, seq_len = 2, 32
26
- hidden_states = torch.randn(batch_size, seq_len, 256)
27
- device = hidden_states.device
28
-
29
- # Q/K/V
30
- q_proj = torch.nn.Linear(256, 256, bias=False)
31
- k_proj = torch.nn.Linear(256, 256, bias=False)
32
- v_proj = torch.nn.Linear(256, 256, bias=False)
33
-
34
- Q = q_proj(hidden_states).view(batch_size, seq_len, 4, 64).transpose(1, 2)
35
- K = k_proj(hidden_states).view(batch_size, seq_len, 4, 64).transpose(1, 2)
36
- V = v_proj(hidden_states).view(batch_size, seq_len, 4, 64).transpose(1, 2)
37
-
38
- print(f"Q shape: {Q.shape}, has_nan: {torch.isnan(Q).any()}")
39
- print(f"K shape: {K.shape}, has_nan: {torch.isnan(K).any()}")
40
- print(f"V shape: {V.shape}, has_nan: {torch.isnan(V).any()}")
41
-
42
- # Compute attention scores
43
- attn_scores = torch.matmul(Q, K.transpose(-1, -2)) / math.sqrt(64)
44
- print(f"Attn scores: min={attn_scores.min():.4f}, max={attn_scores.max():.4f}, has_nan: {torch.isnan(attn_scores).any()}")
45
-
46
- # Check for -inf in scores
47
- print(f"Scores has -inf: {torch.isinf(attn_scores).any()}")
48
- print(f"Scores has inf: {torch.isinf(attn_scores).any()}")
49
-
50
- # Check causal mask
51
- causal_mask = torch.triu(torch.ones(seq_len, seq_len, device=device, dtype=torch.bool), diagonal=1)
52
- causal_mask = causal_mask.float().masked_fill(causal_mask, float('-inf'))
53
- print(f"Causal mask: min={causal_mask.min():.4f}, max={causal_mask.max():.4f}")
54
-
55
- # Add causal mask
56
- attn_scores_masked = attn_scores + causal_mask
57
- print(f"Scores after mask: min={attn_scores_masked.min():.4f}, has_nan: {torch.isnan(attn_scores_masked).any()}")
58
-
59
- # Softmax
60
- attn_probs = F.softmax(attn_scores_masked, dim=-1)
61
- print(f"Attn probs: min={attn_probs.min():.4f}, max={attn_probs.max():.4f}, has_nan: {torch.isnan(attn_probs).any()}")
62
-
63
- # Context
64
- context = torch.matmul(attn_probs, V)
65
- print(f"Context: min={context.min():.4f}, max={context.max():.4f}, has_nan: {torch.isnan(context).any()}")
66
-
67
- # Reshape
68
- context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, 256)
69
- print(f"Context reshaped: min={context.min():.4f}, max={context.max():.4f}, has_nan: {torch.isnan(context).any()}")
70
-
71
- # Output projection
72
- out_proj = torch.nn.Linear(256, 256, bias=False)
73
- output = out_proj(context)
74
- print(f"Output: min={output.min():.4f}, max={output.max():.4f}, has_nan: {torch.isnan(output).any()}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/debug_layer.py DELETED
@@ -1,63 +0,0 @@
1
- """Debug layer by layer"""
2
- import sys
3
- sys.path.insert(0, ".")
4
- import torch
5
-
6
- print("[DEBUG] Testing layer by layer...")
7
-
8
- from models.fusion_model import FusionModel, FusionConfig, FusionAttention
9
-
10
- config = FusionConfig(
11
- vocab_size=10000,
12
- hidden_size=256,
13
- num_hidden_layers=2,
14
- num_attention_heads=4,
15
- intermediate_size=512,
16
- block_size=64,
17
- latent_dim=16,
18
- sbla_mode="pure_sbla",
19
- max_position_embeddings=256,
20
- )
21
-
22
- model = FusionModel(config)
23
- model.eval()
24
-
25
- # Get the attention layer
26
- attn = model.layers[0].attention
27
-
28
- batch_size, seq_len = 2, 32
29
- hidden_states = torch.randn(batch_size, seq_len, 256)
30
-
31
- # Test attention
32
- attn_out = attn.forward(hidden_states)
33
- print(f"Attention output: min={attn_out.min():.4f}, max={attn_out.max():.4f}, has_nan: {torch.isnan(attn_out).any()}")
34
-
35
- # Test RMSNorm
36
- norm = model.layers[0].input_layernorm
37
- norm_out = norm.forward(hidden_states)
38
- print(f"RMSNorm output: min={norm_out.min():.4f}, max={norm_out.max():.4f}, has_nan: {torch.isnan(norm_out).any()}")
39
-
40
- # Test FFN
41
- ffn = model.layers[0]
42
- residual = hidden_states
43
- norm1_out = ffn.input_layernorm(hidden_states)
44
- attn_out = ffn.attention(norm1_out)
45
- after_attn = residual + attn_out
46
- print(f"After attention residual: min={after_attn.min():.4f}, max={after_attn.max():.4f}, has_nan: {torch.isnan(after_attn).any()}")
47
-
48
- norm2_out = ffn.post_attention_layernorm(after_attn)
49
- print(f"Post-attention norm: min={norm2_out.min():.4f}, max={norm2_out.max():.4f}, has_nan: {torch.isnan(norm2_out).any()}")
50
-
51
- gate = torch.nn.functional.silu(ffn.gate_proj(norm2_out))
52
- up = ffn.up_proj(norm2_out)
53
- print(f"Gate: min={gate.min():.4f}, max={gate.max():.4f}, has_nan: {torch.isnan(gate).any()}")
54
- print(f"Up: min={up.min():.4f}, max={up.max():.4f}, has_nan: {torch.isnan(up).any()}")
55
-
56
- gate_up = gate * up
57
- print(f"Gate*Up: min={gate_up.min():.4f}, max={gate_up.max():.4f}, has_nan: {torch.isnan(gate_up).any()}")
58
-
59
- ffn_out = ffn.down_proj(gate_up)
60
- print(f"FFN output: min={ffn_out.min():.4f}, max={ffn_out.max():.4f}, has_nan: {torch.isnan(ffn_out).any()}")
61
-
62
- final = after_attn + ffn_out
63
- print(f"Final layer output: min={final.min():.4f}, max={final.max():.4f}, has_nan: {torch.isnan(final).any()}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/debug_lm.py DELETED
@@ -1,54 +0,0 @@
1
- """Debug norm and lm_head"""
2
- import sys
3
- sys.path.insert(0, ".")
4
- import torch
5
-
6
- print("[DEBUG] Testing norm and lm_head...")
7
-
8
- from models.fusion_model import FusionModel, FusionConfig
9
-
10
- config = FusionConfig(
11
- vocab_size=10000,
12
- hidden_size=256,
13
- num_hidden_layers=2,
14
- num_attention_heads=4,
15
- intermediate_size=512,
16
- block_size=64,
17
- latent_dim=16,
18
- sbla_mode="pure_sbla",
19
- max_position_embeddings=256,
20
- )
21
-
22
- model = FusionModel(config)
23
- model.eval()
24
-
25
- # Simulate hidden states after all layers
26
- batch_size, seq_len = 2, 32
27
- hidden_states = torch.randn(batch_size, seq_len, 256)
28
-
29
- # Final norm
30
- norm_out = model.norm(hidden_states)
31
- print(f"Final norm output: min={norm_out.min():.4f}, max={norm_out.max():.4f}, has_nan: {torch.isnan(norm_out).any()}")
32
-
33
- # LM head
34
- logits = model.lm_head(norm_out)
35
- print(f"Logits: min={logits.min():.4f}, max={logits.max():.4f}, has_nan: {torch.isnan(logits).any()}")
36
-
37
- # Now test with actual layers
38
- input_ids = torch.randint(0, 10000, (batch_size, seq_len))
39
- embed_out = model.embeddings(input_ids)
40
- print(f"Embeddings: min={embed_out.min():.4f}, max={embed_out.max():.4f}, has_nan: {torch.isnan(embed_out).any()}")
41
-
42
- # Forward through layers
43
- hs = model.dropout(embed_out)
44
- print(f"After dropout: min={hs.min():.4f}, max={hs.max():.4f}, has_nan: {torch.isnan(hs).any()}")
45
-
46
- for i, layer in enumerate(model.layers):
47
- hs, _ = layer(hs)
48
- print(f"Layer {i} output: min={hs.min():.4f}, max={hs.max():.4f}, has_nan: {torch.isnan(hs).any()}")
49
-
50
- hs = model.norm(hs)
51
- print(f"After final norm: min={hs.min():.4f}, max={hs.max():.4f}, has_nan: {torch.isnan(hs).any()}")
52
-
53
- logits = model.lm_head(hs)
54
- print(f"Final logits: min={logits.min():.4f}, max={logits.max():.4f}, has_nan: {torch.isnan(logits).any()}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/debug_loss.py DELETED
@@ -1,63 +0,0 @@
1
- """Debug script for NaN loss"""
2
- import sys
3
- sys.path.insert(0, ".")
4
- import torch
5
-
6
- print("[DEBUG] Testing Fusion Model loss calculation...")
7
-
8
- # Import directly
9
- from models.fusion_model import FusionModel, FusionConfig
10
-
11
- config = FusionConfig(
12
- vocab_size=10000,
13
- hidden_size=256,
14
- num_hidden_layers=2,
15
- num_attention_heads=4,
16
- intermediate_size=512,
17
- block_size=64,
18
- latent_dim=16,
19
- sbla_mode="pure_sbla",
20
- max_position_embeddings=256,
21
- )
22
-
23
- model = FusionModel(config)
24
- model.eval()
25
-
26
- # Small test
27
- batch_size, seq_len = 2, 32
28
- input_ids = torch.randint(0, config.vocab_size, (batch_size, seq_len))
29
- attention_mask = torch.ones(batch_size, seq_len)
30
-
31
- with torch.no_grad():
32
- outputs = model(input_ids=input_ids, attention_mask=attention_mask, labels=input_ids, return_dict=True)
33
-
34
- logits = outputs["logits"]
35
- print(f"Logits stats: min={logits.min():.4f}, max={logits.max():.4f}, mean={logits.mean():.4f}")
36
- print(f"Logits has inf: {torch.isinf(logits).any()}")
37
- print(f"Logits has nan: {torch.isnan(logits).any()}")
38
-
39
- # Check logits at last position
40
- last_logits = logits[:, -1, :]
41
- print(f"Last token logits: min={last_logits.min():.4f}, max={last_logits.max():.4f}")
42
-
43
- # Try computing loss manually
44
- shift_logits = logits[..., :-1, :].contiguous()
45
- shift_labels = input_ids[..., 1:].contiguous()
46
- print(f"Shift logits stats: min={shift_logits.min():.4f}, max={shift_logits.max():.4f}")
47
- print(f"Shift logits has inf: {torch.isinf(shift_logits).any()}")
48
- print(f"Shift labels range: {shift_labels.min():.0f} - {shift_labels.max():.0f}")
49
-
50
- loss_fct = torch.nn.CrossEntropyLoss()
51
- try:
52
- loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
53
- print(f"Manual loss: {loss.item():.4f}")
54
- except Exception as e:
55
- print(f"Loss computation error: {e}")
56
-
57
- # Check for any NaN in embeddings
58
- embeds = model.embeddings(input_ids)
59
- print(f"Embeddings: min={embeds.min():.4f}, max={embeds.max():.4f}, has_nan={torch.isnan(embeds).any()}")
60
-
61
- # Test a simple forward without labels
62
- outputs_no_labels = model(input_ids=input_ids, attention_mask=attention_mask, return_dict=True)
63
- print(f"Forward no labels logits: min={outputs_no_labels['logits'].min():.4f}, max={outputs_no_labels['logits'].max():.4f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/debug_mask.py DELETED
@@ -1,68 +0,0 @@
1
- """Debug attention_mask handling"""
2
- import sys
3
- sys.path.insert(0, ".")
4
- import torch
5
-
6
- print("[DEBUG] Testing attention_mask handling...")
7
-
8
- from models.fusion_model import FusionModel, FusionConfig
9
-
10
- config = FusionConfig(
11
- vocab_size=10000,
12
- hidden_size=256,
13
- num_hidden_layers=2,
14
- num_attention_heads=4,
15
- intermediate_size=512,
16
- block_size=64,
17
- latent_dim=16,
18
- sbla_mode="pure_sbla",
19
- max_position_embeddings=256,
20
- )
21
-
22
- model = FusionModel(config)
23
- model.eval()
24
-
25
- batch_size, seq_len = 2, 32
26
- input_ids = torch.randint(0, 10000, (batch_size, seq_len))
27
-
28
- # Case 1: No attention_mask
29
- hs = model.embeddings(input_ids)
30
- hs = model.dropout(hs)
31
- for i, layer in enumerate(model.layers):
32
- hs, _ = layer(hs)
33
- hs = model.norm(hs)
34
- logits1 = model.lm_head(hs)
35
- print(f"No mask logits: min={logits1.min():.4f}, max={logits1.max():.4f}, has_nan: {torch.isnan(logits1).any()}")
36
-
37
- # Case 2: attention_mask as 2D
38
- mask_2d = torch.ones(batch_size, seq_len)
39
- hs = model.embeddings(input_ids)
40
- hs = model.dropout(hs)
41
- for i, layer in enumerate(model.layers):
42
- hs, _ = layer(hs, attention_mask=mask_2d)
43
- hs = model.norm(hs)
44
- logits2 = model.lm_head(hs)
45
- print(f"2D mask logits: min={logits2.min():.4f}, max={logits2.max():.4f}, has_nan: {torch.isnan(logits2).any()}")
46
-
47
- # Case 3: What does _build_causal_mask return for seq_len=32?
48
- device = hs.device
49
- causal_mask = torch.triu(torch.ones(seq_len, seq_len, device=device, dtype=torch.bool), diagonal=1)
50
- causal_mask = causal_mask.float().masked_fill(causal_mask, float('-inf'))
51
- print(f"Causal mask shape: {causal_mask.shape}, has -inf: {(causal_mask == float('-inf')).any()}")
52
- print(f"Causal mask diag: {torch.diag(causal_mask)[:5]}")
53
-
54
- # Case 4: What is combined_mask after adding window_mask?
55
- window_mask = torch.zeros(seq_len, seq_len, device=device)
56
- combined_mask = (causal_mask + window_mask).unsqueeze(0).unsqueeze(0)
57
- print(f"Combined mask: shape={combined_mask.shape}, min={combined_mask.min()}, max={combined_mask.max()}")
58
-
59
- # Check if softmax produces NaN when there are rows full of -inf
60
- attn_scores_test = torch.randn(2, 4, 32, 32)
61
- attn_scores_test = attn_scores_test + combined_mask
62
- print(f"Attn scores after mask: has -inf rows: {(attn_scores_test == float('-inf')).any(dim=-1).all()}")
63
- attn_probs = torch.softmax(attn_scores_test, dim=-1)
64
- print(f"Attn probs: has nan: {torch.isnan(attn_probs).any()}")
65
-
66
- # Case 5: Forward pass with full model
67
- outputs = model(input_ids=input_ids, attention_mask=mask_2d, return_dict=True)
68
- print(f"Full model logits: min={outputs['logits'].min():.4f}, max={outputs['logits'].max():.4f}, has_nan: {torch.isnan(outputs['logits']).any()}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_sblla_integration.py → tests/test_sbla_integration.py RENAMED
File without changes
tests/test_sblla_integration.py DELETED
@@ -1,59 +0,0 @@
1
- """
2
- 测试 SBLA 注意力集成(无 emoji 版本)
3
- """
4
- import sys
5
- sys.path.insert(0, '.')
6
-
7
- from models.fusion_mini import FusionMini, FusionMiniConfig
8
- import torch
9
-
10
- print("测试 SBLA 注意力集成...")
11
- print()
12
-
13
- # 1. 创建配置
14
- print("[1] 创建配置...")
15
- config = FusionMiniConfig(
16
- vocab_size=1000,
17
- hidden_size=128,
18
- num_hidden_layers=2,
19
- num_attention_heads=4,
20
- )
21
- print(" 配置创建成功")
22
- print(f" 隐层大小:{config.hidden_size}")
23
- print(f" 层数:{config.num_hidden_layers}")
24
- print()
25
-
26
- # 2. 创建模型
27
- print("[2] 创建模型(包含 SBLA 注意力)...")
28
- model = FusionMini(config)
29
- print(" 模型创建成功")
30
- param_count = sum(p.numel() for p in model.parameters()) / 1e3
31
- print(f" 参数量:{param_count:.1f}K")
32
- print()
33
-
34
- # 3. 测试前向传播
35
- print("[3] 测试前向传播...")
36
- input_ids = torch.randint(0, 1000, (2, 64))
37
- print(f" 输入形状:{input_ids.shape}")
38
-
39
- outputs = model.forward(input_ids=input_ids, labels=input_ids)
40
- loss_value = outputs["loss"].item()
41
- print(f" 前向传播成功")
42
- print(f" Loss:{loss_value:.4f}")
43
- print()
44
-
45
- # 4. 验证 SBLA 是否使用
46
- print("[4] 验证 SBLA 注意力...")
47
- has_sblla = any("SBLAttention" in str(module) for module in model.modules())
48
- if has_sblla:
49
- print(" SBLA 注意力已集成到模型中")
50
- else:
51
- print(" 未检测到 SBLA 注意力(可能使用了标准注意力)")
52
- print()
53
-
54
- print("所有测试通过!")
55
- print()
56
- print("下一步:")
57
- print(" 1. 重新训练模型(使用 SBLA 注意力)")
58
- print(" 2. 对比标准注意力和 SBLA 的性能")
59
- print(" 3. 推送代码到 GitHub")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
train/full_finetune.py CHANGED
@@ -25,9 +25,9 @@ import torch
25
  import torch.nn as nn
26
  import deepspeed
27
  from transformers import (
28
- AutoTokenizer,
29
  get_linear_schedule_with_warmup,
30
  )
 
31
  from torch.utils.data import Dataset, DataLoader
32
  import json
33
  import os
@@ -158,14 +158,16 @@ def create_local_model(
158
  return model, config
159
 
160
 
161
- def create_tokenizer(vocab_size: int = 32000):
162
  """
163
- 创建 tokenizer
 
 
 
164
  """
165
- logger.info(f"[create_tokenizer] 创建 tokenizer(vocab_size={vocab_size})")
166
- tokenizer = AutoTokenizer.from_pretrained("gpt2")
167
- tokenizer.pad_token = tokenizer.eos_token
168
- tokenizer.add_special_tokens({'pad_token': '[PAD]'})
169
  return tokenizer
170
 
171
 
 
25
  import torch.nn as nn
26
  import deepspeed
27
  from transformers import (
 
28
  get_linear_schedule_with_warmup,
29
  )
30
+ from models.tokenizer import get_tokenizer, get_effective_vocab_size
31
  from torch.utils.data import Dataset, DataLoader
32
  import json
33
  import os
 
158
  return model, config
159
 
160
 
161
+ def create_tokenizer(tokenizer_type: str = "gpt2", vocab_size: int = 32000):
162
  """
163
+ Create tokenizer using the unified tokenizer module.
164
+
165
+ Note: Currently uses GPT2 as placeholder until SentencePiece model is trained.
166
+ The model config vocab_size will be auto-adjusted to match.
167
  """
168
+ effective_vocab = get_effective_vocab_size(tokenizer_type, vocab_size)
169
+ logger.info(f"[create_tokenizer] Creating tokenizer: type={tokenizer_type}, effective_vocab={effective_vocab}")
170
+ tokenizer = get_tokenizer(tokenizer_type, vocab_size=vocab_size)
 
171
  return tokenizer
172
 
173