技术频道导航
HTML/CSS
.NET技术
IIS技术
PHP技术
Js/JQuery
Photoshop
Fireworks
服务器技术
操作系统
网站运营
Python技术
AI技术

赞助商

分类目录

赞助商

最新文章

搜索

本地运行 DeepSeek R1:完整设置指南(2026)

作者:admin    时间:2026-5-11 23:42:16    浏览:

本地运行 DeepSeek R1 解决了云托管大型语言模型存在的三个持续问题:数据隐私、重复的 API 成本以及对网络可用性的依赖。如果你处理敏感数据或在受监管环境中运营,本地推理会让所有提示和响应都留在本地,减少第三方能看到的内容,因为没有任何信息离开你的机器。

本指南涵盖硬件选择、环境设置、基于Docker的部署、推理优化,以及构建隐私优先AI应用的完整Node.js加React集成层。它面向熟悉命令行、Docker 和 JavaScript 的中级开发者。

本地运行 DeepSeek R1:你的完整设置指南

DeepSeek R1 的硬件需求

最低规格与推荐规格

你选择的型号决定了硬件对话的一切。较小的精简版本可装入单个8-24GB消费级GPU内,并有空间承担操作系统开销,而完整的671B型号则需要多GPU服务器配置。

型号变体 量子化 大约需要显存 RAM(CPU备用) 存储 Est. Tokens/sec(GPU)†
1.5B Q4_K_M ~1.5 GB 8 GB ~1.5 GB 40-60
7B Q4_K_M ~5 GB 16 GB ~4.5 GB 25-40
8B Q4_K_M ~6 GB 16 GB ~5 GB 20-35
14B Q4_K_M ~9 GB 32 GB ~8.5 GB 15-25
32B Q4_K_M ~20 GB 64GB ~19 GB 8-15
70B Q4_K_M ~40 GB 128 GB ~40 GB 4-8
671B(完整MoE) Q4_K_M ~350+ GB‡ 512+ GB ~350 GB 1-3

说明性估算;单显卡,Q4_K_M量化,2048令牌上下文,批次大小1。实际结果因GPU型号、驱动版本、CUDA版本、系统RAM带宽和热状态而有显著差异。使用本指南中的基准测试脚本来测量你自己的硬件。

MoE模型每个代币只激活部分参数;实用显存依赖于专家的路由和框架支持。350GB代表全型号重量存储,不一定是峰值主动显存。

FP16相比Q4,显存需求大约翻倍。Q5_K_M介于Q4_K_M和Q8_0之间,带来了适度的画质提升,内存大约多出15%到20%。Q8_0量化将权重存储为8位整数,约占FP16内存需求的50%,在大多数基准测试中质量接近FP16。

为你的硬件选择合适的型号尺寸

对于NVIDIA RTX 4090(24GB显存),14B Q4_K_M版本是单GPU推断的实际上限,且有足够的上下文空间。双3090或4090支持32B量化型号。70B版本实际上需要一块A100(80 GB)或多块具张量并行的消费级GPU。

苹果Silicon M系列机器采用统一内存,这意味着根据社区基准测试,配备192GB统一内存的M2 Ultra可以运行70B的Q4版本。预计每秒大约只有同等尺寸的 NVIDIA 显卡 30-60% 的代币;做基准测试确认。M3和M4 Max芯片配备64-128GB统一内存,能很好地应对32B版本。

你可以单独用CPU运行1.5B和7B版本,但现代8-16核台式CPU(例如Intel Core i9-13900K)大约需要2-5个token/秒;结果会因核心数和内存带宽而异。这使得CPU推理更适合测试和开发,而非交互式使用。

环境设置与前提条件

安装核心依赖

该协议栈需要 Python 3.11 或更高版本(用于模型服务后端和依赖工具),Node.js 20 或更高版本,应用层支持npm或pnpm,Git、curl 以及标准构建基础(gcc、make)。对于NVIDIA GPU,安装最新的NVIDIA驱动(推荐550+系列)和CUDA Toolkit 12.x。AMD GPU用户需要ROCm 6.x;Ollama 中的 AMD ROCm 支持通过 ollama/ollama:rocm Docker 镜像实现。请向docker run --rm --device /dev/kfd --device /dev/dri ollama/ollama:rocm核实并查阅Ollama的ROCm文档以了解主机驱动的需求。

