DeepSeek-OCR模型部署最佳实践

本文档详细介绍如何在 FunModel 平台上实现 DeepSeek-OCR 模型从开发到生产的全链路部署。通过 DevPod 云端开发环境,您将掌握如何开发、调试、封装和部署 DeepSeek-OCR 模型服务,实现开发与运维的无缝协作。

前提条件

基本要求

  1. 拥有阿里云账号。

  2. 登录FunModel 控制台,根据控制台指引完成 RAM 角色授权等配置。

    重要提示:如果您当前使用的是旧版控制台页面,请点击右上角的"新版控制台"按钮切换至新版界面。
  3. 技术储备:具备基本的 Python 和深度学习模型部署知识。

环境准备

建议先完成DeepSeek-OCR快速开始指南中的环境搭建和基础测试,熟悉 DevPod 的基本操作。

开发与调试

本节介绍如何在 DevPod 环境中开发生产级 DeepSeek-OCR 模型服务。

DevPod环境优势

启动 DeepSeek-OCR DevPod 后,您将获得:

  • 预配置环境:已预装 PyTorch、vLLM、Transformers 等深度学习框架。

  • GPU 资源:即开即用的 GPU 算力,无需本地配置。

  • 持久化存储:NAS 挂载路径 /mnt/{模型名称} 自动保存模型文件。

  • 统一工作区:开发、调试、部署在同一环境完成,消除环境差异。

模型服务化开发

为什么需要服务化?

DeepSeek-OCR 官方提供的是命令行脚本,适合本地测试,但无法直接用于生产环境。要将 OCR 能力集成到业务系统中,需要将其封装为 HTTP API 服务:

对比维度

命令行脚本

HTTP 服务

访问方式

需要登录服务器执行

通过 HTTP API 远程调用

业务集成

难以集成到业务系统

任何语言通过 HTTP 调用

并发能力

单进程串行处理

支持多用户并发请求

扩展性

难以水平扩展

可部署多实例负载均衡

创建服务端代码

在 DevPod 中创建 server.py 文件,将模型封装为 FastAPI 服务:

# 在 DevPod 终端中执行cd /workspace/DeepSeek-OCR/DeepSeek-OCR-master/DeepSeek-OCR-vllm
touch server.py

然后在 Web IDE 中打开 server.py,编写服务化代码。

核心代码解析

以下是生产级 DeepSeek-OCR 服务的核心实现,支持图像和 PDF 的批量处理:

1. 模型初始化与配置
# 环境变量配置
if torch.version.cuda == '11.8':
    os.environ["TRITON_PTXAS_PATH"] = "/usr/local/cuda-11.8/bin/ptxas"
os.environ['VLLM_USE_V1'] = '0'
os.environ["CUDA_VISIBLE_DEVICES"] = '0'

# 注册并加载模型
ModelRegistry.register_model("DeepseekOCRForCausalLM", DeepseekOCRForCausalLM)

llm = LLM(
    model=MODEL_PATH,
    hf_overrides={"architectures": ["DeepseekOCRForCausalLM"]},
    block_size=256,
    max_model_len=8192,
    max_num_seqs=max(MAX_CONCURRENCY, 100),
    gpu_memory_utilization=0.9,  # 使用 90% GPU 显存
    disable_mm_preprocessor_cache=True
)
2. API 接口设计
class InputData(BaseModel):
    """支持图像和 PDF 的混合输入"""
    images: Optional[List[str]] = None  # 图像 URL 列表
    pdfs: Optional[List[str]] = None    # PDF URL 列表

class RequestData(BaseModel):
    """请求模型,支持自定义 prompt"""
    input: InputData
    prompt: str = '<image>\nFree OCR.'  # 默认 prompt

class ResponseData(BaseModel):
    """返回 OCR 识别结果"""
    output: List[str]
3. 异步并发处理
async def process_items_async(items_urls: List[str], is_pdf: bool, prompt: str):
    """
    异步处理图像/PDF URL 列表
    - 并发下载文件
    - 线程池处理图像预处理
    - 返回批量推理输入
    """
    loop = asyncio.get_event_loop()
    
    # 并发下载所有文件
    download_tasks = [loop.run_in_executor(None, download_file, url) 
                      for url in items_urls]
    contents = await asyncio.gather(*download_tasks)
    
    # 线程池处理图像
    with ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor:
        process_tasks = [
            loop.run_in_executor(executor, process_single_image_sync, img, prompt)
            for img, prompt in processing_args
        ]
        processed_results = await asyncio.gather(*process_tasks)
    
    return processed_results, num_results_per_input
