纯 Java 跑大模型!DJL 本地部署 Qwen 3.5,零 Python 依赖
文章目录
无意间发现了一个CSDN大神的人工智能教程,忍不住分享一下给大家。很通俗易懂,重点是还非常风趣幽默,像看小说一样。床送门放这了👉 http://blog.csdn.net/jiangjunshow
前言
[TL;DR]
用 Java 跑大模型不用再装 Python!本文教你用亚马逊开源的 DJL(Deep Java Library)+ ONNX Runtime,纯 Java 代码本地部署 Qwen 3.5。从 Maven 依赖到文本生成代码全给你配好,复制粘贴就能跑,真正实现"一个 jar 包走天下"。
一、先吐槽:Java 程序员跑 AI 的"社死"现场
作为一个写了十几年 Java 的老码农,每次想本地跑个大模型试试效果,那画面简直堪比"大型社死现场"——你得先装 Python,然后 pip install 一堆依赖,接着遇到 conda 环境冲突,最后 TensorFlow 和 PyTorch 还能因为 CUDA 版本不对当场罢工。
最搞笑的是,你只是想用 Java 调个模型推理,结果系统里塞了三个 Python 版本、五个虚拟环境,外加一个快 20GB 的 Anaconda。老板问:"不就跑个 AI 吗?至于把服务器硬盘撑爆吗?"你只能尴尬地摸摸鼻子:“这个…深度学习嘛,懂的都懂…”
但今天,这种情况要彻底翻篇了。我要给你介绍一套"纯 Java 解决方案"——用亚马逊开源的 DJL(Deep Java Library) 配合 ONNX Runtime,让你的 Java 应用零 Python 依赖本地跑 Qwen 3.5。没错,就是一个普通的 Maven 项目,打个 jar 包,随手一跑,大模型就启动了。
这就好比以前你必须请个专职翻译(Python 环境)才能跟外国人(AI 模型)说话,现在 DJL 直接给你塞了个同声传译耳机,Java 代码原生的就能跟模型唠嗑。
二、DJL 是个啥?Java 圈的"AI 瑞士军刀"
DJL 全称 Deep Java Library,是亚马逊在 2020 年开源的深度学习框架。它最牛的地方在于引擎无关——底层可以接 ONNX Runtime、PyTorch、TensorFlow 甚至 MXNet,但上层给你的永远是那套熟悉的 Java API。
想象一下,它就像是 JDBC 对于数据库的关系。你不用关心底层是 MySQL 还是 PostgreSQL,反正都是写 SQL。DJL 也是这样,不管是 BERT 分类还是 Qwen 生成,你的代码结构几乎一模一样。
对于本文的场景,我们要用的是 ONNX Runtime 引擎。原因很实在:Qwen 3.5 官方虽然放出的是 PyTorch 格式,但社区已经提供了现成的 ONNX 导出脚本,转换后的模型可以在完全脱离 Python 的环境下运行。这意味着你的生产环境服务器上,再也不用装那个让人头大的 Python 运行时了。
三、Qwen 3.5:阿里家的"多模态小强"
在动手之前,简单交代一下主角 Qwen 3.5。这是阿里通义千问团队在 2025 年底发布的最新一代模型,从小到大的参数版本都有(0.8B、2B、4B、9B、27B 等)。对于本地部署来说,4B 或者 9B 的版本是性价比最高的选择——既能理解复杂指令,又不会吃掉你全部显存。
关键点是,Qwen 3.5 支持导出为 ONNX 格式。你可以从 HuggingFace 上找到社区转换好的 ONNX 版本(搜索关键词 Qwen3.5-4B-ONNX 之类的),或者自己用 optimum-cli 转换一把。转换过程确实需要 Python,但这是一次性的准备工作。转换完成后,扔给 Java 项目,从此跟 Python 说拜拜。
四、动手开干:从零搭建纯 Java 推理环境
4.1 Maven 依赖:就这几样,别多整
打开你的 pom.xml,把下面这几个依赖怼进去。注意版本号,DJL 建议用 0.27.0 以上,ONNX Runtime 用 1.17.0 以上,对 Qwen 3.5 这种新架构支持更好:
0.27.0
1.17.0
ai.djl.onnxruntime
onnxruntime-engine
${djl.version}
com.microsoft.onnxruntime
onnxruntime
${onnxruntime.version}
ai.djl.huggingface
tokenizers
${djl.version}
org.slf4j
slf4j-simple
2.0.9
看到没?全是 Java 生态的东西,没有一个 python-xxx 的依赖。就这几个 jar 包,加起来不到 100MB,比一个 PyTorch 安装包小多了。
4.2 准备模型文件: tokenizer 和 ONNX 模型
在你的项目 src/main/resources 目录下建个文件夹叫 qwen-3.5-4b,里面放两样东西:
model.onnx:转换好的 Qwen 3.5 ONNX 模型文件(可以去 HuggingFace 搜onnx-community/Qwen3.5-4B-ONNX下载)tokenizer.json:对应的分词器配置文件(同样从 HuggingFace 模型仓库下)
如果你嫌手动下载麻烦,写个 Java 工具类自动拉取也行。但初次上手,建议先手动下载,确认路径没毛病。
4.3 核心代码:三步走战略
现在上主菜。我们写个 QwenInference 类,实现文本生成功能。逻辑分为三步:分词(Tokenizer)-> 推理(Inference)-> 解码(Decoding)。
import ai.djl.huggingface.tokenizers.Encoding;
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import ai.onnxruntime.*;
import java.nio.file.Paths;
import java.util.*;
public class QwenInference {
private static final String MODEL_PATH = "src/main/resources/qwen-3.5-4b/model.onnx";
private static final String TOKENIZER_PATH = "src/main/resources/qwen-3.5-4b/tokenizer.json";
private OrtEnvironment environment;
private OrtSession session;
private HuggingFaceTokenizer tokenizer;
// 生成参数,可调
private static final int MAX_LENGTH = 256;
private static final float TEMPERATURE = 0.8f;
private static final int TOP_K = 40;
public QwenInference() throws Exception {
// 初始化 ONNX Runtime 环境
environment = OrtEnvironment.getEnvironment();
OrtSession.SessionOptions options = new OrtSession.SessionOptions();
options.setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL_OPT);
// 如果有 GPU,取消下面这行注释
// options.addCUDA(0);
session = environment.createSession(MODEL_PATH, options);
// 加载 HuggingFace Tokenizer
tokenizer = HuggingFaceTokenizer.newInstance(Paths.get(TOKENIZER_PATH));
}
/**
* 生成文本的核心方法
* @param prompt 输入提示词
* @return 生成的文本
*/
public String generate(String prompt) throws Exception {
// 1. 分词:把中文/英文变成数字 ID
Encoding encoding = tokenizer.encode(prompt);
long[] inputIds = encoding.getIds();
List generatedTokens = new ArrayList<>();
for (long id : inputIds) {
generatedTokens.add(id);
}
// 2. 自回归生成:一个一个 token 地预测
for (int i = 0; i < MAX_LENGTH; i++) {
// 将当前序列转为数组
long[] currentIds = generatedTokens.stream().mapToLong(Long::longValue).toArray();
// 构建输入张量 [batch_size=1, sequence_length]
long[][] inputData = new long[1][currentIds.length];
System.arraycopy(currentIds, 0, inputData[0], 0, currentIds.length);
OnnxTensor inputTensor = OnnxTensor.createTensor(environment, 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];
// 3. 采样:温度缩放 + Top-K
int nextToken = sample(lastLogits);
// 遇到结束符就停
if (nextToken == tokenizer.encode("<|im_end|>").getIds()[0]) {
break;
}
generatedTokens.add((long) nextToken);
// 资源清理
inputTensor.close();
results.close();
}
// 4. 解码:把数字 ID 转回文字
long[] finalIds = generatedTokens.stream().mapToLong(Long::longValue).toArray();
return tokenizer.decode(finalIds);
}
/**
* Top-K 采样策略,让生成结果更自然
*/
private int sample(float[] logits) {
// 温度缩放
for (int i = 0; i < logits.length; i++) {
logits[i] = logits[i] / TEMPERATURE;
}
// 找 Top-K
Integer[] indices = new Integer[logits.length];
for (int i = 0; i < indices.length; i++) indices[i] = i;
Arrays.sort(indices, (a, b) -> Float.compare(logits[b], logits[a]));
// 只对 Top-K 做 softmax
float sum = 0;
for (int i = 0; i < TOP_K; i++) {
logits[indices[i]] = (float) Math.exp(logits[indices[i]]);
sum += logits[indices[i]];
}
// 随机采样
float rand = new Random().nextFloat() * sum;
float cumsum = 0;
for (int i = 0; i < TOP_K; i++) {
cumsum += logits[indices[i]];
if (cumsum >= rand) {
return indices[i];
}
}
return indices[0];
}
public void close() {
if (session != null) {
try { session.close(); } catch (Exception e) {}
}
if (environment != null) {
environment.close();
}
}
// 测试入口
public static void main(String[] args) throws Exception {
QwenInference inference = new QwenInference();
String prompt = "<|im_start|>user\n用Java写个单例模式,要线程安全的<|im_end|>\n<|im_start|>assistant\n";
System.out.println("Prompt: " + prompt);
System.out.println("Generating...");
String result = inference.generate(prompt);
System.out.println("\nGenerated: " + result);
inference.close();
}
}
这段代码的关键点我解释一下:
- 分词环节:我们用 HuggingFaceTokenizer 把字符串转成模型能理解的数字 ID。注意 Qwen 3.5 有特殊的对话模板(
<|im_start|>这种标记),输入时一定要带上,否则模型会懵圈。 - 推理环节:DJL 的 ONNX Runtime 引擎直接裸调原生的 OrtSession,这是为了确保对 Qwen 这种生成式模型有最细粒度的控制。每次输入完整的已生成序列,模型输出下一个 token 的概率分布。
- 采样环节:别直接取概率最大的(那样对话会很机械),而是用 Temperature + Top-K 采样,让回答有"人味"一点。Temperature 0.8 是个比较均衡的值,想更有创意可以调到 1.0,想更确定就调到 0.5。
五、性能调优:让 Java 跑得更狂野
光能跑起来不够,还得跑得快。给你几个实测有效的优化技巧:
5.1 开启动态量化:显存省一半
ONNX Runtime 支持 INT8 动态量化。在创建 SessionOptions 时加一行:
options.addConfigEntry("session.dynamic_quantization", "1");
对于 Qwen 3.5 4B 模型,这能让显存占用从 8GB 降到 4GB 左右,速度还能快 20%,精度损失几乎无感。
5.2 KV Cache:别重复算历史
上面示例代码为了简洁,每次把完整序列喂给模型。实际生产环境你一定要实现 KV Cache(键值缓存),把之前算好的注意力结果存起来。这样生成第 100 个 token 时,不用重新算前 99 个的注意力,推理速度能从"龟爬"变"兔跳"。
实现思路是用 past_key_values 输入输出,这需要你导出的 ONNX 模型支持 use_cache=True 选项。导出时加上 --use-cache 参数即可。
5.3 批处理(Batching):一次服务多个用户
如果你的应用是接口服务,别来一条请求就调一次模型。用 DJL 的 Predictor 批量攒请求,一次推理处理 4-8 条输入。ONNX Runtime 对 batch 的优化很好,吞吐量能提升 3-5 倍。
六、避坑指南:这些雷我已经帮你踩过了
坑 1:模型输入输出名对不上
不同方式导出的 ONNX 模型,输入输出节点名字可能不同。用 Netron 工具打开 .onnx 文件,确认输入节点是 input_ids 还是 input_ids:0,代码里要对应上。
坑 2:Tokenizer 和模型版本不匹配
务必保证 tokenizer.json 和 model.onnx 是同一个模型版本(比如都是 Qwen 3.5 4B)。混用不同版本的 tokenizer,会出现生成的文字是乱码的情况。
坑 3:长文本溢出
如果输入超过模型最大长度(通常是 2048 或 4096),ONNX Runtime 会抛异常。生成前先用 tokenizer 检查长度,超长的话做截断或分段处理。
七、总结:Java 也能玩 AI,而且很香
走完整套流程你会发现,用 Java 部署大模型真不是啥天方夜谭。DJL + ONNX Runtime 的组合,让你能用最熟悉的 Maven 管理依赖,用最熟悉的 Java 语法写 AI 应用,部署的时候就是一个 jar 包甩上去,干净利落。
对于那些已经用 Java 写了十几年业务代码、不想为了 AI 功能重构整个技术栈的团队来说,这简直是天降福音。Qwen 3.5 的中文能力在开源模型里是第一梯队,配合 Java 的生态稳定性,无论是做智能客服、文档生成还是代码辅助,都能稳稳地落地。
所以,下次再有人跟你说"搞 AI 必须用 Python",你可以淡定地打开 IDE,跑一遍上面的代码,然后问他:“你看,这 Java 跑起来,它不香吗?”
更多推荐
所有评论(0)