Files
Docs/DevLogs/Day20.md
2025-12-31 16:18:28 +08:00

375 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Day 20 - 性能瓶颈深度分析与优化
**日期**: 2025-12-24
**主题**: 解决导盲系统开启后卡顿问题
---
## 问题诊断
### 症状
- 导盲系统一开启就卡顿、不流畅、有延迟
- GPU 利用率仅 7%
- 帧处理 FPS 仅 3-4
### 确认的瓶颈(经过再三分析)
| 瓶颈 | 影响 | 状态 |
|------|------|------|
| **过量日志输出** | `_detect_obstacles` 每次调用输出 20+ 行 logger.info | ✅ 已修复 |
| **GPU Semaphore 限流** | 默认只允许 2 个并发 GPU 调用 | ✅ 已修复 |
| **检测串行执行** | 盲道和障碍物检测顺序执行 | ✅ 已修复 |
---
## 实施的优化
### 1. 精简日志输出 (`workflow_blindpath.py`)
**修改前**:每次障碍物检测输出 20+ 行日志
```python
logger.info(f"[_detect_obstacles] 开始执行...")
logger.info(f"[_detect_obstacles] 调用...")
for obj in detected_obstacles:
logger.info(f"物体 {i+1}...")
logger.info(f" - 类别: ...")
logger.info(f" - 面积: ...")
# 每个障碍物 5-6 行!
```
**修改后**:只输出一行摘要(每 30 帧)
```python
if detected_obstacles and self.frame_counter % 30 == 0:
names = [o.get('name', '?') for o in detected_obstacles[:3]]
logger.info(f"[障碍物] 检测到 {len(detected_obstacles)} 个: {names}")
```
### 2. 增加 GPU 并发槽位 (`obstacle_detector_client.py`)
```python
# 修改前
GPU_SLOTS = int(os.getenv("AIGLASS_GPU_SLOTS", "2"))
# 修改后
GPU_SLOTS = int(os.getenv("AIGLASS_GPU_SLOTS", "4"))
```
### 3. GPU 并行检测 (`gpu_parallel.py`)
使用 CUDA Stream 让盲道检测和障碍物检测并行执行。
### 4. TTS WebSocket 断连修复 (`app_main.py`)
**问题**TTS 语音有时不播放,日志显示 `[TTS->WS] Buffering TTS audio`
**原因**`set_tts_websocket(ws)` 只在 `start_ai_with_text()` 时调用WebSocket 重连后引用丢失
**修复**:在 `ws_audio` 连接建立时立即保存引用
```python
@app.websocket("/ws_audio")
async def ws_audio(ws: WebSocket):
global esp32_audio_ws
esp32_audio_ws = ws
from audio_stream import set_tts_websocket
set_tts_websocket(ws) # Day 20: 连接时立即保存
await ws.accept()
```
---
## 修改文件
| 文件 | 修改 |
|------|------|
| `workflow_blindpath.py` | 精简日志输出 |
| `obstacle_detector_client.py` | GPU 槽位 2→4 |
| `gpu_parallel.py` | 新建CUDA Stream 并行 |
| `app_main.py` | TTS 修复、性能诊断 |
---
## 预期效果
- **日志 I/O 减少 95%**:每次检测从 20 行减到 1 行
- **GPU 并发能力翻倍**4 槽位 vs 2 槽位
- **检测延迟减半**:并行执行 vs 串行执行
---
## 可视化网页优化
### UI 改进
| 优化项 | 说明 |
|--------|------|
| **IMU 浮窗** | 宽度 600px添加可折叠功能 |
| **折叠状态优化** | 折叠后只显示标题和按钮,完全隐藏 3D 模型和数据面板 |
| **Badge 动画** | 连接中闪烁 (blink)、已连接脉冲 (pulse) |
| **按钮美化** | 渐变背景 + 悬停上浮效果 |
| **移动端适配** | @media 600px/1100px 响应式布局 |
| **状态中文化** | `📷 已连接` 替代 `Camera: connected` |
| **底部边界修复** | 移除 `.chat { height: 100vh }` 解决超出问题 |
| **背景色统一** | `.stage` 使用 `var(--card)` 与右侧一致 |
| **Favicon 添加** | 添加 `/static/favicon.png` |
### 折叠功能实现
```css
/* 折叠状态 - 只显示标题和按钮 */
.imu-float.collapsed {
width: 180px;
height: 40px;
overflow: hidden;
}
.imu-float.collapsed .imu-row,
.imu-float.collapsed #imu_top_status {
display: none !important;
}
```
```javascript
// JS 折叠逻辑
document.addEventListener('DOMContentLoaded', () => {
const imuFloat = document.getElementById('imuFloat');
const imuToggle = document.getElementById('imuToggle');
imuToggle.onclick = function(e) {
const isCollapsed = imuFloat.classList.toggle('collapsed');
this.textContent = isCollapsed ? '+' : '';
};
});
```
### 修改文件
| 文件 | 修改 |
|------|------|
| `templates/index.html` | 样式优化、折叠按钮、移动端适配、底部边界修复、Favicon |
| `static/main.js` | setBadge 支持 connecting、折叠逻辑、数据面板优化 |
| `static/favicon.png` | 新增网站图标 |
---
## 待验证
部署后观察:
```
[PERF] 帧:60 | 客户端FPS:?? | 帧间隔:??ms | 广播:??ms | 导航:??ms
```
目标:`导航` 耗时 < 80ms
---
## Day 20 追加修复2025-12-24 下午)
### 问题复现
上午优化后,导航仍有严重卡顿。日志分析发现:
| 帧数 | 导航耗时 |
|------|----------|
| 120 | 245.7ms |
| 240 | **1410.1ms** |
| 420 | **1571.5ms** |
| 660 | **1766.0ms** |
**根因**: CUDA Stream 并行检测未生效,两个模型仍串行执行。
### 追加修复
| 文件 | 修改 |
|------|------|
| `workflow_blindpath.py` | `UNIFIED_DETECTION_INTERVAL` 10→20帧 |
| `workflow_blindpath.py` | `OBSTACLE_CACHE_DURATION_FRAMES` 12→20帧 |
| `gpu_parallel.py` | 移除无效 CUDA Stream改用 ThreadPoolExecutor 真正并行 |
### 预期效果
- 检测频率减半 → GPU 负载降低
- 线程池并行 → 盲道和障碍物检测真正同时执行
- 导航耗时目标:稳定在 **200ms 以下**
---
## Day 20 可视化性能优化(下午)
### 问题复现
追加修复后,导航耗时仍为 267-501ms。服务器资源分析发现
| 指标 | 值 | 含义 |
|------|-----|------|
| app_main.py CPU | **120%** | 超过1核但受 Python GIL 限制 |
| GPU 使用率 | **8%** | GPU 等待 CPU利用率低 |
| 可视化耗时 | **200-300ms** | 主要瓶颈! |
**根因**: `_draw_visualizations` 中的 numpy 逐像素半透明混合非常耗 CPU。
### 优化内容
| 文件 | 修改 |
|------|------|
| `workflow_blindpath.py` | mask 半透明填充 → 轮廓绘制 |
| `workflow_blindpath.py` | 移除 `image.copy()` 避免复制开销 |
### 代码对比
```diff
# 修改前(慢 ~200-300ms
- for c in range(3):
- local_region[:, :, c] = np.where(
- binary_mask > 0,
- (1 - alpha) * local_region[:, :, c] + alpha * color_overlay[:, :, c],
- local_region[:, :, c]
- )
# 修改后(快 ~5-10ms
+ cv2.polylines(image, [points], isClosed=True, color=color, thickness=thickness)
```
### 预期效果
- 可视化耗时200-300ms → **~10-20ms**
- 导航总耗时目标:**~100-150ms**
---
## Day 20 Numba 多核加速
### 问题背景
Python GIL 导致无法利用多核 CPU。即使使用 ThreadPoolExecutorCPU 密集型的 numpy 操作也只能使用单核。
### 解决方案
引入 **Numba JIT 编译**,将 Python 代码编译为机器码并启用多核并行:
```python
from numba import jit, prange
@jit(nopython=True, parallel=True, cache=True)
def compute_mask_stats_numba(mask: np.ndarray) -> tuple:
for i in prange(h): # 自动多核并行
for j in range(w):
if mask[i, j] > 0:
...
```
### 新增文件
| 文件 | 说明 |
|------|------|
| `numba_utils.py` | Numba 加速工具函数mask 像素计数、统计、交集计算 |
### 修改内容
| 文件 | 修改 |
|------|------|
| `obstacle_detector_client.py` | 使用 Numba 加速 mask 统计和交集计算 |
| `app_main.py` | 启动时预热 Numba JIT 编译 |
### 预期效果
- **多核利用率提升**:从单核 ~120% 分摊到多核
- **mask 操作加速**numpy 操作 → Numba 编译后 10-100x 提升
- **首次调用无延迟**:启动时预热 JIT 编译
---
## Day 20 TensorRT 加速集成 (17:30)
### 问题背景
为了进一步加速 GPU 推理,将 YOLO 模型导出为 TensorRT 引擎。
### 实施内容
| 步骤 | 说明 |
|------|------|
| 模型导出 | `yolo-seg.engine``yoloe-11l-seg.engine``trafficlight.engine` (FP16) |
| 工具函数 | `model_utils.py` - `get_best_model_path()` 自动选择 .engine |
| 代码适配 | 全部模型加载点改用工具函数 |
### 遇到的问题与修复
#### 问题1`.to()` 和 `.fuse()` 不兼容 TensorRT
```
TypeError: model='model/yolo-seg.engine' should be a *.pt PyTorch model
```
**原因**TensorRT 引擎已经在 GPU 上,不能调用 `.to("cuda")``.fuse()`
**修复**:添加 `is_tensorrt_engine()` 检测函数,条件跳过
| 文件 | 修改 |
|------|------|
| `model_utils.py` | 新增 `is_tensorrt_engine()` 函数 |
| `app_main.py` | 跳过 `.to()``.fuse()` |
| `models.py` | 跳过 `.to()``.fuse()` |
| `yoloe_backend.py` | 跳过 `.to()``get_text_pe()` |
| `obstacle_detector_client.py` | 跳过 `.to()``.fuse()` |
| `workflow_crossstreet.py` | 跳过 `.to()` |
#### 问题2`StopIteration` 错误
```
print(f"[NAVIGATION] 模型设备: {next(obstacle_detector.model.parameters()).device}")
StopIteration
```
**原因**TensorRT 引擎没有 `.parameters()` 迭代器
**修复**:检测 TensorRT 模式时跳过设备打印
#### 问题3推理尺寸不匹配
```
[GPU_PARALLEL] 盲道检测失败: input size torch.Size([1, 3, 640, 640]) not equal to max model size (1, 3, 480, 480)
```
**原因**TensorRT 引擎用 480x480 导出,但代码硬编码 `imgsz=640`
**修复**:统一使用环境变量 `AIGLASS_YOLO_IMGSZ=480`
| 文件 | 修改 |
|------|------|
| `yoloe_backend.py` | 默认 `imgsz` 改为从环境变量读取 |
| `yolomedia.py` | 移除 4 处硬编码 `imgsz=640` |
### 修复后状态
服务启动成功TensorRT 引擎正常加载:
```
[NAVIGATION] TensorRT 引擎已加载,跳过 .to() 和 .fuse()
[TRT] Loaded engine size: 140 MiB
```
---
## ⚠️ 待验证问题 (Day 21 继续)
### 1. 语音识别停止响应 🔴
**症状**:停止盲道导航后,语音指令不再被识别
**日志分析**
- 音频仍在接收:`[AUDIO] 📥 Received: 8900 packets...`
- 但没有 ASR 输出
**可能原因**
- ASR 状态逻辑问题
- 需要检查 `state=CHAT` 时 ASR 是否正常处理
### 2. TensorRT 预热警告(非致命)
```
[NAVIGATION] 模型预热失败: input size torch.Size([1, 3, 640, 640]) not equal to max model size (1, 3, 480, 480)
```
预热代码尝试用 640x640但引擎用 480 导出。实际推理正常,仅预热失败。
### 3. 画面延迟约 2 秒
导航启动后画面流畅但有约 2 秒延迟,需进一步分析是网络还是处理延迟。