haileyok commited on
Commit
94228b9
·
verified ·
1 Parent(s): 8cf9b7a

Module decodes pictures on threads and prepares the next batch while the GPU works

Browse files
Files changed (1) hide show
  1. topic_classifier.py +71 -31
topic_classifier.py CHANGED
@@ -26,6 +26,7 @@ Probabilities for the broad topic and subtopic heads are temperature scaled (con
26
  temperature_path), fitted on held-out validation posts so that the confidence is not over- or under-stated.
27
  """
28
 
 
29
  import json
30
  import os
31
  from pathlib import Path
@@ -172,7 +173,9 @@ class FusionModel(nn.Module):
172
 
173
 
174
  class TopicClassifier:
175
- def __init__(self, folder, device=None, image_encoder=None):
 
 
176
  from safetensors.torch import load_file
177
  from transformers import AutoConfig, AutoModel, AutoProcessor, AutoTokenizer
178
 
@@ -188,16 +191,19 @@ class TopicClassifier:
188
  self._image_model = self._image_processor = None
189
  self._AutoModel, self._AutoProcessor = AutoModel, AutoProcessor
190
  self.t_broad, self.t_path = self.cfg["temperature_broad"], self.cfg["temperature_path"]
 
 
 
191
 
192
  @classmethod
193
- def from_pretrained(cls, path_or_repo, device=None, image_encoder=None):
194
  """A local folder with the repository's files, or a Hugging Face repository id."""
195
  folder = path_or_repo
196
  if not os.path.isdir(folder):
197
  from huggingface_hub import snapshot_download
198
 
199
  folder = snapshot_download(path_or_repo)
200
- return cls(folder, device, image_encoder)
201
 
202
  # -- pictures
203
  def _load_image_encoder(self):
@@ -231,59 +237,93 @@ class TopicClassifier:
231
  except Exception: # noqa: BLE001 broken, truncated or hostile files must not fail a whole batch
232
  return None
233
 
234
- @torch.no_grad()
235
  def embed_pictures(self, ims):
236
  """One embedding per PIL RGB image, as float16 (the encoder's pooled output, as stored in training)."""
237
  self._load_image_encoder()
238
- px = self._image_processor(images=ims, return_tensors="pt")["pixel_values"].to(self.device, self._image_model.dtype)
 
 
 
 
 
239
  out = self._image_model.get_image_features(pixel_values=px)
240
  emb = out if torch.is_tensor(out) else out.pooler_output
241
  return emb.float().cpu().numpy().astype(np.float16)
242
 
243
  # -- predictions
244
- def _batch(self, items):
245
- n, max_images, dim = len(items), self.cfg["max_images"], self.cfg["image_features"]["dim"]
 
 
246
  enc = self.tok([(it["text"] or "")[:MAX_CHARS] for it in items], truncation=True, max_length=self.cfg["max_len"])["input_ids"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  L = max(len(x) for x in enc)
248
  ids = torch.full((n, L), self.tok.pad_token_id, dtype=torch.long)
249
  att = torch.zeros((n, L), dtype=torch.long)
250
- img = np.zeros((n, max_images, dim), np.float16)
251
- have = np.zeros((n, max_images), bool)
252
- ims, where = [], []
253
- for b, (x, it) in enumerate(zip(enc, items)):
254
  ids[b, : len(x)] = torch.tensor(x)
255
  att[b, : len(x)] = 1
256
- k = 0
257
- for p in list(it.get("pictures") or [])[:max_images]:
258
- im = self._open(p)
259
- if im is None: # a picture that cannot be read is skipped, as in training
260
- continue
261
- ims.append(im)
262
- where.append((b, k))
263
- k += 1
264
- if ims:
265
- for (b, k), e in zip(where, self.embed_pictures(ims)):
266
  img[b, k] = e
267
  have[b, k] = True
268
  d = self.device
269
  return (ids.to(d), att.to(d), torch.from_numpy(img).to(d), torch.from_numpy(have).to(d)), have.sum(1)
270
 
 
 
 
271
  @torch.no_grad()
272
  def predict_arrays(self, items, batch_size=32):
273
  """Arrays in the input order: broad [n, 25], paths [n, 118], signals [n, 11], tone [n, 6] (all
274
  probabilities) and pictures_used [n] (how many of each post's pictures could be read and were used)."""
275
  order = np.argsort([len(it["text"] or "") for it in items], kind="stable") # similar lengths together
 
276
  parts, used = [[], [], [], []], []
277
- for s in range(0, len(items), batch_size):
278
- batch = [items[i] for i in order[s:s + batch_size]]
279
- inputs, n_used = self._batch(batch)
280
- with torch.autocast(self.device.type, dtype=torch.bfloat16, enabled=self.device.type == "cuda"):
281
- lb, lp, ls, lt = self.model(*inputs)
282
- probs = (torch.softmax(lb.float() / self.t_broad, -1), torch.softmax(lp.float() / self.t_path, -1),
283
- torch.sigmoid(ls.float()), torch.softmax(lt.float(), -1))
284
- for k in range(4):
285
- parts[k].append(probs[k].cpu().numpy())
286
- used.append(n_used)
 
 
 
 
 
 
 
287
  inv = np.empty(len(order), int)
288
  inv[order] = np.arange(len(order))
289
  out = {name: np.concatenate(p)[inv] for name, p in zip(("broad", "paths", "signals", "tone"), parts)}
 
26
  temperature_path), fitted on held-out validation posts so that the confidence is not over- or under-stated.
