File size: 5,146 Bytes
59cede3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# -*- coding: utf-8 -*-
"""VERITAS 부착 핸들러 — JGOS-398B-fp8 (9-팀장 2026-09-17)

무엇을 하나
  한 번의 호출로 **답과 속 신호를 같이** 돌려준다.
  속 신호 = 마지막 층·마지막 토큰의 은닉벡터(가공 안 된 원본값).
  이것이 VERITAS 가 읽는 바로 그 값이며, **토큰을 한 개도 더 쓰지 않는다**
  (이미 계산된 것을 꺼내 올 뿐이다).

두 가지 모드
  mode="generate" : 답 생성 + 그 답까지 포함한 은닉 (문항 풀이용)
  mode="embed"    : 주어진 글자열의 은닉만 (이미 뽑아 둔 답을 채점할 때)

🔴 규율
  - 잘림(length)을 결과에 반드시 표시한다. 자원 소진을 오답으로 세지 않기 위해서다.
  - 은닉은 정규화하지 않는다. 정규화하면 VERITAS 가 쓰는 크기 정보가 날아간다.
  - 생성분이 reasoning 쪽으로 빠지는 템플릿이면 그것까지 합쳐 돌려준다(버리지 않는다).
"""
from __future__ import annotations

from typing import Any, Dict, List

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer


class EndpointHandler:
    def __init__(self, path: str = ""):
        self.tok = AutoTokenizer.from_pretrained(path)
        if self.tok.pad_token is None:
            self.tok.pad_token = self.tok.eos_token
        self.tok.padding_side = "left"          # 🔴 디코더 모델 배치의 필수 조건
        self.model = AutoModelForCausalLM.from_pretrained(
            path, dtype="auto", device_map="auto"
        ).eval()
        cfg = self.model.config
        cfg = cfg.get_text_config() if hasattr(cfg, "get_text_config") else cfg
        self.n_layers = cfg.num_hidden_layers

    # ---- 내부: 마지막 층·마지막 토큰 은닉 ------------------------------
    @torch.no_grad()
    def _hidden(self, ids) -> List[List[float]]:
        out = self.model(**ids, output_hidden_states=True, use_cache=False)
        h = out.hidden_states[-1]                       # 마지막 층
        mask = ids["attention_mask"]
        last = mask.sum(dim=1) - 1                      # 좌패딩이라 실제 마지막 위치
        picked = h[torch.arange(h.size(0), device=h.device), last, :]
        return picked.float().cpu().tolist()

    def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
        inputs = data.get("inputs")
        if isinstance(inputs, str):
            inputs = [inputs]
        p = data.get("parameters") or {}
        mode = p.get("mode", "generate")

        # ── 은닉만 ──────────────────────────────────────────────────
        if mode == "embed":
            ids = self.tok(inputs, return_tensors="pt", padding=True,
                           truncation=True, max_length=p.get("max_length", 32768))
            ids = {k: v.to(self.model.device) for k, v in ids.items()}
            return {"hidden": self._hidden(ids), "layer": self.n_layers,
                    "normalized": False}

        # ── 생성 + 은닉 ─────────────────────────────────────────────
        max_new = int(p.get("max_new_tokens", 16384))
        n = int(p.get("n", 1))
        temp = float(p.get("temperature", 0.0))
        texts = []
        for s in inputs:
            if p.get("chat", True):
                msgs = [{"role": "user", "content": s}]
                kw = {}
                if p.get("enable_thinking") is not None:
                    kw["enable_thinking"] = p["enable_thinking"]
                try:
                    texts.append(self.tok.apply_chat_template(
                        msgs, tokenize=False, add_generation_prompt=True, **kw))
                except TypeError:
                    texts.append(self.tok.apply_chat_template(
                        msgs, tokenize=False, add_generation_prompt=True))
            else:
                texts.append(s)
        enc = self.tok(texts, return_tensors="pt", padding=True)
        enc = {k: v.to(self.model.device) for k, v in enc.items()}
        gen_kw = dict(max_new_tokens=max_new, pad_token_id=self.tok.pad_token_id)
        if temp > 0:
            gen_kw.update(do_sample=True, temperature=temp, num_return_sequences=n)
        else:
            gen_kw.update(do_sample=False)
        with torch.no_grad():
            g = self.model.generate(**enc, **gen_kw)
        plen = enc["input_ids"].shape[1]
        new = g[:, plen:]
        outs = self.tok.batch_decode(new, skip_special_tokens=True)
        ntok = (new != self.tok.pad_token_id).sum(dim=1).tolist()
        # 🔴 잘림 표시 — 자원 소진을 오답으로 세지 않기 위해 결과에 남긴다
        hit_cap = [int(t >= max_new - 2) for t in ntok]

        att = torch.ones_like(g)
        att[g == self.tok.pad_token_id] = 0
        hid = self._hidden({"input_ids": g, "attention_mask": att})
        return {"generated": outs, "hidden": hid, "n_tokens": ntok,
                "hit_cap": hit_cap, "layer": self.n_layers, "normalized": False}