Strategies for sharing a single RTX GPU between Ollama LLM inference and ComfyUI Stable Diffusion on the same homelab machine.

The VRAM Reality Check: What Actually Fits on a 16GB RTX 5060 Ti

Understanding your hardware limits is the first step to successful GPU sharing:

Ollama VRAM Consumption (Approximate)

  • Qwen2.5-Coder:14b (Q4_K_M): ~7.5 GB VRAM
  • DeepSeek-R1:14b (Q4_K_M): ~7.5 GB VRAM
  • Llama3.1:8b (Q4_K_M): ~4.5 GB VRAM
  • Qwen3:8b (Q4_K_M): ~4.5 GB VRAM
  • Nomic-Embed-Text: ~1.2 GB VRAM
  • System overhead + CUDA context: ~1-2 GB VRAM

ComfyUI VRAM Consumption (Approximate)

  • Base ComfyUI + VAE: ~1.5-2 GB VRAM
  • SDXL Base (1024x1024): ~6-8 GB VRAM
  • SD 1.5 Base (512x512): ~3-4 GB VRAM
  • ControlNet units: ~1-2 GB VRAM each
  • Hi-Res fix + upscaling: +2-4 GB VRAM
  • Batch size >1: Linear increase per image
  • System overhead: ~1-2 GB VRAM

The Hard Limits:

With 16GB total VRAM:

  • Ollama 14b + SDXL 1024x1024 = ~7.5GB + ~7GB = ~14.5GB (tight but possible)
  • Ollama 14b + SD 1.5 + ControlNet = ~7.5GB + ~4GB + ~2GB = ~13.5GB (comfortable)
  • Two 14b models = ~15GB (leaves ~1GB for system - not recommended)
  • Ollama 14b + SDXL + Hi-Res + ControlNet = Likely OOM

Scheduling Strategies: Time-Based vs Priority-Based Access

Two main approaches to GPU sharing, each with different trade-offs:

Strategy 1: Time-Based Scheduling (Shift Work)

Allocate explicit time windows to each application:

# Example: Daily schedule for balanced usage # 6:00 AM - 10:00 AM: Ollama priority (code assistant during work hours) # 10:00 AM - 12:00 PM: ComfyUI priority (creative work) # 12:00 PM - 2:00 PM: Shared/lunch break (light usage both) # 2:00 PM - 6:00 PM: Ollama priority (continued work) # 6:00 PM - 10:00 PM: ComfyUI priority (evening creative sessions) # 10:00 PM - 6:00 AM: Ollama priority (background processing, embeddings) # Implementation via systemd timers or cron # Ollama gets GPU during work hours 0 6 * * * /usr/local/bin/gpu-grant ollama 0 10 * * * /usr/local/bin/gpu-revoke ollama 0 12 * * * /usr/local/bin/gpu-grant comfyui 0 14 * * * /usr/local/bin/gpu-revoke comfyui # ... and so on 

Strategy 2: Priority-Based Access (Reservation System)

Applications request GPU access and get granted based on priority:

# Example: Priority queue implementation # Priority levels (higher number = higher priority) # 10: Interactive user requests (chat, immediate image generation) # 5: Background processing (embeddings, batch jobs) # 1: Maintenance/cleanup tasks # Pseudocode for GPU arbiter class GpuArbiter { constructor() { this.queue = new PriorityQueue(); this.currentHolder = null; } async requestAccess(application, priority, estimatedDuration) { // Add request to queue await this.queue.enqueue({ application, priority, estimatedDuration, timestamp: Date.now() }); // If we're not holding GPU or new request has higher priority if (!this.currentHolder || priority > this.currentHolder.priority) { await this.preemptAndGrant(application, priority); } } async releaseAccess(application) { if (this.currentHolder?.application === application) { this.currentHolder = null; await this.grantNextInQueue(); } } } // Usage in Ollama wrapper class OllamaManager { constructor(gpuArbiter) { this.gpuArbiter = gpuArbiter; } async generate(prompt) { // Request GPU with medium priority await this.gpuArbiter.requestAccess('ollama', 5, 30000); // 30s estimate try { return await this.ollamaInstance.generate(prompt); } finally { await this.gpuArbiter.releaseAccess('ollama'); } } } // Usage in ComfyUI wrapper class ComfyUIManager { constructor(gpuArbiter) { this.gpuArbiter = gpuArbiter; } async generateImage(params) { // Request GPU with high priority for interactive use await this.gpuArbiter.requestAccess('comfyui', 10, 15000); // 15s estimate try { return await this.comfyuiInstance.generateImage(params); } finally { await this.gpuArbiter.releaseAccess('comfyui'); } } } 

