backfire-borderline / bfb232.py
skywolf46's picture
feat: 모델 데이터 업로드
af5c818 verified
Raw History Blame Contribute Delete
12 kB
"""BFB232 prototype inference. MIT license; see LICENSE and THIRD_PARTY_NOTICES.md."""
from __future__ import annotations
import argparse
import bisect
import contextlib
import json
import math
import os
from pathlib import Path
import threading
import time
# Set before the first CUDA operation. Matches the reference experiment.
os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
import torch
from safetensors.torch import load_file
from transformers import AutoModel, AutoTokenizer
import bfb232_text as textio
class BoundaryHead(torch.nn.Module):
"""Frozen B187/B232 'left' binary head; CLOSE score = logit[1] - logit[0]."""
def __init__(self):
super().__init__()
self.base = torch.nn.Sequential(
torch.nn.LayerNorm(768), torch.nn.Linear(768, 96),
torch.nn.GELU(), torch.nn.Linear(96, 2),
)
self.extra = torch.nn.Sequential(
torch.nn.LayerNorm(384), torch.nn.Linear(384, 64),
torch.nn.GELU(), torch.nn.Linear(64, 2),
)
def forward(self, features):
return self.base(features[:, :768]) + self.extra(features[:, 768:1152])
def make_window(tokenizer, text, positions, left, right):
"""Exact bfb130 indexed token anchors and 256-character side pools."""
encoding = tokenizer(text[left:right], return_offsets_mapping=True,
add_special_tokens=True, truncation=False)
offsets = encoding.pop("offset_mapping")
if len(encoding["input_ids"]) > 8192:
raise ValueError("Retokenized window exceeds the reference 8192-token limit")
valid = [i for i, (a, b) in enumerate(offsets) if b > a]
starts = [offsets[i][0] for i in valid]
if starts != sorted(starts):
raise ValueError("Non-monotone tokenizer offsets")
anchors, after, sides = [], [], []
for position in positions:
pos = position - left
at = bisect.bisect_left(starts, pos)
if at == 0:
raise ValueError("Candidate has no preceding token")
a = valid[at - 1]
b = valid[at] if at < len(valid) else a
lo = bisect.bisect_left(starts, max(0, pos - 256))
hi = bisect.bisect_left(starts, pos + 256)
anchors.append(a)
after.append(b)
sides.append((valid[lo:at] or [a], valid[at:hi] or [b]))
return dict(encoding=dict(encoding), anchors=anchors, after=after,
sides=sides, left=left, right=right)
def inference_windows(tokenizer, text, positions):
"""Canonical text/positions; 7900-token lattice, stride 6500, interior owner."""
encoded = tokenizer(text, add_special_tokens=False, return_offsets_mapping=True,
truncation=False, verbose=False)
offsets = encoded["offset_mapping"]
if not positions:
return [], len(offsets)
if len(offsets) <= 7900:
w = make_window(tokenizer, text, positions, 0, len(text))
w["indices"] = list(range(len(positions)))
return [w], len(offsets)
starts = sorted(set(list(range(0, len(offsets) - 7900 + 1, 6500))
+ [len(offsets) - 7900]))
spans = [(offsets[s][0] if s else 0,
offsets[s + 7899][1] if s + 7900 < len(offsets) else len(text))
for s in starts]
groups = {}
for i, p in enumerate(positions):
choices = [j for j, (a, b) in enumerate(spans) if a < p <= b]
if not choices:
raise ValueError("Candidate lies outside the token windows")
owner = max(choices, key=lambda j: min(p - spans[j][0], spans[j][1] - p))
groups.setdefault(owner, []).append(i)
result = []
for j, indices in sorted(groups.items()):
w = make_window(tokenizer, text, [positions[i] for i in indices], *spans[j])
w["indices"] = indices
result.append(w)
return result, len(offsets)
def features_from_hidden(hidden, window):
"""Preserves reference FP32 pooling and operation order."""
pair = torch.cat([hidden[window["anchors"]], hidden[window["after"]]], -1).float()
left = torch.stack([hidden[a].float().mean(0) for a, _ in window["sides"]])
right = torch.stack([hidden[b].float().mean(0) for _, b in window["sides"]])
return torch.cat([pair, left, right, right - left, right * left], -1)
class BFB232:
def __init__(self, tokenizer, encoder, head, config, device):
self.tokenizer, self.encoder, self.head = tokenizer, encoder, head
self.config, self.device = config, torch.device(device)
self.thresholds = dict(config["thresholds"])
self.lock = threading.Lock()
@classmethod
def from_pretrained(cls, model_path, *, device="auto", revision=None,
local_files_only=False, token=None):
"""Load a local release folder or Hub ID; never executes Hub Python code.
For a Hub ID, pin revision to its commit hash. CPU uses FP32; the
reference CUDA path uses BF16 autocast with FP32 stored weights.
"""
folder = Path(model_path)
if not folder.is_dir():
from huggingface_hub import snapshot_download
folder = Path(snapshot_download(
repo_id=str(model_path), revision=revision, token=token,
local_files_only=local_files_only,
allow_patterns=["bfb232_config.json", "encoder/*.json",
"encoder/*.safetensors", "tokenizer/*.json",
"boundary_head.safetensors"],
))
config = json.loads((folder / "bfb232_config.json").read_text("utf-8"))
if config["format_version"] != 1 or config["input_contract"] != textio.VERSION:
raise ValueError("Unsupported BFB232 release/input contract")
if device == "auto":
device = "cuda" if torch.cuda.is_available() and torch.cuda.is_bf16_supported(including_emulation=False) else "cpu"
device = torch.device(device)
if device.type not in ("cpu", "cuda"):
raise ValueError("Only CPU FP32 and CUDA BF16 are supported")
if device.type == "cuda":
if not torch.cuda.is_available():
raise ValueError("CUDA is unavailable; use device='cpu'")
with torch.cuda.device(device):
if not torch.cuda.is_bf16_supported(including_emulation=False):
raise ValueError("CUDA BF16 support is required; use device='cpu'")
torch.set_num_threads(4)
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
torch.backends.cuda.matmul.allow_tf32 = False
torch.backends.cudnn.allow_tf32 = False
tokenizer = AutoTokenizer.from_pretrained(folder / "tokenizer", local_files_only=True)
encoder, info = AutoModel.from_pretrained(
folder / "encoder", local_files_only=True, use_safetensors=True,
dtype=torch.float32, attn_implementation="sdpa", output_loading_info=True,
)
if any(info.get(k) for k in ("missing_keys", "unexpected_keys", "mismatched_keys", "error_msgs")):
raise ValueError(f"Encoder state mismatch: {info}")
head = BoundaryHead()
head.load_state_dict(load_file(str(folder / "boundary_head.safetensors")), strict=True)
encoder.to(device).eval().requires_grad_(False)
head.to(device).eval().requires_grad_(False)
return cls(tokenizer, encoder, head, config, device)
def segment(self, text, language):
"""Return source-preserving segments and original Unicode code-point offsets.
Language must be 'ko' or 'en'; no automatic language detection or
minimum/maximum segment length is imposed. Empty input returns [].
"""
if not isinstance(text, str):
raise TypeError("text must be a string")
if language not in self.thresholds:
raise ValueError("language must be 'ko' or 'en'")
with self.lock:
return self._segment(text, language)
def _segment(self, text, language):
started = time.perf_counter()
canonical = textio.normalize(text)
positions = textio.candidates(canonical.text, language)
original_positions = [canonical.to_original[p] for p in positions]
windows, input_tokens = inference_windows(self.tokenizer, canonical.text, positions)
scores = [None] * len(positions)
processed_tokens = 0
autocast = (torch.autocast("cuda", dtype=torch.bfloat16)
if self.device.type == "cuda" else contextlib.nullcontext())
with torch.inference_mode(), autocast:
for w in windows:
inputs = self.tokenizer.pad([w["encoding"]], padding=True, return_tensors="pt")
inputs = {k: v.to(self.device) for k, v in inputs.items()}
hidden = self.encoder(**inputs).last_hidden_state[0]
logits = self.head(features_from_hidden(hidden, w))
values = (logits[:, 1] - logits[:, 0]).float().cpu().tolist()
for i, score in zip(w["indices"], values):
if scores[i] is not None or not math.isfinite(score):
raise RuntimeError("Invalid/duplicate boundary score")
scores[i] = score
processed_tokens += len(w["encoding"]["input_ids"])
if any(s is None for s in scores):
raise RuntimeError("Missing boundary scores")
cuts = [p for p, s in zip(original_positions, scores) if s >= self.thresholds[language]]
segments, left = [], 0
for right in cuts + ([len(text)] if text else []):
segments.append(dict(index=len(segments) + 1, start=left, end=right,
text=text[left:right], boundary="END" if right == len(text) else "CLOSE"))
left = right
if "".join(s["text"] for s in segments) != text:
raise RuntimeError("Source preservation failed")
return dict(model="BFB232", language=language, segments=segments,
source_preserved=True, input_contract=textio.VERSION,
candidate_positions=original_positions, scores=scores,
threshold=self.thresholds[language],
precision="cuda-bf16" if self.device.type == "cuda" else "cpu-fp32",
metrics=dict(input_chars=len(text), input_tokens=input_tokens,
processed_tokens=processed_tokens, windows=len(windows),
candidates=len(positions), segment_count=len(segments),
total_seconds=time.perf_counter() - started))
def main():
parser = argparse.ArgumentParser(description="BFB232 semantic segmentation")
parser.add_argument("--model", default=str(Path(__file__).parent))
parser.add_argument("--revision", help="Hub commit hash when --model is a Hub ID")
parser.add_argument("--device", default="auto", choices=["auto", "cpu", "cuda"])
parser.add_argument("--language", required=True, choices=["ko", "en"])
parser.add_argument("--input", required=True, type=Path, help="UTF-8 text file")
parser.add_argument("--output", required=True, type=Path, help="New JSON file; refuses overwrite")
args = parser.parse_args()
if args.output.exists():
parser.error("Output exists; choose a new path")
# newline='' preserves CRLF/CR in the returned original-text slices.
with args.input.open(encoding="utf-8", newline="") as source:
text = source.read()
model = BFB232.from_pretrained(args.model, device=args.device, revision=args.revision)
result = model.segment(text, args.language)
with args.output.open("x", encoding="utf-8", newline="\n") as out:
json.dump(result, out, ensure_ascii=False, indent=2)
out.write("\n")
if __name__ == "__main__":
main()