慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

博客园 - 司徒正美
V
V2EX
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
Blog — PlanetScale
Blog — PlanetScale
A
About on SuperTechFans
月光博客
月光博客
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
The GitHub Blog
The GitHub Blog
V
Visual Studio Blog
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python)
使用PaliGemma的實時視頻分類:低延遲VLM推論的架構模式
Pasquale Mol · 2026-05-24 · via DEV Community

在前一篇文章中,我們對三個開源視覺語言模型進行基準測試,針對零樣本目標檢測,得出了令人不適的結論:即使是最快的競爭者 Phi-3.5-vision-instruct,在NVIDIA L4上每個畫面也需要4.45秒。LLaVA-v1.6則需要8.13秒。對於任何需要處理實時視頻流的應用,這些數據都足以使其失去競爭力。但VLMs與實時工作負載根本不兼容的結論值得進一步審視。那個8秒的數據是在一個通用目的的零樣本檢測任務上測量的,要求模型對任意場景中的任意物體進行推理。當你限制問題的範圍時會怎麼樣?當你給模型一個封閉詞彙表、固定解析度、確定性解碼策略和非阻塞性推理管道時?
本文回答了這個問題。使用 Google 的緊湊型視覺語言模型 PaliGemma,我們建立了一個實時視頻分類系統,在 NVIDIA RTX A4500 上每個畫面約耗時 0.8 到 1.2 秒。這相較於 LLaVA 在同等專業硬體上的表現提升了六到八倍,完全通過架構決策實現,而非硬體升級。以下是促使其成為可能的四種模式。

為何 PaliGemma

在探討架構之前,模型本身的選擇值得解釋,因為相較於其實際價值,PaliGemma 在開發者文獻中的出現率顯著偏低。PaliGemma 是由 Google 建立的、具有 30 億參數的視覺語言模型,結合了 SigLIP 視覺編碼器與 Gemma 語言主幹。與 LLaVA-7B 或 Phi-3.5-vision 相比,它的規模大約只有後者的半數,這直接翻譯為在相同硬體上更低的 VRAM 消耗和更快的推論速度。對於分類任務來說,更重要的是,它在包括圖片標註、視覺問答和物體定位在內的廣泛視覺理解基準上進行了明確的微調,這意味著它對於我們即將引導的那種受限、結構化的回應具有強大的先驗知識。

import torch
from transformers import PaliGemmaProcessor, PaliGemmaForConditionalGeneration

MODEL_PATH = "./paligemma_offline"

model = PaliGemmaForConditionalGeneration.from_pretrained(
    MODEL_PATH,
    torch_dtype=torch.bfloat16,
    device_map="auto",
    local_files_only=True
).eval()

processor = PaliGemmaProcessor.from_pretrained(
    MODEL_PATH,
    local_files_only=True
)

進入全螢幕模式 退出全螢幕模式

這裡有兩點值得注意。使用 bfloat16 載入而不是 float32 能將 VRAM 腳印減半,且在分類任務上精度損耗可忽略不計。local_files_only=True 旗標不僅僅是離線環境的便利:在生產系統中,它消除了初始化時的網絡往返,並保證您的推論環境完全可重現。

模式 1:解析度作為延遲旋鈕

在實時 VLM 流程中,影響最大的單一決策是輸入解析度。VLMs 通過將圖像分為塊並將每個塊編碼為一系列標記來處理圖像。一個 1280×720 的畫面產生的標記序列遠大於一個 448×448 的裁剪,並且由於轉換器注意力與序列長度呈平方關係,解析度並非線性成本:它是一種指數成本。在零樣本物體檢測中,空間精度很重要,下掃描是一個真正的權衡。但對於場景級分類任務,你問的是「這個畫面的主要情緒是什麼?」而不是「給我每個物體的像素坐標」,448×448 保留了足夠多的語義信息。

import cv2
import numpy as np
from PIL import Image

def preprocess_frame(frame_bgr: np.ndarray) -> Image.Image:
    # Resize to inference resolution before any VLM processing
    frame_small = cv2.resize(frame_bgr, (448, 448))
    # Convert BGR (OpenCV) to RGB (PIL/transformers)
    return Image.fromarray(cv2.cvtColor(frame_small, cv2.COLOR_BGR2RGB))

進入全螢幕模式 離開全螢幕模式

關鍵洞見在於,解析度應根據您的任務實際所需的信息粒度來選擇,而不是基於輸入流的解析度。如果您的相機以1080p解析度捕捉,但您的分類任務只需要區分五種情緒狀態,那麼您正在支付一筆龐大的計算代價,以換取您永遠不會使用的信息。

模式 2:封閉詞彙的確定性解碼

標準 VLM 使用將模型視為無限文本生成器。你提示它,它從概率分佈中採樣,你接收到一個自然語言的回應,然後你解析它。這是我們在上一篇文章中討論的脆弱性問題的來源,它也是延遲的一個重要來源:高概率分佈的採樣max_new_tokens 意味著模型為每個它生成的詞元運行完整的自回歸迴圈。在分類任務中,您可以完全打破這一點。不是要求模型描述它所看到的事物,而是將其輸出限制為一組固定的有效標籤詞彙,並限制生成為表達其中一個所需的最少詞元數量。

