polats Claude Opus 4.8 (1M context) commited on
Commit
c377173
·
1 Parent(s): 366b4b1

Add local GPU sidecars for tiny-aya + klein (off-ZeroGPU)

Browse files

Run the polats/tiny-army-{tiny-aya,klein}-zerogpu Spaces locally on the
3090 so the main app doesn't burn HF ZeroGPU quota. Each mirrors its
hosted Space's Gradio API contract, so the main app's gradio_client talks
to them unchanged — only the .env TINY_{AYA,KLEIN}_SPACE vars switch from
Space IDs to local URLs (.env is gitignored, so deploys are unaffected).

- spaces/tiny-aya-zerogpu: spaces.GPU -> no-op shim, 4-bit NF4 load, VRAM
cap, .env/token, host/port launch block.
- spaces/klein-zerogpu: sequential CPU offload (fits the desktop-shared
card; 4-bit OOMs at load-time warmup), weiner LoRA pre-applied + default.
- run_sidecars.sh launcher (start/stop/aya/klein; binds 0.0.0.0 for LAN).
- docs/local-sidecars.md.

This is a local-dev branch; it is not the deployed Space branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

docs/local-sidecars.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Local model sidecars (no HuggingFace quota)
2
+
3
+ Two of the hosted ZeroGPU Spaces now run **locally on the 3090** so the main app doesn't burn
4
+ HF quota. They mirror each hosted Space's Gradio API exactly, so the main app's `gradio_client`
5
+ talks to them unchanged — only the `*_SPACE` env var changes from a Space ID to a local URL.
6
+
7
+ | Sidecar | Source dir | Port | Model | `.env` var |
8
+ |------------|-----------------------------|------|--------------------------------|-------------------|
9
+ | Tiny Aya | `spaces/tiny-aya-zerogpu/` | 7864 | `CohereLabs/tiny-aya-global` | `TINY_AYA_SPACE` |
10
+ | Klein | `spaces/klein-zerogpu/` | 7865 | `black-forest-labs/FLUX.2-klein-4B` + `polats/weiner-klein-lora` | `TINY_KLEIN_SPACE` |
11
+
12
+ Mirrors of `polats/tiny-army-tiny-aya-zerogpu` and `polats/tiny-army-klein-zerogpu`, adapted for
13
+ local hardware (no ZeroGPU). The hosted `app.py`s are unchanged in spirit; the local diffs are
14
+ documented in each file's header.
15
+
16
+ ## Run
17
+
18
+ ```bash
19
+ ./run_sidecars.sh # start both, wait until ready (logs in logs/)
20
+ ./run_sidecars.sh aya # just tiny-aya
21
+ ./run_sidecars.sh klein # just klein
22
+ ./run_sidecars.sh stop # stop both
23
+ ```
24
+
25
+ `.env` already points `TINY_AYA_SPACE` / `TINY_KLEIN_SPACE` at the local URLs.
26
+
27
+ ### Network access
28
+
29
+ The sidecars bind to `0.0.0.0` by default, so they're reachable from the **local network** at this
30
+ host's LAN IP (currently `10.0.0.36`, also the tailnet `100.88.3.43`):
31
+
32
+ - Tiny Aya: `http://10.0.0.36:7864`
33
+ - Klein: `http://10.0.0.36:7865`
34
+
35
+ The main app talks to them over loopback (`127.0.0.1`) for speed regardless. Set `BIND=127.0.0.1`
36
+ before `./run_sidecars.sh` to restrict them to this machine only. (They are NOT on the public
37
+ cloudflared/nginx path that exposes kimodo — LAN/tailnet only.) To go back to the
38
+ hosted ZeroGPU Spaces, comment those two lines back to the `polats/...` Space IDs.
39
+
40
+ ## Why these settings (shared-GPU constraints)
41
+
42
+ The 3090 also drives the desktop **and** is shared with a separate ~15 GB text-encoder server
43
+ (`kimodo`), leaving only ~5 GB free. So:
44
+
45
+ - **Tiny Aya** loads in **4-bit NF4** (~7 GB → ~3.5 GB resident). `TINY_AYA_QUANT=bf16` for full
46
+ precision when the card is free.
47
+ - **Klein** loads in **bf16 with `enable_sequential_cpu_offload()`** — the pipeline stays on CPU
48
+ and streams to the GPU submodule-by-submodule, so **peak VRAM is only ~2.5 GB** and it never
49
+ contends with the shared card at load time. (4-bit quant was tried but its load-time GPU warmup
50
+ allocation OOMs against the tight free headroom; sequential offload sidesteps that and keeps full
51
+ bf16 quality.) ~11 s/image at 1024². `TINY_KLEIN_OFFLOAD=model` uses the faster
52
+ whole-component offload (~8 GB peak) when the card is free.
53
+ - The **weiner LoRA** is pre-applied at startup and is the default for the `lora` input, so the
54
+ main app's `/generate(prompt, seed)` calls (which don't pass a LoRA) get weiner portraits.
55
+ Other checkpoints: `weiner750`, `weiner500`, `weiner250`.
56
+
57
+ ## Verify
58
+
59
+ ```bash
60
+ # both endpoints, via the main app (which must be running on :7860)
61
+ curl -s -X POST localhost:7860/portrait -H 'content-type: application/json' \
62
+ -d '{"prompt":"portrait of a knight","engine":"klein"}' -o /tmp/p.webp
63
+ curl -sN -X POST localhost:7860/text/generate/stream -H 'content-type: application/json' \
64
+ -d '{"model":"tiny-aya-global-zerogpu","user":"hi","max_tokens":20}'
65
+ ```
run_sidecars.sh ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Run the Tiny Army model sidecars LOCALLY on the 3090 instead of on ZeroGPU Spaces,
3
+ # so the main app doesn't burn HuggingFace quota.
4
+ #
5
+ # These mirror the hosted Spaces' Gradio API exactly, so the main app talks to them via
6
+ # gradio_client unchanged — just point TINY_AYA_SPACE / TINY_KLEIN_SPACE at the local URLs
7
+ # (already done in .env):
8
+ # TINY_AYA_SPACE=http://127.0.0.1:7864
9
+ # TINY_KLEIN_SPACE=http://127.0.0.1:7865
10
+ #
11
+ # Usage:
12
+ # ./run_sidecars.sh # start both, stream logs to logs/, wait until ready
13
+ # ./run_sidecars.sh aya # just tiny-aya
14
+ # ./run_sidecars.sh klein # just klein
15
+ # ./run_sidecars.sh stop # stop both
16
+ set -euo pipefail
17
+ cd "$(dirname "$0")"
18
+
19
+ PY="${PY:-.venv/bin/python}"
20
+ AYA_PORT="${AYA_PORT:-7864}"
21
+ KLEIN_PORT="${KLEIN_PORT:-7865}"
22
+ # Bind address. 0.0.0.0 = reachable from the LAN (other machines on the network) at this host's
23
+ # IP:PORT. Set BIND=127.0.0.1 to restrict to localhost (main app on the same box only).
24
+ BIND="${BIND:-0.0.0.0}"
25
+ mkdir -p logs
26
+
27
+ start_one() {
28
+ local name="$1" dir="$2" port="$3"
29
+ if [ -f "logs/$name.pid" ] && kill -0 "$(cat "logs/$name.pid")" 2>/dev/null; then
30
+ echo "[$name] already running (pid $(cat "logs/$name.pid")) on :$port"
31
+ return
32
+ fi
33
+ echo "[$name] starting on $BIND:$port (logs/$name.log)"
34
+ PORT="$port" GRADIO_SERVER_NAME="$BIND" nohup "$PY" "spaces/$dir/app.py" \
35
+ > "logs/$name.log" 2>&1 &
36
+ echo $! > "logs/$name.pid"
37
+ }
38
+
39
+ wait_ready() {
40
+ local name="$1" port="$2"
41
+ echo -n "[$name] loading model"
42
+ for _ in $(seq 1 600); do # up to ~10 min (cold model download/load)
43
+ if curl -fsS "http://127.0.0.1:$port/config" >/dev/null 2>&1; then
44
+ echo " — ready at http://127.0.0.1:$port"
45
+ return 0
46
+ fi
47
+ if [ -f "logs/$name.pid" ] && ! kill -0 "$(cat "logs/$name.pid")" 2>/dev/null; then
48
+ echo " — DIED, tail of logs/$name.log:"; tail -n 30 "logs/$name.log"; return 1
49
+ fi
50
+ echo -n "."; sleep 1
51
+ done
52
+ echo " — timed out"; return 1
53
+ }
54
+
55
+ stop_one() {
56
+ local name="$1"
57
+ if [ -f "logs/$name.pid" ]; then
58
+ kill "$(cat "logs/$name.pid")" 2>/dev/null || true
59
+ rm -f "logs/$name.pid"
60
+ echo "[$name] stopped"
61
+ fi
62
+ }
63
+
64
+ case "${1:-all}" in
65
+ stop) stop_one aya; stop_one klein ;;
66
+ aya) start_one aya tiny-aya-zerogpu "$AYA_PORT"; wait_ready aya "$AYA_PORT" ;;
67
+ klein) start_one klein klein-zerogpu "$KLEIN_PORT"; wait_ready klein "$KLEIN_PORT" ;;
68
+ all|*)
69
+ start_one aya tiny-aya-zerogpu "$AYA_PORT"
70
+ start_one klein klein-zerogpu "$KLEIN_PORT"
71
+ wait_ready aya "$AYA_PORT"
72
+ wait_ready klein "$KLEIN_PORT"
73
+ echo "both sidecars up. point the main app at them (already set in .env)."
74
+ ;;
75
+ esac
spaces/klein-zerogpu/README.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Tiny Army Klein ZeroGPU
3
+ emoji: 🤏
4
+ colorFrom: green
5
+ colorTo: gray
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ app_file: app.py
9
+ suggested_hardware: zero-a10g
10
+ pinned: false
11
+ license: apache-2.0
12
+ models:
13
+ - black-forest-labs/FLUX.2-klein-4B
14
+ ---
15
+
16
+ # Tiny Army Klein ZeroGPU
17
+
18
+ Private ZeroGPU sidecar for Tiny Army portrait generation.
spaces/klein-zerogpu/app.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ # LOCAL SIDECAR variant of the polats/tiny-army-klein-zerogpu Space.
4
+ #
5
+ # Same Gradio API contract as the hosted Space (so the main app's gradio_client talks to it
6
+ # unchanged):
7
+ # /generate(prompt, seed:int, lora:str) -> image # returns a PIL image / file path
8
+ #
9
+ # Differences from the hosted ZeroGPU app:
10
+ # * No ZeroGPU. `@spaces.GPU` is a no-op passthrough.
11
+ # * FLUX.2-klein is ~16 GB (transformer + text_encoder) and won't fit on the 3090, which is
12
+ # ALSO shared with a ~15 GB text-encoder server. We load in bf16 on CPU and stream to the GPU
13
+ # with `enable_sequential_cpu_offload()` (submodule granularity), so peak VRAM is only ~1-2 GB
14
+ # and the model never lands on the GPU all at once. (4-bit quantization was tried but its
15
+ # load-time GPU "warmup" allocation spikes above the ~5 GB of free headroom and OOMs; sequential
16
+ # offload sidesteps that AND keeps full bf16 quality. TINY_KLEIN_OFFLOAD=model uses the faster
17
+ # component-granularity offload instead — only safe when the card is mostly free.)
18
+ # * The weiner LoRA (polats/weiner-klein-lora) is loaded by DEFAULT and pre-applied at startup,
19
+ # so the main app's /generate calls (which don't pass a lora) get weiner portraits.
20
+ # * Loads the repo .env for HF_TOKEN.
21
+ import os
22
+ import random
23
+ import time
24
+
25
+ # Reduce CUDA fragmentation under the shared-card memory pressure (the offload churn allocates
26
+ # and frees repeatedly). Must be set before torch initializes CUDA.
27
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
28
+
29
+ try:
30
+ from dotenv import load_dotenv
31
+ load_dotenv(os.path.join(os.path.dirname(__file__), "..", "..", ".env"))
32
+ load_dotenv()
33
+ except Exception: # noqa: BLE001
34
+ pass
35
+
36
+
37
+ # Off ZeroGPU there's no spaces.GPU; make the decorator a no-op passthrough.
38
+ def GPU(*dargs, **dkwargs): # noqa: N802
39
+ def wrap(fn):
40
+ return fn
41
+ if len(dargs) == 1 and callable(dargs[0]) and not dkwargs:
42
+ return dargs[0]
43
+ return wrap
44
+
45
+
46
+ import gradio as gr
47
+ import torch
48
+ from diffusers import Flux2KleinPipeline
49
+
50
+ MODEL_ID = os.environ.get("TINY_KLEIN_MODEL", "black-forest-labs/FLUX.2-klein-4B")
51
+ STEPS = int(os.environ.get("TINY_KLEIN_STEPS", "4"))
52
+ GUIDANCE = float(os.environ.get("TINY_KLEIN_GUIDANCE", "1.0"))
53
+ MAX_SEED = 2_147_483_647
54
+ _HF_TOKEN = os.environ.get("HF_TOKEN") or None
55
+ # Offload strategy. "sequential" (default) streams submodules → ~1-2 GB peak, fits alongside the
56
+ # shared text-encoder server, slower. "model" streams whole components → ~8 GB peak, faster, only
57
+ # safe when the card is mostly free.
58
+ OFFLOAD = os.environ.get("TINY_KLEIN_OFFLOAD", "sequential").strip().lower()
59
+
60
+ # Optional character/style LoRAs, loadable per-request via the `lora` argument.
61
+ # Trained on klein-base; serving on the distilled model is the documented fast path.
62
+ LORA_REPO = os.environ.get("KLEIN_LORA_REPO", "polats/weiner-klein-lora")
63
+ LORAS = {
64
+ "weiner": "weiner_klein_4b_v1.safetensors", # step 1000 (final)
65
+ "weiner750": "weiner_klein_4b_v1_000000750.safetensors",
66
+ "weiner500": "weiner_klein_4b_v1_000000500.safetensors",
67
+ "weiner250": "weiner_klein_4b_v1_000000250.safetensors",
68
+ }
69
+ # Default LoRA applied at startup (and the component default so no-arg /generate calls use it).
70
+ DEFAULT_LORA = os.environ.get("KLEIN_LORA_DEFAULT", "weiner").strip()
71
+
72
+ print(f"[klein] loading {MODEL_ID} (bf16, offload={OFFLOAD if torch.cuda.is_available() else 'cpu'})", flush=True)
73
+ _t0 = time.time()
74
+ # bf16 on CPU — no GPU allocation at load time, so it never contends with the shared card.
75
+ pipe = Flux2KleinPipeline.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16, token=_HF_TOKEN)
76
+ print(f"[klein] loaded in {time.time() - _t0:.1f}s", flush=True)
77
+
78
+ _lora_active: str | None = None
79
+ _offload_enabled = False
80
+
81
+
82
+ def _load_lora(name: str) -> None:
83
+ pipe.load_lora_weights(LORA_REPO, weight_name=LORAS[name], token=_HF_TOKEN)
84
+
85
+
86
+ def _ensure_lora(name: str) -> None:
87
+ global _lora_active
88
+ name = (name or "").strip()
89
+ if name == (_lora_active or ""):
90
+ return
91
+ if _lora_active:
92
+ pipe.unload_lora_weights()
93
+ _lora_active = None
94
+ if name:
95
+ if name not in LORAS:
96
+ raise gr.Error(f"unknown lora '{name}' (have: {', '.join(LORAS)})")
97
+ _load_lora(name)
98
+ _lora_active = name
99
+
100
+
101
+ # Pre-apply the default LoRA, THEN install the offload hooks (load_lora before offload is the
102
+ # reliable order). After this the pipe manages its own device placement — never call .to(cuda).
103
+ if DEFAULT_LORA:
104
+ try:
105
+ _load_lora(DEFAULT_LORA)
106
+ _lora_active = DEFAULT_LORA
107
+ print(f"[klein] applied default LoRA: {DEFAULT_LORA}", flush=True)
108
+ except Exception as e: # noqa: BLE001
109
+ print(f"[klein] WARN: could not preload default LoRA {DEFAULT_LORA!r}: {e}", flush=True)
110
+
111
+ if torch.cuda.is_available():
112
+ if OFFLOAD == "model":
113
+ pipe.enable_model_cpu_offload() # whole-component granularity, ~8 GB peak, faster
114
+ print("[klein] enabled model CPU offload (peak ~ one component)", flush=True)
115
+ else:
116
+ pipe.enable_sequential_cpu_offload() # submodule granularity, ~1-2 GB peak, slower
117
+ print("[klein] enabled sequential CPU offload (peak ~1-2 GB)", flush=True)
118
+ _offload_enabled = True
119
+
120
+
121
+ @GPU(duration=60)
122
+ def generate(prompt: str, seed: int = 42, lora: str = DEFAULT_LORA):
123
+ if not prompt or not prompt.strip():
124
+ raise gr.Error("prompt required")
125
+ _ensure_lora(lora)
126
+ # With offload enabled the pipeline places its own modules; the generator must live on the
127
+ # device the latents end up on (cuda when offloading, else cpu).
128
+ dev = "cuda" if torch.cuda.is_available() else "cpu"
129
+ if seed is None or int(seed) < 0:
130
+ seed = random.randint(0, MAX_SEED)
131
+ img = pipe(
132
+ prompt=prompt.strip(),
133
+ width=1024,
134
+ height=1024,
135
+ num_inference_steps=STEPS,
136
+ guidance_scale=GUIDANCE,
137
+ generator=torch.Generator(device=dev).manual_seed(int(seed)),
138
+ ).images[0]
139
+ if dev == "cuda":
140
+ torch.cuda.empty_cache()
141
+ return img
142
+
143
+
144
+ demo = gr.Interface(
145
+ fn=generate,
146
+ inputs=[
147
+ gr.Textbox(label="Prompt", lines=4),
148
+ gr.Number(label="Seed", value=42, precision=0),
149
+ gr.Textbox(label="LoRA", value=DEFAULT_LORA,
150
+ placeholder="blank = none; weiner / weiner750 / weiner500 / weiner250"),
151
+ ],
152
+ outputs=gr.Image(type="pil", label="Portrait"),
153
+ api_name="generate",
154
+ title="Tiny Army Klein — local sidecar",
155
+ )
156
+
157
+ if __name__ == "__main__":
158
+ demo.queue().launch(
159
+ server_name=os.environ.get("GRADIO_SERVER_NAME", "127.0.0.1"),
160
+ server_port=int(os.environ.get("PORT", "7865")),
161
+ show_error=True,
162
+ )
spaces/klein-zerogpu/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/huggingface/diffusers.git
2
+ torch
3
+ transformers
4
+ accelerate
5
+ sentencepiece
6
+ protobuf
7
+ spaces
8
+ peft
9
+ python-dotenv
spaces/tiny-aya-zerogpu/README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Tiny Army Tiny Aya ZeroGPU
3
+ emoji: 🌍
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 6.15.2
8
+ app_file: app.py
9
+ python_version: "3.11"
10
+ suggested_hardware: zero-a10g
11
+ pinned: false
12
+ license: apache-2.0
13
+ models:
14
+ - CohereLabs/tiny-aya-global
15
+ ---
16
+
17
+ # Tiny Army Tiny Aya ZeroGPU
18
+
19
+ ZeroGPU sidecar for Tiny Army text generation using Cohere Labs Tiny Aya Global.
spaces/tiny-aya-zerogpu/app.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ # LOCAL SIDECAR variant of the polats/tiny-army-tiny-aya-zerogpu Space.
4
+ #
5
+ # Same Gradio API contract as the hosted Space (so the main app's gradio_client talks to it
6
+ # unchanged):
7
+ # /generate(system, user, max_tokens:int, temperature:float) -> str
8
+ # /generate_stream(system, user, max_tokens:int, temperature:float) -> str # CUMULATIVE, streamed
9
+ #
10
+ # Differences from the hosted ZeroGPU app:
11
+ # * No ZeroGPU. `@spaces.GPU` is replaced by a no-op passthrough (the locally-installed
12
+ # `spaces` package has no `.GPU` off-platform), so generate() just runs on the local CUDA
13
+ # device the model is already resident on.
14
+ # * Loads a sibling/repo .env for HF_TOKEN (tiny-aya-global is a gated repo).
15
+ # * Caps this process's VRAM so a spike can't grab the whole 3090 and crash the desktop.
16
+ import os
17
+ import threading
18
+
19
+ os.environ.setdefault("OPENBLAS_NUM_THREADS", "4")
20
+ os.environ.setdefault("OMP_NUM_THREADS", "4")
21
+ os.environ.setdefault("MKL_NUM_THREADS", "4")
22
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
23
+ os.environ.setdefault("GRADIO_SSR_MODE", "false")
24
+
25
+ try:
26
+ from dotenv import load_dotenv
27
+ # repo root .env (two dirs up: spaces/tiny-aya-zerogpu/ -> repo root)
28
+ load_dotenv(os.path.join(os.path.dirname(__file__), "..", "..", ".env"))
29
+ load_dotenv() # also any .env in CWD
30
+ except Exception: # noqa: BLE001
31
+ pass
32
+
33
+ import gradio as gr
34
+ import torch
35
+ from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
36
+
37
+
38
+ # Off ZeroGPU there's no spaces.GPU; make the decorator a no-op passthrough. The model is
39
+ # already on the local CUDA device, so the wrapped fn runs there directly.
40
+ def GPU(*dargs, **dkwargs): # noqa: N802
41
+ def wrap(fn):
42
+ return fn
43
+ if len(dargs) == 1 and callable(dargs[0]) and not dkwargs:
44
+ return dargs[0]
45
+ return wrap
46
+
47
+
48
+ MODEL_ID = os.environ.get("TINY_AYA_MODEL", "CohereLabs/tiny-aya-global")
49
+ DEFAULT_MAX_TOKENS = int(os.environ.get("TINY_AYA_MAX_TOKENS", "400"))
50
+ _HF_TOKEN = os.environ.get("HF_TOKEN") or None
51
+ # 4-bit by default: the 3090 is shared with a ~15 GB text-encoder server, so we NF4-quantize
52
+ # tiny-aya (~7 GB bf16 -> ~2.5 GB) to leave room. TINY_AYA_QUANT=bf16 disables it (best quality).
53
+ QUANT = os.environ.get("TINY_AYA_QUANT", "4bit").strip().lower()
54
+
55
+ # GUARDRAIL: the 3090 also drives the desktop. Cap THIS process's share of the card so an OOM
56
+ # errors out instead of taking down the display.
57
+ if torch.cuda.is_available():
58
+ try:
59
+ torch.cuda.set_per_process_memory_fraction(
60
+ float(os.environ.get("TINY_AYA_VRAM_FRAC", "0.5")), 0
61
+ )
62
+ except Exception: # noqa: BLE001
63
+ pass
64
+
65
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=_HF_TOKEN)
66
+
67
+
68
+ def _load_model():
69
+ if not torch.cuda.is_available():
70
+ return AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype="auto", token=_HF_TOKEN)
71
+ if QUANT == "bf16":
72
+ m = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16, token=_HF_TOKEN)
73
+ return m.to("cuda")
74
+ # 4-bit NF4, loaded straight onto the GPU (bnb models can't be .to()'d afterwards).
75
+ from transformers import BitsAndBytesConfig
76
+ quant = BitsAndBytesConfig(
77
+ load_in_4bit=True,
78
+ bnb_4bit_quant_type="nf4",
79
+ bnb_4bit_compute_dtype=torch.bfloat16,
80
+ bnb_4bit_use_double_quant=True,
81
+ )
82
+ return AutoModelForCausalLM.from_pretrained(
83
+ MODEL_ID, quantization_config=quant, torch_dtype=torch.bfloat16,
84
+ device_map="cuda", token=_HF_TOKEN,
85
+ )
86
+
87
+
88
+ print(f"[tiny-aya] loading {MODEL_ID} quant={QUANT if torch.cuda.is_available() else 'cpu'}", flush=True)
89
+ model = _load_model()
90
+ model.eval()
91
+ print("[tiny-aya] model ready", flush=True)
92
+
93
+ _lock = threading.Lock()
94
+
95
+
96
+ def _messages(system: str, user: str):
97
+ messages = []
98
+ if system and system.strip():
99
+ messages.append({"role": "system", "content": system.strip()})
100
+ messages.append({"role": "user", "content": (user or "").strip()})
101
+ return messages
102
+
103
+
104
+ @GPU(duration=120)
105
+ def generate(system: str, user: str, max_tokens: int = DEFAULT_MAX_TOKENS, temperature: float = 0.8):
106
+ if not user or not user.strip():
107
+ raise gr.Error("user prompt required")
108
+ max_tokens = max(1, min(int(max_tokens or DEFAULT_MAX_TOKENS), 1024))
109
+ temperature = max(0.0, min(float(temperature if temperature is not None else 0.8), 2.0))
110
+ inputs = tokenizer.apply_chat_template(
111
+ _messages(system, user),
112
+ tokenize=True,
113
+ add_generation_prompt=True,
114
+ return_dict=True,
115
+ return_tensors="pt",
116
+ ).to(model.device)
117
+ with _lock, torch.inference_mode():
118
+ outputs = model.generate(
119
+ **inputs,
120
+ max_new_tokens=max_tokens,
121
+ do_sample=temperature > 0,
122
+ temperature=max(temperature, 1e-5),
123
+ top_p=0.95,
124
+ pad_token_id=tokenizer.eos_token_id,
125
+ )
126
+ return tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=True).strip()
127
+
128
+
129
+ @GPU(duration=120)
130
+ def generate_stream(system: str, user: str, max_tokens: int = DEFAULT_MAX_TOKENS, temperature: float = 0.8):
131
+ if not user or not user.strip():
132
+ raise gr.Error("user prompt required")
133
+ max_tokens = max(1, min(int(max_tokens or DEFAULT_MAX_TOKENS), 1024))
134
+ temperature = max(0.0, min(float(temperature if temperature is not None else 0.8), 2.0))
135
+ inputs = tokenizer.apply_chat_template(
136
+ _messages(system, user),
137
+ tokenize=True,
138
+ add_generation_prompt=True,
139
+ return_dict=True,
140
+ return_tensors="pt",
141
+ ).to(model.device)
142
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
143
+
144
+ def run():
145
+ with _lock, torch.inference_mode():
146
+ model.generate(
147
+ **inputs,
148
+ max_new_tokens=max_tokens,
149
+ do_sample=temperature > 0,
150
+ temperature=max(temperature, 1e-5),
151
+ top_p=0.95,
152
+ pad_token_id=tokenizer.eos_token_id,
153
+ streamer=streamer,
154
+ )
155
+
156
+ thread = threading.Thread(target=run, daemon=True)
157
+ thread.start()
158
+ acc = ""
159
+ for token in streamer:
160
+ acc += token
161
+ yield acc
162
+ thread.join(timeout=1)
163
+
164
+
165
+ with gr.Blocks(title="Tiny Army Tiny Aya (local)") as demo:
166
+ gr.Markdown("# Tiny Army Tiny Aya — local sidecar")
167
+ system = gr.Textbox(label="System", lines=5)
168
+ user = gr.Textbox(label="User", lines=5)
169
+ max_tokens = gr.Slider(1, 1024, value=DEFAULT_MAX_TOKENS, step=1, label="Max new tokens")
170
+ temperature = gr.Slider(0, 2, value=0.8, step=0.05, label="Temperature")
171
+ btn = gr.Button("Generate")
172
+ out = gr.Textbox(label="Output", lines=10)
173
+ btn.click(generate, inputs=[system, user, max_tokens, temperature], outputs=out, api_name="generate")
174
+ btn.click(generate_stream, inputs=[system, user, max_tokens, temperature], outputs=out, api_name="generate_stream")
175
+
176
+
177
+ if __name__ == "__main__":
178
+ demo.queue().launch(
179
+ server_name=os.environ.get("GRADIO_SERVER_NAME", "127.0.0.1"),
180
+ server_port=int(os.environ.get("PORT", "7864")),
181
+ show_error=True,
182
+ )
spaces/tiny-aya-zerogpu/requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==6.15.2
2
+ spaces
3
+ torch
4
+ transformers>=5.4.0
5
+ accelerate
6
+ sentencepiece
7
+ protobuf
8
+ huggingface_hub
9
+ python-dotenv
10
+ bitsandbytes