安装 Ollama 以进行本地模型管理

Ollama 提供了一个单一的二进制文件,用于管理模型下载、量化变体以及本地 REST API 进行推理。它抽象了llama.cpp配置的复杂性,同时暴露了一个干净的 HTTP 接口。 

# 安装 Ollama(macOS 和 Linux)
# 安全提示:在执行脚本前,请先检查 ollama.com/install.sh 的内容。
# 此外,你也可以从 ollama.com/download 获取原生安装包或二进制文件。
curl -fsSL https://ollama.com/install.sh | sh

# Windows:请从 ollama.com/download 下载原生安装程序。
# 如果你更喜欢 Linux 环境,WSL2 是一个替代方案。

# 拉取 DeepSeek R1 7B 精简版(其他版本请调整标签)。
ollama pull deepseek-r1:7b

# 对于 14B 变体:
# ollama pull deepseek-r1:14b

# 对于 32B 变体:
# ollama pull deepseek-r1:32b

# 验证模型已下载
ollama list

# 运行快速 CLI 测试
ollama run deepseek-r1:7b "What is the sum of the first 50 prime numbers? Think step by step."

CLI测试应产生包含模型推理链的可见<think>标签,随后给出最终答案。如果收到响应,表示模型已具备功能,准备进行应用集成。 

运行 DeepSeek R1 与 Docker

容器化部署的 Docker 设置

Docker 提供了可复现、隔离的环境,简化了开发和生产机器之间的部署。容器化 Ollama 确保无论主机操作系统如何都能保持一致,使拆除变得简单。

GPU 直通配置

NVIDIA GPU 直通需要 NVIDIA 容器工具包。按照NVIDIA官方主机操作系统的官方文档安装,然后验证:

# 将标签替换为与你已安装的 CUDA 工具包相匹配的版本。
# 请访问 hub.docker.com/r/nvidia/cuda/tags 查看可用的标签。
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
# docker-compose.yml
# Docker Compose v2+ —— version 字段已弃用,应予以省略

services:
  ollama:
    image: ollama/ollama:0.6.5
    container_name: deepseek-ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    restart: unless-stopped

volumes:
  ollama_data:

注:deploy.resources语法需要Docker Compose v2(docker compose,不是旧版docker-compose v1二进制文件)。

运行docker compose up -d后,将模型拉入容器内: 

docker exec -it deepseek-ollama ollama pull deepseek-r1:7b
docker exec -it deepseek-ollama ollama run deepseek-r1:7b "Explain why 0.1 + 0.2 != 0.3 in IEEE 754."

docker exec -it deepseek-ollama nvidia-smi验证GPU访问。如果GPU不可见,请确认NVIDIA容器工具包已安装,且Docker守护进程已重启。

推理优化技术

量化与性能调谐

对于大多数本地部署,Q4_K_M取得了最佳权衡:它将FP16显存需求减半,而社区困惑基准通常显示标准评估中比FP16增长不到2个百分点。自己做个困惑度比较,确认你的工作负载。对于要求更高精度的任务(复杂的数学证明、细致化的代码生成),采用Q5_K_M或 Q8_0 可以提高输出质量,但代价是内存和速度的消耗。

更大的上下文窗口会用更多的显存。Ollama 在其 Modelfile 中设置每个模型的上下文窗口(请用 ollama show deepseek-r1:7b 验证)。在开始ollama serve前设置OLLAMA_NUM_CTX=4096来覆盖它。将上下文增加到4096或8192个令牌,需要相应更多的显存。

在启动ollama serve前,在shell环境中设置的OLLAMA_NUM_THREADS来调整线程计数,而不是在请求时。这只影响CPU执行线程。对于CPU受限的推理部分,应匹配物理核心数而非超线程数;在非正式测试中,仅物理核心配置在CPU受限层中比超线程配置多出约10-20%。

绩效监控

Ollama 的 /api/generate 端点返回包括 total_durationload_durationprompt_eval_countprompt_eval_durationeval_counteval_duration 在内的元数据。这些工具能够精确计算每秒代币数。

// benchmark.mjs — Node.js 性能监控脚本
// 要求 Node.js 18 或更高版本。请通过 `node --version` 进行确认。
const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
const MODEL = process.env.MODEL || "deepseek-r1:7b";