# A generic set of states tailored to your specific domain
VALID_CLASSES = ['active', 'idle', 'error', 'offline', 'unknown']

def classify_frame(model: torch.nn.Module, processor, image: Image.Image) -> str:
    prompt = (
        f"<image> What is the current operational state shown in this frame? "
        f"You MUST choose ONLY ONE from: {VALID_CLASSES}."
    )

    inputs = processor(
        text=prompt,
        images=image,
        return_tensors="pt"
    ).to(model.device, model.dtype)

    with torch.inference_mode():
        output_ids = model.generate(
            **inputs,
            max_new_tokens=10,   # A single class label needs at most 2-3 tokens
            do_sample=False      # Greedy decoding: deterministic, faster, no temperature needed
        )

    raw_output = processor.decode(
        output_ids[0][inputs.input_ids.shape[-1]:],
        skip_special_tokens=True
    ).strip().lower()

    # Sanitize to alphabetic characters only
    return ''.join(filter(str.isalpha, raw_output))

Enter fullscreen mode Exit fullscreen mode

設定do_sample=False 切換到貪婪解碼模式,該模式在每個步驟中總是選擇最高概率的標記。這消除了採樣開銷,並使輸出完全確定性:相同的輸入總是產生相同的輸出,這對於調試和我們下一節將涵蓋的時間平滑模式至關重要。Themax_new_tokens=10 cap 意味著模型在產生標籤後幾乎立即停止,而不是繼續產生沒有人要求的解釋性文本.
結果是,你正在使用一個 3B-參數的 VLM 作為一個高度能夠、具有語義感知能力的分類器,而不是一個生成模型。你獲得了自然語言提示的零樣本靈活性,以及接近專用分類頭的推理特性。

模式 3:時間平滑以確保預測穩定性

即使採用確定性解碼,處理實時視頻的 VLM 也會產生雜訊預測。光照變化、運動模糊、部分隱藏和暫態視覺artifacts會導致模型在連續幀之間輸出不一致的標籤。如果你將原始的每幀預測直接輸送到下游系統,你會得到抖動、不可靠的輸出。解決方案是時間平滑:而不是信任任何單一預測,你維持一個最近預測的滾動窗口,並發出多數投票。

from collections import deque, Counter

class TemporalSmoother:
    def __init__(self, window_size: int = 5):
        self.history = deque(maxlen=window_size)

    def update(self, prediction: str) -> str:
        self.history.append(prediction)
        # Return the most common prediction in the window
        return Counter(self.history).most_common(1)[0][0]

進入全螢幕模式 退出全螢幕模式

在我們的推理率下,5 強的視窗約等於 4 到 6 秒的時間上下文。這足以吸收瞬時雜訊,同時仍能對真實狀態變化做出反應。視窗大小是主要的調整參數:較大的視窗更穩定但反應較慢;較小的視窗反應更迅速但雜訊較多。對於大多數分類任務,3 到 7 強涵蓋了實際範圍。

模式 4:使用解耦共享狀態架構的非阻擋推論

前三種模式優化了推論調用本身。這一種則解決了一個更基礎的系統問題:一個耗時 0.8 到 1.2 秒的 VLM 推論調用,將會在其運行的整個期間阻擋任何線程。如果你的視頻捕捉和你的推論在相同的線程上運行,你的串流將會在推論速率而不是攝像頭速率上卡頓。

天真解法是使用標準的 Python 队列Queue來在線程間傳遞畫面格。然而,這會引發消費者競爭:如果一個渲染線程和一個AI線程都從同一個隊列讀取,它們會消耗畫面格,互相竊取數據,導致嚴重的視覺卡頓和跳過推理週期。生產級解法是非同步共享狀態模式。 配合粒度式鎖定。影片捕捉執行緒扮演生產者角色,不斷覆寫共享的「最新影格」指標。渲染執行緒(在主執行緒上運行,這是 macOS 和 Wayland 上 OpenCV UI 操作的必須條件)和 AI 背景執行緒扮演獨立消費者角色,當它們準備進行下一個循環時,會將最新影格複製到本地記憶體中。

import threading
import time
import numpy as np
import cv2
import torch
from typing import Optional, Any

class SharedState:
    """
    Thread-safe state container.
    The lock is strictly granular: it is only held for memory assignment/copying,
    never during expensive I/O or AI inference operations.
    """
    def __init__(self):
        self.latest_frame: Optional[np.ndarray] = None
        self.prediction: str = "WAITING"
        self.lock = threading.Lock()
        self.running: bool = True

shared = SharedState()