27
  """
28
 
29
+ import concurrent.futures as cf
30
  import json
31
  import os
32
  from pathlib import Path
 
173
 
174
 
175
  class TopicClassifier:
176
+ def __init__(self, folder, device=None, image_encoder=None, decode_threads=None, prefetch=True):
177
+ """decode_threads: threads that decode pictures (default the TOPIC_DECODE_THREADS environment variable, else 8;
178
+ 1 decodes on the calling thread). prefetch: decode the next batch while the current one runs on the GPU."""
179
  from safetensors.torch import load_file
180
  from transformers import AutoConfig, AutoModel, AutoProcessor, AutoTokenizer
181
 
 
191
  self._image_model = self._image_processor = None
192
  self._AutoModel, self._AutoProcessor = AutoModel, AutoProcessor
193
  self.t_broad, self.t_path = self.cfg["temperature_broad"], self.cfg["temperature_path"]
194
+ n = decode_threads if decode_threads is not None else int(os.environ.get("TOPIC_DECODE_THREADS", "8"))
195
+ self._decode_pool = cf.ThreadPoolExecutor(n, thread_name_prefix="decode") if n > 1 else None
196
+ self._prefetch = prefetch
197
 
198
  @classmethod
199
+ def from_pretrained(cls, path_or_repo, device=None, image_encoder=None, **kwargs):
200
  """A local folder with the repository's files, or a Hugging Face repository id."""
201
  folder = path_or_repo
202
  if not os.path.isdir(folder):
203
  from huggingface_hub import snapshot_download
204
 
205
  folder = snapshot_download(path_or_repo)
206
+ return cls(folder, device, image_encoder, **kwargs)
207
 
208
  # -- pictures
209
  def _load_image_encoder(self):
 
237
  except Exception: # noqa: BLE001 broken, truncated or hostile files must not fail a whole batch
238
  return None
239
 
 
240
  def embed_pictures(self, ims):
241
  """One embedding per PIL RGB image, as float16 (the encoder's pooled output, as stored in training)."""
242
  self._load_image_encoder()
243
+ return self._embed_pixels(self._image_processor(images=ims, return_tensors="pt")["pixel_values"])
244
+
245
+ @torch.no_grad()
246
+ def _embed_pixels(self, pixel_values):
247
+ """The picture encoder on the GPU, for pictures the image processor has already resized and normalised."""
248
+ px = pixel_values.to(self.device, self._image_model.dtype)
249
  out = self._image_model.get_image_features(pixel_values=px)
250
  emb = out if torch.is_tensor(out) else out.pooler_output
251
  return emb.float().cpu().numpy().astype(np.float16)
252
 
253
  # -- predictions
254
+ def _prepare(self, items):
255
+ """The CPU part of a batch: tokenize the texts, decode the pictures (on the decode threads) and resize and
256
+ normalise them. It can run for the next batch while the current one is on the GPU."""
257
+ max_images = self.cfg["max_images"]
258
  enc = self.tok([(it["text"] or "")[:MAX_CHARS] for it in items], truncation=True, max_length=self.cfg["max_len"])["input_ids"]
