Skip to content

Commit 3987b9d

Browse files
[feat] Align multimodal OpenAI serving APIs (#1781)
1 parent c7da2f5 commit 3987b9d

22 files changed

Lines changed: 1812 additions & 424 deletions

docs/design/inference_schema_parity_inventory.yaml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ surfaces:
2121
hsdp_shard_dim: generator.engine.parallelism.hsdp_shard_dim
2222
dist_timeout: generator.engine.parallelism.dist_timeout
2323
lora_path: generator.pipeline.components.lora_path
24+
lora_nickname: generator.pipeline.components.lora_nickname
25+
lora_strength: generator.pipeline.components.lora_strength
2426
dit_cpu_offload: generator.engine.offload.dit
2527
use_fsdp_inference: generator.engine.use_fsdp_inference
2628
dit_layerwise_offload: generator.engine.offload.dit_layerwise
@@ -72,7 +74,6 @@ surfaces:
7274
compatibility_only:
7375
mode: "Legacy multi-mode FastVideoArgs switch; typed inference config should not expose execution mode."
7476
inference_mode: "Legacy boolean mirror of mode; kept only through adapters while FastVideoArgs remains."
75-
lora_nickname: "Legacy adapter-selection surface pending LoRA API cleanup."
7677
lora_target_modules: "Legacy LoRA configuration surface pending dedicated component API."
7778
output_type: "Legacy output formatting surface pending GenerationResult cleanup."
7879
VSA_sparsity: "Model-specific inference optimization not yet represented in the typed public schema."
@@ -590,27 +591,54 @@ surfaces:
590591
openai_video_request:
591592
kept:
592593
model: "HTTP adapter model-routing field."
594+
user: "OpenAI-compatible caller tracking field."
595+
task: "SGLang-compatible MiniMax-H3 task selector validated against the startup pipeline."
596+
quality: "vLLM-Omni-compatible quality-intent field; model-specific."
597+
lora: "vLLM-Omni-compatible selector for the adapter fixed at server startup."
593598
moved:
594599
prompt: request.prompt
595600
input_reference: request.inputs.image_path
596601
reference_url: request.inputs.image_path
602+
image_reference: request.inputs.image_path,last_image,references
603+
video_reference: request.inputs.video_path,references
604+
audio_reference: request.inputs.references
605+
video_path: request.inputs.video_path
606+
video_url: request.inputs.video_path
607+
video_params: request.sampling.width,height,num_frames,fps
597608
size:
598609
target: request.sampling.width,height
599610
note: "Adapter parses OpenAI size strings as WIDTHxHEIGHT and forwards width then height."
611+
width: request.sampling.width
612+
height: request.sampling.height
600613
fps: request.sampling.fps
601614
num_frames: request.sampling.num_frames
615+
aspect_ratio: request.sampling.width,height
616+
short_edge: request.sampling.width,height
617+
num_outputs_per_prompt: request.sampling.num_videos_per_prompt
618+
n: request.sampling.num_videos_per_prompt
602619
seed: request.sampling.seed
603620
num_inference_steps: request.sampling.num_inference_steps
604621
guidance_scale: request.sampling.guidance_scale
605622
guidance_scale_2: request.sampling.guidance_scale_2
606623
true_cfg_scale: request.sampling.true_cfg_scale
607624
negative_prompt: request.negative_prompt
608625
enable_teacache: request.runtime.enable_teacache
626+
max_sequence_length: request.sampling.max_sequence_length
627+
boundary_ratio: request.sampling.boundary_ratio
628+
extra_params: request.extensions
609629
output_path: request.output.output_path
610630
compatibility_only:
611631
seconds:
612632
target: request.sampling.num_frames
613633
note: "HTTP adapter duration convenience field. If num_frames is omitted, the adapter computes num_frames = fps * seconds."
634+
start_time_seconds: "vLLM-Omni reference-video offset; rejected by pipelines that cannot represent it."
635+
flow_shift: "vLLM-Omni request field; accepted only when the selected model exposes a matching request parameter."
636+
generate_sound: "vLLM-Omni audio-output intent; accepted only by models with a matching request parameter."
637+
sound_duration: "vLLM-Omni audio-duration intent; accepted only by models with a matching request parameter."
638+
enable_frame_interpolation: "vLLM-Omni post-processing field; unavailable until FastVideo exposes a frame-interpolation stage."
639+
frame_interpolation_exp: "vLLM-Omni post-processing field; unavailable until FastVideo exposes a frame-interpolation stage."
640+
frame_interpolation_scale: "vLLM-Omni post-processing field; unavailable until FastVideo exposes a frame-interpolation stage."
641+
frame_interpolation_model_path: "vLLM-Omni post-processing field; unavailable until FastVideo exposes a frame-interpolation stage."
614642

615643
cli:
616644
notes:
Lines changed: 126 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,158 @@
1-
# OpenAI-compatible HTTP Contract
1+
# OpenAI-compatible HTTP contract
22

3-
The stateless FastVideo HTTP server lives at
4-
[`fastvideo/entrypoints/openai/`](https://github.com/hao-ai-lab/FastVideo/tree/main/fastvideo/entrypoints/openai).
5-
Launch: `fastvideo serve --config serve.yaml`.
3+
FastVideo exposes one model-agnostic REST engine for image and video models.
4+
Launch it from a typed serve config:
5+
6+
```bash
7+
fastvideo serve --config examples/serving/openai_fasth3.yaml
8+
```
9+
10+
All generation routes share one serialized engine. FastVideo pipelines mutate
11+
per-request sampling state, and some adapters merge weights at load time, so a
12+
single loaded pipeline is never entered concurrently by image and video
13+
requests. HTTP handling and job polling remain asynchronous.
614

715
## Endpoints
816

917
| Method | Path | Description |
1018
| --- | --- | --- |
11-
| `POST` | `/v1/videos/generations` | Synchronous video generation |
12-
| `GET` | `/v1/videos` | List prior jobs held in the in-memory store |
13-
| `GET` | `/v1/videos/{id}` | Job status / result |
14-
| `GET` | `/v1/videos/{id}/content` | Download the MP4 once ready |
15-
| `POST` | `/v1/images/generations` | Synchronous image generation |
16-
| `GET` | `/v1/models` | Enumerate registered models |
19+
| `GET` | `/v1/models` | List the served model and optional startup adapter |
20+
| `GET` | `/v1/models/{model}` | Retrieve one served model card |
21+
| `POST` | `/v1/videos` | Submit an asynchronous video job |
22+
| `POST` | `/v1/videos/sync` | Generate and return an MP4 response directly |
23+
| `GET` | `/v1/videos` | List in-memory jobs with `after`, `limit`, and `order` |
24+
| `GET` | `/v1/videos/{id}` | Retrieve job status and metadata |
25+
| `GET` | `/v1/videos/{id}/content` | Download a completed MP4 |
26+
| `DELETE` | `/v1/videos/{id}` | Delete a job and its completed artifact |
27+
| `POST` | `/v1/images` | Generate an image |
28+
| `POST` | `/v1/images/edits` | Generate an image from image references |
29+
| `GET` | `/v1/images/{id}/content` | Download a generated image |
1730
| `GET` | `/health` | Liveness probe |
1831

19-
## `VideoGenerationsRequest` shape
32+
`POST /v1/videos/generations` remains an alias for older FastVideo clients.
2033

21-
Mirrors the OpenAI `POST /v1/videos/generations` shape:
34+
## Video requests
35+
36+
The canonical shape follows vLLM-Omni and accepts SGLang's common flat
37+
extensions. Fields that FastVideo cannot represent for the loaded model fail
38+
at admission with HTTP 400 instead of creating a job that later fails.
2239

2340
```json
2441
{
25-
"prompt": "a fox running through snow",
26-
"size": "1024x1536",
27-
"seconds": 5,
28-
"fps": 24,
29-
"num_frames": 121,
42+
"model": "fasth3",
43+
"prompt": "A fox runs through fresh snow.",
44+
"seconds": "5",
45+
"size": "1344x768",
46+
"video_params": {
47+
"fps": 24,
48+
"num_frames": 124
49+
},
3050
"seed": 42,
31-
"num_inference_steps": 8,
51+
"num_inference_steps": 5,
3252
"guidance_scale": 1.0,
33-
"negative_prompt": "blurry, low quality",
34-
"input_reference": "/path/to/init.png"
53+
"image_reference": [
54+
{"image_url": "https://example.com/first-frame.png"}
55+
],
56+
"extra_params": {
57+
"vsa_mode": "exempt"
58+
}
3559
}
3660
```
3761

38-
SGLang-compatible extensions carried today:
39-
`num_inference_steps`, `guidance_scale`, `guidance_scale_2`,
40-
`true_cfg_scale`, `negative_prompt`, `enable_teacache`, `output_path`.
62+
Resolution precedence matches vLLM-Omni:
63+
64+
1. `size`
65+
2. top-level `width` and `height`
66+
3. `video_params.width` and `video_params.height`
67+
68+
Top-level `fps` and `num_frames` similarly take precedence over the nested
69+
block. If `num_frames` is absent, `seconds * fps` is used. FastVideo also keeps
70+
the legacy `input_reference`, `reference_url`, `video_path`, and `video_url`
71+
spellings.
72+
73+
Reference objects support URL or local-path strings through `image_url`,
74+
`video_url`, and `audio_url`. `file_id` references are schema-compatible but
75+
return HTTP 400 because FastVideo does not provide an OpenAI Files store.
76+
Multipart `input_reference` uploads are saved under the configured output
77+
directory.
78+
79+
## Jobs and synchronous responses
80+
81+
An asynchronous submission returns a `video` object in `queued` state. Its
82+
status advances through `in_progress` to `completed` or `failed`. Completed
83+
jobs expose `file_name`, the FastVideo compatibility extension `file_path`,
84+
timings, and peak-memory metadata when the pipeline reports them.
85+
86+
`POST /v1/videos/sync` returns `video/mp4` bytes. It includes
87+
`X-Request-Id`, `X-Model`, `X-Inference-Time-S`, `X-Stage-Durations`, and
88+
`X-Peak-Memory-MB` headers.
89+
90+
FastVideo's synchronous CUDA execution cannot be interrupted after launch.
91+
Deleting an in-progress resource removes it from the API immediately; the
92+
engine remains serialized until the call exits and then removes any artifact.
93+
94+
## Model and LoRA selection
95+
96+
`server.served_model_name` controls the public model id. If omitted, the
97+
checkpoint path is used. Requests that name another model fail with HTTP 400.
98+
99+
LoRAs are configured under
100+
`generator.pipeline.components.{lora_path,lora_nickname,lora_strength}`. The
101+
startup adapter appears in `/v1/models`, and requests can use either its model
102+
nickname or a vLLM-Omni selector:
103+
104+
```json
105+
{
106+
"prompt": "A fox runs through fresh snow.",
107+
"model": "fasth3-dense-datafree",
108+
"lora": {
109+
"name": "fasth3-dense-datafree",
110+
"path": "/models/adapter_model.safetensors",
111+
"scale": 1.0
112+
}
113+
}
114+
```
41115

42-
## Merge precedence
116+
The selector must match the adapter already loaded at startup. FastH3 adapter
117+
files can contain dense replacement tensors and VSA gates in addition to
118+
low-rank factors, so swapping them inside concurrent requests would corrupt
119+
shared pipeline state. A mismatch is rejected with HTTP 400.
43120

44-
The server builds a `GenerationRequest` each call using three layers,
45-
highest first:
121+
## MiniMax-H3 and FastH3
46122

47-
1. **Request body (client-explicit)** — only fields carried in
48-
`request.model_fields_set` (Pydantic v2). Unset fields do not count,
49-
even if the Pydantic model has a schema default for them.
50-
2. **`ServeConfig.default_request` (operator-explicit)** — projected via
51-
[`explicit_request_updates()`](https://github.com/hao-ai-lab/FastVideo/blob/main/fastvideo/api/compat.py);
52-
only fields the operator actually wrote into the YAML count as
53-
defaults. Every other field inherits the schema default rather than
54-
being pinned.
55-
3. **Hardcoded fallback** — e.g. `fps = 24`.
123+
FastH3 uses the same general routes and adapter. `task` is accepted for
124+
SGLang-compatible H3 clients:
56125

57-
The gate matters: both surfaces carry schema defaults. Without
58-
`model_fields_set` / explicit-path tracking, schema defaults would
59-
masquerade as intent and silently shadow the other side.
126+
- `t2va` uses text only.
127+
- `fl2va` takes one or two image references.
128+
- `ref2va` takes ordered image, video, and audio references and requires a
129+
server started with `MiniMaxH3Ref2VAModularPipeline`.
60130

61-
See [`video_api.py::_build_generation_kwargs`](https://github.com/hao-ai-lab/FastVideo/blob/main/fastvideo/entrypoints/openai/video_api.py)
62-
for the canonical implementation; the per-request assembly lives there,
63-
not in pipeline code.
131+
The released FastH3 pipeline generates one packed video/audio result per
132+
request, uses 24 fps, requires guidance scale 1, and accepts frame counts on
133+
its causal-VAE grid. The serving examples pin its five-point distilled sigma
134+
schedule (four DiT forwards).
64135

65-
## Continuation state
136+
## Defaults and errors
66137

67-
The stateless surface accepts an opaque `ContinuationState` round-trip.
68-
Clients that want continuation pass the prior `state` blob back on the
69-
next request, and receive a new one on the response when
70-
`request.output.return_state = true`.
138+
Incoming explicit fields override operator-explicit `default_request` fields,
139+
which override model preset defaults. Pydantic defaults do not masquerade as
140+
client intent; the transport uses `model_fields_set`, while typed config parsing
141+
tracks the exact paths written by the operator.
71142

72-
Shape:
143+
Errors use the OpenAI envelope:
73144

74145
```json
75146
{
76-
"state": {
77-
"kind": "ltx2.v1",
78-
"payload": { "schema_version": 1, "segment_index": 3, ... }
147+
"error": {
148+
"message": "...",
149+
"type": "invalid_request_error",
150+
"param": null,
151+
"code": 400
79152
}
80153
}
81154
```
82155

83-
Payload is always JSON-serializable. Large tensors may live in an
84-
opaque blob-store reference the client simply round-trips; see
85-
[`LTX2ContinuationState`](https://github.com/hao-ai-lab/FastVideo/blob/main/fastvideo/pipelines/basic/ltx2/continuation.py).
86-
87-
Continuation is not yet wired all the way through to
88-
`generator.generate_video(...)` — PR 7.6 (GPU pool upstream) is the
89-
pipeline-level consumer. PR 7 locked the envelope so this surface is
90-
stable ahead of that plumbing.
91-
92-
## Error codes
93-
94-
| HTTP | Condition |
95-
| --- | --- |
96-
| `400 Bad Request` | Parse/validation failure (unknown field, type mismatch, incompatible preset/state) |
97-
| `404 Not Found` | `GET /v1/videos/{id}` for an unknown job |
98-
| `409 Conflict` | Job id already exists |
99-
| `500 Internal Server Error` | Pipeline raised; body mirrors upstream OpenAI error envelope |
100-
| `503 Service Unavailable` | No generator loaded, or shutdown in progress |
101-
102-
Errors include a JSON body with
103-
`{"error": {"type": "...", "message": "..."}}` matching the OpenAI
104-
Python SDK's expectation.
105-
106-
## What does not cross this boundary
107-
108-
* Flat legacy kwargs (`ltx2_refine_enabled`, `torch_compile_kwargs`,
109-
etc.) — these are init-time, configured via `ServeConfig.generator`,
110-
never per-request.
111-
* Private Dreamverse-only fields — those live in a private adapter on
112-
the Dreamverse side; the public FastVideo surface never promises
113-
backward compatibility for them.
114-
* Raw tensor payloads (`ltx2_audio_clean_latent` et al.) — these are
115-
derived by the pipeline from `ContinuationState`, never shipped as
116-
request fields.
156+
Parse, model-selection, startup-LoRA, and unsupported-parameter failures are
157+
HTTP 400; missing resources are HTTP 404; generation failures are stored on
158+
asynchronous jobs and returned as HTTP 500 when that job is retrieved.

examples/serving/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# OpenAI-compatible serving examples
2+
3+
The REST serving engine is model-agnostic. Any model supported by
4+
`VideoGenerator` can use the same `/v1/models`, `/v1/videos`, and `/v1/images`
5+
surface; the two configs here are FastH3 validation profiles.
6+
7+
Launch the full FastH3 checkpoint:
8+
9+
```bash
10+
fastvideo serve --config examples/serving/openai_fasth3.yaml
11+
```
12+
13+
Launch the dense FastH3 LoRA on the base MiniMax-H3 checkpoint:
14+
15+
```bash
16+
adapter_path="$(hf download \
17+
FastVideo/FastVideo-FastH3-4-step-Preview-v1-LoRA \
18+
dense-datafree/adapter_model.safetensors)"
19+
20+
fastvideo serve --config examples/serving/openai_fasth3_lora.yaml \
21+
--generator.pipeline.components.lora_path "$adapter_path"
22+
```
23+
24+
FastH3 adapters are hybrid startup patches: alongside low-rank factors they
25+
may contain dense deltas and a VSA compression-gate replacement. They must be
26+
selected when the server starts. A request may carry the vLLM-Omni `lora`
27+
selector, but its name, path, and scale must match that startup adapter. A VSA
28+
adapter also needs `attention_backend: VIDEO_SPARSE_ATTN_H3`, `VSA_sparsity`,
29+
and `VSA_tile_size` like the full-checkpoint config.
30+
31+
Submit and poll an asynchronous job:
32+
33+
```bash
34+
job_id="$(curl -sS http://localhost:8000/v1/videos \
35+
-H 'content-type: application/json' \
36+
-d '{"model":"fasth3","prompt":"A fox runs through fresh snow."}' \
37+
| jq -r .id)"
38+
39+
curl -sS "http://localhost:8000/v1/videos/$job_id"
40+
curl -o result.mp4 "http://localhost:8000/v1/videos/$job_id/content"
41+
```
42+
43+
For a blocking call, `POST /v1/videos/sync` returns the MP4 body directly.

0 commit comments

Comments
 (0)