def video_capture_worker(source: int = 0) -> None:
    """Reads frames at hardware speed and updates the shared state."""
    cap = cv2.VideoCapture(source)

    while shared.running:
        ret, frame = cap.read()
        if not ret:
            time.sleep(0.01)
            continue

        with shared.lock:
            # Overwrite with the freshest data.
            # Pointer assignment is fast enough to barely hold the lock.
            shared.latest_frame = frame

    cap.release()

def inference_worker(model: torch.nn.Module, processor: Any) -> None:
    """Consumes the latest frame at the AI's maximum throughput rate."""
    smoother = TemporalSmoother(window_size=5)

    while shared.running:
        with shared.lock:
            # Deep copy to prevent OpenCV from mutating the array during inference
            frame = shared.latest_frame.copy() if shared.latest_frame is not None else None

        if frame is None:
            time.sleep(0.05)
            continue

        try:
            image = preprocess_frame(frame)
            raw_pred = classify_frame(model, processor, image)
            smoothed_pred = smoother.update(raw_pred)

            with shared.lock:
                shared.prediction = smoothed_pred

        except torch.cuda.OutOfMemoryError:
            # Handle temporary VRAM spikes gracefully without killing the thread
            with shared.lock:
                shared.prediction = "OOM_ERROR"
            time.sleep(1.0)

        except Exception as e:
            # Catch corrupt frames or tensor mismatches
            with shared.lock:
                shared.prediction = "ERROR"

# Initialize background workers as Daemon threads
threads = [
    threading.Thread(target=video_capture_worker, args=(0,), daemon=True),
    threading.Thread(target=inference_worker, args=(model, processor), daemon=True),
]

for t in threads:
    t.start()

# Main Thread UI Loop
# UI libraries (cv2.imshow) must run on the main thread to prevent OS-level crashes.
while shared.running:
    with shared.lock:
        frame = shared.latest_frame.copy() if shared.latest_frame is not None else None
        label = shared.prediction

    if frame is not None:
        cv2.putText(frame, label.upper(), (30, 50),
                    cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3)
        cv2.imshow("Live Classification", frame)

    # 30 FPS rendering limit (33ms) + graceful shutdown
    if cv2.waitKey(33) & 0xFF == ord('q'):
        shared.running = False

cv2.destroyAllWindows()

Enter fullscreen mode 退出全螢幕模式

這裡的關鍵設計原則是粒度化鎖:鎖被獲取,numpy 陣列在記憶體中複製(這需要微秒級別的時間),然後鎖立即釋放。在跨越一秒的 VLM 推理調用中持有鎖會將所有三個組件序列化,並摧毀整個架構的目標。使用這種結構,您的視頻捕捉線程以硬件頻率運行(例如,30 fps),您的渲染迴圈以 30 fps 顯示幀,而您的推理線程以自己的非同步速率運行(1 fps)。這三個系統在時間上是獨立的,僅受其各自的硬件限制所約束。

基準總結

在NVIDIA RTX A4500(20GB GDDR6,Ampere架構)上運行完整流程,使用PaliGemma以bfloat16在三分流實時視頻場景中產生高度穩定的性能配置。將輸入解析度限制為448 × 448,並透過貪婪解碼策略將輸出上限設定為最多10個新標記(do_sample=False), 系統達到每個畫面的推論延遲在0.8到1.2秒之間。結合5個畫面的時序平滑窗口,這個配置確保可靠的狀態分類,而解耦的架構允許視頻捕捉線程維持穩定的25 fps,完全獨立於推論瓶頸。

為了比較,LLaVA-v1.6-Mistral-7B 在NVIDIA L4上執行開放詞彙零樣本檢測,每個畫面需要8.13秒。雖然硬體並非直接等價,但差距的幅度證實,是架構限制而非原始計算能力,解釋了絕大多數的差異.

當這個架構有道理時

當你的任務可以被歸類到一個固定的標籤集中、你需要對實時串流進行持續處理而不是對靜態圖像進行批量分析、數據隱私要求禁止將畫面帧發送到外部 API、且你能夠容忍毫秒級別而不是百毫秒級別延遲時,這種模式是極佳的匹配。當你需要傳遞帶速度的真正實時響應時,這並不是正確的工具,在那種情況下,你需要回到 YOLO 的領域,或者使用像前一篇文章中描述的那樣的流程:利用 VLM 在一整夜內自動標註數據集,然後訓練一個專用的輕量級分類器進行生產部署。

結論

「VLMs 太慢無法處理影片」與「VLMs 能在生產影片流程中運作」之間的差距,並非主要是由硬體問題造成的。這是一個架構上的問題。選擇像 PaliGemma 這樣的精簡模型,而非 7B-參數的替代方案,將解析度限制在您的任務實際所需的範圍內,強制在封閉詞彙上進行確定性解碼,在時間上平滑預測,以及將推論與捕捉和渲染解耦:這些都不需要更大型的 GPU。它們需要仔細思考您實際上要求模型做什麼,並根據這個限制來建立您的流程,而不是與其對抗。

從模型載入到多線程推論的完整模式,不到150行的Python程式碼。這對於在實時視頻串流上進行零樣本語義分類來說,是一個合理的價格。