speleoalex commited on
Commit
fc21794
·
verified ·
1 Parent(s): 0432a50

Ship the missing generation module and drop the stale bytecode

Browse files

generate.py imported physisml.generation, which was never uploaded: every download failed with ModuleNotFoundError. The file is byte-identical to the one already published in physisml-en-preview. The six __pycache__ entries were build residue and are removed.

physisml/__pycache__/__init__.cpython-312.pyc DELETED
Binary file (367 Bytes)
 
physisml/__pycache__/affect_state.cpython-312.pyc DELETED
Binary file (19.9 kB)
 
physisml/__pycache__/modulator.cpython-312.pyc DELETED
Binary file (9.55 kB)
 
physisml/__pycache__/tokenizer.cpython-312.pyc DELETED
Binary file (15.9 kB)
 
physisml/__pycache__/torch_model.cpython-312.pyc DELETED
Binary file (15.6 kB)
 
physisml/__pycache__/utils.cpython-312.pyc DELETED
Binary file (5.65 kB)
 
physisml/generation.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Shared generation path for PhysisML checkpoints.
3
+
4
+ This is the code that produces the answers quoted in the README, extracted so
5
+ that the repository's chat script (`standalone/chat.py`) and the script shipped
6
+ with the published weights (`huggingface/generate.py`) cannot drift apart. It
7
+ is a port of the generation path used during training
8
+ (`dynamic_model/exp_b/trainer.py`) and keeps the parts that shape the answers:
9
+
10
+ * optional affective modulation — confidence / pleasure / pain / fear shift
11
+ the logits at every step
12
+ * EOS suppressed before `min_tokens`, otherwise the model answers with a bare
13
+ end-of-sequence token
14
+ * soft stop on sentence-ending punctuation, tested on the DECODED token
15
+ because the vocabulary has multi-character tokens carrying the terminator
16
+ * output slots the tokenizer cannot decode are masked out
17
+
18
+ One thing this path cannot reproduce: the published exact-match scores were
19
+ measured with the stop threshold set from the length of the gold answer, which
20
+ is information a free-form prompt does not have. Expect the model to keep
21
+ talking past its answer more often here.
22
+ """
23
+ import re
24
+
25
+ import torch
26
+
27
+ from .utils import sample_top_k
28
+
29
+
30
+ def undecodable_mask(model, tok):
31
+ """
32
+ Mask over the output layer marking ids the tokenizer cannot turn back into
33
+ bytes. vocab_size is rounded up past the tokenizer's real size, so several
34
+ hundred slots decode to nothing; sampling one derails the whole answer.
35
+ """
36
+ vocab = getattr(tok, "vocab", None)
37
+ if not vocab:
38
+ return None
39
+ missing = [i for i in range(model.vocab_size) if i not in vocab]
40
+ if not missing:
41
+ return None
42
+ mask = torch.zeros(model.vocab_size, dtype=torch.bool)
43
+ mask[torch.tensor(missing, dtype=torch.long)] = True
44
+ return mask
45
+
46
+
47
+ def clean_response(text: str) -> str:
48
+ """
49
+ Strip the two noise artifacts the model learned: a leading '!' (it picked
50
+ up the terminator as a start token) and an echoed 'di:' from the prompt.
51
+ Conservative — leading noise only, never content.
52
+ """
53
+ t = re.sub(r"^[!\s]+", "", text.strip()).strip()
54
+ t = re.sub(r"(?:il|la|lo|le|gli|un|una)\s+di:\s*", "", t,
55
+ flags=re.IGNORECASE).strip()
56
+ t = re.sub(r"di:\s*", "", t, flags=re.IGNORECASE).strip()
57
+ t = re.sub(r"\s{2,}", " ", t).strip()
58
+ return t or text.strip()
59
+
60
+
61
+ def generate(model, tok, prompt: str, *, max_tokens: int, temperature: float,
62
+ top_k: int, min_tokens: int, stop_after: int, context: int,
63
+ modulator=None, affect=None, mask=None) -> str:
64
+ ids = tok.encode(prompt)[-(model.max_seq_len - 1):]
65
+ if not ids:
66
+ return ""
67
+ n_prompt = len(ids)
68
+ eos_id = tok.get_special_id(tok.EOS_TOKEN)
69
+
70
+ with torch.no_grad():
71
+ for step in range(max_tokens):
72
+ ctx = torch.tensor(ids[-context:], dtype=torch.long)
73
+ logits = model.forward(ctx)[-1]
74
+
75
+ if modulator is not None:
76
+ affect.update_from_logits(logits, model.vocab_size)
77
+ # The modulator applies the temperature itself, so the sampler
78
+ # is called with 1.0 afterwards. Greedy therefore cannot be
79
+ # expressed as temperature 0 here: it is top_k = 1, exactly as
80
+ # the training repo's own evaluation harness does it. Passing
81
+ # temperature 0 and leaving top_k at 40 sampled instead —
82
+ # 'cos è un cane?' answered animale, persona and luce on three
83
+ # consecutive runs of what claimed to be greedy decoding.
84
+ logits = modulator.modulate(
85
+ logits, temperature if temperature > 0 else 1.0,
86
+ response_len = len(ids) - n_prompt,
87
+ eos_min_len = min_tokens + 1,
88
+ )
89
+ sample_temperature = 1.0
90
+ sample_k = 1 if temperature <= 0 else top_k
91
+ else:
92
+ sample_temperature = temperature
93
+ sample_k = top_k
94
+
95
+ logits = logits.clone()
96
+ if eos_id is not None and (len(ids) - n_prompt) < min_tokens \
97
+ and eos_id < logits.shape[0]:
98
+ logits[eos_id] = float("-inf")
99
+ if mask is not None:
100
+ logits[mask] = float("-inf")
101
+
102
+ if sample_temperature <= 0 or sample_k == 1:
103
+ next_id = int(torch.argmax(logits).item())
104
+ else:
105
+ next_id = int(sample_top_k(logits.numpy(), k=sample_k,
106
+ temperature=sample_temperature))
107
+ ids.append(next_id)
108
+
109
+ if eos_id is not None and next_id == eos_id and step >= 1:
110
+ break
111
+
112
+ # Soft stop on sentence-ending punctuation. Test the decoded token:
113
+ # multi-character tokens ('la!', 'no!') carry the terminator too.
114
+ if step >= stop_after:
115
+ try:
116
+ tail = tok.decode([next_id]).rstrip()
117
+ except Exception:
118
+ tail = "!" if next_id in (33, 46, 63) else ""
119
+ if tail.endswith(("!", ".", "?")) and (step >= 1 or len(tail) > 1):
120
+ break
121
+
122
+ # The EOS token that ended the loop is inside ids: decode() renders it as
123
+ # the literal '<|EOS|>', so drop every trailing special slot first.
124
+ out = ids[n_prompt:]
125
+ while out and tok.is_special(out[-1]):
126
+ out.pop()
127
+ return clean_response(tok.decode(out))
128
+
129
+
130
+ def load_affect(tok):
131
+ """
132
+ Build the affective modulator if the modules are available. They live in
133
+ dynamic_model/exp_b/ in the repository and are copied into this package by
134
+ scripts/export_hf.py, so both layouts are tried. Returns (modulator,
135
+ affect), or (None, None) when neither is importable.
136
+ """
137
+ try:
138
+ try:
139
+ from .affect_state import AffectState # published package
140
+ from .modulator import AffectModulator
141
+ except ImportError:
142
+ from dynamic_model.exp_b.affect_state import AffectState
143
+ from dynamic_model.exp_b.modulator import AffectModulator
144
+ except ImportError:
145
+ return None, None
146
+ affect = AffectState()
147
+ return AffectModulator(
148
+ affect, eos_token_id=tok.get_special_id(tok.EOS_TOKEN)), affect