Add IPFS HLS streaming and GPU optimizations
- Add IPFSHLSOutput class that uploads segments to IPFS as they're created - Update streaming task to use IPFS HLS output for distributed streaming - Add /ipfs-stream endpoint to get IPFS playlist URL - Update /stream endpoint to redirect to IPFS when available - Add GPU persistence mode (STREAMING_GPU_PERSIST=1) to keep frames on GPU - Add hardware video decoding (NVDEC) support for faster video processing - Add GPU-accelerated primitive libraries: blending_gpu, color_ops_gpu, geometry_gpu - Add streaming_gpu module with GPUFrame class for tracking CPU/GPU data location - Add Dockerfile.gpu for building GPU-enabled worker image Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,16 @@ def upload_to_ipfs(self, local_cid: str, actor_id: str) -> Optional[str]:
|
||||
database.update_cache_item_ipfs_cid(local_cid, ipfs_cid)
|
||||
)
|
||||
|
||||
# Update friendly_names table to use IPFS CID instead of local hash
|
||||
# This ensures assets can be fetched by remote workers via IPFS
|
||||
try:
|
||||
loop.run_until_complete(
|
||||
database.update_friendly_name_cid(actor_id, local_cid, ipfs_cid)
|
||||
)
|
||||
logger.info(f"Friendly name updated: {local_cid[:16]}... -> {ipfs_cid[:16]}...")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update friendly name CID: {e}")
|
||||
|
||||
# Create index from IPFS CID to local cache
|
||||
cache_mgr._set_content_index(ipfs_cid, local_cid)
|
||||
|
||||
|
||||
@@ -83,6 +83,28 @@ def resolve_asset(ref: str, actor_id: Optional[str] = None) -> Optional[Path]:
|
||||
print(f"RESOLVE_ASSET: SUCCESS - resolved to {path}", file=sys.stderr)
|
||||
logger.info(f"Resolved '{ref}' via friendly name to {path}")
|
||||
return path
|
||||
|
||||
# File not in local cache - try fetching from IPFS
|
||||
# The CID from friendly_names is an IPFS CID
|
||||
print(f"RESOLVE_ASSET: file not local, trying IPFS fetch for {cid}", file=sys.stderr)
|
||||
import ipfs_client
|
||||
content = ipfs_client.get_bytes(cid, use_gateway_fallback=True)
|
||||
if content:
|
||||
# Save to local cache
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.sexp') as tmp:
|
||||
tmp.write(content)
|
||||
tmp_path = Path(tmp.name)
|
||||
# Store in cache
|
||||
cached_file, _ = cache_mgr.put(tmp_path, node_type="effect", skip_ipfs=True)
|
||||
# Index by IPFS CID for future lookups
|
||||
cache_mgr._set_content_index(cid, cached_file.cid)
|
||||
print(f"RESOLVE_ASSET: fetched from IPFS and cached at {cached_file.path}", file=sys.stderr)
|
||||
logger.info(f"Fetched '{ref}' from IPFS and cached at {cached_file.path}")
|
||||
return cached_file.path
|
||||
else:
|
||||
print(f"RESOLVE_ASSET: IPFS fetch failed for {cid}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"RESOLVE_ASSET: ERROR - {e}", file=sys.stderr)
|
||||
logger.warning(f"Failed to resolve friendly name '{ref}': {e}")
|
||||
@@ -260,7 +282,8 @@ def run_stream(
|
||||
cache_dir = Path(os.environ.get("CACHE_DIR", "/data/cache"))
|
||||
stream_dir = cache_dir / "streaming" / run_id
|
||||
stream_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = stream_dir / "output.mp4" # Always mp4 for streaming
|
||||
# Use IPFS HLS output for distributed streaming - segments uploaded to IPFS
|
||||
output_path = str(stream_dir) + "/ipfs-hls" # /ipfs-hls suffix triggers IPFS HLS mode
|
||||
|
||||
# Create symlinks to effect directories so relative paths work
|
||||
(work_dir / "sexp_effects").symlink_to(sexp_effects_dir)
|
||||
@@ -320,15 +343,50 @@ def run_stream(
|
||||
|
||||
self.update_state(state='CACHING', meta={'progress': 90})
|
||||
|
||||
# Validate output file (must be > 1KB to have actual frames)
|
||||
if output_path.exists() and output_path.stat().st_size < 1024:
|
||||
raise RuntimeError(f"Output file is too small ({output_path.stat().st_size} bytes) - rendering likely failed")
|
||||
# Get IPFS playlist CID if available (from IPFSHLSOutput)
|
||||
ipfs_playlist_cid = None
|
||||
ipfs_playlist_url = None
|
||||
segment_cids = {}
|
||||
if hasattr(interp, 'output') and hasattr(interp.output, 'playlist_cid'):
|
||||
ipfs_playlist_cid = interp.output.playlist_cid
|
||||
ipfs_playlist_url = interp.output.playlist_url
|
||||
segment_cids = getattr(interp.output, 'segment_cids', {})
|
||||
logger.info(f"IPFS HLS: playlist={ipfs_playlist_cid}, segments={len(segment_cids)}")
|
||||
|
||||
# HLS output creates stream.m3u8 and segment_*.ts files in stream_dir
|
||||
hls_playlist = stream_dir / "stream.m3u8"
|
||||
|
||||
# Validate HLS output (must have playlist and at least one segment)
|
||||
if not hls_playlist.exists():
|
||||
raise RuntimeError("HLS playlist not created - rendering likely failed")
|
||||
|
||||
segments = list(stream_dir.glob("segment_*.ts"))
|
||||
if not segments:
|
||||
raise RuntimeError("No HLS segments created - rendering likely failed")
|
||||
|
||||
logger.info(f"HLS rendering complete: {len(segments)} segments created, IPFS playlist: {ipfs_playlist_cid}")
|
||||
|
||||
# Mux HLS segments into a single MP4 for persistent cache storage
|
||||
final_mp4 = stream_dir / "output.mp4"
|
||||
import subprocess
|
||||
mux_cmd = [
|
||||
"ffmpeg", "-y",
|
||||
"-i", str(hls_playlist),
|
||||
"-c", "copy", # Just copy streams, no re-encoding
|
||||
str(final_mp4)
|
||||
]
|
||||
logger.info(f"Muxing HLS to MP4: {' '.join(mux_cmd)}")
|
||||
result = subprocess.run(mux_cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"HLS mux failed: {result.stderr}")
|
||||
# Fall back to using the first segment for caching
|
||||
final_mp4 = segments[0]
|
||||
|
||||
# Store output in cache
|
||||
if output_path.exists():
|
||||
if final_mp4.exists():
|
||||
cache_mgr = get_cache_manager()
|
||||
cached_file, ipfs_cid = cache_mgr.put(
|
||||
source_path=output_path,
|
||||
source_path=final_mp4,
|
||||
node_type="STREAM_OUTPUT",
|
||||
node_id=f"stream_{task_id}",
|
||||
)
|
||||
@@ -365,6 +423,15 @@ def run_stream(
|
||||
ipfs_cid=ipfs_cid,
|
||||
actor_id=actor_id,
|
||||
))
|
||||
# Register output as video type so frontend displays it correctly
|
||||
_resolve_loop.run_until_complete(database.add_item_type(
|
||||
cid=cached_file.cid,
|
||||
actor_id=actor_id,
|
||||
item_type="video",
|
||||
path=str(cached_file.path),
|
||||
description=f"Stream output from run {run_id}",
|
||||
))
|
||||
logger.info(f"Registered output {cached_file.cid} as video type")
|
||||
# Update pending run status
|
||||
_resolve_loop.run_until_complete(database.update_pending_run_status(
|
||||
run_id=run_id,
|
||||
@@ -381,6 +448,10 @@ def run_stream(
|
||||
"output_cid": cached_file.cid,
|
||||
"ipfs_cid": ipfs_cid,
|
||||
"output_path": str(cached_file.path),
|
||||
# IPFS HLS streaming info
|
||||
"ipfs_playlist_cid": ipfs_playlist_cid,
|
||||
"ipfs_playlist_url": ipfs_playlist_url,
|
||||
"ipfs_segment_count": len(segment_cids),
|
||||
}
|
||||
else:
|
||||
# Update pending run status to failed - reuse module loop
|
||||
|
||||
Reference in New Issue
Block a user