mario-rc commited on
Commit
fd865ef
·
verified ·
1 Parent(s): 049b2cc

Release common-recipe model and update model card

Browse files

Publish the frozen GPU-trained shared-prompt model with verified files and synchronized documentation.

Files changed (6) hide show
  1. README.md +30 -21
  2. config.json +1 -1
  3. model-00001-of-00004.safetensors +1 -1
  4. modeling_custom.py +45 -12
  5. requirements.txt +1 -1
  6. utils.py +143 -4
README.md CHANGED
@@ -51,27 +51,27 @@ The model was trained with data from the
51
 
52
  ## Evaluation
53
 
54
- Results on the held-out multi-domain test set:
55
 
56
  | Metric | Result |
57
  | :--- | :---: |
58
- | Test accuracy (%) | 86.82 |
59
  | Scoring Spearman | 0.7264 |
60
- | Coherence accuracy | 75.59% |
61
- | Commonsense accuracy | 97.39% |
62
- | Empathy accuracy | 92.72% |
63
- | Multicultural accuracy | 74.99% |
64
 
65
  ## Hugging Face Models
66
 
67
  | Model | Base reward model | Test accuracy (%) | Scoring Spearman |
68
  | :--- | :--- | :---: | :---: |
69
- | [**`multi-domain-rm-fsfairx-gemma-2-9b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-fsfairx-gemma-2-9b-it) | [sfairXC/FsfairX-Gemma2-RM-v0.1](https://huggingface.co/sfairXC/FsfairX-Gemma2-RM-v0.1) | **88.01** | 0.7346 |
70
- | [**`multi-domain-rm-skywork-qwen-3-8b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-skywork-qwen-3-8b-it) | [Skywork/Skywork-Reward-V2-Qwen3-8B](https://huggingface.co/Skywork/Skywork-Reward-V2-Qwen3-8B) | **87.82** | 0.7156 |
71
- | [**`multi-domain-rm-fsfairx-llama-3-8b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-fsfairx-llama-3-8b-it) | [sfairXC/FsfairX-LLaMA3-RM-v0.1](https://huggingface.co/sfairXC/FsfairX-LLaMA3-RM-v0.1) | **86.86** | 0.7108 |
72
- | [**`multi-domain-rm-skywork-llama-3.1-8b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-skywork-llama-3.1-8b-it) | [Skywork/Skywork-Reward-V2-Llama-3.1-8B](https://huggingface.co/Skywork/Skywork-Reward-V2-Llama-3.1-8B) | **86.82** | 0.7264 |
73
- | [**`multi-domain-rm-mistral-7b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-mistral-7b-it) | [weqweasdas/RM-Mistral-7B](https://huggingface.co/weqweasdas/RM-Mistral-7B) | **84.41** | 0.6710 |
74
- | [**`multi-domain-rm-qwen-3-nemotron-8b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-qwen-3-nemotron-8b-it) | [nvidia/Qwen3-Nemotron-8B-BRRM](https://huggingface.co/nvidia/Qwen3-Nemotron-8B-BRRM) | **83.65** | 0.6704 |
75
 
76
  ## Usage
77
 
@@ -86,7 +86,7 @@ repo_id = "mario-rc/multi-domain-rm-skywork-llama-3.1-8b-it"
86
  tokenizer = AutoTokenizer.from_pretrained(repo_id, trust_remote_code=True)
87
  model = AutoModel.from_pretrained(
88
  repo_id,
89
- torch_dtype=torch.bfloat16,
90
  device_map="auto",
91
  trust_remote_code=True,
92
  ).eval()
@@ -98,41 +98,48 @@ chosen = prompt + [{
98
  }]
99
  rejected = prompt + [{"role": "assistant", "content": "Tell them to ignore it."}]
100
 
101
- prompt_ids = tokenizer.apply_chat_template(
102
  prompt,
103
  tokenize=True,
104
  add_generation_prompt=True,
105
  return_tensors="pt",
 
106
  ).to(model.device)
107
- chosen_ids = tokenizer.apply_chat_template(
108
  chosen,
109
  tokenize=True,
110
  add_generation_prompt=False,
111
  return_tensors="pt",
 
112
  ).to(model.device)
113
- rejected_ids = tokenizer.apply_chat_template(
114
  rejected,
115
  tokenize=True,
116
  add_generation_prompt=False,
117
  return_tensors="pt",
 
118
  ).to(model.device)
119
 
120
  with torch.inference_mode():
121
- gate = model.compute_gating(input_ids=prompt_ids)
 
 
 
122
  chosen_score = model(
123
- input_ids=chosen_ids,
 
124
  gating_output_override=gate,
125
  ).score
126
  rejected_score = model(
127
- input_ids=rejected_ids,
 
128
  gating_output_override=gate,
129
  ).score
130
 
131
  print({"chosen": chosen_score.item(), "rejected": rejected_score.item()})
132
  ```
133
 
134
- For padded batches, also pass the matching `attention_mask`. Scores are intended for comparison
135
- within a prompt; they are not calibrated probabilities or universal utility values.
136
 
137
  ## Limitations
138
 
@@ -141,6 +148,8 @@ and should be calibrated for each downstream use case. Performance can vary by l
141
  distribution. The model inherits limitations and biases from its base model and training data and
142
  should not be used as the sole decision-maker in high-impact settings.
143
 
 
 
144
  ## Credits
145
 
146
  This model is based on the ArmoRM/RLHFlow reward-modeling approach and adapts it to custom
 
51
 
52
  ## Evaluation
53
 
54
+ Results on the internal multi-domain test set:
55
 
56
  | Metric | Result |
57
  | :--- | :---: |
58
+ | Test accuracy (%) | 86.99 |
59
  | Scoring Spearman | 0.7264 |
60
+ | Coherence accuracy | 76.32% |
61
+ | Commonsense accuracy | 97.33% |
62
+ | Empathy accuracy | 93.30% |
63
+ | Multicultural accuracy | 74.31% |
64
 
65
  ## Hugging Face Models
66
 
67
  | Model | Base reward model | Test accuracy (%) | Scoring Spearman |
68
  | :--- | :--- | :---: | :---: |
69
+ | [**`multi-domain-rm-fsfairx-gemma-2-9b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-fsfairx-gemma-2-9b-it) | [sfairXC/FsfairX-Gemma2-RM-v0.1](https://huggingface.co/sfairXC/FsfairX-Gemma2-RM-v0.1) | **88.80** | 0.7346 |
70
+ | [**`multi-domain-rm-skywork-qwen-3-8b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-skywork-qwen-3-8b-it) | [Skywork/Skywork-Reward-V2-Qwen3-8B](https://huggingface.co/Skywork/Skywork-Reward-V2-Qwen3-8B) | **88.08** | 0.7156 |
71
+ | [**`multi-domain-rm-fsfairx-llama-3-8b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-fsfairx-llama-3-8b-it) | [sfairXC/FsfairX-LLaMA3-RM-v0.1](https://huggingface.co/sfairXC/FsfairX-LLaMA3-RM-v0.1) | **87.75** | 0.7108 |
72
+ | [**`multi-domain-rm-skywork-llama-3.1-8b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-skywork-llama-3.1-8b-it) | [Skywork/Skywork-Reward-V2-Llama-3.1-8B](https://huggingface.co/Skywork/Skywork-Reward-V2-Llama-3.1-8B) | **86.99** | 0.7264 |
73
+ | [**`multi-domain-rm-mistral-7b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-mistral-7b-it) | [weqweasdas/RM-Mistral-7B](https://huggingface.co/weqweasdas/RM-Mistral-7B) | **85.25** | 0.6710 |
74
+ | [**`multi-domain-rm-qwen-3-nemotron-8b-it`**](https://huggingface.co/mario-rc/multi-domain-rm-qwen-3-nemotron-8b-it) | [nvidia/Qwen3-Nemotron-8B-BRRM](https://huggingface.co/nvidia/Qwen3-Nemotron-8B-BRRM) | **84.35** | 0.6704 |
75
 
76
  ## Usage
77
 
 
86
  tokenizer = AutoTokenizer.from_pretrained(repo_id, trust_remote_code=True)
87
  model = AutoModel.from_pretrained(
88
  repo_id,
89
+ dtype=torch.bfloat16,
90
  device_map="auto",
91
  trust_remote_code=True,
92
  ).eval()
 
98
  }]
99
  rejected = prompt + [{"role": "assistant", "content": "Tell them to ignore it."}]
100
 
101
+ prompt_inputs = tokenizer.apply_chat_template(
102
  prompt,
103
  tokenize=True,
104
  add_generation_prompt=True,
105
  return_tensors="pt",
106
+ return_dict=True,
107
  ).to(model.device)
108
+ chosen_inputs = tokenizer.apply_chat_template(
109
  chosen,
110
  tokenize=True,
111
  add_generation_prompt=False,
112
  return_tensors="pt",
113
+ return_dict=True,
114
  ).to(model.device)
115
+ rejected_inputs = tokenizer.apply_chat_template(
116
  rejected,
117
  tokenize=True,
118
  add_generation_prompt=False,
119
  return_tensors="pt",
120
+ return_dict=True,
121
  ).to(model.device)
122
 
123
  with torch.inference_mode():
124
+ gate = model.compute_gating(
125
+ input_ids=prompt_inputs["input_ids"],
126
+ attention_mask=prompt_inputs["attention_mask"],
127
+ )
128
  chosen_score = model(
129
+ input_ids=chosen_inputs["input_ids"],
130
+ attention_mask=chosen_inputs["attention_mask"],
131
  gating_output_override=gate,
132
  ).score
133
  rejected_score = model(
134
+ input_ids=rejected_inputs["input_ids"],
135
+ attention_mask=rejected_inputs["attention_mask"],
136
  gating_output_override=gate,
137
  ).score
138
 
139
  print({"chosen": chosen_score.item(), "rejected": rejected_score.item()})
140
  ```
141
 
142
+ Pass the tokenizer's `input_ids` tensor and matching `attention_mask` to the model. Reuse the same prompt-derived gate for both candidates. Scores are intended for comparison within a prompt; they are not calibrated probabilities or universal utility values.
 
143
 
144
  ## Limitations
145
 
 
148
  distribution. The model inherits limitations and biases from its base model and training data and
149
  should not be used as the sole decision-maker in high-impact settings.
150
 
151
+ The internal test was examined during development, and a source audit identified some train–test prompt overlap. These results are not an independent confirmation of generalization.
152
+
153
  ## Credits
154
 
155
  This model is based on the ArmoRM/RLHFlow reward-modeling approach and adapts it to custom
config.json CHANGED
@@ -42,7 +42,7 @@
42
  "gating_dropout": 0.05,
43
  "gating_hidden_dim": 64,
44
  "gating_learnable_logit_scale": false,
45
- "gating_logit_scale": 2.0,
46
  "gating_n_hidden": 1,
47
  "gating_temperature": 2.0,
48
  "head_dim": 128,
 
42
  "gating_dropout": 0.05,
43
  "gating_hidden_dim": 64,
44
  "gating_learnable_logit_scale": false,
45
+ "gating_logit_scale": 4.0,
46
  "gating_n_hidden": 1,
47
  "gating_temperature": 2.0,
48
  "head_dim": 128,
model-00001-of-00004.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:86ca401765f707e08ad005924b409d4db9d332a95001c903a56bd1df40368657
3
  size 3903440936
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5aa0b32b04e4bd8ed6bd46cb09cec0212a8b532c2b7b0900d1bd49f3e6d82f81
3
  size 3903440936
modeling_custom.py CHANGED
@@ -18,6 +18,8 @@ class GatingNetwork(nn.Module):
18
  learnable_logit_scale: bool = False,
19
  active_attribute_indices: Optional[List[int]] = None):
20
  super().__init__()
 
 
21
  self.temperature = temperature
22
  self.logit_scale = nn.Parameter(
23
  torch.ones(1) * logit_scale, requires_grad=learnable_logit_scale
@@ -25,12 +27,18 @@ class GatingNetwork(nn.Module):
25
  self.dropout_prob = dropout
26
  active_mask = torch.ones(out_features, dtype=torch.bool)
27
  if active_attribute_indices is not None:
28
- if not active_attribute_indices:
29
- raise ValueError("At least one attribute must be active.")
 
 
 
30
  if min(active_attribute_indices) < 0 or max(active_attribute_indices) >= out_features:
31
  raise ValueError("active_attribute_indices contains an out-of-range index.")
32
  active_mask.zero_()
33
  active_mask[list(active_attribute_indices)] = True
 
 
 
34
  # Derived from packaged config; omit from state_dict for legacy compatibility.
35
  self.register_buffer("active_attribute_mask", active_mask, persistent=False)
36
  layers = []
@@ -40,6 +48,17 @@ class GatingNetwork(nn.Module):
40
  layers.append(nn.Linear(in_features, out_features, bias=bias))
41
  self.layers = nn.ModuleList(layers)
42
 
 
 
 
 
 
 
 
 
 
 
 
43
  def forward(self, x: torch.Tensor) -> torch.Tensor:
44
  for i, layer in enumerate(self.layers):
45
  x = layer(x)
@@ -68,24 +87,33 @@ class RewardModelWithGating(PreTrainedModel):
68
  config_class = AutoConfig
69
  base_model_prefix = "model"
70
 
 
 
 
 
 
 
 
 
 
71
  def __init__(self, config):
72
  super().__init__(config)
73
  self.num_labels = config.num_labels
74
  self.model = AutoModel.from_config(config)
75
  config_dict = config.to_dict()
76
-
77
  # Default objective count for this project.
78
  self.num_objectives = config_dict.get("num_objectives", 23)
79
-
80
  self.regression_layer = nn.Linear(config.hidden_size, self.num_objectives, bias=False)
81
  self.post_init()
82
-
83
  # Avoid torch.eye to keep compatibility with BF16 training setups.
84
  I = torch.zeros(self.num_objectives, self.num_objectives)
85
  I[range(self.num_objectives), range(self.num_objectives)] = 1.
86
  self.reward_transform_matrix = nn.Parameter(I)
87
  self.reward_transform_matrix.requires_grad = False
88
-
89
  self.gating = GatingNetwork(config.hidden_size, self.num_objectives,
90
  temperature=config_dict.get("gating_temperature", 10),
91
  logit_scale=config_dict.get("gating_logit_scale", 1.0),
@@ -159,10 +187,10 @@ class RewardModelWithGating(PreTrainedModel):
159
  batch_size = inputs_embeds.shape[0]
160
  else:
161
  raise ValueError("Either input_ids or inputs_embeds must be provided.")
162
-
163
  if self.config.pad_token_id is None and batch_size != 1:
164
  raise ValueError("Cannot handle batch sizes > 1 if no padding token is defined.")
165
-
166
  if self.config.pad_token_id is None:
167
  sequence_lengths = -1
168
  else:
@@ -173,11 +201,11 @@ class RewardModelWithGating(PreTrainedModel):
173
  sequence_lengths = sequence_lengths.to(tokens_hidden_states.device)
174
  else:
175
  sequence_lengths = -1
176
-
177
  dummy_iterator = torch.arange(batch_size, device=tokens_hidden_states.device)
178
  hidden_states = tokens_hidden_states[dummy_iterator, sequence_lengths]
179
  assert hidden_states.shape == (batch_size, self.config.hidden_size)
180
-
181
  rewards = self.regression_layer(hidden_states)
182
  prompt_embedding = None
183
  if gating_output_override is not None:
@@ -189,6 +217,11 @@ class RewardModelWithGating(PreTrainedModel):
189
  f"gating_output_override shape {tuple(gating_output.shape)} does not "
190
  f"match rewards shape {tuple(rewards.shape)}"
191
  )
 
 
 
 
 
192
  else:
193
  if input_ids is None:
194
  raise ValueError("input_ids is required to compute gating token positions.")
@@ -200,7 +233,7 @@ class RewardModelWithGating(PreTrainedModel):
200
  gating_output = self.gating(prompt_embedding)
201
  rewards_adjusted = rewards @ self.reward_transform_matrix
202
  score = torch.sum(gating_output * rewards_adjusted, dim=1)
203
-
204
  return CustomOutput(
205
  rewards=rewards,
206
  hidden_state=hidden_states,
@@ -212,4 +245,4 @@ class RewardModelWithGating(PreTrainedModel):
212
 
213
 
214
  # Backward compatibility alias for existing imports/checkpoints.
215
- LlamaForRewardModelWithGating = RewardModelWithGating
 
18
  learnable_logit_scale: bool = False,
19
  active_attribute_indices: Optional[List[int]] = None):
20
  super().__init__()
21
+ if temperature <= 0:
22
+ raise ValueError("Temperature must be positive.")
23
  self.temperature = temperature
24
  self.logit_scale = nn.Parameter(
25
  torch.ones(1) * logit_scale, requires_grad=learnable_logit_scale
 
27
  self.dropout_prob = dropout
28
  active_mask = torch.ones(out_features, dtype=torch.bool)
29
  if active_attribute_indices is not None:
30
+ if (not isinstance(active_attribute_indices, (list, tuple))
31
+ or not active_attribute_indices
32
+ or any(type(index) is not int for index in active_attribute_indices)
33
+ or len(set(active_attribute_indices)) != len(active_attribute_indices)):
34
+ raise ValueError("Active attribute indices must be unique integers in a nonempty sequence.")
35
  if min(active_attribute_indices) < 0 or max(active_attribute_indices) >= out_features:
36
  raise ValueError("active_attribute_indices contains an out-of-range index.")
37
  active_mask.zero_()
38
  active_mask[list(active_attribute_indices)] = True
39
+ self.active_attribute_indices = tuple(
40
+ range(out_features) if active_attribute_indices is None else active_attribute_indices
41
+ )
42
  # Derived from packaged config; omit from state_dict for legacy compatibility.
43
  self.register_buffer("active_attribute_mask", active_mask, persistent=False)
44
  layers = []
 
48
  layers.append(nn.Linear(in_features, out_features, bias=bias))
49
  self.layers = nn.ModuleList(layers)
50
 
51
+ @torch.no_grad()
52
+ def reset_active_attribute_mask(self):
53
+ """Restore config-derived state after a low-memory/meta-device load.
54
+
55
+ Transformers can materialize non-persistent buffers with empty_like;
56
+ their constructor values therefore are not sufficient initialization.
57
+ This method changes no trained parameter or persistent checkpoint key.
58
+ """
59
+ self.active_attribute_mask.zero_()
60
+ self.active_attribute_mask[list(self.active_attribute_indices)] = True
61
+
62
  def forward(self, x: torch.Tensor) -> torch.Tensor:
63
  for i, layer in enumerate(self.layers):
64
  x = layer(x)
 
87
  config_class = AutoConfig
88
  base_model_prefix = "model"
89
 
90
+ def _init_weights(self, module):
91
+ if isinstance(module, GatingNetwork):
92
+ # The non-persistent mask is absent from saved weights by design.
93
+ # Initialize it when the loader materializes missing/derived state,
94
+ # without reinitializing the gating parameters it already loaded.
95
+ module.reset_active_attribute_mask()
96
+ else:
97
+ super()._init_weights(module)
98
+
99
  def __init__(self, config):
100
  super().__init__(config)
101
  self.num_labels = config.num_labels
102
  self.model = AutoModel.from_config(config)
103
  config_dict = config.to_dict()
104
+
105
  # Default objective count for this project.
106
  self.num_objectives = config_dict.get("num_objectives", 23)
107
+
108
  self.regression_layer = nn.Linear(config.hidden_size, self.num_objectives, bias=False)
109
  self.post_init()
110
+
111
  # Avoid torch.eye to keep compatibility with BF16 training setups.
112
  I = torch.zeros(self.num_objectives, self.num_objectives)
113
  I[range(self.num_objectives), range(self.num_objectives)] = 1.
114
  self.reward_transform_matrix = nn.Parameter(I)
115
  self.reward_transform_matrix.requires_grad = False
116
+
117
  self.gating = GatingNetwork(config.hidden_size, self.num_objectives,
118
  temperature=config_dict.get("gating_temperature", 10),
119
  logit_scale=config_dict.get("gating_logit_scale", 1.0),
 
187
  batch_size = inputs_embeds.shape[0]
188
  else:
189
  raise ValueError("Either input_ids or inputs_embeds must be provided.")
190
+
191
  if self.config.pad_token_id is None and batch_size != 1:
192
  raise ValueError("Cannot handle batch sizes > 1 if no padding token is defined.")
193
+
194
  if self.config.pad_token_id is None:
195
  sequence_lengths = -1
196
  else:
 
201
  sequence_lengths = sequence_lengths.to(tokens_hidden_states.device)
202
  else:
203
  sequence_lengths = -1
204
+
205
  dummy_iterator = torch.arange(batch_size, device=tokens_hidden_states.device)
206
  hidden_states = tokens_hidden_states[dummy_iterator, sequence_lengths]
207
  assert hidden_states.shape == (batch_size, self.config.hidden_size)
208
+
209
  rewards = self.regression_layer(hidden_states)
210
  prompt_embedding = None
211
  if gating_output_override is not None:
 
217
  f"gating_output_override shape {tuple(gating_output.shape)} does not "
218
  f"match rewards shape {tuple(rewards.shape)}"
219
  )
220
+ elif getattr(self.config, "shared_prompt_gating", False):
221
+ raise ValueError(
222
+ "This shared-prompt-gating checkpoint requires gating_output_override. "
223
+ "Compute one prompt-only gate with compute_gating() and reuse it for all candidates."
224
+ )
225
  else:
226
  if input_ids is None:
227
  raise ValueError("input_ids is required to compute gating token positions.")
 
233
  gating_output = self.gating(prompt_embedding)
234
  rewards_adjusted = rewards @ self.reward_transform_matrix
235
  score = torch.sum(gating_output * rewards_adjusted, dim=1)
236
+
237
  return CustomOutput(
238
  rewards=rewards,
239
  hidden_state=hidden_states,
 
245
 
246
 
247
  # Backward compatibility alias for existing imports/checkpoints.
248
+ LlamaForRewardModelWithGating = RewardModelWithGating
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
  torch>=2.6
2
- transformers>=4.51
3
  safetensors>=0.5
4
  accelerate>=1.0
 
1
  torch>=2.6
2
+ transformers==5.3.0
3
  safetensors>=0.5
4
  accelerate>=1.0
utils.py CHANGED
@@ -2,6 +2,7 @@
2
 
3
  import json
4
  import os
 
5
  from typing import Optional, Sequence
6
 
7
  import torch
@@ -18,6 +19,144 @@ def _requires_remote_code(model_path: str) -> bool:
18
  return "qwen3" in model_path_l
19
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # ---------------------------------------------------------------------------
22
  # Tokenizer loading
23
  # ---------------------------------------------------------------------------
@@ -159,10 +298,6 @@ def _resolve_inference_model_path(
159
  if not isinstance(inference_cfg, dict):
160
  inference_cfg = {}
161
 
162
- explicit_model_path = inference_cfg.get("model_path")
163
- if explicit_model_path:
164
- return str(explicit_model_path)
165
-
166
  if cli_model_parent_dir or cli_model_name:
167
  model_parent_dir = str(cli_model_parent_dir or inference_cfg.get("model_parent_dir", "model"))
168
  model_name = cli_model_name or inference_cfg.get("model_name")
@@ -170,6 +305,10 @@ def _resolve_inference_model_path(
170
  raise ValueError("model_name must be provided via --model_name or config.yaml inference.model_name")
171
  return os.path.join(model_parent_dir, str(model_name))
172
 
 
 
 
 
173
  model_name = inference_cfg.get("model_name")
174
  if not model_name:
175
  raise ValueError("model_name must be provided via --model_name or config.yaml inference.model_name")
 
2
 
3
  import json
4
  import os
5
+ import importlib.util
6
  from typing import Optional, Sequence
7
 
8
  import torch
 
19
  return "qwen3" in model_path_l
20
 
21
 
22
+ def _attention_implementation(device: str) -> str | None:
23
+ """Use FlashAttention on CUDA when installed; otherwise use Transformers defaults."""
24
+ if str(device).startswith("cuda") and importlib.util.find_spec("flash_attn") is not None:
25
+ return "flash_attention_2"
26
+ return None
27
+
28
+
29
+ def _stable_int64_id(value) -> int:
30
+ """Return a deterministic non-negative signed-int64 identifier."""
31
+ import hashlib
32
+
33
+ if not isinstance(value, str):
34
+ value = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
35
+ digest = hashlib.blake2b(value.encode("utf-8"), digest_size=8).digest()
36
+ return int.from_bytes(digest, "big", signed=False) & ((1 << 63) - 1)
37
+
38
+
39
+ def debiasing_checkpoint_suffix(debiasing_dims, corr_threshold: float) -> str:
40
+ """Encode reward-transform settings in a stable checkpoint suffix."""
41
+ dims = sorted({int(dimension) for dimension in (debiasing_dims or ()) if int(dimension) >= 0})
42
+ if not dims:
43
+ return "_dbnone"
44
+ threshold = format(float(corr_threshold), ".12g").replace("-", "m").replace(".", "p")
45
+ dimension_text = "-".join(map(str, dims))
46
+ return f"_db{dimension_text}_ct{threshold}"
47
+
48
+
49
+ def validate_shared_routing_config(routing_config) -> dict:
50
+ """Validate the metadata contract required by packaged shared-gate checkpoints."""
51
+ if not isinstance(routing_config, dict):
52
+ raise ValueError("Stage 2 checkpoint is missing its training_config mapping.")
53
+ if routing_config.get("format_version") != 2 or not routing_config.get("shared_prompt_gating", False):
54
+ raise ValueError(
55
+ "Stage 2 checkpoint must declare format_version=2 and "
56
+ "shared_prompt_gating=true; legacy checkpoints are not packageable."
57
+ )
58
+ return routing_config
59
+
60
+
61
+ def score_shared_gate_candidates(
62
+ candidate_attribute_rewards: torch.Tensor,
63
+ shared_gate_weights: torch.Tensor,
64
+ ) -> torch.Tensor:
65
+ """Combine per-candidate attribute rewards with one gate per prompt pair."""
66
+ if candidate_attribute_rewards.ndim != 3 or shared_gate_weights.ndim != 2:
67
+ raise ValueError(
68
+ "Shared-gate scoring expects candidate rewards shaped "
69
+ "[pairs, candidates, attributes] and gate weights shaped "
70
+ "[pairs, attributes]."
71
+ )
72
+ if (
73
+ candidate_attribute_rewards.shape[0] != shared_gate_weights.shape[0]
74
+ or candidate_attribute_rewards.shape[-1] != shared_gate_weights.shape[-1]
75
+ ):
76
+ raise ValueError(
77
+ "Candidate rewards and shared gate weights have incompatible "
78
+ "pair or attribute dimensions."
79
+ )
80
+ return torch.sum(
81
+ candidate_attribute_rewards * shared_gate_weights.unsqueeze(1), dim=-1
82
+ )
83
+
84
+
85
+ def shared_gate_checkpoint_filename(args, model_name: str, preference_name: str, reference_name: str) -> str:
86
+ """Build the canonical Shared-Gate V2 checkpoint filename."""
87
+ from attributes import attribute_selection_suffix
88
+
89
+ defaults = {
90
+ "learning_rate": 0.0005, "weight_decay": 0.0, "n_hidden": 1,
91
+ "hidden_size": 64, "dropout": 0.1, "batch_size": 2048,
92
+ "logit_scale": 2.0, "domain_loss_weight": 0.25,
93
+ "entropy_weight": 0.02, "load_balance_weight": 0.05,
94
+ }
95
+ hyperparameters = "".join(
96
+ f"_{key[:2]}{getattr(args, key, default)}"
97
+ for key, default in defaults.items()
98
+ )
99
+ debiasing_dims = (
100
+ [-1] if str(reference_name).lower() == "null"
101
+ else getattr(args, "debiasing_dims", [-1])
102
+ )
103
+ suffix = debiasing_checkpoint_suffix(
104
+ debiasing_dims, getattr(args, "corr_threshold", 0.04)
105
+ )
106
+ suffix += "_cv" if getattr(args, "curriculum", False) else ""
107
+ suffix += "_bd" if getattr(args, "balance_difficulties", False) else ""
108
+ suffix += "" if getattr(args, "balance_domains", True) else "_ubd"
109
+ suffix += "_lgs" if getattr(args, "learnable_logit_scale", False) else ""
110
+ entropy_floor = getattr(args, "entropy_floor_fraction", 0.35)
111
+ suffix += "" if entropy_floor == 0.35 else f"_ef{entropy_floor}"
112
+ suffix += attribute_selection_suffix(
113
+ getattr(args, "attribute_subset", "full"),
114
+ getattr(args, "exclude_attributes", []),
115
+ )
116
+ gate_input_mode = getattr(args, "gate_input_mode", "prompt")
117
+ gate_mode_codes = {
118
+ "prompt": "prompt",
119
+ "global": "global",
120
+ "shuffled_prompt": "shuffle",
121
+ "candidate_conditioned": "candidate",
122
+ }
123
+ if gate_input_mode not in gate_mode_codes:
124
+ raise ValueError(f"Unknown gate_input_mode: {gate_input_mode}")
125
+ if gate_input_mode != "prompt":
126
+ suffix += f"_gim-{gate_mode_codes[gate_input_mode]}"
127
+ held_out_domain = getattr(args, "held_out_domain", None)
128
+ if held_out_domain:
129
+ suffix += f"_holdout-{held_out_domain}"
130
+ checkpoint_tag = getattr(args, "checkpoint_tag", None)
131
+ suffix += f"_tag-{checkpoint_tag}" if checkpoint_tag else ""
132
+ validation_manifest = getattr(args, "validation_group_ids_path", None)
133
+ if validation_manifest:
134
+ import hashlib
135
+ with open(validation_manifest, "rb") as stream:
136
+ suffix += "_vs-" + hashlib.sha256(stream.read()).hexdigest()[:16]
137
+ suffix += "_refit" if getattr(args, "train_on_all", False) else ""
138
+ filename = (
139
+ f"gating_network_sgv2_{model_name}_mo_{args.multi_objective_dataset_name}_"
140
+ f"pref_{preference_name}_ref_{reference_name}"
141
+ f"_t{getattr(args, 'temperature', 2.0):.1f}"
142
+ f"_n{getattr(args, 'n_steps', 30000)}"
143
+ f"_seed{getattr(args, 'seed', 0)}{hyperparameters}{suffix}.pt"
144
+ )
145
+ # Linux filesystems normally limit a single path component to 255 bytes.
146
+ # Keep short legacy names unchanged, but make long ablation names portable
147
+ # and collision resistant for both Stage 2 saving and Stage 3 lookup.
148
+ max_filename_bytes = 240
149
+ if len(filename.encode("utf-8")) > max_filename_bytes:
150
+ import hashlib
151
+
152
+ digest = hashlib.sha256(filename.encode("utf-8")).hexdigest()[:16]
153
+ extension = ".pt"
154
+ budget = max_filename_bytes - len(f"_h{digest}{extension}")
155
+ filename = f"{filename[:-len(extension)][:budget]}_h{digest}{extension}"
156
+ return filename
157
+
158
+
159
+
160
  # ---------------------------------------------------------------------------
161
  # Tokenizer loading
162
  # ---------------------------------------------------------------------------
 
298
  if not isinstance(inference_cfg, dict):
299
  inference_cfg = {}
300
 
 
 
 
 
301
  if cli_model_parent_dir or cli_model_name:
302
  model_parent_dir = str(cli_model_parent_dir or inference_cfg.get("model_parent_dir", "model"))
303
  model_name = cli_model_name or inference_cfg.get("model_name")
 
305
  raise ValueError("model_name must be provided via --model_name or config.yaml inference.model_name")
306
  return os.path.join(model_parent_dir, str(model_name))
307
 
308
+ explicit_model_path = inference_cfg.get("model_path")
309
+ if explicit_model_path:
310
+ return str(explicit_model_path)
311
+
312
  model_name = inference_cfg.get("model_name")
313
  if not model_name:
314
  raise ValueError("model_name must be provided via --model_name or config.yaml inference.model_name")