Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion conversion/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@
if weight_map is None or not isinstance(weight_map, dict):
raise ValueError(f"Can't load 'weight_map' from {index_name!r}")
tensor_names_from_index.update(weight_map.keys())
part_dict: dict[str, None] = dict.fromkeys(weight_map.values(), None) # ty: ignore[invalid-assignment]

Check warning on line 257 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:257:91: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
part_names = sorted(part_dict.keys())
else:
weight_map = {}
Expand Down Expand Up @@ -1586,15 +1586,15 @@

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(self.dir_model)
vocab_size = self.hparams.get("vocab_size", len(tokenizer.vocab)) # ty: ignore[unresolved-attribute]

Check warning on line 1589 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1589:76: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
assert max(tokenizer.vocab.values()) < vocab_size # ty: ignore[unresolved-attribute]

Check warning on line 1590 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1590:60: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

tokpre = self.get_vocab_base_pre(tokenizer)

reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]

Check warning on line 1594 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1594:93: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
added_vocab = tokenizer.get_added_vocab() # ty: ignore[unresolved-attribute]

Check warning on line 1595 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1595:52: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

added_tokens_decoder = tokenizer.added_tokens_decoder # ty: ignore[unresolved-attribute]

Check warning on line 1597 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1597:64: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

for i in range(vocab_size):
if i not in reverse_vocab:
Expand All @@ -1607,7 +1607,7 @@
# To avoid unexpected issues - we make sure to normalize non-normalized tokens
if not added_tokens_decoder[i].normalized:
previous_token = token
token = tokenizer.decode(tokenizer.encode(token, add_special_tokens=False)) # ty: ignore[unresolved-attribute, invalid-assignment]

Check warning on line 1610 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1610:102: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
if previous_token != token:
logger.info(f"{repr(previous_token)} is encoded and decoded back to {repr(token)} using AutoTokenizer")

Expand Down Expand Up @@ -1989,10 +1989,10 @@
def _set_vocab_hybriddna(self):
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(self.dir_model, trust_remote_code=True)
vocab_size = self.hparams.get("vocab_size", len(tokenizer.vocab)) # ty: ignore[unresolved-attribute]

Check warning on line 1992 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1992:76: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
assert max(tokenizer.vocab.values()) < vocab_size # ty: ignore[unresolved-attribute]

Check warning on line 1993 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1993:60: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment

reverse_vocab = {id_: encoded_tok for encoded_tok, id_ in tokenizer.vocab.items()} # ty: ignore[unresolved-attribute]

Check warning on line 1995 in conversion/base.py

View workflow job for this annotation

GitHub Actions / python type-check

ty (unused-ignore-comment)

conversion/base.py:1995:93: unused-ignore-comment: Unused `ty: ignore` directive help: Remove the unused suppression comment
# k-mers can share text with a base-vocab BPE token (e.g. CCCCCC) and get
# dropped by get_vocab(); a reserved marker suffix (U+E000) keeps each
# k-mer's own id (llama.cpp strips it on detokenization)
Expand Down Expand Up @@ -2505,6 +2505,14 @@
with open(tokenizer_config_path, "r", encoding="utf-8") as f:
tokenizer_config = json.load(f)

tokenizer_class = tokenizer_config.get("tokenizer_class")
if tokenizer_class == "Plamo2Tokenizer":
tokenizer_model = "plamo2"
elif tokenizer_class == "Plamo3Tokenizer":
tokenizer_model = "plamo3"
else:
raise ValueError(f"Unsupported PLaMo tokenizer class: {tokenizer_class}")

# Load tokens from JSONL file (actually a list format)
tokens = []
scores = []
Expand Down Expand Up @@ -2544,7 +2552,7 @@
scores.append(-1000.0)
toktypes.append(gguf.TokenType.UNUSED)

self.gguf_writer.add_tokenizer_model("plamo2")
self.gguf_writer.add_tokenizer_model(tokenizer_model)
self.gguf_writer.add_tokenizer_pre("default")
self.gguf_writer.add_token_list(tokens)
self.gguf_writer.add_token_scores(scores)
Expand Down
1 change: 1 addition & 0 deletions include/llama.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ extern "C" {
LLAMA_VOCAB_TYPE_RWKV = 5, // RWKV tokenizer based on greedy tokenization
LLAMA_VOCAB_TYPE_PLAMO2 = 6, // PLaMo-2 tokenizer based on Aho-Corasick with dynamic programming
LLAMA_VOCAB_TYPE_TEST = 7, // Dummy tokenizer for testing: rolling hash of fixed-size chunks -> tokens, tokens -> hex
LLAMA_VOCAB_TYPE_PLAMO3 = 8, // PLaMo-3 tokenizer with pre-segmentation and dynamic programming
};