4. 批量推理接口
@app.post("/ocr_batch", response_model=ResponseData)
async def ocr_batch_inference(request: RequestData):
    """
    批量 OCR 处理接口
    - 支持图像和 PDF 混合输入
    - 自动处理 PDF 多页场景
    - 返回结构化识别结果
    """
    # 处理图像和 PDF
    all_batch_inputs = []
    if request.input.images:
        batch_inputs_images, counts_images = await process_items_async(
            request.input.images, is_pdf=False, prompt=request.prompt
        )
        all_batch_inputs.extend(batch_inputs_images)
    
    if request.input.pdfs:
        batch_inputs_pdfs, counts_pdfs = await process_items_async(
            request.input.pdfs, is_pdf=True, prompt=request.prompt
        )
        all_batch_inputs.extend(batch_inputs_pdfs)
    
    # 批量推理
    outputs_list = await run_inference(all_batch_inputs)
    
    # 重组结果(PDF 多页合并)
    return ResponseData(output=final_outputs)

完整代码示例

import os
import io
import torch
import uvicorn
import requests
from PIL import Image
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional, Dict, Any, List
import tempfile
import fitz
from concurrent.futures import ThreadPoolExecutor
import asyncio

# Set environment variables
if torch.version.cuda == '11.8':
    os.environ["TRITON_PTXAS_PATH"] = "/usr/local/cuda-11.8/bin/ptxas"
os.environ['VLLM_USE_V1'] = '0'
os.environ["CUDA_VISIBLE_DEVICES"] = '0'

from config import MODEL_PATH, CROP_MODE, MAX_CONCURRENCY, NUM_WORKERS
from vllm import LLM, SamplingParams
from vllm.model_executor.models.registry import ModelRegistry
from deepseek_ocr import DeepseekOCRForCausalLM
from process.ngram_norepeat import NoRepeatNGramLogitsProcessor
from process.image_process import DeepseekOCRProcessor

# Register model
ModelRegistry.register_model("DeepseekOCRForCausalLM", DeepseekOCRForCausalLM)

# Initialize model
print("Loading model...")
llm = LLM(
    model=MODEL_PATH,
    hf_overrides={"architectures": ["DeepseekOCRForCausalLM"]},
    block_size=256,           # Memory block size for KV cache
    enforce_eager=False,      # Use eager mode for better performance with multimodal models
    trust_remote_code=True,   # Allow execution of code from remote repositories
    max_model_len=8192,       # Maximum sequence length the model can handle
    swap_space=0,             # No swapping to CPU, keeping everything on GPU
    max_num_seqs=max(MAX_CONCURRENCY, 100),  # Maximum number of sequences to process concurrently
    tensor_parallel_size=1,   # Number of GPUs for tensor parallelism (1 = single GPU)
    gpu_memory_utilization=0.9,  # Use 90% of GPU memory for model execution
    disable_mm_preprocessor_cache=True  # Disable cache for multimodal preprocessor to avoid issues
)

# Configure sampling parameters
# NoRepeatNGramLogitsProcessor prevents repetition in generated text by tracking n-gram patterns
logits_processors = [NoRepeatNGramLogitsProcessor(ngram_size=20, window_size=50, whitelist_token_ids={128821, 128822})]
sampling_params = SamplingParams(
    temperature=0.0,                    # Deterministic output (greedy decoding)
    max_tokens=8192,                    # Maximum number of tokens to generate
    logits_processors=logits_processors, # Apply the processor to avoid repetitive text
    skip_special_tokens=False,          # Include special tokens in the output
    include_stop_str_in_output=True,    # Include stop strings in the output
)

# Initialize FastAPI app
app = FastAPI(title="DeepSeek-OCR API", version="1.0.0")

class InputData(BaseModel):
    """
    Input data model to define what types of documents to process
    images: Optional list of image URLs to process
    pdfs: Optional list of PDF URLs to process
    Note: At least one of these fields must be provided in a request
    """
    images: Optional[List[str]] = None
    pdfs: Optional[List[str]] = None

class RequestData(BaseModel):
    """
    Main request model that defines the input data and optional prompt
    """
    input: InputData
    # Add prompt as an optional field with a default value
    prompt: str = '<image>\nFree OCR.' # Default prompt

