OppaAI commited on
Commit
e5b79b3
·
1 Parent(s): ef68517

feat: add FLUX.2 Klein image generation service and enable flash-attention for MioTTS

Browse files
Files changed (3) hide show
  1. backend/flux-klein.py +175 -0
  2. backend/miotts.py +2 -2
  3. test.png +0 -0
backend/flux-klein.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ aiko_imagegen.py — FLUX.2 [klein] 9B image generation endpoint for Aiko
3
+ Modal app: oppa-ai-org--aiko-imagegen
4
+
5
+ Requires Modal secrets:
6
+ - huggingface-secret (HF_TOKEN) — needed for gated 9B weights
7
+ """
8
+
9
+ import io
10
+ import base64
11
+ import modal
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # image — bake diffusers + torch into the container
15
+ # ---------------------------------------------------------------------------
16
+ image = (
17
+ modal.Image.debian_slim(python_version="3.11")
18
+ .apt_install("git")
19
+ .pip_install(
20
+ "torch==2.6.0",
21
+ "torchvision",
22
+ extra_index_url="https://download.pytorch.org/whl/cu124",
23
+ )
24
+ .pip_install(
25
+ "git+https://github.com/huggingface/diffusers.git",
26
+ "transformers",
27
+ "accelerate",
28
+ "huggingface_hub",
29
+ "sentencepiece",
30
+ "Pillow",
31
+ "fastapi[standard]",
32
+ )
33
+ .env({
34
+ # disable flash-attn-3 custom op registration that breaks on torch 2.6
35
+ "DIFFUSERS_NO_FLASH_ATTN": "1",
36
+ })
37
+ )
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # volume — cache weights so cold starts don't re-download 18GB every time
41
+ # ---------------------------------------------------------------------------
42
+ volume = modal.Volume.from_name("aiko-imagegen-weights", create_if_missing=True)
43
+ WEIGHTS_DIR = "/weights"
44
+ MODEL_ID = "black-forest-labs/FLUX.2-klein-9B"
45
+
46
+ app = modal.App("aiko-imagegen", image=image)
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # model class — loaded once per container, stays warm between requests
50
+ # ---------------------------------------------------------------------------
51
+ @app.cls(
52
+ gpu="H100",
53
+ secrets=[modal.Secret.from_name("huggingface-secret")],
54
+ volumes={WEIGHTS_DIR: volume},
55
+ timeout=120,
56
+ scaledown_window=300,
57
+ )
58
+ @modal.concurrent(max_inputs=1)
59
+ class AikoImageGen:
60
+
61
+ @modal.enter()
62
+ def load(self):
63
+ import os
64
+ import torch
65
+ from diffusers import Flux2KleinPipeline
66
+ from huggingface_hub import snapshot_download
67
+
68
+ hf_token = os.environ["HF_TOKEN"]
69
+ local_path = f"{WEIGHTS_DIR}/flux2-klein-9b"
70
+
71
+ # download once into the volume, reuse on warm starts
72
+ if not os.path.exists(local_path):
73
+ print("Downloading FLUX.2 klein 9B weights...")
74
+ snapshot_download(
75
+ MODEL_ID,
76
+ local_dir=local_path,
77
+ token=hf_token,
78
+ ignore_patterns=["*.msgpack", "*.h5"],
79
+ )
80
+ volume.commit()
81
+ else:
82
+ print("Weights already cached, loading from volume...")
83
+
84
+ self.pipe = Flux2KleinPipeline.from_pretrained(
85
+ local_path,
86
+ torch_dtype=torch.bfloat16,
87
+ ).to("cuda")
88
+
89
+ print("FLUX.2 klein 9B ready.")
90
+
91
+ @modal.method()
92
+ def generate(
93
+ self,
94
+ prompt: str,
95
+ width: int = 1024,
96
+ height: int = 1024,
97
+ steps: int = 4,
98
+ guidance_scale: float = 1.0,
99
+ seed: int = -1,
100
+ ) -> str:
101
+ """Generate image, return base64-encoded PNG string."""
102
+ import torch
103
+
104
+ generator = None
105
+ if seed >= 0:
106
+ generator = torch.Generator(device="cuda").manual_seed(seed)
107
+
108
+ result = self.pipe(
109
+ prompt=prompt,
110
+ width=width,
111
+ height=height,
112
+ num_inference_steps=steps,
113
+ guidance_scale=guidance_scale,
114
+ generator=generator,
115
+ )
116
+
117
+ image = result.images[0]
118
+
119
+ # encode to base64 PNG for easy HTTP transport
120
+ buf = io.BytesIO()
121
+ image.save(buf, format="PNG")
122
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # FastAPI wrapper — matches the pattern of your existing Aiko endpoints
127
+ # ---------------------------------------------------------------------------
128
+ from fastapi import FastAPI, HTTPException
129
+ from pydantic import BaseModel
130
+
131
+ web_app = FastAPI()
132
+
133
+
134
+ class GenerateRequest(BaseModel):
135
+ prompt: str
136
+ width: int = 1024
137
+ height: int = 1024
138
+ steps: int = 4
139
+ guidance_scale: float = 1.0
140
+ seed: int = -1
141
+
142
+
143
+ class GenerateResponse(BaseModel):
144
+ image_b64: str
145
+ prompt: str
146
+
147
+
148
+ @app.function(
149
+ image=image,
150
+ secrets=[modal.Secret.from_name("huggingface-secret")],
151
+ )
152
+ @modal.asgi_app()
153
+ def fastapi_app():
154
+ model = AikoImageGen()
155
+
156
+ @web_app.post("/generate", response_model=GenerateResponse)
157
+ async def generate(req: GenerateRequest):
158
+ if not req.prompt.strip():
159
+ raise HTTPException(status_code=400, detail="prompt is required")
160
+
161
+ image_b64 = await model.generate.remote.aio(
162
+ prompt=req.prompt,
163
+ width=req.width,
164
+ height=req.height,
165
+ steps=req.steps,
166
+ guidance_scale=req.guidance_scale,
167
+ seed=req.seed,
168
+ )
169
+ return GenerateResponse(image_b64=image_b64, prompt=req.prompt)
170
+
171
+ @web_app.get("/health")
172
+ async def health():
173
+ return {"status": "ok", "model": "FLUX.2-klein-9B"}
174
+
175
+ return web_app
backend/miotts.py CHANGED
@@ -49,7 +49,7 @@ MODELS_DIR = Path("/models")
49
  # ---------------------------------------------------------------------------
50
  # Container image
51
  # ---------------------------------------------------------------------------
52
- cuda_tag = "12.4.0-runtime-ubuntu22.04"
53
 
54
  image = (
55
  modal.Image.from_registry(f"nvidia/cuda:{cuda_tag}", add_python="3.11")
@@ -82,7 +82,7 @@ image = (
82
  "git clone https://github.com/Aratako/MioTTS-Inference.git /opt/miotts",
83
  "cd /opt/miotts && /root/.local/bin/uv sync",
84
  # flash-attn is recommended but slow to build; skip for now, add if needed:
85
- # "cd /opt/miotts && MAX_JOBS=4 /root/.local/bin/uv pip install --no-build-isolation flash-attn",
86
  )
87
  .pip_install("huggingface_hub")
88
  )
 
49
  # ---------------------------------------------------------------------------
50
  # Container image
51
  # ---------------------------------------------------------------------------
52
+ cuda_tag = "12.4.0-devel-ubuntu22.04"
53
 
54
  image = (
55
  modal.Image.from_registry(f"nvidia/cuda:{cuda_tag}", add_python="3.11")
 
82
  "git clone https://github.com/Aratako/MioTTS-Inference.git /opt/miotts",
83
  "cd /opt/miotts && /root/.local/bin/uv sync",
84
  # flash-attn is recommended but slow to build; skip for now, add if needed:
85
+ "cd /opt/miotts && MAX_JOBS=4 /root/.local/bin/uv pip install --no-build-isolation flash-attn",
86
  )
87
  .pip_install("huggingface_hub")
88
  )
test.png ADDED