Problem – Batch Size Overflow During Live Video Streaming Inference
In a production pipeline that consumes a continuous stream of video frames, the inference service throws runtime errors such as:
RuntimeError: Expected tensor size [16, 3, 720, 1280] but got [32, 3, 720, 1280]
AssertionError: batch size exceeds max_batch_size (configured in TorchServe)
These errors appear intermittently when the frame arrival rate spikes (e.g., 30 fps bursts) or when the source resolution changes. The symptom is a sudden increase in latency, request rejections, and in the worst case, CUDA out‑of‑memory crashes.
Root Cause – Static Batch Allocation Mismatch
Two orthogonal mechanisms in PyTorch‑based streaming pipelines can cause the overflow:
- Pre‑allocated input tensors – Many services allocate a fixed‑size buffer at startup using
torch.empty(max_batch, C, H, W)or rely on TorchServe’smax_batch_size. When the incoming batch dimension exceeds this static size,torch.Tensor.resize_refuses to grow the underlying storage, and the runtime raises a size‑mismatch error (see PyTorch Tensor documentation – torch.Tensor.resize_). - DataLoader / collate function expectations – The
forwardmethod of atorch.nn.Moduleexpects a tensor whose first dimension matches the batch size it was called with. If a customcollate_fnassumes a constant batch size (as described in the torch.utils.data.DataLoader documentation), a burst of frames will produce a larger list thattorch.stackcannot concatenate without padding, leading to the “size mismatch” error (GitHub issue #65778).
In short, the pipeline treats the batch dimension as immutable, while the live video source is inherently variable.
Debug – Systematic Investigation Steps
1. Capture the offending request log
2026-09-15 14:32:07,842 ERROR inference_worker.py:112 - RuntimeError: Expected tensor size [16, 3, 720, 1280] but got [32, 3, 720, 1280]
2026-09-15 14:32:07,845 INFO inference_worker.py:118 - Received 32 frames in 33ms (burst @ 30fps)
2. Verify the configured batch limits
# TorchServe config (config.properties)
max_batch_size=16
batch_delay=100
3. Inspect the tensor allocation path
def allocate_input_buffer(max_batch, C, H, W, device):
# Original (buggy) code
return torch.empty(max_batch, C, H, W, device=device)
4. Reproduce locally with a synthetic burst
import torch, time
def simulate_burst(batch_sizes, C=3, H=720, W=1280):
for b in batch_sizes:
frames = torch.randn(b, C, H, W)
try:
model(frames) # forward expects static batch
except RuntimeError as e:
print(f"Failed for batch {b}: {e}")
simulate_burst([8, 16, 32])
5. Check DataLoader collate behavior
def collate_fn(batch):
# Original implementation assumes len(batch) == 1
return torch.stack(batch) # raises size mismatch when len(batch) > 1
6. Review CUDA memory usage before the failure
$ nvidia-smi
+-----------------------------------------------------------------------------+
| Processes: GPU Memory |
| PID Type Process name Usage |
| 12345 C++ python 8200MiB |
+-----------------------------------------------------------------------------+
Solution – Dynamic Batching and Safe Buffer Management
1. Replace static buffer with on‑the‑fly allocation
Use torch.empty_like or allocate per‑batch to let the runtime grow the tensor as needed.
# Before (static)
def allocate_input_buffer(max_batch, C, H, W, device):
return torch.empty(max_batch, C, H, W, device=device)
# After (dynamic)
def allocate_input_buffer(batch, C, H, W, device):
return torch.empty(batch, C, H, W, device=device)
2. Enable TorchServe async batching
Set max_batch_delay and increase max_batch_size to accommodate bursts, while allowing the server to accumulate frames up to the limit.
# config.properties
max_batch_size=32 # match observed peak
max_batch_delay=50 # ms, smaller than latency budget
3. Implement a flexible collate_fn with padding
def collate_fn(batch):
# batch is a list of tensors with shape [C, H, W]
# Pad to the largest height/width if resolution changes
max_h = max(t.shape[1] for t in batch)
max_w = max(t.shape[2] for t in batch)
padded = []
for t in batch:
pad_h = max_h - t.shape[1]
pad_w = max_w - t.shape[2]
padded.append(torch.nn.functional.pad(t, (0, pad_w, 0, pad_h)))
return torch.stack(padded) # now batch dimension is len(batch)
4. Guard the forward method against unexpected batch sizes
class VideoModel(nn.Module):
def forward(self, x):
if x.size(0) > self.max_batch:
raise RuntimeError(f"Batch size {x.size(0)} exceeds allowed {self.max_batch}")
# Normal processing …
return self._run_inference(x)
5. Optional: Use torch.utils.checkpoint to reduce peak memory during large batches
def _run_inference(self, x):
# checkpointing reduces intermediate activation memory
return torch.utils.checkpoint.checkpoint(self._model_body, x)
Verify – Confirming the Fix Works
- Run a controlled burst test (e.g., 40 fps for 5 seconds) and ensure no RuntimeError is logged.
- Check TorchServe metrics:
$ curl http://localhost:8082/metrics | grep batch
torchserve_batch_size{max="32"} 0
torchserve_batch_size{max="32"} 32
$ nvidia-smi --query-gpu=memory.used --format=csv
Memory Used
8200 MiB
Prevent – Operational Guardrails
- Monitoring: Alert when
torchserve_batch_sizehits the configuredmax_batch_sizefor more than 5 seconds. - Dynamic configuration: Deploy a sidecar that watches incoming frame rate and auto‑tunes
max_batch_sizevia TorchServe’s management API. - Input validation: Reject frames that exceed the maximum supported resolution early in the ingestion layer, returning a 400 error instead of propagating to the model.
- Testing: Include property‑based tests that generate random batch sizes and resolutions, asserting that the inference loop never raises a size‑mismatch error.
- Documentation: Explicitly note in the service README that the model expects a variable batch dimension and that static buffers are prohibited.
FAQ – Common Follow‑Up Questions
- Why does the error only appear during spikes and not under steady load?
During steady state the incoming batch size stays belowmax_batch_size. A burst temporarily exceeds the static allocation, triggering the mismatch. Adjustingmax_batch_delaylets TorchServe accumulate frames up to the new limit, smoothing the spike. - Can I keep a pre‑allocated buffer for performance reasons?
Yes, but you must allocate it with the maximum expected batch size and usetensor.narrowortensor[:actual_batch]to view only the active portion. Do not rely onresize_to grow the buffer; it will raise an error if the new size exceeds the original storage. - How do I know which batch size TorchServe actually received?
TorchServe exposes the metrictorchserve_batch_size. Query it via the Prometheus endpoint or use the management API/metricsto see the current batch dimension. - Is padding the right approach when resolution changes?
Padding ensures all tensors in a batch share the sameHandW, allowingtorch.stackto succeed. It adds negligible compute overhead compared to the cost of a failed inference. - What if I need to enforce a hard latency budget and cannot increase
max_batch_delay?
Switch to a per‑frame inference path for high‑priority streams and reserve the batched path for lower‑priority analytics. This hybrid approach keeps latency low while still benefiting from batching when traffic permits.
Related Topic Hub: Model Serving Troubleshooting Hub