async function benchmark(prompt) {
  const start = Date.now();
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 120_000);

  try {
    const res = await fetch(`${OLLAMA_URL}/api/generate`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        model: MODEL,
        prompt,
        stream: false,
      }),
      signal: controller.signal,
    });

    if (!res.ok) {
      const text = await res.text();
      console.error(`Ollama returned HTTP ${res.status}: ${text}`);
      return;
    }

    const data = await res.json();

    const evalDurSec = data.eval_duration / 1e9;
    const promptDurSec = data.prompt_eval_duration / 1e9;
    const evalTokensPerSec = evalDurSec > 0 ? data.eval_count / evalDurSec : 0;
    const promptTokensPerSec = promptDurSec > 0 ? data.prompt_eval_count / promptDurSec : 0;

    console.log(`Model: ${MODEL}`);
    console.log(`Prompt eval: ${data.prompt_eval_count} tokens at ${promptTokensPerSec.toFixed(1)} t/s`);
    console.log(`Generation: ${data.eval_count} tokens at ${evalTokensPerSec.toFixed(1)} t/s`);
    console.log(`Model load time: ${(data.load_duration / 1e9).toFixed(2)}s`);
    console.log(`Total duration: ${(data.total_duration / 1e9).toFixed(2)}s`);
    console.log(`Wall clock: ${((Date.now() - start) / 1000).toFixed(2)}s`);
    console.log(`
Response:
${(data.response ?? "(no response)").slice(0, 300)}...`);
  } catch (err) {
    if (err.name === "AbortError") {
      console.error("Benchmark timed out after 120s");
    } else {
      console.error("Benchmark error:", err);
    }
  } finally {
    clearTimeout(timeout);
  }
}

benchmark(
  "Solve this step by step: A farmer has 17 sheep. All but 9 die. How many are left? Explain your reasoning."
);

node benchmark.mjs运行(原生fetch需要Node.js 18+)。如果每秒令牌数低于硬件预期,考虑缩小上下文窗口大小、切换到更小的量化,或验证GPU卸载是否激活。

对于需要并发请求或更高吞吐量的工作负载,vLLM 及其服务器模式的llama.cpp在批处理、KV 缓存管理和张量并行方面提供了更细致的控制。这些后端需要更多配置,但根据社区基准和llama.cpp文档,它们在高负载下表现优于Ollama。

构建Node.js + React集成

创建Node.js API 服务器

Express 服务器作为 React 前端与本地 Ollama 实例之间的代理,通过服务器发送事件流式响应。

首先,搭建项目和安装依赖:

mkdir deepseek-server && cd deepseek-server
npm init -y
npm pkg set type=module
npm install express cors

⚠️警告:下面展示的服务器是一个开发代理。在任何网络可访问部署前,先添加认证(例如静态API密钥头检查)、速率限制(例如express-rate-limit)并审查提示长度上限。未认证的推理端点允许网络上的任何人都用来饱和你的GPU资源。

// server.js
// 需要 Node.js 18 或更高版本。请运行 node --version 进行确认。
import express from "express";
import cors from "cors";

const app = express();

const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
app.use(cors({ origin: CORS_ORIGIN }));
app.use(express.json());

const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434";
const MODEL = process.env.MODEL || "deepseek-r1:7b";
const TIMEOUT_MS = parseInt(process.env.TIMEOUT_MS || "120000", 10) || 120000;
const MAX_PROMPT_BYTES = parseInt(process.env.MAX_PROMPT_BYTES || "32768", 10);