enum llama_rope_type {
Expand Down
109 changes: 97 additions & 12 deletions src/llama-vocab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1395,7 +1395,7 @@ struct llm_tokenizer_rwkv_session {
};

struct llm_tokenizer_plamo2 : llm_tokenizer {
llm_tokenizer_plamo2(const llama_vocab & vocab) {
llm_tokenizer_plamo2(const llama_vocab & vocab, bool pre_segment) : pre_segment_(pre_segment) {
build(vocab);
}

Expand Down Expand Up @@ -1538,11 +1538,87 @@ struct llm_tokenizer_plamo2 : llm_tokenizer {

std::vector<llama_token> encode(const std::string & text) const {
std::vector<uint32_t> unicode_data = unicode_cpts_from_utf8(text);
// Skip the first code point if it is a BOM (Byte Order Mark)
if (!unicode_data.empty() && unicode_data[0] == 0xFEFF) {
unicode_data.erase(unicode_data.begin());
// The PLaMo-3 tokenizer keeps a leading U+FEFF in the input.
if (!pre_segment_) {
if (!unicode_data.empty() && unicode_data[0] == 0xFEFF) {
unicode_data.erase(unicode_data.begin());
}
return encode_cpts(unicode_data);
}

const size_t n = unicode_data.size();
std::vector<bool> cut(n + 1, false);

// pass 1: <|plamo:...|>
{
static constexpr uint32_t prefix[] = { '<', '|', 'p', 'l', 'a', 'm', 'o', ':' };
const size_t prefix_len = std::size(prefix);
size_t i = 0;
while (i + prefix_len <= n) {
if (!std::equal(prefix, prefix + prefix_len, unicode_data.begin() + i)) {
i++;
continue;
}
// An empty body is valid.
size_t j = i + prefix_len;
// Treat U+001C..U+001F as whitespace (equivalent to Python \s).
while (j < n && j - (i + prefix_len) < 64 && unicode_data[j] != '|' &&
(unicode_data[j] < 0x1C || unicode_data[j] > 0x1F) &&
!unicode_cpt_flags_from_cpt(unicode_data[j]).is_whitespace) {
j++;
}
if (j + 1 < n && unicode_data[j] == '|' && unicode_data[j + 1] == '>') {
cut[i] = true;
cut[j + 2] = true;
i = j + 2;
} else {
i++;
}
}
}

// pass 2: runs of repeated characters / spaces (a run never crosses a boundary from pass 1)
{
size_t i = 0;
while (i < n) {
const uint32_t c = unicode_data[i];
size_t run = 1;
while (i + run < n && unicode_data[i + run] == c && !cut[i + run]) {
run++;
}

const bool is_repeated_chars = c != '\n' && run >= 4;
const bool is_spaces = c == ' ' && run >= 2;
if (is_repeated_chars || is_spaces) {
cut[i] = true;
cut[i + run] = true;
}

// a run that does not match cannot match at any later position either (it only gets shorter)
i += run;
}
}

std::vector<llama_token> output;
size_t seg_start = 0;
for (size_t seg_end = 0; seg_end <= n; ++seg_end) {
// U+EE00 is the tokenizer's private-use boundary marker; literal occurrences split segments and are not emitted.
const bool is_boundary = seg_end < n && unicode_data[seg_end] == 0xEE00;
if (seg_end == n || cut[seg_end] || is_boundary) {
if (seg_start < seg_end) {
const std::vector<uint32_t> segment(unicode_data.begin() + seg_start,
unicode_data.begin() + seg_end);
const std::vector<llama_token> tokens = encode_cpts(segment);
output.insert(output.end(), tokens.begin(), tokens.end());
}
seg_start = seg_end + is_boundary;
}
}

return output;
}

std::vector<llama_token> encode_cpts(const std::vector<uint32_t> & unicode_data) const {
if (unicode_data.empty()) {
return {};
}
Expand Down Expand Up @@ -1661,6 +1737,8 @@ struct llm_tokenizer_plamo2 : llm_tokenizer {
// Flattened table representing the Trie structure
// Each row contains: [piece_length, token_id, score, piece_id]
std::vector<std::vector<int32_t>> table_;

const bool pre_segment_;
};

struct llm_tokenizer_plamo2_session {
Expand Down Expand Up @@ -2127,10 +2205,10 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) {
special_sep_id = LLAMA_TOKEN_NULL;
special_pad_id = LLAMA_TOKEN_NULL;
special_mask_id = LLAMA_TOKEN_NULL;
} else if (tokenizer_model == "plamo2") {
type = LLAMA_VOCAB_TYPE_PLAMO2;
} else if (tokenizer_model == "plamo2" || tokenizer_model == "plamo3") {
type = tokenizer_model == "plamo2" ? LLAMA_VOCAB_TYPE_PLAMO2 : LLAMA_VOCAB_TYPE_PLAMO3;

// PLaMo-2 default special tokens (these will be overridden by model config)
// PLaMo default special tokens (these will be overridden by model config)
special_bos_id = 1; // <|plamo:bos|>
special_eos_id = 2; // <|plamo:eos|>
special_unk_id = 0; // <|plamo:unk|>
Expand Down Expand Up @@ -3183,6 +3261,7 @@ std::string llama_vocab::impl::type_name() const{
case LLAMA_VOCAB_TYPE_UGM: return "UGM";
case LLAMA_VOCAB_TYPE_RWKV: return "RWKV";
case LLAMA_VOCAB_TYPE_PLAMO2: return "PLaMo2";
case LLAMA_VOCAB_TYPE_PLAMO3: return "PLaMo3";
case LLAMA_VOCAB_TYPE_TEST: return "TEST";
default: return "unknown";
}
Expand Down Expand Up @@ -3270,7 +3349,10 @@ void llama_vocab::impl::init_tokenizer(enum llama_vocab_type type) {
tokenizer = std::make_unique<llm_tokenizer_rwkv>(vocab);
break;
case LLAMA_VOCAB_TYPE_PLAMO2:
tokenizer = std::make_unique<llm_tokenizer_plamo2>(vocab);
tokenizer = std::make_unique<llm_tokenizer_plamo2>(vocab, false);
break;
case LLAMA_VOCAB_TYPE_PLAMO3:
tokenizer = std::make_unique<llm_tokenizer_plamo2>(vocab, true);
break;
case LLAMA_VOCAB_TYPE_TEST:
tokenizer = std::make_unique<llm_tokenizer>();
Expand Down Expand Up @@ -3632,6 +3714,7 @@ std::vector<llama_token> llama_vocab::impl::tokenize(
}
} break;
case LLAMA_VOCAB_TYPE_PLAMO2:
case LLAMA_VOCAB_TYPE_PLAMO3:
{
if (add_special && add_bos) {
GGML_ASSERT(special_bos_id != LLAMA_TOKEN_NULL);
Expand Down Expand Up @@ -3803,8 +3886,9 @@ int32_t llama_vocab::impl::token_to_piece(llama_token token, char * buf, int32_t
std::string result = format("%x", token);
return _try_copy(result.data(), result.size());
}
case LLAMA_VOCAB_TYPE_PLAMO2: {
// PLaMo-2 uses similar token handling as BPE/SPM
case LLAMA_VOCAB_TYPE_PLAMO2:
case LLAMA_VOCAB_TYPE_PLAMO3: {
// PLaMo uses similar token handling as BPE/SPM
if (vocab.is_byte(token)) {
// Handle byte tokens like <0xXX>
if (token_text.length() == 6 && token_text.substr(0, 3) == "<0x" && token_text.back() == '>') {
Expand Down Expand Up @@ -4067,8 +4151,9 @@ llama_token llama_vocab::byte_to_token(uint8_t ch) const {
case LLAMA_VOCAB_TYPE_BPE: {
return pimpl->token_to_id.at(unicode_byte_to_utf8(ch));
}
case LLAMA_VOCAB_TYPE_PLAMO2: {
// PLaMo-2 uses byte tokens in format <0xXX>
case LLAMA_VOCAB_TYPE_PLAMO2:
case LLAMA_VOCAB_TYPE_PLAMO3: {
// PLaMo uses byte tokens in format <0xXX>
char hex_str[8];
snprintf(hex_str, sizeof(hex_str), "<0x%02X>", ch);
return pimpl->token_to_id.at(hex_str);
Expand Down
Loading