class ResponseData(BaseModel):
    """
    Response model that returns OCR results for each input document
    """
    output: List[str]

def download_file(url: str) -> bytes:
    """Download file from URL"""
    try:
        response = requests.get(url, timeout=30)
        response.raise_for_status()
        return response.content
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Failed to download file from URL: {str(e)}")

def is_pdf_file(content: bytes) -> bool:
    """Check if the content is a PDF file"""
    return content.startswith(b'%PDF')

def load_image_from_bytes(image_bytes: bytes) -> Image.Image:
    """Load image from bytes"""
    try:
        image = Image.open(io.BytesIO(image_bytes))
        return image.convert('RGB')
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Failed to load image: {str(e)}")

def pdf_to_images(pdf_bytes: bytes, dpi: int = 144) -> list:
    """Convert PDF to images"""
    try:
        images = []
        pdf_document = fitz.open(stream=pdf_bytes, filetype="pdf")
        zoom = dpi / 72.0
        matrix = fitz.Matrix(zoom, zoom)

        for page_num in range(pdf_document.page_count):
            page = pdf_document[page_num]
            pixmap = page.get_pixmap(matrix=matrix, alpha=False)
            img_data = pixmap.tobytes("png")
            img = Image.open(io.BytesIO(img_data))
            images.append(img.convert('RGB'))

        pdf_document.close()
        return images
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Failed to convert PDF to images: {str(e)}")

def process_single_image_sync(image: Image.Image, prompt: str) -> Dict: # Renamed and made sync
    """Process a single image (synchronous function for CPU-bound work)"""
    try:
        cache_item = {
            "prompt": prompt,
            "multi_modal_data": {
                "image": DeepseekOCRProcessor().tokenize_with_images(
                    images=[image],
                    bos=True,
                    eos=True,
                    cropping=CROP_MODE
                )
            },
        }
        return cache_item
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to process image: {str(e)}")

async def process_items_async(items_urls: List[str], is_pdf: bool, prompt: str) -> tuple[List[Dict], List[int]]:
    """
    Process a list of image or PDF URLs asynchronously.
    Downloads files concurrently, then processes images/PDF pages in a thread pool.
    Returns a tuple: (batch_inputs, num_results_per_input)
    """
    loop = asyncio.get_event_loop()

    # 1. Download all files concurrently
    download_tasks = [loop.run_in_executor(None, download_file, url) for url in items_urls]
    contents = await asyncio.gather(*download_tasks)

    # 2. Prepare arguments for processing (determine if PDF/image, count pages)
    processing_args = []
    num_results_per_input = []
    for idx, (url, content) in enumerate(zip(items_urls, contents)):
        if is_pdf:
            if not is_pdf_file(content):
                 raise HTTPException(status_code=400, detail=f"Provided file is not a PDF: {url}")
            images = pdf_to_images(content)
            num_pages = len(images)
            num_results_per_input.append(num_pages)
            # Each page will be processed separately
            processing_args.extend([(img, prompt) for img in images])
        else: # is image
            if is_pdf_file(content):
                # Handle case where an image URL accidentally points to a PDF
                images = pdf_to_images(content)
                num_pages = len(images)
                num_results_per_input.append(num_pages)
                processing_args.extend([(img, prompt) for img in images])
            else:
                image = load_image_from_bytes(content)
                num_results_per_input.append(1)
                processing_args.append((image, prompt))

    # 3. Process images/PDF pages in parallel using ThreadPoolExecutor
    with ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor:
        # Submit all processing tasks
        process_tasks = [
            loop.run_in_executor(executor, process_single_image_sync, img, prompt)
            for img, prompt in processing_args
        ]
        # Wait for all to complete
        processed_results = await asyncio.gather(*process_tasks)

    return processed_results, num_results_per_input

async def run_inference(batch_inputs: List[Dict]) -> List:
    """Run inference on batch inputs"""
    if not batch_inputs:
        return []
    try:
        # Run inference on the entire batch
        outputs_list = llm.generate(
            batch_inputs,
            sampling_params=sampling_params
        )
        return outputs_list
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to run inference: {str(e)}")

