kashif HF Staff commited on
Commit
95c3c68
·
verified ·
1 Parent(s): 86d5f06

Support return_assistant_tokens_mask on slow HybridDNATokenizer for completion-only SFT (apply_chat_template token-boundary mask override)

Browse files
Files changed (1) hide show
  1. tokenizer.py +55 -0
tokenizer.py CHANGED
@@ -519,6 +519,61 @@ class HybridDNATokenizer(PreTrainedTokenizer):
519
 
520
  return BatchEncoding(result, tensor_type=return_tensors)
521
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  def save_vocabulary(self, save_directory: str, filename_prefix: Optional[str] = None) -> Tuple[str]:
523
  vocab_file = os.path.join(
524
  save_directory,
 
519
 
520
  return BatchEncoding(result, tensor_type=return_tensors)
521
 
522
+ # Markers delimiting an assistant turn, used to build the assistant-token mask.
523
+ # Qwen chat format; override on the instance for other bases (e.g. Gemma uses
524
+ # "<start_of_turn>model\n" / "<end_of_turn>").
525
+ ASSISTANT_HEADER = "<|im_start|>assistant\n"
526
+ TURN_END = "<|im_end|>"
527
+
528
+ def apply_chat_template(self, conversation, *args, return_assistant_tokens_mask: bool = False, **kwargs):
529
+ """Completion-only assistant mask WITHOUT a fast tokenizer.
530
+
531
+ HF derives the assistant mask from ``{% generation %}`` char-spans via
532
+ ``BatchEncoding.char_to_token()``, which only fast (Rust) tokenizers implement.
533
+ HybridDNATokenizer is a slow Python tokenizer (the DNA k-mer logic can't be a
534
+ ``tokenizer.json``), so that path raises
535
+ ``char_to_token() is not available when using Python based tokenizers``.
536
+
537
+ Instead we tokenize normally and mark the tokens of every assistant turn -- from
538
+ just after ``ASSISTANT_HEADER`` up to and including the next ``TURN_END``. This
539
+ makes ``return_assistant_tokens_mask=True`` (and thus TRL ``assistant_only_loss``)
540
+ work unchanged, and needs no ``{% generation %}`` markers in the template.
541
+ """
542
+ if not return_assistant_tokens_mask:
543
+ return super().apply_chat_template(conversation, *args, **kwargs)
544
+ kwargs.pop("return_assistant_tokens_mask", None)
545
+ want_tensors = kwargs.pop("return_tensors", None)
546
+ kwargs["tokenize"] = True
547
+ kwargs["return_dict"] = True
548
+ enc = super().apply_chat_template(conversation, *args, return_tensors=None, **kwargs)
549
+ seqs = enc["input_ids"]
550
+ batched = len(seqs) > 0 and isinstance(seqs[0], (list, tuple))
551
+ batch = seqs if batched else [seqs]
552
+ hdr = self(self.ASSISTANT_HEADER, add_special_tokens=False)["input_ids"]
553
+ end = self(self.TURN_END, add_special_tokens=False)["input_ids"]
554
+ masks = []
555
+ for ids in batch:
556
+ ids = list(ids)
557
+ m = [0] * len(ids)
558
+ i = 0
559
+ while i <= len(ids) - len(hdr):
560
+ if ids[i:i + len(hdr)] == hdr:
561
+ j = i + len(hdr)
562
+ while j < len(ids) and ids[j:j + len(end)] != end:
563
+ m[j] = 1
564
+ j += 1
565
+ for k in range(len(end)): # include the turn-end token(s)
566
+ if j + k < len(ids):
567
+ m[j + k] = 1
568
+ i = j + len(end)
569
+ else:
570
+ i += 1
571
+ masks.append(m)
572
+ enc["assistant_masks"] = masks if batched else masks[0]
573
+ if want_tensors is not None:
574
+ enc = enc.convert_to_tensors(tensor_type=want_tensors)
575
+ return enc
576
+
577
  def save_vocabulary(self, save_directory: str, filename_prefix: Optional[str] = None) -> Tuple[str]:
578
  vocab_file = os.path.join(
579
  save_directory,