Jarbas commited on
Commit
a469aa7
·
verified ·
1 Parent(s): 89e0793

Both model sizes on ZeroGPU

Browse files
Files changed (3) hide show
  1. README.md +4 -2
  2. app.py +39 -29
  3. requirements.txt +2 -2
README.md CHANGED
@@ -14,6 +14,7 @@ models:
14
  - TigreGotico/jevasio-0.8b
15
  - TigreGotico/jevasio-4b
16
  - Qwen/Qwen3.5-0.8B-Base
 
17
  datasets:
18
  - TigreGotico/jevasio-decisions
19
  tags:
@@ -28,8 +29,9 @@ Jevásio is a decision model for European Portuguese (pt-PT). It reads a state,
28
  set of typed questions about it. It returns a probability for each option of each question in one forward pass. It
29
  does not generate text.
30
 
31
- This Space runs [Jevásio-0.8B](https://huggingface.co/TigreGotico/jevasio-0.8b) on a CPU, in fp32, so one request
32
- takes a few seconds. [Jevásio-4B](https://huggingface.co/TigreGotico/jevasio-4b) is more accurate and needs a GPU.
 
33
  Both models are [Kev](https://github.com/jaredpalmer/kev) fine-tuned on the
34
  [TigreGotico/jevasio-decisions](https://huggingface.co/datasets/TigreGotico/jevasio-decisions) dataset.
35
 
 
14
  - TigreGotico/jevasio-0.8b
15
  - TigreGotico/jevasio-4b
16
  - Qwen/Qwen3.5-0.8B-Base
17
+ - Qwen/Qwen3.5-4B-Base
18
  datasets:
19
  - TigreGotico/jevasio-decisions
20
  tags:
 
29
  set of typed questions about it. It returns a probability for each option of each question in one forward pass. It
30
  does not generate text.
31
 
32
+ This Space runs both sizes on ZeroGPU, in bf16: [Jevásio-4B](https://huggingface.co/TigreGotico/jevasio-4b), the more
33
+ accurate one, and [Jevásio-0.8B](https://huggingface.co/TigreGotico/jevasio-0.8b), the smaller one. Pick one with the
34
+ model selector.
35
  Both models are [Kev](https://github.com/jaredpalmer/kev) fine-tuned on the
36
  [TigreGotico/jevasio-decisions](https://huggingface.co/datasets/TigreGotico/jevasio-decisions) dataset.
37
 
app.py CHANGED
@@ -1,9 +1,9 @@
1
- """Jevásio on Hugging Face Spaces (Gradio, CPU).
2
 
3
  A state and a set of typed questions in, a probability per option out, in one forward pass. The model is loaded through
4
  Kev's own loader (kev.checkpoint), the same path `python -m kev.serve --run TigreGotico/jevasio-0.8b` takes, and
5
- requests go through kev.api and kev.model.admit exactly as /v1/systemone does. On CPU the weights stay in fp32
6
- (KEV_DTYPE=bf16 overrides it).
7
  """
8
  import html
9
  import json
@@ -12,6 +12,7 @@ import time
12
  from dataclasses import replace
13
 
14
  import gradio as gr
 
15
  import torch
16
  from pydantic import ValidationError
17
 
@@ -19,22 +20,28 @@ from kev.api import SystemOneRequest, output_tokens, render, to_answers, to_reco
19
  from kev.checkpoint import Checkpoint, LoadOptions
20
  from kev.model import admit
21
 
22
- MODEL_REPO = os.environ.get("JEVASIO_MODEL", "TigreGotico/jevasio-0.8b")
 
23
  LINK_08B = "https://huggingface.co/TigreGotico/jevasio-0.8b"
24
  LINK_4B = "https://huggingface.co/TigreGotico/jevasio-4b"
25
  LINK_DATA = "https://huggingface.co/datasets/TigreGotico/jevasio-decisions"
26
 
27
- torch.set_num_threads(os.cpu_count() or 1)
 
 
 
 
 
 
 
 
 
 
28
 
29
- t0 = time.perf_counter()
30
- CHECKPOINT = Checkpoint(MODEL_REPO)
31
- opts = LoadOptions.from_env()
32
- if opts.dtype is None:
33
- opts = replace(opts, dtype=torch.float32)
34
- TOKENIZER, MODEL = CHECKPOINT.load("cpu", opts)
35
- LOAD_SECONDS = time.perf_counter() - t0
36
- print(f"[jevasio] {MODEL_REPO}: base={CHECKPOINT.meta.base}@{CHECKPOINT.meta.base_revision} dtype={MODEL.dtype} "
37
- f"temperature={MODEL.head.temperature:.2f} loaded in {LOAD_SECONDS:.0f} s", flush=True)
38
 
39
 
40
  class RequestError(ValueError):
@@ -50,19 +57,22 @@ def parse_request(payload):
50
  raise RequestError(f"Pedido inválido em `{where}`: {first.get('msg')}") from None
51
 
52
 
53
- def systemone(req):
54
  """The /v1/systemone response body kev.serve returns for this request, plus the latency of the forward pass."""
 
 
 
55
  rec, meta = to_record(req)
56
  try:
57
- enc = admit(MODEL, TOKENIZER, rec)
58
  except ValueError as e:
59
  raise RequestError(str(e)) from None
60
  start = time.perf_counter()
61
- probs = MODEL.probs(enc)
62
  ms = (time.perf_counter() - start) * 1000
63
  answers = to_answers([p.tolist() for p in probs], meta)
64
- return {"model": MODEL_REPO, "answers": answers,
65
- "usage": {"input_tokens": len(enc["ids"]), "output_tokens": output_tokens(TOKENIZER, answers)},
66
  "latency_ms": round(ms, 1)}
67
 
68
 
@@ -91,7 +101,7 @@ def render_answers(req, resp):
91
  usage = resp["usage"]
92
  parts = ['<div style="font-size:12px;color:var(--body-text-color-subdued);margin:6px 0 10px">'
93
  f'{len(resp["answers"])} pergunta(s) · {usage["input_tokens"]} tokens de entrada · '
94
- f'{resp["latency_ms"]:.0f} ms em CPU · nenhum texto gerado</div>']
95
  for qid, a in resp["answers"].items():
96
  instructions = render(req.questions[qid].instructions)
97
  if a["type"] == "noul":
@@ -167,18 +177,18 @@ def build_request(state, choice_id, choice_instr, choice_opts, noul_id, noul_ins
167
  return {"state": parse_state(state), "model": "kev-latest", "questions": questions}
168
 
169
 
170
- def decide(state, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels):
171
  """Answer the builder's questions about the state. Returns the rendered answers, the request and the response."""
172
  try:
173
  payload = build_request(state, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels)
174
  req = parse_request(payload)
175
- resp = systemone(req)
176
  except RequestError as e:
177
  return error_html(str(e)), {}, {"error": str(e)}
178
  return render_answers(req, resp), payload, resp
179
 
180
 
181
- def decide_raw(request_json):
182
  """Answer a raw System One request (JSON text). Returns a status line and the response or an error object."""
183
  try:
184
  payload = json.loads(request_json or "")
@@ -189,7 +199,7 @@ def decide_raw(request_json):
189
  message = 'O pedido tem de ser um objeto JSON com "state" e "questions".'
190
  return f"**Erro:** {message}", {"error": message}
191
  try:
192
- resp = systemone(parse_request(payload))
193
  except RequestError as e:
194
  return f"**Erro:** {e}", {"error": str(e)}
195
  return f"Respondido em {resp['latency_ms']:.0f} ms.", resp
@@ -263,8 +273,7 @@ INTRO = f"""# Jevásio
263
  Modelo de decisão para português europeu. Recebe um **estado** (um texto ou um objeto JSON) e perguntas tipadas, e
264
  devolve uma probabilidade para cada opção de cada pergunta, numa só passagem pelo modelo. Não gera texto.
265
 
266
- Este Space corre o [Jevásio-0.8B]({LINK_08B}) em CPU, por isso cada pedido demora alguns segundos. Existe também o
267
- [Jevásio-4B]({LINK_4B}), mais exato, que precisa de GPU.
268
  """
269
 
270
  ABOUT = f"""Há três tipos de pergunta: `choice` (escolher uma opção com nome), `noul` (sim ou não) e `score` (um nível numa
@@ -273,7 +282,7 @@ valores em português, como nos dados de treino.
273
 
274
  O Jevásio é o [Kev](https://github.com/jaredpalmer/kev) afinado com o conjunto
275
  [TigreGotico/jevasio-decisions]({LINK_DATA}). A probabilidade usa a temperatura de calibração guardada no modelo
276
- (T = {MODEL.head.temperature:.2f}). Antes de definir um limiar, meça o modelo com os seus próprios dados.
277
 
278
  Licença CC BY-NC-SA 4.0: só para investigação não comercial. Não use o modelo em decisões com consequências legais,
279
  médicas, financeiras ou laborais sem revisão humana.
@@ -293,6 +302,7 @@ first = EXAMPLES[0]
293
  with gr.Blocks(title="Jevásio") as demo:
294
  with gr.Column(elem_id="col-container"):
295
  gr.Markdown(INTRO)
 
296
  with gr.Tabs():
297
  with gr.Tab("Perguntas"):
298
  with gr.Row():
@@ -330,7 +340,7 @@ with gr.Blocks(title="Jevásio") as demo:
330
  inputs = [state_box, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels]
331
  gr.Examples(examples=[example_row(ex) for ex in EXAMPLES], example_labels=[ex["name"] for ex in EXAMPLES],
332
  inputs=inputs, label="Exemplos")
333
- run_btn.click(fn=decide, inputs=inputs, outputs=[answers_html, request_json, response_json], api_name="decide")
334
 
335
  with gr.Tab("Avançado"):
336
  gr.Markdown("Cole um pedido em JSON no formato System One (`POST /v1/systemone`) e veja a resposta em JSON.")
@@ -341,7 +351,7 @@ with gr.Blocks(title="Jevásio") as demo:
341
  with gr.Column(scale=1):
342
  raw_status = gr.Markdown()
343
  raw_out = gr.JSON(label="Resposta (JSON)")
344
- raw_btn.click(fn=decide_raw, inputs=[raw_in], outputs=[raw_status, raw_out], api_name="systemone")
345
 
346
  with gr.Tab("Sobre"):
347
  gr.Markdown(ABOUT)
 
1
+ """Jevásio on Hugging Face Spaces (Gradio, ZeroGPU).
2
 
3
  A state and a set of typed questions in, a probability per option out, in one forward pass. The model is loaded through
4
  Kev's own loader (kev.checkpoint), the same path `python -m kev.serve --run TigreGotico/jevasio-0.8b` takes, and
5
+ requests go through kev.api and kev.model.admit exactly as /v1/systemone does. Both sizes load in bf16 at start-up, and
6
+ each forward pass runs inside @spaces.GPU, so a request holds a GPU only for that pass.
7
  """
8
  import html
9
  import json
 
12
  from dataclasses import replace
13
 
14
  import gradio as gr
15
+ import spaces
16
  import torch
17
  from pydantic import ValidationError
18
 
 
20
  from kev.checkpoint import Checkpoint, LoadOptions
21
  from kev.model import admit
22
 
23
+ MODELS = {"Jevásio-4B": "TigreGotico/jevasio-4b", "Jevásio-0.8B": "TigreGotico/jevasio-0.8b"}
24
+ DEFAULT = "Jevásio-4B"
25
  LINK_08B = "https://huggingface.co/TigreGotico/jevasio-0.8b"
26
  LINK_4B = "https://huggingface.co/TigreGotico/jevasio-4b"
27
  LINK_DATA = "https://huggingface.co/datasets/TigreGotico/jevasio-decisions"
28
 
29
+ LOADED = {}
30
+ for name, repo in MODELS.items():
31
+ t0 = time.perf_counter()
32
+ checkpoint = Checkpoint(repo)
33
+ opts = LoadOptions.from_env()
34
+ if opts.dtype is None:
35
+ opts = replace(opts, dtype=torch.bfloat16)
36
+ LOADED[name] = checkpoint.load("cuda", opts)
37
+ tok, model = LOADED[name]
38
+ print(f"[jevasio] {repo}: base={checkpoint.meta.base}@{checkpoint.meta.base_revision} dtype={model.dtype} "
39
+ f"temperature={model.head.temperature:.2f} loaded in {time.perf_counter() - t0:.0f} s", flush=True)
40
 
41
+
42
+ @spaces.GPU(duration=30)
43
+ def forward(name, enc):
44
+ return [p.float().cpu() for p in LOADED[name][1].probs(enc)]
 
 
 
 
 
45
 
46
 
47
  class RequestError(ValueError):
 
57
  raise RequestError(f"Pedido inválido em `{where}`: {first.get('msg')}") from None
58
 
59
 
60
+ def systemone(req, name=DEFAULT):
61
  """The /v1/systemone response body kev.serve returns for this request, plus the latency of the forward pass."""
62
+ if name not in LOADED:
63
+ raise RequestError(f"Modelo desconhecido: {name}. Opções: {', '.join(MODELS)}.")
64
+ tokenizer, model = LOADED[name]
65
  rec, meta = to_record(req)
66
  try:
67
+ enc = admit(model, tokenizer, rec)
68
  except ValueError as e:
69
  raise RequestError(str(e)) from None
70
  start = time.perf_counter()
71
+ probs = forward(name, enc)
72
  ms = (time.perf_counter() - start) * 1000
73
  answers = to_answers([p.tolist() for p in probs], meta)
74
+ return {"model": MODELS[name], "answers": answers,
75
+ "usage": {"input_tokens": len(enc["ids"]), "output_tokens": output_tokens(tokenizer, answers)},
76
  "latency_ms": round(ms, 1)}
77
 
78
 
 
101
  usage = resp["usage"]
102
  parts = ['<div style="font-size:12px;color:var(--body-text-color-subdued);margin:6px 0 10px">'
103
  f'{len(resp["answers"])} pergunta(s) · {usage["input_tokens"]} tokens de entrada · '
104
+ f'{resp["model"].split("/")[1]} · {resp["latency_ms"]:.0f} ms · nenhum texto gerado</div>']
105
  for qid, a in resp["answers"].items():
106
  instructions = render(req.questions[qid].instructions)
107
  if a["type"] == "noul":
 
177
  return {"state": parse_state(state), "model": "kev-latest", "questions": questions}
178
 
179
 
180
+ def decide(name, state, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels):
181
  """Answer the builder's questions about the state. Returns the rendered answers, the request and the response."""
182
  try:
183
  payload = build_request(state, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels)
184
  req = parse_request(payload)
185
+ resp = systemone(req, name)
186
  except RequestError as e:
187
  return error_html(str(e)), {}, {"error": str(e)}
188
  return render_answers(req, resp), payload, resp
189
 
190
 
191
+ def decide_raw(name, request_json):
192
  """Answer a raw System One request (JSON text). Returns a status line and the response or an error object."""
193
  try:
194
  payload = json.loads(request_json or "")
 
199
  message = 'O pedido tem de ser um objeto JSON com "state" e "questions".'
200
  return f"**Erro:** {message}", {"error": message}
201
  try:
202
+ resp = systemone(parse_request(payload), name)
203
  except RequestError as e:
204
  return f"**Erro:** {e}", {"error": str(e)}
205
  return f"Respondido em {resp['latency_ms']:.0f} ms.", resp
 
273
  Modelo de decisão para português europeu. Recebe um **estado** (um texto ou um objeto JSON) e perguntas tipadas, e
274
  devolve uma probabilidade para cada opção de cada pergunta, numa só passagem pelo modelo. Não gera texto.
275
 
276
+ Escolha o modelo: o [Jevásio-4B]({LINK_4B}) é mais exato, o [Jevásio-0.8B]({LINK_08B}) é mais pequeno e rápido.
 
277
  """
278
 
279
  ABOUT = f"""Há três tipos de pergunta: `choice` (escolher uma opção com nome), `noul` (sim ou não) e `score` (um nível numa
 
282
 
283
  O Jevásio é o [Kev](https://github.com/jaredpalmer/kev) afinado com o conjunto
284
  [TigreGotico/jevasio-decisions]({LINK_DATA}). A probabilidade usa a temperatura de calibração guardada no modelo
285
+ (T = {LOADED["Jevásio-4B"][1].head.temperature:.2f} no 4B e {LOADED["Jevásio-0.8B"][1].head.temperature:.2f} no 0.8B). Antes de definir um limiar, meça o modelo com os seus próprios dados.
286
 
287
  Licença CC BY-NC-SA 4.0: só para investigação não comercial. Não use o modelo em decisões com consequências legais,
288
  médicas, financeiras ou laborais sem revisão humana.
 
302
  with gr.Blocks(title="Jevásio") as demo:
303
  with gr.Column(elem_id="col-container"):
304
  gr.Markdown(INTRO)
305
+ model_choice = gr.Radio(list(MODELS), value=DEFAULT, label="Modelo")
306
  with gr.Tabs():
307
  with gr.Tab("Perguntas"):
308
  with gr.Row():
 
340
  inputs = [state_box, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels]
341
  gr.Examples(examples=[example_row(ex) for ex in EXAMPLES], example_labels=[ex["name"] for ex in EXAMPLES],
342
  inputs=inputs, label="Exemplos")
343
+ run_btn.click(fn=decide, inputs=[model_choice, *inputs], outputs=[answers_html, request_json, response_json], api_name="decide")
344
 
345
  with gr.Tab("Avançado"):
346
  gr.Markdown("Cole um pedido em JSON no formato System One (`POST /v1/systemone`) e veja a resposta em JSON.")
 
351
  with gr.Column(scale=1):
352
  raw_status = gr.Markdown()
353
  raw_out = gr.JSON(label="Resposta (JSON)")
354
+ raw_btn.click(fn=decide_raw, inputs=[model_choice, raw_in], outputs=[raw_status, raw_out], api_name="systemone")
355
 
356
  with gr.Tab("Sobre"):
357
  gr.Markdown(ABOUT)
requirements.txt CHANGED
@@ -1,3 +1,3 @@
1
- --extra-index-url https://download.pytorch.org/whl/cpu
2
- torch==2.8.0+cpu
3
  kev @ git+https://github.com/jaredpalmer/kev@1f04cfa28e6a708485b28a9585010b9e436b776e
 
1
+ torch==2.8.0
2
+ spaces
3
  kev @ git+https://github.com/jaredpalmer/kev@1f04cfa28e6a708485b28a9585010b9e436b776e