Instructions to use ZibinDong/ActionCodec2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ZibinDong/ActionCodec2 with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("ZibinDong/ActionCodec2", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download actioncodec2_kernels/bpe.cpp from ZibinDong/ActionCodec2: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/ZibinDong/ActionCodec2/resolve/main/actioncodec2_kernels/bpe.cpp
- Command line
-
hf download hf://ZibinDong/ActionCodec2/actioncodec2_kernels/bpe.cpp
-
curl -L -o bpe.cpp https://huggingface.co/ZibinDong/ActionCodec2/resolve/main/actioncodec2_kernels/bpe.cpp
16.7 kB
| // Native BPE trainer -- the FAST-style ablation baseline. | |
| // | |
| // Set-BPE treats one control step as an unordered set of primitive cells and | |
| // merges spatially before merging across time. Plain BPE instead serializes | |
| // the quantized ``[T, D]`` cell matrix row-major into one symbol sequence and | |
| // runs textbook byte-pair encoding on it, which is what FAST does after its | |
| // DCT stage. Both trainers consume the identical quantized corpus, | |
| // so a paired run isolates the tokenizer structure and nothing else. | |
| // | |
| // Correctness is identical to the Python reference trainer in | |
| // ``actioncodec2/bpe_training.py``, including the tie-break (largest count, | |
| // then lexicographically smallest ordered ``(left, right)`` pair) and the | |
| // left-to-right application order, so the two emit the same merge list. | |
| // | |
| // Where the time goes, and what is done about it: | |
| // * occurrences of every observed ordered pair are indexed once, so a merge | |
| // touches only the positions that actually carry it; | |
| // * the corpus is a doubly linked list over one flat symbol array, so | |
| // collapsing a pair is O(1) and never rewrites the tail; | |
| // * counters live in the same flat linear-probing table as the Set-BPE | |
| // kernels (common.hpp), and the heap is lazy with per-merge deduplication | |
| // of the keys a merge could have changed. | |
| namespace py = pybind11; | |
| namespace { | |
| // BPE pairs are ordered, so the key keeps ``left`` in the high word. The | |
| // heap tie-break in common.hpp prefers the smallest key, which is therefore the | |
| // lexicographically smallest ``(left, right)`` -- the Python reference rule. | |
| inline uint64_t ordered_key(int32_t left, int32_t right) { | |
| return (static_cast<uint64_t>(static_cast<uint32_t>(left)) << 32) | | |
| static_cast<uint64_t>(static_cast<uint32_t>(right)); | |
| } | |
| inline int32_t key_left(uint64_t key) { return static_cast<int32_t>(key >> 32); } | |
| inline int32_t key_right(uint64_t key) { return static_cast<int32_t>(key & 0xffffffffull); } | |
| // Per-merge set of pair keys whose counters moved. A merge can only touch | |
| // pairs incident to its two parents or its child, so the live population is | |
| // bounded by a small multiple of the vocabulary; clearing is a generation bump | |
| // rather than a table sweep. | |
| class StampSet { | |
| public: | |
| explicit StampSet(size_t capacity_hint) { | |
| size_t target = 1024; | |
| while (target < capacity_hint * 4) target <<= 1; | |
| keys_.assign(target, 0); | |
| stamps_.assign(target, 0); | |
| mask_ = target - 1; | |
| } | |
| void reset() { | |
| ++generation_; | |
| members_.clear(); | |
| if (generation_ != 0) return; | |
| std::fill(stamps_.begin(), stamps_.end(), 0); | |
| generation_ = 1; | |
| } | |
| void insert(uint64_t key) { | |
| size_t slot = mix(key) & mask_; | |
| while (stamps_[slot] == generation_) { | |
| if (keys_[slot] == key) return; | |
| slot = (slot + 1) & mask_; | |
| } | |
| if ((members_.size() + 1) * 4 > (mask_ + 1) * 3) { | |
| grow(); | |
| slot = mix(key) & mask_; | |
| while (stamps_[slot] == generation_) { | |
| if (keys_[slot] == key) return; | |
| slot = (slot + 1) & mask_; | |
| } | |
| } | |
| keys_[slot] = key; | |
| stamps_[slot] = generation_; | |
| members_.push_back(key); | |
| } | |
| const std::vector<uint64_t>& members() const { return members_; } | |
| private: | |
| static uint64_t mix(uint64_t value) { | |
| value ^= value >> 30; | |
| value *= 0xbf58476d1ce4e5b9ull; | |
| value ^= value >> 27; | |
| value *= 0x94d049bb133111ebull; | |
| return value ^ (value >> 31); | |
| } | |
| void grow() { | |
| const size_t buckets = (mask_ + 1) << 1; | |
| keys_.assign(buckets, 0); | |
| stamps_.assign(buckets, 0); | |
| mask_ = buckets - 1; | |
| generation_ = 1; | |
| for (const uint64_t key : members_) { | |
| size_t slot = mix(key) & mask_; | |
| while (stamps_[slot] == generation_) slot = (slot + 1) & mask_; | |
| keys_[slot] = key; | |
| stamps_[slot] = generation_; | |
| } | |
| } | |
| std::vector<uint64_t> keys_; | |
| std::vector<uint32_t> stamps_; | |
| std::vector<uint64_t> members_; | |
| size_t mask_ = 0; | |
| uint32_t generation_ = 1; | |
| }; | |
| // Occurrence index. ``slots`` maps a pair key to its 1-based row in | |
| // ``lists``; a value of zero means "no row yet", which is exactly the absent | |
| // value CounterMap already reports, so no separate presence bit is needed. | |
| struct OccurrenceIndex { | |
| ac2::CounterMap slots{1 << 16}; | |
| std::vector<std::vector<int32_t>> lists; | |
| std::vector<uint64_t> keys; | |
| int64_t stored = 0; | |
| int64_t row_for(uint64_t key) { | |
| const int64_t existing = slots.get(key); | |
| if (existing > 0) return existing - 1; | |
| const int64_t row = static_cast<int64_t>(lists.size()); | |
| lists.emplace_back(); | |
| keys.push_back(key); | |
| slots.add(key, row + 1); | |
| return row; | |
| } | |
| void append(uint64_t key, int32_t position) { | |
| lists[static_cast<size_t>(row_for(key))].push_back(position); | |
| ++stored; | |
| } | |
| }; | |
| struct Corpus { | |
| std::vector<int32_t> symbol; // live token id per slot, -1 once absorbed | |
| std::vector<int32_t> prev; | |
| std::vector<int32_t> next; | |
| int64_t actions = 0; | |
| int64_t episodes = 0; | |
| }; | |
| void ingest_chunk(Corpus& corpus, const int32_t* cells, int64_t rows, int32_t dimension, | |
| const int32_t* offsets, const int32_t* bins, const int64_t* episode_offsets, | |
| int64_t episode_count) { | |
| for (int64_t episode = 0; episode < episode_count; ++episode) { | |
| const int64_t start = episode_offsets[episode]; | |
| const int64_t stop = episode_offsets[episode + 1]; | |
| if (stop <= start || start < 0 || stop > rows) { | |
| throw std::invalid_argument("BPE episode offsets must be increasing and in range"); | |
| } | |
| const size_t first = corpus.symbol.size(); | |
| const int32_t* initial = cells + start * dimension; | |
| for (int64_t row = start; row < stop; ++row) { | |
| const int32_t* values = cells + row * dimension; | |
| for (int32_t dim = 0; dim < dimension; ++dim) { | |
| const int32_t value = values[dim]; | |
| if ((value >= 0) != (initial[dim] >= 0)) { | |
| throw std::invalid_argument("active dimensions must remain constant within an episode"); | |
| } | |
| if (value < 0) { | |
| if (value != -1) throw std::invalid_argument("cells must contain -1 or a valid index"); | |
| continue; | |
| } | |
| if (value >= bins[dim]) throw std::invalid_argument("cell index exceeds its bin count"); | |
| corpus.symbol.push_back(offsets[dim] + value); | |
| } | |
| } | |
| const size_t last = corpus.symbol.size(); | |
| if (last == first) throw std::invalid_argument("every control step needs an active coordinate"); | |
| if (last > static_cast<size_t>(INT32_MAX)) { | |
| throw std::invalid_argument( | |
| "BPE corpus exceeds 2^31 primitive cells; lower the fitting coverage"); | |
| } | |
| corpus.prev.resize(last); | |
| corpus.next.resize(last); | |
| for (size_t index = first; index < last; ++index) { | |
| corpus.prev[index] = index == first ? -1 : static_cast<int32_t>(index - 1); | |
| corpus.next[index] = index + 1 == last ? -1 : static_cast<int32_t>(index + 1); | |
| } | |
| corpus.actions += stop - start; | |
| ++corpus.episodes; | |
| } | |
| } | |
| } // namespace | |
| py::tuple train_bpe_native(py::iterable cell_chunks, const std::vector<int32_t>& bins, | |
| int64_t vocab_size, int64_t max_token_cells, int threads) { | |
| const int32_t dimension = static_cast<int32_t>(bins.size()); | |
| if (dimension < 1) throw std::invalid_argument("bins must not be empty"); | |
| std::vector<int32_t> offsets(dimension + 1, 0); | |
| for (int32_t index = 0; index < dimension; ++index) { | |
| if (bins[index] < 1) throw std::invalid_argument("every dimension needs at least one bin"); | |
| offsets[index + 1] = offsets[index] + bins[index]; | |
| } | |
| const int32_t atom_count = offsets[dimension]; | |
| if (vocab_size < atom_count) { | |
| throw std::invalid_argument("vocab_size is below the number of primitive atoms"); | |
| } | |
| if (max_token_cells < 1) throw std::invalid_argument("max_token_cells must be positive"); | |
| // Greedy merge updates are serial. Preserve the requested option for the | |
| // shared trainer API without changing the calling thread's OpenMP settings. | |
| const int thread_count = 1; | |
| Corpus corpus; | |
| for (py::handle raw_chunk : cell_chunks) { | |
| py::tuple chunk = py::cast<py::tuple>(raw_chunk); | |
| if (chunk.size() != 2) throw std::invalid_argument("each chunk must be (cells, offsets)"); | |
| auto cells = py::cast<py::array_t<int32_t, py::array::c_style | py::array::forcecast>>(chunk[0]); | |
| auto bounds = | |
| py::cast<py::array_t<int64_t, py::array::c_style | py::array::forcecast>>(chunk[1]); | |
| auto cells_view = cells.unchecked<2>(); | |
| auto bounds_view = bounds.unchecked<1>(); | |
| if (cells_view.shape(1) != dimension) { | |
| throw std::invalid_argument("cells must have one column per bin count"); | |
| } | |
| if (bounds_view.shape(0) < 2) throw std::invalid_argument("offsets must bound one episode"); | |
| { | |
| py::gil_scoped_release release; | |
| ingest_chunk(corpus, cells_view.data(0, 0), cells_view.shape(0), dimension, offsets.data(), | |
| bins.data(), bounds_view.data(0), bounds_view.shape(0) - 1); | |
| } | |
| } | |
| if (corpus.symbol.empty()) throw std::invalid_argument("BPE training needs one episode"); | |
| const int64_t initial_length = static_cast<int64_t>(corpus.symbol.size()); | |
| int64_t length = initial_length; | |
| std::vector<int32_t>& symbol = corpus.symbol; | |
| std::vector<int32_t>& prev = corpus.prev; | |
| std::vector<int32_t>& next = corpus.next; | |
| ac2::CounterMap counts(1 << 16); | |
| OccurrenceIndex index; | |
| std::vector<std::pair<int32_t, int32_t>> merges; | |
| std::vector<int32_t> token_cells(static_cast<size_t>(atom_count), 1); | |
| int64_t initial_pair_types = 0; | |
| int64_t heap_pops = 0; | |
| int64_t counter_updates = 0; | |
| int64_t compactions = 0; | |
| int64_t peak_stored = 0; | |
| int64_t oversized_pairs = 0; | |
| int32_t observed_cells = 1; | |
| { | |
| py::gil_scoped_release release; | |
| // One pass builds ascending occurrence lists; the counter is their length. | |
| for (int64_t position = 0; position < initial_length; ++position) { | |
| const int32_t follower = next[position]; | |
| if (follower < 0) continue; | |
| index.append(ordered_key(symbol[position], symbol[follower]), | |
| static_cast<int32_t>(position)); | |
| } | |
| for (size_t row = 0; row < index.lists.size(); ++row) { | |
| counts.add(index.keys[row], static_cast<int64_t>(index.lists[row].size())); | |
| } | |
| initial_pair_types = static_cast<int64_t>(index.lists.size()); | |
| peak_stored = index.stored; | |
| std::vector<ac2::HeapEntry> heap; | |
| heap.reserve(index.lists.size()); | |
| counts.for_each([&](uint64_t key, int64_t value) { heap.push_back({value, key, 0}); }); | |
| std::make_heap(heap.begin(), heap.end(), ac2::HeapOrder{}); | |
| token_cells.reserve(static_cast<size_t>(vocab_size)); | |
| merges.reserve(static_cast<size_t>(vocab_size - atom_count)); | |
| ac2::CounterMap oversized(1 << 12); | |
| StampSet touched(static_cast<size_t>(vocab_size)); | |
| std::vector<int32_t> occurrences; | |
| while (static_cast<int64_t>(atom_count + merges.size()) < vocab_size && !heap.empty()) { | |
| const ac2::HeapEntry top = ac2::heap_pop(heap); | |
| ++heap_pops; | |
| const uint64_t key = top.key; | |
| const int64_t frequency = counts.get(key); | |
| if (frequency <= 0 || frequency != top.count) continue; | |
| if (oversized.get(key) != 0) continue; | |
| const int32_t left = key_left(key); | |
| const int32_t right = key_right(key); | |
| const int64_t span = static_cast<int64_t>(token_cells[left]) + token_cells[right]; | |
| if (span > max_token_cells) { | |
| oversized.add(key, 1); | |
| continue; | |
| } | |
| const int32_t child = atom_count + static_cast<int32_t>(merges.size()); | |
| token_cells.push_back(static_cast<int32_t>(span)); | |
| observed_cells = std::max(observed_cells, static_cast<int32_t>(span)); | |
| occurrences.clear(); | |
| occurrences.swap(index.lists[static_cast<size_t>(index.slots.get(key) - 1)]); | |
| index.stored -= static_cast<int64_t>(occurrences.size()); | |
| // Left-to-right application is what makes overlapping runs deterministic; | |
| // later appends leave the list unsorted, so restore the order first. | |
| std::sort(occurrences.begin(), occurrences.end()); | |
| touched.reset(); | |
| touched.insert(key); | |
| int64_t applied = 0; | |
| for (const int32_t position : occurrences) { | |
| if (symbol[position] != left) continue; | |
| const int32_t follower = next[position]; | |
| if (follower < 0 || symbol[follower] != right) continue; | |
| const int32_t before = prev[position]; | |
| const int32_t after = next[follower]; | |
| if (before >= 0) { | |
| const uint64_t stale = ordered_key(symbol[before], left); | |
| counts.add(stale, -1); | |
| touched.insert(stale); | |
| } | |
| if (after >= 0) { | |
| const uint64_t stale = ordered_key(right, symbol[after]); | |
| counts.add(stale, -1); | |
| touched.insert(stale); | |
| } | |
| symbol[position] = child; | |
| symbol[follower] = -1; | |
| next[position] = after; | |
| if (after >= 0) prev[after] = position; | |
| prev[follower] = -1; | |
| next[follower] = -1; | |
| if (before >= 0) { | |
| const uint64_t fresh = ordered_key(symbol[before], child); | |
| counts.add(fresh, 1); | |
| index.append(fresh, before); | |
| touched.insert(fresh); | |
| } | |
| if (after >= 0) { | |
| const uint64_t fresh = ordered_key(child, symbol[after]); | |
| counts.add(fresh, 1); | |
| index.append(fresh, position); | |
| touched.insert(fresh); | |
| } | |
| ++applied; | |
| } | |
| counts.add(key, -applied); | |
| counter_updates += 4 * applied; | |
| length -= applied; | |
| merges.emplace_back(left, right); | |
| for (const uint64_t member : touched.members()) { | |
| const int64_t value = counts.get(member); | |
| if (value > 0) ac2::heap_push(heap, {value, member, 0}); | |
| } | |
| peak_stored = std::max(peak_stored, index.stored); | |
| // Superseded positions stay in the lists of pairs that never won. Sweep | |
| // them out once they outnumber the live corpus, so memory tracks the | |
| // corpus rather than the merge history. | |
| if (index.stored > 3 * length) { | |
| int64_t retained = 0; | |
| for (size_t row = 0; row < index.lists.size(); ++row) { | |
| auto& list = index.lists[row]; | |
| if (list.empty()) continue; | |
| const int32_t expected_left = key_left(index.keys[row]); | |
| const int32_t expected_right = key_right(index.keys[row]); | |
| size_t write = 0; | |
| for (const int32_t position : list) { | |
| const int32_t follower = next[position]; | |
| if (symbol[position] != expected_left) continue; | |
| if (follower < 0 || symbol[follower] != expected_right) continue; | |
| list[write++] = position; | |
| } | |
| list.resize(write); | |
| if (write * 2 < list.capacity()) list.shrink_to_fit(); | |
| retained += static_cast<int64_t>(write); | |
| } | |
| index.stored = retained; | |
| ++compactions; | |
| } | |
| if (heap.size() > 4 * counts.size() + 65536) { | |
| heap.clear(); | |
| counts.for_each([&](uint64_t member, int64_t value) { heap.push_back({value, member, 0}); }); | |
| std::make_heap(heap.begin(), heap.end(), ac2::HeapOrder{}); | |
| } | |
| } | |
| oversized_pairs = static_cast<int64_t>(oversized.live_entries()); | |
| } | |
| py::list merge_list; | |
| for (const auto& merge : merges) { | |
| merge_list.append(py::make_tuple(merge.first, merge.second)); | |
| } | |
| py::dict report; | |
| report["episodes"] = corpus.episodes; | |
| report["actions"] = corpus.actions; | |
| report["atoms"] = static_cast<int64_t>(atom_count); | |
| report["merges"] = static_cast<int64_t>(merges.size()); | |
| report["vocab_size"] = static_cast<int64_t>(atom_count + merges.size()); | |
| report["cells"] = initial_length; | |
| report["tokens_per_action_initial"] = | |
| static_cast<double>(initial_length) / static_cast<double>(corpus.actions); | |
| report["tokens_per_action"] = | |
| static_cast<double>(length) / static_cast<double>(corpus.actions); | |
| report["compression"] = static_cast<double>(initial_length) / static_cast<double>(length); | |
| report["max_token_cells"] = max_token_cells; | |
| report["max_token_cells_observed"] = static_cast<int64_t>(observed_cells); | |
| report["oversized_pairs_skipped"] = oversized_pairs; | |
| report["initial_pair_types"] = initial_pair_types; | |
| report["pair_types_peak"] = static_cast<int64_t>(index.lists.size()); | |
| report["occurrence_slots_peak"] = peak_stored; | |
| report["occurrence_compactions"] = compactions; | |
| report["heap_pops"] = heap_pops; | |
| report["counter_updates"] = counter_updates; | |
| report["threads"] = thread_count; | |
| report["requested_threads"] = threads; | |
| report["trainer"] = "native_sparse_exact"; | |
| return py::make_tuple(merge_list, report); | |
| } | |