The use case: Needing to cancel specific workflows by ID without affecting others
- Dominant language
- Python
- Stars
- 133k
- Forks
- 15.7k
- Avg merge
- 1d 6h
- Merged PRs (30d)
- 155
Description
### Feature Idea
Adding a new endpoint like `/prompt/cancel/{prompt_id} `for surgical cancellation
> ComfyUI currently lacks the ability to surgically cancel a specific workflow by its prompt ID.
_In multi-user or automated environments, we need the ability to cancel individual workflows without affecting others._
Sending `POST` req: `http://127.0.0.1:8000/prompt/cancel/{prompt_id}`
### Responses
`200 OK`: Successfully canceled the workflow
```json
{
"success": true,
"message": "Successfully canceled currently running prompt abc123",
"status": "running"
}
```
`404 Not Found`: Workflow not found
```
{
"success": false,
"error": "Prompt abc123 not found in execution system",
"status": "not_found"
}
```
`400 Bad Request`: Workflow already completed
```json
{
"success": false,
"error": "Cannot cancel prompt abc123 as it has already completed execution",
"status": "completed"
}
```
`500 Internal Server Error`: Server error
```json
{
"success": false,
"error": "Internal error: [error message]"
```
### Existing Solutions
The only effective method current is to `POST` an `/interrupt` , but `/interrupt` endpoint is a global kill switch that stops all running workflows? Is that true? It's a bit confusing
### Other
In ComfyUI's server implementation, the `/interrupt` endpoint is registered like this:
```python
# From server.py
@routes.post('/interrupt')
async def interrupt(request):
prompt_queue.interrupt()
return web.json_response({}, status=200)
```
This simple implementation just calls `prompt_queue.interrupt()` method.
The prompt queue's `interrupt() `method is more complex
```python
# From execution.py
def interrupt(self):
"""Set the 'interrupted' flag and take additional steps to halt execution"""
# Set the interrupted flag
self.set_flag("interrupted", True)
# The key part: Signal to the execution system to stop
if hasattr(self, 'execution_thread') and self.execution_thread is not None:
self.execution_thread.interrupt()
# Clear any currently executing nodes
if 'executing_nodes' in self.flags:
for node_id in self.flags['executing_nodes']:
self.interrupt_node(node_id)
```
The execution thread has its own interrupt mechanism:
```python
# From execution_thread.py
def interrupt(self):
"""Interrupt the execution thread"""
# Special handling for different execution states
if self.execution_state == EXECUTION_STATE.SAMPLING:
# Interrupt sampling process
self.sampling_interrupt = True
# Direct call to CUDA/GPU interrupt if available
if hasattr(self.sampler, 'interrupt'):
self.sampler.interrupt()
# Set thread-level interrupted flag
self.interrupted = True
```
The execution loop checks for interruption at strategic points:
```python
# Simplified from the execution loop
def execute_node(node_id, prompt):
# Check for interruption before executing
if self.is_interrupted():
raise InterruptedException("Execution was interrupted")
# Execute the node...
result = node.execute(inputs)
# Check again after execution
if self.is_interrupted():
raise InterruptedException("Execution was interrupted")
```
For long-running operations like sampling (⚠️ important for us as we want to probably cancel here ⚠️), there's a deeper mechanism:
```python
# Inside sampling code
def sample(self, steps, ...):
for step in range(steps):
# Process step...
# Check for interruption at each step
if self.check_interrupt():
print("Sampling interrupted at step", step)
break
```
How how GPU operations are interrupted ?
```python
# In sampler implementation
def interrupt(self):
# Set a CUDA event to signal interruption
if hasattr(torch, 'cuda') and torch.cuda.is_available():
torch.cuda.synchronize() # Force synchronization
self.interrupted_event.set() # Signal to GPU operations
```
After this surgery 😵😵😵😵
I tried to have a **custom node** for **custom_route**.
`__init__.py` - Entry point that registers our custom routes
`routes.py` - Implementation of the `/prompt/cancel/{prompt_id}` endpoint
That accessed ComfyUI's Internal Queue and examined ComfyUI's structure through debugging and found:
1. Server has a `prompt_queue `with attributes like `currently_running`, `queue`, and `history`
2. The `currently_running` structure is a dictionary where 0 contains a tuple with prompt ID at index 1
3. The queue items follow a similar pattern
So I tried to:
1. Find the specific prompt by ID in `currently_running` or `queue`
2. For queued items: Remove them from the queue (works correctly) ✅
3. For running items: Attempt to interrupt only that specific workflow (doesn't work) ❌
uber simplified implementation:
```python
@server.routes.post('/prompt/cancel/{prompt_id}')
async def cancel_specific_prompt(request):
prompt_id = request.match_info.get('prompt_id')
# Get prompt queue
prompt_queue = server.prompt_queue
# Check if this prompt is currently running
if prompt_queue.currently_running:
running_data = prompt_queue.currently_running[0]
running_prompt_id = running_data[1]
if running_prompt_id == prompt_id:
# We tried multiple approaches:
# 1. Setting the interrupted flag
prompt_queue.set_flag("interrupted", True)
# 2. Directly accessing the execution thread (didn't work)
# prompt_queue.execution_thread.interrupt()
# 3. Making a direct request to /interrupt (works but cancels EVERYTHING)
async with aiohttp.ClientSession() as session:
await session.post(f"http://127.0.0.1:8000/interrupt")
return web.json_response({"success": True})
# Check if prompt is in the queue (this part works well)
for i, item in enumerate(prompt_queue.queue):
item_prompt_id = item[1]
if item_prompt_id == prompt_id:
prompt_queue.delete_queue_item(i)
return web.json_response({"success": True})
return web.json_response({"success": False, "error": "Prompt not found"})
```
```bash
0%| | 0/20 [00:00
DEBUG: Extracted running prompt_id: 1a6ab0a5-ca47-472b-a2e1-c1e724e644a2
DEBUG: Found matching currently running prompt. Forcefully interrupting.
FETCH ComfyRegistry Data: 35/80
25%|██▌ | 5/20 [00:09<00:29, 1.95s/it]FETCH ComfyRegistry Data: 40/80
35%|███▌ | 7/20 [00:13<00:25, 1.93s/it]FETCH ComfyRegistry Data: 45/80
Successfully sent force interrupt request to http://127.0.0.1:8000/interrupt
...
```
Contributor guide
Assessment
This issue has not been assessed yet.