From baf42de41fe76d07a764027adf1c28808b1d484c Mon Sep 17 00:00:00 2001 From: opentaco Date: Thu, 20 Jul 2023 19:46:35 +0200 Subject: [PATCH 01/10] Add DirectPreferenceRewardModel --- openvalidators/config.py | 6 +++ openvalidators/event.py | 2 + openvalidators/neuron.py | 5 ++ openvalidators/reward/__init__.py | 1 + openvalidators/reward/config.py | 4 +- openvalidators/reward/dpo.py | 80 +++++++++++++++++++++++++++++++ 6 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 openvalidators/reward/dpo.py diff --git a/openvalidators/config.py b/openvalidators/config.py index 6565cef..1ec0c35 100644 --- a/openvalidators/config.py +++ b/openvalidators/config.py @@ -249,6 +249,12 @@ def add_args(cls, parser): help="Weight for the reciprocate reward model", default=DefaultRewardFrameworkConfig.reciprocate_model_weight, ) + parser.add_argument( + "--reward.dpo_weight", + type=float, + help="Weight for the dpo reward model", + default=DefaultRewardFrameworkConfig.dpo_model_weight, + ) parser.add_argument( "--reward.rlhf_weight", type=float, diff --git a/openvalidators/event.py b/openvalidators/event.py index d790318..bbf797d 100644 --- a/openvalidators/event.py +++ b/openvalidators/event.py @@ -41,6 +41,7 @@ class EventSchema: nsfw_filter: Optional[List[float]] # Output vector of the nsfw filter reciprocate_reward_model: Optional[List[float]] # Output vector of the reciprocate reward model diversity_reward_model: Optional[List[float]] # Output vector of the diversity reward model + dpo_reward_model: Optional[List[float]] # Output vector of the dpo reward model rlhf_reward_model: Optional[List[float]] # Output vector of the rlhf reward model prompt_reward_model: Optional[List[float]] # Output vector of the prompt reward model relevance_filter: Optional[List[float]] # Output vector of the relevance scoring reward model @@ -58,6 +59,7 @@ def from_dict(event_dict: dict, disable_log_rewards: bool) -> 'EventSchema': 'relevance_filter': event_dict.get(RewardModelType.relevance.value), 'reciprocate_reward_model': event_dict.get(RewardModelType.reciprocate.value), 'diversity_reward_model': event_dict.get(RewardModelType.diversity.value), + 'dpo_reward_model': event_dict.get(RewardModelType.dpo.value), 'rlhf_reward_model': event_dict.get(RewardModelType.rlhf.value), 'prompt_reward_model': event_dict.get(RewardModelType.prompt.value), } diff --git a/openvalidators/neuron.py b/openvalidators/neuron.py index 8944767..9995a78 100644 --- a/openvalidators/neuron.py +++ b/openvalidators/neuron.py @@ -35,6 +35,7 @@ from openvalidators.reward import ( Blacklist, NSFWRewardModel, + DirectPreferenceRewardModel, OpenAssistantRewardModel, ReciprocateRewardModel, BertRelevanceRewardModel, @@ -142,6 +143,7 @@ def __init__(self): else: self.reward_weights = torch.tensor( [ + self.config.reward.dpo_weight, self.config.reward.rlhf_weight, self.config.reward.reciprocate_weight, self.config.reward.dahoas_weight, @@ -160,6 +162,9 @@ def __init__(self): raise Exception(message) self.reward_functions = [ + DirectPreferenceRewardModel(device=self.device) + if self.config.reward.dpo_weight > 0 + else MockRewardModel(RewardModelType.dpo.value), OpenAssistantRewardModel(device=self.device) if self.config.reward.rlhf_weight > 0 else MockRewardModel(RewardModelType.rlhf.value), diff --git a/openvalidators/reward/__init__.py b/openvalidators/reward/__init__.py index 6a94469..28a8a5a 100644 --- a/openvalidators/reward/__init__.py +++ b/openvalidators/reward/__init__.py @@ -1,5 +1,6 @@ from .blacklist import Blacklist from .nsfw import NSFWRewardModel +from .dpo import DirectPreferenceRewardModel from .open_assistant import OpenAssistantRewardModel from .reciprocate import ReciprocateRewardModel from .relevance import BertRelevanceRewardModel diff --git a/openvalidators/reward/config.py b/openvalidators/reward/config.py index 2f4b63b..2037a73 100644 --- a/openvalidators/reward/config.py +++ b/openvalidators/reward/config.py @@ -20,6 +20,7 @@ class RewardModelType(Enum): + dpo = 'dpo_reward_model' rlhf = 'rlhf_reward_model' reciprocate = 'reciprocate_reward_model' dahoas = 'dahoas_reward_model' @@ -35,7 +36,8 @@ class DefaultRewardFrameworkConfig: """Reward framework default configuration. Note: All the weights should add up to 1.0. """ - rlhf_model_weight: float = 0.6 + dpo_model_weight: float = 0.2 + rlhf_model_weight: float = 0.4 reciprocate_model_weight: float = 0.4 dahoas_model_weight: float = 0 prompt_model_weight: float = 0 diff --git a/openvalidators/reward/dpo.py b/openvalidators/reward/dpo.py new file mode 100644 index 0000000..780f18f --- /dev/null +++ b/openvalidators/reward/dpo.py @@ -0,0 +1,80 @@ +# The MIT License (MIT) +# Copyright © 2021 Yuma Rao + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import torch +from typing import List +from .config import RewardModelType +from .reward import BaseRewardModel +from transformers import AutoTokenizer, AutoModelForCausalLM + + +class DirectPreferenceRewardModel(BaseRewardModel): + + reward_model_name: str = "TheBloke/Llama-2-7B-fp16" + + @property + def name(self) -> str: return RewardModelType.dpo.value + + def __init__(self, device: str): + super().__init__() + self.device = device + self.tokenizer = AutoTokenizer.from_pretrained(DirectPreferenceRewardModel.reward_model_name) + self.model = AutoModelForCausalLM.from_pretrained(DirectPreferenceRewardModel.reward_model_name, + torch_dtype=torch.float16).to(self.device) + + def reward_single(self, prompt: str, completion: str, name: str) -> float: + r""" Calculates a direct preference optimization (DPO) style reward for a completion, + which is a reference model's average log-probability for completion tokens given a prompt. + Uses guidance from https://github.com/eric-mitchell/direct-preference-optimization/blob/main/trainers.py. + """ + with torch.no_grad(): + # Tokenize the combined prompt + completion. + combined = self.tokenizer(prompt + completion, return_tensors="pt").input_ids[0].to(self.device) # [seq_len] + # Tokenize only the prompt, to help determine prompt token length. + prompt_part = self.tokenizer(prompt, return_tensors="pt").input_ids[0].to(self.device) # [prompt_len] + # Ensure that the prompt_part tokens align with the combined tokens. + assert (prompt_part == combined[:len(prompt_part)]).all() + + labels = combined.clone() # [seq_len] + # Label only each next token prediction ground-truth. + labels = labels[1:] # [seq_len-1] + # Ignore prompt part for calculating reward. + labels[1:len(prompt_part)] = -100 + loss_mask = (labels != -100) # [seq_len-1] + + # Dummy token to allow for indexing, but loss will be ignored. + labels[labels == -100] = 0 + # Reshape for gather operation. + labels = labels.unsqueeze(0).unsqueeze(2) # [batch_size=1, seq_len-1, :] + + # Forward pass to calculate logit predictions for each sequence position. + logits = self.model(combined.unsqueeze(0)).logits # [batch_size=1, seq_len, vocab_len] + # Predict only where labels are available + logits = logits[:, :-1, :] # [batch_size=1, seq_len-1, vocab_len] + # Rescale via log(softmax(logits)). + logits = logits.log_softmax(-1) + # Calculate the model's log-probability for each actual completion token. + per_token_logps = torch.gather(logits, dim=2, index=labels).squeeze(2) # [batch_size=1, seq_len-1] + # Average log-probability over completion sequence. + reward = (per_token_logps * loss_mask).sum(-1) / loss_mask.sum(-1) # [batch_size=1] + reward = reward[0].cpu().detach() + + return reward + + def get_rewards(self, prompt: str, completions: List[str], name: str) -> torch.FloatTensor: + return torch.tensor([self.reward_single(prompt, completion, name) for completion in completions], + dtype=torch.float32).to(self.device) From 8fe9acc9a65dfa72cdc9a527b49d9a9972414eab Mon Sep 17 00:00:00 2001 From: opentaco Date: Thu, 20 Jul 2023 19:47:16 +0200 Subject: [PATCH 02/10] Maint comment update --- openvalidators/reward/dpo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openvalidators/reward/dpo.py b/openvalidators/reward/dpo.py index 780f18f..9fc0b02 100644 --- a/openvalidators/reward/dpo.py +++ b/openvalidators/reward/dpo.py @@ -63,7 +63,7 @@ def reward_single(self, prompt: str, completion: str, name: str) -> float: # Forward pass to calculate logit predictions for each sequence position. logits = self.model(combined.unsqueeze(0)).logits # [batch_size=1, seq_len, vocab_len] - # Predict only where labels are available + # Predict only where labels are available. logits = logits[:, :-1, :] # [batch_size=1, seq_len-1, vocab_len] # Rescale via log(softmax(logits)). logits = logits.log_softmax(-1) From 01f0c3f6dfff3165dfedb8e929a3329c4178808f Mon Sep 17 00:00:00 2001 From: opentaco Date: Thu, 20 Jul 2023 20:36:46 +0200 Subject: [PATCH 03/10] Support Llama-2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 18de33a..d8bf1a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bittensor==5.2.0 -transformers<=4.28.0 +transformers<=4.31.0 wandb==0.15.3 datasets==2.12.0 plotly==5.14.1 From 4b2d7fe1a4e02b6ce320790391b93308b82b3d50 Mon Sep 17 00:00:00 2001 From: opentaco Date: Thu, 20 Jul 2023 21:00:16 +0200 Subject: [PATCH 04/10] Update token handling --- openvalidators/reward/dpo.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openvalidators/reward/dpo.py b/openvalidators/reward/dpo.py index 9fc0b02..45f58db 100644 --- a/openvalidators/reward/dpo.py +++ b/openvalidators/reward/dpo.py @@ -46,14 +46,12 @@ def reward_single(self, prompt: str, completion: str, name: str) -> float: combined = self.tokenizer(prompt + completion, return_tensors="pt").input_ids[0].to(self.device) # [seq_len] # Tokenize only the prompt, to help determine prompt token length. prompt_part = self.tokenizer(prompt, return_tensors="pt").input_ids[0].to(self.device) # [prompt_len] - # Ensure that the prompt_part tokens align with the combined tokens. - assert (prompt_part == combined[:len(prompt_part)]).all() labels = combined.clone() # [seq_len] + # Ignore prompt part for calculating reward. + labels[:len(prompt_part)] = -100 # Label only each next token prediction ground-truth. labels = labels[1:] # [seq_len-1] - # Ignore prompt part for calculating reward. - labels[1:len(prompt_part)] = -100 loss_mask = (labels != -100) # [seq_len-1] # Dummy token to allow for indexing, but loss will be ignored. From 59b7ec0b92a4b5eb0ee472001e666a90593b0c5c Mon Sep 17 00:00:00 2001 From: opentaco Date: Thu, 20 Jul 2023 21:01:39 +0200 Subject: [PATCH 05/10] Update requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d8bf1a2..7a0758d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bittensor==5.2.0 -transformers<=4.31.0 +transformers>=4.31.0 wandb==0.15.3 datasets==2.12.0 plotly==5.14.1 From d1e1f9f55bc89998074c335badeee5449b1d9aef Mon Sep 17 00:00:00 2001 From: opentaco Date: Thu, 20 Jul 2023 21:03:10 +0200 Subject: [PATCH 06/10] Update requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a0758d..d8bf1a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ bittensor==5.2.0 -transformers>=4.31.0 +transformers<=4.31.0 wandb==0.15.3 datasets==2.12.0 plotly==5.14.1 From e8f7559de0235cd1d9ea1a2594070c5a89417a76 Mon Sep 17 00:00:00 2001 From: opentaco Date: Fri, 21 Jul 2023 17:00:55 +0200 Subject: [PATCH 07/10] Add trace --- openvalidators/reward/dpo.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openvalidators/reward/dpo.py b/openvalidators/reward/dpo.py index 45f58db..83f0b26 100644 --- a/openvalidators/reward/dpo.py +++ b/openvalidators/reward/dpo.py @@ -16,6 +16,7 @@ # DEALINGS IN THE SOFTWARE. import torch +import bittensor as bt from typing import List from .config import RewardModelType from .reward import BaseRewardModel @@ -53,7 +54,8 @@ def reward_single(self, prompt: str, completion: str, name: str) -> float: # Label only each next token prediction ground-truth. labels = labels[1:] # [seq_len-1] loss_mask = (labels != -100) # [seq_len-1] - + bt.logging.trace(f"DirectPreferenceRewardModel | len(combined)={len(combined)}, " + f"len(prompt)={len(prompt_part)}") # Dummy token to allow for indexing, but loss will be ignored. labels[labels == -100] = 0 # Reshape for gather operation. @@ -63,6 +65,8 @@ def reward_single(self, prompt: str, completion: str, name: str) -> float: logits = self.model(combined.unsqueeze(0)).logits # [batch_size=1, seq_len, vocab_len] # Predict only where labels are available. logits = logits[:, :-1, :] # [batch_size=1, seq_len-1, vocab_len] + bt.logging.trace(f"DirectPreferenceRewardModel | logits: {logits}") + # Rescale via log(softmax(logits)). logits = logits.log_softmax(-1) # Calculate the model's log-probability for each actual completion token. @@ -71,6 +75,8 @@ def reward_single(self, prompt: str, completion: str, name: str) -> float: reward = (per_token_logps * loss_mask).sum(-1) / loss_mask.sum(-1) # [batch_size=1] reward = reward[0].cpu().detach() + bt.logging.trace(f"DirectPreferenceRewardModel | reward: {reward}") + return reward def get_rewards(self, prompt: str, completions: List[str], name: str) -> torch.FloatTensor: From 7e8a0d08ed2aa757dab67865dd74921c0c47536a Mon Sep 17 00:00:00 2001 From: opentaco Date: Fri, 21 Jul 2023 17:54:20 +0200 Subject: [PATCH 08/10] Handle nan/inf --- openvalidators/reward/dpo.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openvalidators/reward/dpo.py b/openvalidators/reward/dpo.py index 83f0b26..ea1f493 100644 --- a/openvalidators/reward/dpo.py +++ b/openvalidators/reward/dpo.py @@ -54,8 +54,6 @@ def reward_single(self, prompt: str, completion: str, name: str) -> float: # Label only each next token prediction ground-truth. labels = labels[1:] # [seq_len-1] loss_mask = (labels != -100) # [seq_len-1] - bt.logging.trace(f"DirectPreferenceRewardModel | len(combined)={len(combined)}, " - f"len(prompt)={len(prompt_part)}") # Dummy token to allow for indexing, but loss will be ignored. labels[labels == -100] = 0 # Reshape for gather operation. @@ -65,7 +63,6 @@ def reward_single(self, prompt: str, completion: str, name: str) -> float: logits = self.model(combined.unsqueeze(0)).logits # [batch_size=1, seq_len, vocab_len] # Predict only where labels are available. logits = logits[:, :-1, :] # [batch_size=1, seq_len-1, vocab_len] - bt.logging.trace(f"DirectPreferenceRewardModel | logits: {logits}") # Rescale via log(softmax(logits)). logits = logits.log_softmax(-1) @@ -75,10 +72,13 @@ def reward_single(self, prompt: str, completion: str, name: str) -> float: reward = (per_token_logps * loss_mask).sum(-1) / loss_mask.sum(-1) # [batch_size=1] reward = reward[0].cpu().detach() - bt.logging.trace(f"DirectPreferenceRewardModel | reward: {reward}") - - return reward + # NaNs can possibly arise through log(0)=-inf, replace with suitably small logits. + if torch.isnan(reward) or torch.isinf(reward): + return -11. # exp(-11)=1.67e-5 < 2e-5=1/50257 (typical vocab size) + return reward.item() def get_rewards(self, prompt: str, completions: List[str], name: str) -> torch.FloatTensor: - return torch.tensor([self.reward_single(prompt, completion, name) for completion in completions], - dtype=torch.float32).to(self.device) + rewards = torch.tensor([self.reward_single(prompt, completion, name) for completion in completions], + dtype=torch.float32).to(self.device) + bt.logging.trace(f"DirectPreferenceRewardModel | rewards: {rewards.tolist()}") + return rewards From 2996a22db788d0ac6b7155f626d23c01a63602d4 Mon Sep 17 00:00:00 2001 From: opentaco Date: Thu, 27 Jul 2023 20:00:38 +0200 Subject: [PATCH 09/10] Use BTLM and check max seq length --- openvalidators/reward/dpo.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openvalidators/reward/dpo.py b/openvalidators/reward/dpo.py index ea1f493..3a5860b 100644 --- a/openvalidators/reward/dpo.py +++ b/openvalidators/reward/dpo.py @@ -25,7 +25,7 @@ class DirectPreferenceRewardModel(BaseRewardModel): - reward_model_name: str = "TheBloke/Llama-2-7B-fp16" + reward_model_name: str = "cerebras/btlm-3b-8k-base" @property def name(self) -> str: return RewardModelType.dpo.value @@ -35,6 +35,7 @@ def __init__(self, device: str): self.device = device self.tokenizer = AutoTokenizer.from_pretrained(DirectPreferenceRewardModel.reward_model_name) self.model = AutoModelForCausalLM.from_pretrained(DirectPreferenceRewardModel.reward_model_name, + trust_remote_code=True, torch_dtype=torch.float16).to(self.device) def reward_single(self, prompt: str, completion: str, name: str) -> float: @@ -48,6 +49,14 @@ def reward_single(self, prompt: str, completion: str, name: str) -> float: # Tokenize only the prompt, to help determine prompt token length. prompt_part = self.tokenizer(prompt, return_tensors="pt").input_ids[0].to(self.device) # [prompt_len] + # Completion doesn't fit into model sequence, so return lowest reward. + if self.tokenizer.model_max_length <= len(prompt_part): + return -11. # exp(-11)=1.67e-5 < 2e-5=1/50257 (typical vocab size) + + # Truncate combined to fit into model max sequence length. + if self.tokenizer.model_max_length < len(combined): + combined = combined[:self.tokenizer.model_max_length] + labels = combined.clone() # [seq_len] # Ignore prompt part for calculating reward. labels[:len(prompt_part)] = -100 From e1519d17ac61da8e7a183d400d81543f9cadf788 Mon Sep 17 00:00:00 2001 From: opentaco Date: Thu, 24 Aug 2023 11:01:57 +0200 Subject: [PATCH 10/10] Add DPO to test --- tests/test_event.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_event.py b/tests/test_event.py index 7fb9f2b..ff366f5 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -42,6 +42,7 @@ def test_event_from_dict_all_forward_columns_match(self): RewardModelType.nsfw.value: [1.0], RewardModelType.reciprocate.value: [1.0], RewardModelType.diversity.value: [1.0], + RewardModelType.dpo.value: [1.0], RewardModelType.rlhf.value: [1.0], RewardModelType.prompt.value: [1.0], RewardModelType.relevance.value: [1.0], @@ -100,6 +101,7 @@ def test_event_from_dict_forward_no_reward_logging(self): assert event.nsfw_filter is None assert event.reciprocate_reward_model is None assert event.diversity_reward_model is None + assert event.dpo_reward_model is None assert event.rlhf_reward_model is None assert event.prompt_reward_model is None assert event.relevance_filter is None @@ -141,6 +143,7 @@ def test_event_from_dict_forward_reward_logging_mismatch(self): assert event.nsfw_filter is None assert event.reciprocate_reward_model is None assert event.diversity_reward_model is None + assert event.dpo_reward_model is None assert event.rlhf_reward_model is None assert event.prompt_reward_model is None assert event.relevance_filter is None