1. 项目概述:纯Java环境下的Qwen 3.5大模型部署方案
作为一名长期深耕Java生态的开发者,每当需要在生产环境部署AI模型时,总会面临Python依赖的困扰。传统方案要求我们配置复杂的Python环境、处理版本冲突,甚至需要为不同模型维护多个虚拟环境。这种状况直到我发现DJL(Deep Java Library)才彻底改变——它让Java开发者能够用纯Java代码直接运行像Qwen 3.5这样的大语言模型,完全摆脱Python依赖。
这个方案的核心价值在于:
- 环境纯净:只需标准的Java开发环境(JDK+Maven),无需安装Python或任何科学计算库
- 部署简单:最终产物是标准的JAR包,可以直接集成到现有Java应用中
- 性能可控:通过ONNX Runtime的优化,在CPU/GPU上都能获得不错的推理速度
- 维护方便:依赖管理完全遵循Java生态的标准实践
2. 技术选型解析:为什么选择DJL+ONNX Runtime组合
2.1 DJL框架的架构优势
DJL的设计哲学与Java生态高度契合。它的核心模块djl-api定义了统一的深度学习接口,类似于JDBC在数据库访问中的角色。这意味着:
- 引擎无关性:底层可以切换ONNX Runtime、PyTorch、TensorFlow等引擎,而上层API保持不变
- 自动设备检测:能自动利用GPU加速(如果可用),无需修改代码
- 内存管理:与JVM的GC机制良好配合,避免原生内存泄漏
对于Qwen 3.5这样的Transformer模型,DJL特别优化了以下方面:
- 内置HuggingFace tokenizers的Java实现
- 支持流式生成(token-by-token)
- 提供批处理(batching)接口
2.2 ONNX Runtime的工程价值
选择ONNX Runtime作为后端引擎基于以下考量:
- 跨平台支持:提供完善的Java绑定,且在各平台表现一致
- 量化支持:支持INT8量化,显著降低显存占用(对4B参数的Qwen 3.5尤为关键)
- 执行优化:内置算子融合、内存复用等优化技术
特别值得注意的是,ONNX模型一旦导出就完全独立于训练框架。这意味着:
- 模型文件是自包含的,包含所有计算图结构和参数
- 推理时不需要PyTorch/TensorFlow等原始框架
- 模型权重以优化后的格式存储,加载更快
3. 环境准备与模型获取
3.1 开发环境配置
建议使用以下环境组合:
bash复制JDK 17+ (推荐Amazon Corretto)
Maven 3.8+
IDE任选(IntelliJ IDEA/VSCode等)
对于GPU加速,需要额外准备:
- NVIDIA显卡驱动(470+)
- CUDA Toolkit 11.7+
- cuDNN 8.5+
提示:即使没有GPU,ONNX Runtime的CPU版本也能运行,只是速度较慢
3.2 模型文件准备
Qwen 3.5的ONNX模型可以通过以下方式获取:
-
官方渠道:
- 从HuggingFace下载PyTorch版本后自行转换
- 使用
optimum-cli工具转换:bash复制optimum-cli export onnx --model Qwen/Qwen3.5-4B --task text-generation qwen-onnx/
-
社区预转换:
- 在HuggingFace搜索"Qwen3.5-4B-ONNX"
- 推荐模型:
onnx-community/Qwen3.5-4B-ONNX
关键文件说明:
model.onnx:模型计算图与参数(约8GB)tokenizer.json:分词器配置文件config.json:模型结构配置(可选)
4. 核心实现详解
4.1 项目依赖配置
完整的pom.xml依赖配置如下:
xml复制<properties>
<djl.version>0.27.0</djl.version>
<onnxruntime.version>1.17.0</onnxruntime.version>
</properties>
<dependencies>
<!-- DJL核心 -->
<dependency>
<groupId>ai.djl</groupId>
<artifactId>api</artifactId>
<version>${djl.version}</version>
</dependency>
<!-- ONNX Runtime引擎 -->
<dependency>
<groupId>ai.djl.onnxruntime</groupId>
<artifactId>onnxruntime-engine</artifactId>
<version>${djl.version}</version>
</dependency>
<!-- ONNX原生接口 -->
<dependency>
<groupId>com.microsoft.onnxruntime</groupId>
<artifactId>onnxruntime</artifactId>
<version>${onnxruntime.version}</version>
</dependency>
<!-- 分词器 -->
<dependency>
<groupId>ai.djl.huggingface</groupId>
<artifactId>tokenizers</artifactId>
<version>${djl.version}</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
4.2 模型初始化逻辑
完整的初始化流程包含以下关键步骤:
- 创建ONNX Runtime环境:
java复制OrtEnvironment env = OrtEnvironment.getEnvironment();
OrtSession.SessionOptions options = new OrtSession.SessionOptions();
// 启用所有优化
options.setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT);
// 启用CUDA加速(如果可用)
if(OrtEnvironment.getEnvironment().getAvailableProviders().contains("CUDA")) {
options.addCUDA(0); // 使用第一个GPU
}
// 启用动态量化
options.addConfigEntry("session.dynamic_quantization", "1");
- 加载分词器:
java复制Path tokenizerPath = Paths.get("src/main/resources/qwen-3.5-4b/tokenizer.json");
HuggingFaceTokenizer tokenizer = HuggingFaceTokenizer.newInstance(tokenizerPath);
- 创建推理会话:
java复制Path modelPath = Paths.get("src/main/resources/qwen-3.5-4b/model.onnx");
OrtSession session = env.createSession(modelPath.toString(), options);
4.3 文本生成实现
文本生成的核心流程可分为三个阶段:
- 输入预处理:
java复制String prompt = "<|im_start|>user\nJava如何实现快速排序?<|im_end|>\n<|im_start|>assistant\n";
Encoding encoding = tokenizer.encode(prompt);
long[] inputIds = encoding.getIds();
- 自回归生成:
java复制List<Long> generatedTokens = new ArrayList<>();
for (long id : inputIds) {
generatedTokens.add(id);
}
for (int i = 0; i < MAX_LENGTH; i++) {
// 准备输入张量
long[] currentIds = generatedTokens.stream().mapToLong(l -> l).toArray();
long[][] inputData = {currentIds}; // batch_size=1
// 创建ONNX张量
OnnxTensor inputTensor = OnnxTensor.createTensor(env, inputData);
// 执行推理
OrtSession.Result results = session.run(Collections.singletonMap("input_ids", inputTensor));
// 处理输出logits
float[][][] logits = (float[][][]) results.get(0).getValue();
float[] lastLogits = logits[0][currentIds.length - 1];
// 采样下一个token
int nextToken = sampleWithTemperature(lastLogits, 0.8f);
// 遇到终止符则停止
if (nextToken == tokenizer.encode("<|im_end|>").getIds()[0]) {
break;
}
generatedTokens.add((long) nextToken);
// 释放资源
inputTensor.close();
results.close();
}
- 输出解码:
java复制long[] finalIds = generatedTokens.stream().mapToLong(l -> l).toArray();
String output = tokenizer.decode(finalIds);
4.4 高级生成策略
实现更自然的文本生成需要以下策略:
- 温度采样:
java复制private int sampleWithTemperature(float[] logits, float temperature) {
// 温度缩放
float[] scaledLogits = new float[logits.length];
for (int i = 0; i < logits.length; i++) {
scaledLogits[i] = logits[i] / temperature;
}
// Softmax计算
float maxLogit = Arrays.stream(scaledLogits).max().getAsFloat();
float sum = 0f;
for (int i = 0; i < scaledLogits.length; i++) {
scaledLogits[i] = (float) Math.exp(scaledLogits[i] - maxLogit);
sum += scaledLogits[i];
}
// 随机采样
float rand = new Random().nextFloat() * sum;
float cumsum = 0f;
for (int i = 0; i < scaledLogits.length; i++) {
cumsum += scaledLogits[i];
if (cumsum >= rand) {
return i;
}
}
return 0;
}
- Top-K过滤:
java复制private int sampleTopK(float[] logits, int topK) {
// 创建索引数组
Integer[] indices = new Integer[logits.length];
for (int i = 0; i < indices.length; i++) indices[i] = i;
// 按logit值排序
Arrays.sort(indices, (a, b) -> Float.compare(logits[b], logits[a]));
// 只保留Top-K
float sum = 0f;
float[] filtered = new float[logits.length];
for (int i = 0; i < topK; i++) {
filtered[indices[i]] = logits[indices[i]];
sum += filtered[indices[i]];
}
// 采样
float rand = new Random().nextFloat() * sum;
float cumsum = 0f;
for (int i = 0; i < topK; i++) {
cumsum += filtered[indices[i]];
if (cumsum >= rand) {
return indices[i];
}
}
return indices[0];
}
5. 性能优化技巧
5.1 KV Cache实现
KV Cache可以避免重复计算历史token的注意力,实现方式:
- 修改模型导出:
bash复制optimum-cli export onnx --model Qwen3.5-4B \
--task text-generation-with-past \
--device cuda \
--opset 17 \
qwen-onnx/
- 推理时维护cache:
java复制// 初始化cache
Map<String, OnnxTensor> cache = initKVCache();
// 每次推理传入past_key_values
Map<String, OnnxTensor> inputs = new HashMap<>();
inputs.put("input_ids", inputTensor);
inputs.putAll(cache);
// 获取新的cache并更新
OrtSession.Result results = session.run(inputs);
cache = extractKVCache(results);
5.2 批处理优化
通过批处理提高吞吐量:
java复制// 准备批输入
List<String> prompts = Arrays.asList("prompt1", "prompt2", "prompt3");
List<Encoding> encodings = tokenizer.batchEncode(prompts);
// 创建批输入张量
long[][] batchIds = new long[prompts.size()][];
for (int i = 0; i < encodings.size(); i++) {
batchIds[i] = encodings.get(i).getIds();
}
// 执行批推理
OnnxTensor batchInput = OnnxTensor.createTensor(env, batchIds);
OrtSession.Result batchResults = session.run(
Collections.singletonMap("input_ids", batchInput)
);
5.3 量化压缩
减小模型内存占用的方法:
- 静态量化:
python复制# 在模型转换时执行
quantizer = ORTQuantizer.from_pretrained("qwen-onnx/")
quantizer.quantize(save_dir="qwen-quantized/")
- 动态量化(运行时):
java复制options.addConfigEntry("session.dynamic_quantization", "1");
options.addConfigEntry("session.dynamic_quantization.quant_mode", "QInt8");
6. 生产环境部署建议
6.1 服务化架构
推荐采用以下架构:
code复制客户端 → HTTP服务(Jetty/Spring Boot) → 模型服务池 → ONNX Runtime
关键配置:
- 每个模型实例独占线程
- 使用对象池管理模型实例
- 设置合理的超时时间
6.2 资源监控
需要监控的关键指标:
- 显存使用量
- 推理延迟(P99/P95)
- 吞吐量(RPS)
- 线程池状态
6.3 模型更新
建议采用蓝绿部署:
- 新模型部署到备用目录
- 通过配置切换模型路径
- 旧版本保留回滚能力
7. 常见问题排查
7.1 内存泄漏处理
典型症状:
- 长时间运行后OOM
- 原生内存持续增长
解决方案:
- 确保所有OnnxTensor都正确close()
- 定期重启服务进程
- 使用-XX:MaxDirectMemorySize限制堆外内存
7.2 性能调优
慢速推理的可能原因:
-
未启用GPU加速
- 检查CUDA环境变量
- 确认onnxruntime-gpu包已安装
-
未使用优化选项
java复制
options.setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT); options.setExecutionMode(OrtSession.SessionOptions.ExecutionMode.ORT_SEQUENTIAL);
7.3 生成质量优化
改善生成效果的方法:
- 调整温度参数(0.3~1.0)
- 结合Top-P采样
- 添加重复惩罚(repetition_penalty)
8. 进阶扩展方向
8.1 多模态支持
Qwen 3.5支持图像输入,可通过以下方式扩展:
- 准备视觉编码器ONNX模型
- 实现跨模态注意力融合
- 处理图像预处理(归一化/裁剪)
8.2 函数调用
实现工具使用能力:
- 在prompt中描述工具
- 解析模型输出的JSON
- 执行外部API调用
- 将结果反馈给模型
8.3 微调集成
虽然本文聚焦推理,但DJL也支持训练:
- 使用PyTorch引擎进行LoRA微调
- 导出为ONNX格式
- 在Java环境加载微调后模型
经过实际项目验证,这套Java方案在以下场景表现优异:
- 需要与现有Java系统深度集成的AI功能
- 对Python环境有严格限制的生产环境
- 要求快速启动、稳定运行的批处理任务
对于长期受困于Python环境管理的Java团队,这确实是一条值得尝试的新路径。它不仅简化了部署流程,更重要的是让AI能力真正成为Java应用的自然扩展,而非需要特殊维护的外围组件。