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` 包。如下图所示:[![`logging` 包](http://static.iocoder.cn/images/MyBatis/2020_01_07/01.png)](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)](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> 处,我们可以看到,按照 SLF4Jcommons-loggingLog4J2Log4JJDK loggingno 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 属性。

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 的实现类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);
    }

}
  • 在构造方法中,我们可以看到,会根据不同的情况,创建 Slf4jLocationAwareLoggerImplSlf4jLoggerImpl 对象。为什么呢?因为 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);
    }
    
  • 所以,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.outSystem.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.ConnectionLoggerorg.apache.ibatis.logging.jdbc.PreparedStatementLoggerorg.apache.ibatis.logging.jdbc.ResultSetLoggerorg.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);
}
  • columnMapcolumnValues 中,记录 key value 键值对。

4.3 getValue

#getValue() 方法,获得参数值。代码如下:

// BaseJdbcLogger.java

protected String getValue() {
    return columnValues.isEmpty() ? "" : columnValues.toString();
}
  • 返回 columnValuestoString() 字符串。

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();
}
  • 返回 columnMapkeySettoString() 字符串。

4.6 clearColumn

#clearColumn() 方法,清空 columnMapcolumnValues 。代码如下:

// BaseJdbcLogger.java

protected void clearColumn() {
    columnMap.clear();
    columnValues.clear();
}

4.7 其它方法

BaseJdbcLogger 中,还有其它方法,比较简单,胖友自己研究。

5. 小结

本文,还是比较简单的。重点是 LogFactory 和 Log 的集成多种日志框架的实现。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