@@ -47,24 +47,32 @@ class AsyncVisionEncoder(Generic[RawT, ItemT]):
4747
4848 The worker calls ``load`` once at startup and ``await``s ``encode`` per
4949 request; ``shutdown`` on teardown. All model knowledge lives in ``backend``;
50- this class owns the preprocess pool, the A5 barrier, and the single actor
51- thread that runs ``build`` / ``forward_batch`` / ``close``.
50+ this class owns the optional preprocess pool, the A5 barrier, and the single
51+ actor thread that runs ``build`` / ``forward_batch`` / ``close``.
5252 """
5353
5454 def __init__ (
5555 self ,
5656 backend : VisionEncoderBackend [RawT , ItemT ],
5757 * ,
58- preprocess_concurrency : int = 4 ,
58+ preprocess_concurrency : int | None = None ,
5959 name : str = "vision-encoder" ,
6060 ) -> None :
61- if preprocess_concurrency < 1 :
62- raise ValueError ("preprocess_concurrency must be >= 1" )
61+ # The backend declares whether it needs off-loop preprocessing; the
62+ # explicit arg is an override for tuning. 0 ⇒ no pool (raws pass straight
63+ # to forward_batch).
64+ conc = (
65+ backend .preprocess_concurrency
66+ if preprocess_concurrency is None
67+ else preprocess_concurrency
68+ )
69+ if conc < 0 :
70+ raise ValueError ("preprocess_concurrency must be >= 0" )
6371 self ._backend = backend
64- self ._preprocess_concurrency = preprocess_concurrency
72+ self ._preprocess_concurrency = conc
6573 self ._name = name
6674 self ._actor : ThreadPoolExecutor | None = None # build + every forward
67- self ._pool : ThreadPoolExecutor | None = None # off-loop preprocess
75+ self ._pool : ThreadPoolExecutor | None = None # off-loop preprocess (or None)
6876
6977 # ---- lifecycle ---------------------------------------------------------
7078
@@ -75,17 +83,22 @@ def load(self, model_id: str) -> None:
7583 so a misconfigured encoder errors at startup. Single-shot: a second
7684 ``load()`` raises rather than orphaning the first actor thread and model.
7785 """
78- if self ._actor is not None or self . _pool is not None :
86+ if self ._actor is not None :
7987 raise RuntimeError ("AsyncVisionEncoder.load() called twice" )
8088 try :
8189 # One actor thread so build + every forward share a thread; a single
8290 # worker also serializes forwards (FIFO) — no cross-request batching.
8391 self ._actor = ThreadPoolExecutor (
8492 max_workers = 1 , thread_name_prefix = f"{ self ._name } -actor"
8593 )
86- self ._pool = ThreadPoolExecutor (
87- max_workers = self ._preprocess_concurrency ,
88- thread_name_prefix = f"{ self ._name } -pre" ,
94+ # No pool when concurrency is 0 — preprocess is skipped (passthrough).
95+ self ._pool = (
96+ ThreadPoolExecutor (
97+ max_workers = self ._preprocess_concurrency ,
98+ thread_name_prefix = f"{ self ._name } -pre" ,
99+ )
100+ if self ._preprocess_concurrency > 0
101+ else None
89102 )
90103 self ._actor .submit (self ._backend .build , model_id ).result ()
91104 self .validate ()
@@ -110,30 +123,38 @@ def get_image_placeholder_token_id(self) -> int:
110123 # ---- request path ------------------------------------------------------
111124
112125 async def encode (self , raws : List [RawT ]) -> List [torch .Tensor ]:
113- """Preprocess (off-loop, A5 barrier) then run a single serial forward.
114-
115- Returns one ``(n_visual_tokens, lm_hidden_dim)`` CPU tensor per raw input,
116- in order. Raises if any image's preprocess fails (no GPU work) or if the
117- forward fails.
126+ """Optionally preprocess (off-loop, A5 barrier) then run a single serial
127+ forward.
128+
129+ With no preprocess pool (``preprocess_concurrency == 0``) raws go straight
130+ to ``forward_batch`` (the backend folds any prep in there). Returns one
131+ ``(n_visual_tokens, lm_hidden_dim)`` CPU tensor per raw input, in order.
132+ Raises if any image's preprocess fails (no GPU work) or if the forward
133+ fails.
118134 """
119- if self ._actor is None or self . _pool is None :
135+ if self ._actor is None :
120136 raise RuntimeError ("AsyncVisionEncoder.encode() called before load()" )
121137 if not raws :
122138 return []
123139 loop = asyncio .get_running_loop ()
124- # A5 barrier: preprocess all images concurrently, wait for EVERY one to
125- # settle, and run the forward only if all succeeded. return_exceptions=True
126- # makes the gather a true barrier (no short-circuit).
127- tasks = [
128- loop .run_in_executor (self ._pool , self ._backend .preprocess , raw )
129- for raw in raws
130- ]
131- settled = await asyncio .gather (* tasks , return_exceptions = True )
132- errors = [r for r in settled if isinstance (r , BaseException )]
133- if errors :
134- raise errors [0 ]
135- preprocessed : List [Preprocessed ] = list (settled ) # type: ignore[arg-type]
136- items = [p .item for p in preprocessed ]
140+ if self ._pool is None :
141+ # No preprocess phase: raw IS the item. No A5 barrier needed — the
142+ # single forward is already all-or-nothing for the request.
143+ items : List [ItemT ] = list (raws ) # type: ignore[arg-type] # ItemT==RawT
144+ else :
145+ # A5 barrier: preprocess all images concurrently, wait for EVERY one to
146+ # settle, run the forward only if all succeeded. return_exceptions=True
147+ # makes the gather a true barrier (no short-circuit).
148+ tasks = [
149+ loop .run_in_executor (self ._pool , self ._backend .preprocess , raw )
150+ for raw in raws
151+ ]
152+ settled = await asyncio .gather (* tasks , return_exceptions = True )
153+ errors = [r for r in settled if isinstance (r , BaseException )]
154+ if errors :
155+ raise errors [0 ]
156+ preprocessed : List [Preprocessed ] = list (settled ) # type: ignore[arg-type]
157+ items = [p .item for p in preprocessed ]
137158 # Direct, serialized forward on the actor thread (eager; target_bucket
138159 # defaults to None — there is no graph ladder until CUDA-graph batching
139160 # is supported).
0 commit comments