Livt packages

Eccelerators.Tokenization

Version 0.2.0

Bounded BPE and Unigram tokenization, text normalization, and decoding components for Livt hardware.

Published files (27)

src/Gpt2/Gpt2BpeTokenizer64.lvt

namespace Eccelerators.Tokenization.Gpt2

using Eccelerators.Tokenization.Bpe

/**
 * Tokenizes a bounded ASCII input with GPT-2 byte-level BPE semantics.
 *
 * The caller supplies vocabulary and merge storage. Input is accumulated with
 * AcceptAscii, then Tokenize produces at most 64 token IDs. Carriage return and
 * line feed are ordinary input bytes; callers should consume transport or
 * framing delimiters unless they belong to the input text.
 */
component Gpt2BpeTokenizer64
{
	// Maximum accepted ASCII bytes and produced token IDs.
	public const MAX_INPUT_BYTES: int = 64
	public const MAX_OUTPUT_TOKENS: int = 64

	// ASCII bytes used by the GPT-2 pre-tokenization expression.
	const ASCII_TAB: byte = 0x09
	const ASCII_LINE_FEED: byte = 0x0a
	const ASCII_VERTICAL_TAB: byte = 0x0b
	const ASCII_FORM_FEED: byte = 0x0c
	const ASCII_CARRIAGE_RETURN: byte = 0x0d
	const ASCII_SPACE: byte = 0x20
	const ASCII_APOSTROPHE: byte = 0x27
	const ASCII_DIGIT_FIRST: byte = 0x30
	const ASCII_DIGIT_LAST: byte = 0x39
	const ASCII_UPPERCASE_FIRST: byte = 0x41
	const ASCII_UPPERCASE_LAST: byte = 0x5a
	const ASCII_LOWERCASE_FIRST: byte = 0x61
	const ASCII_LOWERCASE_LAST: byte = 0x7a
	const ASCII_MAXIMUM: int = 127
	const ASCII_CASE_OFFSET: int = 32

	// Lowercase suffix letters recognized by the GPT-2 contraction alternatives.
	const CONTRACTION_D: byte = 0x64
	const CONTRACTION_E: byte = 0x65
	const CONTRACTION_L: byte = 0x6c
	const CONTRACTION_M: byte = 0x6d
	const CONTRACTION_R: byte = 0x72
	const CONTRACTION_S: byte = 0x73
	const CONTRACTION_T: byte = 0x74
	const CONTRACTION_V: byte = 0x76

	vocabulary: IGpt2Vocabulary
	mergeEngine: BpeMergeEngine64
	inputBytes: byte[64]
	outputTokens: int[64]
	inputLength: int
	outputCount: int
	error: bool

	/**
	 * Connects the tokenizer to model-specific vocabulary and merge storage.
	 */
	new(vocabulary: IGpt2Vocabulary, mergeTable: IBpeMergeTable)
	{
		this.vocabulary = vocabulary
		this.mergeEngine = new BpeMergeEngine64(mergeTable)
	}

	/**
	 * Clears accumulated input, token output, and error state.
	 */
	public fn Reset()
	{
		inputLength = 0
		outputCount = 0
		error = false
		this.mergeEngine.Reset()
	}

	/**
	 * Appends one seven-bit ASCII byte and returns whether it was accepted.
	 */
	public fn AcceptAscii(value: byte) bool
	{
		if ((value as int) > ASCII_MAXIMUM || inputLength >= MAX_INPUT_BYTES) {
			error = true
			return false
		}

		inputBytes[inputLength] = value
		inputLength++
		return true
	}

	/**
	 * Tokenizes all accepted bytes and replaces the previous output sequence.
	 */
	public fn Tokenize()
	{
		outputCount = 0
		if (error) { return }

		var index = 0
		while (index < inputLength) {
			var value = inputBytes[index]
			if (IsWhitespace(value)) {
				var whitespaceEnd = index + 1
				var scanningWhitespace = true
				while (whitespaceEnd < inputLength && scanningWhitespace) {
					if (IsWhitespace(inputBytes[whitespaceEnd])) {
						whitespaceEnd++
					} else {
						scanningWhitespace = false
					}
				}

				var finalWhitespace = inputBytes[whitespaceEnd - 1]
				if (whitespaceEnd >= inputLength || finalWhitespace != ASCII_SPACE) {
					TokenizePiece(index, whitespaceEnd - index)
					index = whitespaceEnd
				} else {
					var whitespaceLength = whitespaceEnd - index
					if (whitespaceLength > 1) {
						TokenizePiece(index, whitespaceLength - 1)
					}

					var pieceStart = whitespaceEnd - 1
					var contentStart = whitespaceEnd
					var contentEnd = FindClassEnd(contentStart)
					TokenizePiece(pieceStart, contentEnd - pieceStart)
					index = contentEnd
				}
			} else {
				var contractionLength = FindContractionLength(index)
				if (contractionLength > 0) {
					TokenizePiece(index, contractionLength)
					index = index + contractionLength
				} else {
					var pieceEnd = FindClassEnd(index)
					TokenizePiece(index, pieceEnd - index)
					index = pieceEnd
				}
			}
		}
	}

	/**
	 * Returns the number of ASCII bytes accumulated for the next tokenization.
	 */
	public fn GetInputLength() int { return inputLength }

	/**
	 * Returns the number of produced token IDs.
	 */
	public fn GetTokenCount() int { return outputCount }

	/**
	 * Returns one output token ID, or zero outside the output sequence.
	 */
	public fn GetToken(index: int) int
	{
		if (index < 0 || index >= outputCount) { return 0 }
		return outputTokens[index]
	}

	/**
	 * Returns whether input, lookup, piece, or output capacity was invalid.
	 */
	public fn HasError() bool { return error }

	/**
	 * Returns whether the byte belongs to an ASCII letter run.
	 */
	inline fn IsLetter(value: byte) bool
	{
		return (value >= ASCII_UPPERCASE_FIRST && value <= ASCII_UPPERCASE_LAST) ||
			(value >= ASCII_LOWERCASE_FIRST && value <= ASCII_LOWERCASE_LAST)
	}

	/**
	 * Returns whether the byte belongs to an ASCII digit run.
	 */
	inline fn IsDigit(value: byte) bool
	{
		return value >= ASCII_DIGIT_FIRST && value <= ASCII_DIGIT_LAST
	}

	/**
	 * Returns whether the byte is ASCII whitespace.
	 */
	inline fn IsWhitespace(value: byte) bool
	{
		return value == ASCII_TAB || value == ASCII_LINE_FEED ||
			value == ASCII_VERTICAL_TAB || value == ASCII_FORM_FEED ||
			value == ASCII_CARRIAGE_RETURN || value == ASCII_SPACE
	}

	/**
	 * Converts one ASCII letter to lowercase for contraction recognition.
	 */
	inline fn Lower(value: byte) byte
	{
		if (value >= ASCII_UPPERCASE_FIRST && value <= ASCII_UPPERCASE_LAST) {
			return ((value as int) + ASCII_CASE_OFFSET) as byte
		}
		return value
	}

	/**
	 * Finds the end of the letter, digit, or punctuation run at start.
	 */
	fn FindClassEnd(start: int) int
	{
		if (start >= inputLength) { return inputLength }

		var first = inputBytes[start]
		var end = start + 1
		if (IsLetter(first)) {
			while (end < inputLength) {
				if (!IsLetter(inputBytes[end])) { return end }
				end++
			}
			return end
		}

		if (IsDigit(first)) {
			while (end < inputLength) {
				if (!IsDigit(inputBytes[end])) { return end }
				end++
			}
			return end
		}

		while (end < inputLength) {
			var value = inputBytes[end]
			if (IsWhitespace(value) || IsLetter(value) || IsDigit(value)) { return end }
			end++
		}
		return end
	}

	/**
	 * Returns a GPT-2 contraction piece length beginning at start, or zero.
	 */
	fn FindContractionLength(start: int) int
	{
		if (inputBytes[start] != ASCII_APOSTROPHE || start + 1 >= inputLength) { return 0 }

		var first = Lower(inputBytes[start + 1])
		if (first == CONTRACTION_S || first == CONTRACTION_T ||
			first == CONTRACTION_M || first == CONTRACTION_D) {
			return 2
		}

		if (start + 2 >= inputLength) { return 0 }
		var second = Lower(inputBytes[start + 2])
		if ((first == CONTRACTION_R && second == CONTRACTION_E) ||
			(first == CONTRACTION_V && second == CONTRACTION_E) ||
			(first == CONTRACTION_L && second == CONTRACTION_L)) { return 3 }
		return 0
	}

	/**
	 * Converts and merges one pre-tokenized byte piece into the output stream.
	 */
	fn TokenizePiece(start: int, length: int)
	{
		this.mergeEngine.Reset()
		for (var offset = 0; offset < length; offset++) {
			this.vocabulary.LookupByte(inputBytes[start + offset])
			if (!this.vocabulary.HasByteToken()) {
				error = true
				return
			}

			var byteToken = this.vocabulary.GetByteToken()
			if (!this.mergeEngine.AppendSymbol(byteToken)) {
				error = true
				return
			}
		}

		this.mergeEngine.Compute()
		if (this.mergeEngine.HasError()) {
			error = true
			return
		}

		for (var index = 0; index < this.mergeEngine.GetCount(); index++) {
			if (outputCount >= MAX_OUTPUT_TOKENS) {
				error = true
				return
			}

			var outputToken = this.mergeEngine.GetToken(index)
			outputTokens[outputCount] = outputToken
			outputCount++
		}
	}
}