Paul1k commited on
Commit
3db8a0d
Β·
verified Β·
1 Parent(s): c541012

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +39 -48
app.py CHANGED
@@ -6,15 +6,11 @@ import torch
6
  import torch.nn.functional as F
7
  import numpy as np
8
  import io
 
9
  import os
10
  import asyncio
11
  from torchvision.transforms.functional import normalize
12
-
13
- # ── BiRefNet / RMBG-2.0 Model Import ──────────────────────────────
14
- # RMBG-2.0 is Bria's production release built on the BiRefNet architecture.
15
- # It scores ~93 on DIS5K benchmarks vs ~86 for RMBG-1.4 β€” the jump that
16
- # takes hair/fur edge quality from "Good" to "Excellent".
17
- from transformers import AutoModelForImageSegmentation
18
 
19
  # ── CPU Thread Tuning ──────────────────────────────────────────────
20
  # Must be set before any model loading
@@ -32,7 +28,7 @@ import torch._dynamo
32
  torch._dynamo.config.suppress_errors = True
33
 
34
  # ── App Setup ──────────────────────────────────────────────────────
35
- app = FastAPI(title="Lampdra API", version="2.0")
36
 
37
  ALLOWED_ORIGINS = [
38
  "https://lampdra.com",
@@ -74,36 +70,31 @@ MAX_PIXELS = 10000 * 10000 # 100MP β€” generous for real photos
74
  POOL_SIZE = 1
75
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
76
 
77
- # ── BiRefNet Preprocessing Constants ──────────────────────────────
78
- # IMPORTANT CHANGE from RMBG-1.4:
79
- # RMBG-1.4 used mean=[0.5,0.5,0.5] / std=[1.0,1.0,1.0] (simple centre-scale).
80
- # RMBG-2.0 / BiRefNet uses standard ImageNet normalisation.
81
- # Using the wrong constants produces grey/washed-out masks β€” hair especially.
82
- BIREFNET_MEAN = [0.485, 0.456, 0.406]
83
- BIREFNET_STD = [0.229, 0.224, 0.225]
84
- INPUT_SIZE = (1024, 1024) # BiRefNet native resolution β€” unchanged from RMBG-1.4
85
 
86
  # Pool queue β€” asyncio.Queue is thread-safe and blocks callers
87
  # when all copies are busy until one becomes free
88
  model_pool = asyncio.Queue()
89
 
90
- def build_model_copy() -> torch.nn.Module:
91
- """
92
- Load one copy of RMBG-2.0 (BiRefNet architecture).
93
- Weights are downloaded from HF hub on first run and cached at
94
- ~/.cache/huggingface/hub β€” subsequent Space restarts are instant.
95
- trust_remote_code=True is required by the RMBG-2.0 model card.
96
- """
97
- m = AutoModelForImageSegmentation.from_pretrained(
98
- "briaai/RMBG-2.0",
99
- trust_remote_code=True,
100
- )
101
  m.eval()
102
  m.to(DEVICE)
 
 
 
103
  try:
104
  m = torch.compile(m)
105
  except Exception:
106
- pass # safe to skip if g++ / inductor unavailable
107
  return m
108
 
109
  # ── Ready flag β€” Cause 3 fix ──────────────────────────────────────
@@ -119,29 +110,29 @@ _model_ready = False
119
  @app.on_event("startup")
120
  async def startup_event():
121
  global _model_ready
122
- dummy = torch.zeros(1, 3, INPUT_SIZE[0], INPUT_SIZE[1]).to(DEVICE)
123
  for i in range(POOL_SIZE):
124
  m = build_model_copy()
 
 
125
  def _run_warmup(model=m):
126
  with torch.inference_mode():
127
  model(dummy)
128
  await asyncio.to_thread(_run_warmup)
129
- print(f" Copy {i + 1}/{POOL_SIZE} warmed up (RMBG-2.0 / BiRefNet)")
130
  model_pool.put_nowait(m) # slot added AFTER warmup β€” not before
131
  del dummy
132
- _model_ready = True
133
- print(f"Pool ready β€” {POOL_SIZE} BiRefNet copies loaded, accepting requests.")
134
 
135
  # ── Preprocessing ──────────────────────────────────────────────────
136
- def preprocess(image: Image.Image) -> torch.Tensor:
137
- """
138
- Resize to 1024Γ—1024 and apply ImageNet normalisation.
139
- BOX filter is fastest for downscaling (18ms vs 27ms for BILINEAR).
140
- """
141
- img = image.convert("RGB").resize(INPUT_SIZE, Image.BOX)
142
  arr = np.array(img, dtype=np.float32) / 255.0
143
  tensor = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0).to(DEVICE)
144
- tensor = normalize(tensor, BIREFNET_MEAN, BIREFNET_STD)
145
  return tensor
146
 
147
  # ── Routes ─────────────────────────────────────────────────────────
@@ -149,11 +140,12 @@ def preprocess(image: Image.Image) -> torch.Tensor:
149
  @app.head("/")
150
  def root():
