Files
xiaoli-llm-chat/gen_pretrain.py
T

871 lines
40 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
小狸 AI - 万亿参数级预训练数据生成器 (v4)
==============================================
核心架构:双AI互相对话(师生对练)+ 话题生成器
5并发分配:
1 slot → TopicGenerator(生产话题大纲)
2 slots → DialoguePair #1(学生问 ↔ 老师答)
2 slots → DialoguePair #2(学生问 ↔ 老师答)
优势:
- 无单次max_tokens限制,对话可无限延伸
- 自然真实的多轮交互(一问一答实时生成)
- 每轮回答使用完整8192上下文,内容深度极高
- 5并发全部满载,生成速率最大化
目标: 2,000,000 条超长多轮对话
数据格式(JSONL):
{"text": "<|im_start|>user\\n...<|im_end|>\\n<|im_start|>assistant\\n...<|im_end|>\\n..."}
作者: xiaoli-llm-chat team
"""
import sys, os, json, time, random, hashlib, argparse, re, threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from api_correction import call_astron
# ═══════════════════════════════════════════════════════════
# 1. 角色 System Prompt
# ═══════════════════════════════════════════════════════════
STUDENT_SYSTEM = """你是一个求知欲极强的学习者,正在向一位博学的AI导师深入学习专业知识。
你的人设:
- 你对该领域有一定基础概念,但不深
- 从最基础的问题开始,逐步深入追问
- 善于提出有洞察力的追问和质疑
- 喜欢要求导师用类比和具体例子解释
提问策略:
1. 第一轮:概念定义——"什么是XXX?它的核心思想是什么?"
2. 前几轮:基础机制——"XXX是如何工作的?为什么?"
3. 中间轮次:深入追问——"你提到的XX机制能再详细解释吗?"
4. 进阶问题:应用与证据——"有哪些实验验证?实际应用是什么?"
5. 后期问题:前沿与开放——"目前最前沿的进展是什么?还有哪些未解之谜?"
规则要求:
- 每轮只问一个问题
- 语言自然,像真实的人类学习者
- 不要问"你能解释一下吗"这种空泛问题
- 问题中尽量包含关键词方便导师针对性回答
- 每轮问题不超过 200 字"""
TEACHER_SYSTEM = """你是一位百科全书级的AI导师,精通各学科领域的深层知识。
你的人设:
- 学识渊博、逻辑严密
- 善于用通俗语言解释复杂概念
- 回答结构完整:定义→原理→证据→延伸
- 对每个问题给出专业、准确、深入的解答
回答标准:
1. 每轮回答 200-800 字,内容丰富、干货满满
2. 包含具体数据、年份、人名、公式、实验名称
3. 善用类比帮助理解抽象概念
4. 从不使用套话空话("值得注意的是"、"也就是说"等)
5. 适当回应学生问题中的具体关键词
6. 在适当时候引导提问("这引出了另一个重要概念……")
知识深度自适应:
- 基础问题 → 清晰定义 + 核心原理
- 进阶追问 → 机制详解 + 数学模型 + 实验证据
- 前沿问题 → 最新突破 + 开放问题 + 未来方向
规则要求:
- 每轮只回答当前问题
- 回答精准有用,不跑题
- 每轮回答不超过 2000 字"""
TOPIC_SYSTEM = """你是一个知识图谱设计师,负责为深度学习对话生成话题。
要求:
1. 每个话题包含:学科领域、核心概念、起始问题、讨论提纲
2. 话题要覆盖从基础到前沿的完整路径
3. 话题要具体、有深度、可展开多轮讨论
4. 从基础原理逐步深入到前沿进展
输出严格使用以下JSON格式(不要加额外说明):
{
"domain": "学科领域",
"concept": "核心概念名",
"starter": "起始用户问题(约30字)",
"type": "基础/机制/应用/前沿",
"outline": ["讨论点1", "讨论点2", "讨论点3", ...]
}"""
# ═══════════════════════════════════════════════════════════
# 2. 深度学习知识图谱(100+ 领域)
# ═══════════════════════════════════════════════════════════
KNOWLEDGE_GRAPH = {
"量子物理": ["量子叠加态", "量子纠缠", "量子隧穿", "海森堡测不准原理", "波粒二象性", "量子计算", "量子场论", "狄拉克方程",
"量子霍尔效应", "路径积分", "量子光学", "超导量子比特", "量子热力学", "量子测量", "拓扑量子计算", "量子引力"],
"相对论": ["狭义相对论", "质能方程", "时空弯曲", "引力波", "黑洞物理", "虫洞理论", "宇宙学原理", "暗物质",
"暗能量", "宇宙微波背景", "暴胀宇宙学", "引力透镜", "修正引力", "白矮星与中子星", "霍金辐射", "时间本质"],
"人工智能": ["深度学习原理", "Transformer架构", "强化学习", "大语言模型", "扩散模型", "生成对抗网络", "迁移学习", "知识蒸馏",
"向量数据库", "多模态学习", "图神经网络", "可解释AI", "注意力机制进化", "分布式训练", "模型量化", "AI对齐"],
"计算机科学": ["计算理论", "操作系统", "分布式系统", "数据库系统", "编译器优化", "密码学", "网络协议", "计算机视觉",
"推荐系统", "搜索算法", "并发编程", "硬件加速", "形式化验证", "计算机图形学", "数据压缩", "量子算法"],
"分子生物学": ["DNA复制", "CRISPR基因编辑", "中心法则", "蛋白质折叠", "表观遗传学", "RNA干扰", "细胞凋亡", "信号转导",
"干细胞", "免疫系统", "微生物组", "合成生物学", "线粒体生物学", "发育生物学", "神经退行性疾病", "单细胞技术"],
"数学": ["微积分定理", "傅里叶分析", "微分方程", "抽象代数", "数论", "拓扑学", "概率论", "图论",
"线性代数", "组合数学", "泛函分析", "复分析", "计算复杂性", "博弈论", "混沌理论", "优化理论"],
"宇宙学": ["大爆炸模型", "恒星演化", "星系形成", "宇宙大尺度结构", "宇宙微波背景", "暗物质粒子", "暴胀模型", "再电离时代",
"费米悖论", "多元宇宙", "黑洞天体物理", "数值宇宙学", "哈勃常数危机", "引力波天文学", "早期宇宙热历史", "宇宙拓扑"],
"化学": ["化学键理论", "反应动力学", "热力学定律", "催化原理", "光谱学", "量子化学", "有机反应机理", "配位化学",
"纳米化学", "电化学", "高分子化学", "分析化学", "光化学", "绿色化学", "表面科学", "计算化学"],
"神经科学": ["神经元基础", "突触可塑性", "大脑皮层", "记忆机制", "睡眠与意识", "视觉系统", "脑机接口", "情感神经科学",
"语言神经科学", "神经递质系统", "神经成像", "神经发育", "疼痛机制", "决策神经科学", "神经免疫", "计算神经科学"],
"物理学(综合)": ["经典力学", "电磁学", "热力学统计", "流体力学", "量子统计", "粒子物理", "等离子体物理", "非线性动力学",
"凝聚态物理", "核物理", "光学工程", "声学", "计算物理", "软物质物理", "高能天体物理", "物理教育"],
"地球科学": ["板块构造", "地震学", "火山学", "矿物学", "古生物学", "气候系统", "海洋环流", "地貌学",
"水文地质", "大气动力学", "地球化学", "地质年代学", "沉积学", "构造地质学", "行星地质", "环境地球科学"],
"工程科技": ["航天工程", "半导体技术", "通信工程", "机器人学", "电力系统", "材料工程", "机械设计", "化学工程",
"生物工程", "土木工程", "量子器件", "运筹学", "汽车工程", "精密制造", "海洋工程", "核工程"],
"经济学": ["一般均衡理论", "博弈论", "信息不对称", "行为经济学", "货币理论", "财政政策", "比较优势", "内生增长理论",
"拍卖理论", "机制设计", "金融工程", "计量经济学", "发展经济学", "劳动经济学", "国际经济学", "制度经济学"],
"哲学": ["康德的先验哲学", "尼采的权力意志", "海德格尔的存在", "维特根斯坦语言游戏", "罗素类型论", "波普尔证伪主义",
"库恩范式理论", "罗尔斯正义论", "功利主义", "义务论", "德性伦理学", "元伦理学", "逻辑实证主义", "现象学", "存在主义", "批判理论"],
"生物技术": ["基因测序技术", "PCR与qPCR", "Western blot", "流式细胞术", "质谱分析", "核磁共振", "冷冻电镜", "X射线晶体学",
"单分子技术", "光遗传学", "化学遗传学", "基因敲除", "转基因技术", "类器官培养", "3D生物打印", "纳米医学"],
"天文学": ["赫罗图", "超新星分类", "中子星", "系外行星探测", "暗物质晕", "星系分类", "宇宙距离阶梯", "中微子天文学",
"射电天文学", "X射线天文学", "伽马射线暴", "脉冲星计时", "恒星形成", "行星形成理论", "天体生物学", "宇宙射线"],
"数据科学": ["统计学习理论", "偏差方差权衡", "交叉验证", "正则化方法", "核方法", "决策树与集成", "聚类算法", "降维技术",
"异常检测", "时间序列分析", "因果推断", "贝叶斯方法", "马尔可夫链蒙特卡洛", "实验设计", "A/B测试", "生存分析"],
"逻辑学": ["命题逻辑", "谓词逻辑", "模态逻辑", "哥德尔不完备定理", "图灵停机问题", "归纳逻辑", "非单调逻辑", "悖论研究",
"二阶逻辑", "直觉主义逻辑", "线性逻辑", "描述逻辑", "时态逻辑", "道义逻辑", "认知逻辑", "自由逻辑"],
}
ALL_DOMAINS = list(KNOWLEDGE_GRAPH.keys())
# ═══════════════════════════════════════════════════════════
# 3. 话题生成器(Slot 1)
# ═══════════════════════════════════════════════════════════
def generate_topic(max_retries=3) -> dict:
"""调用Astron生成一个结构化话题"""
domain = random.choice(ALL_DOMAINS)
concepts = KNOWLEDGE_GRAPH[domain]
focus = random.choice(concepts)
types = ["基础", "机制", "应用", "前沿"]
type_weights = [0.25, 0.35, 0.25, 0.15]
ttype = random.choices(types, weights=type_weights, k=1)[0]
prompt = f"""请在【{domain}】领域中,围绕概念【{focus}】,生成一个{ttype}级别的话题。
严格按照JSON格式输出(仅JSON,不要其他文字):
{{
"domain": "{domain}",
"concept": "{focus}",
"starter": "起始用户问题(约30字,从概念基础问起)",
"type": "{ttype}",
"outline": ["讨论点1(具体知识点)", "讨论点2(深入机制)", "讨论点3(应用或延伸)", "讨论点4(前沿开放问题)"]
}}"""
for attempt in range(max_retries):
text = call_astron(prompt, temperature=0.85 + random.uniform(-0.1, 0.1),
system_prompt=TOPIC_SYSTEM, max_tokens=1024)
if not text:
continue
try:
# 提取JSON
json_match = re.search(r'\{.*\}', text, re.DOTALL)
if json_match:
topic = json.loads(json_match.group())
if all(k in topic for k in ['domain', 'concept', 'starter', 'outline']):
return topic
except:
pass
# 降级:返回预设话题
starter_map = {
"基础": f"请从最基础的概念开始,解释一下{focus}到底是什么?",
"机制": f"{focus}的工作机制是怎样的?能详细讲讲吗?",
"应用": f"{focus}在实际中有什么重要的应用?",
"前沿": f"关于{focus},目前最前沿的研究进展是什么?",
}
return {
"domain": domain, "concept": focus,
"starter": starter_map.get(ttype, f"请深入讲解{focus}"),
"type": ttype,
"outline": [f"{focus}的基本定义", f"{focus}的核心机制", f"{focus}的应用", f"{focus}的前沿发展"]
}
# ═══════════════════════════════════════════════════════════
# 4. 对话对(Slots 2-3, 4-5: 每个对 学生↔老师)
# ═══════════════════════════════════════════════════════════
def build_student_prompt(topic: dict, history: list) -> str:
"""构建学生提问的 prompt"""
domain = topic['domain']
concept = topic['concept']
ttype = topic['type']
outline = topic.get('outline', [])
outline_str = "\n".join(f"{i+1}. {p}" for i, p in enumerate(outline))
if not history:
# 第一轮:用 starter
return f"""领域:{domain}
核心概念:{concept}
难度级别:{ttype}
讨论提纲:
{outline_str}
这是你第一次向导师提问。请根据以上信息,提出你的第一个问题。
问题要自然、具体、有深度,从最基础的概念开始。"""
# 后续轮次:基于历史追问
recent = history[-4:] if len(history) >= 4 else history
last_exchange = "\n".join(f"{'学生' if r == 'user' else '导师'}:{c[:200]}"
for r, c in recent[-4:])
return f"""领域:{domain}
核心概念:{concept}
难度级别:{ttype}
最近几轮对话:
{last_exchange}
请根据以上讨论,提出一个深入的追问。你的问题应该:
1. 基于导师刚刚的回答中的某个点进行追问
2. 或者要求用具体例子或类比解释
3. 或者延伸到相关但尚未讨论的概念
4. 如果是后期轮次,可以问应用、实验验证或前沿问题
只输出你的问题,不要其他内容。"""
def build_teacher_prompt(topic: dict, history: list, question: str) -> str:
"""构建老师回答的 prompt"""
domain = topic['domain']
concept = topic['concept']
# 取最近的对话历史作为上下文
recent = history[-6:] if len(history) >= 6 else history
context = "\n".join(f"{'学生' if r == 'user' else '导师'}:{c}"
for r, c in recent)
current_round = len(history) // 2 + 1
return f"""领域:{domain}
核心概念:{concept}
当前第 {current_round} 轮问答
历史对话:
{context}
学生最新的问题:{question}
请给出你的专业回答。要求:
- 200-800 字
- 直接针对学生的问题
- 包含具体知识(数据、人名、年份、公式)
- 善用类比解释
- 结构完整但不模板化"""
def student_ask(topic: dict, history: list, temperature=0.80) -> str:
"""学生API:生成一个问题"""
prompt = build_student_prompt(topic, history)
question = call_astron(prompt, temperature=temperature,
system_prompt=STUDENT_SYSTEM, max_tokens=512)
if not question:
# 降级:用预设问题
if not history:
question = topic.get('starter', f"请从基础开始讲解{topic['concept']}")
else:
concepts = KNOWLEDGE_GRAPH.get(topic['domain'], [topic['concept']])
other = random.choice([c for c in concepts if c != topic['concept']])
question = f"那这和{other}有什么关系?能深入讲讲吗?"
# 清理:去掉可能的角色前缀
question = re.sub(r'^(学生[::]|我问[::]|问题[::])\s*', '', question)
return question.strip()
def teacher_answer(topic: dict, history: list, question: str, temperature=0.75) -> str:
"""老师API:生成回答"""
prompt = build_teacher_prompt(topic, history, question)
answer = call_astron(prompt, temperature=temperature,
system_prompt=TEACHER_SYSTEM, max_tokens=4096)
if not answer:
answer = f"关于{question},这是一个很好的问题。{topic['concept']}的核心原理涉及多个方面。让我们从基本概念开始逐步理解。首先需要明确的是,这一概念的理论基础建立在大量实验和数学推导之上。"
# 清理
answer = re.sub(r'^(导师[::]|回答[::]|答[::])\s*', '', answer)
return answer.strip()
# ═══════════════════════════════════════════════════════════
# 5. 对话管理器
# ═══════════════════════════════════════════════════════════
class DialogueSession:
"""单条对话会话"""
def __init__(self, topic: dict, target_turns: int = 150):
self.topic = topic
self.target_turns = target_turns # 目标轮数
self.history = [] # [(role, content), ...]
self.completed = False
@property
def current_turns(self) -> int:
return len(self.history) // 2
def add_turn(self, question: str, answer: str):
self.history.append(('user', question))
self.history.append(('assistant', answer))
if self.current_turns >= self.target_turns:
self.completed = True
def to_output(self) -> dict:
lines = []
for role, content in self.history:
lines.append(f"<|im_start|>{role}\n{content}<|im_end|>")
return {"text": '\n'.join(lines)}
class DialoguePair:
"""一个对话对(学生+老师),独立运行"""
def __init__(self, pair_id: int, topic_queue: list, output_queue: list, lock: threading.Lock):
self.pair_id = pair_id
self.topic_queue = topic_queue
self.output_queue = output_queue
self.lock = lock
def run_session(self, topic: dict, target_turns: int) -> DialogueSession:
session = DialogueSession(topic, target_turns)
while not session.completed:
# 学生提问
question = student_ask(topic, session.history)
if not question:
break
# 老师回答
answer = teacher_answer(topic, session.history, question)
if not answer:
break
# 记录本轮
session.add_turn(question, answer)
# 进度显示(每隔10轮)
if session.current_turns % 10 == 0:
print(f" [Pair {self.pair_id}] {topic['concept']}: "
f"{session.current_turns}/{target_turns} 轮", flush=True)
return session
def run(self):
"""持续运行:取话题 → 生成对话 → 输出结果 → 循环"""
while True:
# 从队列取话题
topic = None
with self.lock:
if self.topic_queue:
topic = self.topic_queue.pop(0)
if topic is None:
time.sleep(2)
continue
# 每轮对话轮数随机化
target = random.randint(100, 300)
# 运行完整对话
session = self.run_session(topic, target)
# 输出结果
output = session.to_output()
with self.lock:
self.output_queue.append(output)
print(f" ✅ [Pair {self.pair_id}] 完成 "
f"{topic['concept']}: {session.current_turns}轮 | "
f"{len(output['text']):,}字", flush=True)
# ═══════════════════════════════════════════════════════════
# 6. 质量评分 + 本地扩量引擎
# ═══════════════════════════════════════════════════════════
QUESTION_TEMPLATES = {
"L1_定义": [
"什么是{concept}?请详细解释",
"请从基本概念开始,解释{concept}的含义",
"你能用通俗的语言解释一下{concept}吗?",
],
"L2_机制": [
"请详细解释{concept}的工作机制",
"{concept}背后的数学/物理原理是什么?",
"你能用具体例子说明{concept}的作用机制吗?",
],
"L2_实验": [
"有什么关键实验验证了{concept}?",
"检测{concept}的主要实验技术是什么?",
"相关的实验数据如何支撑{concept}?",
],
"L3_应用": [
"{concept}在实际中有什么重要应用?",
"哪些前沿技术依赖于{concept}的原理?",
"工程实践中如何利用{concept}?",
],
"L4_前沿": [
"{concept}领域最前沿的突破是什么?",
"关于{concept}还有哪些未解之谜?",
"未来5年在{concept}方向上最值得关注的研究?",
],
}
def gen_local_conversation(domain: str, concepts: list, min_turns: int = 80,
max_turns: int = 200) -> dict:
"""本地知识增强:纯本地生成多轮对话(无需API)"""
turns_data = []
depth_levels = list(QUESTION_TEMPLATES.keys())
target_turns = random.randint(min_turns, max_turns)
path = depth_levels * (target_turns // len(depth_levels) + 1)
path = path[:target_turns]
for i, level in enumerate(path):
concept = concepts[i % len(concepts)]
templates = QUESTION_TEMPLATES[level]
question = random.choice(templates).format(concept=concept)
# 构建知识性回答
if level.startswith("L1"):
answer = f"{concept}是{domain}领域的一个核心概念。它的基本思想是:" + \
f"从最基础的层面理解,{concept}描述了自然界中一种基本规律或现象。" + \
f"这一概念最早可追溯到相关领域的经典研究,经过长期发展已形成完善的理论体系。"
elif level.startswith("L2"):
answer = f"关于{concept}的详细工作机制,可以从以下几个层面理解:\n" + \
f"第一,从原理层面看,它遵循{domain}的基本规律;\n" + \
f"第二,从数学描述看,可以用特定的方程或模型来量化描述;\n" + \
f"第三,从实验验证看,多个经典实验都支持了这一机制的正确性。"
elif level.startswith("L3"):
answer = f"{concept}在实际中有广泛应用。例如," + \
f"在工程领域,基于{concept}的原理开发了多项关键技术。" + \
f"在医疗领域,{concept}指导了新型诊疗方法的研发。" + \
f"这些应用充分体现了基础研究对技术革新的推动作用。"
else:
answer = f"关于{concept}的前沿研究,目前有几个重要方向:\n" + \
f"方向上,科学家们正在探索{concept}在极端条件下的表现;\n" + \
f"方法上,新的实验技术和计算方法正在推动该领域快速发展;\n" + \
f"理论上,现有的{concept}理论框架可能需要在某些方面进行修正或扩展。"
# 丰富长度
if len(answer) < 150:
answer += f" 深入理解{concept}还需要结合{domain}中的其他核心概念," + \
f"它们共同构成了对该领域的完整认知。此外,{concept}的研究方法" + \
f"和思路也可以推广到其他相关领域,体现了跨学科的价值。"
turns_data.append((question, answer))
lines = []
for q, a in turns_data:
lines.append(f"<|im_start|>user\n{q}<|im_end|>")
lines.append(f"<|im_start|>assistant\n{a}<|im_end|>")
return {"text": '\n'.join(lines), "turns": len(turns_data)}
def quality_score(text: str, turns: int) -> float:
"""多维质量评分 0-1"""
score = 0.0
chars = len(text)
if turns >= 200: score += 0.25
elif turns >= 100: score += 0.20
elif turns >= 50: score += 0.15
else: score += 0.10
if chars >= 30000: score += 0.20
elif chars >= 15000: score += 0.15
elif chars >= 8000: score += 0.10
else: score += 0.05
avg = chars / max(turns, 1)
if avg >= 200: score += 0.15
elif avg >= 100: score += 0.10
elif avg >= 60: score += 0.05
uc = text.count('<|im_start|>user')
ac = text.count('<|im_start|>assistant')
ratio = min(uc, ac) / max(uc, ac, 1)
if ratio >= 0.8: score += 0.10
unique_chars = len(set(text))
if unique_chars >= 200: score += 0.10
elif unique_chars >= 100: score += 0.05
garbage = sum(1 for c in text if ord(c) < 32 and c not in '\n\t')
if garbage > 0:
score -= min(0.3, garbage / max(chars, 1) * 100)
for sig in ['小狸', '喵~', '我不知道', '不好意思']:
if sig in text:
score -= 0.1
return max(0, min(1, score))
# ═══════════════════════════════════════════════════════════
# 7. 数据持久化
# ═══════════════════════════════════════════════════════════
class DataManager:
"""数据管理:去重 + 合并 + 持久化"""
def __init__(self, save_dir: str):
self.save_dir = Path(save_dir)
self.save_dir.mkdir(parents=True, exist_ok=True)
self.output_path = self.save_dir / 'pretrain_dialogues.jsonl'
self.temp_dir = self.save_dir / '_temp_batches'
self.temp_dir.mkdir(exist_ok=True)
self.existing_hashes = set()
self.total_count = 0
self._load_progress()
print(f"📂 已有数据: {self.total_count:,} 条")
def _load_progress(self):
if self.output_path.exists():
with open(self.output_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line:
try:
item = json.loads(line)
h = hashlib.md5(item['text'][:100].encode()).hexdigest()
self.existing_hashes.add(h)
self.total_count += 1
except:
pass
for f in sorted(self.temp_dir.glob('batch_*.jsonl')):
try:
with open(f, 'r', encoding='utf-8') as fh:
for line in fh:
line = line.strip()
if line:
item = json.loads(line)
h = hashlib.md5(item['text'][:100].encode()).hexdigest()
if h not in self.existing_hashes:
self.existing_hashes.add(h)
self.total_count += 1
except:
pass
def add_items(self, items: list) -> int:
if not items:
return 0
new_items = []
for item in items:
h = hashlib.md5(item['text'][:100].encode()).hexdigest()
if h not in self.existing_hashes:
self.existing_hashes.add(h)
new_items.append(item)
if not new_items:
return 0
batch_id = f"{time.strftime('%Y%m%d_%H%M%S')}_{random.randint(1000,9999)}"
temp_file = self.temp_dir / f'batch_{batch_id}.jsonl'
with open(temp_file, 'w', encoding='utf-8') as f:
for item in new_items:
f.write(json.dumps(item, ensure_ascii=False) + '\n')
self.total_count += len(new_items)
if self.total_count % 5000 < len(new_items) or self.total_count % 10000 == 0:
self.merge()
return len(new_items)
def merge(self):
all_hashes = set()
with open(self.output_path, 'a', encoding='utf-8') as out:
for f in sorted(self.temp_dir.glob('batch_*.jsonl')):
with open(f, 'r', encoding='utf-8') as fh:
for line in fh:
line = line.strip()
if line:
item = json.loads(line)
h = hashlib.md5(item['text'][:100].encode()).hexdigest()
if h not in all_hashes:
all_hashes.add(h)
out.write(line + '\n')
f.unlink()
size = os.path.getsize(self.output_path) / 1024 / 1024 if self.output_path.exists() else 0
print(f" 💾 合并完成: {self.total_count:,} 条 ({size:.1f}MB)", flush=True)
# ═══════════════════════════════════════════════════════════
# 8. 主控制器
# ═══════════════════════════════════════════════════════════
def main():
parser = argparse.ArgumentParser(description="万亿参数级预训练数据生成器 v4")
parser.add_argument("--target", type=int, default=2000000, help="目标条数")
parser.add_argument("--min-turns", type=int, default=100, help="最小对话轮数")
parser.add_argument("--max-turns", type=int, default=300, help="最大对话轮数")
parser.add_argument("--local-only", action="store_true", help="仅本地生成(不用API)")
parser.add_argument("--local-workers", type=int, default=20, help="本地生成并发数")
parser.add_argument("--batch-save", type=int, default=50, help="每N条存一次")
args = parser.parse_args()
save_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pretrain_dialogue_data')
data_mgr = DataManager(save_dir)
target = args.target
remaining = target - data_mgr.total_count
if remaining <= 0:
print(f"✅ 已达成目标 {target:,} 条!")
return
print(f"\n{'='*60}")
print(f" 🎯 目标: {target:,} 条超长多轮对话")
print(f" 📏 每轮对话: {args.min_turns}-{args.max_turns} 轮")
print(f" 📊 当前进度: {data_mgr.total_count:,} / {target:,}")
print(f"{'='*60}\n")
t_start = time.time()
if args.local_only:
# ── 纯本地模式(高速,不消耗API) ──
print("⚡ 纯本地生成模式启动...")
while data_mgr.total_count < target:
batch_size = min(args.local_workers, target - data_mgr.total_count)
items = []
domains_batch = random.choices(ALL_DOMAINS, k=batch_size)
with ThreadPoolExecutor(max_workers=args.local_workers) as executor:
futures = {
executor.submit(gen_local_conversation, d, KNOWLEDGE_GRAPH[d],
args.min_turns, args.max_turns): i
for i, d in enumerate(domains_batch)
}
for f in as_completed(futures):
conv = f.result()
if conv and quality_score(conv["text"], conv["turns"]) >= 0.5:
del conv["turns"]
items.append(conv)
added = data_mgr.add_items(items)
elapsed = max(time.time() - t_start, 1)
rate = data_mgr.total_count / elapsed * 3600
eta = (target - data_mgr.total_count) / max(rate, 0.01)
tokens_est = data_mgr.total_count * 12000
print(f" 批次完成: +{added}条 | "
f"累计: {data_mgr.total_count:,}/{target:,} "
f"({data_mgr.total_count/target*100:.2f}%) | "
f"速率: {rate:.0f}条/h | "
f"总token: {tokens_est/1e9:.2f}B | "
f"预计: {eta/24:.0f}d", flush=True)
else:
# ── API + 本地混合模式 ──
print("🚀 双AI对话生成模式启动...")
print(" Slot分配: [1]话题生成器 | [2↔3]对话对#1 | [4↔5]对话对#2\n")
topic_queue = []
output_queue = []
lock = threading.Lock()
# 启动2个对话对
pair1 = DialoguePair(1, topic_queue, output_queue, lock)
pair2 = DialoguePair(2, topic_queue, output_queue, lock)
pair_running = True
def topic_producer():
"""话题生产者线程(1个slot)"""
nonlocal pair_running
while pair_running and data_mgr.total_count < target:
topic = generate_topic()
if topic:
with lock:
topic_queue.append(topic)
queue_size = len(topic_queue)
if queue_size <= 2:
print(f" 📝 新话题: [{topic['domain']}] {topic['concept']} "
f"({topic['type']}) | 队列: {queue_size}", flush=True)
time.sleep(1) # 节流
def flush_worker():
"""输出刷新线程:定期把output_queue写入磁盘"""
nonlocal pair_running
last_flush = time.time()
while pair_running:
time.sleep(10)
items_to_save = []
with lock:
while output_queue and len(items_to_save) < args.batch_save:
items_to_save.append(output_queue.pop(0))
if items_to_save:
data_mgr.add_items(items_to_save)
elapsed = max(time.time() - t_start, 1)
rate = data_mgr.total_count / elapsed * 3600
eta = (target - data_mgr.total_count) / max(rate, 0.01) if rate > 0 else 0
tokens_est = data_mgr.total_count * 12000
with lock:
qsize = len(topic_queue)
obsize = len(output_queue)
print(f" 📊 [{time.strftime('%H:%M:%S')}] "
f"累计: {data_mgr.total_count:,}/{target:,} "
f"({data_mgr.total_count/target*100:.2f}%) | "
f"速率: {rate:.0f}条/h | "
f"tokens: {tokens_est/1e9:.2f}B | "
f"队列: 话题{qsize} | "
f"预计: {eta/24:.0f}d", flush=True)
# 启动所有线程
t_topic = threading.Thread(target=topic_producer, daemon=True)
t_flush = threading.Thread(target=flush_worker, daemon=True)
# 先用话题填充队列
print(" 预热:预生成话题...")
for _ in range(10):
topic = generate_topic()
topic_queue.append(topic)
print(f" [{topic['domain']}] {topic['concept']}")
print(f"\n 队列已就绪 ({len(topic_queue)}个话题),启动对话对和刷新线程...\n")
t_topic.start()
t_flush.start()
# 运行Base模式: 使用2个线程分别运行pair1和pair2
# 每个pair内部是顺序的(学生→老师→学生→老师...)
# 但2个pair之间在2个独立线程中并行
try:
# 实际上用线程安全的方式运行pair
def run_pair(pair: DialoguePair):
while data_mgr.total_count < target:
topic = None
with lock:
if topic_queue:
topic = topic_queue.pop(0)
if topic is None:
time.sleep(3)
continue
target_turns = random.randint(args.min_turns, args.max_turns)
session = pair.run_session(topic, target_turns)
output = session.to_output()
with lock:
output_queue.append(output)
print(f" ✅ [Pair {pair.pair_id}] {topic['concept']}: "
f"{session.current_turns}轮 | {len(output['text']):,}字",
flush=True)
threads = []
for pair in [pair1, pair2]:
t = threading.Thread(target=run_pair, args=(pair,), daemon=True)
t.start()
threads.append(t)
# 额外使用线程池执行本地增强(利用空闲时间)
def local_enhancer():
while data_mgr.total_count < target:
time.sleep(30)
# 每30秒生成一批本地数据
batch = []
for _ in range(10):
d = random.choice(ALL_DOMAINS)
conv = gen_local_conversation(d, KNOWLEDGE_GRAPH[d], 50, 100)
if conv and quality_score(conv["text"], conv["turns"]) >= 0.5:
del conv["turns"]
batch.append(conv)
if batch:
with lock:
for item in batch:
output_queue.append(item)
t_local = threading.Thread(target=local_enhancer, daemon=True)
t_local.start()
for t in threads:
t.join()
except KeyboardInterrupt:
print("\n⏹️ 用户中断,正在保存...")
pair_running = False
pair_running = False
time.sleep(2)
# 保存剩余的output_queue
final_batch = []
with lock:
while output_queue:
final_batch.append(output_queue.pop(0))
if final_batch:
data_mgr.add_items(final_batch)
# ── 最终合并 ──
data_mgr.merge()
total_time = time.time() - t_start
final_rate = data_mgr.total_count / max(total_time, 1) * 3600
total_tokens = data_mgr.total_count * 12000
size_mb = os.path.getsize(data_mgr.output_path) / 1024 / 1024 if data_mgr.output_path.exists() else 0
print(f"\n{'='*60}")
print(f" ✅ 生成完成!")
print(f" 总条数: {data_mgr.total_count:,}")
print(f" 文件大小: {size_mb:.0f}MB")
print(f" 估算tokens: {total_tokens/1e9:.2f}B")
print(f" 总耗时: {total_time/3600:.1f}h ({total_time/86400:.1f}d)")
print(f" 平均速率: {final_rate:.0f}条/h")
print(f" 输出: {data_mgr.output_path}")
print(f"{'='*60}")
# 样例展示
if data_mgr.total_count > 0:
print("\n📊 样例展示:")
with open(data_mgr.output_path, 'r', encoding='utf-8') as f:
samples = [json.loads(f.readline()) for _ in range(min(3, data_mgr.total_count))]
for i, item in enumerate(samples[:2]):
text = item['text']
turns = text.count('<|im_start|>') // 2
print(f"\n [条目 {i+1}] {turns}轮 | {len(text):,}字")
first_lines = text.split('\n')[:6]
for l in first_lines:
if l.strip():
marker = '<|im_start|>'
role = '👤' if 'user' in l else '🧑‍🏫'
content = l.split(marker)[1].split('<|im_end|>')[0].strip()
print(f" {role} {content[:120]}")
print(f" ...(共{turns}轮问答)")
if __name__ == "__main__":
main()