@app.post("/ocr_batch", response_model=ResponseData)
async def ocr_batch_inference(request: RequestData):
    """
    Main OCR batch processing endpoint
    Accepts a list of image URLs and/or PDF URLs for OCR processing
    Returns a list of OCR results corresponding to each input document
    Supports both individual image processing and PDF-to-image conversion
    """
    print(f"Received request data: {request}")
    try:
        input_data = request.input
        prompt = request.prompt # Get the prompt from the request
        if not input_data.images and not input_data.pdfs:
            raise HTTPException(status_code=400, detail="Either 'images' or 'pdfs' (or both) must be provided as lists.")

        all_batch_inputs = []
        final_output_parts = []

        # Process images if provided
        if input_data.images:
            batch_inputs_images, counts_images = await process_items_async(input_data.images, is_pdf=False, prompt=prompt)
            all_batch_inputs.extend(batch_inputs_images)
            final_output_parts.append(counts_images)

        # Process PDFs if provided
        if input_data.pdfs:
            batch_inputs_pdfs, counts_pdfs = await process_items_async(input_data.pdfs, is_pdf=True, prompt=prompt)
            all_batch_inputs.extend(batch_inputs_pdfs)
            final_output_parts.append(counts_pdfs)

        if not all_batch_inputs:
             raise HTTPException(status_code=400, detail="No valid images or PDF pages were processed from the input URLs.")

        # Run inference on the combined batch
        outputs_list = await run_inference(all_batch_inputs)

        # Reconstruct final output list based on counts
        final_outputs = []
        output_idx = 0
        # Flatten the counts list
        all_counts = [count for sublist in final_output_parts for count in sublist]

        for count in all_counts:
            # Get 'count' number of outputs for this input
            input_outputs = outputs_list[output_idx : output_idx + count]
            output_texts = []
            for output in input_outputs:
                content = output.outputs[0].text
                if '<|end▁of▁sentence|>' in content:
                    content = content.replace('<|end▁of▁sentence|>', '')
                output_texts.append(content)

            # Combine pages if it was a multi-page PDF input (or image treated as PDF)
            if count > 1:
                combined_text = "\n<--- Page Split --->\n".join(output_texts)
                final_outputs.append(combined_text)
            else:
                # Single image or single-page PDF
                final_outputs.append(output_texts[0] if output_texts else "")

            output_idx += count # Move to the next set of outputs

        return ResponseData(output=final_outputs)

    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")


@app.get("/health")
async def health_check():
    """Health check endpoint"""
    return {"status": "healthy"}

@app.get("/")
async def root():
    """Root endpoint"""
    return {"message": "DeepSeek-OCR API is running (Batch endpoint available at /ocr_batch)"}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000, workers=1)

本地测试

启动服务

在 DevPod 终端中启动推理服务:

cd /workspace/DeepSeek-OCR/DeepSeek-OCR-master/DeepSeek-OCR-vllm
python server.py

服务将在 http://127.0.0.1:8000 启动。

测试单张图像

curl -X POST http://127.0.0.1:8000/ocr_batch \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "images": [
        "https://paddle-model-ecology.bj.bcebos.com/paddlex/imgs/demo_image/paddleocr_vl_demo.png"
      ]
    },
    "prompt": "<image>\n<|grounding|>Convert the document to markdown."
  }'

测试PDF文档

curl -X POST http://127.0.0.1:8000/ocr_batch \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "pdfs": [
        "https://images.devsapp.cn/test/ocr-test.pdf"
      ]
    },
    "prompt": "<image>\nFree OCR."
  }'

测试混合输入

curl -X POST http://127.0.0.1:8000/ocr_batch \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "images": [
        "https://paddle-model-ecology.bj.bcebos.com/paddlex/imgs/demo_image/paddleocr_vl_demo.png"
      ],
      "pdfs": [
        "https://images.devsapp.cn/test/ocr-test.pdf"
      ]
    },
    "prompt": "<image>\nFree OCR."
  }'

远程调试

DevPod 支持通过代理地址进行远程调试,便于使用 Postman 等工具测试。

获取代理地址

  1. 在 DevPod 控制台点击快速访问标签页。

  2. 获取代理路径,示例如下:

    https://devpod-e***a-lwt***jyw.cn-hangzhou.ide.fc.aliyun.com/proxy/8000/

使用代理地址测试

示例如下:

curl -X POST \
  "https://devpod-e***a-lwt***jyw.cn-hangzhou.ide.fc.aliyun.com/proxy/8000/ocr_batch" \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "pdfs": ["https://images.devsapp.cn/test/ocr-test.pdf"]
    },
    "prompt": "<image>\nFree OCR."
  }'
说明

