main
py 72 lines 2.85 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Safe deserialization helpers for client-supplied embedding tensors.
4
5 `check_sparse_tensor_invariants_threadsafe()` is a thread-safe wrapper for
6 sparse tensor invariant validation. `safe_to_dense()` bounds the memory a
7 deserialized tensor may allocate when densified.
8
9 PyTorch's `torch.sparse.check_sparse_tensor_invariants()` context manager
10 manipulates a **process-global** flag (save/enable/restore). When multiple
11 embedding-load operations run concurrently on a thread-pool executor, one
12 context can restore the flag to `False` while another thread is still inside
13 its guard, bypassing the invariant check.
14
15 All call sites MUST use `check_sparse_tensor_invariants_threadsafe()`
16 which serializes access behind a lock.
17 """
18
19 import contextlib
20 import threading
21
22 import torch
23
24 import vllm.envs as envs
25 from vllm.exceptions import VLLMValidationError
26 from vllm.utils.mem_constants import MiB_bytes
27
28 _SPARSE_LOAD_LOCK = threading.Lock()
29
30
31 @contextlib.contextmanager
32 def check_sparse_tensor_invariants_threadsafe():
33 with _SPARSE_LOAD_LOCK, torch.sparse.check_sparse_tensor_invariants():
34 yield
35
36
37 def safe_to_dense(tensor: object, *, parameter: str) -> torch.Tensor:
38 """Densify a client-supplied embedding tensor within a memory budget.
39
40 `torch.load(..., weights_only=True)` prevents arbitrary code execution but
41 not memory amplification: a sparse COO tensor carries its own declared
42 shape, so the dense allocation is bounded by that shape rather than by the
43 request. A 2.6 KB payload declaring `(30000, 30000)` materializes 3.4 GiB,
44 and the shape can be picked to fit whatever host is being targeted. The
45 invariant checks only reject *invalid* tensors (indices outside the
46 declared shape); a single non-zero element inside a `(2**20, 2**20)` shape
47 is perfectly valid and passes them.
48
49 So reject oversized payloads based on the dense size they declare, before
50 `to_dense()` allocates anything.
51 """
52 if not isinstance(tensor, torch.Tensor):
53 raise VLLMValidationError(
54 f"`{parameter}` payload did not deserialize to a torch.Tensor.",
55 parameter=parameter,
56 )
57
58 max_bytes = envs.VLLM_MAX_EMBED_DECODE_BYTES
59 if max_bytes > 0:
60 dense_bytes = tensor.numel() * tensor.element_size()
61 if dense_bytes > max_bytes:
62 raise VLLMValidationError(
63 f"`{parameter}` declares shape {tuple(tensor.shape)}, which "
64 f"would allocate {dense_bytes} bytes "
65 f"({dense_bytes / MiB_bytes:.0f} MiB) once densified, "
66 f"exceeding the {max_bytes} byte limit. Set "
67 f"VLLM_MAX_EMBED_DECODE_BYTES to increase this limit.",
68 parameter=parameter,
69 value=dense_bytes,
70 )
71
72 return tensor.to_dense()