151
  return {
152
- "status" : "Lampdra API is running",
153
- "model" : "RMBG-2.0 (BiRefNet)",
154
- "device" : DEVICE,
155
- "pool_size" : POOL_SIZE,
156
- "pool_available": model_pool.qsize() if _model_ready else 0,
 
157
  }
158
 
159
  @app.post("/remove-background")
@@ -258,8 +250,7 @@ async def remove_background(
258
  return Response(content=output_buffer.getvalue(), media_type="image/webp")
259
 
260
  finally:
261
- # Always return the model to the pool β€” even on exceptions.
262
- # put_nowait() is correct here: queue has no maxsize so it never
263
- # raises QueueFull, and it skips coroutine overhead on every request.
264
- # Consistent with startup_event which also uses put_nowait.
265
- model_pool.put_nowait(model)
 
6
  import torch.nn.functional as F
7
  import numpy as np
8
  import io
9
+ import sys
10
  import os
11
  import asyncio
12
  from torchvision.transforms.functional import normalize
13
+ from safetensors.torch import load_file
 
 
 
 
 
14
 
15
  # ── CPU Thread Tuning ──────────────────────────────────────────────
16
  # Must be set before any model loading
 
28
  torch._dynamo.config.suppress_errors = True
29
 
30
  # ── App Setup ──────────────────────────────────────────────────────
31
+ app = FastAPI(title="Lampdra API", version="1.0")
32
 
33
  ALLOWED_ORIGINS = [
34
  "https://lampdra.com",
 
70
  POOL_SIZE = 1
71
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
72
 
73
+ sys.path.insert(0, "/app")
74
+ from rmbg.briarmbg import BriaRMBG
75
+
76
+ # Load weights from disk once β€” all pool copies share this read
77
+ print(f"Device: {DEVICE}")
78
+ print(f"Loading weights from disk...")
79
+ state_dict = load_file("/app/rmbg/model.safetensors")
 
80
 
81
  # Pool queue β€” asyncio.Queue is thread-safe and blocks callers
82
  # when all copies are busy until one becomes free
83
  model_pool = asyncio.Queue()
84
 
85
+ def build_model_copy():
86
+ """Build one independent copy of the model."""
87
+ m = BriaRMBG()
88
+ m.load_state_dict(state_dict)
 
 
 
 
 
 
 
89
  m.eval()
90
  m.to(DEVICE)
91
+ # torch.compile β€” compiles model to native machine code
92
+ # First inference is slower (compile happens then)
93
+ # Every subsequent call is 20-40% faster
94
  try:
95
  m = torch.compile(m)
96
  except Exception:
97
+ pass # compile not available on all platforms, safe to skip
98
  return m
99
 
100
  # ── Ready flag β€” Cause 3 fix ──────────────────────────────────────
 
110
  @app.on_event("startup")
111
  async def startup_event():
112
  global _model_ready
113
+ dummy = torch.zeros(1, 3, 1024, 1024).to(DEVICE)
114
  for i in range(POOL_SIZE):
115
  m = build_model_copy()
116
+ # Warmup in thread β€” event loop stays free, but pool is still empty
117
+ # so router cannot send requests here even if it tries
118
  def _run_warmup(model=m):
119
  with torch.inference_mode():
120
  model(dummy)
121
  await asyncio.to_thread(_run_warmup)
122
+ print(f" Copy {i + 1}/{POOL_SIZE} warmed up and compiled")
123
  model_pool.put_nowait(m) # slot added AFTER warmup β€” not before
124
  del dummy
125
+ _model_ready = True # only NOW does root() report real pool_available
126
+ print(f"Pool ready β€” {POOL_SIZE} copies loaded, warmed up, accepting requests.")
127
 
128
  # ── Preprocessing ──────────────────────────────────────────────────
129
+ def preprocess(image: Image.Image, size=(1024, 1024)):
130
+ # All images resize to 1024Γ—1024 here regardless of original size
131
+ # This is why the model RAM usage is always predictable
132
+ img = image.convert("RGB").resize(size, Image.BOX) # BOX=18ms vs BILINEAR=27ms for downscale
 
 
133
  arr = np.array(img, dtype=np.float32) / 255.0
134
  tensor = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0).to(DEVICE)
135
+ tensor = normalize(tensor, [0.5, 0.5, 0.5], [1.0, 1.0, 1.0])
136
  return tensor
137
 
138
  # ── Routes ─────────────────────────────────────────────────────────
 
140
  @app.head("/")
141
  def root():
142
  return {
143
+ "status" : "Lampdra API is running",
144
+ "device" : DEVICE,
145
+ "pool_size" : POOL_SIZE,
146
+ # Returns 0 until warmup completes β€” prevents router sending
147
+ # requests to a Space that is still loading or compiling
148
+ "pool_available" : model_pool.qsize() if _model_ready else 0,
149
  }
150
 
151
  @app.post("/remove-background")
 
250
  return Response(content=output_buffer.getvalue(), media_type="image/webp")
251
 
252
  finally:
253
+ # ALWAYS return the model copy to the pool
254
+ # This runs even if the request crashes with any Python-level error
255
+ # Pool never permanently loses a slot due to soft crashes
256
+ await model_pool.put(model)