259
+ refs, sources = [], []
260
+ for b, it in enumerate(items):
261
+ for p in list(it.get("pictures") or [])[:max_images]:
262
+ refs.append(b)
263
+ sources.append(p)
264
+ if self._decode_pool is not None and len(sources) > 1:
265
+ decoded = list(self._decode_pool.map(self._open, sources))
266
+ else:
267
+ decoded = [self._open(p) for p in sources]
268
+ readable = [im for im in decoded if im is not None]
269
+ px = None
270
+ if readable:
271
+ self._load_image_encoder()
272
+ px = self._image_processor(images=readable, return_tensors="pt")["pixel_values"] # resize and normalise on the CPU
273
+ return enc, refs, decoded, px
274
+
275
+ def _finish(self, n, prepared):
276
+ """The GPU part: embed the decoded pictures and build the model's input tensors."""
277
+ max_images, dim = self.cfg["max_images"], self.cfg["image_features"]["dim"]
278
+ enc, refs, decoded, px = prepared
279
  L = max(len(x) for x in enc)
280
  ids = torch.full((n, L), self.tok.pad_token_id, dtype=torch.long)
281
  att = torch.zeros((n, L), dtype=torch.long)
282
+ for b, x in enumerate(enc):
 
 
 
283
  ids[b, : len(x)] = torch.tensor(x)
284
  att[b, : len(x)] = 1
285
+ img = np.zeros((n, max_images, dim), np.float16)
286
+ have = np.zeros((n, max_images), bool)
287
+ where, slot = [], [0] * n
288
+ for b, im in zip(refs, decoded):
289
+ if im is None: # a picture that cannot be read is skipped, as in training
290
+ continue
291
+ where.append((b, slot[b]))
292
+ slot[b] += 1
293
+ if px is not None:
294
+ for (b, k), e in zip(where, self._embed_pixels(px)):
295
  img[b, k] = e
296
  have[b, k] = True
297
  d = self.device
298
  return (ids.to(d), att.to(d), torch.from_numpy(img).to(d), torch.from_numpy(have).to(d)), have.sum(1)
299
 
300
+ def _batch(self, items):
301
+ return self._finish(len(items), self._prepare(items))
302
+
303
  @torch.no_grad()
304
  def predict_arrays(self, items, batch_size=32):
305
  """Arrays in the input order: broad [n, 25], paths [n, 118], signals [n, 11], tone [n, 6] (all
306
  probabilities) and pictures_used [n] (how many of each post's pictures could be read and were used)."""
307
  order = np.argsort([len(it["text"] or "") for it in items], kind="stable") # similar lengths together
308
+ batches = [[items[i] for i in order[s:s + batch_size]] for s in range(0, len(items), batch_size)]
309
  parts, used = [[], [], [], []], []
310
+ ahead = cf.ThreadPoolExecutor(1, thread_name_prefix="prefetch") if self._prefetch and len(batches) > 1 else None
311
+ try:
312
+ nxt = ahead.submit(self._prepare, batches[0]) if ahead else None
313
+ for k, batch in enumerate(batches):
314
+ prepared = nxt.result() if ahead else self._prepare(batch)
315
+ nxt = ahead.submit(self._prepare, batches[k + 1]) if ahead and k + 1 < len(batches) else None
316
+ inputs, n_used = self._finish(len(batch), prepared)
317
+ with torch.autocast(self.device.type, dtype=torch.bfloat16, enabled=self.device.type == "cuda"):
318
+ lb, lp, ls, lt = self.model(*inputs)
319
+ probs = (torch.softmax(lb.float() / self.t_broad, -1), torch.softmax(lp.float() / self.t_path, -1),
320
+ torch.sigmoid(ls.float()), torch.softmax(lt.float(), -1))
321
+ for j in range(4):
322
+ parts[j].append(probs[j].cpu().numpy())
323
+ used.append(n_used)
324
+ finally:
325
+ if ahead:
326
+ ahead.shutdown(wait=True)
327
  inv = np.empty(len(order), int)
328
  inv[order] = np.arange(len(order))
329
  out = {name: np.concatenate(p)[inv] for name, p in zip(("broad", "paths", "signals", "tone"), parts)}