DeepSeek-OCR部署优化:模型分片加载与显存动态分配策略分享
DeepSeek-OCR部署优化:模型分片加载与显存动态分配策略分享
1. 引言
如果你尝试过部署DeepSeek-OCR这样的视觉大模型,可能遇到过这样的尴尬:模型文件动辄几十GB,显存要求动不动就24GB起步,普通开发者手头的硬件根本跑不起来。更让人头疼的是,即使你有足够大的显存,模型加载时间也长得让人怀疑人生——第一次启动等个十几分钟是家常便饭。
我在实际部署DeepSeek-OCR-2时也遇到了同样的问题。官方推荐的配置是24GB显存起步,这直接把大部分个人开发者和中小团队挡在了门外。但仔细分析后发现,模型虽然庞大,但在处理单张图片时,并不需要一次性加载所有参数到显存中。
经过几周的摸索和实践,我总结出了一套模型分片加载与显存动态分配的优化策略。这套方案能让DeepSeek-OCR在16GB甚至12GB显存的显卡上稳定运行,同时将首次加载时间从十几分钟缩短到2-3分钟。更重要的是,这些优化完全不影响模型的识别精度和效果。
2. 问题分析:为什么DeepSeek-OCR这么“吃”显存?
2.1 模型规模与显存需求
DeepSeek-OCR-2作为一个多模态视觉大模型,参数量达到了惊人的规模。我们来算一笔账:
- 模型权重文件:通常超过30GB(FP16精度)
- 激活值内存:推理时中间结果需要大量显存
- KV缓存:处理长文档时需要缓存注意力机制的键值对
- 系统开销:PyTorch框架本身的内存占用
当所有这些加起来,24GB的显存要求其实并不夸张。但问题在于,这些内存需求是峰值需求,而不是持续需求。
2.2 传统加载方式的局限性
传统的模型加载方式很简单粗暴:
# 传统加载方式 - 一次性全部加载
from transformers import AutoModel
model = AutoModel.from_pretrained(
"/path/to/deepseek-ocr-2",
torch_dtype=# 1. 概述
本文,我们来分享 MyBatis 的日志模块,对应 `logging` 包。如下图所示:[](http://static.iocoder.cn/images/MyBatis/2020_01_07/01.png)`logging` 包
在 [《精尽 MyBatis 源码分析 —— 项目结构一览》](http://svip.iocoder.cn/MyBatis/intro) 中,简单介绍了这个模块如下:
> 无论在开发测试环境中,还是在线上生产环境中,日志在整个系统中的地位都是非常重要的。良好的日志功能可以帮助开发人员和测试人员快速定位 Bug 代码,也可以帮助运维人员快速定位性能瓶颈等问题。目前的 Java 世界中存在很多优秀的日志框架,例如 Log4j、 Log4j2、Slf4j 等。
>
> MyBatis 作为一个设计优良的框架,除了提供详细的日志输出信息,还要能够集成多种日志框架,其日志模块的一个主要功能就是**集成第三方日志框架**。
本文涉及的类如下图所示:[](http://static.iocoder.cn/images/MyBatis/2020_01_07/02.png)类图
- 从图中,我们可以看到,MyBatis 直接提供了第三方日志框架的集成,而不需要我们自己来适配。
- 并且,MyBatis 提供了多种第三方日志框架的集成,并且有优先级顺序。
下面,让我们开始遨游 `logging` 包。
# 2. LogFactory
`org.apache.ibatis.logging.LogFactory` ,Log 工厂类。
## 2.1 构造方法
```java
// LogFactory.java
/**
* Marker to be used by logging implementations that support markers
*/
public static final String MARKER = "MYBATIS";
/**
* 使用的 Log 的构造方法
*/
private static Constructor<? extends Log> logConstructor;
static {
// <1> 逐个尝试,判断使用哪个 Log 的实现类,即初始化 logConstructor 属性
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
-
<1>处,在类加载时,通过#tryImplementation(Runnable runnable)方法,逐个尝试,判断使用哪个 Log 的实现类,即初始化logConstructor属性。代码如下:// LogFactory.java private static void tryImplementation(Runnable runnable) { if (logConstructor == null) { try { runnable.run(); } catch (Throwable t) { // ignore } } }- 当
logConstructor为空时,执行runnable的方法。
- 当
-
<1>处,我们可以看到,按照SLF4J、commons-logging、Log4J2、Log4J、JDK logging、no logging的顺序尝试。也就是说,只要前面的日志框架存在,则使用它。 -
另外,
<1>处,对应#useXXXLogging()方法,代码如下:// LogFactory.java public static synchronized void useSlf4jLogging() { setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class); } public static synchronized void useCommonsLogging() { setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class); } public static synchronized void useLog4JLogging() { setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class); } public static synchronized void useLog4J2Logging() { setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class); } public static synchronized void useJdkLogging() { setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class); } public static synchronized void useStdOutLogging() { setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class); } public static synchronized void useNoLogging() { setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class); }-
每个方法,都调用了
#setImplementation(Class<? extends Log> implClass)方法。代码如下:// LogFactory.java private static void setImplementation(Class<? extends Log> implClass) { try { // 获得参数为 String 的构造方法 Constructor<? extends Log> candidate = implClass.getConstructor(String.class); // 创建 Log 对象 Log log = candidate.newInstance(LogFactory.class.getName()); if (log.isDebugEnabled()) { log.debug("Logging initialized using '" + implClass + "' adapter."); } // 创建成功,意味着可以使用,设置为 logConstructor logConstructor = candidate; } catch (Throwable t) { throw new LogException("Error setting Log implementation. Cause: " + t, t); } }- 通过反射的方式,创建对应的 Log 对象。如果创建成功,意味着可以使用,则赋值给
logConstructor属性。
- 通过反射的方式,创建对应的 Log 对象。如果创建成功,意味着可以使用,则赋值给
-
2.2 getLog
#getLog(...) 方法,获得 Log 对象。代码如下:
// LogFactory.java
public static Log getLog(Class<?> aClass) {
return getLog(aClass.getName());
}
public static Log getLog(String logger) {
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}
- 通过
logConstructor属性,创建 Log 对象。
2.3 小结
LogFactory 负责创建 Log 对象。但是,实际上,它内部管理的是创建 Log 对象的构造方法。
3. Log
org.apache.ibatis.logging.Log ,MyBatis Log 接口。代码如下:
// Log.java
public interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
- 和主流的日志框架的接口基本一致。
3.1 Log 的实现类
在 logging 包下,有多个 Log 的实现类。如下图所示:
Log 的实现类
- 每个实现类,对应一个第三方的日志框架。并且,每个实现类的代码都比较类似,都是调用第三方日志框架的对应方法。
- 下面,我们以 Slf4jImpl 类来举例子。代码如下:
// Slf4jImpl.java
public class Slf4jImpl implements Log {
private Log log;
public Slf4jImpl(String clazz) {
// 使用 SLF4J 的 LoggerFactory 创建 org.slf4j.Logger 对象
Logger logger = LoggerFactory.getLogger(clazz);
// 如果是 LocationAwareLogger 类型,则创建 Slf4jLocationAwareLoggerImpl 对象
if (logger instanceof LocationAwareLogger) {
try {
// check for slf4j >= 1.6 method signature
logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class);
log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger);
return;
} catch (SecurityException e) {
// fail-back to Slf4jLoggerImpl
} catch (NoSuchMethodException e) {
// fail-back to Slf4jLoggerImpl
}
}
// Logger is not LocationAwareLogger or slf4j version < 1.6
// 否则,创建 Slf4jLoggerImpl 对象
log = new Slf4jLoggerImpl(logger);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.error(s, e);
}
@Override
public void error(String s) {
log.error(s);
}
@Override
public void debug(String s) {
log.debug(s);
}
@Override
public void trace(String s) {
log.trace(s);
}
@Override
public void warn(String s) {
log.warn(s);
}
}
-
在构造方法中,我们可以看到,会根据不同的情况,创建
Slf4jLocationAwareLoggerImpl或Slf4jLoggerImpl对象。为什么呢?因为 SLF4J 有org.slf4j.impl.LocationAwareLogger这个类,需要做适配。当然,两个类的代码基本一致,除了打印日志会调用参数更多的方法。以#debug(String s)方法举例子,代码如下:// Slf4jLocationAwareLoggerImpl.java @Override public void debug(String s) { if (logger.isDebugEnabled()) { logger.log(null, FQCN, LocationAwareLogger.DEBUG_INT, s, null, null); } } // Slf4jLoggerImpl.java @Override public void debug(String s) { logger.debug(s); }- 那么,为什么会有
org.slf4j.impl.LocationAwareLogger这个类呢?感兴趣的胖友,可以看看 《slf4j中locationAwareLogger和Logger的区别和联系》 。
- 那么,为什么会有
-
所以,Slf4jImpl 是一个适配器,适配的是
org.slf4j.Logger类。 -
其它 Log 的实现类,也类似,胖友可以自己看看。
3.2 StdOutImpl
org.apache.ibatis.logging.stdout.StdOutImpl ,实现 Log 接口,StdOut 实现类。代码如下:
// StdOutImpl.java
public class StdOutImpl implements Log {
public StdOutImpl(String clazz) {
// Do Nothing
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public void error(String s, Throwable e) {
System.err.println(s);
e.printStackTrace(System.err);
}
@Override
public void error(String s) {
System.err.println(s);
}
@Override
public void debug(String s) {
System.out.println(s);
}
@Override
public void trace(String s) {
System.out.println(s);
}
@Override
public void warn(String s) {
System.out.println(s);
}
}
- 直接使用
System.out和System.err输出日志。
3.3 NoLoggingImpl
org.apache.ibatis.logging.nologging.NoLoggingImpl ,实现 Log 接口,无日志实现类。代码如下:
// NoLoggingImpl.java
public class NoLoggingImpl implements Log {
public NoLoggingImpl(String clazz) {
// Do Nothing
}
@Override
public boolean isDebugEnabled() {
return false;
}
@Override
public boolean isTraceEnabled() {
return false;
}
@Override
public void error(String s, Throwable e) {
// Do Nothing
}
@Override
public void error(String s) {
// Do Nothing
}
@Override
public void debug(String s) {
// Do Nothing
}
@Override
public void trace(String s) {
// Do Nothing
}
@Override
public void warn(String s) {
// Do Nothing
}
}
- 每个方法,都是空实现。也就是说,不打印日志。
4. BaseJdbcLogger
在 logging 包的 jdbc 包下,有 5 个类,是基于 JDBC 的日志实现。整体类图如下:
类图
- 实际上,BaseJdbcLogger 是一个抽象类,它定义了一系列的抽象方法,然后基于模板方法模式,打印 JDBC 相关的日志。而它的四个子类,分别打印对应类型的日志。
- 那么,BaseJdbcLogger 是怎么集成到 MyBatis 中,打印 JDBC 相关的日志呢?答案在
org.apache.ibatis.logging.jdbc.ConnectionLogger、org.apache.ibatis.logging.jdbc.PreparedStatementLogger、org.apache.ibatis.logging.jdbc.ResultSetLogger、org.apache.ibatis.logging.jdbc.StatementLogger这四个类中。当然,这个不是本文的重点,我们会在后续的文章中,详细解析。
下面,让我们来看看 BaseJdbcLogger 的代码实现。
4.1 构造方法
// BaseJdbcLogger.java
/**
* 常用的 Set 的方法名集合
*/
private static final Set<String> SET_METHODS = new HashSet<>();
/**
* 执行 SQL 语句的方法名集合
*/
private static final Set<String> EXECUTE_METHODS = new HashSet<>();
static {
SET_METHODS.add("setString");
SET_METHODS.add("setInt");
SET_METHODS.add("setBoolean");
SET_METHODS.add("setShort");
SET_METHODS.add("setLong");
SET_METHODS.add("setDouble");
SET_METHODS.add("setFloat");
SET_METHODS.add("setTimestamp");
SET_METHODS.add("setDate");
SET_METHODS.add("setTime");
SET_METHODS.add("setArray");
SET_METHODS.add("setBigDecimal");
SET_METHODS.add("setAsciiStream");
SET_METHODS.add("setBinaryStream");
SET_METHODS.add("setBlob");
SET_METHODS.add("setBytes");
SET_METHODS.add("setCharacterStream");
SET_METHODS.add("setNCharacterStream");
SET_METHODS.add("setClob");
SET_METHODS.add("setNClob");
SET_METHODS.add("setObject");
SET_METHODS.add("setNull");
EXECUTE_METHODS.add("execute");
EXECUTE_METHODS.add("executeUpdate");
EXECUTE_METHODS.add("executeQuery");
EXECUTE_METHODS.add("addBatch");
}
/**
* Log 对象,用于输出日志
*/
protected final Log statementLog;
/**
* 查询的列数
*/
protected final int queryStack;
/**
* Constructor
*
* @param log the log
* @param queryStack the query stack
*/
public BaseJdbcLogger(Log log, int queryStack) {
this.statementLog = log;
if (queryStack == 0) {
this.queryStack = 1;
} else {
this.queryStack = queryStack;
}
}
SET_METHODS静态属性,常用的 Set 的方法名集合。例如,java.sql.PreparedStatement的#setString(int parameterIndex, String x)方法。EXECUTE_METHODS静态属性,执行 SQL 语句的方法名集合。例如,java.sql.PreparedStatement的#execute()方法。statementLog属性,Log 对象,用于输出日志。queryStack属性,查询的列数。默认值为 1 。
4.2 setColumn
#setColumn(String key, Object value) 方法,记录 Set 方法的键值对。代码如下:
// BaseJdbcLogger.java
/**
* Map 集合,记录 PreparedStatement 设置的参数和参数值
*/
private final Map<String, Object> columnMap = new HashMap<>();
/**
* List 集合,记录 PreparedStatement 设置的参数和参数值
*/
private final List<Object> columnValues = new ArrayList<>();
/**
* 记录 Set 方法的键值对
*
* @param key 键
* @param value 值
*/
protected void setColumn(String key, Object value) {
columnMap.put(key, value);
columnValues.add(value);
}
- 在
columnMap和columnValues中,记录keyvalue键值对。
4.3 getValue
#getValue() 方法,获得参数值。代码如下:
// BaseJdbcLogger.java
protected String getValue() {
return columnValues.isEmpty() ? "" : columnValues.toString();
}
- 返回
columnValues的toString()字符串。
4.4 getParameterValueString
#getParameterValueString() 方法,获得参数值的字符串。代码如下:
// BaseJdbcLogger.java
protected String getParameterValueString() {
List<Object> typeList = new ArrayList<>(columnMap.values());
if (typeList.isEmpty()) {
return "";
}
// 使用 columnMap 拼接
StringBuffer parameters = new StringBuffer();
Iterator<Object> iterator = typeList.iterator();
for (int i = 0; iterator.hasNext(); i++) {
Object value = iterator.next();
if (value instanceof String || value instanceof Date || value instanceof Timestamp) { // 字符串,日期,时间戳
parameters.append("'" + value + "'");
} else if (value instanceof Boolean) { // Boolean
Boolean b = (Boolean) value;
parameters.append(b ? 1 : 0);
} else {
parameters.append(value);
}
// 非最后一个,则添加 ","
if (iterator.hasNext()) {
parameters.append(", ");
}
}
return parameters.toString();
}
- 使用
columnMap拼接参数值的字符串。
4.5 getColumnString
#getColumnString() 方法,获得参数的字符串。代码如下:
// BaseJdbcLogger.java
protected String getColumnString() {
return columnMap.keySet().toString();
}
- 返回
columnMap的keySet的toString()字符串。
4.6 clearColumn
#clearColumn() 方法,清空 columnMap 和 columnValues 。代码如下:
// BaseJdbcLogger.java
protected void clearColumn() {
columnMap.clear();
columnValues.clear();
}
4.7 其它方法
BaseJdbcLogger 中,还有其它方法,比较简单,胖友自己研究。
5. 小结
本文,还是比较简单的。重点是 LogFactory 和 Log 的集成多种日志框架的实现。
更多推荐
所有评论(0)