一次真实的排查记录:将 530M 的机器学习栈移出 Flask 主进程,并通过性能分析和缓存无意间使检索速度提升了 32 倍。

Stars

8

7 天增长

暂无数据

Fork 数

1

开放 Issue

0

开源协议

MIT

最近更新

2026-07-27

AI 仓库情报摘要
FR-AI / ANALYSIS

为什么值得关注

展示了系统化的诊断方法(先测量后猜测)、基于风险的架构决策,以及提供可复现的代码和基准测试。

适合谁使用

  • Python Web 开发者
  • 负责内存优化的 DevOps 工程师
  • 在低资源环境下部署模型的 ML 工程师
  • 关注性能分析和缓存策略的开发者

典型使用场景

  • 优化带有 ML 组件的低内存 Flask 应用
  • 将重型模型推理分离为独立微服务
  • 通过分析和缓存修复意外的性能瓶颈(BM25 索引重建)
  • 实现安全的降级和重试策略以应对服务不可用

项目优势

  • Flask RSS 从 888M 降至 68M(稳定)
  • 语义检索延迟从 10.99s 降至 0.34s(快 32 倍)
  • 结果一致验证(逐位一致)
  • 清晰的权衡考量(风险 vs 收益,可回滚)

使用前须知

  • 依赖 systemd 实现空闲进程退出(5 秒重启窗口)
  • 冷启动需要约 20 秒(模型加载 8s + BM25 索引重建 11.5s)
  • BM25 缓存占用约 60M 内存,尚未优化为磁盘持久化
  • 解决方案针对特定应用架构(Flask + BM25 + sentence‑transformers)

README 快速开始

把 530M 的 ML 栈请出 Flask 主进程,顺便让检索快了 32 倍

一次真实的排查记录。起因是一个跑在 3.6G 内存小机器上的 Flask 应用,进程内存三天涨到 888M。

结论先放这里,省得你翻到最后:

之前之后
Flask 主进程 RSS888M(三天)68M(稳定)
一次语义检索10.99s0.34s
检索结果逐位一致,没变

两件事其实没什么关系,是排查过程中前后脚撞上的。但它们合起来说明一个道理:动手优化之前先测量,你以为的瓶颈通常不是瓶颈。我在这篇里两次猜错,都写出来了。


一、现象

一个 Flask 应用,功能是笔记 + 对话,带语义检索(bge 中文小模型做 embedding,BM25 做关键词兜底)。机器 3.6G 内存。

现象是主进程 RSS 一路涨:

重启后          79M
17 分钟后      662M
三天后         888M

看起来像内存泄漏。但"看起来像"不等于是。

二、第一步:先证伪,别急着抓泄漏

如果真是泄漏,那一定有某个代码路径反复执行、每次留下一点。最省事的验证方式是按真实流量比例打请求,看 RSS 和对象数怎么走

我没用 py-spy、没上 tracemalloc,就用 Flask 自带的 test_client 在独立进程里跑:

import gc, collections, time

def rss_kb():
    for line in open('/proc/self/status'):
        if line.startswith('VmRSS:'):
            return int(line.split()[1])

def obj_profile():
    c = collections.Counter()
    for o in gc.get_objects():
        c[type(o).__name__] += 1
    return c

import app as A
client = A.app.test_client()

gc.collect()
base_rss, base_obj = rss_kb(), obj_profile()

# 1) 先测后台线程空转:什么都不请求,只等
time.sleep(45)
gc.collect()
print('空转 45 秒:', (rss_kb() - base_rss) / 1024, 'M')

# 2) 再按真实流量比例逐个端点压
for method, url in ENDPOINTS:
    gc.collect(); before = rss_kb()
    for _ in range(150):
        client.get(url)
    gc.collect(); after = rss_kb()
    print(f'{url:42} {(after-before)/1024:+.1f}M')

# 3) 最后看对象类型的净增长
gc.collect()
for name, n in (obj_profile() - base_obj).most_common(12):
    print(f'{name:28} +{n}')

完整脚本在 code/memprobe.py

结果:

空转 45 秒后                    +0.3M
/api/notes_by_date              +0.3M
/api/profile                    -0.3M
/api/feed                       +0.8M
/api/tags/list                  +0.0M
/api/messages                   +2.0M
/api/shelf                      +0.0M
...
1200 个请求总计                 +3M

对象数增长 top:
  function                     +23
  ReferenceType                +19
  builtin_function_or_method   +19
  SplitResult                  +18
  list                          +4

读接口是干净的。 1200 个请求只涨 3M,对象增长几十个而且全是模块导入的正常副产物(functionModuleSpecSourceFileLoader 这些是首次走到某分支时 import 带进来的,一次性)。后台线程空转也干净。

那 888M 从哪来的?

三、分层测:不是泄漏,是一次性加载

既然不是反复累积,那就是某个东西一次性吃掉一大块然后赖着不走。分层测最直接:

def rss_mb():
    for line in open('/proc/self/status'):
        if line.startswith('VmRSS:'):
            retu

项目描述

把 530M 的 ML 栈请出 Flask 主进程,顺便让语义检索快了 32 倍。一次真实排查的完整记录:怎么证伪内存泄漏、为什么拆进程而不换 ONNX、以及 profile 如何打脸我的直觉。

相关仓库与替代方案

根据分类、Topic 和编程语言匹配的相似项目。

slvDev
精选
slvDev GitHub avatar

esp32-ai

A 28.9 million parameter language model runs on an $8 ESP32-S3 microcontroller entirely on-device, generating simple stories at about 9.5 tokens per second.

AI 与机器学习大语言模型
1,960
gavamedia
精选
gavamedia GitHub avatar

deltafin

Deltafin is a research project that runs the 2.8-trillion-parameter Mixture-of-Experts model Kimi K3 on a single Apple Silicon Mac (e.g., M1 Max with 64 GB) at about 16 seconds per token, using exact, reproducible inference with local or streaming expert loading.

AI 与机器学习大语言模型
304
jamesob
精选
jamesob GitHub avatar

local-llm

A comprehensive guide for building and configuring a high-end local machine to run state-of-the-art LLMs, with detailed hardware choices, BIOS tuning, and Docker-based model serving.

AI 与机器学习大语言模型
1,660