nari-router-4b / modeling_sysone.py
leapshared's picture
nari-router-4b: nari-decision-4b v3.2 + ์šฐ๋ฆฌ ๋ง LoRA(ํ•ฉ์นจ), ์”จ์•— 1 ยท 3์—ํฌํฌ ยท lm_weight 0.5 ยท ์˜จ๋„ 1.86
5451004 verified
Raw History Blame Contribute Delete
20.2 kB
"""System One ๊ฒฐ์ • ๋ชจ๋ธ: Qwen3.5 ํ…์ŠคํŠธ ๋ชจ๋ธ(LoRA ๋ณ‘ํ•ฉ) + ๊ฒฐ์ • ํ—ค๋“œ (Hugging Face `trust_remote_code`).
์ƒํƒœ(state)์™€ ํƒ€์ž…์ด ์ •ํ•ด์ง„ ์งˆ๋ฌธ(noulยทchoiceยทscore)์„ ๋ฐ›์•„, ๊ธ€์„ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ํ•œ ๋ฒˆ์˜ forward๋กœ
์งˆ๋ฌธ๋งˆ๋‹ค ์„ ํƒ์ง€๋ณ„ ๋ณด์ •๋œ ํ™•๋ฅ ์„ ๋‚ธ๋‹ค.
from transformers import AutoModel, AutoTokenizer
tok = AutoTokenizer.from_pretrained(REPO, trust_remote_code=True)
model = AutoModel.from_pretrained(REPO, trust_remote_code=True).eval()
model.decide(
tok,
state="๊ณ ๊ฐ: ๊ฒฐ์ œ๊ฐ€ ๋‘ ๋ฒˆ ๋์–ด์š”. ํ•œ ๊ฑด์€ ์ทจ์†Œํ•ด ์ฃผ์„ธ์š”.",
questions={
"refund": {"type": "noul", "instructions": "ํ™˜๋ถˆยท์ทจ์†Œ ์š”์ฒญ์ธ๊ฐ€์š”?"},
"team": {"type": "choice", "instructions": "๋‹ด๋‹น ํŒ€์„ ๊ณ ๋ฅด์„ธ์š”.",
"options": ["billing", "shipping", "account"]},
},
)
GPU์—์„œ ํ•œ ๊ฑด์”ฉ ํŒ๋‹จํ•  ๋•Œ๋Š” `model.to("cuda").enable_cuda_graphs()`๋ฅผ ํ•œ ๋ฒˆ ๋ถ€๋ฅด๋ฉด decide()๊ฐ€ ๊ธธ์ด ๊ตฌ๊ฐ„๋ณ„
CUDA ๊ทธ๋ž˜ํ”„๋กœ ๋ˆ๋‹ค (A100์—์„œ ์•ฝ 3๋ฐฐ ๋น ๋ฅด๋‹ค. ์•„๋ž˜ `_GraphRunner`).
์ž…๋ ฅ ํ˜•์‹ v2(config.input_format)๋ถ€ํ„ฐ๋Š” ๊ฐ™์€ forward์—์„œ ์–ธ์–ด ๋ชจ๋ธ ๊ธ€์ž ๊ฒฝ๋กœ๋„ ์ฝ๋Š”๋‹ค: ์„ ํƒ์ง€์— ๊ธ€์ž(A., B., โ€ฆ)๋ฅผ
๋ถ™์ด๊ณ  ์งˆ๋ฌธ๋งˆ๋‹ค ๋‹ต ์ž๋ฆฌ๋ฅผ ๋‘์–ด, ๊ทธ ์ž๋ฆฌ์˜ ๋‹ค์Œ ํ† ํฐ ํ™•๋ฅ (์ถœ๋ ฅ์ธต = ์ž…๋ ฅ ์ž„๋ฒ ๋”ฉ)์„ ๊ฒฐ์ • ํ—ค๋“œ์˜ ํ™•๋ฅ ๊ณผ ์„ž๋Š”๋‹ค.
์ด ํŒŒ์ผ์€ ์ €์žฅ์†Œ์— ๊ฐ™์ด ์˜ฌ๋ผ๊ฐ€ ๋ฐ›๋Š” ์ชฝ์—์„œ ์‹คํ–‰๋œ๋‹ค. ํ•™์Šต ์ฝ”๋“œ(sysone)์— ๊ธฐ๋Œ€์ง€ ์•Š๊ณ , ์ž…๋ ฅ ํ˜•์‹์€ ํ•™์Šต ๋•Œ์™€
๊ฐ™์€ ๊ทœ์น™์„ ๋”ฐ๋ฅธ๋‹ค (์ƒํƒœ โ†’ ์งˆ๋ฌธ๋งˆ๋‹ค ๋จธ๋ฆฌ๋งยท์ง€์‹œ๋ฌธยท์„ ํƒ์ง€ ์ค„, ์„ ํƒ์ง€ ์ค„์˜ ๋งˆ์ง€๋ง‰ ํ† ํฐ ์œ„์น˜๋กœ ์ ์ˆ˜, ์ƒํƒœ๊ฐ€ ๊ธธ๋ฉด ๋’ค๋ฅผ ์ž๋ฅธ๋‹ค).
"""
from __future__ import annotations
import contextlib
import json
import threading
from typing import Any, ClassVar
import torch
from torch import nn
from transformers import AutoConfig, AutoModel, PreTrainedModel
from .configuration_sysone import SysOneConfig
QTYPES = ("noul", "choice", "score")
NOUL_OPTIONS = ("true", "false")
STATE_HEADER = "### ์ƒํƒœ (state)\n"
MIN_STATE_TOKENS = 32
TYPE_HINT = {
"noul": "์˜ˆ/์•„๋‹ˆ์˜ค (true/false)",
"choice": "ํ•˜๋‚˜ ๊ณ ๋ฅด๊ธฐ",
"score": "์ˆœ์„œ ์ฒ™๋„ (๋‚ฎ์Œโ†’๋†’์Œ)",
}
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
NOUL_SHOWN = {"true": "true (์˜ˆ / yes)", "false": "false (์•„๋‹ˆ์˜ค / no)"}
LETTER_HINT = (
"Answer with one letter." # ์งˆ๋ฌธ๋งˆ๋‹ค ๋”ฐ๋กœ ๋ฌป๋Š” ํ”„๋กฌํ”„ํŠธ(sysone.baseline)์˜ ๋งˆ์ง€๋ง‰ ์ค„๊ณผ ๊ฐ™๋‹ค
)
ANSWER_CUE = LETTER_HINT + "\n๋‹ต:"
ALL_HINT = "Answer each question with one letter."
CHAT_SYSTEM = (
"You are a careful decision model. Read the state and answer each multiple-choice question "
"with the letter of exactly one option."
)
CHAT_OPEN = f"<|im_start|>system\n{CHAT_SYSTEM}<|im_end|>\n<|im_start|>user\n"
CHAT_CLOSE = "<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n"
CHAT_CLOSE_NOTHINK = "<|im_end|>\n<|im_start|>assistant\n"
TRUNC_MARK = "\n(โ€ฆ์ค‘๋žตโ€ฆ)\n"
REOPEN = "<|im_end|>\n<|im_start|>user\n" # v4chat: ๋นˆ ์–ด์‹œ์Šคํ„ดํŠธ ์ฐจ๋ก€๋ฅผ ๋‹ซ๊ณ  ๋‹ค์Œ ์งˆ๋ฌธ์˜ ์‚ฌ์šฉ์ž ์ฐจ๋ก€๋ฅผ ์—ฐ๋‹ค
TAIL = -2
AFTER = -3
class DecisionHead(nn.Module):
"""์„ ํƒ์ง€ ์œ„์น˜ hidden์„ ์ฟผ๋ฆฌ๋กœ: ์„ ํƒ์ง€๋ผ๋ฆฌ self-attention + ์ „์ฒด ์‹œํ€€์Šค cross-attention + ์„ ํ˜• prior."""
def __init__(
self,
d_model: int,
d_head: int = 512,
n_layers: int = 2,
n_heads: int = 8,
dropout: float = 0.0,
):
super().__init__()
self.norm_in = nn.LayerNorm(d_model)
self.mem_proj = nn.Linear(d_model, d_head)
self.q_proj = nn.Linear(d_model, d_head)
self.type_emb = nn.Embedding(len(QTYPES), d_head)
layer = nn.TransformerDecoderLayer(
d_head, n_heads, 4 * d_head, dropout, batch_first=True, norm_first=True
)
self.layers = nn.TransformerDecoder(layer, n_layers)
self.norm_out = nn.LayerNorm(d_head)
self.out = nn.Linear(d_head, 1)
self.prior = nn.Linear(d_model, 1)
def forward(self, hidden, attention_mask, opt_pos, opt_mask, opt_type):
h = self.norm_in(hidden.float())
opt_h = h.gather(1, opt_pos[..., None].expand(-1, -1, h.size(-1)))
q = self.q_proj(opt_h) + self.type_emb(opt_type)
x = self.layers(
q,
self.mem_proj(h),
tgt_key_padding_mask=~opt_mask,
memory_key_padding_mask=~attention_mask,
)
return (self.out(self.norm_out(x)) + self.prior(opt_h)).squeeze(-1)
def parse_questions(questions: dict[str, Any]) -> list[dict[str, Any]]:
out = []
for name, q in questions.items():
qtype = q.get("type")
if qtype not in QTYPES:
raise ValueError(f"์งˆ๋ฌธ {name}: type์€ {QTYPES} ์ค‘ ํ•˜๋‚˜ (๋ฐ›์€ ๊ฐ’: {qtype})")
if qtype == "noul":
options = list(NOUL_OPTIONS)
else:
options = [str(o) for o in q.get("options") or []]
if len(options) < 2 or len(set(options)) != len(options):
raise ValueError(f"์งˆ๋ฌธ {name}: ์„œ๋กœ ๋‹ค๋ฅธ ์„ ํƒ์ง€๊ฐ€ 2๊ฐœ ์ด์ƒ ํ•„์š”ํ•˜๋‹ค")
out.append({"name": name, "type": qtype, "instructions": q.get("instructions", ""),
"criteria": q.get("criteria", ""), "options": options}) # fmt: skip
if not out:
raise ValueError("์งˆ๋ฌธ์ด ์—†๋‹ค")
return out
def _ids(tok, text: str) -> list[int]:
return tok(text, add_special_tokens=False)["input_ids"]
def _cut_state(tok, s_all: list[int], budget: int, fmt: str) -> list[int]:
"""v1์€ ์ƒํƒœ ๋’ค๋ฅผ ์ž๋ฅด๊ณ , v2๋Š” ์•ž 1/4๊ณผ ๋’ค 3/4๋ฅผ ๋‚จ๊ฒจ ๊ฐ€์šด๋ฐ์— ์ค„์ž„ ํ‘œ์‹œ๋ฅผ ๋„ฃ๋Š”๋‹ค (์ตœ์‹  ๋ฐœํ™”ยท๊ธ€์˜ ๋์„ ์ง€ํ‚จ๋‹ค)."""
if len(s_all) <= budget:
return s_all
if fmt == "v1":
return s_all[:budget]
mark = _ids(tok, TRUNC_MARK)
keep = max(0, budget - len(mark))
head = keep // 4
return s_all[:head] + mark + s_all[len(s_all) - (keep - head) :]
def encode(
tok, state: Any, questions: dict[str, Any], max_len: int, fmt: str = "v1"
) -> dict[str, Any]:
"""[์ƒํƒœ ๋จธ๋ฆฌ๋ง + ์ƒํƒœ] [์งˆ๋ฌธ 1 ๋จธ๋ฆฌ๋งยท์ง€์‹œ๋ฌธยท๊ธฐ์ค€ยท์„ ํƒ์ง€ ์ค„ (+ v2 ๋‹ต ์ž๋ฆฌ)] [์งˆ๋ฌธ 2 ...] โ†’ ํ† ํฐ id์™€ ์œ„์น˜.
v2: ์„ ํƒ์ง€ ์ค„ `A. ์„ ํƒ์ง€`, ์„ ํƒ์ง€ ๋’ค `๋‹ต:` (์„ ํƒ์ง€ 26๊ฐœ ์ดํ•˜์ผ ๋•Œ). v2chat: ์ฑ„ํŒ… ํ˜•์‹์œผ๋กœ ๊ฐ์‹ธ๊ณ  ๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ์€
์–ด์‹œ์Šคํ„ดํŠธ ์ฐจ๋ก€ ์‹œ์ž‘์—์„œ ๋‹ตํ•œ๋‹ค. ํ•™์Šต ์ฝ”๋“œ(sysone.encode)์™€ ๊ฐ™์€ ๊ทœ์น™์ด๋‹ค.
"""
qs = parse_questions(questions)
v2, after = fmt != "v1", fmt == "v3chat"
chat = fmt in ("v2chat", "v3chat", "v4chat", "v4chat-nothink")
turns, in_answer = fmt in ("v4chat", "v4chat-nothink"), False
close = CHAT_CLOSE_NOTHINK if fmt == "v4chat-nothink" else CHAT_CLOSE
prefix = [t for t in (tok.cls_token_id, tok.bos_token_id) if t is not None][:1]
q_ids: list[int] = []
opt_rel, opt_q, opt_type, slot_rel, n_letters = [], [], [], [], []
for qi, q in enumerate(qs):
head = f"\n### ์งˆ๋ฌธ {qi + 1}: {q['name']} [{TYPE_HINT[q['type']]}]\n{q['instructions']}\n"
if q["criteria"]:
head += f"๊ธฐ์ค€: {q['criteria']}\n"
if in_answer:
q_ids += _ids(tok, REOPEN)
in_answer = False
q_ids += _ids(tok, head)
lm = v2 and len(q["options"]) <= len(LETTERS)
for j, opt in enumerate(q["options"]):
shown = NOUL_SHOWN[opt] if v2 and q["type"] == "noul" else opt
q_ids += _ids(tok, f"{LETTERS[j]}. {shown}\n" if lm else f"- {shown}\n")
opt_rel.append(len(q_ids) - 1)
opt_q.append(qi)
opt_type.append(QTYPES.index(q["type"]))
if lm and turns:
q_ids += _ids(tok, LETTER_HINT + close)
slot_rel.append(len(q_ids) - 1)
in_answer = True
elif lm and after:
slot_rel.append(AFTER)
elif lm and chat and qi == len(qs) - 1:
q_ids += _ids(tok, LETTER_HINT)
slot_rel.append(TAIL)
elif lm:
q_ids += _ids(tok, ANSWER_CUE)
slot_rel.append(len(q_ids) - 1)
else:
slot_rel.append(-1)
n_letters.append(len(q["options"]) if lm else 0)
a_ids: list[int] = []
lm_qs = [qi for qi, p in enumerate(slot_rel) if p == AFTER]
if lm_qs:
q_ids += _ids(tok, LETTER_HINT if len(lm_qs) == 1 else ALL_HINT)
if len(lm_qs) == 1:
slot_rel[lm_qs[0]] = TAIL
else:
for i, qi in enumerate(lm_qs):
a_ids += _ids(tok, ("\n" if i else "") + f"์งˆ๋ฌธ {qi + 1}:")
slot_rel[qi] = AFTER - len(a_ids)
open_ids = _ids(tok, CHAT_OPEN) if chat else []
close_ids = _ids(tok, CHAT_CLOSE) if chat and not turns else []
header = _ids(tok, STATE_HEADER)
budget = (max_len - len(prefix) - len(open_ids) - len(header) - len(q_ids) - len(close_ids)
- len(a_ids)) # fmt: skip
if budget < MIN_STATE_TOKENS:
raise ValueError(
f"์งˆ๋ฌธยท์„ ํƒ์ง€๋งŒ {len(q_ids)} ํ† ํฐ์ด๋ผ max_len={max_len}์— ์ƒํƒœ๊ฐ€ ๋“ค์–ด๊ฐˆ ์ž๋ฆฌ๊ฐ€ ์—†๋‹ค"
)
text = state if isinstance(state, str) else json.dumps(state, ensure_ascii=False, indent=1)
s_all = _ids(tok, text)
s_ids = _cut_state(tok, s_all, budget, fmt)
base = len(prefix) + len(open_ids) + len(header) + len(s_ids)
input_ids = prefix + open_ids + header + s_ids + q_ids + close_ids + a_ids
turn_end = base + len(q_ids) + len(close_ids)
def slot(p: int) -> int:
if p >= 0:
return base + p
if p == TAIL:
return turn_end - 1
return turn_end + (AFTER - p) - 1 if p < AFTER else -1
slot_pos = [slot(p) for p in slot_rel]
return {"input_ids": input_ids, "opt_pos": [base + p for p in opt_rel], "opt_q": opt_q,
"opt_type": opt_type, "slot_pos": slot_pos, "n_letters": n_letters, "questions": qs,
"truncated": len(s_all) > budget} # fmt: skip
def letter_logits(hidden, slot_pos, emb, letter_ids):
"""๋‹ต ์ž๋ฆฌ hidden (B, Q, d) ร— ์ถœ๋ ฅ์ธต(= ์ž…๋ ฅ ์ž„๋ฒ ๋”ฉ)์˜ ๊ธ€์ž ํ–‰ โ†’ ๊ธ€์ž logit (B, Q, 26). ๋ณ€ํ˜•๋ผ๋ฆฌ logsumexp."""
d = hidden.size(-1)
h = hidden.gather(1, slot_pos.clamp(min=0)[..., None].expand(-1, -1, d)).float()
w = emb[letter_ids.reshape(-1)].float().view(*letter_ids.shape, d)
return torch.einsum("bqd,kld->bqkl", h, w).logsumexp(2)
def temperature_bucket(qtype: str, n_opts: int, head_only: bool = False) -> str:
"""์˜จ๋„ ๊ตฌ๊ฐ„. ํ˜•์‹ v2์—์„œ ๊ธ€์ž ๊ฒฝ๋กœ๊ฐ€ ์—†๋Š” ์งˆ๋ฌธ(์„ ํƒ์ง€ 27๊ฐœ ์ด์ƒ)์€ ๋”ฐ๋กœ ์“ด๋‹ค."""
if head_only:
return f"{qtype}:27+"
return f"{qtype}:{n_opts if n_opts <= 2 else '3-5' if n_opts <= 5 else '6+'}"
def graph_buckets(max_len: int) -> list[int]:
"""CUDA ๊ทธ๋ž˜ํ”„ ๊ธธ์ด ๊ตฌ๊ฐ„: 512๊นŒ์ง€ 32์”ฉ, 1024๊นŒ์ง€ 64์”ฉ, ๊ทธ ๋’ค๋Š” 128์”ฉ max_len์„ ๋ฎ์„ ๋•Œ๊นŒ์ง€.
์ž…๋ ฅ์„ ๊ตฌ๊ฐ„ ๊ธธ์ด๊นŒ์ง€ ์ฑ„์›Œ ๊ณ„์‚ฐํ•˜๋ฏ€๋กœ ๊ตฌ๊ฐ„์ด ์ด˜์ด˜ํ• ์ˆ˜๋ก ๋œ ๋‚ญ๋น„ํ•œ๋‹ค (ํ˜•์‹ v4chat์—์„œ 221ํ† ํฐ์ด 256์ด ์•„๋‹ˆ๋ผ 224๋กœ).
"""
out, n = [], 0
while n < max_len:
n += 32 if n < 512 else 64 if n < 1024 else 128
out.append(n)
return out
class _GraphRunner:
"""๋ฐฐ์น˜ 1 ๋ฐฑ๋ณธ forward๋ฅผ ๊ธธ์ด ๊ตฌ๊ฐ„๋งˆ๋‹ค CUDA ๊ทธ๋ž˜ํ”„๋กœ ํ•œ ๋ฒˆ ๋…นํ™”ํ•ด ๋‘๊ณ  ๋‹ค์‹œ ์žฌ์ƒํ•œ๋‹ค (`enable_cuda_graphs`).
- eager ์‹คํ–‰์€ ์ธต๋งˆ๋‹ค ์ž‘์€ ์ปค๋„์„ ํ•˜๋‚˜์”ฉ ๋ถ€๋ฅธ๋‹ค (4B๋Š” forward ํ•œ ๋ฒˆ์— 2์ฒœ์—ฌ ๊ฐœ). ์งง์€ ์ž…๋ ฅ์—์„œ๋Š” GPU ๊ณ„์‚ฐ๋ณด๋‹ค
์ด ํ˜ธ์ถœ ๋น„์šฉ์ด ์ปค์„œ ๊ธธ์ด์™€ ์ƒ๊ด€์—†์ด ์ง€์—ฐ์ด ๊ฑฐ์˜ ๊ฐ™์•˜๋‹ค. ์žฌ์ƒ์€ ํ˜ธ์ถœ ๋น„์šฉ์„ ์—†์•ค๋‹ค
- ์ž…๋ ฅ์€ ๊ตฌ๊ฐ„ ๊ธธ์ด๊นŒ์ง€ ์˜ค๋ฅธ์ชฝ์„ ์ฑ„์šด๋‹ค. ์ธ๊ณผ ๋ชจ๋ธ์ด๋ผ ์ฑ„์šด ํ† ํฐ์€ ์•ž์ชฝ hidden์„ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค (์ˆ˜์น˜ ์˜ค์ฐจ๋งŒ ๋‹ค๋ฅด๋‹ค)
- ๊ทธ๋ž˜ํ”„๋“ค์€ ๋ฉ”๋ชจ๋ฆฌ ํ’€ ํ•˜๋‚˜๋ฅผ ๊ฐ™์ด ์“ด๋‹ค. ์ถœ๋ ฅ ๋ฒ„ํผ๋„ ๊ณต์œ ๋ผ ์žฌ์ƒ๋ถ€ํ„ฐ ํ—ค๋“œ ๊ณ„์‚ฐ๊นŒ์ง€ ์ž ๊ธˆ ์•ˆ์—์„œ ํ•œ๋‹ค
"""
def __init__(self, backbone: nn.Module, max_len: int):
self.backbone = backbone
self.device = next(backbone.parameters()).device
self.buckets = graph_buckets(max_len)
self.pool = torch.cuda.graph_pool_handle()
self.graphs: dict[int, tuple[Any, torch.Tensor, torch.Tensor]] = {}
self.lock = threading.Lock()
def capture(self, length: int) -> None:
static = torch.zeros(1, length, dtype=torch.long, device=self.device)
stream = torch.cuda.Stream(self.device)
stream.wait_stream(torch.cuda.current_stream(self.device))
with torch.no_grad(), torch.cuda.stream(stream):
for _ in range(2): # Triton ์ปค๋„ ์ž๋™ ์กฐ์ •๊ณผ cuBLAS ์ค€๋น„๋ฅผ ๋…นํ™” ์ „์— ๋๋‚ธ๋‹ค
self.backbone(input_ids=static, use_cache=False)
torch.cuda.current_stream(self.device).wait_stream(stream)
graph = torch.cuda.CUDAGraph()
with torch.no_grad(), torch.cuda.graph(graph, pool=self.pool):
out = self.backbone(input_ids=static, use_cache=False).last_hidden_state
self.graphs[length] = (graph, static, out)
def __call__(self, input_ids: torch.Tensor) -> torch.Tensor | None:
"""(1, n) โ†’ (1, n, d) hidden. ๊ฐ€์žฅ ๊ธด ๊ตฌ๊ฐ„๋ณด๋‹ค ๊ธธ๋ฉด None์ด๋‹ค (ํ˜ธ์ถœํ•œ ์ชฝ์ด eager๋กœ ๊ณ„์‚ฐํ•œ๋‹ค)."""
n = input_ids.size(1)
length = next((b for b in self.buckets if b >= n), None)
if input_ids.size(0) != 1 or length is None:
return None
if length not in self.graphs:
self.capture(length)
graph, static, out = self.graphs[length]
static[:, :n].copy_(input_ids)
static[:, n:].zero_()
graph.replay()
return out[:, :n]
class SysOneForDecision(PreTrainedModel):
config_class = SysOneConfig
base_model_prefix = "sysone"
# ํ—ค๋“œ๋Š” float32๋กœ ํ•™์Šตํ–ˆ๋‹ค. _keep_in_fp32_modules๋Š” fp16์—๋งŒ ์ ์šฉ๋˜์–ด bf16๊นŒ์ง€ ๋ง‰์œผ๋ ค๋ฉด _strict๊ฐ€ ํ•„์š”ํ•˜๋‹ค
_keep_in_fp32_modules: ClassVar[list[str]] = ["head"]
_keep_in_fp32_modules_strict: ClassVar[list[str]] = ["head"]
_supports_sdpa = True
def __init__(self, config: SysOneConfig):
super().__init__(config)
text = dict(config.text_config)
self.backbone = AutoModel.from_config(AutoConfig.for_model(text.pop("model_type"), **text))
self.head = DecisionHead(self.backbone.config.hidden_size, **config.head)
self._graphs: _GraphRunner | None = None
self._letter_ids: torch.Tensor | None = None
self.post_init()
def forward(self, input_ids, attention_mask, opt_pos, opt_mask, opt_type):
hidden = self.backbone(
input_ids=input_ids, attention_mask=attention_mask.long(), use_cache=False
).last_hidden_state
return self.head(hidden, attention_mask, opt_pos, opt_mask, opt_type)
def enable_cuda_graphs(self, capture_all: bool = True) -> SysOneForDecision:
"""decide()์˜ ๋ฐฑ๋ณธ์„ ๊ธธ์ด ๊ตฌ๊ฐ„๋ณ„ CUDA ๊ทธ๋ž˜ํ”„๋กœ ๋Œ๋ฆฐ๋‹ค (๋ฐฐ์น˜ 1).
GPU๋กœ ์˜ฎ๊ธด ๋’ค ๋ถ€๋ฅธ๋‹ค. capture_all์ด๋ฉด ๋ชจ๋“  ๊ตฌ๊ฐ„์„ ์ง€๊ธˆ ๋…นํ™”ํ•˜๊ณ  (A100์—์„œ ์•ฝ 5์ดˆ, ์ถ”๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ 1GiB ์•ˆ์ชฝ),
์•„๋‹ˆ๋ฉด ์ฒ˜์Œ ์“ฐ๋Š” ๊ตฌ๊ฐ„์„ ๊ทธ๋•Œ ๋…นํ™”ํ•œ๋‹ค. ์žฅ์น˜ยทdtypeยท๊ฐ€์ค‘์น˜๋ฅผ ๋ฐ”๊พธ๋ฉด ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์•ผ ํ•œ๋‹ค.
"""
device = next(self.backbone.parameters()).device
if device.type != "cuda":
raise RuntimeError(
"CUDA ๊ทธ๋ž˜ํ”„๋Š” CUDA ์žฅ์น˜์—์„œ๋งŒ ์“ธ ์ˆ˜ ์žˆ๋‹ค. model.to('cuda') ๋’ค์— ๋ถ€๋ฅผ ๊ฒƒ"
)
runner = _GraphRunner(self.backbone, self.config.max_len)
if capture_all:
for length in runner.buckets:
runner.capture(length)
self._graphs = runner
return self
def disable_cuda_graphs(self) -> None:
self._graphs = None
@torch.no_grad()
def decide(
self, tokenizer, state: Any, questions: dict[str, Any], max_len: int | None = None
) -> dict[str, Any]:
"""์งˆ๋ฌธ๋งˆ๋‹ค ์„ ํƒ์ง€๋ณ„ ํ™•๋ฅ (๋ณด์ • ์˜จ๋„ ์ ์šฉ).
choiceยทscore: answer(๊ฐ€์žฅ ๋†’์€ ์„ ํƒ์ง€)ยทconfidence, score๋Š” expected(์„ ํƒ์ง€ ์ˆœ๋ฒˆ 1๋ถ€ํ„ฐ์˜ ๊ธฐ๋Œ“๊ฐ’)๋„ ์ค€๋‹ค.
noul: answer(bool)ยทp_true. truncated๋Š” ์ƒํƒœ๊ฐ€ max_len์„ ๋„˜์–ด ์ž˜๋ ธ๋Š”์ง€๋‹ค.
์ž…๋ ฅ ํ˜•์‹ v2๋ถ€ํ„ฐ๋Š” ๊ธ€์ž ๊ฒฝ๋กœ๊ฐ€ ์žˆ๋Š” ์งˆ๋ฌธ์— paths(๊ฒฐ์ • ํ—ค๋“œยท์–ธ์–ด ๋ชจ๋ธ ๊ธ€์ž์˜ ๊ฒฝ๋กœ๋ณ„ ํ™•๋ฅ , ๋ณด์ • ์ „)๋„ ์ค€๋‹ค.
"""
if (
self.head.out.weight.dtype != torch.float32
): # ๋‹ค๋ฅธ dtype์œผ๋กœ ์˜ฎ๊ฒจ์กŒ์–ด๋„ ํ—ค๋“œ๋Š” float32๋กœ ๊ณ„์‚ฐํ•œ๋‹ค
self.head.float()
fmt = getattr(self.config, "input_format", "v1")
enc = encode(tokenizer, state, questions, max_len or self.config.max_len, fmt)
dev = self.head.out.weight.device
ids = torch.tensor([enc["input_ids"]], device=dev)
n = len(enc["opt_pos"])
mask = torch.ones_like(ids, dtype=torch.bool)
pos = torch.tensor([enc["opt_pos"]], device=dev)
opt_mask = torch.ones(1, n, dtype=torch.bool, device=dev)
opt_type = torch.tensor([enc["opt_type"]], device=dev)
use_lm = fmt != "v1" and any(enc["n_letters"])
runner = self._graphs if self._graphs is not None and dev == self._graphs.device else None
letters = None
with runner.lock if runner is not None else contextlib.nullcontext():
hidden = runner(ids) if runner is not None else None
if hidden is None:
hidden = self.backbone(
input_ids=ids, attention_mask=mask.long(), use_cache=False
).last_hidden_state
logits = self.head(hidden, mask, pos, opt_mask, opt_type)[0].float().cpu()
if use_lm:
if self._letter_ids is None or self._letter_ids.device != dev:
self._letter_ids = torch.tensor(self.config.letter_ids, device=dev)
slots = torch.tensor([enc["slot_pos"]], device=dev)
emb = self.backbone.get_input_embeddings().weight
letters = letter_logits(hidden, slots, emb, self._letter_ids)[0].float().cpu()
temps = self.config.temperatures or {}
w = float(getattr(self.config, "lm_weight", 0.5))
answers = {}
for qi, q in enumerate(enc["questions"]):
cols = [j for j, x in enumerate(enc["opt_q"]) if x == qi]
n_let = enc["n_letters"][qi] if fmt != "v1" else None
final, paths = logits[cols], None
if n_let and letters is not None:
lm = letters[qi, :n_let]
paths = {"head": final.softmax(-1).tolist(), "lm": lm.softmax(-1).tolist()}
final = (1 - w) * final.log_softmax(-1) + w * lm.log_softmax(-1)
t = temps.get(temperature_bucket(q["type"], len(cols), n_let == 0), temps.get("*", 1.0))
probs = (final / t).softmax(-1).tolist()
out: dict[str, Any] = {"probs": dict(zip(q["options"], (round(p, 4) for p in probs)))}
if paths:
out["paths"] = {k: dict(zip(q["options"], (round(p, 4) for p in v)))
for k, v in paths.items()} # fmt: skip
if q["type"] == "noul":
out |= {"answer": probs[0] >= 0.5, "p_true": round(probs[0], 4)}
else:
best = max(range(len(probs)), key=probs.__getitem__)
out |= {"answer": q["options"][best], "confidence": round(probs[best], 4)}
if q["type"] == "score":
out["expected"] = round(sum((k + 1) * p for k, p in enumerate(probs)), 3)
answers[q["name"]] = out
return {"answers": answers, "truncated": enc["truncated"]}
def decide_record(
self, tokenizer, record: dict[str, Any], max_len: int | None = None
) -> dict[str, Any]:
"""{"id", "state", "questions"} ๋ ˆ์ฝ”๋“œ ํ•˜๋‚˜ (ํ•™์Šต ๋ฐ์ดํ„ฐ์™€ ๊ฐ™์€ ํ˜•์‹)."""
return {
"id": record.get("id"),
**self.decide(tokenizer, record["state"], record["questions"], max_len),
}