Ideogram 4
Ideogram's open release, in the NF4 weights its authors publish for diffusers. Known for typography, so text inside the image comes out readable. Its weights are gated, so this one needs your own Hugging Face token stored as HF_TOKEN.
What runs
This is the whole app. The entry names the model and what serves it, and the build turns that into these files: there is nothing else to write.
"""Ideogram 4, generated from a declarative template."""
from __future__ import annotations
import base64
import io
from pydantic import BaseModel, ConfigDict, Field
from runware_serverless import serve
MODEL = "ideogram-ai/ideogram-4-nf4-diffusers"
REVISION = "1874bc70267ba2c823a7239e1d70dd308c8d64dc"
CACHE = "/weights"
DEFAULT_STEPS = 48
GUIDANCE = None
"""A distilled model is trained with guidance at 1, so the pipeline's own
default would push it somewhere it was never trained to go. None leaves the
pipeline to decide."""
SIZE = {"width": 1024, "height": 1024}
"""Passed on every call because the pipeline's own default is this model's
maximum, and the cost is the square of it."""
class Request(BaseModel):
# An input this model does not read is a line the caller believed was doing
# something, so it is refused rather than dropped. Without this, pydantic
# ignores it: a width sent to a model of fixed size, or a misspelled seed,
# both come back as a result that silently answered a different request.
model_config = ConfigDict(extra="forbid")
prompt: str = Field(description="What to generate.")
seed: int | None = Field(default=None, description="Repeat a result by repeating its seed.")
class Generated(BaseModel):
image: str = Field(description="The result as a base64 PNG.")
steps: int
seed: int | None
@serve
class GeneratedImage:
def load(self) -> None:
if hasattr(self, "pipeline"):
return
# Imported here rather than at the top of the file. The build imports
# this file with the platform's own dependencies, so nothing above
# exists until a worker runs.
import torch
from diffusers import DiffusionPipeline
self.torch = torch
self.pipeline = DiffusionPipeline.from_pretrained(
MODEL,
revision=REVISION,
cache_dir=CACHE,
# Each component lands on the card as it loads, rather than the
# pipeline being assembled on the host and moved. A worker's host RAM
# is half its card's VRAM, so moving a built pipeline has a ceiling
# well below the card's, and the kill arrives as the container's exit
# 137 rather than as a CUDA error. Moving one also refuses weights
# that arrive on the meta device, which a pre-quantized checkpoint's
# do.
device_map="cuda",
)
# The first run compiles kernels and allocates, so no request pays for
# it. It also moves a failure into the readiness check, where it is
# reported, rather than into somebody's first call.
#
# At the entry's own step count rather than at one. A pipeline carrying
# a guidance schedule requires the two to agree, and refuses a single
# step with "guidance_schedule must have length num_inference_steps".
# Warming at the real count also exercises the path a request takes.
self.pipeline(prompt="warmup", num_inference_steps=DEFAULT_STEPS, **SIZE)
def predict(self, request: Request) -> Generated:
generator = None
if request.seed is not None:
generator = self.torch.Generator(device="cuda").manual_seed(request.seed)
extra = {} if GUIDANCE is None else {"guidance_scale": GUIDANCE}
image = self.pipeline(
prompt=request.prompt,
num_inference_steps=DEFAULT_STEPS,
generator=generator,
**SIZE,
**extra,
).images[0]
buffer = io.BytesIO()
image.save(buffer, format="PNG")
return Generated(
image=base64.b64encode(buffer.getvalue()).decode("ascii"),
steps=DEFAULT_STEPS,
seed=request.seed,
)configVersion: 1
id: ideogram-4
name: Ideogram 4
source:
type: code
entry: model.py
compute:
gpuType: l40s
build:
requirements:
- torch==2.14.1
- torchvision==0.29.1
- diffusers==0.42.1
- transformers==5.21.0
- accelerate==1.15.0
- bitsandbytes==0.50.2
baseImage: python:3.12
volumes:
- /weights
secrets:
- name: HF_TOKENDeploying
Deploy it as it is, or take the files above and deploy your own copy. Nothing you change reaches back to this page.
runware serverless deploy --from https://runware.ai/docs/serverless/examples/ideogram-4.ziprunware serverless deploy model.py \
--id ideogram-4 \
--gpu-type l40s \
--base-image python:3.12 \
--requirement torch==2.14.1 \
--requirement torchvision==0.29.1 \
--requirement diffusers==0.42.1 \
--requirement transformers==5.21.0 \
--requirement accelerate==1.15.0 \
--requirement bitsandbytes==0.50.2 \
--volume /weights \
--secret HF_TOKENCalling it
One request that works, against the app you just deployed. The fields are the ones the entry declares above, and anything it does not declare is refused rather than ignored.
import { createClient } from '@runware/sdk'
const client = await createClient({ apiKey: process.env.RUNWARE_API_KEY })
const task = await client.invoke({
appId: 'ideogram-4',
endpointPath: 'predict',
payload: {
request: {
prompt: 'A paper boat drifting down a rain gutter, close up'
}
}
})import asyncio
import os
from runware import Runware
async def main():
client = Runware(api_key=os.environ["RUNWARE_API_KEY"])
try:
task = await client.invoke(
"ideogram-4",
"predict",
{
"request": {
"prompt": "A paper boat drifting down a rain gutter, close up"
}
},
)
finally:
await client.close()
asyncio.run(main())curl https://api.serverless.runware.ai/v1/apps/ideogram-4/invoke-async/predict \
-H "Authorization: Bearer $RUNWARE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payload": {
"request": {
"prompt": "A paper boat drifting down a rain gutter, close up"
}
}
}'cat <<'EOF' | runware serverless apps invoke ideogram-4 predict -f -
{
"request": {
"prompt": "A paper boat drifting down a rain gutter, close up"
}
}
EOF{
"taskId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payload": {
"request": {
"prompt": "A paper boat drifting down a rain gutter, close up"
}
}
}What it costs
This entry is pinned to l40s, which is the card it was tested on rather than the only one it runs on: --gpu-typedeploys it somewhere else. You pay for that card by the second while a worker holds it, and for nothing else. Pricing covers what meters, and the rates are published per GPU-second.