Hybrid Approach Recommendation:

Combine both strategies:

  • Use time-based scheduling for predictable workloads (work hours vs evening)
  • Use priority-based preemption for urgent interactive requests
  • Allow background tasks to use GPU during idle periods
  • Implement graceful preemption (save state, restore later)

VRAM Partitioning: Static vs Dynamic Allocation

Instead of pure time-sharing, consider splitting the GPU memory:

Static Partitioning (Fixed Split)

Divide VRAM upfront and never exceed your allocation:

7GB/9GB Split (Ollama Heavy)

  • Ollama: 7GB (fits Qwen2.5-Coder:14b or DeepSeek-R1:14b)
  • ComfyUI: 9GB (SDXL 1024x1024 + 1 ControlNet + basic upscaling)
  • Best for: Development work where coding assistance is primary

5GB/11GB Split (Balanced)

  • Ollama: 5GB (fits Llama3.1:8b or Qwen3:8b)
  • ComfyUI: 11GB (SDXL + multiple ControlNets + Hi-Res fix)
  • Best for: Mixed usage where both get reasonable resources

3GB/13GB Split (ComfyUI Heavy)

  • Ollama: 3GB (very small models or embedding-only)
  • ComfyUI: 13GB (SDXL batch processing, video generation, extensive ControlNet)
  • Best for: Art generation focus with occasional LLM queries

Static Partitioning Limitations:

While simple, this approach has drawbacks:

  • Wasted resources when one application is idle
  • No ability to handle bursts beyond allocation
  • Requires restarting applications to change partition sizes
  • Complex to implement correctly with CUDA/Vulkan

Dynamic Partitioning (Preferred Approach)

Allow applications to use available VRAM, but with limits and priorities:

# Implementation using CUDA MPS (Multi-Process Service) or similar # Actually, for most users, a simpler approach works better:  # 1. Monitor VRAM usage in real-time # 2. Set soft limits that applications should stay under # 3. Allow temporary exceeding with graceful degradation # 4. Send warnings when approaching hard limits  # Example monitoring script #!/usr/bin/env python3 import subprocess import time import requests def get_vram_usage(): """Get current VRAM usage in MB""" try: result = subprocess.run([ 'nvidia-smi', '--query-gpu=memory.used,memory.total', '--format=csv,noheader,nounits' ], capture_output=True, text=True, check=True) used, total = map(int, result.stdout.strip().split(', ')) return used, total except Exception as e: print(f"Error getting VRAM usage: {e}") return 0, 16384 # 16GB in MB  def check_and_warn(): used, total = get_vram_usage() percent = (used / total) * 100 if percent > 90: print(f"⚠️ HIGH VRAM USAGE: {used}MB / {total}MB ({percent:.1f}%)") # Could trigger notifications or automatic cleanup  elif percent > 80: print(f"⚠️ Elevated VRAM usage: {used}MB / {total}MB ({percent:.1f}%)") return percent > 95 # Return True if approaching critical  # Main monitoring loop if __name__ == "__main__": print("Starting VRAM monitor...") while True: if check_and_warn(): # In a real implementation, you might:  # 1. Send a desktop notification  # 2. Trigger graceful cleanup in applications  # 3. Log the event for analysis  pass time.sleep(5) # Check every 5 seconds 

Practical Dynamic Strategy:

For most homelab users:

  • Run Ollama with smaller models during the day (8b parameters)
  • Switch to larger models (14b) during dedicated LLM sessions
  • Use ComfyUI with moderate settings most of the time
  • Save extreme ComfyUI settings for dedicated creative sessions
  • Use VRAM monitoring to avoid surprises

