| // Copyright 2026 The IREE Authors | |
| // | |
| // Licensed under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| #include "iree/tokenizer/postprocessor.h" | |
| #include "iree/base/api.h" | |
| #include "iree/tokenizer/vocab/vocab.h" | |
| //===----------------------------------------------------------------------===// | |
| // Validation | |
| //===----------------------------------------------------------------------===// | |
| static iree_status_t iree_tokenizer_postprocessor_validate_template( | |
| const iree_tokenizer_postprocessor_template_t* t, | |
| const char* template_name) { | |
| uint8_t total = iree_tokenizer_postprocessor_template_total_count(t); | |
| if (total > IREE_TOKENIZER_POSTPROCESSOR_MAX_PIECES) { | |
| return iree_make_status( | |
| IREE_STATUS_INVALID_ARGUMENT, | |
| "postprocessor %s template has %u total pieces (max %u)", template_name, | |
| (unsigned)total, (unsigned)IREE_TOKENIZER_POSTPROCESSOR_MAX_PIECES); | |
| } | |
| return iree_ok_status(); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Lifecycle | |
| //===----------------------------------------------------------------------===// | |
| iree_status_t iree_tokenizer_postprocessor_initialize( | |
| const iree_tokenizer_postprocessor_template_t* single_template, | |
| const iree_tokenizer_postprocessor_template_t* pair_template, | |
| iree_tokenizer_postprocessor_flags_t flags, | |
| iree_tokenizer_postprocessor_t* out_postprocessor) { | |
| IREE_ASSERT_ARGUMENT(single_template); | |
| IREE_ASSERT_ARGUMENT(out_postprocessor); | |
| IREE_TRACE_ZONE_BEGIN(z0); | |
| memset(out_postprocessor, 0, sizeof(*out_postprocessor)); | |
| // Validate template sizes. | |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( | |
| z0, iree_tokenizer_postprocessor_validate_template(single_template, | |
| "single")); | |
| if (pair_template) { | |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( | |
| z0, | |
| iree_tokenizer_postprocessor_validate_template(pair_template, "pair")); | |
| } | |
| // Copy templates (fixed-size inline structs). | |
| memcpy(&out_postprocessor->single, single_template, | |
| sizeof(iree_tokenizer_postprocessor_template_t)); | |
| if (pair_template) { | |
| memcpy(&out_postprocessor->pair, pair_template, | |
| sizeof(iree_tokenizer_postprocessor_template_t)); | |
| out_postprocessor->has_pair = true; | |
| } | |
| out_postprocessor->flags = flags; | |
| IREE_TRACE_ZONE_END(z0); | |
| return iree_ok_status(); | |
| } | |
| void iree_tokenizer_postprocessor_deinitialize( | |
| iree_tokenizer_postprocessor_t* postprocessor) { | |
| if (!postprocessor) return; | |
| IREE_TRACE_ZONE_BEGIN(z0); | |
| memset(postprocessor, 0, sizeof(*postprocessor)); | |
| IREE_TRACE_ZONE_END(z0); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Encode Operations | |
| //===----------------------------------------------------------------------===// | |
| // Emits special tokens from a template phase into the token output. | |
| // Writes tokens starting at |output_offset|, bounded by output.capacity. | |
| // |base| selects the starting index in the template's arrays. | |
| // |position| tracks progress (in/out) and reaches |count| when complete. | |
| // Special tokens get zero-length offsets (no corresponding source text). | |
| static iree_host_size_t iree_tokenizer_postprocessor_emit_phase( | |
| const iree_tokenizer_postprocessor_template_t* template_data, uint8_t base, | |
| uint8_t count, uint8_t* IREE_RESTRICT position, | |
| iree_tokenizer_token_output_t output, iree_host_size_t output_offset) { | |
| iree_host_size_t emitted = 0; | |
| while (*position < count) { | |
| // Check for available capacity with overflow protection. | |
| iree_host_size_t out_index = 0; | |
| if (!iree_host_size_checked_add(output_offset, emitted, &out_index) || | |
| out_index >= output.capacity) { | |
| break; // Overflow or no room. | |
| } | |
| uint8_t template_index = base + *position; | |
| output.token_ids[out_index] = template_data->token_ids[template_index]; | |
| if (output.type_ids) { | |
| output.type_ids[out_index] = template_data->type_ids[template_index]; | |
| } | |
| if (output.token_offsets) { | |
| output.token_offsets[out_index].start = 0; | |
| output.token_offsets[out_index].end = 0; | |
| } | |
| ++(*position); | |
| ++emitted; | |
| } | |
| return emitted; | |
| } | |
| iree_host_size_t iree_tokenizer_postprocessor_emit_prefix( | |
| iree_tokenizer_postprocessor_encode_state_t* state, | |
| iree_tokenizer_token_output_t output, iree_host_size_t output_offset) { | |
| if (state->phase != IREE_TOKENIZER_POSTPROCESSOR_PHASE_PREFIX) return 0; | |
| iree_host_size_t emitted = iree_tokenizer_postprocessor_emit_phase( | |
| state->active_template, /*base=*/0, state->active_template->prefix_count, | |
| &state->position, output, output_offset); | |
| if (state->position >= state->active_template->prefix_count) { | |
| state->phase = IREE_TOKENIZER_POSTPROCESSOR_PHASE_SEQUENCE_A; | |
| state->position = 0; | |
| } | |
| return emitted; | |
| } | |
| iree_host_size_t iree_tokenizer_postprocessor_emit_infix( | |
| iree_tokenizer_postprocessor_encode_state_t* state, | |
| iree_tokenizer_token_output_t output, iree_host_size_t output_offset) { | |
| if (state->phase != IREE_TOKENIZER_POSTPROCESSOR_PHASE_INFIX) return 0; | |
| uint8_t base = state->active_template->prefix_count; | |
| iree_host_size_t emitted = iree_tokenizer_postprocessor_emit_phase( | |
| state->active_template, base, state->active_template->infix_count, | |
| &state->position, output, output_offset); | |
| if (state->position >= state->active_template->infix_count) { | |
| state->phase = IREE_TOKENIZER_POSTPROCESSOR_PHASE_SEQUENCE_B; | |
| state->position = 0; | |
| } | |
| return emitted; | |
| } | |
| void iree_tokenizer_postprocessor_assign_type_ids( | |
| const iree_tokenizer_postprocessor_encode_state_t* state, | |
| iree_tokenizer_token_output_t output, iree_host_size_t offset, | |
| iree_host_size_t count) { | |
| if (!output.type_ids || count == 0) return; | |
| uint8_t type_id = 0; | |
| if (state->phase == IREE_TOKENIZER_POSTPROCESSOR_PHASE_SEQUENCE_A) { | |
| type_id = state->active_template->sequence_a_type_id; | |
| } else if (state->phase == IREE_TOKENIZER_POSTPROCESSOR_PHASE_SEQUENCE_B) { | |
| type_id = state->active_template->sequence_b_type_id; | |
| } else { | |
| return; | |
| } | |
| // Bounds check with overflow protection: offset + count must not exceed | |
| // capacity and must not overflow. | |
| iree_host_size_t end = 0; | |
| if (!iree_host_size_checked_add(offset, count, &end) || | |
| end > output.capacity) { | |
| return; // Overflow or out of bounds. | |
| } | |
| memset(&output.type_ids[offset], type_id, count); | |
| } | |
| iree_host_size_t iree_tokenizer_postprocessor_emit_suffix( | |
| iree_tokenizer_postprocessor_encode_state_t* state, | |
| iree_tokenizer_token_output_t output, iree_host_size_t output_offset) { | |
| if (state->phase != IREE_TOKENIZER_POSTPROCESSOR_PHASE_SUFFIX) return 0; | |
| uint8_t base = state->active_template->prefix_count + | |
| state->active_template->infix_count; | |
| iree_host_size_t emitted = iree_tokenizer_postprocessor_emit_phase( | |
| state->active_template, base, state->active_template->suffix_count, | |
| &state->position, output, output_offset); | |
| if (state->position >= state->active_template->suffix_count) { | |
| state->phase = IREE_TOKENIZER_POSTPROCESSOR_PHASE_DONE; | |
| } | |
| return emitted; | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Offset Trimming | |
| //===----------------------------------------------------------------------===// | |
| // Counts leading whitespace in a token string, returning the number of bytes | |
| // in the original input that should be trimmed from offset.start. | |
| // | |
| // ByteLevel encoding uses special Unicode characters to represent whitespace: | |
| // U+0120 (Ġ, UTF-8: 0xC4 0xA0) represents space (0x20) | |
| // U+010A (Ċ, UTF-8: 0xC4 0x8A) represents newline (0x0A) | |
| // U+010D (ċ, UTF-8: 0xC4 0x8D) represents carriage return (0x0D) | |
| // Each sentinel in the token text corresponds to exactly 1 byte in the | |
| // original input. | |
| // | |
| // Returns the count in terms of ORIGINAL INPUT bytes, not token text bytes. | |
| static iree_host_size_t iree_tokenizer_postprocessor_count_leading_whitespace( | |
| iree_string_view_t text) { | |
| iree_host_size_t count = 0; | |
| iree_host_size_t position = 0; | |
| while (position < text.size) { | |
| uint8_t byte = (uint8_t)text.data[position]; | |
| if (byte == 0x20 || byte == 0x09 || byte == 0x0A || byte == 0x0D) { | |
| // ASCII whitespace: space, tab, newline, carriage return. | |
| // 1 byte in token text = 1 byte in original input. | |
| ++count; | |
| ++position; | |
| } else if (position + 1 < text.size && byte == 0xC4) { | |
| // Check for ByteLevel whitespace sentinels (all start with 0xC4). | |
| uint8_t next_byte = (uint8_t)text.data[position + 1]; | |
| if (next_byte == 0xA0 || next_byte == 0x8A || next_byte == 0x8D) { | |
| // Ġ (0xA0) = space, Ċ (0x8A) = newline, ċ (0x8D) = carriage return. | |
| // 2 bytes in token text = 1 byte in original input. | |
| ++count; | |
| position += 2; | |
| } else { | |
| break; | |
| } | |
| } else { | |
| // Non-whitespace character. | |
| break; | |
| } | |
| } | |
| return count; | |
| } | |
| // Counts trailing whitespace in a token string, returning the number of bytes | |
| // in the original input that should be trimmed from offset.end. | |
| static iree_host_size_t iree_tokenizer_postprocessor_count_trailing_whitespace( | |
| iree_string_view_t text) { | |
| iree_host_size_t count = 0; | |
| iree_host_size_t position = text.size; | |
| while (position > 0) { | |
| uint8_t byte = (uint8_t)text.data[position - 1]; | |
| if (byte == 0x20 || byte == 0x09 || byte == 0x0A || byte == 0x0D) { | |
| // ASCII whitespace. | |
| ++count; | |
| --position; | |
| } else if (position >= 2 && (uint8_t)text.data[position - 2] == 0xC4) { | |
| // Check for ByteLevel whitespace sentinels (second byte of UTF-8). | |
| if (byte == 0xA0 || byte == 0x8A || byte == 0x8D) { | |
| // Ġ (0xA0) = space, Ċ (0x8A) = newline, ċ (0x8D) = carriage return. | |
| ++count; | |
| position -= 2; | |
| } else { | |
| break; | |
| } | |
| } else { | |
| break; | |
| } | |
| } | |
| return count; | |
| } | |
| void iree_tokenizer_postprocessor_trim_token_offsets( | |
| iree_tokenizer_postprocessor_encode_state_t* state, | |
| const iree_tokenizer_vocab_t* vocab, iree_tokenizer_token_output_t output, | |
| iree_host_size_t model_token_start, iree_host_size_t model_token_count) { | |
| // Early exit if trimming is disabled or no offsets to trim. | |
| if (!state || | |
| !iree_any_bit_set(state->flags, | |
| IREE_TOKENIZER_POSTPROCESSOR_FLAG_TRIM_OFFSETS)) { | |
| return; | |
| } | |
| if (!output.token_offsets || model_token_count == 0) return; | |
| if (!vocab) return; | |
| for (iree_host_size_t i = 0; i < model_token_count; ++i) { | |
| // Compute token_index with overflow protection. | |
| iree_host_size_t token_index = 0; | |
| if (!iree_host_size_checked_add(model_token_start, i, &token_index) || | |
| token_index >= output.capacity) { | |
| break; | |
| } | |
| iree_tokenizer_token_id_t token_id = output.token_ids[token_index]; | |
| iree_string_view_t token_text = | |
| iree_tokenizer_vocab_token_text(vocab, token_id); | |
| if (token_text.size == 0) continue; | |
| iree_host_size_t leading = | |
| iree_tokenizer_postprocessor_count_leading_whitespace(token_text); | |
| iree_host_size_t trailing = | |
| iree_tokenizer_postprocessor_count_trailing_whitespace(token_text); | |
| // Special case: if ADD_PREFIX_SPACE is set and this is the first model | |
| // token across the entire encode, preserve exactly 1 leading space (it was | |
| // artificially added and is part of the token's content). | |
| if (!state->first_model_token_trimmed && | |
| iree_any_bit_set(state->flags, | |
| IREE_TOKENIZER_POSTPROCESSOR_FLAG_ADD_PREFIX_SPACE) && | |
| leading > 0) { | |
| --leading; | |
| } | |
| state->first_model_token_trimmed = true; | |
| iree_tokenizer_offset_t* offset = &output.token_offsets[token_index]; | |
| // Trim leading: advance start but don't exceed end. Use checked arithmetic | |
| // to avoid overflow when offset->start + leading exceeds SIZE_MAX. | |
| iree_host_size_t new_start = 0; | |
| if (!iree_host_size_checked_add(offset->start, leading, &new_start) || | |
| new_start > offset->end) { | |
| new_start = offset->end; | |
| } | |
| // Trim trailing: move end back but don't go below new_start. | |
| // The span available for trimming is [new_start, offset->end]. | |
| iree_host_size_t new_end = offset->end; | |
| if (trailing > 0) { | |
| iree_host_size_t span = new_end - new_start; | |
| if (trailing >= span) { | |
| new_end = new_start; | |
| } else { | |
| new_end -= trailing; | |
| } | |
| } | |
| offset->start = new_start; | |
| offset->end = new_end; | |
| } | |
| } |