nathansut1 commited on
Commit
d8cc6e6
Β·
verified Β·
1 Parent(s): 310b934

Upload example_gpu_pipeline.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. example_gpu_pipeline.py +171 -0
example_gpu_pipeline.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Full GPU pipeline example for the SBB binarization ONNX model.
3
+
4
+ Shows how to keep the entire image processing chain on the GPU using CuPy,
5
+ with only the JPEG decode and TIFF save happening on CPU. This is the
6
+ approach you'd use for a production pipeline where throughput matters.
7
+
8
+ pip install onnxruntime-gpu cupy-cuda12x numpy Pillow
9
+ python3 example_gpu_pipeline.py input.jpg output.tif
10
+
11
+ On first run, TensorRT builds an optimized engine (~60-90s). This is
12
+ cached in ./trt_cache/ and reused on subsequent runs.
13
+ """
14
+
15
+ import sys
16
+ import os
17
+ import numpy as np
18
+ import cupy as cp
19
+ import onnxruntime as ort
20
+ from PIL import Image
21
+
22
+ MODEL = "model_convtranspose.onnx"
23
+ PATCH_SIZE = 448
24
+ BATCH_SIZE = 64
25
+
26
+ # ── Normalization LUT ────────────────────────────────────────────────────────
27
+ # The original TF model normalizes with: np.array(img) / 255.0 which does
28
+ # float64 division then truncates to float32. Doing float32 division directly
29
+ # gives different rounding for some values (off by 1 ULP), which can flip
30
+ # pixels at the binarization threshold. This LUT preserves the exact behavior.
31
+ _NORM_LUT = cp.array(
32
+ np.array([np.float32(np.float64(i) / 255.0) for i in range(256)],
33
+ dtype=np.float32)
34
+ )
35
+
36
+
37
+ def create_session(model_path):
38
+ """Create an ONNX Runtime session with TensorRT backend.
39
+
40
+ TensorRT compiles the model into an optimized GPU engine on first run.
41
+ The engine is cached to disk so subsequent runs start in ~2 seconds.
42
+ """
43
+ cache_dir = "./trt_cache"
44
+ os.makedirs(cache_dir, exist_ok=True)
45
+ return ort.InferenceSession(model_path, providers=[
46
+ ("TensorrtExecutionProvider", {
47
+ "device_id": 0,
48
+ "trt_fp16_enable": False, # FP32 for accuracy
49
+ "trt_engine_cache_enable": True,
50
+ "trt_engine_cache_path": cache_dir,
51
+ "trt_builder_optimization_level": 3,
52
+ }),
53
+ ("CUDAExecutionProvider", {"device_id": 0}),
54
+ ])
55
+
56
+
57
+ def extract_patches_gpu(img_gpu, patch_size):
58
+ """Extract non-overlapping patches on GPU. Zero-pads edges."""
59
+ h, w = img_gpu.shape[:2]
60
+ positions = [(x, y) for y in range(0, h, patch_size)
61
+ for x in range(0, w, patch_size)]
62
+
63
+ patches = cp.zeros((len(positions), patch_size, patch_size, 3), dtype=cp.uint8)
64
+ for i, (x, y) in enumerate(positions):
65
+ ph = min(patch_size, h - y)
66
+ pw = min(patch_size, w - x)
67
+ patches[i, :ph, :pw, :] = img_gpu[y:y+ph, x:x+pw, :]
68
+
69
+ return patches, positions
70
+
71
+
72
+ def infer_patches(session, patches_uint8):
73
+ """Normalize and run inference, one batch at a time.
74
+
75
+ Normalizing per-batch (64 patches = 154MB) instead of all at once
76
+ (500+ patches = 2.6GB) avoids GPU memory fragmentation.
77
+ """
78
+ inp = session.get_inputs()[0].name
79
+ out = session.get_outputs()[0].name
80
+ n = patches_uint8.shape[0]
81
+ out_ch = session.get_outputs()[0].shape[3] or 2
82
+
83
+ all_output = cp.zeros((n, PATCH_SIZE, PATCH_SIZE, out_ch), dtype=cp.float32)
84
+
85
+ for i in range(0, n, BATCH_SIZE):
86
+ end = min(i + BATCH_SIZE, n)
87
+
88
+ # Normalize on GPU via LUT (uint8 -> float32, 8ms per batch)
89
+ batch_float = _NORM_LUT[patches_uint8[i:end].astype(cp.int32)]
90
+
91
+ # Transfer to CPU for ONNX Runtime inference
92
+ result = session.run([out], {inp: batch_float.get()})[0]
93
+
94
+ # Transfer result back to GPU for post-processing
95
+ all_output[i:end] = cp.asarray(result)
96
+
97
+ return all_output
98
+
99
+
100
+ def postprocess_gpu(output):
101
+ """Extract foreground probability, threshold, binarize β€” all on GPU."""
102
+ probs = output[:, :, :, 1] # channel 1 = foreground
103
+ quantized = (probs * 255.0).astype(cp.uint8)
104
+ return cp.where(quantized <= 128, cp.uint8(255), cp.uint8(0))
105
+
106
+
107
+ def reconstruct_gpu(patches, positions, width, height):
108
+ """Reconstruct full image from patches with overlap averaging β€” all on GPU."""
109
+ result = cp.zeros((height, width), dtype=cp.float32)
110
+ weight = cp.zeros((height, width), dtype=cp.float32)
111
+
112
+ for i, (x, y) in enumerate(positions):
113
+ ah = min(PATCH_SIZE, height - y)
114
+ aw = min(PATCH_SIZE, width - x)
115
+ result[y:y+ah, x:x+aw] += patches[i, :ah, :aw].astype(cp.float32)
116
+ weight[y:y+ah, x:x+aw] += 1.0
117
+
118
+ return (result / cp.maximum(weight, 1.0)).astype(cp.uint8)
119
+
120
+
121
+ def binarize_image(input_path, output_path, model_path=MODEL):
122
+ """Full pipeline: JPEG in -> binarized TIFF out.
123
+
124
+ Data flow:
125
+ CPU: decode JPEG
126
+ CPU -> GPU: upload image (~5ms)
127
+ GPU: extract patches (~7ms)
128
+ GPU -> CPU -> GPU: normalize, infer, collect (~175ms per batch)
129
+ GPU: threshold + binarize (~1ms)
130
+ GPU: reconstruct from patches (~13ms)
131
+ GPU -> CPU: download result (~2ms)
132
+ CPU: save Group4 TIFF
133
+ """
134
+ # CPU: decode
135
+ img = np.array(Image.open(input_path).convert("RGB"))
136
+ h, w = img.shape[:2]
137
+
138
+ # CPU -> GPU
139
+ img_gpu = cp.asarray(img)
140
+
141
+ # GPU: patch extraction
142
+ patches, positions = extract_patches_gpu(img_gpu, PATCH_SIZE)
143
+
144
+ # Inference (normalize on GPU, infer via ORT, results back on GPU)
145
+ session = create_session(model_path)
146
+ output = infer_patches(session, patches)
147
+
148
+ # GPU: threshold
149
+ binary = postprocess_gpu(output)
150
+
151
+ # GPU: reconstruct
152
+ result_gpu = reconstruct_gpu(binary, positions, w, h)
153
+
154
+ # GPU -> CPU
155
+ result = result_gpu.get()
156
+
157
+ # CPU: save
158
+ Image.fromarray(result, "L").convert("1").save(
159
+ output_path, format="TIFF", compression="group4", dpi=(300, 300)
160
+ )
161
+ print(f"Saved {output_path}")
162
+
163
+ # Clean up GPU memory
164
+ del img_gpu, patches, output, binary, result_gpu
165
+
166
+
167
+ if __name__ == "__main__":
168
+ if len(sys.argv) < 3:
169
+ print(f"Usage: {sys.argv[0]} <input.jpg> <output.tif>")
170
+ sys.exit(1)
171
+ binarize_image(sys.argv[1], sys.argv[2])