Instructions to use lxcxjxhx/Qwen3.5-0.8B-HOS with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use lxcxjxhx/Qwen3.5-0.8B-HOS with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="lxcxjxhx/Qwen3.5-0.8B-HOS") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] pipe(text=messages)# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("lxcxjxhx/Qwen3.5-0.8B-HOS") model = AutoModelForMultimodalLM.from_pretrained("lxcxjxhx/Qwen3.5-0.8B-HOS", device_map="auto") messages = [ { "role": "user", "content": [ {"type": "image", "url": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/p-blog/candy.JPG"}, {"type": "text", "text": "What animal is on the candy?"} ] }, ] inputs = processor.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(processor.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use lxcxjxhx/Qwen3.5-0.8B-HOS with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "lxcxjxhx/Qwen3.5-0.8B-HOS" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "lxcxjxhx/Qwen3.5-0.8B-HOS", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker
docker model run hf.co/lxcxjxhx/Qwen3.5-0.8B-HOS
- SGLang
How to use lxcxjxhx/Qwen3.5-0.8B-HOS with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "lxcxjxhx/Qwen3.5-0.8B-HOS" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "lxcxjxhx/Qwen3.5-0.8B-HOS", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "lxcxjxhx/Qwen3.5-0.8B-HOS" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "lxcxjxhx/Qwen3.5-0.8B-HOS", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe this image in one sentence." }, { "type": "image_url", "image_url": { "url": "https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg" } } ] } ] }' - Docker Model Runner
How to use lxcxjxhx/Qwen3.5-0.8B-HOS with Docker Model Runner:
docker model run hf.co/lxcxjxhx/Qwen3.5-0.8B-HOS
lxcxjxhx commited on
Commit ·
10b6631
1
Parent(s): 1c08cb6
Add inference optimization and training scripts for Qwen3.5-0.8B-HOS
Browse files- configs/train.yaml +59 -0
- datasets/dataset_info.json +38 -0
- inference/benchmark.py +396 -0
- inference/quantize.py +306 -0
- inference/sglang_serve.py +225 -0
- inference/vllm_serve.py +184 -0
- model.safetensors-00001-of-00001.safetensors +0 -3
- requirements.txt +31 -0
- run.sh +50 -0
- scripts/clean_data.py +271 -0
- scripts/prepare_data.sh +71 -0
- scripts/prepare_env.sh +50 -0
- scripts/setup_proxy.sh +25 -0
- scripts/train.sh +56 -0
- scripts/upload.sh +86 -0
configs/train.yaml
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
### Qwen3.5-0.8B-HOS QLoRA 训练配置 (针对 0.8B 小模型优化)
|
| 2 |
+
|
| 3 |
+
### 模型配置
|
| 4 |
+
model_name_or_path: lxcxjxhx/Qwen3.5-0.8B-HOS
|
| 5 |
+
trust_remote_code: true
|
| 6 |
+
|
| 7 |
+
### 训练方法
|
| 8 |
+
stage: sft
|
| 9 |
+
do_train: true
|
| 10 |
+
finetuning_type: lora
|
| 11 |
+
|
| 12 |
+
### 数据集配置
|
| 13 |
+
dataset: merged_cybersec
|
| 14 |
+
dataset_dir: datasets
|
| 15 |
+
template: qwen
|
| 16 |
+
cutoff_len: 2048 # 0.8B 模型可以使用更长的序列
|
| 17 |
+
overwrite_cache: true
|
| 18 |
+
preprocessing_num_workers: 8
|
| 19 |
+
|
| 20 |
+
### QLoRA 量化配置 (4-bit NF4)
|
| 21 |
+
quantization_bit: 4
|
| 22 |
+
quantization_method: bitsandbytes
|
| 23 |
+
bnb_4bit_quant_type: nf4
|
| 24 |
+
bnb_4bit_use_double_quant: true
|
| 25 |
+
bnb_4bit_compute_dtype: bfloat16
|
| 26 |
+
|
| 27 |
+
### LoRA 配置 (0.8B 模型优化)
|
| 28 |
+
lora_rank: 16 # 针对小模型降低 rank
|
| 29 |
+
lora_alpha: 32 # alpha = 2 * rank
|
| 30 |
+
lora_dropout: 0.05
|
| 31 |
+
lora_target: all
|
| 32 |
+
|
| 33 |
+
### 训练参数 (0.8B 模型优化,显存需求更低)
|
| 34 |
+
per_device_train_batch_size: 4 # 0.8B 模型可以使用更大的 batch_size
|
| 35 |
+
gradient_accumulation_steps: 4 # 累积 4 步,等效 batch_size = 16
|
| 36 |
+
learning_rate: 2e-5 # 指定的学习率
|
| 37 |
+
num_train_epochs: 3
|
| 38 |
+
lr_scheduler_type: cosine
|
| 39 |
+
warmup_ratio: 0.1
|
| 40 |
+
max_grad_norm: 1.0
|
| 41 |
+
logging_steps: 10
|
| 42 |
+
save_steps: 200
|
| 43 |
+
save_total_limit: 3
|
| 44 |
+
overwrite_output_dir: true
|
| 45 |
+
|
| 46 |
+
### 输出配置
|
| 47 |
+
output_dir: outputs/qwen35-0.8b-cybersec-qlora
|
| 48 |
+
|
| 49 |
+
### 精度配置
|
| 50 |
+
bf16: true
|
| 51 |
+
|
| 52 |
+
### 性能优化
|
| 53 |
+
gradient_checkpointing: true # 启用梯度检查点,节省显存
|
| 54 |
+
optim: adamw_torch_fused # 使用融合优化器
|
| 55 |
+
|
| 56 |
+
### HuggingFace Hub 配置
|
| 57 |
+
push_to_hub: true
|
| 58 |
+
hub_model_id: lxcxjxhx/Qwen3.5-0.8B-HOS
|
| 59 |
+
hub_strategy: every_save
|
datasets/dataset_info.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cybersecurity_hq": {
|
| 3 |
+
"file_name": "cybersecurity_hq.jsonl",
|
| 4 |
+
"formatting": "alpaca",
|
| 5 |
+
"columns": {
|
| 6 |
+
"prompt": "instruction",
|
| 7 |
+
"query": "input",
|
| 8 |
+
"response": "output"
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"cybersecurity_sharegpt": {
|
| 12 |
+
"file_name": "cybersecurity_sharegpt.jsonl",
|
| 13 |
+
"formatting": "alpaca",
|
| 14 |
+
"columns": {
|
| 15 |
+
"prompt": "instruction",
|
| 16 |
+
"query": "input",
|
| 17 |
+
"response": "output"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"cnvd_vulnerability": {
|
| 21 |
+
"file_name": "cnvd_vulnerability.jsonl",
|
| 22 |
+
"formatting": "alpaca",
|
| 23 |
+
"columns": {
|
| 24 |
+
"prompt": "instruction",
|
| 25 |
+
"query": "input",
|
| 26 |
+
"response": "output"
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
"merged_cybersec": {
|
| 30 |
+
"file_name": "merged_cybersec.jsonl",
|
| 31 |
+
"formatting": "alpaca",
|
| 32 |
+
"columns": {
|
| 33 |
+
"prompt": "instruction",
|
| 34 |
+
"query": "input",
|
| 35 |
+
"response": "output"
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
inference/benchmark.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Qwen3.5-0.8B-HOS 推理性能基准测试脚本
|
| 4 |
+
|
| 5 |
+
测试指标:
|
| 6 |
+
- 吞吐量 (tokens/sec)
|
| 7 |
+
- 首token延迟 (Time to First Token, TTFT)
|
| 8 |
+
- 每token生成时间 (Time Per Output Token, TPOT)
|
| 9 |
+
- 显存占用
|
| 10 |
+
- 不同batch size和sequence length下的性能
|
| 11 |
+
|
| 12 |
+
使用方法:
|
| 13 |
+
python benchmark.py
|
| 14 |
+
|
| 15 |
+
可选参数:
|
| 16 |
+
--model: 模型路径或HF仓库ID
|
| 17 |
+
--backend: 推理后端 (transformers, vllm, sglang)
|
| 18 |
+
--batch-sizes: 测试的batch size列表,如 "1,2,4,8"
|
| 19 |
+
--seq-lengths: 测试的序列长度列表,如 "128,256,512,1024"
|
| 20 |
+
--output-len: 生成的token数量
|
| 21 |
+
--output: 结果保存路径
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
import argparse
|
| 25 |
+
import json
|
| 26 |
+
import time
|
| 27 |
+
import os
|
| 28 |
+
from typing import List, Dict, Any
|
| 29 |
+
import torch
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def get_gpu_memory():
|
| 33 |
+
"""获取GPU显存使用情况"""
|
| 34 |
+
if torch.cuda.is_available():
|
| 35 |
+
allocated = torch.cuda.memory_allocated() / 1024**3
|
| 36 |
+
reserved = torch.cuda.memory_reserved() / 1024**3
|
| 37 |
+
return {"allocated_gb": allocated, "reserved_gb": reserved}
|
| 38 |
+
return {"allocated_gb": 0, "reserved_gb": 0}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def benchmark_transformers(model_path: str, batch_sizes: List[int],
|
| 42 |
+
seq_lengths: List[int], output_len: int = 128) -> List[Dict[str, Any]]:
|
| 43 |
+
"""使用HuggingFace Transformers进行基准测试"""
|
| 44 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 45 |
+
|
| 46 |
+
print(f"\n{'='*60}")
|
| 47 |
+
print(f"使用 Transformers 后端测试模型: {model_path}")
|
| 48 |
+
print(f"{'='*60}")
|
| 49 |
+
|
| 50 |
+
# 加载模型和分词器
|
| 51 |
+
print("加载模型...")
|
| 52 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 53 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 54 |
+
model_path,
|
| 55 |
+
torch_dtype=torch.float16,
|
| 56 |
+
device_map="auto",
|
| 57 |
+
trust_remote_code=True
|
| 58 |
+
)
|
| 59 |
+
model.eval()
|
| 60 |
+
|
| 61 |
+
results = []
|
| 62 |
+
|
| 63 |
+
for batch_size in batch_sizes:
|
| 64 |
+
for seq_length in seq_lengths:
|
| 65 |
+
print(f"\n测试 batch_size={batch_size}, seq_length={seq_length}...")
|
| 66 |
+
|
| 67 |
+
# 准备输入
|
| 68 |
+
input_text = "信息安全是保护计算机系统" * (seq_length // 10)
|
| 69 |
+
inputs = tokenizer(input_text, return_tensors="pt", max_length=seq_length, truncation=True)
|
| 70 |
+
input_ids = inputs["input_ids"].to(model.device)
|
| 71 |
+
|
| 72 |
+
# 扩展为batch
|
| 73 |
+
input_ids = input_ids.repeat(batch_size, 1)
|
| 74 |
+
|
| 75 |
+
# 清空GPU缓存
|
| 76 |
+
torch.cuda.empty_cache()
|
| 77 |
+
torch.cuda.reset_peak_memory_stats()
|
| 78 |
+
|
| 79 |
+
# 预热
|
| 80 |
+
with torch.no_grad():
|
| 81 |
+
_ = model.generate(input_ids, max_new_tokens=10, do_sample=False)
|
| 82 |
+
|
| 83 |
+
# 测试首token延迟
|
| 84 |
+
torch.cuda.synchronize()
|
| 85 |
+
start_time = time.time()
|
| 86 |
+
|
| 87 |
+
with torch.no_grad():
|
| 88 |
+
outputs = model.generate(
|
| 89 |
+
input_ids,
|
| 90 |
+
max_new_tokens=output_len,
|
| 91 |
+
do_sample=False,
|
| 92 |
+
temperature=1.0,
|
| 93 |
+
use_cache=True
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
torch.cuda.synchronize()
|
| 97 |
+
total_time = time.time() - start_time
|
| 98 |
+
|
| 99 |
+
# 计算指标
|
| 100 |
+
generated_tokens = outputs.shape[1] - input_ids.shape[1]
|
| 101 |
+
total_tokens = generated_tokens * batch_size
|
| 102 |
+
throughput = total_tokens / total_time
|
| 103 |
+
|
| 104 |
+
# 估算TTFT(使用第一次生成的时间)
|
| 105 |
+
ttft = total_time / (generated_tokens + 1) * 1000 # 转换为毫秒
|
| 106 |
+
|
| 107 |
+
# 计算TPOT
|
| 108 |
+
tpot = (total_time * 1000) / generated_tokens # 转换为毫秒
|
| 109 |
+
|
| 110 |
+
# 获取显存使用
|
| 111 |
+
memory = get_gpu_memory()
|
| 112 |
+
|
| 113 |
+
result = {
|
| 114 |
+
"backend": "transformers",
|
| 115 |
+
"batch_size": batch_size,
|
| 116 |
+
"seq_length": seq_length,
|
| 117 |
+
"output_length": output_len,
|
| 118 |
+
"generated_tokens": generated_tokens,
|
| 119 |
+
"total_time_sec": total_time,
|
| 120 |
+
"throughput_tokens_per_sec": throughput,
|
| 121 |
+
"ttft_ms": ttft,
|
| 122 |
+
"tpot_ms": tpot,
|
| 123 |
+
"memory_allocated_gb": memory["allocated_gb"],
|
| 124 |
+
"memory_reserved_gb": memory["reserved_gb"],
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
results.append(result)
|
| 128 |
+
|
| 129 |
+
print(f" 吞吐量: {throughput:.2f} tokens/sec")
|
| 130 |
+
print(f" TTFT: {ttft:.2f} ms")
|
| 131 |
+
print(f" TPOT: {tpot:.2f} ms")
|
| 132 |
+
print(f" 显存占用: {memory['allocated_gb']:.2f} GB")
|
| 133 |
+
|
| 134 |
+
# 释放模型
|
| 135 |
+
del model, tokenizer
|
| 136 |
+
torch.cuda.empty_cache()
|
| 137 |
+
|
| 138 |
+
return results
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def benchmark_vllm(model_path: str, batch_sizes: List[int],
|
| 142 |
+
seq_lengths: List[int], output_len: int = 128) -> List[Dict[str, Any]]:
|
| 143 |
+
"""使用vLLM进行基准测试"""
|
| 144 |
+
try:
|
| 145 |
+
from vllm import LLM, SamplingParams
|
| 146 |
+
except ImportError:
|
| 147 |
+
print("错误: 未安装vLLM,请运行: pip install vllm")
|
| 148 |
+
return []
|
| 149 |
+
|
| 150 |
+
print(f"\n{'='*60}")
|
| 151 |
+
print(f"使用 vLLM 后端测试模型: {model_path}")
|
| 152 |
+
print(f"{'='*60}")
|
| 153 |
+
|
| 154 |
+
# 初始化vLLM引擎
|
| 155 |
+
print("初始化vLLM引擎...")
|
| 156 |
+
llm = LLM(
|
| 157 |
+
model=model_path,
|
| 158 |
+
dtype="float16",
|
| 159 |
+
trust_remote_code=True,
|
| 160 |
+
max_model_len=2048,
|
| 161 |
+
gpu_memory_utilization=0.9
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
results = []
|
| 165 |
+
|
| 166 |
+
for batch_size in batch_sizes:
|
| 167 |
+
for seq_length in seq_lengths:
|
| 168 |
+
print(f"\n测试 batch_size={batch_size}, seq_length={seq_length}...")
|
| 169 |
+
|
| 170 |
+
# 准备输入
|
| 171 |
+
input_text = "信息安全是保护计算机系统" * (seq_length // 10)
|
| 172 |
+
prompts = [input_text] * batch_size
|
| 173 |
+
|
| 174 |
+
# 采样参数
|
| 175 |
+
sampling_params = SamplingParams(
|
| 176 |
+
temperature=1.0,
|
| 177 |
+
max_tokens=output_len,
|
| 178 |
+
use_beam_search=False
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# 清空GPU缓存
|
| 182 |
+
torch.cuda.empty_cache()
|
| 183 |
+
torch.cuda.reset_peak_memory_stats()
|
| 184 |
+
|
| 185 |
+
# 测试
|
| 186 |
+
torch.cuda.synchronize()
|
| 187 |
+
start_time = time.time()
|
| 188 |
+
|
| 189 |
+
outputs = llm.generate(prompts, sampling_params)
|
| 190 |
+
|
| 191 |
+
torch.cuda.synchronize()
|
| 192 |
+
total_time = time.time() - start_time
|
| 193 |
+
|
| 194 |
+
# 计算指标
|
| 195 |
+
generated_tokens = sum(len(output.outputs[0].token_ids) for output in outputs)
|
| 196 |
+
total_tokens = generated_tokens
|
| 197 |
+
throughput = total_tokens / total_time
|
| 198 |
+
|
| 199 |
+
# 估算TTFT和TPOT
|
| 200 |
+
ttft = total_time / (generated_tokens / batch_size + 1) * 1000
|
| 201 |
+
tpot = (total_time * 1000) / (generated_tokens / batch_size)
|
| 202 |
+
|
| 203 |
+
# 获取显存使用
|
| 204 |
+
memory = get_gpu_memory()
|
| 205 |
+
|
| 206 |
+
result = {
|
| 207 |
+
"backend": "vllm",
|
| 208 |
+
"batch_size": batch_size,
|
| 209 |
+
"seq_length": seq_length,
|
| 210 |
+
"output_length": output_len,
|
| 211 |
+
"generated_tokens": generated_tokens,
|
| 212 |
+
"total_time_sec": total_time,
|
| 213 |
+
"throughput_tokens_per_sec": throughput,
|
| 214 |
+
"ttft_ms": ttft,
|
| 215 |
+
"tpot_ms": tpot,
|
| 216 |
+
"memory_allocated_gb": memory["allocated_gb"],
|
| 217 |
+
"memory_reserved_gb": memory["reserved_gb"],
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
results.append(result)
|
| 221 |
+
|
| 222 |
+
print(f" 吞吐量: {throughput:.2f} tokens/sec")
|
| 223 |
+
print(f" TTFT: {ttft:.2f} ms")
|
| 224 |
+
print(f" TPOT: {tpot:.2f} ms")
|
| 225 |
+
print(f" 显存占用: {memory['allocated_gb']:.2f} GB")
|
| 226 |
+
|
| 227 |
+
return results
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def benchmark_sglang(model_path: str, batch_sizes: List[int],
|
| 231 |
+
seq_lengths: List[int], output_len: int = 128) -> List[Dict[str, Any]]:
|
| 232 |
+
"""使用SGLang进行基准测试"""
|
| 233 |
+
try:
|
| 234 |
+
import sglang as sgl
|
| 235 |
+
except ImportError:
|
| 236 |
+
print("错误: 未安装SGLang,请运行: pip install sglang")
|
| 237 |
+
return []
|
| 238 |
+
|
| 239 |
+
print(f"\n{'='*60}")
|
| 240 |
+
print(f"使用 SGLang 后端测试模型: {model_path}")
|
| 241 |
+
print(f"{'='*60}")
|
| 242 |
+
|
| 243 |
+
# 初始化SGLang运行时
|
| 244 |
+
print("初始化SGLang运行时...")
|
| 245 |
+
runtime = sgl.Runtime(
|
| 246 |
+
model_path=model_path,
|
| 247 |
+
trust_remote_code=True,
|
| 248 |
+
context_length=2048,
|
| 249 |
+
)
|
| 250 |
+
sgl.set_default_backend(runtime)
|
| 251 |
+
|
| 252 |
+
results = []
|
| 253 |
+
|
| 254 |
+
for batch_size in batch_sizes:
|
| 255 |
+
for seq_length in seq_lengths:
|
| 256 |
+
print(f"\n测试 batch_size={batch_size}, seq_length={seq_length}...")
|
| 257 |
+
|
| 258 |
+
# 准备输入
|
| 259 |
+
input_text = "信息安全是保护计算机系统" * (seq_length // 10)
|
| 260 |
+
prompts = [input_text] * batch_size
|
| 261 |
+
|
| 262 |
+
# 清空GPU缓存
|
| 263 |
+
torch.cuda.empty_cache()
|
| 264 |
+
torch.cuda.reset_peak_memory_stats()
|
| 265 |
+
|
| 266 |
+
# 测试
|
| 267 |
+
torch.cuda.synchronize()
|
| 268 |
+
start_time = time.time()
|
| 269 |
+
|
| 270 |
+
@sgl.function
|
| 271 |
+
def generate_text(s, prompt):
|
| 272 |
+
s += prompt + sgl.gen("response", max_tokens=output_len, temperature=1.0)
|
| 273 |
+
|
| 274 |
+
states = [generate_text.run(prompt=p) for p in prompts]
|
| 275 |
+
|
| 276 |
+
torch.cuda.synchronize()
|
| 277 |
+
total_time = time.time() - start_time
|
| 278 |
+
|
| 279 |
+
# 计算指标
|
| 280 |
+
generated_tokens = sum(len(state["response"]) for state in states) // 2 # 粗略估算
|
| 281 |
+
total_tokens = generated_tokens
|
| 282 |
+
throughput = total_tokens / total_time
|
| 283 |
+
|
| 284 |
+
# 估算TTFT和TPOT
|
| 285 |
+
ttft = total_time / (generated_tokens / batch_size + 1) * 1000
|
| 286 |
+
tpot = (total_time * 1000) / (generated_tokens / batch_size)
|
| 287 |
+
|
| 288 |
+
# 获取显存使用
|
| 289 |
+
memory = get_gpu_memory()
|
| 290 |
+
|
| 291 |
+
result = {
|
| 292 |
+
"backend": "sglang",
|
| 293 |
+
"batch_size": batch_size,
|
| 294 |
+
"seq_length": seq_length,
|
| 295 |
+
"output_length": output_len,
|
| 296 |
+
"generated_tokens": generated_tokens,
|
| 297 |
+
"total_time_sec": total_time,
|
| 298 |
+
"throughput_tokens_per_sec": throughput,
|
| 299 |
+
"ttft_ms": ttft,
|
| 300 |
+
"tpot_ms": tpot,
|
| 301 |
+
"memory_allocated_gb": memory["allocated_gb"],
|
| 302 |
+
"memory_reserved_gb": memory["reserved_gb"],
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
results.append(result)
|
| 306 |
+
|
| 307 |
+
print(f" 吞吐量: {throughput:.2f} tokens/sec")
|
| 308 |
+
print(f" TTFT: {ttft:.2f} ms")
|
| 309 |
+
print(f" TPOT: {tpot:.2f} ms")
|
| 310 |
+
print(f" 显存占用: {memory['allocated_gb']:.2f} GB")
|
| 311 |
+
|
| 312 |
+
runtime.shutdown()
|
| 313 |
+
|
| 314 |
+
return results
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def print_summary(results: List[Dict[str, Any]]):
|
| 318 |
+
"""打印测试结果汇总"""
|
| 319 |
+
if not results:
|
| 320 |
+
print("\n没有测试结果可汇总")
|
| 321 |
+
return
|
| 322 |
+
|
| 323 |
+
print(f"\n{'='*80}")
|
| 324 |
+
print("基准测试结果汇总")
|
| 325 |
+
print(f"{'='*80}")
|
| 326 |
+
print(f"{'Backend':<12} {'Batch':<6} {'SeqLen':<7} {'OutLen':<7} "
|
| 327 |
+
f"{'Throughput':<12} {'TTFT(ms)':<10} {'TPOT(ms)':<10} {'Memory(GB)':<11}")
|
| 328 |
+
print(f"{'-'*80}")
|
| 329 |
+
|
| 330 |
+
for r in results:
|
| 331 |
+
print(f"{r['backend']:<12} {r['batch_size']:<6} {r['seq_length']:<7} "
|
| 332 |
+
f"{r['output_length']:<7} {r['throughput_tokens_per_sec']:<12.2f} "
|
| 333 |
+
f"{r['ttft_ms']:<10.2f} {r['tpot_ms']:<10.2f} {r['memory_allocated_gb']:<11.2f}")
|
| 334 |
+
|
| 335 |
+
print(f"{'='*80}\n")
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def main():
|
| 339 |
+
"""主函数"""
|
| 340 |
+
parser = argparse.ArgumentParser(description="Qwen3.5-0.8B-HOS 推理性能基准测试")
|
| 341 |
+
parser.add_argument("--model", type=str, default="lxcxjxhx/Qwen3.5-0.8B-HOS",
|
| 342 |
+
help="模型路径或HF仓库ID")
|
| 343 |
+
parser.add_argument("--backend", type=str, default="transformers",
|
| 344 |
+
choices=["transformers", "vllm", "sglang"],
|
| 345 |
+
help="推理后端")
|
| 346 |
+
parser.add_argument("--batch-sizes", type=str, default="1,2,4,8",
|
| 347 |
+
help="测试的batch size列表,如 '1,2,4,8'")
|
| 348 |
+
parser.add_argument("--seq-lengths", type=str, default="128,256,512,1024",
|
| 349 |
+
help="测试的序列长度列表,如 '128,256,512,1024'")
|
| 350 |
+
parser.add_argument("--output-len", type=int, default=128,
|
| 351 |
+
help="生成的token数量")
|
| 352 |
+
parser.add_argument("--output", type=str, default="benchmark_results.json",
|
| 353 |
+
help="结果保存路径")
|
| 354 |
+
|
| 355 |
+
args = parser.parse_args()
|
| 356 |
+
|
| 357 |
+
# 解析列表参数
|
| 358 |
+
batch_sizes = [int(x) for x in args.batch_sizes.split(",")]
|
| 359 |
+
seq_lengths = [int(x) for x in args.seq_lengths.split(",")]
|
| 360 |
+
|
| 361 |
+
print(f"\n{'='*60}")
|
| 362 |
+
print(f"Qwen3.5-0.8B-HOS 推理性能基准测试")
|
| 363 |
+
print(f"{'='*60}")
|
| 364 |
+
print(f"模型: {args.model}")
|
| 365 |
+
print(f"后端: {args.backend}")
|
| 366 |
+
print(f"Batch sizes: {batch_sizes}")
|
| 367 |
+
print(f"Sequence lengths: {seq_lengths}")
|
| 368 |
+
print(f"Output length: {args.output_len}")
|
| 369 |
+
print(f"{'='*60}\n")
|
| 370 |
+
|
| 371 |
+
# 执行基准测试
|
| 372 |
+
if args.backend == "transformers":
|
| 373 |
+
results = benchmark_transformers(
|
| 374 |
+
args.model, batch_sizes, seq_lengths, args.output_len
|
| 375 |
+
)
|
| 376 |
+
elif args.backend == "vllm":
|
| 377 |
+
results = benchmark_vllm(
|
| 378 |
+
args.model, batch_sizes, seq_lengths, args.output_len
|
| 379 |
+
)
|
| 380 |
+
elif args.backend == "sglang":
|
| 381 |
+
results = benchmark_sglang(
|
| 382 |
+
args.model, batch_sizes, seq_lengths, args.output_len
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
# 打印汇总
|
| 386 |
+
print_summary(results)
|
| 387 |
+
|
| 388 |
+
# 保存结果
|
| 389 |
+
if results:
|
| 390 |
+
with open(args.output, 'w', encoding='utf-8') as f:
|
| 391 |
+
json.dump(results, f, indent=2, ensure_ascii=False)
|
| 392 |
+
print(f"结果已保存到: {args.output}")
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
if __name__ == "__main__":
|
| 396 |
+
main()
|
inference/quantize.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Qwen3.5-0.8B-HOS 量化脚本
|
| 4 |
+
|
| 5 |
+
支持量化方法:
|
| 6 |
+
- AWQ 4-bit:激活感知量化,精度损失最小
|
| 7 |
+
- GPTQ 8-bit:基于 GPU 的 Post-Training 量化
|
| 8 |
+
- bitsandbytes 4/8-bit:动态量化,无需校准数据
|
| 9 |
+
|
| 10 |
+
使用方法:
|
| 11 |
+
# AWQ 4-bit 量化
|
| 12 |
+
python quantize.py --method awq --bits 4
|
| 13 |
+
|
| 14 |
+
# GPTQ 8-bit 量化
|
| 15 |
+
python quantize.py --method gptq --bits 8
|
| 16 |
+
|
| 17 |
+
# bitsandbytes 4-bit 动态量化(最简单,无需校准数据)
|
| 18 |
+
python quantize.py --method bnb --bits 4
|
| 19 |
+
|
| 20 |
+
# 测试量化后模型
|
| 21 |
+
python quantize.py --method awq --bits 4 --test
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
import argparse
|
| 25 |
+
import os
|
| 26 |
+
import time
|
| 27 |
+
import torch
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def quantize_awq(model_path, output_path, bits=4):
|
| 31 |
+
"""AWQ 4-bit 量化
|
| 32 |
+
|
| 33 |
+
AWQ (Activation-aware Weight Quantization) 通过保护显著权重通道
|
| 34 |
+
实现高精度 4-bit 量化,适合 0.8B 小模型。
|
| 35 |
+
"""
|
| 36 |
+
print(f"=== AWQ {bits}-bit 量化 ===")
|
| 37 |
+
print(f"输入模型: {model_path}")
|
| 38 |
+
print(f"输出路径: {output_path}")
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
from awq import AutoAWQForCausalLM
|
| 42 |
+
from transformers import AutoTokenizer
|
| 43 |
+
|
| 44 |
+
# 加载模型和分词器
|
| 45 |
+
print("加载模型...")
|
| 46 |
+
model = AutoAWQForCausalLM.from_pretrained(
|
| 47 |
+
model_path,
|
| 48 |
+
trust_remote_code=True,
|
| 49 |
+
)
|
| 50 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 51 |
+
model_path,
|
| 52 |
+
trust_remote_code=True,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
# AWQ 量化配置
|
| 56 |
+
quant_config = {
|
| 57 |
+
"zero_point": True,
|
| 58 |
+
"q_group_size": 128,
|
| 59 |
+
"w_bit": bits,
|
| 60 |
+
"version": "GEMM",
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
print(f"开始 AWQ {bits}-bit 量化...")
|
| 64 |
+
model.quantize(tokenizer, quant_config=quant_config)
|
| 65 |
+
|
| 66 |
+
# 保存量化模型
|
| 67 |
+
print(f"保存量化模型到: {output_path}")
|
| 68 |
+
model.save_quantized(output_path)
|
| 69 |
+
tokenizer.save_pretrained(output_path)
|
| 70 |
+
|
| 71 |
+
print("AWQ 量化完成!")
|
| 72 |
+
return output_path
|
| 73 |
+
|
| 74 |
+
except ImportError:
|
| 75 |
+
print("错误: 需要安装 autoawq")
|
| 76 |
+
print(" pip install autoawq")
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def quantize_gptq(model_path, output_path, bits=8):
|
| 81 |
+
"""GPTQ 量化
|
| 82 |
+
|
| 83 |
+
GPTQ 基于 Optimal Brain Quantization 框架,通过逐层量化
|
| 84 |
+
和误差补偿实现高精度量化。
|
| 85 |
+
"""
|
| 86 |
+
print(f"=== GPTQ {bits}-bit 量化 ===")
|
| 87 |
+
print(f"输入模型: {model_path}")
|
| 88 |
+
print(f"输出路径: {output_path}")
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
from transformers import AutoTokenizer
|
| 92 |
+
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
|
| 93 |
+
|
| 94 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 95 |
+
model_path,
|
| 96 |
+
trust_remote_code=True,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# GPTQ 量化配置
|
| 100 |
+
quantize_config = BaseQuantizeConfig(
|
| 101 |
+
bits=bits,
|
| 102 |
+
group_size=128,
|
| 103 |
+
desc_act=False,
|
| 104 |
+
damp_percent=0.1,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
print("加载模型...")
|
| 108 |
+
model = AutoGPTQForCausalLM.from_pretrained(
|
| 109 |
+
model_path,
|
| 110 |
+
quantize_config,
|
| 111 |
+
trust_remote_code=True,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
# 准备校准数据(使用简单文本)
|
| 115 |
+
print("准备校准数据...")
|
| 116 |
+
calibration_texts = [
|
| 117 |
+
"信息安全是保护计算机系统和网络免受未经授权的访问、攻击或破坏的实践。",
|
| 118 |
+
"SQL注入是一种常见的Web安全漏洞,攻击者通过注入恶意SQL代码来操纵数据库。",
|
| 119 |
+
"渗透测试是一种模拟真实攻击者的方法来评估系统安全性的技术。",
|
| 120 |
+
"防火墙是网络安全的第一道防线,用于监控和控制进出网络的流量。",
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
calibration_data = [
|
| 124 |
+
tokenizer(text, return_tensors="pt", max_length=512, truncation=True)
|
| 125 |
+
for text in calibration_texts
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
print(f"开始 GPTQ {bits}-bit 量化...")
|
| 129 |
+
model.quantize(calibration_data)
|
| 130 |
+
|
| 131 |
+
print(f"保存量化模型到: {output_path}")
|
| 132 |
+
model.save_quantized(output_path)
|
| 133 |
+
tokenizer.save_pretrained(output_path)
|
| 134 |
+
|
| 135 |
+
print("GPTQ 量化完成!")
|
| 136 |
+
return output_path
|
| 137 |
+
|
| 138 |
+
except ImportError:
|
| 139 |
+
print("错误: 需要安装 auto-gptq")
|
| 140 |
+
print(" pip install auto-gptq")
|
| 141 |
+
return None
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def quantize_bnb(model_path, output_path, bits=4):
|
| 145 |
+
"""bitsandbytes 动态量化
|
| 146 |
+
|
| 147 |
+
最简单的量化方法,无需校准数据,适合快速部署。
|
| 148 |
+
支持 4-bit (NF4/FP4) 和 8-bit 量化。
|
| 149 |
+
"""
|
| 150 |
+
print(f"=== bitsandbytes {bits}-bit 量化 ===")
|
| 151 |
+
print(f"输入模型: {model_path}")
|
| 152 |
+
print(f"输出路径: {output_path}")
|
| 153 |
+
|
| 154 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
|
| 155 |
+
|
| 156 |
+
if bits == 4:
|
| 157 |
+
quant_config = BitsAndBytesConfig(
|
| 158 |
+
load_in_4bit=True,
|
| 159 |
+
bnb_4bit_quant_type="nf4",
|
| 160 |
+
bnb_4bit_use_double_quant=True,
|
| 161 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 162 |
+
)
|
| 163 |
+
elif bits == 8:
|
| 164 |
+
quant_config = BitsAndBytesConfig(
|
| 165 |
+
load_in_8bit=True,
|
| 166 |
+
)
|
| 167 |
+
else:
|
| 168 |
+
raise ValueError(f"不支持的量化位数: {bits}")
|
| 169 |
+
|
| 170 |
+
print("加载并量化模型...")
|
| 171 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 172 |
+
model_path,
|
| 173 |
+
quantization_config=quant_config,
|
| 174 |
+
trust_remote_code=True,
|
| 175 |
+
device_map="auto",
|
| 176 |
+
)
|
| 177 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 178 |
+
model_path,
|
| 179 |
+
trust_remote_code=True,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
# 保存量化后的模型
|
| 183 |
+
os.makedirs(output_path, exist_ok=True)
|
| 184 |
+
print(f"保存量化模型到: {output_path}")
|
| 185 |
+
model.save_pretrained(output_path)
|
| 186 |
+
tokenizer.save_pretrained(output_path)
|
| 187 |
+
|
| 188 |
+
print(f"bitsandbytes {bits}-bit 量化完成!")
|
| 189 |
+
return output_path
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def test_quantized_model(model_path, bits=4):
|
| 193 |
+
"""测试量化后模型的推理"""
|
| 194 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 195 |
+
|
| 196 |
+
print(f"\n=== 测试量化模型 ===")
|
| 197 |
+
print(f"模型路径: {model_path}")
|
| 198 |
+
|
| 199 |
+
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
|
| 200 |
+
|
| 201 |
+
# 根据量化方法选择加载方式
|
| 202 |
+
if "awq" in model_path.lower() or "gptq" in model_path.lower():
|
| 203 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 204 |
+
model_path,
|
| 205 |
+
trust_remote_code=True,
|
| 206 |
+
device_map="auto",
|
| 207 |
+
)
|
| 208 |
+
else:
|
| 209 |
+
# bitsandbytes 量化模型
|
| 210 |
+
quant_config = BitsAndBytesConfig(
|
| 211 |
+
load_in_4bit=(bits == 4),
|
| 212 |
+
load_in_8bit=(bits == 8),
|
| 213 |
+
bnb_4bit_quant_type="nf4",
|
| 214 |
+
bnb_4bit_use_double_quant=True,
|
| 215 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 216 |
+
)
|
| 217 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 218 |
+
model_path,
|
| 219 |
+
quantization_config=quant_config,
|
| 220 |
+
trust_remote_code=True,
|
| 221 |
+
device_map="auto",
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# 测试推理
|
| 225 |
+
test_prompts = [
|
| 226 |
+
"什么是DDoS攻击?",
|
| 227 |
+
"如何防范中间人攻击?",
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
for prompt in test_prompts:
|
| 231 |
+
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
|
| 232 |
+
|
| 233 |
+
start = time.time()
|
| 234 |
+
with torch.no_grad():
|
| 235 |
+
outputs = model.generate(
|
| 236 |
+
**inputs,
|
| 237 |
+
max_new_tokens=128,
|
| 238 |
+
temperature=0.7,
|
| 239 |
+
top_p=0.9,
|
| 240 |
+
)
|
| 241 |
+
elapsed = time.time() - start
|
| 242 |
+
|
| 243 |
+
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 244 |
+
token_count = outputs.shape[1] - inputs["input_ids"].shape[1]
|
| 245 |
+
|
| 246 |
+
print(f"\nPrompt: {prompt}")
|
| 247 |
+
print(f"回复: {response[:200]}...")
|
| 248 |
+
print(f"生成 {token_count} tokens, 耗时 {elapsed:.2f}s")
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def get_model_size(model_path):
|
| 252 |
+
"""获取模型文件大小"""
|
| 253 |
+
total_size = 0
|
| 254 |
+
for dirpath, _, filenames in os.walk(model_path):
|
| 255 |
+
for f in filenames:
|
| 256 |
+
if f.endswith((".safetensors", ".bin", ".pt")):
|
| 257 |
+
fp = os.path.join(dirpath, f)
|
| 258 |
+
total_size += os.path.getsize(fp)
|
| 259 |
+
return total_size / (1024 ** 3) # GB
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
if __name__ == "__main__":
|
| 263 |
+
parser = argparse.ArgumentParser(description="Qwen3.5-0.8B-HOS 量化脚本")
|
| 264 |
+
parser.add_argument("--model", type=str, default="lxcxjxhx/Qwen3.5-0.8B-HOS",
|
| 265 |
+
help="输入模型路径")
|
| 266 |
+
parser.add_argument("--output", type=str, default=None,
|
| 267 |
+
help="输出模型路径")
|
| 268 |
+
parser.add_argument("--method", type=str, default="bnb",
|
| 269 |
+
choices=["awq", "gptq", "bnb"],
|
| 270 |
+
help="量化方法")
|
| 271 |
+
parser.add_argument("--bits", type=int, default=4, choices=[4, 8],
|
| 272 |
+
help="量化位数")
|
| 273 |
+
parser.add_argument("--test", action="store_true",
|
| 274 |
+
help="测试量化后模型")
|
| 275 |
+
|
| 276 |
+
args = parser.parse_args()
|
| 277 |
+
|
| 278 |
+
if args.output is None:
|
| 279 |
+
args.output = f"{args.model.replace('/', '-')}-{args.method}-{args.bits}bit"
|
| 280 |
+
|
| 281 |
+
print(f"量化方法: {args.method}")
|
| 282 |
+
print(f"量化位数: {args.bits}-bit")
|
| 283 |
+
|
| 284 |
+
# 量化前模型大小
|
| 285 |
+
if os.path.exists(args.model):
|
| 286 |
+
original_size = get_model_size(args.model)
|
| 287 |
+
print(f"原始模型大小: {original_size:.2f} GB")
|
| 288 |
+
|
| 289 |
+
# 执行量化
|
| 290 |
+
if args.method == "awq":
|
| 291 |
+
result = quantize_awq(args.model, args.output, args.bits)
|
| 292 |
+
elif args.method == "gptq":
|
| 293 |
+
result = quantize_gptq(args.model, args.output, args.bits)
|
| 294 |
+
elif args.method == "bnb":
|
| 295 |
+
result = quantize_bnb(args.model, args.output, args.bits)
|
| 296 |
+
|
| 297 |
+
if result and os.path.exists(result):
|
| 298 |
+
quantized_size = get_model_size(result)
|
| 299 |
+
print(f"\n量化后模型大小: {quantized_size:.2f} GB")
|
| 300 |
+
if os.path.exists(args.model):
|
| 301 |
+
compression = (1 - quantized_size / original_size) * 100
|
| 302 |
+
print(f"压缩率: {compression:.1f}%")
|
| 303 |
+
|
| 304 |
+
# 测试
|
| 305 |
+
if args.test and result:
|
| 306 |
+
test_quantized_model(result, args.bits)
|
inference/sglang_serve.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Qwen3.5-0.8B-HOS SGLang 推理服务
|
| 4 |
+
|
| 5 |
+
支持特性:
|
| 6 |
+
- RadixAttention:前缀缓存加速,重复前缀请求速度提升 3-5x
|
| 7 |
+
- JSON Schema 约束生成:确保输出符合指定格式
|
| 8 |
+
- 高效连续批处理
|
| 9 |
+
|
| 10 |
+
使用方法:
|
| 11 |
+
# 启动服务
|
| 12 |
+
python sglang_serve.py --serve
|
| 13 |
+
|
| 14 |
+
# 带 JSON 约束的推理测试
|
| 15 |
+
python sglang_serve.py --test
|
| 16 |
+
|
| 17 |
+
# 命令行交互
|
| 18 |
+
python sglang_serve.py --chat
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import argparse
|
| 22 |
+
import json
|
| 23 |
+
import time
|
| 24 |
+
import torch
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def load_model(model_path="lxcxjxhx/Qwen3.5-0.8B-HOS"):
|
| 28 |
+
"""加载模型到 SGLang 引擎"""
|
| 29 |
+
import sglang as sgl
|
| 30 |
+
|
| 31 |
+
# RadixAttention 自动启用
|
| 32 |
+
runtime = sgl.Runtime(
|
| 33 |
+
model_path=model_path,
|
| 34 |
+
# 0.8B 小模型配置
|
| 35 |
+
tp_size=1,
|
| 36 |
+
mem_fraction_static=0.9,
|
| 37 |
+
context_length=8192,
|
| 38 |
+
trust_remote_code=True,
|
| 39 |
+
)
|
| 40 |
+
return runtime
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def serve(model_path="lxcxjxhx/Qwen3.5-0.8B-HOS", host="0.0.0.0", port=30000):
|
| 44 |
+
"""启动 SGLang OpenAI 兼容 API 服务
|
| 45 |
+
|
| 46 |
+
启动后可通过以下方式调用:
|
| 47 |
+
curl http://localhost:30000/v1/completions \\
|
| 48 |
+
-H "Content-Type: application/json" \\
|
| 49 |
+
-d '{"model": "Qwen3.5-0.8B-HOS", "prompt": "什么是SQL注入?", "max_tokens": 256}'
|
| 50 |
+
"""
|
| 51 |
+
import sglang as sgl
|
| 52 |
+
|
| 53 |
+
print(f"启动 SGLang 服务: http://{host}:{port}")
|
| 54 |
+
print(f"模型: {model_path}")
|
| 55 |
+
print("特性: RadixAttention, Continuous Batching, JSON Schema 约束生成")
|
| 56 |
+
|
| 57 |
+
runtime = sgl.Runtime(
|
| 58 |
+
model_path=model_path,
|
| 59 |
+
host=host,
|
| 60 |
+
port=port,
|
| 61 |
+
tp_size=1,
|
| 62 |
+
mem_fraction_static=0.9,
|
| 63 |
+
context_length=8192,
|
| 64 |
+
trust_remote_code=True,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
print(f"\n服务已启动,按 Ctrl+C 停止")
|
| 68 |
+
try:
|
| 69 |
+
runtime.loop()
|
| 70 |
+
except KeyboardInterrupt:
|
| 71 |
+
runtime.shutdown()
|
| 72 |
+
print("\n服务已停止")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def test_json_constrained(model_path="lxcxjxhx/Qwen3.5-0.8B-HOS"):
|
| 76 |
+
"""测试 JSON Schema 约束生成"""
|
| 77 |
+
import sglang as sgl
|
| 78 |
+
|
| 79 |
+
print(f"加载模型: {model_path}")
|
| 80 |
+
sgl.set_default_backend(sgl.RuntimeEndpoint(f"http://localhost:30000"))
|
| 81 |
+
|
| 82 |
+
# 定义安全事件分析的 JSON Schema
|
| 83 |
+
security_schema = {
|
| 84 |
+
"type": "object",
|
| 85 |
+
"properties": {
|
| 86 |
+
"threat_level": {"type": "string", "enum": ["low", "medium", "high", "critical"]},
|
| 87 |
+
"attack_type": {"type": "string"},
|
| 88 |
+
"affected_system": {"type": "string"},
|
| 89 |
+
"recommendation": {"type": "string"},
|
| 90 |
+
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
|
| 91 |
+
},
|
| 92 |
+
"required": ["threat_level", "attack_type", "recommendation"],
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
@sgl.function
|
| 96 |
+
def analyze_security(s, log_entry):
|
| 97 |
+
s += f"分析以下安全日志条目,以 JSON 格式输出分析结果:\n{log_entry}\n"
|
| 98 |
+
s += sgl.gen("analysis", max_tokens=256, json_schema=security_schema)
|
| 99 |
+
|
| 100 |
+
test_logs = [
|
| 101 |
+
"2024-01-15 03:22:11 ALERT: Multiple failed SSH login attempts from 192.168.1.100 (50 attempts in 5 minutes)",
|
| 102 |
+
"2024-01-15 04:15:33 WARNING: Unusual outbound traffic detected from server-web-01 to external IP 203.0.113.50 on port 4444",
|
| 103 |
+
"2024-01-15 05:01:22 CRITICAL: SQL injection pattern detected in web request: ' OR 1=1; DROP TABLE users;--",
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
print(f"\n=== JSON 约束生成测试 ===")
|
| 107 |
+
for i, log in enumerate(test_logs):
|
| 108 |
+
start = time.time()
|
| 109 |
+
state = analyze_security.run(log=log)
|
| 110 |
+
elapsed = time.time() - start
|
| 111 |
+
|
| 112 |
+
print(f"\n--- 日志 {i+1} ---")
|
| 113 |
+
print(f"输入: {log[:80]}...")
|
| 114 |
+
try:
|
| 115 |
+
result = json.loads(state["analysis"])
|
| 116 |
+
print(f"分析结果: {json.dumps(result, indent=2, ensure_ascii=False)}")
|
| 117 |
+
except json.JSONDecodeError:
|
| 118 |
+
print(f"原始输出: {state['analysis']}")
|
| 119 |
+
print(f"耗时: {elapsed:.2f}s")
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def test_inference(model_path="lxcxjxhx/Qwen3.5-0.8B-HOS"):
|
| 123 |
+
"""基础推理测试"""
|
| 124 |
+
import sglang as sgl
|
| 125 |
+
|
| 126 |
+
print(f"加载模型: {model_path}")
|
| 127 |
+
runtime = load_model(model_path)
|
| 128 |
+
|
| 129 |
+
prompts = [
|
| 130 |
+
"请解释SQL注入攻击的原理和防御方法。",
|
| 131 |
+
"什么是XSS跨站脚本攻击?如何防范?",
|
| 132 |
+
"描述一次渗透测试的完整流程。",
|
| 133 |
+
]
|
| 134 |
+
|
| 135 |
+
print(f"\n=== SGLang 推理测试 ===")
|
| 136 |
+
start = time.time()
|
| 137 |
+
|
| 138 |
+
# 使用 RadixAttention 的批处理推理
|
| 139 |
+
outputs = runtime.generate(
|
| 140 |
+
prompts,
|
| 141 |
+
sampling_params={
|
| 142 |
+
"temperature": 0.7,
|
| 143 |
+
"top_p": 0.9,
|
| 144 |
+
"max_new_tokens": 256,
|
| 145 |
+
},
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
elapsed = time.time() - start
|
| 149 |
+
total_tokens = sum(len(o["token_ids"]) for o in outputs)
|
| 150 |
+
|
| 151 |
+
print(f"总耗时: {elapsed:.2f}s")
|
| 152 |
+
print(f"总 token 数: {total_tokens}")
|
| 153 |
+
print(f"吞吐量: {total_tokens / elapsed:.1f} tokens/s")
|
| 154 |
+
|
| 155 |
+
for i, output in enumerate(outputs):
|
| 156 |
+
print(f"\n--- 请求 {i+1} ---")
|
| 157 |
+
print(f"生成: {output['text'][:200]}...")
|
| 158 |
+
|
| 159 |
+
runtime.shutdown()
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def chat_mode(model_path="lxcxjxhx/Qwen3.5-0.8B-HOS"):
|
| 163 |
+
"""命令行交互模式"""
|
| 164 |
+
runtime = load_model(model_path)
|
| 165 |
+
|
| 166 |
+
print("\n=== Qwen3.5-0.8B-HOS SGLang 交互模式 ===")
|
| 167 |
+
print("输入 'quit' 或 'exit' 退出\n")
|
| 168 |
+
|
| 169 |
+
while True:
|
| 170 |
+
try:
|
| 171 |
+
user_input = input("用户: ").strip()
|
| 172 |
+
if user_input.lower() in ("quit", "exit", "q"):
|
| 173 |
+
break
|
| 174 |
+
if not user_input:
|
| 175 |
+
continue
|
| 176 |
+
|
| 177 |
+
prompt = f"<|im_start|>user\n{user_input}<|im_end|>\n<|im_start|>assistant\n"
|
| 178 |
+
|
| 179 |
+
start = time.time()
|
| 180 |
+
outputs = runtime.generate(
|
| 181 |
+
[prompt],
|
| 182 |
+
sampling_params={
|
| 183 |
+
"temperature": 0.7,
|
| 184 |
+
"top_p": 0.9,
|
| 185 |
+
"max_new_tokens": 512,
|
| 186 |
+
},
|
| 187 |
+
)
|
| 188 |
+
elapsed = time.time() - start
|
| 189 |
+
|
| 190 |
+
response = outputs[0]["text"]
|
| 191 |
+
token_count = len(outputs[0]["token_ids"])
|
| 192 |
+
|
| 193 |
+
print(f"\n助手: {response}")
|
| 194 |
+
print(f"[{token_count} tokens, {elapsed:.2f}s, {token_count/elapsed:.1f} tokens/s]\n")
|
| 195 |
+
|
| 196 |
+
except KeyboardInterrupt:
|
| 197 |
+
break
|
| 198 |
+
|
| 199 |
+
runtime.shutdown()
|
| 200 |
+
print("\n再见!")
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
if __name__ == "__main__":
|
| 204 |
+
parser = argparse.ArgumentParser(description="Qwen3.5-0.8B-HOS SGLang 推理服务")
|
| 205 |
+
parser.add_argument("--model", type=str, default="lxcxjxhx/Qwen3.5-0.8B-HOS",
|
| 206 |
+
help="模型路径或 HF 仓库 ID")
|
| 207 |
+
parser.add_argument("--serve", action="store_true", help="启动 API 服务")
|
| 208 |
+
parser.add_argument("--test", action="store_true", help="运行推理测试")
|
| 209 |
+
parser.add_argument("--test-json", action="store_true", help="测试 JSON 约束生成")
|
| 210 |
+
parser.add_argument("--chat", action="store_true", help="命令行交互模式")
|
| 211 |
+
parser.add_argument("--host", type=str, default="0.0.0.0", help="服务监听地址")
|
| 212 |
+
parser.add_argument("--port", type=int, default=30000, help="服务端口")
|
| 213 |
+
|
| 214 |
+
args = parser.parse_args()
|
| 215 |
+
|
| 216 |
+
if args.serve:
|
| 217 |
+
serve(args.model, args.host, args.port)
|
| 218 |
+
elif args.test:
|
| 219 |
+
test_inference(args.model)
|
| 220 |
+
elif args.test_json:
|
| 221 |
+
test_json_constrained(args.model)
|
| 222 |
+
elif args.chat:
|
| 223 |
+
chat_mode(args.model)
|
| 224 |
+
else:
|
| 225 |
+
serve(args.model, args.host, args.port)
|
inference/vllm_serve.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Qwen3.5-0.8B-HOS vLLM 推理服务
|
| 4 |
+
|
| 5 |
+
支持特性:
|
| 6 |
+
- PagedAttention:动态 KV cache 管理,提升吞吐量
|
| 7 |
+
- Continuous Batching:动态批处理,减少 padding 开销
|
| 8 |
+
- torch.compile 算子融合加速
|
| 9 |
+
|
| 10 |
+
使用方法:
|
| 11 |
+
# 启动 OpenAI 兼容 API 服务
|
| 12 |
+
python vllm_serve.py --serve
|
| 13 |
+
|
| 14 |
+
# 直接推理测试
|
| 15 |
+
python vllm_serve.py --test
|
| 16 |
+
|
| 17 |
+
# 命令行交互
|
| 18 |
+
python vllm_serve.py --chat
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import argparse
|
| 22 |
+
import time
|
| 23 |
+
import torch
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def load_model(model_path="lxcxjxhx/Qwen3.5-0.8B-HOS", dtype="bfloat16"):
|
| 27 |
+
"""加载模型到 vLLM 引擎"""
|
| 28 |
+
from vllm import LLM, SamplingParams
|
| 29 |
+
|
| 30 |
+
dtype_map = {
|
| 31 |
+
"bfloat16": torch.bfloat16,
|
| 32 |
+
"float16": torch.float16,
|
| 33 |
+
"float32": torch.float32,
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
llm = LLM(
|
| 37 |
+
model=model_path,
|
| 38 |
+
dtype=dtype_map.get(dtype, torch.bfloat16),
|
| 39 |
+
# PagedAttention 配置
|
| 40 |
+
gpu_memory_utilization=0.9,
|
| 41 |
+
max_model_len=8192,
|
| 42 |
+
# Continuous Batching(vLLM 默认启用)
|
| 43 |
+
max_num_seqs=256,
|
| 44 |
+
# 0.8B 小模型单卡无需 tensor parallelism
|
| 45 |
+
tensor_parallel_size=1,
|
| 46 |
+
# 启用 torch.compile 加速
|
| 47 |
+
enforce_eager=False,
|
| 48 |
+
trust_remote_code=True,
|
| 49 |
+
)
|
| 50 |
+
return llm
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def serve(model_path="lxcxjxhx/Qwen3.5-0.8B-HOS", host="0.0.0.0", port=8000):
|
| 54 |
+
"""启动 OpenAI 兼容 API 服务
|
| 55 |
+
|
| 56 |
+
启动后可通过以下方式调用:
|
| 57 |
+
curl http://localhost:8000/v1/completions \\
|
| 58 |
+
-H "Content-Type: application/json" \\
|
| 59 |
+
-d '{"model": "Qwen3.5-0.8B-HOS", "prompt": "什么是SQL注入?", "max_tokens": 256}'
|
| 60 |
+
"""
|
| 61 |
+
print(f"启动 vLLM 服务: http://{host}:{port}")
|
| 62 |
+
print(f"模型: {model_path}")
|
| 63 |
+
print("API 端点: /v1/completions, /v1/chat/completions")
|
| 64 |
+
|
| 65 |
+
# 使用 vLLM 内置的 OpenAI 兼容服务器
|
| 66 |
+
from vllm.entrypoints.openai.api_server import run_server
|
| 67 |
+
import uvicorn
|
| 68 |
+
|
| 69 |
+
uvicorn.run(
|
| 70 |
+
"vllm.entrypoints.openai.api_server:app",
|
| 71 |
+
host=host,
|
| 72 |
+
port=port,
|
| 73 |
+
log_level="info",
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def test_inference(model_path="lxcxjxhx/Qwen3.5-0.8B-HOS"):
|
| 78 |
+
"""推理性能测试"""
|
| 79 |
+
from vllm import LLM, SamplingParams
|
| 80 |
+
|
| 81 |
+
print(f"加载模型: {model_path}")
|
| 82 |
+
llm = load_model(model_path)
|
| 83 |
+
|
| 84 |
+
# 测试用例:信息安全领域问题
|
| 85 |
+
prompts = [
|
| 86 |
+
"请解释SQL注入攻击的原理和防御方法。",
|
| 87 |
+
"什么是XSS跨站脚本攻击?如何防范?",
|
| 88 |
+
"描述一次渗透测试的完整流程。",
|
| 89 |
+
"如何使用Wireshark进行网络流量分析?",
|
| 90 |
+
"解释RSA加密算法的工作原理。",
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
+
sampling_params = SamplingParams(
|
| 94 |
+
temperature=0.7,
|
| 95 |
+
top_p=0.9,
|
| 96 |
+
max_tokens=256,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
print(f"\n测试 {len(prompts)} 个请求...")
|
| 100 |
+
start = time.time()
|
| 101 |
+
outputs = llm.generate(prompts, sampling_params)
|
| 102 |
+
elapsed = time.time() - start
|
| 103 |
+
|
| 104 |
+
total_tokens = sum(len(o.outputs[0].token_ids) for o in outputs)
|
| 105 |
+
print(f"\n=== 推理测试结果 ===")
|
| 106 |
+
print(f"总耗时: {elapsed:.2f}s")
|
| 107 |
+
print(f"总生成 token 数: {total_tokens}")
|
| 108 |
+
print(f"吞吐量: {total_tokens / elapsed:.1f} tokens/s")
|
| 109 |
+
print(f"平均每请求: {elapsed / len(prompts):.2f}s")
|
| 110 |
+
|
| 111 |
+
for i, output in enumerate(outputs):
|
| 112 |
+
print(f"\n--- 请求 {i+1} ---")
|
| 113 |
+
print(f"Prompt: {output.prompt[:50]}...")
|
| 114 |
+
print(f"生成: {output.outputs[0].text[:200]}...")
|
| 115 |
+
print(f"Token 数: {len(output.outputs[0].token_ids)}")
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def chat_mode(model_path="lxcxjxhx/Qwen3.5-0.8B-HOS"):
|
| 119 |
+
"""命令行交互模式"""
|
| 120 |
+
from vllm import LLM, SamplingParams
|
| 121 |
+
|
| 122 |
+
print(f"加载模型: {model_path}")
|
| 123 |
+
llm = load_model(model_path)
|
| 124 |
+
|
| 125 |
+
sampling_params = SamplingParams(
|
| 126 |
+
temperature=0.7,
|
| 127 |
+
top_p=0.9,
|
| 128 |
+
max_tokens=512,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
print("\n=== Qwen3.5-0.8B-HOS 交互模式 ===")
|
| 132 |
+
print("输入 'quit' 或 'exit' 退出\n")
|
| 133 |
+
|
| 134 |
+
while True:
|
| 135 |
+
try:
|
| 136 |
+
user_input = input("用户: ").strip()
|
| 137 |
+
if user_input.lower() in ("quit", "exit", "q"):
|
| 138 |
+
break
|
| 139 |
+
if not user_input:
|
| 140 |
+
continue
|
| 141 |
+
|
| 142 |
+
# 构造 chat template
|
| 143 |
+
prompt = f"<|im_start|>user\n{user_input}<|im_end|>\n<|im_start|>assistant\n"
|
| 144 |
+
|
| 145 |
+
start = time.time()
|
| 146 |
+
outputs = llm.generate([prompt], sampling_params)
|
| 147 |
+
elapsed = time.time() - start
|
| 148 |
+
|
| 149 |
+
response = outputs[0].outputs[0].text
|
| 150 |
+
token_count = len(outputs[0].outputs[0].token_ids)
|
| 151 |
+
|
| 152 |
+
print(f"\n助手: {response}")
|
| 153 |
+
print(f"[{token_count} tokens, {elapsed:.2f}s, {token_count/elapsed:.1f} tokens/s]\n")
|
| 154 |
+
|
| 155 |
+
except KeyboardInterrupt:
|
| 156 |
+
break
|
| 157 |
+
|
| 158 |
+
print("\n再见!")
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
parser = argparse.ArgumentParser(description="Qwen3.5-0.8B-HOS vLLM 推理服务")
|
| 163 |
+
parser.add_argument("--model", type=str, default="lxcxjxhx/Qwen3.5-0.8B-HOS",
|
| 164 |
+
help="模型路径或 HF 仓库 ID")
|
| 165 |
+
parser.add_argument("--dtype", type=str, default="bfloat16",
|
| 166 |
+
choices=["bfloat16", "float16", "float32"],
|
| 167 |
+
help="推理精度")
|
| 168 |
+
parser.add_argument("--serve", action="store_true", help="启动 API 服务")
|
| 169 |
+
parser.add_argument("--test", action="store_true", help="运行推理测试")
|
| 170 |
+
parser.add_argument("--chat", action="store_true", help="命令行交互模式")
|
| 171 |
+
parser.add_argument("--host", type=str, default="0.0.0.0", help="服务监听地址")
|
| 172 |
+
parser.add_argument("--port", type=int, default=8000, help="服务端口")
|
| 173 |
+
|
| 174 |
+
args = parser.parse_args()
|
| 175 |
+
|
| 176 |
+
if args.serve:
|
| 177 |
+
serve(args.model, args.host, args.port)
|
| 178 |
+
elif args.test:
|
| 179 |
+
test_inference(args.model)
|
| 180 |
+
elif args.chat:
|
| 181 |
+
chat_mode(args.model)
|
| 182 |
+
else:
|
| 183 |
+
# 默认启动服务
|
| 184 |
+
serve(args.model, args.host, args.port)
|
model.safetensors-00001-of-00001.safetensors
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:c2b1e5a17d9c1e27685d92ed9b382911ebb99955ecd89052d1721241adfbab6c
|
| 3 |
-
size 1746942600
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Qwen3.5-0.8B-HOS 训练依赖
|
| 2 |
+
# 适用于 LLaMA-Factory QLoRA 训练流程
|
| 3 |
+
|
| 4 |
+
# 核心依赖
|
| 5 |
+
transformers>=4.40.0
|
| 6 |
+
tokenizers>=0.19.0
|
| 7 |
+
accelerate>=0.29.0
|
| 8 |
+
peft>=0.10.0
|
| 9 |
+
bitsandbytes>=0.43.0
|
| 10 |
+
|
| 11 |
+
# 数据处理
|
| 12 |
+
datasets>=2.18.0
|
| 13 |
+
huggingface-hub>=0.22.0
|
| 14 |
+
|
| 15 |
+
# 去重和清洗
|
| 16 |
+
datasketch>=1.6.5
|
| 17 |
+
simhash>=2.1.2
|
| 18 |
+
|
| 19 |
+
# 训练优化
|
| 20 |
+
einops>=0.7.0
|
| 21 |
+
scipy>=1.13.0
|
| 22 |
+
|
| 23 |
+
# 推理加速
|
| 24 |
+
vllm>=0.4.0
|
| 25 |
+
|
| 26 |
+
# 工具库
|
| 27 |
+
tqdm>=4.66.0
|
| 28 |
+
rich>=13.7.0
|
| 29 |
+
|
| 30 |
+
# 日志和监控
|
| 31 |
+
tensorboard>=2.16.0
|
run.sh
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# 主入口脚本 - 串联所有训练步骤
|
| 3 |
+
# 适用于 Qwen3.5-0.8B-HOS 训练流程
|
| 4 |
+
|
| 5 |
+
set -e
|
| 6 |
+
|
| 7 |
+
echo "============================================"
|
| 8 |
+
echo " HOS Security Model Training"
|
| 9 |
+
echo " Qwen3.5-0.8B-HOS"
|
| 10 |
+
echo "============================================"
|
| 11 |
+
|
| 12 |
+
# 解析参数
|
| 13 |
+
NO_PROXY=false
|
| 14 |
+
for arg in "$@"; do
|
| 15 |
+
case $arg in
|
| 16 |
+
--no-proxy)
|
| 17 |
+
NO_PROXY=true
|
| 18 |
+
shift
|
| 19 |
+
;;
|
| 20 |
+
esac
|
| 21 |
+
done
|
| 22 |
+
|
| 23 |
+
# Step 1: 代理配置(可选)
|
| 24 |
+
if [ "$NO_PROXY" = false ]; then
|
| 25 |
+
echo "[1/5] 配置网络代理..."
|
| 26 |
+
source scripts/setup_proxy.sh
|
| 27 |
+
else
|
| 28 |
+
echo "[1/5] 跳过代理配置"
|
| 29 |
+
fi
|
| 30 |
+
|
| 31 |
+
# Step 2: 安装依赖
|
| 32 |
+
echo "[2/5] 安装环境依赖..."
|
| 33 |
+
bash scripts/prepare_env.sh
|
| 34 |
+
|
| 35 |
+
# Step 3: 下载并清洗数据
|
| 36 |
+
echo "[3/5] 准备数据集..."
|
| 37 |
+
bash scripts/prepare_data.sh
|
| 38 |
+
|
| 39 |
+
# Step 4: 训练
|
| 40 |
+
echo "[4/5] 启动训练..."
|
| 41 |
+
bash scripts/train.sh
|
| 42 |
+
|
| 43 |
+
# Step 5: 上传
|
| 44 |
+
echo "[5/5] 上传模型..."
|
| 45 |
+
bash scripts/upload.sh
|
| 46 |
+
|
| 47 |
+
echo "============================================"
|
| 48 |
+
echo " 训练完成!"
|
| 49 |
+
echo " 模型: https://huggingface.co/lxcxjxhx/Qwen3.5-0.8B-HOS"
|
| 50 |
+
echo "============================================"
|
scripts/clean_data.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
数据清洗脚本 - 用于清洗和预处理网络安全训练数据
|
| 5 |
+
适用于 Qwen3.5-0.8B-HOS 训练流程
|
| 6 |
+
|
| 7 |
+
功能:
|
| 8 |
+
1. 去重(MinHash + SimHash)
|
| 9 |
+
2. 去噪(移除低质量数据)
|
| 10 |
+
3. 格式标准化(统一为 Alpaca 格式)
|
| 11 |
+
4. 隐私脱敏(移除敏感信息)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
import os
|
| 16 |
+
import re
|
| 17 |
+
import hashlib
|
| 18 |
+
from typing import Dict, List, Optional
|
| 19 |
+
from datasketch import MinHash, MinHashLSH
|
| 20 |
+
from simhash import Simhash
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# 配置
|
| 24 |
+
DATA_DIR = "datasets"
|
| 25 |
+
OUTPUT_DIR = "datasets/cleaned"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def compute_minhash(text: str, num_perm: int = 128) -> MinHash:
|
| 29 |
+
"""计算文本的 MinHash 签名"""
|
| 30 |
+
m = MinHash(num_perm=num_perm)
|
| 31 |
+
for word in text.split():
|
| 32 |
+
m.update(word.encode('utf-8'))
|
| 33 |
+
return m
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def compute_simhash(text: str, hashbits: int = 64) -> Simhash:
|
| 37 |
+
"""计算文本的 SimHash 指纹"""
|
| 38 |
+
return Simhash(text, hashbits=hashbits)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def deduplicate_minhash(data: List[Dict], threshold: float = 0.8) -> List[Dict]:
|
| 42 |
+
"""使用 MinHash 进行去重"""
|
| 43 |
+
print(f" MinHash 去重 (阈值: {threshold})...")
|
| 44 |
+
lsh = MinHashLSH(threshold=threshold, num_perm=128)
|
| 45 |
+
|
| 46 |
+
unique_data = []
|
| 47 |
+
duplicates = 0
|
| 48 |
+
|
| 49 |
+
for idx, item in enumerate(data):
|
| 50 |
+
text = f"{item.get('instruction', '')} {item.get('input', '')} {item.get('output', '')}"
|
| 51 |
+
m = compute_minhash(text)
|
| 52 |
+
|
| 53 |
+
key = f"doc_{idx}"
|
| 54 |
+
if not lsh.query(m):
|
| 55 |
+
lsh.insert(key, m)
|
| 56 |
+
unique_data.append(item)
|
| 57 |
+
else:
|
| 58 |
+
duplicates += 1
|
| 59 |
+
|
| 60 |
+
print(f" 原始: {len(data)}, 去重后: {len(unique_data)}, 移除: {duplicates}")
|
| 61 |
+
return unique_data
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def deduplicate_simhash(data: List[Dict], threshold: int = 3) -> List[Dict]:
|
| 65 |
+
"""使用 SimHash 进行去重"""
|
| 66 |
+
print(f" SimHash 去重 (汉明距离阈值: {threshold})...")
|
| 67 |
+
|
| 68 |
+
unique_data = []
|
| 69 |
+
seen_hashes = []
|
| 70 |
+
duplicates = 0
|
| 71 |
+
|
| 72 |
+
for item in data:
|
| 73 |
+
text = f"{item.get('instruction', '')} {item.get('input', '')} {item.get('output', '')}"
|
| 74 |
+
sh = compute_simhash(text)
|
| 75 |
+
|
| 76 |
+
is_duplicate = False
|
| 77 |
+
for seen_sh in seen_hashes:
|
| 78 |
+
if sh.distance(seen_sh) <= threshold:
|
| 79 |
+
is_duplicate = True
|
| 80 |
+
duplicates += 1
|
| 81 |
+
break
|
| 82 |
+
|
| 83 |
+
if not is_duplicate:
|
| 84 |
+
seen_hashes.append(sh)
|
| 85 |
+
unique_data.append(item)
|
| 86 |
+
|
| 87 |
+
print(f" 原始: {len(data)}, 去重后: {len(unique_data)}, 移除: {duplicates}")
|
| 88 |
+
return unique_data
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def remove_noise(data: List[Dict]) -> List[Dict]:
|
| 92 |
+
"""去噪 - 移除低质量数据"""
|
| 93 |
+
print(" 去噪处理...")
|
| 94 |
+
|
| 95 |
+
clean_data = []
|
| 96 |
+
removed = 0
|
| 97 |
+
|
| 98 |
+
for item in data:
|
| 99 |
+
instruction = item.get('instruction', '')
|
| 100 |
+
input_text = item.get('input', '')
|
| 101 |
+
output = item.get('output', '')
|
| 102 |
+
|
| 103 |
+
# 过滤条件
|
| 104 |
+
# 1. 移除空内容
|
| 105 |
+
if not instruction or not output:
|
| 106 |
+
removed += 1
|
| 107 |
+
continue
|
| 108 |
+
|
| 109 |
+
# 2. 移除过短的内容
|
| 110 |
+
if len(output.strip()) < 10:
|
| 111 |
+
removed += 1
|
| 112 |
+
continue
|
| 113 |
+
|
| 114 |
+
# 3. 移除明显无关的内容
|
| 115 |
+
noise_patterns = [
|
| 116 |
+
r'^\s*$', # 纯空白
|
| 117 |
+
r'^(test|测试|demo|示例)\s*$', # 测试内容
|
| 118 |
+
r'^(TODO|FIXME|HACK)', # 代码注释
|
| 119 |
+
]
|
| 120 |
+
|
| 121 |
+
is_noise = False
|
| 122 |
+
for pattern in noise_patterns:
|
| 123 |
+
if re.search(pattern, output, re.IGNORECASE):
|
| 124 |
+
is_noise = True
|
| 125 |
+
break
|
| 126 |
+
|
| 127 |
+
if is_noise:
|
| 128 |
+
removed += 1
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
clean_data.append(item)
|
| 132 |
+
|
| 133 |
+
print(f" 原始: {len(data)}, 去噪后: {len(clean_data)}, 移除: {removed}")
|
| 134 |
+
return clean_data
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def standardize_format(data: List[Dict]) -> List[Dict]:
|
| 138 |
+
"""格式标准化 - 统一为 Alpaca 格式"""
|
| 139 |
+
print(" 格式标准化...")
|
| 140 |
+
|
| 141 |
+
standardized = []
|
| 142 |
+
|
| 143 |
+
for item in data:
|
| 144 |
+
# 确保必要的字段存在
|
| 145 |
+
standardized_item = {
|
| 146 |
+
'instruction': item.get('instruction', '').strip(),
|
| 147 |
+
'input': item.get('input', '').strip(),
|
| 148 |
+
'output': item.get('output', '').strip()
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
# 清理多余空白
|
| 152 |
+
for key in standardized_item:
|
| 153 |
+
standardized_item[key] = re.sub(r'\s+', ' ', standardized_item[key]).strip()
|
| 154 |
+
|
| 155 |
+
standardized.append(standardized_item)
|
| 156 |
+
|
| 157 |
+
print(f" 标准化完成: {len(standardized)} 条记录")
|
| 158 |
+
return standardized
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def desensitize(data: List[Dict]) -> List[Dict]:
|
| 162 |
+
"""隐私脱敏 - 移除敏感信息"""
|
| 163 |
+
print(" 隐私脱敏...")
|
| 164 |
+
|
| 165 |
+
desensitized = []
|
| 166 |
+
|
| 167 |
+
# 敏感信息正则表达式
|
| 168 |
+
sensitive_patterns = {
|
| 169 |
+
'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
|
| 170 |
+
'phone': r'\b1[3-9]\d{9}\b', # 中国大陆手机号
|
| 171 |
+
'id_card': r'\b\d{17}[\dXx]\b', # 身份证号
|
| 172 |
+
'ip': r'\b(?:\d{1,3}\.){3}\d{1,3}\b', # IP 地址
|
| 173 |
+
'password': r'(password|密码|pwd)\s*[:=]\s*\S+', # 密码
|
| 174 |
+
'api_key': r'(api[_-]?key|apikey)\s*[:=]\s*[\w-]+', # API Key
|
| 175 |
+
'token': r'(token|access[_-]?token)\s*[:=]\s*[\w.-]+', # Token
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
for item in data:
|
| 179 |
+
new_item = item.copy()
|
| 180 |
+
|
| 181 |
+
for key in ['instruction', 'input', 'output']:
|
| 182 |
+
text = new_item.get(key, '')
|
| 183 |
+
|
| 184 |
+
# 替换敏感信息
|
| 185 |
+
for pattern_name, pattern in sensitive_patterns.items():
|
| 186 |
+
if pattern_name in ['email', 'phone', 'id_card']:
|
| 187 |
+
text = re.sub(pattern, f'<{pattern_name.upper()}>', text)
|
| 188 |
+
elif pattern_name in ['password', 'api_key', 'token']:
|
| 189 |
+
text = re.sub(pattern, f'<{pattern_name.upper()}>', text, flags=re.IGNORECASE)
|
| 190 |
+
|
| 191 |
+
new_item[key] = text
|
| 192 |
+
|
| 193 |
+
desensitized.append(new_item)
|
| 194 |
+
|
| 195 |
+
print(f" 脱敏完成: {len(desensitized)} 条记录")
|
| 196 |
+
return desensitized
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def process_dataset(input_file: str, output_file: str):
|
| 200 |
+
"""处理单个数据集文件"""
|
| 201 |
+
print(f"\n处理: {input_file}")
|
| 202 |
+
|
| 203 |
+
# 读取数据
|
| 204 |
+
data = []
|
| 205 |
+
with open(input_file, 'r', encoding='utf-8') as f:
|
| 206 |
+
for line in f:
|
| 207 |
+
if line.strip():
|
| 208 |
+
data.append(json.loads(line))
|
| 209 |
+
|
| 210 |
+
print(f" 原始数据: {len(data)} 条")
|
| 211 |
+
|
| 212 |
+
# 1. 去重 (MinHash)
|
| 213 |
+
data = deduplicate_minhash(data, threshold=0.8)
|
| 214 |
+
|
| 215 |
+
# 2. 去重 (SimHash)
|
| 216 |
+
data = deduplicate_simhash(data, threshold=3)
|
| 217 |
+
|
| 218 |
+
# 3. 去噪
|
| 219 |
+
data = remove_noise(data)
|
| 220 |
+
|
| 221 |
+
# 4. 格式标准化
|
| 222 |
+
data = standardize_format(data)
|
| 223 |
+
|
| 224 |
+
# 5. 隐私脱敏
|
| 225 |
+
data = desensitize(data)
|
| 226 |
+
|
| 227 |
+
# 保存结果
|
| 228 |
+
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
| 229 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
| 230 |
+
for item in data:
|
| 231 |
+
f.write(json.dumps(item, ensure_ascii=False) + '\n')
|
| 232 |
+
|
| 233 |
+
print(f" 处理后数据: {len(data)} 条")
|
| 234 |
+
print(f" 保存至: {output_file}")
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def main():
|
| 238 |
+
"""主函数"""
|
| 239 |
+
print("============================================")
|
| 240 |
+
print(" 数据清洗脚本")
|
| 241 |
+
print(" Qwen3.5-0.8B-HOS")
|
| 242 |
+
print("============================================")
|
| 243 |
+
|
| 244 |
+
# 创建输出目录
|
| 245 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 246 |
+
|
| 247 |
+
# 处理所有数据集
|
| 248 |
+
datasets = [
|
| 249 |
+
'cybersecurity_hq.jsonl',
|
| 250 |
+
'cybersecurity_sharegpt.jsonl',
|
| 251 |
+
'cnvd_vulnerability.jsonl',
|
| 252 |
+
'merged_cybersec.jsonl'
|
| 253 |
+
]
|
| 254 |
+
|
| 255 |
+
for dataset in datasets:
|
| 256 |
+
input_file = os.path.join(DATA_DIR, dataset)
|
| 257 |
+
output_file = os.path.join(OUTPUT_DIR, dataset)
|
| 258 |
+
|
| 259 |
+
if os.path.exists(input_file):
|
| 260 |
+
process_dataset(input_file, output_file)
|
| 261 |
+
else:
|
| 262 |
+
print(f"\n跳过: {input_file} (文件不存在)")
|
| 263 |
+
|
| 264 |
+
print("\n============================================")
|
| 265 |
+
print(" 数据清洗完成!")
|
| 266 |
+
print(f" 输出目录: {OUTPUT_DIR}")
|
| 267 |
+
print("============================================")
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
if __name__ == "__main__":
|
| 271 |
+
main()
|
scripts/prepare_data.sh
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# 数据下载与清洗脚本
|
| 3 |
+
# 适用于 Qwen3.5-0.8B-HOS 训练流程
|
| 4 |
+
|
| 5 |
+
set -e
|
| 6 |
+
|
| 7 |
+
echo "============================================"
|
| 8 |
+
echo " 准备训练数据集"
|
| 9 |
+
echo "============================================"
|
| 10 |
+
|
| 11 |
+
# 数据集目录
|
| 12 |
+
DATA_DIR="datasets"
|
| 13 |
+
mkdir -p "$DATA_DIR"
|
| 14 |
+
|
| 15 |
+
# 从 HuggingFace 下载数据集
|
| 16 |
+
echo "[1/4] 下载数据集..."
|
| 17 |
+
python -c "
|
| 18 |
+
from datasets import load_dataset
|
| 19 |
+
from huggingface_hub import hf_hub_download
|
| 20 |
+
import os
|
| 21 |
+
|
| 22 |
+
# 下载 infosec-dataset-training 数据集
|
| 23 |
+
print('下载 cybersecurity_hq...')
|
| 24 |
+
ds = load_dataset('lxcxjxhx/infosec-dataset-training', 'cybersecurity_hq', trust_remote_code=True)
|
| 25 |
+
ds['train'].to_json(os.path.join('$DATA_DIR', 'cybersecurity_hq.jsonl'), orient='records', lines=True, force_ascii=False)
|
| 26 |
+
|
| 27 |
+
print('下载 cybersecurity_sharegpt...')
|
| 28 |
+
ds = load_dataset('lxcxjxhx/infosec-dataset-training', 'cybersecurity_sharegpt', trust_remote_code=True)
|
| 29 |
+
ds['train'].to_json(os.path.join('$DATA_DIR', 'cybersecurity_sharegpt.jsonl'), orient='records', lines=True, force_ascii=False)
|
| 30 |
+
|
| 31 |
+
print('下载 cnvd_vulnerability...')
|
| 32 |
+
ds = load_dataset('lxcxjxhx/infosec-dataset-training', 'cnvd_vulnerability', trust_remote_code=True)
|
| 33 |
+
ds['train'].to_json(os.path.join('$DATA_DIR', 'cnvd_vulnerability.jsonl'), orient='records', lines=True, force_ascii=False)
|
| 34 |
+
|
| 35 |
+
print('下载 merged_cybersec...')
|
| 36 |
+
ds = load_dataset('lxcxjxhx/infosec-dataset-training', 'merged_cybersec', trust_remote_code=True)
|
| 37 |
+
ds['train'].to_json(os.path.join('$DATA_DIR', 'merged_cybersec.jsonl'), orient='records', lines=True, force_ascii=False)
|
| 38 |
+
|
| 39 |
+
print('数据集下载完成!')
|
| 40 |
+
"
|
| 41 |
+
|
| 42 |
+
# 数据清洗
|
| 43 |
+
echo "[2/4] 数据清洗..."
|
| 44 |
+
python scripts/clean_data.py
|
| 45 |
+
|
| 46 |
+
# 验证数据集
|
| 47 |
+
echo "[3/4] 验证数据集..."
|
| 48 |
+
python -c "
|
| 49 |
+
import json
|
| 50 |
+
import os
|
| 51 |
+
|
| 52 |
+
data_dir = '$DATA_DIR'
|
| 53 |
+
files = ['cybersecurity_hq.jsonl', 'cybersecurity_sharegpt.jsonl', 'cnvd_vulnerability.jsonl', 'merged_cybersec.jsonl']
|
| 54 |
+
|
| 55 |
+
for f in files:
|
| 56 |
+
path = os.path.join(data_dir, f)
|
| 57 |
+
if os.path.exists(path):
|
| 58 |
+
with open(path, 'r', encoding='utf-8') as file:
|
| 59 |
+
count = sum(1 for _ in file)
|
| 60 |
+
print(f'{f}: {count} 条记录')
|
| 61 |
+
else:
|
| 62 |
+
print(f'{f}: 文件不存在!')
|
| 63 |
+
"
|
| 64 |
+
|
| 65 |
+
# 复制 dataset_info.json
|
| 66 |
+
echo "[4/4] 配置数据集信息..."
|
| 67 |
+
cp datasets/dataset_info.json "$DATA_DIR/dataset_info.json" 2>/dev/null || echo "dataset_info.json 已存在"
|
| 68 |
+
|
| 69 |
+
echo "============================================"
|
| 70 |
+
echo " 数据集准备完成!"
|
| 71 |
+
echo "============================================"
|
scripts/prepare_env.sh
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# 环境安装脚本 - 安装训练所需的所有依赖
|
| 3 |
+
# 适用于 Qwen3.5-0.8B-HOS 训练流程
|
| 4 |
+
|
| 5 |
+
set -e
|
| 6 |
+
|
| 7 |
+
echo "============================================"
|
| 8 |
+
echo " 安装训练环境依赖"
|
| 9 |
+
echo "============================================"
|
| 10 |
+
|
| 11 |
+
# 检查 Python 版本
|
| 12 |
+
echo "[1/6] 检查 Python 版本..."
|
| 13 |
+
python --version
|
| 14 |
+
|
| 15 |
+
# 升级 pip
|
| 16 |
+
echo "[2/6] 升级 pip..."
|
| 17 |
+
python -m pip install --upgrade pip
|
| 18 |
+
|
| 19 |
+
# 安装 PyTorch (CUDA 12.1)
|
| 20 |
+
echo "[3/6] 安装 PyTorch..."
|
| 21 |
+
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
|
| 22 |
+
|
| 23 |
+
# 安装 LLaMA-Factory
|
| 24 |
+
echo "[4/6] 安装 LLaMA-Factory..."
|
| 25 |
+
pip install git+https://github.com/hiyouga/LLaMA-Factory.git
|
| 26 |
+
|
| 27 |
+
# 安装 Unsloth (加速训练)
|
| 28 |
+
echo "[5/6] 安装 Unsloth..."
|
| 29 |
+
pip install unsloth
|
| 30 |
+
|
| 31 |
+
# 安装 vLLM (推理加速)
|
| 32 |
+
echo "[6/6] 安装 vLLM..."
|
| 33 |
+
pip install vllm
|
| 34 |
+
|
| 35 |
+
# 安装其他依赖
|
| 36 |
+
echo "[7/7] 安装其他依赖..."
|
| 37 |
+
pip install -r requirements.txt
|
| 38 |
+
|
| 39 |
+
# 验证安装
|
| 40 |
+
echo "============================================"
|
| 41 |
+
echo " 验证安装..."
|
| 42 |
+
echo "============================================"
|
| 43 |
+
python -c "import torch; print(f'PyTorch: {torch.__version__}')"
|
| 44 |
+
python -c "import torch; print(f'CUDA Available: {torch.cuda.is_available()}')"
|
| 45 |
+
python -c "import torch; print(f'CUDA Version: {torch.version.cuda}')"
|
| 46 |
+
python -c "import transformers; print(f'Transformers: {transformers.__version__}')"
|
| 47 |
+
|
| 48 |
+
echo "============================================"
|
| 49 |
+
echo " 环境安装完成!"
|
| 50 |
+
echo "============================================"
|
scripts/setup_proxy.sh
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# 代理配置脚本 - 配置网络代理以访问外部资源
|
| 3 |
+
# 适用于 Qwen3.5-0.8B-HOS 训练流程
|
| 4 |
+
|
| 5 |
+
echo "============================================"
|
| 6 |
+
echo " 配置网络代理 (端口 7897)"
|
| 7 |
+
echo "============================================"
|
| 8 |
+
|
| 9 |
+
# 设置代理环境变量
|
| 10 |
+
export http_proxy=http://127.0.0.1:7897
|
| 11 |
+
export https_proxy=http://127.0.0.1:7897
|
| 12 |
+
export HTTP_PROXY=http://127.0.0.1:7897
|
| 13 |
+
export HTTPS_PROXY=http://127.0.0.1:7897
|
| 14 |
+
|
| 15 |
+
# Git 代理配置
|
| 16 |
+
git config --global http.proxy http://127.0.0.1:7897
|
| 17 |
+
git config --global https.proxy http://127.0.0.1:7897
|
| 18 |
+
|
| 19 |
+
# HuggingFace 代理配置
|
| 20 |
+
export HF_HUB_ENABLE_HF_TRANSFER=1
|
| 21 |
+
|
| 22 |
+
echo "代理配置完成:"
|
| 23 |
+
echo " HTTP_PROXY: $http_proxy"
|
| 24 |
+
echo " HTTPS_PROXY: $https_proxy"
|
| 25 |
+
echo "============================================"
|
scripts/train.sh
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# 训练启动脚本 - 使用 LLaMA-Factory 进行 QLoRA 训练
|
| 3 |
+
# 适用于 Qwen3.5-0.8B-HOS 训练流程
|
| 4 |
+
|
| 5 |
+
set -e
|
| 6 |
+
|
| 7 |
+
echo "============================================"
|
| 8 |
+
echo " 启动 QLoRA 训练"
|
| 9 |
+
echo " 模型: Qwen3.5-0.8B-HOS"
|
| 10 |
+
echo "============================================"
|
| 11 |
+
|
| 12 |
+
# 检查配置文件
|
| 13 |
+
CONFIG_FILE="configs/train.yaml"
|
| 14 |
+
if [ ! -f "$CONFIG_FILE" ]; then
|
| 15 |
+
echo "错误: 配置文件不存在: $CONFIG_FILE"
|
| 16 |
+
exit 1
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
# 检查数据集
|
| 20 |
+
DATASET_DIR="datasets"
|
| 21 |
+
if [ ! -d "$DATASET_DIR" ]; then
|
| 22 |
+
echo "错误: 数据集目录不存在: $DATASET_DIR"
|
| 23 |
+
exit 1
|
| 24 |
+
fi
|
| 25 |
+
|
| 26 |
+
# 创建输出目录
|
| 27 |
+
OUTPUT_DIR="outputs/qwen35-0.8b-cybersec-qlora"
|
| 28 |
+
mkdir -p "$OUTPUT_DIR"
|
| 29 |
+
|
| 30 |
+
# 显示训练配置
|
| 31 |
+
echo "训练配置:"
|
| 32 |
+
echo " 配置文件: $CONFIG_FILE"
|
| 33 |
+
echo " 数据集目录: $DATASET_DIR"
|
| 34 |
+
echo " 输出目录: $OUTPUT_DIR"
|
| 35 |
+
echo ""
|
| 36 |
+
|
| 37 |
+
# 显示系统信息
|
| 38 |
+
echo "系统信息:"
|
| 39 |
+
python -c "import torch; print(f' PyTorch: {torch.__version__}')"
|
| 40 |
+
python -c "import torch; print(f' CUDA Available: {torch.cuda.is_available()}')"
|
| 41 |
+
if python -c "import torch; exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null; then
|
| 42 |
+
python -c "import torch; print(f' GPU: {torch.cuda.get_device_name(0)}')"
|
| 43 |
+
python -c "import torch; print(f' GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB')"
|
| 44 |
+
fi
|
| 45 |
+
echo ""
|
| 46 |
+
|
| 47 |
+
# 开始训练
|
| 48 |
+
echo "开始训练..."
|
| 49 |
+
echo "============================================"
|
| 50 |
+
|
| 51 |
+
llamafactory-cli train "$CONFIG_FILE"
|
| 52 |
+
|
| 53 |
+
echo "============================================"
|
| 54 |
+
echo " 训练完成!"
|
| 55 |
+
echo " 模型输出: $OUTPUT_DIR"
|
| 56 |
+
echo "============================================"
|
scripts/upload.sh
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# 上传脚本 - 合并 adapter 并上传到 HuggingFace Hub
|
| 3 |
+
# 适用于 Qwen3.5-0.8B-HOS 训练流程
|
| 4 |
+
|
| 5 |
+
set -e
|
| 6 |
+
|
| 7 |
+
echo "============================================"
|
| 8 |
+
echo " 上传模型到 HuggingFace Hub"
|
| 9 |
+
echo " 目标: lxcxjxhx/Qwen3.5-0.8B-HOS"
|
| 10 |
+
echo "============================================"
|
| 11 |
+
|
| 12 |
+
# 配置
|
| 13 |
+
OUTPUT_DIR="outputs/qwen35-0.8b-cybersec-qlora"
|
| 14 |
+
MERGED_DIR="outputs/qwen35-0.8b-merged"
|
| 15 |
+
HUB_MODEL_ID="lxcxjxhx/Qwen3.5-0.8B-HOS"
|
| 16 |
+
|
| 17 |
+
# 检查训练输出
|
| 18 |
+
if [ ! -d "$OUTPUT_DIR" ]; then
|
| 19 |
+
echo "错误: 训练输出目录不存在: $OUTPUT_DIR"
|
| 20 |
+
exit 1
|
| 21 |
+
fi
|
| 22 |
+
|
| 23 |
+
# Step 1: 合并 adapter
|
| 24 |
+
echo "[1/3] 合并 LoRA adapter..."
|
| 25 |
+
python -c "
|
| 26 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 27 |
+
from peft import PeftModel
|
| 28 |
+
import torch
|
| 29 |
+
|
| 30 |
+
print('加载基础模型...')
|
| 31 |
+
base_model = AutoModelForCausalLM.from_pretrained(
|
| 32 |
+
'lxcxjxhx/Qwen3.5-0.8B-HOS',
|
| 33 |
+
torch_dtype=torch.bfloat16,
|
| 34 |
+
trust_remote_code=True
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
print('加载 LoRA adapter...')
|
| 38 |
+
model = PeftModel.from_pretrained(base_model, '$OUTPUT_DIR')
|
| 39 |
+
|
| 40 |
+
print('合并模型...')
|
| 41 |
+
model = model.merge_and_unload()
|
| 42 |
+
|
| 43 |
+
print('保存合并后的模型...')
|
| 44 |
+
model.save_pretrained('$MERGED_DIR')
|
| 45 |
+
|
| 46 |
+
print('保存 tokenizer...')
|
| 47 |
+
tokenizer = AutoTokenizer.from_pretrained('lxcxjxhx/Qwen3.5-0.8B-HOS', trust_remote_code=True)
|
| 48 |
+
tokenizer.save_pretrained('$MERGED_DIR')
|
| 49 |
+
|
| 50 |
+
print('合并完成!')
|
| 51 |
+
"
|
| 52 |
+
|
| 53 |
+
# Step 2: 推送到 HuggingFace Hub
|
| 54 |
+
echo "[2/3] 推送到 HuggingFace Hub..."
|
| 55 |
+
python -c "
|
| 56 |
+
from huggingface_hub import HfApi
|
| 57 |
+
|
| 58 |
+
api = HfApi()
|
| 59 |
+
|
| 60 |
+
print('上传模型到: $HUB_MODEL_ID')
|
| 61 |
+
api.upload_folder(
|
| 62 |
+
folder_path='$MERGED_DIR',
|
| 63 |
+
repo_id='$HUB_MODEL_ID',
|
| 64 |
+
repo_type='model',
|
| 65 |
+
commit_message='Upload Qwen3.5-0.8B-HOS merged model'
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
print('上传完成!')
|
| 69 |
+
"
|
| 70 |
+
|
| 71 |
+
# Step 3: 验证上传
|
| 72 |
+
echo "[3/3] 验证上传..."
|
| 73 |
+
python -c "
|
| 74 |
+
from huggingface_hub import model_info
|
| 75 |
+
|
| 76 |
+
info = model_info('$HUB_MODEL_ID')
|
| 77 |
+
print(f'模型: {info.modelId}')
|
| 78 |
+
print(f'最后更新: {info.lastModified}')
|
| 79 |
+
print(f'标签: {info.tags}')
|
| 80 |
+
print('验证完成!')
|
| 81 |
+
"
|
| 82 |
+
|
| 83 |
+
echo "============================================"
|
| 84 |
+
echo " 上传完成!"
|
| 85 |
+
echo " 模型地址: https://huggingface.co/$HUB_MODEL_ID"
|
| 86 |
+
echo "============================================"
|