tugrulv89's picture
Match research worker to Git 4ba59dcc3
df262ae verified
Raw History Blame Contribute Delete
5.39 kB
"""Isolated Qwen Image 2.1 research worker; the 2511 handler stays unchanged."""
import io
import json
import os
import time
import traceback
import uuid
from pathlib import Path
os.environ['HF_HUB_CACHE'] = '/runpod-volume/huggingface-cache/hub'
os.environ['HF_HUB_OFFLINE'] = '1'
os.environ['TRANSFORMERS_OFFLINE'] = '1'
import requests
from PIL import Image
_pipe = None
_bucket = None
def sampling(job):
allowed = {'job_id', 'image_url', 'image_urls', 'prompt', 'negative_prompt',
'seed', 'size', 'num_inference_steps', 'true_cfg_scale'}
if set(job) - allowed:
raise ValueError('Unsupported job fields: ' + ', '.join(sorted(set(job) - allowed)))
width, height = map(int, job.get('size', '1024*1536').split('*'))
steps = int(job.get('num_inference_steps', 40))
cfg = float(job.get('true_cfg_scale', 1.0))
if not (64 <= width <= 4096 and 64 <= height <= 4096 and
width % 16 == height % 16 == 0 and 1 <= steps <= 60 and 1 <= cfg <= 5):
raise ValueError('Invalid resolution or sampling parameters')
if not job.get('prompt', '').strip():
raise ValueError('An explicit editing prompt is required')
return {'prompt': job['prompt'], 'negative_prompt': job.get('negative_prompt'),
'width': width, 'height': height, 'num_inference_steps': steps,
'true_cfg_scale': cfg, 'use_kv_cache': True,
'output_resolution': 1024}
def cached_model_path():
folder = Path(os.environ['HF_HUB_CACHE']) / (
'models--' + os.environ['QWEN21_MODEL_REPO'].replace('/', '--')) / (
'snapshots/' + os.environ['QWEN21_MODEL_REVISION'])
if not (folder / 'model_index.json').is_file():
raise FileNotFoundError('Pinned model is missing from RunPod model cache: ' + str(folder))
return str(folder)
def pipeline():
global _pipe
if _pipe is None:
import torch
from diffusers import QwenImage21Pipeline
_pipe = QwenImage21Pipeline.from_pretrained(
cached_model_path(), local_files_only=True,
torch_dtype=torch.bfloat16)
_pipe.enable_model_cpu_offload()
return _pipe
def upload(photo):
global _bucket
if _bucket is None:
from google.cloud import storage
from google.oauth2 import service_account
raw = os.environ['JSON_PATH_KEY']
try:
credentials = service_account.Credentials.from_service_account_info(json.loads(raw))
except json.JSONDecodeError:
credentials = service_account.Credentials.from_service_account_file(raw)
_bucket = storage.Client(credentials=credentials).bucket(os.environ.get('GCS_BUCKET_NAME', 'shoupapp'))
buf = io.BytesIO()
photo.save(buf, 'PNG')
blob = _bucket.blob('research/qwen21/' + uuid.uuid4().hex + '.png')
blob.upload_from_string(buf.getvalue(), content_type='image/png')
return blob.public_url
def handler(event):
data = event['input']
if data.get('task') != 'edit' or not 1 <= len(data.get('jobs', [])) <= 4:
raise ValueError('Research worker accepts one to four explicit edit jobs')
results = []
for job in data['jobs']:
try:
args = sampling(job)
urls = job.get('image_urls') or [job.get('image_url')]
if not 1 <= len(urls) <= 10 or not all(urls):
raise ValueError('One to ten source image URLs are required')
images = []
for url in urls:
response = requests.get(url, timeout=60)
response.raise_for_status()
images.append(Image.open(io.BytesIO(response.content)).convert('RGBA'))
import torch
pipe = pipeline()
torch.cuda.synchronize()
torch.cuda.reset_peak_memory_stats()
started = time.monotonic()
photo = pipe(image=images, generator=torch.Generator('cuda').manual_seed(
int(job.get('seed', 20261040))), **args).images[0]
torch.cuda.synchronize()
inference_ms = round((time.monotonic() - started) * 1000)
results.append({'job_id': job.get('job_id'), 'ok': True,
'output_url': upload(photo.convert('RGB')),
'native_alpha_range': photo.getchannel('A').getextrema() if photo.mode == 'RGBA' else None,
'inference_ms': inference_ms,
'denoising_steps': len(pipe.scheduler.timesteps),
'gpu': torch.cuda.get_device_name(),
'peak_reserved_gib': round(torch.cuda.max_memory_reserved() / 1024**3, 2),
'model_revision': os.environ['QWEN21_MODEL_REVISION']})
except Exception as exc:
detail = traceback.format_exc()
for key in ('HF_TOKEN', 'JSON_PATH_KEY'):
if os.environ.get(key):
detail = detail.replace(os.environ[key], '[REDACTED]')
print('Research job failed:', type(exc).__name__, detail, flush=True)
results.append({'job_id': job.get('job_id'), 'ok': False,
'error': type(exc).__name__ + ': edit failed; inspect private worker logs'})
return {'results': results}
if __name__ == '__main__':
import runpod
runpod.serverless.start({'handler': handler})