app.post("/api/chat", async (req, res) => {
  const { prompt } = req.body;
  if (!prompt) return res.status(400).json({ error: "prompt is required" });
  if (Buffer.byteLength(prompt, "utf8") > MAX_PROMPT_BYTES) {
    return res.status(413).json({ error: "prompt exceeds maximum allowed length" });
  }

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);

  let reader;

  try {
    const ollamaRes = await fetch(`${OLLAMA_URL}/api/generate`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ model: MODEL, prompt, stream: true }),
      signal: controller.signal,
    });

    if (!ollamaRes.ok) {
      clearTimeout(timeout);
      res.write(`data: ${JSON.stringify({ error: "Ollama error", status: ollamaRes.status })}

`);
      return res.end();
    }

    reader = ollamaRes.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value, { stream: true });
      const lines = chunk.split("
").filter(Boolean);
      for (const line of lines) {
        try {
          const parsed = JSON.parse(line);
          res.write(`data: ${JSON.stringify({ token: parsed.response, done: parsed.done })}

`);
        } catch {
          // skip malformed chunks
        }
      }
    }
  } catch (err) {
    if (reader) {
      await reader.cancel().catch(() => {});
    }
    if (err.name === "AbortError") {
      res.write(`data: ${JSON.stringify({ error: "Request timed out" })}

`);
    } else {
      console.error("Stream error:", err);
      res.write(`data: ${JSON.stringify({ error: err.message })}

`);
    }
  } finally {
    clearTimeout(timeout);
    res.write("data: [DONE]

");
    res.end();
  }
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => console.log(`API server running on port ${PORT}`));

构建 React 聊天接口

DeepSeek R1 将其推理步骤包裹在<think>...</think>标签中。前端解析这些内容,以展示推理与最终答案分开。

首先,为React项目做支架:

npm create vite@latest deepseek-ui -- --template react
cd deepseek-ui
npm install

deepseek-ui目录中创建一个.env文件:

VITE_API_URL=http://localhost:3001

然后用导入Chat组件替换src/App.jsx的内容,创建src/Chat.jsx

// src/Chat.jsx
import { useState, useRef, useEffect } from "react";

const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3001";

function parseThinkBlocks(text) {
  const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
  const reasoning = [];
  let match;
  while ((match = thinkRegex.exec(text)) !== null) {
    reasoning.push(match[1].trim());
  }
  thinkRegex.lastIndex = 0;
  const answer = text.replace(thinkRegex, "").trim();
  return { reasoning, answer };
}