Model Quantization: Your Best Friend for VRAM Efficiency

Choosing the right quantization level dramatically affects what fits:

Quantization Impact on 14b Models

Quantization VRAM Usage Quality Impact Speed Impact
Q8_0 ~10.5 GB Minimal None
Q6_K ~9.0 GB Very minor None
Q5_K_M ~7.8 GB Minor None
Q4_K_M ~7.5 GB Noticeable but acceptable None
Q3_K_M ~6.2 GB Moderate None
Q2_K ~5.0 GB Significant None

Practical Quantization Recommendations

  • For coding assistance: Q4_K_M or Q5_K_M (good balance)
  • For mathematical/logical tasks: Q5_K_M or Q6_K (better reasoning)
  • For casual chat: Q3_K_M or Q2_K (acceptable quality, saves VRAM)
  • For embeddings: Always use quantized versions (tiny VRAM footprint)
  • Avoid: Floating point (F16) unless you have 24GB+ VRAM
# Example: Ollama model quantization choices # Pull different quantizations for different use cases # High-quality coding assistant (when you have the VRAM to spare) ollama pull qwen2.5-coder:14b-q5_K_M # General purpose assistant (balanced) ollama pull qwen2.5-coder:14b-q4_K_M # Lightweight assistant (when sharing GPU heavily) ollama pull qwen2.5-coder:14b-q3_K_M # Embedding model (tiny footprint) ollama pull nomic-embed-text:latest # Switch between them based on current needs # You can even run multiple quantizations of same model! 

ComfyUI Optimization: Getting More from Limited VRAM

Stable Diffusion has many knobs to tune for VRAM efficiency:

Essential ComfyUI VRAM Savers:

  • Use SD 1.5 instead of SDXL when possible: ~50% VRAM reduction
  • Enable attention slicing: trades compute for VRAM (often worth it)
  • Use VAE tiling: processes image in chunks to reduce peak VRAM
  • Limit ControlNet units: each additional unit adds ~1-2GB VRAM
  • Use lower batch sizes: batch=1 is VRAM efficient
  • Enable model offloading: move unused components to RAM
  • Use xformers: more memory-efficient attention implementation
  • Prefer FP8 over FP16 when available: halves VRAM for certain operations

Resolution and Performance Trade-offs:

VRAM by Resolution (SD 1.5)

Resolution Base VRAM With Hi-Res Fix Typical Use Case
512x512 ~3.5 GB ~5.5 GB Quick iterations, concepts
768x768 ~5.0 GB ~8.0 GB Balanced quality/speed
1024x1024 ~7.5 GB ~12.0 GB High quality generations
1152x896 ~8.5 GB ~13.5 GB Widescreen compositions

VRAM by Resolution (SDXL)

Resolution Base VRAM With Hi-Res Fix Typical Use Case
768x768 ~6.0 GB ~9.0 GB Moderate quality
1024x1024 ~8.0 GB ~12.5 GB Standard quality
1152x896 ~9.0 GB ~14.0 GB Widescreen (needs optimization)
1280x720 ~9.5 GB ~15.0 GB Landscape (tight fit)
# Example: ComfyUI launch with VRAM optimizations # Add these to your ComfyUI startup script or command line #!/bin/bash # Launch ComfyUI with memory optimizations export COMMANDLINE_ARGS="--medvram --opencv --disable-numpy-random-seed" # --medvram: Medium VRAM optimization (enables attention slicing, etc.) # --lowvram: More aggressive (slower but uses less VRAM) # --disable-numpy-random-seed: Fixes nondeterminism in some cases # Alternative: Use environment variables export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 # Helps with memory fragmentation # Start ComfyUI python main.py --listen --port 8188 # For extreme VRAM constraints, consider: # --ckpt sd-v1-5.ckpt # Use smaller model # --no-half-vae # Disable VAE FP16 (saves VRAM, costs quality) 

Queue Management: Preventing Resource Exhaustion

Even with good scheduling, prevent overload with proper queueing:

Ollama Request Queuing:

Prevent overwhelming the LLM with concurrent requests:

