|
| 1 | +""" |
| 2 | +.. _shared_dynamic_dims: |
| 3 | +
|
| 4 | +Sharing Dynamic Dimensions Across Inputs |
| 5 | +========================================================== |
| 6 | +
|
| 7 | +When a model takes multiple inputs whose dynamic axes must be **equal at |
| 8 | +runtime** — for example, HuggingFace-style encoders where ``input_ids`` and |
| 9 | +``attention_mask`` are both shaped ``[batch, seq_len]`` — naively assigning an |
| 10 | +independent dynamic dimension to each input causes ``torch.export`` to raise a |
| 11 | +``ConstraintViolationError``. The exporter detects that the two independent |
| 12 | +symbols are forced equal by the model's forward pass (e.g. a broadcast) and |
| 13 | +rejects the export. |
| 14 | +
|
| 15 | +``torch_tensorrt.Input(shared_dims={axis: name})`` solves this: axes that share |
| 16 | +the same name across inputs are exported as a single ``torch.export.Dim``, so |
| 17 | +the equality constraint is satisfied automatically. All dynamic-shape intent |
| 18 | +lives on the ``Input`` objects — no separate ``dynamic_shapes`` argument or |
| 19 | +``torch.export`` knowledge is required at the call site. |
| 20 | +""" |
| 21 | + |
| 22 | +# %% |
| 23 | +# Imports and Model Definition |
| 24 | +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 25 | + |
| 26 | +import torch |
| 27 | +import torch.nn as nn |
| 28 | +import torch_tensorrt |
| 29 | +from torch_tensorrt.dynamo.utils import COSINE_THRESHOLD, cosine_similarity |
| 30 | + |
| 31 | +# %% |
| 32 | +# Define a HuggingFace-style encoder whose two inputs share the batch axis. |
| 33 | +# The ``embed * mask`` broadcast forces ``input_ids.shape[0] == |
| 34 | +# attention_mask.shape[0]`` at every forward call — exactly the pattern that |
| 35 | +# triggers ``ConstraintViolationError`` when the batch axis is exported as two |
| 36 | +# independent ``Dim`` objects. |
| 37 | + |
| 38 | + |
| 39 | +class SharedDimEncoder(nn.Module): |
| 40 | + def __init__(self, vocab: int = 1024, hidden: int = 64): |
| 41 | + super().__init__() |
| 42 | + self.embed = nn.Embedding(vocab, hidden) |
| 43 | + self.proj = nn.Linear(hidden, hidden) |
| 44 | + |
| 45 | + def forward(self, input_ids: torch.Tensor, attention_mask: torch.Tensor): |
| 46 | + x = self.embed(input_ids) # [B, S, hidden] |
| 47 | + mask = attention_mask.unsqueeze(-1).to(x.dtype) # [B, S, 1] |
| 48 | + return self.proj(x * mask) # [B, S, hidden] |
| 49 | + |
| 50 | + |
| 51 | +model = SharedDimEncoder().cuda().eval() |
| 52 | + |
| 53 | +# %% |
| 54 | +# Without ``shared_dims`` — raises ``ConstraintViolationError`` |
| 55 | +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 56 | +# |
| 57 | +# Using independent ``Input`` objects like this would fail at export time: |
| 58 | +# |
| 59 | +# .. code-block:: python |
| 60 | +# |
| 61 | +# inputs = [ |
| 62 | +# torch_tensorrt.Input(min_shape=(1,16), opt_shape=(4,16), max_shape=(8,16), dtype=torch.int64), |
| 63 | +# torch_tensorrt.Input(min_shape=(1,16), opt_shape=(4,16), max_shape=(8,16), dtype=torch.int64), |
| 64 | +# ] |
| 65 | +# # torch.export mints independent symbols s0, s1 for the batch axis of |
| 66 | +# # each input. The broadcast forces Eq(s0, s1), which the exporter rejects. |
| 67 | +# |
| 68 | +# %% |
| 69 | +# With ``shared_dims`` — correct approach (positional inputs) |
| 70 | +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 71 | +# |
| 72 | +# Annotate the batch axis (axis 0) with the same name ``"B"`` on both inputs. |
| 73 | +# Torch-TensorRT creates a single shared ``torch.export.Dim("B")`` for that |
| 74 | +# axis so the equality constraint is satisfied up front. |
| 75 | + |
| 76 | +inputs = [ |
| 77 | + torch_tensorrt.Input( |
| 78 | + min_shape=(1, 16), |
| 79 | + opt_shape=(4, 16), |
| 80 | + max_shape=(8, 16), |
| 81 | + dtype=torch.int64, |
| 82 | + name="input_ids", |
| 83 | + shared_dims={0: "B"}, |
| 84 | + ), |
| 85 | + torch_tensorrt.Input( |
| 86 | + min_shape=(1, 16), |
| 87 | + opt_shape=(4, 16), |
| 88 | + max_shape=(8, 16), |
| 89 | + dtype=torch.int64, |
| 90 | + name="attention_mask", |
| 91 | + shared_dims={0: "B"}, |
| 92 | + ), |
| 93 | +] |
| 94 | + |
| 95 | +trt_model = torch_tensorrt.compile( |
| 96 | + model, |
| 97 | + ir="dynamo", |
| 98 | + inputs=inputs, |
| 99 | + min_block_size=1, |
| 100 | + cache_built_engines=False, |
| 101 | + reuse_cached_engines=False, |
| 102 | +) |
| 103 | + |
| 104 | +# %% |
| 105 | +# Verify correctness at multiple batch sizes within the declared range. |
| 106 | + |
| 107 | +for batch_size in (4, 2, 1): |
| 108 | + ids = torch.randint(0, 1024, (batch_size, 16), dtype=torch.int64, device="cuda") |
| 109 | + mask = torch.ones((batch_size, 16), dtype=torch.int64, device="cuda") |
| 110 | + |
| 111 | + with torch.no_grad(): |
| 112 | + ref = model(ids, mask) |
| 113 | + out = trt_model(ids, mask) |
| 114 | + |
| 115 | + cos_sim = cosine_similarity(ref, out) |
| 116 | + assert ( |
| 117 | + cos_sim > COSINE_THRESHOLD |
| 118 | + ), f"Numerical mismatch at batch_size={batch_size}: cos_sim={cos_sim:.4f}" |
| 119 | + print(f"batch_size={batch_size} cos_sim={cos_sim:.6f} ✓") |
| 120 | + |
| 121 | +# %% |
| 122 | +# With ``shared_dims`` — kwarg inputs |
| 123 | +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 124 | +# |
| 125 | +# The same feature works with ``kwarg_inputs``, which is the natural form for |
| 126 | +# HuggingFace models whose ``forward`` signature uses keyword arguments. |
| 127 | + |
| 128 | +kwarg_inputs = { |
| 129 | + "input_ids": torch_tensorrt.Input( |
| 130 | + min_shape=(1, 16), |
| 131 | + opt_shape=(4, 16), |
| 132 | + max_shape=(8, 16), |
| 133 | + dtype=torch.int64, |
| 134 | + name="input_ids", |
| 135 | + shared_dims={0: "B"}, |
| 136 | + ), |
| 137 | + "attention_mask": torch_tensorrt.Input( |
| 138 | + min_shape=(1, 16), |
| 139 | + opt_shape=(4, 16), |
| 140 | + max_shape=(8, 16), |
| 141 | + dtype=torch.int64, |
| 142 | + name="attention_mask", |
| 143 | + shared_dims={0: "B"}, |
| 144 | + ), |
| 145 | +} |
| 146 | + |
| 147 | +trt_model_kwargs = torch_tensorrt.compile( |
| 148 | + model, |
| 149 | + ir="dynamo", |
| 150 | + kwarg_inputs=kwarg_inputs, |
| 151 | + min_block_size=1, |
| 152 | + cache_built_engines=False, |
| 153 | + reuse_cached_engines=False, |
| 154 | +) |
| 155 | + |
| 156 | +ids = torch.randint(0, 1024, (4, 16), dtype=torch.int64, device="cuda") |
| 157 | +mask = torch.ones((4, 16), dtype=torch.int64, device="cuda") |
| 158 | + |
| 159 | +with torch.no_grad(): |
| 160 | + ref = model(input_ids=ids, attention_mask=mask) |
| 161 | + out = trt_model_kwargs(input_ids=ids, attention_mask=mask) |
| 162 | + |
| 163 | +cos_sim = cosine_similarity(ref, out) |
| 164 | +assert cos_sim > COSINE_THRESHOLD, f"kwarg path mismatch: cos_sim={cos_sim:.4f}" |
| 165 | +print(f"kwarg_inputs path cos_sim={cos_sim:.6f} ✓") |
| 166 | + |
| 167 | +# %% |
| 168 | +# Sharing multiple axes |
| 169 | +# ^^^^^^^^^^^^^^^^^^^^^^ |
| 170 | +# |
| 171 | +# If both batch and sequence length are dynamic and must be shared, annotate |
| 172 | +# both axes on each input: |
| 173 | +# |
| 174 | +# .. code-block:: python |
| 175 | +# |
| 176 | +# shared_dims={0: "B", 1: "S"} |
| 177 | +# |
| 178 | +# The same name on the same axis across different inputs produces one shared |
| 179 | +# ``Dim``; different names on different axes produce independent ``Dim``\s. |
| 180 | + |
| 181 | +print("\nAll checks passed.") |
0 commit comments