Spaces:
Running
on
Zero
Running
on
Zero
| # 🎯 Spaces GPU 最佳实践指南 | |
| ## 📚 spaces.GPU 工作原理 | |
| ### 架构概览 | |
| ``` | |
| ┌─────────────────────────────────────────────────────────┐ | |
| │ 主进程 (Main Process) │ | |
| │ - CPU 环境 │ | |
| │ - ❌ 不能初始化 CUDA │ | |
| │ - ✅ 可以创建 Gradio UI │ | |
| │ - ✅ 可以创建 ModelInference 实例(但不加载模型) │ | |
| └─────────────────────────────────────────────────────────┘ | |
| │ | |
| │ 调用 @spaces.GPU 装饰的函数 | |
| │ | |
| ▼ | |
| ┌─────────────────────────────────────────────────────────┐ | |
| │ 子进程 (GPU Worker Process) │ | |
| │ - GPU 环境 │ | |
| │ - ✅ 可以初始化 CUDA │ | |
| │ - ✅ 可以加载模型到 GPU │ | |
| │ - ✅ 运行推理 │ | |
| │ - ✅ 全局变量缓存(每个子进程独立) │ | |
| └─────────────────────────────────────────────────────────┘ | |
| │ | |
| │ pickle 序列化返回值 | |
| │ | |
| ▼ | |
| ┌─────────────────────────────────────────────────────────┐ | |
| │ 主进程接收返回值 │ | |
| │ - ✅ 必须是 CPU 数据(numpy, 基本类型) │ | |
| │ - ❌ 不能包含 CUDA 张量 │ | |
| └─────────────────────────────────────────────────────────┘ | |
| ``` | |
| ## ✅ 最佳实践:模型加载策略 | |
| ### ❌ 错误做法 1:主进程加载模型 | |
| ```python | |
| # ❌ 错误:在主进程加载模型 | |
| class EventHandlers: | |
| def __init__(self): | |
| self.model_inference = ModelInference() | |
| # ❌ 如果在主进程调用这个,会触发 CUDA 初始化错误 | |
| self.model_inference.initialize_model("cuda") # 💥 | |
| ``` | |
| **为什么错误?** | |
| - 主进程不能初始化 CUDA | |
| - 会立即报错:`CUDA must not be initialized in the main process` | |
| ### ❌ 错误做法 2:实例变量存储模型 | |
| ```python | |
| # ❌ 错误:使用实例变量存储模型 | |
| class ModelInference: | |
| def __init__(self): | |
| self.model = None # ❌ 实例变量 | |
| def initialize_model(self, device): | |
| if self.model is None: | |
| self.model = load_model() # ❌ 保存在实例中 | |
| return self.model | |
| ``` | |
| **为什么错误?** | |
| - 实例在主进程创建 | |
| - 模型状态可能跨进程混乱 | |
| - 第二次调用时状态不确定 | |
| ### ✅ 正确做法:子进程全局变量缓存 | |
| ```python | |
| # ✅ 正确:使用全局变量在子进程中缓存 | |
| _MODEL_CACHE = None # 全局变量,每个子进程独立 | |
| class ModelInference: | |
| def __init__(self): | |
| # ✅ 不存储任何状态 | |
| pass | |
| def initialize_model(self, device: str = "cuda"): | |
| global _MODEL_CACHE | |
| if _MODEL_CACHE is None: | |
| # ✅ 在子进程中加载(第一次调用时) | |
| print("Loading model in GPU subprocess...") | |
| model_dir = os.environ.get("DA3_MODEL_DIR", "...") | |
| _MODEL_CACHE = DepthAnything3.from_pretrained(model_dir) | |
| _MODEL_CACHE = _MODEL_CACHE.to(device) # ✅ 在子进程中移动 | |
| _MODEL_CACHE.eval() | |
| else: | |
| # ✅ 复用缓存的模型 | |
| print("Using cached model") | |
| return _MODEL_CACHE # ✅ 返回模型,不存储 | |
| ``` | |
| **为什么正确?** | |
| - ✅ 模型只在子进程加载(GPU 环境) | |
| - ✅ 全局变量在子进程内安全(每个子进程独立) | |
| - ✅ 不污染主进程 | |
| - ✅ 可以缓存复用(避免重复加载) | |
| ## 🎯 完整实现示例 | |
| ### 文件结构 | |
| ``` | |
| app.py # 主入口,配置 @spaces.GPU | |
| depth_anything_3/app/modules/ | |
| ├── model_inference.py # 模型推理(使用全局变量) | |
| └── event_handlers.py # 事件处理(主进程,不加载模型) | |
| ``` | |
| ### 1. app.py - 装饰器配置 | |
| ```python | |
| import spaces | |
| from depth_anything_3.app.modules.model_inference import ModelInference | |
| # ✅ 装饰 run_inference 方法 | |
| original_run_inference = ModelInference.run_inference | |
| @spaces.GPU(duration=120) | |
| def gpu_run_inference(self, *args, **kwargs): | |
| """ | |
| 在 GPU 子进程中运行推理。 | |
| 这个函数会在独立的 GPU 子进程中执行, | |
| 可以安全地初始化 CUDA 和加载模型。 | |
| """ | |
| return original_run_inference(self, *args, **kwargs) | |
| # 替换原方法 | |
| ModelInference.run_inference = gpu_run_inference | |
| # ✅ 主进程:只创建应用,不加载模型 | |
| if __name__ == "__main__": | |
| app = DepthAnything3App(...) | |
| app.launch(host="0.0.0.0", port=7860) | |
| ``` | |
| ### 2. model_inference.py - 模型管理 | |
| ```python | |
| import torch | |
| from depth_anything_3.api import DepthAnything3 | |
| # ======================================== | |
| # ✅ 全局变量缓存(子进程安全) | |
| # ======================================== | |
| _MODEL_CACHE = None | |
| class ModelInference: | |
| def __init__(self): | |
| """ | |
| 初始化 - 不存储任何状态。 | |
| 注意:这个实例在主进程创建,但模型加载在子进程。 | |
| """ | |
| pass # ✅ 无实例变量 | |
| def initialize_model(self, device: str = "cuda"): | |
| """ | |
| 在子进程中加载模型。 | |
| 使用全局变量缓存,因为: | |
| 1. @spaces.GPU 在子进程运行 | |
| 2. 每个子进程有独立的全局命名空间 | |
| 3. 可以安全缓存,避免重复加载 | |
| """ | |
| global _MODEL_CACHE | |
| if _MODEL_CACHE is None: | |
| # 第一次调用:加载模型 | |
| model_dir = os.environ.get("DA3_MODEL_DIR", "...") | |
| print(f"🔄 Loading model in GPU subprocess from {model_dir}") | |
| _MODEL_CACHE = DepthAnything3.from_pretrained(model_dir) | |
| _MODEL_CACHE = _MODEL_CACHE.to(device) # ✅ 在子进程中移动 | |
| _MODEL_CACHE.eval() | |
| print(f"✅ Model loaded on {device}") | |
| else: | |
| # 后续调用:复用缓存 | |
| print("✅ Using cached model") | |
| # 确保在正确的设备上(防御性编程) | |
| _MODEL_CACHE = _MODEL_CACHE.to(device) | |
| return _MODEL_CACHE | |
| def run_inference(self, target_dir, ...): | |
| """ | |
| 运行推理 - 在 GPU 子进程中执行。 | |
| 这个函数被 @spaces.GPU 装饰,会在子进程运行。 | |
| """ | |
| # ✅ 在子进程中获取模型(局部变量) | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| model = self.initialize_model(device) # ✅ 返回模型,不存储 | |
| # ✅ 运行推理 | |
| with torch.no_grad(): | |
| prediction = model.inference(...) | |
| # ✅ 处理结果 | |
| # ... | |
| # ✅ 关键:返回前移动所有 CUDA 张量到 CPU | |
| prediction = self._move_to_cpu(prediction) | |
| return prediction, processed_data | |
| def _move_to_cpu(self, prediction): | |
| """移动所有 CUDA 张量到 CPU,确保 pickle 安全""" | |
| # ... 实现见下文 | |
| return prediction | |
| ``` | |
| ### 3. event_handlers.py - 主进程代码 | |
| ```python | |
| class EventHandlers: | |
| def __init__(self): | |
| """ | |
| 主进程初始化 - 不加载模型。 | |
| 注意:这里创建 ModelInference 实例是安全的, | |
| 因为它不立即加载模型。模型会在子进程中加载。 | |
| """ | |
| # ✅ 可以创建实例(不加载模型) | |
| self.model_inference = ModelInference() | |
| # ❌ 不要在这里调用 initialize_model() | |
| # ❌ 不要在这里加载模型 | |
| def gradio_demo(self, ...): | |
| """ | |
| Gradio 回调 - 在主进程调用。 | |
| 这个函数会调用 self.model_inference.run_inference, | |
| 而 run_inference 被 @spaces.GPU 装饰,会在子进程运行。 | |
| """ | |
| # ✅ 调用被装饰的方法(自动在子进程运行) | |
| result = self.model_inference.run_inference(...) | |
| return result | |
| ``` | |
| ## 🔑 关键原则总结 | |
| ### ✅ DO(应该做) | |
| 1. **主进程:只创建实例,不加载模型** | |
| ```python | |
| # ✅ 主进程 | |
| model_inference = ModelInference() # 安全 | |
| # 不调用 initialize_model() | |
| ``` | |
| 2. **子进程:使用全局变量缓存模型** | |
| ```python | |
| # ✅ 子进程(@spaces.GPU 装饰的函数内) | |
| _MODEL_CACHE = None # 全局变量 | |
| model = initialize_model() # 在子进程加载 | |
| ``` | |
| 3. **返回前:移动所有张量到 CPU** | |
| ```python | |
| # ✅ 返回前 | |
| prediction = move_all_tensors_to_cpu(prediction) | |
| return prediction | |
| ``` | |
| 4. **清理 GPU 内存** | |
| ```python | |
| # ✅ 推理后 | |
| torch.cuda.empty_cache() | |
| ``` | |
| ### ❌ DON'T(不应该做) | |
| 1. **主进程:不要初始化 CUDA** | |
| ```python | |
| # ❌ 主进程 | |
| model.to("cuda") # 💥 错误 | |
| torch.cuda.is_available() # 💥 可能触发初始化 | |
| ``` | |
| 2. **不要用实例变量存储模型** | |
| ```python | |
| # ❌ | |
| self.model = load_model() # 状态混乱 | |
| ``` | |
| 3. **不要返回 CUDA 张量** | |
| ```python | |
| # ❌ | |
| return prediction # 如果包含 CUDA 张量,会报错 | |
| ``` | |
| 4. **不要在 __init__ 中加载模型** | |
| ```python | |
| # ❌ | |
| def __init__(self): | |
| self.model = load_model() # 在主进程执行,会报错 | |
| ``` | |
| ## 📊 执行流程对比 | |
| ### ❌ 错误流程 | |
| ``` | |
| 主进程启动 | |
| ↓ | |
| 创建 ModelInference() 实例 | |
| ↓ | |
| __init__ 中 self.model = None # ✅ 安全 | |
| ↓ | |
| 第一次调用 run_inference | |
| ↓ | |
| @spaces.GPU 创建子进程 | |
| ↓ | |
| 子进程:self.model = load_model() # ✅ 在子进程 | |
| ↓ | |
| 返回 prediction(包含 CUDA 张量) # ❌ 错误 | |
| ↓ | |
| pickle 尝试在主进程重建 CUDA 张量 # 💥 报错 | |
| ``` | |
| ### ✅ 正确流程 | |
| ``` | |
| 主进程启动 | |
| ↓ | |
| 创建 ModelInference() 实例(无状态) # ✅ | |
| ↓ | |
| 第一次调用 run_inference | |
| ↓ | |
| @spaces.GPU 创建子进程 | |
| ↓ | |
| 子进程:_MODEL_CACHE = load_model() # ✅ 全局变量 | |
| ↓ | |
| 子进程:model = _MODEL_CACHE # ✅ 局部变量 | |
| ↓ | |
| 子进程:prediction = model.inference(...) | |
| ↓ | |
| 子进程:prediction = move_to_cpu(prediction) # ✅ | |
| ↓ | |
| 返回 prediction(所有张量在 CPU) # ✅ | |
| ↓ | |
| 主进程:安全接收 CPU 数据 # ✅ | |
| ``` | |
| ## 🧪 验证清单 | |
| ### 主进程检查 | |
| ```python | |
| # ✅ 应该通过 | |
| def test_main_process(): | |
| # 可以创建实例 | |
| model_inference = ModelInference() | |
| # 不应该有模型 | |
| assert not hasattr(model_inference, 'model') or model_inference.model is None | |
| # 不应该初始化 CUDA | |
| # (这个测试需要在主进程运行) | |
| ``` | |
| ### 子进程检查 | |
| ```python | |
| # ✅ 应该通过 | |
| @spaces.GPU | |
| def test_gpu_subprocess(): | |
| model_inference = ModelInference() | |
| # 可以加载模型 | |
| model = model_inference.initialize_model("cuda") | |
| assert model is not None | |
| # 模型应该在 GPU | |
| # (检查模型参数设备) | |
| # 可以运行推理 | |
| # ... | |
| # 返回前应该移到 CPU | |
| # ... | |
| ``` | |
| ## 🎓 常见问题 | |
| ### Q1: 为什么不能用实例变量? | |
| **A:** 因为实例在主进程创建,如果存储模型状态,会跨进程混乱。 | |
| ```python | |
| # ❌ 问题 | |
| self.model = load_model() # 状态可能混乱 | |
| # ✅ 解决 | |
| _MODEL_CACHE = load_model() # 每个子进程独立 | |
| ``` | |
| ### Q2: 全局变量安全吗? | |
| **A:** 是的!因为: | |
| - 每个子进程有独立的全局命名空间 | |
| - 主进程不会访问子进程的全局变量 | |
| - 不会跨进程污染 | |
| ### Q3: 模型会重复加载吗? | |
| **A:** 不会!因为: | |
| - 全局变量在子进程内缓存 | |
| - 同一个子进程的多次调用会复用 | |
| - 不同子进程各自缓存(如果需要) | |
| ### Q4: 如何清理模型? | |
| **A:** 通常不需要手动清理,因为: | |
| - 子进程结束后自动清理 | |
| - 如果需要,可以在子进程中: | |
| ```python | |
| global _MODEL_CACHE | |
| _MODEL_CACHE = None | |
| del model | |
| torch.cuda.empty_cache() | |
| ``` | |
| ## 📝 完整代码模板 | |
| ```python | |
| # ======================================== | |
| # model_inference.py | |
| # ======================================== | |
| _MODEL_CACHE = None # 全局缓存 | |
| class ModelInference: | |
| def __init__(self): | |
| pass # 无状态 | |
| def initialize_model(self, device="cuda"): | |
| global _MODEL_CACHE | |
| if _MODEL_CACHE is None: | |
| _MODEL_CACHE = load_model().to(device) | |
| return _MODEL_CACHE | |
| def run_inference(self, ...): | |
| model = self.initialize_model("cuda") | |
| prediction = model.inference(...) | |
| prediction = self._move_to_cpu(prediction) | |
| return prediction | |
| # ======================================== | |
| # app.py | |
| # ======================================== | |
| @spaces.GPU(duration=120) | |
| def gpu_run_inference(self, *args, **kwargs): | |
| return ModelInference.run_inference(self, *args, **kwargs) | |
| ModelInference.run_inference = gpu_run_inference | |
| ``` | |
| ## 🎯 总结 | |
| **核心原则:** | |
| 1. ✅ **主进程 = CPU 环境**,不加载模型,不初始化 CUDA | |
| 2. ✅ **子进程 = GPU 环境**,加载模型,运行推理 | |
| 3. ✅ **全局变量缓存**,每个子进程独立 | |
| 4. ✅ **返回 CPU 数据**,确保 pickle 安全 | |
| 遵循这些原则,你的 Spaces GPU 应用就能稳定运行!🚀 | |