也可以使用 Postman、Insomnia 等 API 测试工具,通过图形界面进行调试,更加直观便捷。

镜像构建与部署

当模型服务在开发环境中验证通过后,后续可将其封装为容器镜像并部署到生产环境。

镜像构建

DevPod 提供一键构建功能,将当前开发环境打包为标准容器镜像:

  1. 在 DevPod 控制台点击制作镜像

  2. 选择ACR实例并配置镜像信息。

  3. 系统将自动构建并推送镜像到指定容器仓库。

详情:更多关于镜像构建的详细步骤,请参考 镜像构建与ACR集成

模型部署

镜像构建推送完毕后,镜像已经存储到ACR,此时可以一键部署为FunModel模型服务。

  1. 在镜像构建完成后,点击直接部署

  2. 配置服务参数(如启动命令、监听端口、超时时间等)。

  3. 点击开始部署,系统将自动部署模型服务。

部署验证

部署完成后,通过以下方式验证服务:

  1. 在线调试:使用 FunModel 控制台的在线调试功能快速测试。

  2. API 调用:获取服务域名,通过 HTTP 客户端调用。

  3. 性能测试:使用压测工具验证并发能力。

监控与迭代

FunModel 提供完整的监控与运维能力:

监控指标

  • 性能监控:实时查看 GPU 利用率、请求延迟、吞吐量。

  • 日志分析:集中收集所有实例日志,支持关键词检索和错误追踪。

  • 调用统计:查看 API 调用次数、成功率、错误分布。

变更管理

  • 部署记录:每次配置变更(实例规格、超时时间、扩缩容策略)都有完整记录。

  • 版本回滚:支持快速回滚到历史稳定版本。

  • 灰度发布:支持按流量比例逐步切换新版本。

迭代流程

当需要优化模型或修复问题时:

  1. 发现问题:通过监控面板或日志分析定位问题。

  2. 开发修复:直接在 DevPod 中修改代码并测试。

  3. 验证方案:在开发环境充分验证修复效果。

  4. 构建部署:制作新镜像并一键部署到生产环境。

  5. 观察效果:通过监控验证问题是否解决。

整个流程在统一环境中完成,避免环境不一致导致的问题,真正实现开发与运维的无缝协作。

最佳实践总结

核心优势

使用DevPod部署DeepSeek-OCR模型服务的关键优势:

  1. 环境一致性:开发、测试、生产环境完全一致,消除“环境漂移”问题。

  2. 资源弹性:按需分配 GPU 资源,开发时使用低配实例节省成本,生产时按需扩容。

  3. 工作流集成:无需在多个平台间切换,所有操作在一个工作区完成。

  4. 零学习曲线:无需掌握 Kubernetes、Dockerfile 等复杂概念,专注业务价值。

  5. 快速迭代:从代码修改到上线验证,整个周期可缩短至分钟级。

两种工作流模式

根据团队规模和工程化需求,FunModel支持两种工作流:

DevFlow1:一键部署流(推荐个人和小团队)

适合快速验证想法和迭代优化的场景。

特点

  • 无需编写 Dockerfile

  • 一键完成构建和部署

  • 适合快速原型验证

  • 降低工程化门槛

流程图

DevFlow1 工作流

操作步骤

  1. 开发阶段:在 DevPod 中编写代码、安装依赖、调试功能。

  2. 构建阶段:点击制作镜像,系统自动将当前环境打包为镜像。

  3. 部署阶段:点击直接部署,配置服务参数后一键上线。

  4. 迭代阶段:发现问题后直接在DevPod中修改,重新构建部署。

DevFlow2:标准工程流(推荐企业团队)

适合追求工程规范和长期可维护性的团队。

特点

  • 代码与配置版本化管理

  • 支持多人协作和 Code Review

  • 可集成 CI/CD 流水线

  • 部署过程可追溯可复现

流程图

DevFlow2 工作流

操作步骤

  1. 开发准备:从 Git 仓库的指定 Commit 启动 DevPod,确保基线一致。

  2. 功能开发:在 DevPod 中进行代码迭代、依赖安装、集成测试。

  3. 准备部署:编写或调整 Dockerfile,精确配置镜像构建逻辑。

  4. 构建测试:系统根据 Dockerfile 构建镜像,执行端到端测试。

  5. 代码提交:将代码和 Dockerfile 一同提交至 Git 仓库。

  6. 自动发布:通过 CI/CD 流水线自动构建镜像并部署到生产环境。