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
File size: 16,653 Bytes
1bfc3f0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 | // 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.
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <pybind11/stl.h>
#include <algorithm>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <vector>
#include "common.hpp"
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);
}
|