export default function Chat() {
  const [prompt, setPrompt] = useState("");
  const [rawResponse, setRawResponse] = useState("");
  const [loading, setLoading] = useState(false);
  const [showReasoning, setShowReasoning] = useState(false);
  const abortRef = useRef(null);

  // Clean up any in-flight stream on unmount
  useEffect(() => {
    return () => abortRef.current?.abort();
  }, []);

  async function handleSubmit(e) {
    e.preventDefault();
    if (!prompt.trim() || loading) return;

    setRawResponse("");
    setLoading(true);
    setShowReasoning(false);

    const controller = new AbortController();
    abortRef.current = controller;

    try {
      const res = await fetch(`${API_URL}/api/chat`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt }),
        signal: controller.signal,
      });

      const reader = res.body.getReader();
      const decoder = new TextDecoder();
      let accumulated = "";
      let done_streaming = false;

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        const text = decoder.decode(value, { stream: true });
        const lines = text.split("
").filter((l) => l.startsWith("data: "));
        for (const line of lines) {
          const payload = line.slice(6);
          if (payload === "[DONE]") { done_streaming = true; break; }
          try {
            const parsed = JSON.parse(payload);
            if (parsed.error) {
              accumulated += `
[Error: ${parsed.error}]`;
            } else if (parsed.token) {
              accumulated += parsed.token;
            }
          } catch {
            // skip
          }
        }
        setRawResponse(accumulated);
        if (done_streaming) break;
      }
    } catch (err) {
      if (err.name !== "AbortError") {
        setRawResponse((prev) => prev + `
[Error: ${err.message}]`);
      }
    } finally {
      setLoading(false);
    }
  }

  const { reasoning, answer } = parseThinkBlocks(rawResponse);

  return (
    <div style={{ maxWidth: 720, margin: "2rem auto", fontFamily: "system-ui" }}>
      <h1>DeepSeek R1 Local Chat</h1>
      <form onSubmit={handleSubmit}>
        <textarea
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          rows={4}
          style={{ width: "100%", fontSize: "1rem", padding: "0.5rem" }}
          placeholder="Enter a reasoning-heavy prompt..."
        />
        <div style={{ marginTop: "0.5rem", display: "flex", gap: "0.5rem" }}>
          <button type="submit" disabled={loading}>
            {loading ? "Generating..." : "Send"}
          </button>
          <button
            type="button"
            onClick={() => abortRef.current?.abort()}
            disabled={!loading}
          >
            Cancel
          </button>
        </div>
      </form>

      {reasoning.length > 0 && (
        <div style={{ marginTop: "1rem" }}>
          <button onClick={() => setShowReasoning(!showReasoning)}>
            {showReasoning ? "Hide" : "Show"} Reasoning ({reasoning.length} block
            {reasoning.length > 1 ? "s" : ""})
          </button>
          {showReasoning && (
            <pre
              style={{
                background: "#f0f4f8",
                padding: "1rem",
                borderRadius: 6,
                whiteSpace: "pre-wrap",
                marginTop: "0.5rem",
                fontSize: "0.9rem",
              }}
            >
              {reasoning.join("

---

")}
            </pre>
          )}
        </div>
      )}

      {answer && (
        <div style={{ marginTop: "1rem", lineHeight: 1.6 }}>
          <h2>Answer</h2>
          <div style={{ whiteSpace: "pre-wrap" }}>{answer}</div>
        </div>
      )}
    </div>
  );
}

本地运行全栈

按顺序启动服务:Ollama(或 Docker 容器),然后是 Node.js 服务器,最后是 React 开发服务器。

# 终端 1:启动 Ollama(仅当其尚未作为系统服务运行时执行)
# 如果 Ollama 是通过安装脚本安装的,且已作为系统服务在运行,
# 请跳过此命令,以避免 11434 端口发生冲突。
# 检查方法:systemctl status ollama (Linux)
ollama serve

# 终端 2:启动 Node.js API 服务器
cd deepseek-server
node server.js

# 终端 3:启动 React 开发服务器
cd deepseek-ui
npm run dev

用一个推理性强的题目来测试,比如“如果球棒和球加起来1.10美元,而球棒比球贵1.00美元,那么球的价格是多少?“请讲理理智。”

常见问题:“连接被拒”通常意味着 Ollama 没有运行或绑定在不同的端口上。内存不足错误表示该型号超出可用显存;降为更小的变体或更低的量化。在模型加载到GPU内存时,初始请求预期会有较慢的第一令牌延迟;后续请求会重用缓存模型。

完整实施清单

  • ☐ 验证硬件符合所选型号的最低要求(见上文显存表)
  • ☐ 安装NVIDIA驱动和CUDA Toolkit 12.x(或确认Apple Silicon统一内存是否足够)
  • ☐ 安装 Ollama 并拉出目标 DeepSeek R1 型号
  • ☐ 验证CLI推断是否适用于测试提示符,并确认<think>标签出现
  • ☐ 使用提供的 docker-compose.yml 设置带有 GPU 直通的 Docker (可选但推荐)
  • ☐ 调谐量化电平(Q4_K_M默认,Q5_K_M或Q8_0以获得更高保真度)和上下文窗口大小
  • ☐ 使用Node.js基准脚本每秒基准测试,并确认可接受的性能
  • ☐ 构建支持 SSE 流媒体的 Node.js Express API 代理
  • ☐ 构建带有<think>块解析和可折叠推理显示的React聊天界面
  • ☐ 在多个领域(数学、逻辑、代码)中,端到端地用复杂的推理提示进行测试
  • ☐ 为生产配置:添加认证、速率限制,并审查API服务器的提示长度上限;添加进程管理器(PM2或SystemD)、结构化错误处理和请求日志

接下来会发生什么

整个流程现在运行在本地硬件上,提示和响应都留在你的机器上。如果需要完全网络隔离,请检查 Ollama 的遥测设置,确认没有传输使用数据。

自然的下一步是:利用LoRA适配器在特定领域数据集上微调蒸馏变体(对于本文所述硬件上的7B和14B提炼变体来说是可行的;更大的变体则需要显著增加显存和训练基础设施)。将RAG管道与本地矢量数据库如ChromaDB集成。将容器化配置部署到私有服务器上,以便全团队访问。Ollama 模型库(ollama.com/library)和 DeepSeek 官方文档提供了更新的模型标签和配置参考。

为了找到适合你工作负载的质量和吞吐量的平衡,可以用本指南中的基准测试脚本对两个量化水平运行,并在固定评估集中比较输出质量。

标签: DeepSeek  
本站声明:本站为非经营性网站,文章内容来源或整理于网络,本站不提供软件下载服务,侵删联系:webkaka#foxmail.com
相关文章
    x