# Simple request queue for Ollama import asyncio import time from collections import deque from dataclasses import dataclass from typing import Callable, Any @dataclass class OllamaRequest: prompt: str model: str future: asyncio.Future timestamp: float priority: int = 0 # Higher = more urgent  class OllamaRequestQueue: def __init__(self, max_concurrent=1): self.max_concurrent = max_concurrent self.queue = deque() self.processing = 0 self.results = {} async def add_request(self, prompt, model="qwen2.5-coder:14b", priority=0): """Add a request to the queue and return a future for the result""" future = asyncio.Future() request = OllamaRequest(prompt, model, future, time.time(), priority) # Insert in priority order  inserted = False for i, req in enumerate(self.queue): if priority > req.priority: self.queue.insert(i, request) inserted = True break if not inserted: self.queue.append(request) await self._process_queue() return future async def _process_queue(self): """Process requests from the queue""" while (self.queue and self.processing < self.max_concurrent): request = self.queue.popleft() self.processing += 1 # Process the request (this would call your Ollama client)  asyncio.create_task(self._handle_request(request)) async def _handle_request(self, request): """Actually process a single request""" try: # Simulate calling Ollama - replace with actual implementation  start = time.time() # result = await ollama_client.generate(  # model=request.model,  # prompt=request.prompt  # )  await asyncio.sleep(2) # Simulate processing time  result = f"Response to: {request.prompt[:50]}..." # Calculate and store result  request.future.set_result({ 'response': result, 'processing_time': time.time() - start }) except Exception as e: request.future.set_exception(e) finally: self.processing -= 1 await self._process_queue() # Process next in queue  # Usage queue = OllamaRequestQueue(max_concurrent=1) # Sequential processing  # Add requests - they'll be processed sequentially future1 = queue.add_request("Explain quantum computing") future2 = queue.add_request("Write a Python function to sort a list") # Wait for results result1 = await future1 result2 = await future2 

ComfyUI Job Queuing:

Manage image generation requests to prevent VRAM spikes:

# Simple ComfyUI job queue import asyncio import uuid from dataclasses import dataclass from enum import Enum class JobStatus(Enum): QUEUED = "queued" PROCESSING = "processing" COMPLETED = "completed" FAILED = "failed" @dataclass class ComfyUIJob: id: str prompt: str params: dict status: JobStatus result: any = None error: str = None created_at: float = 0 started_at: float = 0 completed_at: float = 0 class ComfyUIJobQueue: def __init__(self, max_concurrent=1): self.max_concurrent = max_concurrent self.queue = [] # List of ComfyUIJob  self.processing = {} # id -> ComfyUIJob  self.results = {} # id -> result  async def add_job(self, prompt, params=None, priority=0): """Add a job to the queue""" if params is None: params = {} job = ComfyUIJob( id=str(uuid.uuid4()), prompt=prompt, params=params, status=JobStatus.QUEUED, created_at=time.time() ) # Add to queue (simple FIFO for now - could add priority)  self.queue.append(job) await self._process_queue() return job.id async def _process_queue(self): """Start processing queued jobs""" while (len(self.queue) > 0 and len(self.processing) < self.max_concurrent): job = self.queue.pop(0) job.status = JobStatus.PROCESSING job.started_at = time.time() self.processing[job.id] = job # Process the job  asyncio.create_task(self._handle_job(job)) async def _handle_job(self, job): """Actually process a ComfyUI job""" try: # Simulate calling ComfyUI - replace with actual API call  await asyncio.sleep(5) # Simulate image generation time  # In reality, you'd call ComfyUI's API here  result = { 'image_path': f"/tmp/comfyui_{job.id}.png", 'prompt': job.prompt, 'params': job.params, 'generation_time': time.time() - job.started_at } job.status = JobStatus.COMPLETED job.result = result job.completed_at = time.time() self.results[job.id] = result except Exception as e: job.status = JobStatus.FAILED job.error = str(e) job.completed_at = time.time() finally: # Clean up processing tracking  if job.id in self.processing: del self.processing[job.id] # Process next job  await self._process_queue() def get_job_status(self, job_id):Source:https://dev.to/ayraix/ollama-gpu-scheduling-running-inference-and-comfyui-on-one-rtx-without-oom-1j75

Related posts