Theo Bouwman
12/15/2025, 7:40 PMMarvin
12/15/2025, 7:40 PMTheo Bouwman
12/15/2025, 7:42 PMMarvin
12/15/2025, 7:44 PMtimeout_seconds, timeouts are cooperative and cannot interrupt long-running C extensions like PyTorch/Transformers model loading. If the load is stuck inside compiled code or a blocked system call, the Python-level timeout won’t fire until control returns to Python.
- That explains “loads forever” — without a hard boundary, the task will wait indefinitely if the HF/torch call hangs.
Relevant docs and source
- Timeouts/cancellation guide: How to cancel workflows if they run too long
- Task timeout behavior in code: utilities/timeout.py and task_engine.py (where task timeouts are applied)
- Cancellation implementation details: cancellation.py (notes on limitations around C extensions)
What I recommend
1) Add a task timeout anyway (soft guard)
- This won’t kill a stuck torch load, but it will fail/retry quickly when the hang is not in C (e.g., Python-level I/O).
- Consider jitter to avoid thundering herds.
@task(
name="initialize_classifier",
retries=2,
retry_delay_seconds=[15, 60],
retry_jitter_factor=0.3,
timeout_seconds=90, # soft timeout
)
def initialize_classifier_task():
logger = get_run_logger()
if torch.backends.mps.is_available():
device = "mps"
<http://logger.info|logger.info>("Using MPS (Metal) acceleration")
elif torch.cuda.is_available():
device = 0
<http://logger.info|logger.info>("Using CUDA GPU")
else:
device = -1
<http://logger.info|logger.info>("Using CPU")
<http://logger.info|logger.info>(f"Loading model: {MODEL_NAME}...")
# Strongly consider preventing network on init to avoid hangs
classifier = pipeline(
"zero-shot-classification",
model=MODEL_NAME,
device=device,
torch_dtype=torch.float32,
# local_files_only=True, # enable if you pre-bundle weights
)
<http://logger.info|logger.info>("Model loaded successfully!")
return classifier, device
2) For a hard stop, load in a subprocess with an OS-enforced timeout
- Python can’t interrupt torch C code, but the OS can kill a stuck subprocess. You cannot pass the pipeline object back across processes, so the typical pattern is:
- Move model use into the subprocess as well (load + do work + return results)
- Or run a long-lived model-serving process (HTTP/gRPC) and call it from Prefect
Example (subprocess for “health check”/warmup; adapt to also run your inference and return results instead of trying to return the pipeline):
@task(retries=2, retry_delay_seconds=30)
def warmup_model_hard_timeout(timeout=60):
import sys, subprocess, textwrap
code = textwrap.dedent(f"""
import torch
from transformers import pipeline
pipe = pipeline("zero-shot-classification", model="{MODEL_NAME}", device=-1, torch_dtype=torch.float32)
out = pipe("warmup", candidate_labels=["a","b"])
print("READY")
""")
try:
p = subprocess.run([sys.executable, "-c", code], timeout=timeout, capture_output=True, text=True)
p.check_returncode()
if "READY" not in p.stdout:
raise RuntimeError(f"Warmup failed: {p.stdout} {p.stderr}")
return True
except subprocess.TimeoutExpired:
raise RuntimeError(f"Model warmup exceeded {timeout}s (killed)")
If you need to keep the model in memory for multiple calls, consider:
- Running a small model-serving container (FastAPI/uvicorn) in the same pod or as a separate service and call it from Prefect.Marvin
12/15/2025, 7:44 PMHF_HOME to that path and pass local_files_only=True to pipeline.
- Pin a revision for the model to avoid changing downloads.
- Ensure egress/network stability if you’re pulling from the Hub; intermittent network stalls can look like a “hang”.
- Optional envs that can help with observability and faster transfers:
- TRANSFORMERS_VERBOSITY=info
- HF_HUB_ENABLE_HF_TRANSFER=1
- HF_HUB_DISABLE_TELEMETRY=1
4) Add a flow-level backstop timeout (optional)
- You can also set a timeout on the flow to fail the entire run if something truly runaway occurs.
A couple of clarifying questions to tailor the fix:
- Are you on Prefect 3.x and running on Linux in K8s? (Timeouts are not supported on Windows.)
- Do you see any HF downloads during the “stuck” runs, or is the model fully cached?
- Do you need to reuse the in-memory classifier across multiple tasks, or could you move loading + inference into a single step (or a model-serving sidecar)?
If you share the exact MODEL_NAME and Transformers/Torch versions, I can suggest concrete pipeline(...) flags and a Dockerfile snippet to pre-bundle the model.Theo Bouwman
12/15/2025, 10:12 PMMarvin
12/15/2025, 10:12 PMMarvin
12/15/2025, 10:13 PMtimeout_seconds can’t interrupt it.
- Passing the pipeline object between tasks can break if it needs pickling (e.g., different task runners or distributed execution). It’s not reliably serializable.
- Reuse is only within a single flow run; the process ends after the run, so you’ll reload on the next run.
2) Load and run inference in the same task (no reuse)
- Pattern: Each inference task loads the model and immediately runs inference.
- Pros: Easy to wrap with a subprocess for a hard timeout; isolates hangs to a single task call.
- Cons: You pay the model load cost every time — slow and wasteful unless you do very few inferences per run.
- When you need a hard timeout, this is the simplest to implement now.
3) Model-serving sidecar/service (recommended for reuse + hard timeouts)
- Pattern: Run a separate long-lived process (e.g., a FastAPI/uvicorn server) that loads the model once and serves requests. Your Prefect task calls it over HTTP/gRPC with client-side timeouts.
- Pros:
- Model is warm and reused across many flow runs.
- You can enforce timeouts at the request layer and use K8s liveness/readiness probes to kill/restart the server if it hangs.
- Fully isolates model C-level hangs from your Prefect worker.
- Cons: Requires deploying/operating an extra component (sidecar container in the same pod, or a separate service).
4) Advanced: a managed worker process you can restart
- Pattern: Maintain a long-lived subprocess (or a small process pool) that loads the model once. Send inference jobs via a queue/IPC. If a job exceeds a deadline, terminate that process and respawn it.
- Pros: Reuse the model across requests without a full HTTP service; you can still hard-kill a hung process.
- Cons: More code to manage lifecycle and restarts; similar operational complexity to a microservice.
How to choose
- If you do many inferences per run or across runs: use a model-serving sidecar/service so you load once and enforce hard timeouts robustly.
- If you do few inferences and want simplicity now: do load+infer in a single task and wrap it in a subprocess with an OS-enforced timeout.
- If you tried to pass the pipeline object between tasks: expect serialization and execution-environment pitfalls; it’s not a safe long-term pattern.
If you share:
- How many inferences you run per flow (and per day),
- Whether you run on a Kubernetes worker (one pod per flow run) and
- Whether operating a small sidecar service is acceptable,
I can suggest a concrete design (FastAPI sidecar example + Prefect task client with timeouts), or a subprocess-wrapped single-task pattern you can drop in.