Instructions to use XXXpilar/Fastfill-test with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use XXXpilar/Fastfill-test with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 11,625 Bytes
e6f80f3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | """OpenAI-compatible endpoint that speaks the WorldEdge request (scope doc Appendix B, fields in interface.py).
python -m fastfill.serve --model outputs/ff-v1-merged --port 8001
# EmbodiedGen side: WORLDEDGE_FASTFILL_URL=http://<host>:8001/v1 WORLDEDGE_FASTFILL_MODEL=ff-v1-merged
python -m fastfill.serve # self-check (no model)
The last user message is the request JSON (the body's "model" field is not read). The reply's content is
placements_from_text's JSON ({"placements": [{"id", "position_m", "yaw_rad", "support_parent", "support_surface"?}], "raw_placements": [...],
"validation": {"raw", "repaired"}, "error": null, "unsupported": [...]}) plus "model" (--name, default the checkpoint
directory's name) and "checkpoint" (its absolute path): EmbodiedGen caches that content under its own
WORLDEDGE_FASTFILL_MODEL, so set that to the served name, and a cached layout still says which checkpoint made it.
A request with nothing to place (no objects, or all anchored to a wall or ceiling) is answered 200 with no placements
and its unsupported list, without calling the model (training never has an empty room).
HTTP 422 and no layout when the request is rejected (a prompt of --max_len tokens or more included: prompt_too_long),
the answer does not parse (a missing or extra object included), or the repaired layout still fails the canonical
checks: FastFill places every object validly or fails back to the caller, it never drops one. Every error body carries
model and checkpoint; a 422 after generation also the model's text (raw_text) and the unsupported list.
Greedy decoding, one request at a time.
The JSON body requires a positive Content-Length of at most MAX_REQUEST_BYTES (1 MiB). Missing/invalid lengths
and malformed or excessively nested JSON receive 400; oversized bodies receive 413 before any body read.
"""
import argparse
import json
import os
import sys
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from fastfill.interface import placements_from_text, request_to_room
from fastfill.scene import messages, prompt_text
MAX_REQUEST_BYTES = 1024 * 1024
class PromptTooLong(ValueError):
"""Raised by generate() for a prompt of max_len tokens or more (vLLM cannot start on it): answered with 422."""
def answer(req, generate):
"""Request dict -> (HTTP status, body). generate(model messages) -> model text, or raises PromptTooLong."""
try:
room, ctx = request_to_room(req)
except ValueError as e:
return 422, {"error": {"message": str(e), "type": "invalid_request_error"}}
if not room["objects"]: # the empty answer is the only valid one: not generated
return 200, placements_from_text('{"placements":[]}', room, ctx)
try:
text = generate(messages(room, room["constraints"], with_target=False))
except PromptTooLong as e:
return 422, {"error": {"message": str(e), "type": "invalid_request_error", "unsupported": ctx["unsupported"]}}
out = placements_from_text(text, room, ctx)
if out["error"]:
return 422, {"error": {"message": f"model answer rejected: {out['error']}", "type": "invalid_layout",
"raw_text": text, "unsupported": out["unsupported"]}}
if not out["validation"]["repaired"]["ok"]: # every object must be placed validly; else the caller decides
return 422, {"error": {"message": "layout fails validation even after repair", "type": "invalid_layout",
"raw_placements": out["raw_placements"], "validation": out["validation"],
"raw_text": text, "unsupported": out["unsupported"]}}
return 200, out
def make_handler(generate, name, checkpoint):
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
if not self.path.endswith("/chat/completions"):
return self.reply(404, {"error": {"message": "POST .../v1/chat/completions"}})
try:
length = int(self.headers["Content-Length"])
if length <= 0:
raise ValueError("Content-Length must be positive")
if length > MAX_REQUEST_BYTES:
return self.reply(413, {"error": {"message": f"body exceeds {MAX_REQUEST_BYTES} bytes",
"type": "invalid_request_error"}})
body = json.loads(self.rfile.read(length))
req = json.loads(body["messages"][-1]["content"])
except (ValueError, KeyError, IndexError, TypeError, RecursionError) as e:
return self.reply(400, {"error": {"message": f"bad body: {e!r}", "type": "invalid_request_error"}})
try:
status, out = answer(req, generate)
except Exception as e: # a bug or a dead model: say so instead of dropping the connection
return self.reply(500, {"error": {"message": repr(e), "type": "server_error"}})
if status == 200:
content = json.dumps({**out, "model": name, "checkpoint": checkpoint})
out = {"id": f"fastfill-{time.time_ns()}", "object": "chat.completion", "created": int(time.time()),
"model": name, "choices": [{"index": 0, "finish_reason": "stop",
"message": {"role": "assistant", "content": content}}],
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}}
self.reply(status, out)
def reply(self, status, obj):
if status != 200:
obj["error"].update(model=name, checkpoint=checkpoint)
data = json.dumps(obj).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return Handler
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--model", required=True, help="merged checkpoint dir")
ap.add_argument("--name", help="model name echoed in replies (default: the checkpoint directory's name)")
ap.add_argument("--host", default="0.0.0.0")
ap.add_argument("--port", type=int, default=8001)
ap.add_argument("--max_len", type=int, default=40960, help="model sequence limit; the answer may use what the prompt leaves")
a = ap.parse_args()
from vllm import LLM, SamplingParams
llm = LLM(model=a.model, max_model_len=a.max_len)
tok = llm.get_tokenizer()
def generate(msgs):
p = prompt_text(tok, msgs)
n = len(tok(p)["input_ids"])
if n >= a.max_len: # vLLM raises on such a prompt: refuse it as a bad request instead
raise PromptTooLong(f"prompt_too_long: {n} tokens, limit {a.max_len}")
sp = SamplingParams(temperature=0, max_tokens=a.max_len - n)
return llm.generate([p], sp, use_tqdm=False)[0].outputs[0].text
ckpt = os.path.abspath(a.model) if os.path.isdir(a.model) else a.model # a dir, or a hub id as given
name = a.name or os.path.basename(os.path.normpath(ckpt))
print(f"FastFill endpoint: http://{a.host}:{a.port}/v1 model {name} = {ckpt}", flush=True)
HTTPServer((a.host, a.port), make_handler(generate, name, ckpt)).serve_forever()
if __name__ == "__main__":
if sys.argv[1:]:
main()
else: # self-check: answer() and the HTTP wrapper, a stub model
import threading
import urllib.error
import urllib.request
calls = []
def stub(text):
def gen(msgs):
calls.append(json.loads(msgs[1]["content"]))
if text is None:
raise PromptTooLong("prompt_too_long: 50000 tokens, limit 40960")
return text
return gen
req = {"room": {"boundary_xy": [[0, 0], [4, 0], [4, 4], [0, 4]], "height_m": 2.8,
"fixed_geometry": [{"id": "col_1", "category": "column", "size_xyz_m": [0.3, 0.3, 2.8],
"position_m": [3.5, 3.5, 0]}]},
"objects_to_place": [{"id": "table_1", "category": "table", "size_xyz_m": [1.4, 0.8, 0.75]},
{"id": "cup_1", "category": "cup", "size_xyz_m": [0.08, 0.08, 0.1]},
{"id": "clock_1", "category": "clock", "size_xyz_m": [0.3, 0.05, 0.3], "anchor": "wall"}]}
good = '{"placements":[{"id":"table_1","pos":[2,2,0],"yaw":0},{"id":"cup_1","on":"table_1","pos":[2,2,0.75],"yaw":0}]}'
clock = [{"id": "clock_1", "reason": "unsupported_anchor"}]
st, out = answer(req, stub(good))
assert st == 200 and len(calls) == 1 and out["validation"]["repaired"]["ok"] and out["unsupported"] == clock, out
# nothing placeable: no objects, or only wall / ceiling items -> 200, empty placements, the model never called
for objs, unsup in (([], []), (req["objects_to_place"][2:], clock)):
st, out = answer({**req, "objects_to_place": objs}, stub("never"))
assert st == 200 and len(calls) == 1 and out["placements"] == [] and out["unsupported"] == unsup, out
assert out["error"] is None and out["validation"]["repaired"]["ok"], out
# 422 after generation keeps the model's text and the unsupported list: bad JSON, a missing object, a layout
# that fails after repair (table through the column), a prompt over the limit (no text)
on_col = good.replace("[2,2,0]", "[3.4,3.4,0]").replace("[2,2,0.75]", "[3.4,3.4,0.75]")
for text, kind in (("{not json", "invalid_layout"), (good.split(',{"id":"cup_1"')[0] + "]}", "invalid_layout"),
(on_col, "invalid_layout"), (None, "invalid_request_error")):
st, out = answer(req, stub(text))
e = out["error"]
assert st == 422 and e["type"] == kind and e["unsupported"] == clock and e.get("raw_text") == text, out
assert out["error"]["message"].startswith("prompt_too_long")
assert answer(req, stub(on_col))[1]["error"]["validation"]["repaired"]["fixed_blocking"] == [["table_1", "col_1"]]
# over HTTP: every reply names the served model and checkpoint (200 in the content EmbodiedGen caches)
texts = [good, "{not json"]
srv = HTTPServer(("127.0.0.1", 0), make_handler(lambda m: texts.pop(0), "ff-test", "/ckpt/ff-test"))
threading.Thread(target=srv.serve_forever, daemon=True).start()
def post(obj):
body = json.dumps({"model": "fastfill", "messages": [{"role": "user", "content": json.dumps(obj)}]}).encode()
try:
with urllib.request.urlopen(f"http://127.0.0.1:{srv.server_port}/v1/chat/completions", body) as f:
return f.status, json.load(f)
except urllib.error.HTTPError as f:
return f.code, json.load(f)
st, rep = post(req)
content = json.loads(rep["choices"][0]["message"]["content"])
assert st == 200 and rep["model"] == "ff-test" and (content["model"], content["checkpoint"]) == ("ff-test", "/ckpt/ff-test")
st, rep = post(req)
assert st == 422 and rep["error"]["model"] == "ff-test" and rep["error"]["raw_text"] == "{not json", rep
assert post({"room": {}})[0] == 422 and post({"room": {}})[1]["error"]["checkpoint"] == "/ckpt/ff-test"
srv.shutdown()
print("serve.py self-check ok")
|