diff --git a/.cargo/config.toml b/.cargo/config.toml index 6330d419..1ecf5a7b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,5 @@ # Needed for WASM unstable features [build] -rustflags = [ "--cfg=web_sys_unstable_apis" ] -rustdocflags = [ "--cfg=web_sys_unstable_apis" ] -#target = "wasm32-unknown-unknown" +rustflags = ["--cfg=web_sys_unstable_apis"] +rustdocflags = ["--cfg=web_sys_unstable_apis"] +target = "wasm32-unknown-unknown" diff --git a/.gitignore b/.gitignore index 3ffe4ce7..647364f7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,6 @@ Cargo.lock *.pdb .python-version -crates/ratchet-core/kernel-generated/** -**/*.svg fixtures/ **/.DS_Store @@ -30,3 +28,9 @@ venv/ # proptest regression tests proptest-regressions/ + +# samply profile +profile.json + +# webdriver configs +webdriver.json diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 3b6fbc7b..00000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,85 +0,0 @@ -# Ratchet - -Ratchet is a web-first ML framework, designed to run cross-platform & in the browser. - -## Design Decisions - -> Through specialization, comes efficiency. - -Ratchet is designed for 1 thing only: **Inference on WebGPU**. - -This leads us to a few design decisions: -1. Ratchet is **lazy**, no computation is done until the entire computation graph is built and executed. This aligns closely with CUDAGraphs & Command buffers. -2. Ratchet supports **BOTH** static & dynamic graphs, see [Unified Graph Execution by Jittor](http://scis.scichina.com/en/2020/222103.pdf) for more details. -3. Memory planning is crucial. Creation and first bind of a buffer is *expensive* in WebGPU. Therefore, Ratchet uses a greedy algorithm to pool buffers for intermediate results of the CFG. - -Take for example Whisper from OpenAI. This is an encoder-decoder model, where the encoder is completely static (i.e everything is known at compile time), and the decoder is very dynamic (KV caching, seq_len increments every step). By allowing both paradigms, we can maximise performance. - -## Memory Management - -Ratchets top level `Tensor` is just an `Arc` around the `Inner`. Tensors should be cheaply cloneable. -`Inner` contains a struct `Storage`, this is an enum around our 2 managed structures for CPU & GPU: `CpuStorage` & `GpuStorage`. -`CpuStorage` is an `Arc>`, and `GpuStorage` is an `Arc>`. - - -## Quantization - -Due to the buffer binding model of WebGPU, quantisation requires some careful thought in WebGPU. -First let's understand what's required when quantizing / dequantzing. - -[Quantization - Neural Network Distiller](https://intellabs.github.io/distiller/algo_quantization.html) - -To be brief, values are grouped into blocks (let's say 16 values). This block of values has 1 or more associated, half or full precision values. These values are used to scale the block of values. The question is, how do you manage this in memory? - -### Approach 1: Separate tensors -With your own quant scheme, you could have 2(3) separate tensors, one for weights and one for scales. -This is pretty ideal, because then in the shader you can do the buffer binding like below: - -```wgsl -@group(0) @binding(0) -var A: array>; - -@group(0) @binding(1) -var B: array; //this is the quantized weights, wgpu only supports 32 bit values for now - -@group(0) @binding(2) -var absmax: array; - -@group(1) @binding(0) -var C: array>; -``` -The above bindings are optimal for performance, and that's what we are optimizing for the most. - -But if you have 2 separate tensors, what does your model loading code look like? What does your matmul API look like? - -ONNX and others have a different operation altogether `QMatmul`. You'll also require 2 entirely different model implementations like so: -`https://github.com/huggingface/candle/blob/main/candle-transformers/src/models/whisper/quantized_model.rs` -`https://github.com/huggingface/candle/blob/main/candle-transformers/src/models/whisper/model.rs` - -This to me seems quite annoying. Is there any way we can avoid it? - -I think to summarize the hard requirements of this: -1. Maximal performance is the #1 priority, everything else is secondary. -2. 1 model implementation is very very desirable. -3. The API should be invisible, the user should just call `.matmul()` with Tensor B of datatype Q4_XYZ, and it should just work. - -I think the fastest way to achieve that is to use a custom quantization scheme for now. We can revisit this. - -## Operations - -1. Matmul - family of operations for matrix multiplication, e.g `SGEMM`, `HGEMM`, `QGEMM`, `QGEMV` etc. -2. Reindex - family of operations that can be distilled to reindex(I, SO, f) → O, where I is the input, SO is the shape of the output, and f is the function that maps the output index to the input index. This is a very powerful operation, and can be used to implement many other operations. -3. Reduce - family of operations that can be distilled down to a reduction over a dimension, e.g `sum` or `mean`. -4. Unary - family of operations that applies a function to each element of the input, peformed in place by default (unless not possible), e.g `relu` or `sigmoid`. -5. Binary - family of operations that performs an elementwise operation between 2 tensors, e.g `add` or `sub`. -6. Custom - user provided custom operation. - -Whats the minimal set of operations required to express all DL models? Unsure but this is a decent start. - -#### Reindex -Reindex is a family of operations that can all be modelled as `reindex(I, SO, f) → O`, where I is the input, SO is the shape of the output, and f is the function that maps the output index to the input index. We dispatch |So| threads. - -Inside the Reindex family you have: -1. Permute: rearranges elements, 1:1 mapping between input & output indices. -2. Slice: slices a tensor, 1:<=1 mapping between input & output indices. -3. Broadcast: broadcasts a tensor, 1:>=1 mapping between input & output indices. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index ccc6507f..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,167 +0,0 @@ -# Running Tests for the Ratchet Rust Package - -This guide outlines the steps necessary to set up and run tests for the Ratchet Rust package. Please follow these steps carefully to ensure a smooth testing process. - -## Setup Instructions - -### Clone the Repository - -First, ensure you have Git installed. Clone of the Ratchet repository from GitHub and navigate into the project directory: - -```sh -git clone https://github.com/FL33TW00D/ratchet.git -cd ratchet/ -``` - -### Setup Rust and Cargo - -Ensure you have Rust and Cargo installed. If not, please refer to the Rust installation guide to set up Rust and Cargo. - -### Setup `just` - -Ensure you have `just`, a command runner that simplifies running project-specific commands, installed. If `just` is not already installed on your system, you can install it using Cargo, Rust's package manager: - -```sh -cargo install just -``` - -### Setup Python - -There are two ways to setup Python for the project: using `pyenv` or using `conda`. - -#### Option 1: Using pyenv - -##### Step 1: Install `pyenv` - -First, make sure to install [pyenv](https://github.com/pyenv/pyenv#getting-pyenv). `pyenv` lets you manage multiple versions of Python. Please make sure you follow the install guide and source the correct environment variables. - -##### Step 2: Install python 3.10.6 - -Use `just` to install `python3.10.6` and enable it as the local python version for the project. - -> **NOTE** : `PyO3`\*\* needs Python to be built with `enable-shared` flag. - -```sh -just install-pyo3 -``` - -##### Step 3: Create virtual environment (Optional) - -This step is optional but _highly_ recommended. You should create and source a virtual environment using your favorite tool (`uv`, `venv`, `virtualenv`...). We'll use the built-in `venv` module: - -```sh -python -m venv venv -source venv/bin/activate -``` - -##### Step 4: Install python dependencies - -Install the Python dependencies recursively: - -```sh -python -m pip install -r requirements.txt -``` - -##### Step 5: Configure Python Environment for PyO3 - -PyO3 uses a build script to determine the Python version and set the correct linker arguments. To override the Python interpreter to the virtual environment, run the following: - -```sh -export PYO3_PYTHON=$(which python) -echo $PYO3_PYTHON -``` - -#### Option 2: Using conda - -##### Step 1: Create a new conda environment - -``` -conda create -n ratchet python=3.10 -``` - -##### Step 2: Install dependencies - -``` -pip install -r requirements.txt -``` - -##### Step 3: Configure Cargo - -Edit `/.cargo/config.toml` to add the linker config: - -``` -# .cargo/config.toml -[build] -rustflags = [ - "--cfg=web_sys_unstable_apis", - # Add these two lines and replace PATH_TO_CONDA with your conda directory: - "-C", - "link-args=-Wl,-rpath,/envs/ratchet/lib/", -] -``` - -### Setup Node.js - -Ensure you have Node.js v18 or later installed. If not, please refer to the Node.js installation guide to set up Node.js. - -After installing Node.js, run `corepack enable` to enable the Node.js [corepack](https://github.com/nodejs/corepack) feature. - -Then run `pnpm install` to install the Node.js dependencies. - -## Test config - -We'll first verify that your pyo3 config is correctly setup: - -``` -PYO3_PRINT_CONFIG=1 cargo build -``` - -Building the project will throw an error(!) and print the config: - -``` -(exit status: 101) - --- stdout - cargo:rerun-if-env-changed=PYO3_PRINT_CONFIG - - -- PYO3_PRINT_CONFIG=1 is set, printing configuration and halting compile -- - implementation=CPython - version=3.10 - shared=true - abi3=false - lib_name=python3.10 - lib_dir= - executable= - pointer_width=64 - build_flags= - suppress_build_script_link_lines=false -``` - -If that looks like this, you are good to go 🎉 - -## Run Tests - -Finally, run the tests for the package using Cargo: - -```sh -cargo test -``` - -To run the `PyO3` tests, add the `pyo3` flag: - -```sh -cargo test --features pyo3 -``` - -## Run WASM Tests - -To run WASM tests (e.g., the whisper test) run: - -```sh -just wasm-test ratchet-models chrome -``` - -And check the result in: - -``` -http://localhost:8000 -``` diff --git a/Cargo.toml b/Cargo.toml index eae21a07..7e992470 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,14 @@ [workspace] members = [ - "crates/ratchet-hub", - "crates/ratchet-core", - "crates/ratchet-web", - "crates/ratchet-loader", - "crates/ratchet-models", - "crates/ratchet-nn", - "crates/ratchet-hub", - "crates/ratchet-cli", - "crates/ratchet-macros", - "crates/ratchet-datasets", + "crates/piston-core", + "crates/piston-web", + "crates/piston-models", + "crates/piston-nn", + "crates/piston-macros", + "crates/piston-datasets", ] resolver = "2" -edition = "2021" +edition = "2024" [profile.test] debug = 2 @@ -21,14 +17,14 @@ debug-assertions = true [profile.release] panic = 'abort' lto = "fat" -codegen-units = 1 +opt-level = 3 [profile.profiling] inherits = "release" debug = 2 [workspace.dependencies] -wgpu = { git = "https://github.com/vinhowe/wgpu", branch = "feature/multi-dim-compute-subgroups", features = [ +wgpu = { git = "https://github.com/vinhowe/wgpu", rev = "8aa00b0ef4b2903b542741213f3ad186c4c32186", features = [ "fragile-send-sync-non-atomic-wasm", ] } bytemuck = { version = "1.14.0", features = [ @@ -49,7 +45,7 @@ anyhow = "1.0.79" tokenizers = "0.19.1" js-sys = "0.3.64" -wasm-bindgen = "0.2.91" +wasm-bindgen = "0.2.104" wasm-bindgen-test = "0.3.34" cfg-if = "1.0.0" chrono = "0.4.35" @@ -57,11 +53,11 @@ clap = "4.5.3" console_error_panic_hook = "0.1.7" console_log = "1.0.0" dot3 = "0.1.0" -encase = { git = "https://github.com/cwfitzgerald/encase", branch = "add-member" } +encase = { git = "https://github.com/vinhowe/encase", branch = "add-member" } env_logger = "0.11.3" fern = "0.6.2" getrandom = "0.2" -glam = "0.28.0" +glam = "0.30.4" globwalk = "0.8.1" gloo-net = { version = "0.5.0", default-features = false } hound = "3.5.1" @@ -108,3 +104,8 @@ memmap2 = "0.9.5" maybe-async = "0.2" async-trait = "0.1.77" bitvec = { version = "1", default-features = false, features = ["alloc"] } +paste = { version = "1.0.15" } +tsify = { version = "0.5.5", features = ["js"] } + +[patch.crates-io] +wasm-bindgen = { git = "https://github.com/vinhowe/wasm-bindgen", rev = "4b4f9cd9731cf35725727bcac92940d51a559a50" } diff --git a/LICENSE b/LICENSE index 99969770..70ff3f1c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,6 @@ MIT License +Copyright (c) 2025 Thomas Vincent Howe Copyright (c) 2024 Christopher Fleetwood Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index 11980782..789be9e1 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,8 @@ -

- For the time being, I don't have instructions on how to run this. You're on your own for a tiny bit :) -

+# sequence toy + Piston library -
- -

-(backward) -

-
-
+Train small sequence models in your browser with WebGPU. + +## Attribution This is a fork of [Ratchet](https://github.com/huggingface/ratchet), hacked and butchered to add backpropogation, to show that it is technically possible to train language models in a (WebGPU-enabled) browser. diff --git a/crates/ratchet-core/Cargo.toml b/crates/piston-core/Cargo.toml similarity index 87% rename from crates/ratchet-core/Cargo.toml rename to crates/piston-core/Cargo.toml index ef2e9f68..230797bb 100644 --- a/crates/ratchet-core/Cargo.toml +++ b/crates/piston-core/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "ratchet" +name = "piston" version = "0.1.0" -edition = "2021" +edition = "2024" [features] default = ["rand", "testing"] @@ -13,8 +13,8 @@ pyo3 = ["dep:pyo3", "dep:numpy", "dep:regex"] debug = [] #dump every node [dependencies] -ratchet-macros = { path = "../ratchet-macros" } -inline-wgsl = { git = "https://github.com/FL33TW00D/inline-wgsl.git", branch = "master" } +piston-macros = { path = "../piston-macros" } +inline-wgsl = { git = "https://github.com/vinhowe/inline-wgsl.git", branch = "master" } wgpu = { workspace = true } bytemuck = { workspace = true } half = { workspace = true } @@ -32,9 +32,6 @@ parking_lot = { workspace = true } smallvec = { workspace = true } encase = { workspace = true, features = ["smallvec", "glam"] } pollster = { workspace = true } -getrandom = { workspace = true, features = [ - "js", -] } # Needed for wasm support in `num` trait num = { workspace = true } rand_distr = { workspace = true, optional = true } rand = { workspace = true, optional = true } @@ -71,9 +68,16 @@ maybe-async = { workspace = true } async-trait = "0.1.77" smallvec = { workspace = true, features = ["serde"] } +[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] +features = ["GpuBuffer", "GpuDevice"] +workspace = true + [dev-dependencies] env_logger = { workspace = true } rand = { workspace = true } test-strategy = { workspace = true } ndarray = { workspace = true } proptest = { workspace = true } + +[package.metadata.cargo-machete] +ignored = ["strum"] \ No newline at end of file diff --git a/crates/piston-core/src/backprop.rs b/crates/piston-core/src/backprop.rs new file mode 100644 index 00000000..f2f21ad8 --- /dev/null +++ b/crates/piston-core/src/backprop.rs @@ -0,0 +1,1236 @@ +/// Adapted from candle: +/// https://github.com/huggingface/candle/blob/main/candle-core/src/backprop.rs +/// Methods for backpropagation of gradients. +use crate::ops::{BinaryOp, TernaryOp, UnaryOp}; +use crate::{ + Affine, Alibi, Binary, Broadcast, Cast, Concat, Conv, DType, Flip, Gather, GroupNorm, IndexAdd, + IndexSelect, LazyOp, Matmul, Norm, NormOp, OpTensor, Permute, Powf, Reduce, ReduceOp, Reindex, + RoPE, ScatterAdd, ScopePusher, Slice, Softmax, Tensor, TensorId, TensorOptions, + TensorTypeOrScalar, TensorTypeOrScalarEnum, Ternary, Unary, View, WhereCond, cat, rvec, zeros, +}; +use crate::{HashMap, HashSet, TriluOp}; +use anyhow::Result; + +#[derive(thiserror::Error, Debug)] +pub enum BackpropError { + #[error("Tensor is not resolved")] + BackwardNotSupported { op: &'static str }, +} + +// arg has been reduced to node via reduce_dims, expand it back to arg. +// This has to handle keepdims. +fn broadcast_back(arg: &Tensor, node: &Tensor, reduced_dims: &[usize]) -> Result { + if arg.dim() == node.dim() { + // keepdim = true + node.clone().broadcast_to(arg.shape().clone()) + } else { + // keepdim = false + node.clone() + .view(reduced_dims)? + .broadcast_to(arg.shape().clone()) + } +} + +/// Get the gradient tensor associated with the given tensor, or, if it does not exist, +/// insert a tensor of zeroes, with the same shape and type as the given tensors and return it +fn or_insert(tensor: &Tensor) -> Result { + let grad = match tensor.grad() { + Some(grad) => grad, + None => { + let grad = tensor.clone().zeros_like(Default::default())?; + tensor.set_grad(Some(grad.clone())); + grad + } + }; + Ok(grad) +} + +/// Context for gradient accumulation during backpropagation. +/// Tracks which tensors should receive gradients and provides methods for accumulation. +struct GradAccumContext { + tracked: HashSet, +} + +impl GradAccumContext { + fn new(tracked_nodes: &[OpTensor]) -> Self { + let tracked = tracked_nodes.iter().map(|node| node.id()).collect(); + Self { tracked } + } + + /// Add gradient to tensor if it's being tracked + fn add(&self, tensor: &Tensor, grad: Tensor) -> Result<()> { + if !self.tracked.contains(&tensor.id()) { + return Ok(()); + } + match tensor.grad() { + Some(existing_grad) => { + tensor.set_grad(Some(existing_grad.clone().add(grad)?)); + } + None => { + // TODO(vinhowe): This is a hack to avoid creating zeros and then adding; it does + // increase perf. + // It's not great; we should do a tensor copy or something. + tensor.set_grad(Some(grad.mul(1.)?)); + } + } + Ok(()) + } + + /// Subtract gradient from tensor if it's being tracked + fn sub(&self, tensor: &Tensor, grad: Tensor) -> Result<()> { + if !self.tracked.contains(&tensor.id()) { + return Ok(()); + } + match tensor.grad() { + Some(existing_grad) => { + tensor.set_grad(Some(existing_grad.clone().sub(grad)?)); + } + None => { + // TODO(vinhowe): This is a hack to avoid creating zeros and then adding; it does + // increase perf. + // It's not great; we should do a tensor copy or something. + tensor.set_grad(Some(grad.neg()?)); + } + } + Ok(()) + } +} + +thread_local! { + static PISTON_GRAD_DO_NOT_DETACH: bool = { + match std::env::var("PISTON_GRAD_DO_NOT_DETACH") { + Ok(s) => { + !s.is_empty() && s != "0" + }, + Err(_) => false, + } + } +} + +impl Tensor { + /// Return all the nodes that lead to this value in a topologically sorted vec, the first + /// elements having dependencies on the latter ones, e.g. the first element if any is the + /// argument. + /// This assumes that the op graph is a DAG. + // TODO(vinhowe): This could be consolidated with execution_order and whatever caching we + // do... + fn sorted_nodes(&self) -> Vec { + fn walk( + node: &OpTensor, + nodes: Vec, + already_seen: &mut HashMap, + ) -> (bool, Vec) { + if let Some(&tg) = already_seen.get(&node.id()) { + return (tg, nodes); + } + let mut track_grad = false; + let mut nodes = if node.requires_grad() { + // Do not call recursively on the "leaf" nodes. + track_grad = true; + nodes + } else if matches!(node.dtype(), DType::I32 | DType::U32) { + nodes + } else { + match node.op() { + LazyOp::IndexAdd(IndexAdd { + dst: t1, + src: t2, + ids: t3, + .. + }) + | LazyOp::ScatterAdd(ScatterAdd { + dst: t1, + src: t2, + ids: t3, + .. + }) + | LazyOp::Ternary(Ternary { + input: t1, + tensor1: t2, + tensor2: t3, + .. + }) => { + let (tg, nodes) = walk(t1, nodes, already_seen); + track_grad |= tg; + let (tg, nodes) = walk(t2, nodes, already_seen); + track_grad |= tg; + let (tg, nodes) = walk(t3, nodes, already_seen); + track_grad |= tg; + nodes + } + LazyOp::WhereCond(WhereCond { + condition, + on_true, + on_false, + }) => { + let (tg, mut nodes) = walk(condition, nodes, already_seen); + track_grad |= tg; + if let TensorTypeOrScalarEnum::Tensor(on_true) = on_true { + let (tg, _nodes) = walk(on_true, nodes, already_seen); + track_grad |= tg; + nodes = _nodes; + } + if let TensorTypeOrScalarEnum::Tensor(on_false) = on_false { + let (tg, _nodes) = walk(on_false, nodes, already_seen); + track_grad |= tg; + nodes = _nodes; + } + nodes + } + LazyOp::Conv(Conv { + input: lhs, + weight: rhs, + .. + }) + | LazyOp::Gather(Gather { + src: lhs, ids: rhs, .. + }) + | LazyOp::Select(IndexSelect { + src: lhs, + indices: rhs, + .. + }) + | LazyOp::Matmul(Matmul { lhs, rhs, .. }) => { + let (tg, nodes) = walk(lhs, nodes, already_seen); + track_grad |= tg; + let (tg, nodes) = walk(rhs, nodes, already_seen); + track_grad |= tg; + nodes + } + LazyOp::Binary(Binary { lhs, rhs, .. }) => { + let (tg, nodes) = walk(lhs, nodes, already_seen); + track_grad |= tg; + if let TensorTypeOrScalarEnum::Tensor(rhs) = rhs { + let (tg, nodes) = walk(rhs, nodes, already_seen); + track_grad |= tg; + nodes + } else { + nodes + } + } + LazyOp::Lerp(crate::ops::Lerp { input, end, weight }) => { + let (tg, nodes) = walk(input, nodes, already_seen); + track_grad |= tg; + let (tg, nodes) = walk(end, nodes, already_seen); + track_grad |= tg; + if let TensorTypeOrScalarEnum::Tensor(weight) = weight { + let (tg, nodes) = walk(weight, nodes, already_seen); + track_grad |= tg; + nodes + } else { + nodes + } + } + LazyOp::Concat(Concat { inputs, .. }) => { + inputs.iter().fold(nodes, |nodes, input| { + let (tg, nodes) = walk(input, nodes, already_seen); + track_grad |= tg; + nodes + }) + } + LazyOp::Affine(Affine { + src: input, mul, .. + }) => { + if *mul == 0. { + nodes + } else { + let (tg, nodes) = walk(input, nodes, already_seen); + track_grad |= tg; + nodes + } + } + LazyOp::Unary(Unary { + input: node, + op: + UnaryOp::Gelu + | UnaryOp::Tanh + | UnaryOp::Exp + | UnaryOp::Log + | UnaryOp::Sin + | UnaryOp::Cos + | UnaryOp::Abs + | UnaryOp::Square + | UnaryOp::Sqrt + | UnaryOp::Relu + | UnaryOp::Relu2 + | UnaryOp::Neg + | UnaryOp::Reciprocal + | UnaryOp::Silu + | UnaryOp::Sigmoid + | UnaryOp::Swiglu, + .. + }) + | LazyOp::Reduce(Reduce { + input: node, + op: ReduceOp::Min | ReduceOp::Sum | ReduceOp::Max | ReduceOp::Norm2, + .. + }) + | LazyOp::Reindex(Reindex::Permute(Permute { src: node, .. })) + | LazyOp::Reindex(Reindex::Broadcast(Broadcast { src: node, .. })) + | LazyOp::Reindex(Reindex::Slice(Slice { src: node, .. })) + | LazyOp::Reindex(Reindex::Flip(Flip { src: node, .. })) + | LazyOp::Softmax(Softmax { input: node, .. }) + | LazyOp::RoPE(RoPE { input: node, .. }) + | LazyOp::Powf(Powf { src: node, .. }) => { + let (tg, nodes) = walk(node, nodes, already_seen); + track_grad |= tg; + nodes + } + LazyOp::View(View { src: node, .. }) => { + let (tg, nodes) = walk(node, nodes, already_seen); + track_grad |= tg; + nodes + } + LazyOp::Norm(NormOp::RMSNorm(Norm { input: node, .. })) + | LazyOp::Norm(NormOp::LayerNorm(Norm { input: node, .. })) + | LazyOp::Norm(NormOp::GroupNorm(GroupNorm { + norm: Norm { input: node, .. }, + .. + })) => { + let (tg, nodes) = walk(node, nodes, already_seen); + track_grad |= tg; + nodes + } + LazyOp::Cast(Cast { input, .. }) => { + if input.dtype().is_float() { + let (tg, nodes) = walk(input, nodes, already_seen); + track_grad |= tg; + nodes + } else { + nodes + } + } + LazyOp::IndexWrite(_) => todo!("index write walking for backprop"), + LazyOp::Copy(_) => todo!("copy walking for backprop"), + LazyOp::Detach(_) + | LazyOp::Cmp(_) + | LazyOp::Unary(Unary { + op: + UnaryOp::IsInf + | UnaryOp::IsNan + | UnaryOp::Ceil + | UnaryOp::Floor + | UnaryOp::LogicalNot, + .. + }) + | LazyOp::Const + | LazyOp::Alibi(_) + | LazyOp::Reduce(Reduce { + op: ReduceOp::ArgMax | ReduceOp::ArgMin, + .. + }) + | LazyOp::Eye(_) + | LazyOp::FillPointwise(_) + | LazyOp::Bernoulli(_) + | LazyOp::Multinomial(_) + | LazyOp::Arange(_) + | LazyOp::Cache(_) + | LazyOp::Trilu(_) + | LazyOp::TopK(_) + | LazyOp::OneHot(_) => nodes, + } + }; + already_seen.insert(node.id(), track_grad); + if track_grad { + nodes.push(node.clone()); + log::trace!("Tracking grad for node {:?}", node.id()); + } else { + log::trace!("Not tracking grad for node {:?}", node.id()); + } + (track_grad, nodes) + } + let (_tg, mut nodes) = walk( + &self.inner_or_source().clone(), + vec![], + &mut HashMap::default(), + ); + nodes.reverse(); + nodes + } + + pub fn backward(&self) -> Result<()> { + let _scope_guard = ScopePusher::new("backward"); + let sorted_nodes = self.sorted_nodes(); + + // Create gradient context for tracked tensors + let ctx = GradAccumContext::new(&sorted_nodes); + + self.set_grad(Some( + self.clone().ones_like(Default::default())?.contiguous()?, + )); + for node in sorted_nodes.iter() { + let node = node.clone().wrap(); + let _op_scope_guard = ScopePusher::new(&format!("for:{}", node.op().name())); + if node.requires_grad() { + continue; + } + log::trace!("Backwarding {:?}", node.id()); + // This just says that we don't track intermediate gradients. + let grad = if node.retains_grad() { + node.grad() + } else { + node.take_grad() + } + .expect("piston internal error - grad not populated"); + // From candle: + // https://github.com/huggingface/candle/issues/1241 + // Ideally, we would make these operations in place where possible to ensure that we + // do not have to allocate too often. Here we just call `.detach` to avoid computing + // the backprop graph of the backprop itself. This would be an issue for second order + // derivatives but these are out of scope at the moment. + let do_not_detach = PISTON_GRAD_DO_NOT_DETACH.with(|b| *b); + let grad = if do_not_detach { grad } else { grad.detach()? }; + match node.op() { + LazyOp::Binary(Binary { + lhs, + rhs, + op: BinaryOp::Add, + }) => { + let lhs = lhs.wrap(); + ctx.add(&lhs, grad.clone())?; + if let TensorTypeOrScalarEnum::Tensor(rhs) = rhs { + let rhs = rhs.wrap(); + ctx.add(&rhs, grad)?; + } + } + LazyOp::Binary(Binary { + lhs, + rhs, + op: BinaryOp::Sub, + }) => { + let lhs = lhs.wrap(); + ctx.add(&lhs, grad.clone())?; + if let TensorTypeOrScalarEnum::Tensor(rhs) = rhs { + let rhs = rhs.wrap(); + ctx.sub(&rhs, grad)?; + } + } + LazyOp::Binary(Binary { + lhs, + rhs, + op: BinaryOp::Mul, + }) => { + let lhs = lhs.wrap(); + let rhs = rhs.map_tensor(|t| t.wrap())?; + let lhs_grad = grad.clone().mul(rhs.clone())?; + ctx.add(&lhs, lhs_grad)?; + if let TensorTypeOrScalarEnum::Tensor(rhs) = rhs { + let rhs_grad = grad.mul(lhs.clone())?; + ctx.add(&rhs, rhs_grad)?; + } + } + LazyOp::Binary(Binary { + lhs, + rhs, + op: BinaryOp::Div, + }) => { + let lhs = lhs.wrap(); + let rhs = rhs.map_tensor(|t| t.wrap())?; + let lhs_grad = grad.clone().div(rhs.clone())?; + ctx.add(&lhs, lhs_grad)?; + if let TensorTypeOrScalarEnum::Tensor(rhs) = rhs { + let rhs_grad = grad.mul(lhs.clone())?.div(rhs.clone().square()?)?; + ctx.sub(&rhs, rhs_grad)?; + } + } + LazyOp::Binary(Binary { + lhs, + rhs, + op: BinaryOp::Pow, + }) => { + let lhs = lhs.wrap(); + let rhs = rhs.map_tensor(|t| t.wrap())?; + let lhs_grad = grad.clone().mul(rhs.clone())?; + ctx.add(&lhs, lhs_grad)?; + if let TensorTypeOrScalarEnum::Tensor(rhs) = rhs { + let rhs_grad = grad.clone().mul(lhs.clone())?.div(rhs.clone())?; + ctx.add(&rhs, rhs_grad)?; + } + } + LazyOp::Binary(Binary { + lhs, + rhs, + op: BinaryOp::Maximum, + }) + | LazyOp::Binary(Binary { + lhs, + rhs, + op: BinaryOp::Minimum, + }) => { + let lhs = lhs.wrap(); + let rhs = rhs.map_tensor(|t| t.wrap())?; + let mask_lhs = node.clone().eq(lhs.clone())?.cast(grad.dtype())?; + let mask_rhs = node.clone().eq(rhs.clone())?.cast(grad.dtype())?; + + // If both masks are 1 one the same point, we want to scale the + // gradient by 0.5 rather than 1. + let lhs_grad = mask_lhs + .clone() + .mul(grad.clone())? + .div((mask_rhs.clone() + 1.)?)?; + ctx.add(&lhs, lhs_grad)?; + + if let TensorTypeOrScalarEnum::Tensor(rhs) = rhs { + let rhs_grad = mask_rhs.mul(grad)?.div((mask_lhs + 1.)?)?; + ctx.add(&rhs, rhs_grad)?; + } else { + return Err(BackpropError::BackwardNotSupported { + op: "Maximum/Minimum with scalar rhs", + })?; + } + } + LazyOp::Ternary(Ternary { + input, + tensor1, + tensor2, + value, + op: TernaryOp::Addcdiv, + }) => { + let input = input.wrap(); + let tensor1 = tensor1.wrap(); + let tensor2 = tensor2.wrap(); + // addcdiv: input + value * (tensor1 / tensor2) + // Gradient for input is simply grad + ctx.add(&input, grad.clone())?; + + // Gradient for tensor1 is grad * value / tensor2 + let tensor1_grad = grad + .clone() + .mul(tensor2.clone().recip()?)? + .affine(value, 0.)?; + ctx.add(&tensor1, tensor1_grad)?; + + // Gradient for tensor2 is -grad * value * tensor1 / tensor2^2 + let tensor2_grad = grad + .mul(tensor1.clone())? + .div(tensor2.clone().square()?)? + .affine(-value, 0.)?; + ctx.add(&tensor2, tensor2_grad)?; + } + LazyOp::Ternary(Ternary { + input, + tensor1, + tensor2, + value, + op: TernaryOp::Addcmul, + }) => { + let input = input.wrap(); + let tensor1 = tensor1.wrap(); + let tensor2 = tensor2.wrap(); + // addcmul: input + value * (tensor1 * tensor2) + // Gradient for input is simply grad + ctx.add(&input, grad.clone())?; + + // Gradient for tensor1 is grad * value * tensor2 + let tensor1_grad = grad.clone().mul(tensor2.clone())?.affine(value, 0.)?; + ctx.add(&tensor1, tensor1_grad)?; + + // Gradient for tensor2 is grad * value * tensor1 + let tensor2_grad = grad.mul(tensor1.clone())?.affine(value, 0.)?; + ctx.add(&tensor2, tensor2_grad)?; + } + LazyOp::Lerp(crate::ops::Lerp { input, end, weight }) => { + let input = input.wrap(); + let end = end.wrap(); + let weight = weight.map_tensor(|t| t.wrap())?; + + // lerp: output = input + weight * (end - input) + // dL/dinput = grad * (1 - weight) + // dL/dend = grad * weight + // dL/dweight = grad * (end - input) (only when weight is a tensor) + + // Compute the gradients that are common to both scalar and tensor `weight`. + let (input_grad, end_grad) = match weight { + TensorTypeOrScalarEnum::Tensor(weight_tensor) => { + let input_grad = grad.clone().mul((1.0 - weight_tensor.clone())?)?; + let end_grad = grad.clone().mul(weight_tensor.clone())?; + // Gradient w.r.t. the weight tensor itself. + let weight_grad = grad.clone().mul(end.clone().sub(input.clone())?)?; + ctx.add(&weight_tensor, weight_grad)?; + (input_grad, end_grad) + } + TensorTypeOrScalarEnum::Scalar(weight_scalar) => { + let input_grad = grad.clone().mul(1.0 - weight_scalar)?; + let end_grad = grad.mul(weight_scalar)?; + (input_grad, end_grad) + } + }; + + ctx.add(&input, input_grad)?; + ctx.add(&end, end_grad)?; + } + LazyOp::WhereCond(WhereCond { + condition, + on_true, + on_false, + }) => { + let condition = condition.wrap(); + let on_true = on_true.map_tensor(|t| t.wrap())?; + let on_false = on_false.map_tensor(|t| t.wrap())?; + let zeros = grad.clone().zeros_like(Default::default())?; + if let TensorTypeOrScalarEnum::Tensor(on_true) = on_true { + let t_grad = grad.clone().where_cond(condition.clone(), zeros.clone())?; + ctx.add(&on_true, t_grad)?; + } + if let TensorTypeOrScalarEnum::Tensor(on_false) = on_false { + let f_grad = zeros.clone().where_cond(condition.clone(), grad)?; + ctx.add(&on_false, f_grad)?; + } + } + LazyOp::Matmul(Matmul { + lhs, + rhs, + trans_lhs, + trans_rhs, + trans_dst, + bias, + }) => { + let lhs = lhs.wrap(); + let rhs = rhs.wrap(); + let bias = bias.map(|t| t.wrap()); + + let lhs_grad = + grad.clone() + .gemm(rhs.clone(), None, trans_dst, !trans_rhs, trans_lhs)?; + ctx.add(&lhs, lhs_grad)?; + + let rhs_grad = + lhs.clone() + .gemm(grad.clone(), None, !trans_lhs, trans_dst, trans_rhs)?; + ctx.add(&rhs, rhs_grad)?; + + // Calculate the gradient with respect to the bias term + if let Some(bias) = bias { + let bias_grad = grad.sum(1, true)?; // Assuming bias is summed over the appropriate axis + ctx.add(&bias, bias_grad)?; + } + } + LazyOp::Reindex(Reindex::Broadcast(Broadcast { src, .. })) => { + let src = src.wrap(); + let src_shape = src.shape(); + let node_shape = node.shape(); + let arg_dims = src_shape.inner(); + let node_dims = node_shape.inner(); + + let left_dims = node_dims.len() - arg_dims.len(); + let mut sum_dims: Vec = (0..left_dims).collect(); + for (dim, (node_dim, arg_dim)) in node_dims[left_dims..] + .iter() + .zip(arg_dims.iter()) + .enumerate() + { + if node_dim != arg_dim { + sum_dims.push(dim + left_dims); + } + } + + let mut arg_grad = grad.sum(sum_dims.as_slice(), true)?; + for _i in 0..left_dims { + arg_grad = arg_grad.squeeze(())?; + } + ctx.add(&src, arg_grad.broadcast_to(src.shape().clone())?)?; + } + LazyOp::Reindex(Reindex::Slice(Slice { src: arg, indices })) => { + let arg = arg.wrap(); + let arg_shape = arg.shape(); + let arg_dims = arg_shape.inner(); + let index_lens = indices.iter().map(|range| range.end - range.start); + + // Get index of first dimension with length that doesn't match as a heuristic + // to make cat work + let first_different_index = arg_dims + .iter() + .zip(index_lens) + .position(|(arg_dim, slice_dim)| *arg_dim != slice_dim) + .unwrap(); + + let left_pad = if indices[first_different_index].start == 0 { + None + } else { + let mut dims = arg_dims.clone(); + dims[first_different_index] = indices[first_different_index].start; + Some(zeros(dims, TensorOptions::new().device(arg.device()))?) + }; + + let right_pad = + if arg_dims[first_different_index] == indices[first_different_index].end { + None + } else { + let mut dims = arg_dims.clone(); + dims[first_different_index] = arg_dims[first_different_index] + - indices[first_different_index].end; + Some(zeros(dims, TensorOptions::new().device(arg.device()))?) + }; + + let arg_grad = match (left_pad, right_pad) { + (None, None) => grad.clone(), + (Some(left_pad), None) => { + cat(rvec![left_pad, grad], first_different_index)? + } + (None, Some(right_pad)) => { + cat(rvec![grad, right_pad], first_different_index)? + } + (Some(left_pad), Some(right_pad)) => { + cat(rvec![left_pad, grad, right_pad], first_different_index)? + } + }; + + ctx.add(&arg, arg_grad)?; + } + LazyOp::Reindex(Reindex::Permute(Permute { src: arg, dims })) => { + let arg = arg.wrap(); + let mut inv_dims = rvec![0; dims.len()]; + for (i, &dim) in dims.iter().enumerate() { + inv_dims[dim] = i; + } + let arg_grad = grad.permute(inv_dims)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Reindex(Reindex::Flip(Flip { src: arg, dims })) => { + let arg = arg.wrap(); + let arg_grad = grad.flip(dims.clone())?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Reduce(Reduce { + input: arg, + reduced_shape, + op: ReduceOp::Sum, + .. + }) => { + let arg = arg.wrap(); + let grad = broadcast_back(&arg, &grad, reduced_shape.inner())?; + ctx.add(&arg, grad)?; + } + LazyOp::Reduce(Reduce { + input: arg, + reduced_shape, + op: ReduceOp::Norm2, + .. + }) => { + let arg = arg.wrap(); + // For L2 norm, gradient is: 2 * x / ||x||_2 + // But we need to be careful about the chain rule with the broadcast + let norm_result = broadcast_back(&arg, &node, reduced_shape.inner())?; + let input_broadcasted = broadcast_back(&arg, &arg, reduced_shape.inner())?; + let grad_broadcasted = broadcast_back(&arg, &grad, reduced_shape.inner())?; + + // Compute 2 * x / ||x||_2 * grad + let grad_input = input_broadcasted + .mul(2.0)? + .div(norm_result)? + .mul(grad_broadcasted)?; + + ctx.add(&arg, grad_input)?; + } + LazyOp::Reduce(Reduce { + input: arg, + reduced_shape, + op: ReduceOp::Max, + .. + }) => { + let arg = arg.wrap(); + let node = broadcast_back(&arg, &node, reduced_shape.inner())?; + let grad = broadcast_back(&arg, &grad, reduced_shape.inner())?; + let grad = node.eq(arg.clone())?.cast(grad.dtype())?.mul(grad)?; + ctx.add(&arg, grad.broadcast_to(arg.shape().clone())?)?; + } + LazyOp::Reduce(Reduce { + input: arg, + reduced_shape, + op: ReduceOp::Min, + .. + }) => { + let arg = arg.wrap(); + let node = broadcast_back(&arg, &node, reduced_shape.inner())?; + let grad = broadcast_back(&arg, &grad, reduced_shape.inner())?; + let grad = node.eq(arg.clone())?.cast(grad.dtype())?.mul(grad)?; + ctx.add(&arg, grad.broadcast_to(arg.shape().clone())?)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Log, + }) => { + let arg = arg.wrap(); + let arg_grad = (grad / arg.clone())?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Sin, + }) => { + let arg = arg.wrap(); + let arg_grad = (grad * arg.clone().cos())?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Cos, + }) => { + let arg = arg.wrap(); + let arg_grad = (grad * arg.clone().sin())?; + ctx.sub(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Tanh, + }) => { + let arg = arg.wrap(); + let minus_dtanh = (node.square()? - 1.)?; + let arg_grad = (grad.clone() * minus_dtanh)?; + ctx.sub(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Abs, + }) => { + let arg = arg.wrap(); + let ones = arg.clone().ones_like(Default::default())?; + let abs_grad = arg + .clone() + .ge(arg.clone().zeros_like(Default::default())?)? + .where_cond(ones.clone(), ones.neg()?)?; + let arg_grad = (grad * abs_grad)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Exp, + }) => { + let arg = arg.wrap(); + let arg_grad = (grad * node.clone())?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Neg, + }) => { + let arg = arg.wrap(); + ctx.sub(&arg, grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Reciprocal, + }) => { + let arg = arg.wrap(); + let arg_grad = (grad / arg.clone().square()?)?; + ctx.sub(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: _, + op: UnaryOp::Ceil, + }) => Err(BackpropError::BackwardNotSupported { op: "ceil" })?, + LazyOp::Unary(Unary { + input: _, + op: UnaryOp::Floor, + }) => Err(BackpropError::BackwardNotSupported { op: "floor" })?, + LazyOp::Unary(Unary { + input: _, + op: UnaryOp::LogicalNot, + }) => Err(BackpropError::BackwardNotSupported { op: "logical_not" })?, + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Gelu, + }) => { + let arg = arg.wrap(); + let cube = arg.clone().pow(3.)?; + let tanh = (0.0356774 * cube.clone() + (0.797885 * arg.clone())?)?.tanh()?; + let gelu_grad = (((0.5 * tanh.clone())? + + (0.0535161 * cube + (0.398942 * arg.clone())?)? + * (1. - tanh.clone().pow(2.)?))? + + 0.5)?; + let arg_grad = (grad * gelu_grad)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Relu, + }) => { + let arg = arg.wrap(); + let relu_grad = arg + .clone() + .ge(arg.clone().zeros_like(Default::default())?)? + .cast(arg.dtype())?; + let arg_grad = grad.mul(relu_grad)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Relu2, + }) => { + let arg = arg.wrap(); + let relu_grad = arg.clone().affine(2.0, 0.0)?.mul( + arg.clone() + .ge(arg.clone().zeros_like(Default::default())?)? + .cast(arg.dtype())?, + )?; + let arg_grad = grad.mul(relu_grad)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Silu, + }) => { + let arg = arg.wrap(); + let sigmoid_arg = (arg.clone().neg()?.exp()? + 1.)?.recip()?; + let silu_grad = + (sigmoid_arg.clone() * (1. + (arg.clone() * (1. - sigmoid_arg)?)?)?)?; + let arg_grad = grad.mul(silu_grad)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Swiglu, + }) => { + let arg = arg.wrap(); + // swiglu(x) = x^2 * sigma(x) + // + // By product rule: + // d/dx [x^2 * sigma(x)] = 2x * sigma(x) + x^2 * sigma(x)*(1 - sigma(x)). + + // 1) Compute sigma(x) = 1 / (1 + e^-x). + let sigmoid_arg = (arg.clone().neg()?.exp()? + 1.)?.recip()?; + + // By product rule: + // 2) term1 = 2x * sigma(x). + let product_term_1 = (arg.clone() * 2.)?.mul(sigmoid_arg.clone())?; + + // 3) term2 = x^2 * sigma(x)*(1 - sigma(x)). + let product_term_2 = arg + .clone() + .square()? + .mul(sigmoid_arg.clone())? + .mul((1. - sigmoid_arg.clone())?)?; + + // 4) Final derivative wrt x is term1 + term2; multiply by the chain-rule grad. + let swiglu_grad = product_term_1.add(product_term_2)?; + let arg_grad = grad.mul(swiglu_grad)?; + + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Square, + }) => { + let arg = arg.wrap(); + let arg_grad = arg.clone().mul(grad)?.affine(2., 0.)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Sqrt, + }) => { + let arg = arg.wrap(); + let arg_grad = grad.div(node.clone())?.affine(0.5, 0.)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Unary(Unary { + input: arg, + op: UnaryOp::Sigmoid, + }) => { + let arg = arg.wrap(); + // sigmoid'(x) = sigmoid(x) * (1 - sigmoid(x)) + let arg_grad = grad.clone().mul(node.clone())?.mul((1. - node.clone())?)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Detach(_) => todo!("detach backprop"), + LazyOp::Reduce(Reduce { + input: _, + op: ReduceOp::ArgMax, + .. + }) + | LazyOp::Reduce(Reduce { + input: _, + op: ReduceOp::ArgMin, + .. + }) + | LazyOp::Eye(_) + | LazyOp::OneHot(_) + | LazyOp::FillPointwise(_) + | LazyOp::Bernoulli(_) + | LazyOp::Multinomial(_) + | LazyOp::Arange(_) + | LazyOp::Unary(Unary { + op: UnaryOp::IsInf | UnaryOp::IsNan, + .. + }) => {} + // TopK itself doesn't handle any inputs in the graph; the Gather we parameterize + // with its indices does. Since we have a gather case, we're good to go. + LazyOp::TopK(_) => {} + LazyOp::View(View { src: arg, .. }) => { + let arg = arg.wrap(); + let arg_grad = grad.clone().view(arg.shape().clone())?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Select(IndexSelect { + src: arg, + indices, + dim, + }) => { + let arg = arg.wrap(); + if ctx.tracked.contains(&arg.id()) { + let sum_grad = or_insert(&arg)?; + arg.set_grad(Some(sum_grad.clone().index_add_( + indices.wrap(), + grad.clone(), + dim, + )?)); + } + } + LazyOp::Softmax(Softmax { input: arg, dim }) => { + let arg = arg.wrap(); + // Get the softmax output (s) + let softmax_output = node.clone(); + + // Compute the sum of the gradients + let sum_grad = grad.clone().sum(dim, true)?; + + // Compute the gradient with respect to the softmax input + let input_grad = softmax_output + .clone() + .mul(grad.clone())? + .sub(softmax_output.clone().mul(sum_grad)?)?; + + ctx.add(&arg, input_grad)?; + } + LazyOp::Norm(NormOp::LayerNorm(Norm { + input: arg, + scale, + bias, + eps, + })) => { + let arg = arg.wrap(); + let scale = scale.map(|t| t.wrap()); + let bias = bias.map(|t| t.wrap()); + + // TODO(vinhowe): The following is an AI-generated celebration of laziness, + // and it requires many, many backward ops for a single forward op. This should + // instead be implemented in a single backward kernel, and its implementation + // should be understood by its author (the human, not Gemini 2.5 Pro). + let rank = arg.dim(); + let norm_axis = rank - 1; + let d = arg.shape()[norm_axis] as f32; + + // Determine axes to reduce over for gamma/beta grads (all except norm_axis) + let mut sum_axes: Vec = (0..rank).collect(); + sum_axes.remove(norm_axis); + + // Recompute intermediate values using the correct normalization axis + // Ideally, these should be cached from the forward pass + let mean = arg + .clone() + // Keepdim for broadcasting + .sum(norm_axis, true)? + .affine(1. / d, 0.)?; + let var = arg + .clone() + .sub(mean.clone())? + .square()? + .sum(norm_axis, true)? + .affine(1. / d, 0.)?; // Keepdim for broadcasting + + let std = (var.clone() + eps)?.sqrt()?; + let x_normed = arg.clone().sub(mean)?.div(std.clone())?; + + // Compute gradients w.r.t scale (gamma) and bias (beta) + let grad_gamma = x_normed + .clone() + .mul(grad.clone())? + .sum(sum_axes.as_slice(), true)?; + if let Some(scale) = scale.as_ref() { + ctx.add(&scale.clone(), grad_gamma.squeeze(())?)?; + } + + if let Some(bias) = bias { + let grad_beta = grad.clone().sum(sum_axes.as_slice(), true)?; + ctx.add(&bias, grad_beta.squeeze(())?)?; + } + + // Compute gradient w.r.t normalized input + let grad_x_normed = match scale { + Some(scale) => grad.clone().mul(scale)?, + None => grad.clone(), + }; + + // Compute gradients w.r.t mean and variance (using correct reduction axis) + // dL/dmu = sum(dL/dx_normed * (-1/std)) over norm_axis + let grad_mean = grad_x_normed + .clone() + .sum(norm_axis, true)? + .neg()? + .div(std.clone())?; + + // dL/dVar = sum(dL/dx_normed * (-x_normed)) / (2 * std^2) over norm_axis + let grad_var = grad_x_normed + .clone() + .mul(x_normed.clone())? + .sum(norm_axis, true)? + .neg()? + .div(var.clone().affine(1., eps)?)? // std^2 = var + eps + .affine(0.5, 0.)?; + + // Compute gradient w.r.t input x using the chain rule: + // dL/dx = (dL/dx_normed / std) + (dL/dvar * dvar/dx) + (dL/dmu * dmu/dx) + // dvar/dx = (2/N) * (x - mu) + // dmu/dx = 1/N + let grad_x = grad_x_normed + .div(std.clone())? // (dL/dx_normed / std) + .add( + grad_var + .mul(x_normed.clone().mul(std)?)? + .affine(2. / d, 0.)?, + )? + // dL/dvar * (2/N) * (x - mu) = dL/dvar * (2/N) * x_normed * std + .add(grad_mean.affine(1. / d, 0.)?)?; // dL/dmu * (1/N) + + ctx.add(&arg, grad_x)?; + } + LazyOp::Norm(NormOp::RMSNorm(Norm { + input: arg, + scale, + eps, + .. + })) => { + let arg = arg.wrap(); + let scale = scale.map(|t| t.wrap()); + // Root Mean Square Layer Normalization (RMSNorm) backward pass. + // See https://arxiv.org/abs/1910.07467 for formulation. + // Forward: y = scale * (x * inv_std) where + // inv_std = 1 / sqrt(mean(x^2) + eps) + // + // Derivatives: + // d_scale = sum(dy * x * inv_std) + // d_x = inv_std * dY * scale - (x * inv_std^3) * mean(dY * scale * x) / d + // where d is the size of the normalization dimension. + + let rank = arg.dim(); + let norm_axis = rank - 1; + let d = arg.shape()[norm_axis] as f32; + + // Axes to reduce over when computing d_scale (all except norm_axis) + let mut sum_axes: Vec = (0..rank).collect(); + sum_axes.remove(norm_axis); + + // Recompute statistics needed for backward. + let var = arg + .clone() + .square()? // x^2 + .sum(norm_axis, true)? // sum over norm axis + .affine(1.0 / d, 0.0)?; // mean of squares + let inv_std = (var.clone() + eps)?.sqrt()?.recip()?; // 1 / sqrt(mean(x^2) + eps) + + // x_hat = x * inv_std + let x_normed = arg.clone().mul(inv_std.clone())?; + + // Gradient w.r.t. scale (gamma) + let grad_gamma = x_normed + .clone() + .mul(grad.clone())? + .sum(sum_axes.as_slice(), true)?; + if let Some(scale) = scale.as_ref() { + ctx.add(&scale.clone(), grad_gamma.squeeze(())?)?; + } + + // Intermediate: dY * scale (or identity if no scale) + let grad_scaled = match scale.as_ref() { + Some(scale) => grad.clone().mul(scale.clone())?, + None => grad.clone(), + }; + + // Projection term: sum(dY * scale * x) over norm_axis (keepdim) + let proj = grad_scaled.clone().mul(arg.clone())?.sum(norm_axis, true)?; + + // inv_std^3 + let inv_std_cubed = inv_std.clone().pow(3.0)?; + + // Compute dX + let term1 = grad_scaled.mul(inv_std.clone())?; // inv_std * dY * scale + let term2 = arg + .clone() + .mul(inv_std_cubed)? + .mul(proj)? + .affine(1.0 / d, 0.0)?; // (x * inv_std^3) * proj / d + let grad_x = term1.sub(term2)?; + + ctx.add(&arg, grad_x)?; + } + LazyOp::Affine(Affine { src: arg, mul, .. }) => { + let arg = arg.wrap(); + let arg_grad = grad.affine(mul, 0.)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Gather(Gather { src, ids, dim, .. }) => { + let src = src.wrap(); + let ids = ids.wrap(); + // We can't use or_insert here because we need to scatter into a zero tensor. + let sum_grad = src.clone().zeros_like(Default::default())?; + let src_grad = sum_grad.scatter_add(ids.clone(), grad.clone(), dim)?; + ctx.add(&src, src_grad)?; + } + LazyOp::ScatterAdd(ScatterAdd { dst, src, ids, dim }) => { + let dst = dst.wrap(); + let src = src.wrap(); + let ids = ids.wrap(); + ctx.add(&dst, grad.clone())?; + let src_grad = grad.gather(dim, ids.clone())?; + ctx.add(&src, src_grad)?; + } + LazyOp::Trilu(TriluOp { src: arg, upper, k }) => { + let arg = arg.wrap(); + let masked_grad = if upper { grad.triu(k)? } else { grad.tril(k)? }; + ctx.add(&arg, masked_grad)?; + } + LazyOp::Alibi(Alibi { input, .. }) => { + let input = input.wrap(); + ctx.add(&input, grad)?; + } + LazyOp::Cast(Cast { + input, + dst_dtype: _, + }) => { + let input = input.wrap(); + ctx.add(&input, grad.cast(input.dtype())?)?; + } + LazyOp::Concat(Concat { inputs, dim }) => { + // Split the upstream gradient along `dim` and route slices to inputs + let mut offset: usize = 0; + for input in inputs.iter() { + let input = input.clone().wrap(); + let mut ranges = rvec![]; + for axis in 0..node.dim() { + if axis == dim { + let len = input.shape()[dim]; + ranges.push(offset..offset + len); + } else { + ranges.push(0..input.shape()[axis]); + } + } + let input_grad = grad.clone().slice(&ranges)?; + ctx.add(&input, input_grad)?; + offset += input.shape()[dim]; + } + } + LazyOp::Norm(_) => todo!("norm backprop"), + LazyOp::Const => panic!("piston internal error - const node in backprop"), + LazyOp::Cmp(_) => todo!("cmp backprop"), + LazyOp::Powf(_) => todo!("powf backprop"), + LazyOp::RoPE(RoPE { + input: arg, + dim, + base, + offset, + .. + }) => { + let arg = arg.wrap(); + let arg_grad = grad.rope_backward_(dim, base, offset)?; + ctx.add(&arg, arg_grad)?; + } + LazyOp::Conv(_) => todo!("conv backprop"), + LazyOp::IndexWrite(_) => todo!("index write backprop"), + LazyOp::IndexAdd(_) => todo!("index add backprop"), + LazyOp::Cache(_) => todo!("cache backprop"), + LazyOp::Copy(_) => todo!("copy backprop"), + }; + } + Ok(()) + } +} diff --git a/crates/ratchet-core/src/compiled_op.rs b/crates/piston-core/src/compiled_op.rs similarity index 83% rename from crates/ratchet-core/src/compiled_op.rs rename to crates/piston-core/src/compiled_op.rs index 09876e93..51c73033 100644 --- a/crates/ratchet-core/src/compiled_op.rs +++ b/crates/piston-core/src/compiled_op.rs @@ -1,12 +1,15 @@ #[cfg(feature = "debug")] +use crate::TensorId; +#[cfg(feature = "debug")] use crate::gpu::BindGroupLayoutEntryDescriptor; -use crate::gpu::{ - BindGroupDescriptor, BindGroupLayoutHandle, ComputePipelineHandle, GpuBindGroup, WgpuDevice, - WorkgroupCount, +use crate::{KernelKey, OpTensor, OperationError, PooledGPUBuffer, RVec, drvec, rvec}; +use crate::{ + TensorId, + gpu::{ + BindGroupDescriptor, BindGroupLayoutHandle, ComputePipelineHandle, GpuBindGroup, + WgpuDevice, WorkgroupCount, + }, }; -#[cfg(feature = "debug")] -use crate::TensorId; -use crate::{drvec, rvec, KernelKey, OperationError, PooledGPUBuffer, RVec, Tensor}; use derive_new::new; use std::sync::Arc; use wgpu::DynamicOffset; @@ -40,6 +43,11 @@ pub struct CompiledOp { pub(crate) storage_groups: RVec, pub(crate) offset: DynamicOffset, //offset into the metadata uniform buffer pub kernel_key: KernelKey, + // Mapping between tensor and compiled op is not necessarily 1:1—for example: things like AMP + // likely insert casts between tensor operations. + pub tensor_id: Option, + #[cfg(not(feature = "debug"))] + pub debug_buffer: Option, #[cfg(feature = "debug")] pub debug_buffer: Option<(TensorId, Arc)>, #[cfg(feature = "debug")] @@ -48,6 +56,7 @@ pub struct CompiledOp { pub storage_bind_group_layout_entries: RVec, } +#[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum Compiled { Copy(CompiledCopy), @@ -58,8 +67,8 @@ impl CompiledOp { const MAX_BINDINGS_PER_GROUP: usize = 8; pub fn create_storage_bind_groups( - srcs: &[&Tensor], - dst: &Tensor, + srcs: &[&OpTensor], + dst: &OpTensor, bind_group_layouts: RVec, device: &WgpuDevice, inplace: bool, diff --git a/crates/piston-core/src/cpu/binary.rs b/crates/piston-core/src/cpu/binary.rs new file mode 100644 index 00000000..8bd74b34 --- /dev/null +++ b/crates/piston-core/src/cpu/binary.rs @@ -0,0 +1,189 @@ +use crate::cpu::cpu_store_result; +use crate::{ + Binary, BinaryOp, CPUOperation, DType, OpTensor, OperationError, TensorDType, + TensorTypeOrScalar, TensorTypeOrScalarEnum, +}; +use core::marker::PhantomData; +use half::{bf16, f16}; +use maybe_async::maybe_async; +use num_traits::NumOps; + +// Helper function to cast f32 scalar to the target type T +fn cast_scalar_to_type(scalar: f32) -> T { + match T::dtype() { + DType::F32 => unsafe { std::mem::transmute_copy(&scalar) }, + DType::F16 => unsafe { std::mem::transmute_copy(&f16::from_f32(scalar)) }, + DType::BF16 => unsafe { std::mem::transmute_copy(&bf16::from_f32(scalar)) }, + _ => panic!("Unsupported scalar cast to type: {:?}", T::dtype()), + } +} + +#[inline] +pub(crate) fn binary_map( + lhs: &[T], + rhs: &[T], + dst: &mut [U], + f: fn(T, T) -> U, +) { + assert_eq!(lhs.len(), dst.len()); + assert_eq!(rhs.len(), dst.len()); + for ((l, r), d) in lhs + .iter() + .copied() + .zip(rhs.iter().copied()) + .zip(dst.iter_mut()) + { + *d = f(l, r); + } +} + +pub(crate) fn binary_map_scalar( + lhs: &[T], + rhs: T, + dst: &mut [U], + f: fn(T, T) -> U, +) { + assert_eq!(lhs.len(), dst.len()); + for (l, d) in lhs.iter().copied().zip(dst.iter_mut()) { + *d = f(l, rhs); + } +} + +#[inline] +pub(crate) fn binary_map_inplace(lhs: &mut [T], rhs: &[T], f: fn(T, T) -> T) { + assert_eq!(lhs.len(), rhs.len()); + lhs.iter_mut().zip(rhs.iter()).for_each(|(l, r)| { + *l = f(*l, *r); + }); +} + +pub(crate) fn binary_map_scalar_inplace(lhs: &mut [T], rhs: T, f: fn(T, T) -> T) { + lhs.iter_mut().for_each(|l| { + *l = f(*l, rhs); + }); +} + +#[inline] +#[maybe_async] +pub(crate) async fn binary_apply( + lhs: &OpTensor, + rhs: &OpTensor, + dst: &OpTensor, + f: fn(T, T) -> U, +) -> Result<(), OperationError> { + let lhs = lhs.to_vec::().await?; + let rhs = rhs.to_vec::().await?; + let mut result = vec![U::zero(); dst.shape().numel()]; + binary_map(&lhs, &rhs, &mut result, f); + cpu_store_result(dst, &result); + Ok(()) +} + +#[inline] +#[maybe_async] +pub(crate) async fn binary_apply_inplace>( + lhs: &OpTensor, + rhs: &RHS, + dst: &OpTensor, + f: fn(T, T) -> T, +) -> Result<(), OperationError> { + let mut lhs = lhs.to_vec::().await?; + match rhs.tensor_or_scalar()? { + TensorTypeOrScalarEnum::Tensor(rhs) => { + let rhs = rhs.to_vec::().await?; + binary_map_inplace(&mut lhs, &rhs, f); + } + TensorTypeOrScalarEnum::Scalar(rhs) => { + let rhs_typed = cast_scalar_to_type::(rhs); + binary_map_scalar_inplace(&mut lhs, rhs_typed, f); + } + } + cpu_store_result(dst, &lhs); + Ok(()) +} + +pub struct BinaryOps { + dtype: PhantomData, +} + +macro_rules! impl_cpu_binary_op { + ($method_name:ident, $dtype:ident, $op:expr) => { + #[maybe_async] + async fn $method_name>( + lhs: &OpTensor, + rhs: &RHS, + dst: &OpTensor, + ) -> Result { + binary_apply_inplace::<$dtype, RHS>(lhs, rhs, dst, $op).await?; + Ok(dst.clone()) + } + }; +} + +macro_rules! cpu_binary_op_fn { + ($method_name:ident, $op:expr) => { + #[inline] + pub(crate) fn $method_name(lhs: &mut [T], rhs: &[T]) { + binary_map_inplace::(lhs, rhs, $op); + } + }; +} + +cpu_binary_op_fn!(add, |lhs, rhs| lhs + rhs); +cpu_binary_op_fn!(sub, |lhs, rhs| lhs - rhs); +cpu_binary_op_fn!(mul, |lhs, rhs| lhs * rhs); +cpu_binary_op_fn!(div, |lhs, rhs| lhs / rhs); +cpu_binary_op_fn!(maximum, |lhs, rhs| if lhs > rhs { lhs } else { rhs }); + +macro_rules! impl_cpu_binary { + ($dtype:ident) => { + impl BinaryOps<$dtype> { + impl_cpu_binary_op!(add, $dtype, |lhs, rhs| lhs + rhs); + impl_cpu_binary_op!(sub, $dtype, |lhs, rhs| lhs - rhs); + impl_cpu_binary_op!(mul, $dtype, |lhs, rhs| lhs * rhs); + impl_cpu_binary_op!(div, $dtype, |lhs, rhs| lhs / rhs); + impl_cpu_binary_op!(maximum, $dtype, |lhs, rhs| if lhs > rhs { + lhs + } else { + rhs + }); + impl_cpu_binary_op!(minimum, $dtype, |lhs, rhs| if lhs < rhs { + lhs + } else { + rhs + }); + #[maybe_async] + pub async fn apply(op: &Binary, dst: OpTensor) -> Result { + match op.op() { + BinaryOp::Add => Self::add(op.lhs(), op.rhs(), &dst).await, + BinaryOp::Sub => Self::sub(op.lhs(), op.rhs(), &dst).await, + BinaryOp::Mul => Self::mul(op.lhs(), op.rhs(), &dst).await, + BinaryOp::Div => Self::div(op.lhs(), op.rhs(), &dst).await, + BinaryOp::Pow => Err(OperationError::UnknownError(anyhow::anyhow!( + "Pow is not supported for CPU" + ))), + BinaryOp::Maximum => Self::maximum(op.lhs(), op.rhs(), &dst).await, + BinaryOp::Minimum => Self::minimum(op.lhs(), op.rhs(), &dst).await, + } + } + } + }; +} + +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +impl CPUOperation for Binary { + #[maybe_async] + async fn apply_cpu(&self, dst: OpTensor) -> Result { + match dst.dtype() { + DType::F32 => BinaryOps::::apply(self, dst).await, + DType::F16 => BinaryOps::::apply(self, dst).await, + DType::BF16 => BinaryOps::::apply(self, dst).await, + _ => todo!(), + } + } +} + +impl_cpu_binary!(f32); +impl_cpu_binary!(f16); +impl_cpu_binary!(bf16); diff --git a/crates/ratchet-core/src/cpu/gemm.rs b/crates/piston-core/src/cpu/gemm.rs similarity index 67% rename from crates/ratchet-core/src/cpu/gemm.rs rename to crates/piston-core/src/cpu/gemm.rs index b932b37b..56de08e1 100644 --- a/crates/ratchet-core/src/cpu/gemm.rs +++ b/crates/piston-core/src/cpu/gemm.rs @@ -1,11 +1,12 @@ use crate::{ - cpu::cpu_store_result, CPUOperation, DType, InvariantError, Matmul, MatmulSpec, OperationError, - Shape, Strides, Tensor, TensorDType, + CPUOperation, DType, InvariantError, Matmul, MatmulSpec, OpTensor, OperationError, Shape, + Stride, TensorDType, cpu::cpu_store_result, }; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use core::str::FromStr; -use gemm::{gemm as gemm_kernel, Parallelism}; +use gemm::{Parallelism, gemm as gemm_kernel}; use half::{bf16, f16}; +use maybe_async::maybe_async; use std::num::NonZeroUsize; fn get_num_threads() -> NonZeroUsize { @@ -29,15 +30,15 @@ fn get_parallelism() -> Parallelism { fn calculate_skips( lhs_shape: &Shape, - lhs_strides: &[isize], + lhs_stride: &[isize], rhs_shape: &Shape, - rhs_strides: &[isize], + rhs_stride: &[isize], rank: usize, m: usize, n: usize, k: usize, ) -> Result<(usize, usize)> { - let lhs_skip: usize = match lhs_strides[..rank - 2] { + let lhs_skip: usize = match lhs_stride[..rank - 2] { [s1, stride] if s1 == stride * lhs_shape[1] as isize => stride as usize, [_, stride] if lhs_shape[0] == 1 => stride as usize, [stride, _] if lhs_shape[1] == 1 => stride as usize, @@ -45,7 +46,7 @@ fn calculate_skips( [] => m * k, _ => Err(anyhow!("non-contiguous lhs"))?, }; - let rhs_skip: usize = match rhs_strides[..rank - 2] { + let rhs_skip: usize = match rhs_stride[..rank - 2] { [s1, stride] if s1 == stride * rhs_shape[1] as isize => stride as usize, [_, stride] if rhs_shape[0] == 1 => stride as usize, [stride, _] if rhs_shape[1] == 1 => stride as usize, @@ -59,39 +60,39 @@ fn calculate_skips( pub(crate) fn gemm( lhs: &[T], lhs_shape: &Shape, - lhs_strides: &Strides, + lhs_stride: &Stride, rhs: &[T], rhs_shape: &Shape, - rhs_strides: &Strides, - dst_strides: &Strides, + rhs_stride: &Stride, + dst_stride: &Stride, b: usize, m: usize, n: usize, k: usize, ) -> Result, OperationError> { - let lhs_strides = lhs_strides.to_vec(); - let rhs_strides = rhs_strides.to_vec(); - let rank = lhs_shape.rank(); + let lhs_stride = lhs_stride.to_vec(); + let rhs_stride = rhs_stride.to_vec(); + let rank = lhs_shape.dim(); - let lhs_cs = lhs_strides[rank - 1]; - let lhs_rs = lhs_strides[rank - 2]; + let lhs_cs = lhs_stride[rank - 1]; + let lhs_rs = lhs_stride[rank - 2]; - let rhs_cs = rhs_strides[rank - 1]; - let rhs_rs = rhs_strides[rank - 2]; + let rhs_cs = rhs_stride[rank - 1]; + let rhs_rs = rhs_stride[rank - 2]; let (lhs_skip, rhs_skip) = calculate_skips( lhs_shape, - &lhs_strides, + &lhs_stride, rhs_shape, - &rhs_strides, + &rhs_stride, rank, m, n, k, )?; let dst_skip: usize = m * n; - let dst_rs = dst_strides[0]; - let dst_cs = dst_strides[1]; + let dst_rs = dst_stride[0]; + let dst_cs = dst_stride[1]; let mut dst = vec![T::zero(); b * m * n]; @@ -133,38 +134,32 @@ fn gemm_impl( ) -> Result, OperationError> { let lhs_shape = spec.lhs_shape(); let rhs_shape = spec.rhs_shape(); - let lhs_strides = spec.lhs_strides(); - let rhs_strides = spec.rhs_strides(); - let dst_strides = spec.dst_strides(); + let lhs_stride = spec.lhs_stride(); + let rhs_stride = spec.rhs_stride(); + let dst_stride = spec.dst_stride(); let b = spec.stacks(); let m = spec.m(); let n = spec.n(); let k = spec.k(); gemm( - lhs, - lhs_shape, - lhs_strides, - rhs, - rhs_shape, - rhs_strides, - dst_strides, - b, - m, - n, - k, + lhs, lhs_shape, lhs_stride, rhs, rhs_shape, rhs_stride, dst_stride, b, m, n, k, ) } +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] impl CPUOperation for Matmul { - fn apply_cpu(&self, dst: Tensor) -> Result { - fn run_gemm( + #[maybe_async] + async fn apply_cpu(&self, dst: OpTensor) -> Result { + #[maybe_async] + async fn run_gemm( spec: MatmulSpec, - lhs: &Tensor, - rhs: &Tensor, - dst: &Tensor, + lhs: &OpTensor, + rhs: &OpTensor, + dst: &OpTensor, ) -> Result<(), OperationError> { - let lhs = lhs.to_vec::()?; - let rhs = rhs.to_vec::()?; + let lhs = lhs.to_vec::().await?; + let rhs = rhs.to_vec::().await?; let result = if spec.trans_dst() { gemm_impl::(spec, &rhs, &lhs)? @@ -178,10 +173,10 @@ impl CPUOperation for Matmul { let Matmul { lhs, rhs, .. } = self; - match self.lhs.dt() { - DType::F32 => run_gemm::(spec, lhs, rhs, &dst), - DType::F16 => run_gemm::(spec, lhs, rhs, &dst), - DType::BF16 => run_gemm::(spec, lhs, rhs, &dst), + match self.lhs.dtype() { + DType::F32 => run_gemm::(spec, lhs, rhs, &dst).await, + DType::F16 => run_gemm::(spec, lhs, rhs, &dst).await, + DType::BF16 => run_gemm::(spec, lhs, rhs, &dst).await, dtype => Err(InvariantError::UnsupportedDType(dtype))?, }?; Ok(dst) diff --git a/crates/piston-core/src/cpu/mod.rs b/crates/piston-core/src/cpu/mod.rs new file mode 100644 index 00000000..8d57bbed --- /dev/null +++ b/crates/piston-core/src/cpu/mod.rs @@ -0,0 +1,254 @@ +mod binary; +pub mod gemm; +mod norm; +pub mod reindex; +pub mod rope; +mod softmax; +mod unary; +mod utils; + +use crate::{ + Cast, Concat, DType, IndexSelect, InvariantError, LazyOp, OpTensor, Operation, OperationError, + RVec, Shape, TensorDType, dequantize, +}; +use half::{bf16, f16}; +use maybe_async::maybe_async; +use rope::cpu_rope; +use unary::unary_apply_fn; +use utils::cpu_store_result; + +#[maybe_async] +pub async fn apply_operation(op: LazyOp, dst: OpTensor) -> Result { + match op { + LazyOp::Binary(b) => b.apply_cpu(dst).await, + LazyOp::Ternary(_t) => todo!(), + LazyOp::Cast(c) => cpu_cast(c, dst).await, + LazyOp::Matmul(m) => m.apply_cpu(dst).await, + LazyOp::Softmax(s) => s.apply_cpu(dst).await, + LazyOp::RoPE(r) => cpu_rope(r, dst).await, + LazyOp::Alibi(_a) => todo!(), + LazyOp::Unary(u) => u.apply_cpu(dst).await, + LazyOp::Reindex(r) => r.apply_cpu(dst).await, + LazyOp::Concat(c) => cpu_concat(c, dst).await, + LazyOp::Norm(n) => n.apply_cpu(dst).await, + LazyOp::Affine(_a) => todo!(), + LazyOp::Lerp(_l) => todo!(), + LazyOp::Cmp(_c) => todo!(), + LazyOp::Powf(_p) => todo!(), + LazyOp::Conv(_c) => todo!(), + LazyOp::Select(i) => cpu_index_select(i, dst).await, + LazyOp::IndexWrite(_i) => todo!(), + LazyOp::Cache(_c) => todo!(), + LazyOp::Trilu(_t) => todo!(), + LazyOp::Const => todo!(), + LazyOp::View(_) => todo!(), + LazyOp::WhereCond(_w) => todo!(), + LazyOp::Reduce(_r) => todo!(), + LazyOp::Gather(_g) => todo!(), + LazyOp::Eye(_e) => todo!(), + LazyOp::FillPointwise(_f) => todo!(), + LazyOp::OneHot(_o) => todo!(), + LazyOp::TopK(_t) => todo!(), + LazyOp::Bernoulli(_b) => todo!(), + LazyOp::Multinomial(_m) => todo!(), + LazyOp::Arange(_a) => todo!(), + LazyOp::IndexAdd(_i) => todo!(), + LazyOp::ScatterAdd(_s) => todo!(), + LazyOp::Detach(_d) => todo!(), + LazyOp::Copy(_c) => todo!(), + } +} + +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +pub trait CPUOperation: Operation { + async fn apply_cpu(&self, dst: OpTensor) -> Result; +} + +#[maybe_async] +async fn index_select( + index_select: IndexSelect, + dst: OpTensor, +) -> Result { + let src = index_select.src(); + let indices = index_select.indices(); + let dim = index_select.dim(); + + // TODO: Add support for other indexing types + if !matches!(indices.dtype(), DType::I32) { + return Err(InvariantError::DTypeMismatch { + expected: DType::I32, + actual: indices.dtype(), + } + .into()); + } + + let mut dst_dims = src.shape().to_vec(); + let indices_dims = indices.shape().to_vec(); + + let src_dim = dst_dims[dim]; + let n_ids = indices_dims[0]; + dst_dims[dim] = n_ids; + + let dst_len: usize = dst_dims.iter().product(); + let left_len: usize = dst_dims[..dim].iter().product(); + let right_len: usize = dst_dims[dim + 1..].iter().product(); + + let src = src.to_vec::().await?; + let indices = indices.to_vec::().await?; + let mut result = vec![T::zero(); dst_len]; + + for left_i in 0..left_len { + let start_src_idx = left_i * right_len * src_dim; + let start_dst_idx = left_i * right_len * n_ids; + for (i, idx) in indices.iter().enumerate().take(n_ids) { + let src_idx = start_src_idx + *idx as usize * right_len; + let dst_idx = start_dst_idx + i * right_len; + result[dst_idx..dst_idx + right_len] + .copy_from_slice(&src[src_idx..src_idx + right_len]); + } + } + cpu_store_result(&dst, &result); + Ok(dst) +} + +#[maybe_async] +async fn qindex_select(op: IndexSelect, dst: OpTensor) -> Result { + // NOTE: qindex_select is functional but not optimized at all. + // Currently we simply dequantize the entire input tensor to f32 and then call index_select. + // Because of borrowing rules dequantizing also requires a deep clone of the input tensor, which is less than ideal. + // In the future we would rather directly index the raw buffer of the quantized tensor and dequantize only what is required. + // TODO: Add support for direct indexing + partial dequantization + let src = op.src().deep_clone().await?.wrap(); + + // NOTE: Support for other quantization types is dependent on the corresponding dequantization functions. + let src = dequantize(src)?; + let indices = op.indices().clone(); + let dim = op.dim(); + + index_select::(IndexSelect::new(src.into(), indices, dim), dst).await +} + +#[maybe_async] +pub async fn cpu_index_select(i: IndexSelect, dst: OpTensor) -> Result { + match i.src().dtype() { + DType::F32 => index_select::(i, dst).await, + DType::F16 => index_select::(i, dst).await, + DType::BF16 => index_select::(i, dst).await, + DType::Q8_0F(_) => qindex_select(i, dst).await, + dtype => Err(InvariantError::UnsupportedDType(dtype).into()), + } +} + +#[maybe_async] +pub async fn cpu_cast(cast: Cast, dst: OpTensor) -> Result { + if cast.input().dtype() == cast.dst_dtype() { + return Ok(cast.input().clone()); + } + match (cast.input().dtype(), cast.dst_dtype()) { + // F32 -> + (DType::F32, DType::F16) => { + unary_apply_fn::(cast.input(), &dst, f16::from_f32).await? + } + (DType::F32, DType::BF16) => { + unary_apply_fn::(cast.input(), &dst, bf16::from_f32).await? + } + (DType::F32, DType::I32) => { + unary_apply_fn::(cast.input(), &dst, |x| x as i32).await? + } + (DType::F32, DType::U32) => { + unary_apply_fn::(cast.input(), &dst, |x| x as u32).await? + } + + // F16 -> + (DType::F16, DType::F32) => { + unary_apply_fn::(cast.input(), &dst, f32::from).await? + } + + // BF16 -> + (DType::BF16, DType::F32) => { + unary_apply_fn::(cast.input(), &dst, f32::from).await? + } + + // I32 -> + (DType::I32, DType::F32) => { + unary_apply_fn::(cast.input(), &dst, |x| x as f32).await? + } + + // U32 -> + (DType::U32, DType::F32) => { + unary_apply_fn::(cast.input(), &dst, |x| x as f32).await? + } + + _ => unimplemented!( + "Cannot cast {:?} -> {:?}", + cast.input().dtype(), + cast.dst_dtype() + ), + }; + + Ok(dst) +} + +pub(crate) fn concat( + inputs: &[(Shape, Vec)], + dim: usize, + dst_shape: &Shape, + dst: &mut [T], +) -> Result<(), OperationError> { + let dst_dim_len = dst_shape[dim]; + let block: usize = dst_shape.iter().skip(1 + dim).product(); + let dst_s = block * dst_dim_len; + let src_o = 0; + let mut dst_o = 0; + for (src_s, src) in inputs { + let a_dim: usize = src_s.iter().take(dim).product(); + let b_dim = block * src_s[dim]; + for idx in 0..a_dim { + let dst_idx = idx * dst_s + dst_o; + let src_idx = idx * b_dim + src_o; + let dst_t = &mut dst[dst_idx..dst_idx + b_dim]; + let src = &src[src_idx..src_idx + b_dim]; + dst_t.copy_from_slice(src) + } + dst_o += b_dim; + } + Ok(()) +} + +#[maybe_async] +pub(crate) async fn apply_concat( + inputs: RVec, + dim: usize, + dst: OpTensor, +) -> Result { + let dst_size = dst.shape().numel(); + let mut result = vec![T::zero(); dst_size]; + + let mut inputs_result = Vec::with_capacity(inputs.len()); + for t in inputs.iter() { + let result: Result<_, OperationError> = match t.to_vec::().await { + Ok(v) => Ok((t.shape().clone(), v)), + Err(e) => Err(e.into()), + }; + inputs_result.push(result?); + } + let inputs = inputs_result; + + concat(&inputs, dim, dst.shape(), &mut result)?; + cpu_store_result(&dst, &result); + Ok(dst) +} + +#[maybe_async] +pub async fn cpu_concat( + Concat { inputs, dim }: Concat, + dst: OpTensor, +) -> Result { + match dst.dtype() { + DType::F32 => apply_concat::(inputs, dim, dst).await, + DType::F16 => apply_concat::(inputs, dim, dst).await, + DType::BF16 => apply_concat::(inputs, dim, dst).await, + dtype => Err(InvariantError::UnsupportedDType(dtype).into()), + } +} diff --git a/crates/ratchet-core/src/cpu/norm.rs b/crates/piston-core/src/cpu/norm.rs similarity index 52% rename from crates/ratchet-core/src/cpu/norm.rs rename to crates/piston-core/src/cpu/norm.rs index ce3389ab..f30dc0fe 100644 --- a/crates/ratchet-core/src/cpu/norm.rs +++ b/crates/piston-core/src/cpu/norm.rs @@ -4,54 +4,61 @@ use crate::cpu::unary::unary_map_inplace; use crate::cpu::utils::cpu_store_result; use crate::reindex::broadcast_vector; use crate::{ - shape, CPUOperation, DType, GroupNorm, InvariantError, Norm, NormOp, OperationError, Shape, - Tensor, TensorDType, + CPUOperation, DType, GroupNorm, InvariantError, Norm, NormOp, OpTensor, OperationError, Shape, + TensorDType, }; use core::iter::Sum; use half::{bf16, f16}; +use maybe_async::maybe_async; use num::Float; use num_traits::NumOps; +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] impl CPUOperation for NormOp { - fn apply_cpu(&self, dst: Tensor) -> Result { + #[maybe_async] + async fn apply_cpu(&self, dst: OpTensor) -> Result { match self { - NormOp::LayerNorm(n) => apply_layer_norm(n, dst), - NormOp::RMSNorm(n) => apply_rms_norm(n, dst), - NormOp::GroupNorm(g) => apply_group_norm(g, dst), + NormOp::LayerNorm(n) => apply_layer_norm(n, dst).await, + NormOp::RMSNorm(n) => apply_rms_norm(n, dst).await, + NormOp::GroupNorm(g) => apply_group_norm(g, dst).await, } } } -fn apply_layer_norm( +#[maybe_async] +async fn apply_layer_norm( Norm { input, scale, bias, eps, }: &Norm, - dst: Tensor, -) -> Result { - if input.dt() != scale.dt() { + dst: OpTensor, +) -> Result { + if let Some(scale) = scale + && input.dtype() != scale.dtype() + { return Err(InvariantError::DTypeMismatch { - expected: input.dt(), - actual: scale.dt(), + expected: input.dtype(), + actual: scale.dtype(), } .into()); } - if let Some(b) = bias { - if b.dt() != input.dt() { - return Err(InvariantError::DTypeMismatch { - expected: input.dt(), - actual: b.dt(), - } - .into()); + if let Some(b) = bias + && b.dtype() != input.dtype() + { + return Err(InvariantError::DTypeMismatch { + expected: input.dtype(), + actual: b.dtype(), } + .into()); } - match input.dt() { - DType::F32 => layer_norm::(input, scale, bias, *eps, &dst)?, - DType::F16 => layer_norm::(input, scale, bias, *eps, &dst)?, - DType::BF16 => layer_norm::(input, scale, bias, *eps, &dst)?, + match input.dtype() { + DType::F32 => layer_norm::(input, scale.as_ref(), bias, *eps, &dst).await?, + DType::F16 => layer_norm::(input, scale.as_ref(), bias, *eps, &dst).await?, + DType::BF16 => layer_norm::(input, scale.as_ref(), bias, *eps, &dst).await?, _ => todo!(), }; @@ -88,25 +95,29 @@ where result } -fn layer_norm( - input: &Tensor, - scale: &Tensor, - bias: &Option, +#[maybe_async] +async fn layer_norm( + input: &OpTensor, + scale: Option<&OpTensor>, + bias: &Option, eps: f32, - dst: &Tensor, + dst: &OpTensor, ) -> Result<(), OperationError> where T: TensorDType + Float + NumOps + for<'a> Sum<&'a T>, { let src_shape = input.shape(); - let rank = input.rank(); + let rank = input.dim(); let N = src_shape[rank - 1]; - let norm_shape = shape!(N); + let norm_shape = N; - let input = input.to_vec::()?; - let scale = scale.to_vec::()?; + let input = input.to_vec::().await?; + let scale = match scale { + Some(s) => Some(s.to_vec::().await?), + None => None, + }; let bias = match bias { - Some(b) => Some(b.to_vec::()?), + Some(b) => Some(b.to_vec::().await?), None => None, }; @@ -133,11 +144,13 @@ where broadcast_vector(&x2, &mut v); mul(&mut x, &v); - let scale_b = broadcast(&scale, &norm_shape, src_shape); - mul(&mut x, &scale_b); + if let Some(scale) = scale { + let scale_b = broadcast(&scale, norm_shape, src_shape); + mul(&mut x, &scale_b); + } if let Some(bias) = bias { - let bias_b = broadcast(&bias, &norm_shape, src_shape); + let bias_b = broadcast(&bias, norm_shape, src_shape); add(&mut x, &bias_b); } @@ -146,52 +159,64 @@ where Ok(()) } -fn apply_rms_norm( +#[maybe_async] +async fn apply_rms_norm( Norm { input, scale, bias, eps, }: &Norm, - dst: Tensor, -) -> Result { - if input.dt() != scale.dt() { + dst: OpTensor, +) -> Result { + if let Some(scale) = scale + && input.dtype() != scale.dtype() + { return Err(InvariantError::DTypeMismatch { - expected: input.dt(), - actual: scale.dt(), + expected: input.dtype(), + actual: scale.dtype(), } .into()); } - if let Some(b) = bias { - if b.dt() != input.dt() { - return Err(InvariantError::DTypeMismatch { - expected: input.dt(), - actual: b.dt(), - } - .into()); + if let Some(b) = bias + && b.dtype() != input.dtype() + { + return Err(InvariantError::DTypeMismatch { + expected: input.dtype(), + actual: b.dtype(), } + .into()); } - match input.dt() { - DType::F32 => rms_norm::(input, scale, *eps, &dst)?, - DType::F16 => rms_norm::(input, scale, *eps, &dst)?, - DType::BF16 => rms_norm::(input, scale, *eps, &dst)?, + match input.dtype() { + DType::F32 => rms_norm::(input, scale.as_ref(), *eps, &dst).await?, + DType::F16 => rms_norm::(input, scale.as_ref(), *eps, &dst).await?, + DType::BF16 => rms_norm::(input, scale.as_ref(), *eps, &dst).await?, _ => todo!(), }; Ok(dst) } -fn rms_norm(input: &Tensor, scale: &Tensor, eps: f32, dst: &Tensor) -> Result<(), OperationError> +#[maybe_async] +async fn rms_norm( + input: &OpTensor, + scale: Option<&OpTensor>, + eps: f32, + dst: &OpTensor, +) -> Result<(), OperationError> where T: TensorDType + Float + NumOps + for<'a> Sum<&'a T>, { let src_shape = input.shape(); - let rank = input.rank(); + let rank = input.dim(); let N = src_shape[rank - 1]; - let mut x = input.to_vec::()?; - let scale = scale.to_vec::()?; + let mut x = input.to_vec::().await?; + let scale = match scale { + Some(s) => Some(s.to_vec::().await?), + None => None, + }; let mut x2 = x.clone(); square(&mut x2); @@ -204,15 +229,18 @@ where broadcast_vector(&x2, &mut v); mul(&mut x, &v); - let scale_b = broadcast(&scale, &shape!(N), src_shape); - mul(&mut x, &scale_b); + if let Some(scale) = scale { + let scale_b = broadcast(&scale, (N,), src_shape); + mul(&mut x, &scale_b); + } cpu_store_result(dst, &x); Ok(()) } -fn apply_group_norm(_n: &GroupNorm, dst: Tensor) -> Result { +#[maybe_async] +async fn apply_group_norm(_n: &GroupNorm, dst: OpTensor) -> Result { //let result = norm(&b.src.to_vec::()?, b.src.shape(), b.to()); //cpu_store_result(&dst, &result); Ok(dst) @@ -220,8 +248,10 @@ fn apply_group_norm(_n: &GroupNorm, dst: Tensor) -> Result Result { + match self { + Reindex::Permute(p) => p.apply_cpu(dst).await, + Reindex::Slice(s) => s.apply_cpu(dst).await, + Reindex::Broadcast(b) => b.apply_cpu(dst).await, + Reindex::Flip(f) => f.apply_cpu(dst).await, + } + } +} + +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +impl CPUOperation for Permute { + #[maybe_async] + async fn apply_cpu(&self, dst: OpTensor) -> Result { + match dst.dtype() { + DType::F32 => apply_permute::(self, dst).await, + DType::BF16 => apply_permute::(self, dst).await, + DType::F16 => apply_permute::(self, dst).await, + DType::I32 => apply_permute::(self, dst).await, + DType::U32 => apply_permute::(self, dst).await, + _ => todo!(), + } + } +} + +#[maybe_async] +async fn apply_permute( + p: &Permute, + dst: OpTensor, +) -> Result { + let perm: [usize; 4] = p.promote().as_slice().try_into().unwrap(); + let Permute { src, dims: _ } = p; + let result = permute(&src.to_vec::().await?, src.shape(), dst.shape(), perm); + cpu_store_result(&dst, &result); + Ok(dst) +} + +// TODO: Optimize. +// This generic implementation is almost a direct copy from the gpu impl, +// and can definitely be way more performant. +fn permute, DstShape: Into>( + src: &[T], + src_shape: SrcShape, + dst_shape: DstShape, + perm: [usize; 4], +) -> Vec { + // We now know that these will always be len 4, same as gpu impl. + let src_shape = &Shape::promote(src_shape.into(), 4); + let dst_shape = &Shape::promote(dst_shape.into(), 4); + + let mut result = vec![T::zero(); src_shape.numel()]; + + let src_stride = &Stride::from(src_shape); + let dst_stride = &Stride::from(dst_shape); + + let src_stride: [usize; 4] = src_stride.into(); + let dst_stride: [usize; 4] = dst_stride.into(); + + (0..result.len()).for_each(|i| { + let dst_index = offset_to_ndindex(i, dst_stride); + let mut src_index = [0; 4]; + src_index[perm[0]] = dst_index[0]; + src_index[perm[1]] = dst_index[1]; + src_index[perm[2]] = dst_index[2]; + src_index[perm[3]] = dst_index[3]; + let src_offset = nd_index_to_offset(src_index, src_stride); + result[i] = src[src_offset] + }); + result +} + +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +impl CPUOperation for Slice { + #[maybe_async] + async fn apply_cpu(&self, dst: OpTensor) -> Result { + match dst.dtype() { + DType::F32 => apply_slice::(self, dst).await, + DType::BF16 => apply_slice::(self, dst).await, + DType::F16 => apply_slice::(self, dst).await, + DType::I32 => apply_slice::(self, dst).await, + DType::U32 => apply_slice::(self, dst).await, + _ => todo!(), + } + } +} + +#[maybe_async] +async fn apply_slice(s: &Slice, dst: OpTensor) -> Result { + let (start, stop): (Vec<_>, Vec<_>) = s.indices().iter().map(|r| (r.start, r.end)).unzip(); + let result = slice(&s.src.to_vec::().await?, s.src.stride(), &start, &stop); + + cpu_store_result(&dst, &result); + Ok(dst) +} + +pub(crate) fn slice( + src: &[T], + src_stride: &Stride, + start: &[usize], + stop: &[usize], +) -> Vec { + assert!(start.len() == stop.len()); + assert!(start.len() == src_stride.rank()); + start.iter().zip(stop.iter()).for_each(|(s, t)| { + assert!(s < t); + }); + + let dst_shape: Vec = stop.iter().zip(start.iter()).map(|(s, t)| s - t).collect(); + let dst_numel: usize = dst_shape.iter().product(); + + let mut dst = vec![T::zero(); dst_numel]; + + let mut dst_dots = vec![]; + for d in 0..dst_shape.len() { + dst_dots.push(dst_shape[d + 1..].iter().product::().max(1)); + } + + (0..dst.len()).for_each(|i| { + let mut src_index = 0; + let mut tmp = i; + for d in 0..dst_shape.len() { + let coord = tmp / dst_dots[d]; + tmp %= dst_dots[d]; + src_index += (coord + start[d]) * src_stride[d] as usize; + } + dst[i] = src[src_index]; + }); + + dst +} + +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +impl CPUOperation for Broadcast { + #[maybe_async] + async fn apply_cpu(&self, dst: OpTensor) -> Result { + match dst.dtype() { + DType::F32 => apply_broadcast::(self, dst).await, + DType::BF16 => apply_broadcast::(self, dst).await, + DType::F16 => apply_broadcast::(self, dst).await, + DType::I32 => apply_broadcast::(self, dst).await, + DType::U32 => apply_broadcast::(self, dst).await, + _ => todo!(), + } + } +} + +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +impl CPUOperation for Flip { + #[maybe_async] + async fn apply_cpu(&self, dst: OpTensor) -> Result { + match dst.dtype() { + DType::F32 => apply_flip::(self, dst).await, + DType::BF16 => apply_flip::(self, dst).await, + DType::F16 => apply_flip::(self, dst).await, + DType::I32 => apply_flip::(self, dst).await, + DType::U32 => apply_flip::(self, dst).await, + _ => todo!(), + } + } +} + +#[maybe_async] +async fn apply_flip(f: &Flip, dst: OpTensor) -> Result { + let result = flip_cpu::(&f.src.to_vec::().await?, f.src.shape(), &f.dims); + super::utils::cpu_store_result(&dst, &result); + Ok(dst) +} + +fn flip_cpu>( + src: &[T], + src_shape: SrcShape, + dims: &[usize], +) -> Vec { + let src_shape = &Shape::promote(src_shape.into(), 4); + let src_stride = &Stride::from(src_shape); + + let src_shape_arr: [usize; 4] = src_shape.try_into().unwrap(); + let src_stride_arr: [usize; 4] = src_stride.into(); + + let dst_numel = src_shape.numel(); + let mut result = vec![T::zero(); dst_numel]; + + // Build promoted flip mask + let pad_len = 0; + let mut mask = [false; 4]; + for &d in dims.iter() { + let pd = d + pad_len; + if pd < 4 { + mask[pd] = true; + } + } + + // dst_shape == src_shape for flip + let dst_stride_arr = src_stride_arr; + + (0..dst_numel).for_each(|i| { + let dst_index = offset_to_ndindex(i, dst_stride_arr); + let mut src_index = [0usize; 4]; + for ax in 0..4 { + src_index[ax] = if mask[ax] { + src_shape_arr[ax] - 1 - dst_index[ax] + } else { + dst_index[ax] + }; + } + let src_offset = nd_index_to_offset(src_index, src_stride_arr); + result[i] = src[src_offset]; + }); + + result +} + +#[maybe_async] +async fn apply_broadcast( + b: &Broadcast, + dst: OpTensor, +) -> Result { + let result = broadcast(&b.src.to_vec::().await?, b.src.shape(), b.to()); + cpu_store_result(&dst, &result); + Ok(dst) +} + +pub(crate) fn broadcast_vector(src: &[T], dst: &mut [T]) { + let chunk_size = dst.len() / src.len(); + + (0..dst.len()) + .step_by(chunk_size) + .enumerate() + .for_each(|(i, chunk)| { + dst[chunk..chunk + chunk_size].fill(src[i]); + }); +} + +pub(crate) fn broadcast, DstShape: Into>( + src: &[T], + src_shape: SrcShape, + dst_shape: DstShape, +) -> Vec { + let src_shape = src_shape.into(); + let dst_shape = dst_shape.into(); + let mut result = vec![T::zero(); dst_shape.numel()]; + + if src_shape.is_scalar() { + // Life is simple + result.fill(src[0]); + } else if src_shape.is_vector() { + // If from is a vector and the first dimension is the broadcasting dimension + if src_shape[0] > 1 && src_shape[0] == dst_shape[0] { + broadcast_vector(src, &mut result) + } else { + generic_broadcast(src, &mut result, src_shape, dst_shape) + } + } else { + generic_broadcast(src, &mut result, src_shape, dst_shape) + } + + result +} + +// TODO: Optimize. +// This generic implementation is almost a direct copy from the gpu impl, +// and can definitely be way more performant. +fn generic_broadcast, DstShape: Into>( + src: &[T], + result: &mut [T], + src_shape: SrcShape, + dst_shape: DstShape, +) { + // We now know that these will always be len 4, same as gpu impl. + let src_shape = &Shape::promote(src_shape.into(), 4); + let dst_shape = &Shape::promote(dst_shape.into(), 4); + + let src_stride = &Stride::from(src_shape); + let dst_stride = &Stride::from(dst_shape); + + let src_shape: [usize; 4] = src_shape.try_into().unwrap(); + let src_stride: [usize; 4] = src_stride.into(); + let dst_stride: [usize; 4] = dst_stride.into(); + + fn select(a: [usize; 4], b: [usize; 4], t: [bool; 4]) -> [usize; 4] { + let mut result = [0; 4]; + result[0] = if t[0] { a[0] } else { b[0] }; + result[1] = if t[1] { a[1] } else { b[1] }; + result[2] = if t[2] { a[2] } else { b[2] }; + result[3] = if t[3] { a[3] } else { b[3] }; + result + } + + let shape_onedim_lookup: [bool; 4] = [ + src_shape[0] != 1, + src_shape[1] != 1, + src_shape[2] != 1, + src_shape[3] != 1, + ]; + (0..result.len()).for_each(|i| { + let dst_index = offset_to_ndindex(i, dst_stride); + let src_index = select(dst_index, [0; 4], shape_onedim_lookup); + let src_offset = nd_index_to_offset(src_index, src_stride); + result[i] = src[src_offset] + }); +} + +#[inline] +fn offset_to_ndindex(offset: usize, stride: [usize; 4]) -> [usize; 4] { + let mut indices = [0; 4]; + let mut remaining = offset; + + let idx = remaining / stride[0]; + indices[0] = idx; + remaining -= idx * stride[0]; + + let idx = remaining / stride[1]; + indices[1] = idx; + remaining -= idx * stride[1]; + + let idx = remaining / stride[2]; + indices[2] = idx; + remaining -= idx * stride[2]; + + indices[3] = remaining; + indices +} + +#[inline] +fn nd_index_to_offset(ndindex: [usize; 4], stride: [usize; 4]) -> usize { + ndindex[0] * stride[0] + + ndindex[1] * stride[1] + + ndindex[2] * stride[2] + + ndindex[3] * stride[3] +} diff --git a/crates/ratchet-core/src/cpu/rope.rs b/crates/piston-core/src/cpu/rope.rs similarity index 84% rename from crates/ratchet-core/src/cpu/rope.rs rename to crates/piston-core/src/cpu/rope.rs index a1d407f0..5642cd6a 100644 --- a/crates/ratchet-core/src/cpu/rope.rs +++ b/crates/piston-core/src/cpu/rope.rs @@ -1,16 +1,18 @@ use crate::{ - concat, + DType, OpTensor, OperationError, RoPE, Shape, Stride, concat, cpu::{cpu_store_result, gemm::gemm, reindex::slice}, - shape, DType, OperationError, RoPE, Shape, Strides, Tensor, + shape, }; +use maybe_async::maybe_async; -pub fn cpu_rope(op: RoPE, dst: Tensor) -> Result { - match op.input().dt() { +#[maybe_async] +pub async fn cpu_rope(op: RoPE, dst: OpTensor) -> Result { + match op.input().dtype() { DType::F32 => { let dim = op.dim(); let base = op.base(); let offset = op.offset(); - let src = op.input().to_vec::()?; + let src = op.input().to_vec::().await?; let result = rope(src, op.input().shape(), dim, base, offset)?; cpu_store_result(&dst, &result) } @@ -39,18 +41,18 @@ fn compute_theta( .collect::>(); let p_shape = shape!(seq_len, 1); - let p_strides = Strides::from(&p_shape); + let p_stride = Stride::from(&p_shape); let i_shape = shape!(1, half_dim); - let i_strides = Strides::from(&i_shape); - let dst_strides = Strides::from(&shape!(seq_len, half_dim)); + let i_stride = Stride::from(&i_shape); + let dst_stride = Stride::from(&shape!(seq_len, half_dim)); let theta = gemm( &positions, &p_shape, - &p_strides, + &p_stride, &inv_freqs, &i_shape, - &i_strides, - &dst_strides, + &i_stride, + &dst_stride, 1, seq_len, half_dim, @@ -72,16 +74,16 @@ fn rope( let half_dim = dim / 2; let theta = compute_theta(dim, seq_len, base, offset)?; let (sin, cos): (Vec, Vec) = theta.iter().map(|i| i.sin_cos()).unzip(); - let src_strides = Strides::from(shape); + let src_stride = Stride::from(shape); let x1 = slice( &src, - &src_strides, + &src_stride, &[0, 0, 0, 0], &[batches, num_heads, seq_len, half_dim], ); let x2 = slice( &src, - &src_strides, + &src_stride, &[0, 0, 0, half_dim], &[batches, num_heads, seq_len, dim], ); @@ -129,7 +131,7 @@ fn rope( if dim < shape[3] { let r3 = slice( &src, - &src_strides, + &src_stride, &[0, 0, 0, dim], &[batches, num_heads, seq_len, head_dim], ); diff --git a/crates/piston-core/src/cpu/softmax.rs b/crates/piston-core/src/cpu/softmax.rs new file mode 100644 index 00000000..524bf29f --- /dev/null +++ b/crates/piston-core/src/cpu/softmax.rs @@ -0,0 +1,47 @@ +use crate::cpu::utils::cpu_store_result; +use crate::{CPUOperation, DType, OpTensor, OperationError, Softmax, TensorDType}; +use half::{bf16, f16}; +use maybe_async::maybe_async; +use num::Float; +use num_traits::NumAssignOps; + +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +impl CPUOperation for Softmax { + #[maybe_async] + async fn apply_cpu(&self, dst: OpTensor) -> Result { + let Softmax { input, dim } = self; + match input.dtype() { + DType::F32 => softmax::(input, *dim, &dst).await?, + DType::F16 => softmax::(input, *dim, &dst).await?, + DType::BF16 => softmax::(input, *dim, &dst).await?, + _ => todo!(), + } + + Ok(dst) + } +} + +#[maybe_async] +async fn softmax(input: &OpTensor, dim: usize, dst: &OpTensor) -> Result<(), OperationError> +where + T: TensorDType + Float + NumAssignOps, +{ + let src_shape = input.shape(); + let mut input = input.to_vec::().await?; + let N = src_shape[dim]; + input.chunks_mut(N).for_each(|chunk| { + let mut sum = T::zero(); + for item in chunk.iter_mut().take(N) { + *item = item.exp(); + sum += *item; + } + for item in chunk.iter_mut().take(N) { + *item /= sum; + } + }); + + cpu_store_result(dst, &input); + + Ok(()) +} diff --git a/crates/ratchet-core/src/cpu/unary.rs b/crates/piston-core/src/cpu/unary.rs similarity index 67% rename from crates/ratchet-core/src/cpu/unary.rs rename to crates/piston-core/src/cpu/unary.rs index 54271b30..adb1f908 100644 --- a/crates/ratchet-core/src/cpu/unary.rs +++ b/crates/piston-core/src/cpu/unary.rs @@ -1,7 +1,8 @@ use crate::cpu::cpu_store_result; -use crate::{CPUOperation, DType, OperationError, Tensor, TensorDType, Unary, UnaryOp}; +use crate::{CPUOperation, DType, OpTensor, OperationError, TensorDType, Unary, UnaryOp}; use core::marker::PhantomData; use half::{bf16, f16}; +use maybe_async::maybe_async; use num_traits::Float; #[inline] @@ -24,12 +25,13 @@ pub(crate) fn unary_map_inplace(src: &mut [T], f: fn(T) -> T) { } #[inline] -pub(crate) fn unary_apply_fn( - input: &Tensor, - dst: &Tensor, +#[maybe_async] +pub(crate) async fn unary_apply_fn( + input: &OpTensor, + dst: &OpTensor, f: fn(T) -> U, ) -> Result<(), OperationError> { - let input = input.to_vec::()?; + let input = input.to_vec::().await?; let mut result = vec![U::zero(); dst.shape().numel()]; unary_apply_fn_helper(&input, &mut result, f); cpu_store_result(dst, &result); @@ -70,26 +72,30 @@ macro_rules! impl_unary_ops { * x * ($conv(1.0) / ($conv(1.0) + (-x).exp()))); - fn apply(op: &Unary, dst: Tensor) -> Result { + #[maybe_async] + async fn apply(op: &Unary, dst: OpTensor) -> Result { match op.op() { - UnaryOp::Gelu => Self::gelu(op.input(), dst), - UnaryOp::Tanh => Self::tanh(op.input(), dst), - UnaryOp::Exp => Self::exp(op.input(), dst), - UnaryOp::Log => Self::log(op.input(), dst), - UnaryOp::Sin => Self::sin(op.input(), dst), - UnaryOp::Cos => Self::cos(op.input(), dst), - UnaryOp::Abs => Self::abs(op.input(), dst), - UnaryOp::Square => Self::square(op.input(), dst), - UnaryOp::Sqrt => Self::sqrt(op.input(), dst), - UnaryOp::Relu => Self::relu(op.input(), dst), - UnaryOp::Relu2 => Self::relu2(op.input(), dst), - UnaryOp::Floor => Self::floor(op.input(), dst), - UnaryOp::Ceil => Self::ceil(op.input(), dst), - UnaryOp::Neg => Self::neg(op.input(), dst), - UnaryOp::Reciprocal => Self::reciprocal(op.input(), dst), - UnaryOp::Silu => Self::silu(op.input(), dst), - UnaryOp::Sigmoid => Self::sigmoid(op.input(), dst), - UnaryOp::Swiglu => Self::swiglu(op.input(), dst), + UnaryOp::Gelu => Self::gelu(op.input(), dst).await, + UnaryOp::Tanh => Self::tanh(op.input(), dst).await, + UnaryOp::Exp => Self::exp(op.input(), dst).await, + UnaryOp::Log => Self::log(op.input(), dst).await, + UnaryOp::Sin => Self::sin(op.input(), dst).await, + UnaryOp::Cos => Self::cos(op.input(), dst).await, + UnaryOp::Abs => Self::abs(op.input(), dst).await, + UnaryOp::Square => Self::square(op.input(), dst).await, + UnaryOp::Sqrt => Self::sqrt(op.input(), dst).await, + UnaryOp::Relu => Self::relu(op.input(), dst).await, + UnaryOp::Relu2 => Self::relu2(op.input(), dst).await, + UnaryOp::Floor => Self::floor(op.input(), dst).await, + UnaryOp::Ceil => Self::ceil(op.input(), dst).await, + UnaryOp::Neg => Self::neg(op.input(), dst).await, + UnaryOp::Reciprocal => Self::reciprocal(op.input(), dst).await, + UnaryOp::Silu => Self::silu(op.input(), dst).await, + UnaryOp::Sigmoid => Self::sigmoid(op.input(), dst).await, + UnaryOp::Swiglu => Self::swiglu(op.input(), dst).await, + UnaryOp::LogicalNot => todo!("logical_not"), + UnaryOp::IsNan => todo!("isnan"), + UnaryOp::IsInf => todo!("isinf"), } } } @@ -98,19 +104,23 @@ macro_rules! impl_unary_ops { macro_rules! impl_cpu_unary_op { ($method_name:ident, $op:expr) => { - fn $method_name(input: &Tensor, dst: Tensor) -> Result { - unary_apply_fn(input, &dst, $op)?; + #[maybe_async] + async fn $method_name(input: &OpTensor, dst: OpTensor) -> Result { + unary_apply_fn(input, &dst, $op).await?; Ok(dst) } }; } +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] impl CPUOperation for Unary { - fn apply_cpu(&self, dst: Tensor) -> Result { - match dst.dt() { - DType::F32 => UnaryOps::::apply(self, dst), - DType::F16 => UnaryOps::::apply(self, dst), - DType::BF16 => UnaryOps::::apply(self, dst), + #[maybe_async] + async fn apply_cpu(&self, dst: OpTensor) -> Result { + match dst.dtype() { + DType::F32 => UnaryOps::::apply(self, dst).await, + DType::F16 => UnaryOps::::apply(self, dst).await, + DType::BF16 => UnaryOps::::apply(self, dst).await, _ => todo!(), } } @@ -118,7 +128,7 @@ impl CPUOperation for Unary { macro_rules! impl_cpu_unary { ($dtype:ident) => { - impl_cpu_unary!($dtype, |x| x); + impl_cpu_unary!($dtype, |x: $dtype| -> $dtype { x }); }; ($dtype:ident, $conv:expr) => { impl_unary_ops!($dtype, $conv); diff --git a/crates/ratchet-core/src/cpu/utils.rs b/crates/piston-core/src/cpu/utils.rs similarity index 73% rename from crates/ratchet-core/src/cpu/utils.rs rename to crates/piston-core/src/cpu/utils.rs index e3874291..c5d45d65 100644 --- a/crates/ratchet-core/src/cpu/utils.rs +++ b/crates/piston-core/src/cpu/utils.rs @@ -1,8 +1,8 @@ -use crate::{CPUBuffer, Shape, Storage, Strides, Tensor}; +use crate::{CPUBuffer, OpTensor, Shape, Storage, Stride}; use bytemuck::NoUninit; use std::ops::Range; -pub fn cpu_store_result(dst: &Tensor, data: &[T]) { +pub fn cpu_store_result(dst: &OpTensor, data: &[T]) { dst.update_storage(Storage::CPU(CPUBuffer::from_slice(data, dst.shape()))); } @@ -13,26 +13,26 @@ pub enum TensorIterator<'a> { } impl<'a> TensorIterator<'a> { - pub fn new(shape: &'a Shape, strides: &'a Strides, offset: usize) -> Self { + pub fn new(shape: &'a Shape, stride: &'a Stride, offset: usize) -> Self { let mut block_size: usize = 1; let mut contiguous_dims: usize = 0; - for (&stride, &dim) in strides.iter().zip(shape.iter()).rev() { + for (&stride, &dim) in stride.iter().zip(shape.iter()).rev() { if stride as usize != block_size { break; } - block_size *= dim as usize; + block_size *= dim; contiguous_dims += 1; } - let index_dims = shape.rank() - contiguous_dims; + let index_dims = shape.dim() - contiguous_dims; if index_dims == 0 { Self::Contiguous(offset..block_size) } else { - Self::Strided(StridedIterator::new(&shape, &strides, offset, block_size)) + Self::Strided(StridedIterator::new(shape, stride, offset, block_size)) } } } -impl<'a> Iterator for TensorIterator<'a> { +impl Iterator for TensorIterator<'_> { type Item = usize; fn next(&mut self) -> Option { @@ -46,7 +46,7 @@ impl<'a> Iterator for TensorIterator<'a> { #[derive(Clone)] pub struct StridedIterator<'a> { shape: &'a [usize], - strides: &'a [isize], + stride: &'a [isize], next_index: Option, multi_index: Vec, block_size: usize, @@ -56,13 +56,13 @@ pub struct StridedIterator<'a> { impl<'a> StridedIterator<'a> { pub fn new( shape: &'a [usize], - strides: &'a [isize], + stride: &'a [isize], start_offset: usize, block_len: usize, ) -> Self { Self { shape, - strides, + stride, next_index: if shape.iter().product::() == 0 { None } else { @@ -75,14 +75,11 @@ impl<'a> StridedIterator<'a> { } } -impl<'a> Iterator for StridedIterator<'a> { +impl Iterator for StridedIterator<'_> { type Item = usize; fn next(&mut self) -> Option { - let storage_index = match self.next_index { - None => return None, - Some(storage_index) => storage_index, - }; + let storage_index = self.next_index?; if self.block_size > 1 { if self.block_step < self.block_size { @@ -99,7 +96,7 @@ impl<'a> Iterator for StridedIterator<'a> { .multi_index .iter_mut() .zip(self.shape.iter()) - .zip(self.strides.iter()) + .zip(self.stride.iter()) .rev() { let next_i = *multi_i + 1; @@ -122,15 +119,15 @@ impl<'a> Iterator for StridedIterator<'a> { } } -impl<'a> From<(&'a Shape, &'a Strides)> for StridedIterator<'a> { - fn from((shape, strides): (&'a Shape, &'a Strides)) -> Self { - StridedIterator::new(shape.as_slice(), strides.as_slice(), 0, 1) +impl<'a> From<(&'a Shape, &'a Stride)> for StridedIterator<'a> { + fn from((shape, stride): (&'a Shape, &'a Stride)) -> Self { + StridedIterator::new(shape.as_slice(), stride.as_slice(), 0, 1) } } -impl<'a> From<(&'a Shape, &'a Strides, usize)> for StridedIterator<'a> { - fn from((shape, strides, offset): (&'a Shape, &'a Strides, usize)) -> Self { - StridedIterator::new(shape.as_slice(), strides.as_slice(), offset, 1) +impl<'a> From<(&'a Shape, &'a Stride, usize)> for StridedIterator<'a> { + fn from((shape, stride, offset): (&'a Shape, &'a Stride, usize)) -> Self { + StridedIterator::new(shape.as_slice(), stride.as_slice(), offset, 1) } } @@ -139,7 +136,7 @@ mod tests { use proptest::prelude::*; use test_strategy::proptest; - use crate::{shape, Shape, Strides}; + use crate::{Shape, Stride, shape}; use super::TensorIterator; @@ -165,10 +162,10 @@ mod tests { #[proptest(cases = 16)] fn test_tensor_iter_contiguous(prob: IterProblem) { let shape = prob.shape; - let strides = Strides::from(&shape); + let stride = Stride::from(&shape); let offset = prob.offset; - let iter = TensorIterator::new(&shape, &strides, offset); + let iter = TensorIterator::new(&shape, &stride, offset); assert!(matches!(iter, TensorIterator::Contiguous(_))); match iter { @@ -180,12 +177,12 @@ mod tests { #[proptest(cases = 16)] fn test_tensor_iter_strided(prob: IterProblem) { let mut shape = prob.shape; - let mut strides = Strides::from(&shape); - strides.transpose(); + let mut stride = Stride::from(&shape); + stride.transpose(); shape.transpose(); let offset = prob.offset; - let iter = TensorIterator::new(&shape, &strides, offset); + let iter = TensorIterator::new(&shape, &stride, offset); assert!(matches!(iter, TensorIterator::Strided(_))); match iter { @@ -204,12 +201,12 @@ mod tests { #[test] fn test_tensor_iter_strided_sanity() { let mut shape = shape!(2, 4, 3); - let mut strides = Strides::from(&shape); - strides.transpose(); + let mut stride = Stride::from(&shape); + stride.transpose(); shape.transpose(); let offset = 2; - let iter = TensorIterator::new(&shape, &strides, offset); + let iter = TensorIterator::new(&shape, &stride, offset); let actual: Vec = iter.collect(); let expected = vec![ 2, 5, 8, 11, 3, 6, 9, 12, 4, 7, 10, 13, 14, 17, 20, 23, 15, 18, 21, 24, 16, 19, 22, 25, diff --git a/crates/ratchet-core/src/device.rs b/crates/piston-core/src/device.rs similarity index 94% rename from crates/ratchet-core/src/device.rs rename to crates/piston-core/src/device.rs index bffa1f67..63c51827 100644 --- a/crates/ratchet-core/src/device.rs +++ b/crates/piston-core/src/device.rs @@ -1,9 +1,9 @@ use crate::{ - gpu::{AllocatorError, PoolError, WgpuDevice}, DType, + gpu::{AllocatorError, PoolError, WgpuDevice}, }; use parking_lot::RwLock; -use rand::{rngs::StdRng, SeedableRng}; +use rand::{SeedableRng, rngs::StdRng}; use std::{ env, sync::{Arc, OnceLock}, @@ -45,10 +45,10 @@ pub enum Device { // Helper function to get a seeded RNG based on env var or entropy fn get_seeded_rng() -> StdRng { - if let Ok(seed_str) = env::var("RATCHET_SEED") { - if let Ok(seed) = seed_str.parse::() { - return StdRng::seed_from_u64(seed); - } + if let Ok(seed_str) = env::var("PISTON_SEED") + && let Ok(seed) = seed_str.parse::() + { + return StdRng::seed_from_u64(seed); } StdRng::from_entropy() } @@ -119,7 +119,7 @@ impl Device { } pub fn label(&self) -> String { - format!("{:?}", self) + format!("{self:?}") } pub fn try_gpu(&self) -> Result<&WgpuDevice, DeviceError> { diff --git a/crates/ratchet-core/src/dtype/blocks.rs b/crates/piston-core/src/dtype/blocks.rs similarity index 96% rename from crates/ratchet-core/src/dtype/blocks.rs rename to crates/piston-core/src/dtype/blocks.rs index ffd239b2..b99221c6 100644 --- a/crates/ratchet-core/src/dtype/blocks.rs +++ b/crates/piston-core/src/dtype/blocks.rs @@ -1,9 +1,9 @@ #![allow(non_camel_case_types)] -/// Ratchet memory layouts. +/// Piston memory layouts. /// /// We closely follow the memory layout of the original GGUF implementation, /// but often need 2 variants of each block type for devices that don't support f16. -use crate::{rvec, Align, BufferSegment, DType, RVec, TensorDType}; +use crate::{Align, BufferSegment, DType, RVec, TensorDType, rvec}; use derive_new::new; use half::f16; use num_traits::{AsPrimitive, Float, FromPrimitive, NumAssign}; @@ -86,7 +86,7 @@ macro_rules! impl_hash_q { ($type:ty) => { impl Hash for $type { fn hash(&self, state: &mut H) { - self.0 .0.hash(state); + self.0.0.hash(state); } } }; @@ -193,7 +193,7 @@ pub trait Quantized { const MASK: i32 = (1 << Self::LSHIFT) - 1; const RSHIFT: usize = Self::GROUP_SIZE - Self::LSHIFT; - fn dt() -> DType; + fn dtype() -> DType; } impl Quantized for Q8_0F { type FP = f32; @@ -201,7 +201,7 @@ impl Quantized for Q8_0F { const GROUP_SIZE: usize = 32; const SF: f32 = ((1 << 7) - 1) as f32; - fn dt() -> DType { + fn dtype() -> DType { DType::Q8_0F(Q8_0F::default()) } } @@ -211,7 +211,7 @@ impl Quantized for Q8_0H { const GROUP_SIZE: usize = 32; const SF: f16 = f16::from_f32_const(Q8_0F::SF); - fn dt() -> DType { + fn dtype() -> DType { DType::Q8_0H(Q8_0H::default()) } } @@ -221,7 +221,7 @@ impl Quantized for Q4_KF { const GROUP_SIZE: usize = 32; const SF: f32 = 7.0; - fn dt() -> DType { + fn dtype() -> DType { DType::Q4_KF(Q4_KF::default()) } } @@ -231,7 +231,7 @@ impl Quantized for Q4_KH { const GROUP_SIZE: usize = 32; const SF: f16 = f16::from_f32_const(7.0); - fn dt() -> DType { + fn dtype() -> DType { DType::Q4_KH(Q4_KH::default()) } } diff --git a/crates/ratchet-core/src/dtype/mod.rs b/crates/piston-core/src/dtype/mod.rs similarity index 56% rename from crates/ratchet-core/src/dtype/mod.rs rename to crates/piston-core/src/dtype/mod.rs index 97791a96..15f81917 100644 --- a/crates/ratchet-core/src/dtype/mod.rs +++ b/crates/piston-core/src/dtype/mod.rs @@ -91,12 +91,26 @@ impl DType { matches!(self, DType::Q4_KH(_) | DType::Q4_KF(_)) } + pub fn is_signed(self) -> bool { + matches!( + self, + DType::F16 + | DType::BF16 + | DType::F32 + | DType::I32 + | DType::Q8_0H(_) + | DType::Q8_0F(_) + | DType::Q4_KH(_) + | DType::Q4_KF(_) + ) + } + pub fn is_float(self) -> bool { matches!(self, DType::F16 | DType::BF16 | DType::F32) } /// Returns the activation dtype for the given quantized dtype. - pub fn activation_dt(&self) -> DType { + pub fn activation_dtype(&self) -> DType { match self { DType::Q8_0H(_) => DType::F16, DType::Q8_0F(_) => DType::F32, @@ -177,7 +191,7 @@ impl BufferSegment { pub trait TensorDType: Clone + std::fmt::Debug + PartialEq + 'static + num_traits::Zero + Send + Sync + bytemuck::Pod { - fn dt() -> DType; + fn dtype() -> DType; fn one() -> Self; } @@ -185,7 +199,7 @@ pub trait TensorDType: macro_rules! map_type { ($t:ty, $v:ident) => { impl TensorDType for $t { - fn dt() -> DType { + fn dtype() -> DType { DType::$v } @@ -199,7 +213,7 @@ macro_rules! map_type { macro_rules! map_half_type { ($t:ty, $v:ident) => { impl TensorDType for $t { - fn dt() -> DType { + fn dtype() -> DType { DType::$v } @@ -216,10 +230,159 @@ map_type!(u32, U32); map_half_type!(f16, F16); map_half_type!(bf16, BF16); +/** + * Promotes two data types to a common type according to a type hierarchy. + * The promotion hierarchy is: uint32 < int32 < float16 < float32 + * + * @param dtype1 - First data type + * @param dtype2 - Second data type + * @returns The promoted data type that can represent both inputs + */ +pub fn promote_types(dtype1: DType, dtype2: DType) -> anyhow::Result { + // If types are the same, no promotion needed + if dtype1 == dtype2 { + return Ok(dtype1); + } + + // Special-case handling when U32 is involved + match (dtype1, dtype2) { + (a @ DType::U32, b) | (b, a @ DType::U32) => { + return if b.is_float() { + Ok(b) + } else if a.is_float() { + Ok(a) + } else { + Err(anyhow::anyhow!( + "Promotion for uint16, uint32, uint64 types is not supported, attempted to promote {} and {}", + a, + b + )) + }; + } + _ => {} + } + + // Type hierarchy: u32 < i32 < f16 < f32; others fall back to highest precedence + fn precedence(dtype: &DType) -> u8 { + match *dtype { + DType::U32 => 0, + DType::I32 => 1, + DType::F16 => 2, + DType::F32 => 3, + _ => 4, + } + } + + let p1 = precedence(&dtype1); + let p2 = precedence(&dtype2); + Ok(if p1 >= p2 { dtype1 } else { dtype2 }) +} + +pub fn promoted_cast< + T1: Into, + T2: Into + From, + T3: TensorTypeOrScalar + From>, +>( + tensor1: T1, + tensor2: T3, +) -> anyhow::Result<(OpTensor, T3)> { + let tensor1 = tensor1.into(); + // let tensor2 = tensor2.map_tensor(|t| t.into()).tensor_or_scalar()?; + + let dtype1 = tensor1.dtype(); + let dtype2 = match tensor2.map_tensor(|t| t.into()).tensor_or_scalar()? { + TensorTypeOrScalarEnum::Tensor(ref t) => t.dtype(), + TensorTypeOrScalarEnum::Scalar(_) => DType::F32, + }; + + let promotedType = promote_types(dtype1, dtype2)?; + + // Cast each tensor only if its type is different from the promoted type + let promoted_tensor1 = if dtype1 == promotedType { + tensor1 + } else { + cast_kernel(tensor1, promotedType)? + }; + let promoted_tensor2 = if dtype2 == promotedType { + tensor2 + } else { + tensor2 + .map_tensor(|t| cast_kernel(t.into(), promotedType))? + .transpose()? + .map_tensor(|t| T2::from(t))? + .into() + }; + + Ok((promoted_tensor1, promoted_tensor2)) +} + +/// Promotes `tensor1` together with two additional `TensorTypeOrScalar` +/// arguments to a common dtype, casting as needed. +/// +/// Mirrors `promoted_cast` (binary) and specializes `promoted_cast_many` +/// for the common ternary case. Returns the potentially casted `tensor1` +/// and the two additional arguments, preserving their tensor-or-scalar form. +pub fn promoted_cast_ternary< + T1: Into, + T2: Into + From, + T3: TensorTypeOrScalar + From>, + T4: Into + From, + T5: TensorTypeOrScalar + From>, +>( + tensor1: T1, + tensor2: T3, + tensor3: T5, +) -> anyhow::Result<(OpTensor, T3, T5)> { + let tensor1: OpTensor = tensor1.into(); + + let dtype1 = tensor1.dtype(); + let dtype2 = match tensor2.map_tensor(|t| t.into()).tensor_or_scalar()? { + TensorTypeOrScalarEnum::Tensor(ref t) => t.dtype(), + TensorTypeOrScalarEnum::Scalar(_) => DType::F32, + }; + let dtype3 = match tensor3.map_tensor(|t| t.into()).tensor_or_scalar()? { + TensorTypeOrScalarEnum::Tensor(ref t) => t.dtype(), + TensorTypeOrScalarEnum::Scalar(_) => DType::F32, + }; + + let promoted = promote_types(promote_types(dtype1, dtype2)?, dtype3)?; + + let promoted_tensor1 = if dtype1 == promoted { + tensor1 + } else { + cast_kernel(tensor1, promoted)? + }; + + let promoted_tensor2 = if dtype2 == promoted { + tensor2 + } else { + tensor2 + .map_tensor(|t| cast_kernel(t.into(), promoted))? + .transpose()? + .map_tensor(|t| T2::from(t))? + .into() + }; + + let promoted_tensor3 = if dtype3 == promoted { + tensor3 + } else { + tensor3 + .map_tensor(|t| cast_kernel(t.into(), promoted))? + .transpose()? + .map_tensor(|t| T4::from(t))? + .into() + }; + + Ok((promoted_tensor1, promoted_tensor2, promoted_tensor3)) +} + #[cfg(test)] use proptest::prelude::*; -use crate::{rvec, Align, RVec, MIN_STORAGE_BUFFER_SIZE}; +use crate::{ + Align, MIN_STORAGE_BUFFER_SIZE, OpTensor, RVec, TensorTypeOrScalar, TensorTypeOrScalarEnum, + cast_kernel, rvec, +}; #[cfg(test)] impl Arbitrary for DType { diff --git a/crates/ratchet-core/src/enforcer.rs b/crates/piston-core/src/enforcer.rs similarity index 92% rename from crates/ratchet-core/src/enforcer.rs rename to crates/piston-core/src/enforcer.rs index 6c19223d..cc70b620 100644 --- a/crates/ratchet-core/src/enforcer.rs +++ b/crates/piston-core/src/enforcer.rs @@ -33,6 +33,8 @@ pub enum InvariantError { DuplicateDims, #[error("Broadcasting failed: {0:?}")] BroadcastingFailed(Vec), + #[error("The expanded size of the tensor must match the existing size.")] + InplaceBroadcast, #[error("Dim out of range {dim} in shape {shape:?}.")] DimOutOfRange { dim: usize, shape: Shape }, } diff --git a/crates/ratchet-core/src/executable.rs b/crates/piston-core/src/executable.rs similarity index 58% rename from crates/ratchet-core/src/executable.rs rename to crates/piston-core/src/executable.rs index 150c8eda..ab9a9032 100644 --- a/crates/ratchet-core/src/executable.rs +++ b/crates/piston-core/src/executable.rs @@ -1,21 +1,27 @@ use crate::gpu::{GpuUniform, PoolError, StaticResourcePoolAccessor, WgpuDevice}; -use crate::{Compiled, Storage}; +use crate::{ + Compiled, CompiledCopy, CompiledOp, ExportedTensorProfilingEntry, HashMap, PooledGPUBuffer, + StepLogConfig, Storage, TensorId, +}; + // #[cfg(feature = "debug")] (for tensor?) -use crate::Tensor; +use crate::OpTensor; +use crate::gpu::Profiler; #[cfg(feature = "debug")] -use crate::{CPUBuffer, DType, HashMap, TensorId}; +use crate::{DType, HashMap, TensorId}; #[cfg(feature = "debug")] +use crate::{DeviceStorage, KernelKey, RVec}; +use derive_new::new; use maybe_async::maybe_async; #[cfg(feature = "debug")] use parking_lot::RwLock; #[cfg(feature = "debug")] use slotmap::Key; +#[cfg(not(feature = "debug"))] +use std::collections::BTreeMap; +use std::iter::Peekable; #[cfg(feature = "debug")] use std::sync::Arc; - -#[cfg(feature = "debug")] -use crate::{wgpu_buffer_to_cpu_buffer, DeviceStorage, KernelKey, RVec}; -use derive_new::new; use wgpu::SubmissionIndex; #[cfg(feature = "debug")] @@ -36,6 +42,8 @@ pub struct Executable { storage: Option>>, pub(crate) steps: Vec, gpu_uniform: GpuUniform, + #[cfg(not(feature = "debug"))] + pub(crate) debug_list: Option>, #[cfg(feature = "debug")] pub(crate) debug_list: Option>, #[cfg(feature = "debug")] @@ -51,83 +59,208 @@ pub enum ExecutionError { DebuggingError(&'static str), } +pub struct ExecutionResult { + pub profiling_entries: Option>, + pub gpu_bufs: Option>, +} + impl Executable { - #[cfg(not(feature = "gpu-profiling"))] - pub fn dispatch(&self, device: &WgpuDevice) -> Result { + // Helper function to set up pipeline and dispatch workgroups + #[inline] + fn compute_pass_inner( + compute_pass: &mut wgpu::ComputePass<'_>, + op: &CompiledOp, + pipeline_resources: &crate::StaticResourcePoolReadLockAccessor< + '_, + crate::ComputePipelineHandle, + wgpu::ComputePipeline, + >, + gpu_uniform: &GpuUniform, + ) -> Result<(), ExecutionError> { + compute_pass.set_pipeline(pipeline_resources.get(op.pipeline_handle())?); + for (group_index, bind_group) in op.storage_groups().iter().enumerate() { + compute_pass.set_bind_group(group_index as u32, Some(&**bind_group), &[]); + } + let uniform_group_index = op.storage_groups().len() as u32; + let uniform_group = gpu_uniform.bind_group(); + compute_pass.set_bind_group(uniform_group_index, Some(&**uniform_group), &[op.offset()]); + let [x_count, y_count, z_count] = op.workgroup_count().as_slice(); + compute_pass.dispatch_workgroups(x_count, y_count, z_count); + Ok(()) + } + + #[cfg(all(not(feature = "gpu-profiling"), not(feature = "debug")))] + #[maybe_async] + pub async fn dispatch( + &mut self, + device: &WgpuDevice, + step_log_config: Option<&StepLogConfig>, + ) -> Result<(SubmissionIndex, ExecutionResult), ExecutionError> { let pipeline_resources: crate::StaticResourcePoolReadLockAccessor< '_, crate::ComputePipelineHandle, wgpu::ComputePipeline, > = device.pipeline_resources(); + let mut encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + Some(device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None })); + + #[inline] + fn create_timestamp_writes<'a>( + profiler: &'a mut Option, + op: &CompiledOp, + ) -> Option> { + if let Some(profiler) = profiler { + let label = format!("{}_{}", op.kernel_key, op.workgroup_count()); + op.tensor_id + .map(|id| profiler.create_timestamp_queries(id, label.as_str())) + } else { + None + } + } - // Create a peekable iterator over the steps. - let mut steps_iter = self.steps.iter().peekable(); + let compute_step_count = self + .steps + .iter() + .filter(|step| matches!(step, Compiled::Compute(_))) + .count(); + + let grouped_iter = GroupedIter::new(self.steps.iter_mut()); + + let mut profiler = step_log_config.as_ref().and_then(|config| { + config + .profiling + .then(|| Profiler::new(device.clone(), compute_step_count as _)) + }); + + // let mut debug_steps = vec![]; + let mut gpu_bufs: Option> = None; + + for group in grouped_iter { + match group { + GroupedOp::Compute(mut ops) => { + let mut cpass = None; + + let ops_iter = ops.iter_mut().peekable(); + for op in ops_iter { + let timestamp_writes = create_timestamp_writes(&mut profiler, op); + + // Existing compute pass; shared + if let Some(compute_pass) = cpass.as_mut() { + Self::compute_pass_inner( + compute_pass, + op, + &pipeline_resources, + &self.gpu_uniform, + )?; + } else { + // Create a new encoder if one doesn't exist already + if encoder.is_none() { + encoder = Some(device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: None }, + )); + } - while let Some(step) = steps_iter.next() { - match step { - Compiled::Compute(op) => { - // Start a new compute pass for contiguous compute operations. - let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("ratchet inference pass"), - timestamp_writes: None, - }); - - // Process the current compute operation. - cpass.set_pipeline(pipeline_resources.get(op.pipeline_handle())?); - for (group_index, bind_group) in op.storage_groups().iter().enumerate() { - cpass.set_bind_group(group_index as u32, bind_group, &[]); - } - let uniform_group_index = op.storage_groups().len() as u32; - let uniform_group = self.gpu_uniform.bind_group(); - cpass.set_bind_group(uniform_group_index, uniform_group, &[op.offset()]); - let [x_count, y_count, z_count] = op.workgroup_count().as_slice(); - cpass.dispatch_workgroups(x_count, y_count, z_count); + let mut new_cpass = encoder + .as_mut() + .unwrap() + .begin_compute_pass(&wgpu::ComputePassDescriptor { + label: None, + timestamp_writes, + }) + .forget_lifetime(); - // Consume all subsequent contiguous compute operations. - while let Some(next_step) = steps_iter.peek() { - if let Compiled::Compute(_) = next_step { - // Consume this step. - let next_op = if let Compiled::Compute(op) = steps_iter.next().unwrap() - { - op - } else { - unreachable!() - }; - - cpass.set_pipeline(pipeline_resources.get(next_op.pipeline_handle())?); - for (group_index, bind_group) in - next_op.storage_groups().iter().enumerate() - { - cpass.set_bind_group(group_index as u32, bind_group, &[]); - } - let uniform_group_index = next_op.storage_groups().len() as u32; - let uniform_group = self.gpu_uniform.bind_group(); - cpass.set_bind_group( - uniform_group_index, - uniform_group, - &[next_op.offset()], + Self::compute_pass_inner( + &mut new_cpass, + op, + &pipeline_resources, + &self.gpu_uniform, + )?; + + cpass = Some(new_cpass); + } + + if profiler.is_some() { + // In profile mode, we need to create a new cpass for each op + cpass = None; + } + + if let Some(debug_buffer) = op.debug_buffer.take() { + // Cpass needs to be dropped before we submit the encoder + cpass = None; + + let result_t = self + .debug_list + .as_ref() + .expect("Debug list is not set") + .get(&op.tensor_id.expect("Tensor id is not set")) + .expect("Tensor not found in debug list"); + + let gpu_storage = result_t.storage(); + let result_buf = &gpu_storage + .as_ref() + .ok_or(ExecutionError::DebuggingError("Failed to get result buf."))? + .try_gpu() + .map_err(|_| { + ExecutionError::DebuggingError("Result buf is not on GPU.") + })? + .inner; + + encoder.as_mut().unwrap().copy_buffer_to_buffer( + result_buf, + 0, + &debug_buffer, + 0, + Some(result_t.num_bytes() as _), ); - let [x_count, y_count, z_count] = next_op.workgroup_count().as_slice(); - cpass.dispatch_workgroups(x_count, y_count, z_count); - } else { - break; + + gpu_bufs + .get_or_insert_default() + .insert(op.tensor_id.expect("Tensor id is not set"), debug_buffer); } } - // The compute pass is automatically ended when `cpass` goes out of scope. } - Compiled::Copy(op) => { + GroupedOp::Copy(op) => { + // Create a new encoder if one doesn't exist already + if encoder.is_none() { + encoder = Some(device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: None }, + )); + } + // Process the copy operation outside of a compute pass. let src = op.src().as_ref(); let dst = op.dst().as_ref(); let size = op.size(); - encoder.copy_buffer_to_buffer(src, 0, dst, 0, size); + encoder + .as_mut() + .unwrap() + .copy_buffer_to_buffer(src, 0, dst, 0, size); } } } - Ok(device.queue().submit(Some(encoder.finish()))) + if let Some(profiler) = profiler.as_mut() { + profiler.resolve(encoder.as_mut().unwrap()); + } + + let index = device + .queue() + .submit(Some(encoder.take().unwrap().finish())); + + let profiling_entries = if let Some(profiler) = profiler { + Some(profiler.read_timestamps().await) + } else { + None + }; + + Ok(( + index, + ExecutionResult { + profiling_entries, + gpu_bufs, + }, + )) } #[cfg(feature = "debug")] @@ -152,29 +285,26 @@ impl Executable { while let Some((step_index, step)) = steps_iter.next() { match step { Compiled::Compute(op) => { - // Group contiguous compute operations. let mut compute_group = vec![(step_index, op)]; while let Some(&(next_index, next_step)) = steps_iter.peek() { if let Compiled::Compute(next_op) = next_step { compute_group.push((next_index, next_op)); - steps_iter.next(); // Consume the op. + steps_iter.next(); } else { break; } } for &(step_index, op) in &compute_group { - // Create a single encoder for the entire compute-group. let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None, }); { - // Begin a compute pass for all the grouped compute operations. let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("ratchet inference pass"), + label: Some("piston inference pass"), timestamp_writes: None, }); log::debug!( @@ -191,20 +321,12 @@ impl Executable { .map(|e| e.handle.data()) .collect::>(), ); - cpass.set_pipeline(pipeline_resources.get(op.pipeline_handle())?); - for (group_index, bind_group) in op.storage_groups().iter().enumerate() - { - cpass.set_bind_group(group_index as u32, bind_group, &[]); - } - let uniform_group_index = op.storage_groups().len() as u32; - let uniform_group = self.gpu_uniform.bind_group(); - cpass.set_bind_group( - uniform_group_index, - uniform_group, - &[op.offset()], - ); - let [x_count, y_count, z_count] = op.workgroup_count().as_slice(); - cpass.dispatch_workgroups(x_count, y_count, z_count); + Self::compute_pass_inner( + &mut cpass, + op, + &pipeline_resources, + &self.gpu_uniform, + )?; } // Process the debugging copy operations associated with each compute op. @@ -293,10 +415,10 @@ impl Executable { } }) { let d = device.clone(); - let dt = debug_list[si].dtype; + let dtype = debug_list[si].dtype; let (id, debug_buffer) = step.debug_buffer.clone().unwrap(); let debug_input_buffers = step.debug_input_buffers.clone().unwrap(); - let alignment = dt.size_of(); + let alignment = dtype.size_of(); let kernel_key = step.kernel_key.clone(); #[allow(clippy::too_many_arguments)] @@ -309,7 +431,7 @@ impl Executable { kernel_key: KernelKey, alignment: usize, id: TensorId, - dt: DType, + dtype: DType, output_cpu_bufs: &Arc>>, ) { let cpu_buf = @@ -324,7 +446,7 @@ impl Executable { si, id, kernel_key, - cpu_buf.dump(dt, (cpu_buf.n_bytes() / 4) <= 8) + cpu_buf.dump(dtype, (cpu_buf.n_bytes() / 4) <= 8) ); for (i, (id, cpu_buf)) in input_bufs.iter().enumerate() { @@ -332,7 +454,7 @@ impl Executable { "\x1b[32;1minput {} ({:?})\x1b[0m: {:?}\n\n", i, id, - cpu_buf.dump(dt, (cpu_buf.n_bytes() / 4) <= 8) + cpu_buf.dump(dtype, (cpu_buf.n_bytes() / 4) <= 8) )); } @@ -351,7 +473,7 @@ impl Executable { kernel_key, alignment, id, - dt, + dtype, self.cpu_bufs.as_ref().unwrap(), )); } @@ -365,7 +487,7 @@ impl Executable { kernel_key, alignment, id, - dt, + dtype, self.cpu_bufs.as_ref().unwrap(), ); } @@ -393,46 +515,42 @@ impl Executable { while let Some(step) = steps_iter.next() { match step { Compiled::Compute(op) => { - // Group all contiguous compute operations. let mut compute_group = vec![op]; while let Some(next_step) = steps_iter.peek() { if let Compiled::Compute(next_op) = next_step { compute_group.push(next_op); - steps_iter.next(); // consume the op + // consume the op + steps_iter.next(); } else { break; } } - // Use the first op's label for the entire group. - let first_op = compute_group.first().unwrap(); - let group_label = format!( - "grouped: {} ({} ops)", - first_op.kernel_key, - compute_group.len() - ); - let timestamp_writes = - Some(profiler.create_timestamp_queries(0, group_label.as_str())); - - // Begin one compute pass for the grouped compute operations. - let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: Some("ratchet inference pass"), - timestamp_writes, - }); for op in compute_group { + let label = + format!("{}_{}", op.kernel_key, op.workgroup_count().to_string()); + let timestamp_writes = + Some(profiler.create_timestamp_queries(0, label.as_str())); + let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: None, + timestamp_writes, + }); cpass.set_pipeline(pipeline_resources.get(op.pipeline_handle())?); + for (group_index, bind_group) in op.storage_groups().iter().enumerate() { cpass.set_bind_group(group_index as u32, bind_group, &[]); } + let uniform_group_index = op.storage_groups().len() as u32; let uniform_group = self.gpu_uniform.bind_group(); cpass.set_bind_group(uniform_group_index, uniform_group, &[op.offset()]); + let [x_count, y_count, z_count] = op.workgroup_count().as_slice(); cpass.dispatch_workgroups(x_count, y_count, z_count); } } Compiled::Copy(op) => { - // Process copy operations individually. + // TODO(vinhowe): Decide on the best way to profile these? let mut encoder_copy = device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); let src = op.src().as_ref(); @@ -446,7 +564,7 @@ impl Executable { profiler.resolve(&mut encoder); let index = device.queue().submit(Some(encoder.finish())); - profiler.read_timestamps(true); + profiler.print_timestamps(true); Ok(index) } @@ -454,7 +572,7 @@ impl Executable { self.storage = Some(storage); } - pub fn with_tensors(self, tensors: &Vec<&Tensor>) -> anyhow::Result { + pub fn with_tensors(self, tensors: &Vec<&OpTensor>) -> anyhow::Result { assert_eq!( tensors.len(), self.storage.as_ref().unwrap().len(), @@ -472,10 +590,10 @@ impl Executable { "Storage: {:?}", storage.as_ref().map(|s| s.plot_fmt().to_string()) ); - if let Some(storage) = storage { - if !tensor.resolved() { - tensor.update_storage(storage.clone()); - } + if let Some(storage) = storage + && !tensor.resolved() + { + tensor.update_storage(storage.clone()); } } @@ -483,6 +601,7 @@ impl Executable { storage: self.storage, steps: self.steps, gpu_uniform: self.gpu_uniform, + debug_list: None, #[cfg(feature = "debug")] debug_list: None, #[cfg(feature = "debug")] @@ -490,3 +609,50 @@ impl Executable { }) } } + +enum GroupedOp<'a> { + Compute(Vec<&'a mut CompiledOp>), + Copy(&'a mut CompiledCopy), +} + +struct GroupedIter<'a, I> +where + I: Iterator, +{ + iter: Peekable, +} + +impl<'a, I> GroupedIter<'a, I> +where + I: Iterator, +{ + fn new(iter: I) -> Self { + Self { + iter: iter.peekable(), + } + } +} + +impl<'a, I> Iterator for GroupedIter<'a, I> +where + I: Iterator, +{ + type Item = GroupedOp<'a>; + + fn next(&mut self) -> Option { + // Get the first item of the next group. + let first = self.iter.next()?; + match first { + Compiled::Compute(op) => { + let mut ops = vec![op]; + while let Some(Compiled::Compute(_)) = self.iter.peek() { + if let Some(Compiled::Compute(next_op)) = self.iter.next() { + ops.push(next_op); + } + } + Some(GroupedOp::Compute(ops)) + } + Compiled::Copy(op) => Some(GroupedOp::Copy(op)), + } + } +} diff --git a/crates/ratchet-core/src/gpu/align.rs b/crates/piston-core/src/gpu/align.rs similarity index 92% rename from crates/ratchet-core/src/gpu/align.rs rename to crates/piston-core/src/gpu/align.rs index 98d345c6..2c26b905 100644 --- a/crates/ratchet-core/src/gpu/align.rs +++ b/crates/piston-core/src/gpu/align.rs @@ -1,12 +1,12 @@ //!WebGPU is very specific about buffer alignment. -//!Since in Ratchet, any buffer may be copied back from GPU -> CPU, all buffers have a size +//!Since in Piston, any buffer may be copied back from GPU -> CPU, all buffers have a size //!that is a multiple of COPY_BUFFER_ALIGNMENT (4 bytes). //! //!However, WebGPU also has more stringent alignment for storage buffer offsets. //!This is controlled by `min_storage_buffer_offset_alignment` in wgpu::Limits. //!This defaults to 256 //! -//!For quantized data types in Ratchet, each "segment" of quantized block (mins, scales, qs, zero +//!For quantized data types in Piston, each "segment" of quantized block (mins, scales, qs, zero //!point etc.) is extracted and put into separate segments. Thus, these segments must be aligned to //!256. diff --git a/crates/ratchet-core/src/gpu/buffer_allocator/allocator.rs b/crates/piston-core/src/gpu/buffer_allocator/allocator.rs similarity index 84% rename from crates/ratchet-core/src/gpu/buffer_allocator/allocator.rs rename to crates/piston-core/src/gpu/buffer_allocator/allocator.rs index 907c6ea2..43160da9 100644 --- a/crates/ratchet-core/src/gpu/buffer_allocator/allocator.rs +++ b/crates/piston-core/src/gpu/buffer_allocator/allocator.rs @@ -1,10 +1,11 @@ +#![allow(clippy::doc_overindented_list_items)] use super::TensorUsageRecord; use crate::{ + DeviceError, GpuCompileKey, OpTensor, TensorId, gpu::{ BufferDescriptor, BufferPool, BufferUsagesExt, CpuUniform, GpuBufferHandle, - PooledGPUBuffer, TensorUsageRecords, WgpuDevice, UNIFORM_ALIGN, + PooledGPUBuffer, TensorUsageRecords, UNIFORM_ALIGN, WgpuDevice, }, - DeviceError, GpuCompileKey, Tensor, TensorId, }; use crate::{HashMap, LazyOp}; use parking_lot::RwLock; @@ -20,6 +21,7 @@ pub enum AllocatorError { pub struct BufferAllocator { pool: RwLock, + vram_limit: RwLock>, } impl Default for BufferAllocator { @@ -32,9 +34,14 @@ impl BufferAllocator { pub fn new() -> Self { Self { pool: BufferPool::new().into(), + vram_limit: RwLock::new(None), } } + pub fn set_vram_limit(&self, vram_limit: Option) { + *self.vram_limit.write() = vram_limit; + } + pub fn begin_pass(&self, pass_index: u64) { self.pool.write().begin_pass(pass_index); } @@ -49,7 +56,9 @@ impl BufferAllocator { device: &WgpuDevice, immediate: bool, ) -> PooledGPUBuffer { - self.pool.write().get_or_create(desc, device, immediate) + self.pool + .write() + .get_or_create(desc, device, immediate, *self.vram_limit.read()) } pub fn create_buffer_init( @@ -67,7 +76,10 @@ impl BufferAllocator { contents }; - let buf = self.pool.write().get_or_create(desc, device, false); + let buf = self + .pool + .write() + .get_or_create(desc, device, false, *self.vram_limit.read()); let mut buffer_view = device.queue().write_buffer_with( &buf.inner, 0, @@ -93,7 +105,10 @@ impl BufferAllocator { false, ); - let resource = self.pool.write().get_or_create(&desc, device, false); + let resource = + self.pool + .write() + .get_or_create(&desc, device, false, *self.vram_limit.read()); let mut buffer_view = device.queue().write_buffer_with( &resource.inner, 0, @@ -133,7 +148,7 @@ impl BufferAllocator { } } - if std::env::var("RATCHET_DEBUG").is_ok() { + if std::env::var("PISTON_DEBUG").is_ok() { return self.create_buffer(&descriptor, device, true); } @@ -154,12 +169,12 @@ impl BufferAllocator { /// 3. If our PARENT (i.e the buffer we are about to apply an operation to) has multiple consumers /// if it has multiple consumers, you can't inplace fn determine_tensor_source<'a>( - source: &'a Tensor, + source: &'a OpTensor, gpu_compile_keys: &HashMap, - ) -> &'a Tensor { + ) -> &'a OpTensor { let old_source_id = source.id(); let mut candidate = source; - log::trace!("Determining source for {:?}", old_source_id); + log::trace!("Determining source for {old_source_id:?}"); // If we start on a view, we need to iterate unconditionally // until we find a non-view operation @@ -173,7 +188,7 @@ impl BufferAllocator { loop { if candidate.op().srcs().is_empty() || !true_source.op().supports_inplace() - || candidate.is_variable() + || candidate.requires_grad() { //If no sources, we are at the root //Or if the operation doesn't support inplace @@ -217,7 +232,7 @@ impl BufferAllocator { //2. When we encounter the last consumer of a tensor, we start recording the interval. //3. When we encounter the producer of a tensor, we stop recording the interval. fn calculate_usage_records( - execution_order: &[&Tensor], + execution_order: &[&OpTensor], gpu_compile_keys: &HashMap, ) -> HashMap { let mut records = @@ -238,7 +253,7 @@ impl BufferAllocator { .or_insert_with(|| TensorUsageRecord { id: None, producer: None, - is_variable: None, + requires_grad: None, last_consumer: topo_len - iter, last_consumer_id: t.id(), size: true_source.num_bytes(), @@ -248,7 +263,7 @@ impl BufferAllocator { if let Some(record) = records.get_mut(&t.id()) { record.id = Some(t.id()); record.producer = Some(topo_len - iter); - record.is_variable = Some(t.is_variable()); + record.requires_grad = Some(t.requires_grad()); } } @@ -262,8 +277,8 @@ impl BufferAllocator { //Takes in const assignments as inplace may be performed on constants pub fn greedy_by_size( &self, - execution_order: &[&Tensor], - output_tensors: &BTreeMap, + execution_order: &[&OpTensor], + output_tensors: &BTreeMap, assignments: &mut HashMap, gpu_compile_keys: &HashMap, use_shared_buffers: bool, @@ -275,7 +290,7 @@ impl BufferAllocator { for record in records.0.iter() { let should_be_shared = use_shared_buffers - && !(record.is_variable.unwrap_or(false) + && !(record.requires_grad.unwrap_or(false) || output_tensors.get(&record.last_consumer_id).is_some()); let mut best_obj = None; @@ -325,10 +340,10 @@ impl BufferAllocator { } for source in t.op().srcs() { let true_source = Self::determine_tensor_source(source, gpu_compile_keys); - if true_source.id() != source.id() { - if let Some(buf) = assignments.get(&true_source.id()) { - assignments.insert(source.id(), buf.clone()); - } + if true_source.id() != source.id() + && let Some(buf) = assignments.get(&true_source.id()) + { + assignments.insert(source.id(), buf.clone()); } } } @@ -344,7 +359,7 @@ impl BufferAllocator { /// /// Simple greedy algorithm /// 1. Iterate over all tensors in reverse order (leaf -> root) - /// 2. For each tensor, loop through it's input values. + /// 2. For each tensor, loop through its input values. /// a. Assign a buffer for each input value, if it is not already assigned /// b. If the input value is an inplace operation, traverse upwards until we find /// the "true" buffer source (i.e the first non-inplace operation). @@ -352,8 +367,8 @@ impl BufferAllocator { /// and earlier tensors can use it. pub fn allocate_cfg( &self, - execution_order: &[&Tensor], - output_tensors: &BTreeMap, + execution_order: &[&OpTensor], + output_tensors: &BTreeMap, gpu_compile_keys: &HashMap, use_shared_buffers: bool, device: &WgpuDevice, @@ -409,25 +424,25 @@ impl BufferAllocator { assignments.insert(output.id(), output_buffer); } - #[cfg(debug_assertions)] - { - let mut output_allocations = BTreeMap::new(); - for t in execution_order.iter() { - if let Some(allocation) = assignments.get(&t.id()) { - if output_tensors.contains_key(&t.id()) { - output_allocations.insert(allocation.global_id(), t.id()); - } else if let Some(output_id) = output_allocations.get(&allocation.global_id()) - { - panic!( - "Allocation {:?} used by output tensor {:?} was reused by tensor {:?}", - allocation.global_id(), - output_id, - t.id() - ); - } - } - } - } + // #[cfg(debug_assertions)] + // { + // let mut output_allocations = BTreeMap::new(); + // for t in execution_order.iter() { + // if let Some(allocation) = assignments.get(&t.id()) { + // if output_tensors.contains_key(&t.id()) { + // output_allocations.insert(allocation.global_id(), t.id()); + // } else if let Some(output_id) = output_allocations.get(&allocation.global_id()) + // { + // panic!( + // "Allocation {:?} used by output tensor {:?} was reused by tensor {:?}", + // allocation.global_id(), + // output_id, + // t.id() + // ); + // } + // } + // } + // } log::debug!( "Total bytes allocated: {}kb", @@ -444,4 +459,12 @@ impl BufferAllocator { pub fn usage_bytes(&self) -> u64 { self.pool.read().total_gpu_size_in_bytes() } + + pub fn reset_usage_peaks(&self) { + self.pool.read().reset_usage_peaks(); + } + + pub fn peak_usage_bytes_since_reset(&self) -> u64 { + self.pool.read().peak_total_gpu_size_in_bytes_since_reset() + } } diff --git a/crates/piston-core/src/gpu/buffer_allocator/lazy_graph_executor.rs b/crates/piston-core/src/gpu/buffer_allocator/lazy_graph_executor.rs new file mode 100644 index 00000000..e8454d47 --- /dev/null +++ b/crates/piston-core/src/gpu/buffer_allocator/lazy_graph_executor.rs @@ -0,0 +1,828 @@ +use crate::{ + Compiled, CpuUniform, DebugSelection, Executable, ExecutionError, ExecutionResult, GPUBuffer, + HashMap, HashSet, Hasher as HasherType, Inner, LazyOp, StepLog, StepLogConfig, Storage, + TensorError, WgpuDevice, reset_scope_context, +}; +#[cfg(feature = "debug")] +use crate::{DebugTensor, Device, DeviceStorage}; +use crate::{OpTensor, TensorId}; +use maybe_async::maybe_async; +use parking_lot::RwLock; +use std::collections::BTreeMap; +use std::hash::Hasher; +use std::sync::{Arc, Weak}; + +#[derive(Debug, thiserror::Error)] +pub enum LazyGraphExecutorError { + #[error( + "one of the variables needed for gradient computation has been modified by an inplace operation." + )] + InplaceError, + #[error(transparent)] + TensorError(#[from] crate::TensorError), + #[error(transparent)] + DeviceError(#[from] crate::DeviceError), + #[error(transparent)] + OperationError(#[from] crate::OperationError), +} + +enum EmitStatus { + Emitting, + Emitted, +} + +type EmissionMap = HashMap; +type PostOrderData<'a> = Vec<&'a OpTensor>; + +struct CachedExecutable { + executable: Arc, + is_shared_realloc: bool, +} + +pub struct LazyGraphExecutor { + tensors: Arc>>>, + cache: HashMap, + step_log_config: Option, + pass_index: u64, + inplace_support: bool, + caching_enabled: bool, + shared_object_allocation_enabled: bool, +} + +fn panic_cycle(id: TensorId) { + panic!( + "Cycle detected whilst computing topological order: {id:?}. Try plotting with feature `plotting`." + ); +} + +#[cfg(feature = "debug")] +macro_rules! mut_in_debug { + ($ident:ident) => { mut $ident }; +} + +#[cfg(not(feature = "debug"))] +macro_rules! mut_in_debug { + ($ident:ident) => { + $ident + }; +} + +fn compute_post_order(tensor: &OpTensor) -> Vec<&OpTensor> { + let mut post_order = Vec::new(); + let mut node_stack = vec![tensor]; + let mut emission_map = EmissionMap::default(); + while let Some(node) = node_stack.last().cloned() { + match emission_map.get(&node.id()) { + None => { + emission_map.insert(node.id(), EmitStatus::Emitting); + for src in node.op().srcs() { + if let Some(EmitStatus::Emitting) = emission_map.get(&src.id()) { + panic_cycle(src.id()); + } + + node_stack.push(src); + } + } + Some(EmitStatus::Emitting) => { + for src in node.op().srcs() { + if let Some(EmitStatus::Emitting) = emission_map.get(&src.id()) { + panic_cycle(src.id()); + } + } + emission_map.insert(node.id(), EmitStatus::Emitted); + post_order.push(node); + node_stack.pop(); + } + Some(EmitStatus::Emitted) => { + node_stack.pop(); + } + } + } + post_order +} + +fn compute_post_order_from_nodes(roots: Vec<&OpTensor>) -> PostOrderData<'_> { + let mut post_order = Vec::new(); + for root in roots { + post_order.extend(compute_post_order(root)); + } + post_order +} + +impl LazyGraphExecutor { + pub fn new( + inplace_support: bool, + caching_enabled: bool, + shared_object_allocation_enabled: bool, + ) -> Self { + Self { + tensors: Arc::new(RwLock::new(BTreeMap::default())), + cache: HashMap::default(), + pass_index: Default::default(), + inplace_support, + step_log_config: None, + caching_enabled, + shared_object_allocation_enabled, + } + } + + pub fn register_tensor(&self, tensor: &OpTensor) { + log::trace!("Registering tensor {:?}", tensor.id()); + self.tensors + .write() + .insert(tensor.id(), Arc::downgrade(&tensor.inner)); + } + + /// Unregisters a tensor by its `TensorId`. + pub fn unregister_tensor(&self, id: TensorId) { + log::trace!("Unregistering tensor {id:?}"); + self.tensors.write().remove(&id); + } + + fn get_live_tensors(&self) -> BTreeMap { + self.tensors + .read() + .iter() + // Attempt to upgrade from Weak → Arc. + // If it succeeds, wrap Arc in Tensor. + .filter_map(|(id, weak_inner)| { + weak_inner + .upgrade() + .map(|arc_inner| (*id, OpTensor { inner: arc_inner })) + .filter(|(_, t)| !t.resolved()) + }) + .collect() + } + + fn run_post_order<'a>(&self, tensors: Vec<&'a OpTensor>) -> PostOrderData<'a> { + compute_post_order_from_nodes(tensors) + } + + #[maybe_async] + pub async fn sync_live_tensors_graph( + &mut self, + gpu_device: &WgpuDevice, + ) -> anyhow::Result<(), LazyGraphExecutorError> { + reset_scope_context(); + log::trace!("Syncing live tensors graph"); + let tensors = self.get_live_tensors(); + log::debug!("All registered IDs: {:?}", self.tensors.read().keys()); + let owned_tensors = tensors.keys().cloned().collect(); + self.sync_tensors_graph_impl(tensors, Some(owned_tensors), gpu_device) + .await + } + + #[maybe_async] + pub async fn sync_tensors_graph( + &mut self, + tensors: Vec<&OpTensor>, + gpu_device: &WgpuDevice, + ) -> anyhow::Result<(), LazyGraphExecutorError> { + self.sync_tensors_graph_impl( + tensors.into_iter().map(|t| (t.id(), t.clone())).collect(), + None, + gpu_device, + ) + .await + } + + #[maybe_async] + async fn run_executable( + &mut self, + executable: &mut Executable, + gpu_device: &WgpuDevice, + immediate: bool, + ) -> anyhow::Result { + log::debug!("Running executable"); + #[cfg(feature = "debug")] + let index = executable.dispatch_debugging(gpu_device)?; + + #[cfg(not(feature = "debug"))] + let (index, result) = executable + .dispatch(gpu_device, self.step_log_config.as_ref()) + .await?; + + if immediate { + gpu_device + .poll(wgpu::PollType::Wait { + submission_index: Some(index), + timeout: None, + }) + .unwrap(); + } + Ok(result) + } + + #[maybe_async] + async fn sync_tensors_graph_impl( + &mut self, + tensors: BTreeMap, + owned_tensors: Option>, + gpu_device: &WgpuDevice, + ) -> Result<(), LazyGraphExecutorError> { + // First check if the tensors are already resolved + log::debug!("Syncing tensors graph"); + if tensors.values().all(|t| t.resolved()) { + return Ok(()); + } + + let use_cache = self.caching_enabled; + + // Notably, we compute post order first because we want to hash the tensors in post order, + // since each hash depends on the hashes of its sources. It's not clear to me that this + // violates some important unspoken assumption on the part of the LazyTensor authors. + // We also flip the hash order—post order first, then insertion order—because it's more + // convenient to treat it as one big hash pass. + // let tensors = tensors.clone(); + let combined_post_orders = self.run_post_order(tensors.values().collect()); + + let mut indices = Vec::with_capacity(tensors.len()); + let mut tensor_ids = HashSet::with_capacity_and_hasher(tensors.len(), Default::default()); + + let mut hasher = HasherType::default(); + let mut tensor_hashes = BTreeMap::default(); + + // Keep track of tensors that have been used as an src by another tensor + let mut used_as_src = HashSet::with_capacity_and_hasher(tensors.len(), Default::default()); + + let mut uniform = CpuUniform::new(); + let mut compile_keys = HashMap::default(); + #[cfg(feature = "plotting")] + let mut strong_counts_inplace = HashMap::default(); + + // Keep track of the real source of each tensor; important to help resolve handle those + // annoying views correctly. + let mut tensor_sources = HashMap::default(); + + let mut seen_nodes = HashSet::default(); + let mut post_order = Vec::new(); + + // let mut can_inplace_count = 0; + // let mut no_op_support_for_inplacing_count = 0; + // let mut no_inplacing_because_source_requires_grad_count = 0; + // let post_order_len = combined_post_orders.len(); + + // First we loop over the post order to hash the tensors in the right order + for tensor in combined_post_orders.into_iter() { + if seen_nodes.insert(tensor.id()) { + post_order.push(tensor); + // Scope to drop tensor_hashes before inserting + let srcs = tensor.op().srcs(); + // log::trace!( + // "{:?}: Srcs: {:?}", + // tensor.id(), + // srcs.iter().map(|s| s.id()).collect::>() + // ); + let first_src = srcs.first().cloned(); + + let mut to_modify = None; + if !matches!(tensor.op(), LazyOp::View(_)) { + tensor_sources.insert(tensor.id(), tensor); + to_modify = first_src.map(|src| { + tensor_sources + .get(&src.id()) + .cloned() + .expect("Source missing entry in tensor_sources") + }); + } else if let Some(src) = tensor_sources + .get(&first_src.expect("All views should have one src").id()) + .cloned() + { + tensor_sources.insert(tensor.id(), src); + to_modify = Some(src); + } + + let can_inplace = match to_modify { + Some(to_modify_src) => { + log::trace!( + "{:?}: Supports inplace: {:?}, is parameter: {:?}", + tensor.id(), + tensor.op().supports_inplace(), + to_modify_src.requires_grad() + ); + + if tensor.is_inplace() { + true + } else { + // TODO(vinhowe): This really is horrible; we should be able to just + // check if the op supports inplace. + match tensor.op() { + LazyOp::Softmax(_) + | LazyOp::ScatterAdd(_) + | LazyOp::IndexAdd(_) => true, + LazyOp::Detach(d) => { + matches!( + d.as_ref(), + LazyOp::Softmax(_) + | LazyOp::ScatterAdd(_) + | LazyOp::IndexAdd(_) + ) + } + _ => { + // vinhowe: we need to check if the src is a parameter, because + // we can't inplace parameters unless we've disabled gradient tracking. + if !tensor.op().supports_inplace() + || to_modify_src.requires_grad() + { + false + } else { + // Typical references: + // 1. Its original consumer. Whatever scope it was created + // in. + // 2. `tensors`, as passed into this method, if it wasn't + // resolved and we upgraded its weak reference. This + // happens when we do a sync of live tensors, say, in an + // optimizer step, but a one-off sync won't do this. This + // is why we have the optional `owned_tensors`. + // If these two are the only references, then we can + // inplace. Usually, additional references include, not in + // any particular order: + // 3. The optimizer, if it is a parameter. We'll also check + // if the src is a parameter. + // 4+ Any other Tensor consumers in the post-order. If it's + // not a parameter, these are the references we're + // concerned about messing with. + // + // If we own a copy, 2, otherwise 1. + let expected_strong = owned_tensors + .as_ref() + .and_then(|ot| { + ot.contains(&to_modify_src.id()).then_some(2) + }) + .unwrap_or(1); + + to_modify_src.strong_count() <= expected_strong + } + } + } + } + } + None => false, + }; + + #[cfg(feature = "plotting")] + strong_counts_inplace.insert(tensor.id(), (tensor.strong_count(), can_inplace)); + log::trace!( + "Can inplace: {:?}, op: {:?} ({:?}), strong: {:?}", + can_inplace, + tensor.op().name(), + tensor.id(), + to_modify.as_ref().map(|t| t.strong_count()) + ); + let compile_key = tensor.gpu_compile_key(can_inplace, &mut uniform); + let ir = tensor.op().ir(); + ir.shape_hash(&mut hasher, &tensor_hashes, &compile_key); + if let Some(compile_key) = compile_key { + compile_keys.insert(tensor.id(), compile_key); + } + let hash = hasher.finish(); + tensor_hashes.insert(tensor.id(), hash); + log::debug!("IR: {ir:?}"); + log::debug!("Tensor hash: {hash:#x} (op: {:?})", tensor.op().name()); + for src in tensor.op().srcs() { + used_as_src.insert(src.id()); + } + } else { + // If we've already seen this node, just add its hash to the hasher + hasher.write_u64( + *tensor_hashes + .get(&tensor.id()) + .expect("Missing shape hash for tensor"), + ); + } + } + + log::debug!("Post-order hash: {:?}", hasher.finish()); + + let output_tensors = tensors + .iter() + .filter(|(id, _)| !used_as_src.contains(id)) + .map(|(id, tensor)| (*id, tensor)) + .collect::>(); + + #[cfg(feature = "plotting")] + crate::plot::render_to_file( + &post_order, + &output_tensors, + &strong_counts_inplace, + None, + construct_plot_filename("post_order", self.pass_index, self.inplace_support), + ) + .unwrap(); + + for (i, (id, tensor)) in tensors.iter().enumerate() { + if !tensor_ids.insert(id) || tensor.resolved() { + continue; + } + + #[cfg(feature = "debug")] + if !tensor_hashes.contains_key(id) { + log::warn!("Missing shape hash for tensor {:?}", id); + continue; + } + hasher.write_u64( + *tensor_hashes + .get(id) + .expect("Missing shape hash for tensor"), + ); + indices.push(i); + } + let hash = hasher.finish(); + + log::debug!("Shape hash: {hash:?}"); + + #[cfg(feature = "debug")] + let mut cpu_bufs = HashMap::default(); + + #[cfg(feature = "debug")] + // Get CPU buffers from existing allocations + for tensor in &post_order { + let storage_guard = tensor.storage(); + match storage_guard.as_ref() { + Some(Storage::GPU(gpu_buf)) => { + log::trace!("Getting CPU buffer for {tensor.id():?}"); + cpu_bufs.insert( + tensor.id(), + gpu_buf.to_cpu(&Device::GPU(gpu_device.clone()))?, + ); + } + Some(Storage::CPU(cpu_buf)) => { + log::trace!("Using existing CPU buffer for {:?}", tensor.id()); + cpu_bufs.insert(tensor.id(), cpu_buf.clone()); + } + None => {} + } + } + + let (mut cached_exec, do_shared_realloc, is_shared_realloc) = if use_cache { + self.cache + .remove(&hash) + .map(|cached_exec| { + if cached_exec.is_shared_realloc { + // Cache hit, no need to realloc, shared realloc + (Arc::try_unwrap(cached_exec.executable).ok(), false, true) + } else { + // Cache hit, not shared realloc and needs shared realloc, not yet shared + // realloc + (None, true, false) + } + }) + // Cache miss, no need to realloc, can't be shared realloc + .unwrap_or((None, false, false)) + } else { + // Not using cache, no need to realloc, we don't allow shared realloc + (None, false, false) + }; + + let mut compiled_ops = Vec::with_capacity(post_order.len()); + + gpu_device.begin_pass(self.pass_index); + + let mut allocations = if cached_exec.is_none() || do_shared_realloc { + Some(gpu_device.allocate_cfg( + &post_order, + &output_tensors, + &compile_keys, + self.shared_object_allocation_enabled, + gpu_device, + )?) + } else { + None + }; + + #[cfg(debug_assertions)] + { + let resolved_tensors = post_order.iter().filter(|t| t.resolved()).count(); + let resolved_tensors_len = post_order.len(); + log::trace!( + "Post order: {:?}", + post_order.iter().map(|t| t.id()).collect::>() + ); + log::trace!( + "Resolved tensors: {:?}", + post_order + .iter() + .filter(|t| t.resolved()) + .map(|t| t.id()) + .collect::>() + ); + log::debug!( + "Length of resolved tensors in post order: {resolved_tensors} / {resolved_tensors_len}" + ); + } + + #[cfg(feature = "debug")] + let mut compute_dsts = Vec::new(); + + #[cfg(feature = "plotting")] + crate::plot::render_to_file( + &post_order, + &output_tensors, + &strong_counts_inplace, + None, + construct_plot_filename("prealloc", self.pass_index, self.inplace_support), + ) + .unwrap(); + + #[cfg(not(feature = "debug"))] + let mut debug_list = BTreeMap::new(); + + let mut i = 0; + for t in &post_order { + if t.op().is_const() || t.resolved() { + continue; + } + + if let Some(allocations) = &mut allocations { + let id = t.id(); + let inner = allocations.remove(&id).ok_or(TensorError::NoStorage(id))?; + t.update_storage(Storage::GPU(GPUBuffer { + inner, + alignment: t.dtype().size_of(), + cpu_size: Some(t.num_bytes()), + })); + } + + if let Some(compile_key) = compile_keys.get(&t.id()) { + let selected_for_step_log = self + .step_log_config + .as_ref() + .map(|c| c.debug_selection.as_ref()) + .and_then(|s| { + s.as_ref().map(|s| match s { + DebugSelection::All => true, + DebugSelection::Some(scopes) => { + if let Some(scope) = t.scope() { + scopes.contains(scope) + } else { + false + } + } + }) + }) + .unwrap_or(false); + + #[cfg(not(feature = "debug"))] + let debug_list_ref = &mut debug_list; + + // TODO(vinhowe): Rethink this whole thing and don't use a function here. + #[cfg(not(feature = "debug"))] + let mut set_debug_buffer = + move |compiled_op: &mut Compiled| -> Result<(), TensorError> { + let tensor_debug_buffer = t.debug_tensor(); + if selected_for_step_log || tensor_debug_buffer.is_some() { + // We ignore any requests to debug copy items + if let Compiled::Compute(op) = compiled_op { + let debug_tensor = if let Some(tensor) = tensor_debug_buffer { + tensor + } else { + t.get_or_create_debug_tensor()? + }; + let storage_guard = debug_tensor.storage(); + let debug_buffer = storage_guard + .as_ref() + .expect("Debug tensor should have a storage") + .try_gpu()?; + op.debug_buffer = Some(debug_buffer.inner.clone()); + debug_list_ref.insert(t.id(), (*t).clone()); + }; + }; + Ok(()) + }; + + if let Some(exec) = cached_exec.as_mut() { + let compiled_op = &mut exec.steps[i]; + #[cfg(not(feature = "debug"))] + set_debug_buffer(compiled_op)?; + } else if let Some(mut compiled_op) = + t.compile_gpu(compile_key, gpu_device, selected_for_step_log) + { + #[cfg(not(feature = "debug"))] + set_debug_buffer(&mut compiled_op)?; + compiled_ops.push(Some(compiled_op)); + } else { + log::warn!("Compilation failed for operation: {:?}", t.op().name()); + compiled_ops.push(None); + }; + i += 1; + + #[cfg(feature = "debug")] + compute_dsts.push((*t).clone()); + } + } + + // At this point we have a cached executable that we want to ignore. + cached_exec = cached_exec.map(|exec| exec.with_tensors(&post_order).unwrap()); + + #[cfg(feature = "debug")] + let debug_list = compute_dsts + .into_iter() + .map(|t| { + DebugTensor::new( + t.storage().clone(), + t.dtype(), + t.op() + .srcs() + .iter() + .map(|s| { + DebugTensor::new(s.storage().clone(), s.dtype(), vec![], s.num_bytes()) + }) + .collect(), + t.num_bytes(), + ) + }) + .collect::>(); + + let is_cached = cached_exec.is_some(); + + let mut executable; + if let Some(mut_in_debug!(cached_exec)) = cached_exec { + log::debug!("Using cached executable"); + + #[cfg(feature = "debug")] + let mut cpu_bufs = HashMap::default(); + + #[cfg(feature = "debug")] + // Get CPU buffers from existing allocations + for tensor in &post_order { + let storage_guard = tensor.storage(); + match storage_guard.as_ref() { + Some(Storage::GPU(gpu_buf)) => { + log::trace!("Getting CPU buffer for {tensor.id():?}"); + cpu_bufs.insert( + tensor.id(), + gpu_buf.to_cpu(&Device::GPU(gpu_device.clone()))?, + ); + } + Some(Storage::CPU(cpu_buf)) => { + log::trace!("Using existing CPU buffer for {tensor.id():?}"); + cpu_bufs.insert(tensor.id(), cpu_buf.clone()); + } + None => {} + } + } + + #[cfg(feature = "debug")] + { + cached_exec.debug_list = Some(debug_list); + cached_exec.cpu_bufs = Some(Arc::new(RwLock::new(cpu_bufs))); + } + + executable = cached_exec; + } else { + if use_cache { + // On a cache miss: Clear cache because currently I don't know how to make sure + // allocations are compatible between runs. + self.cache.clear(); + } + + #[cfg(feature = "plotting")] + crate::plot::render_to_file( + &post_order, + &output_tensors, + &strong_counts_inplace, + None, + construct_plot_filename("alloc", self.pass_index, self.inplace_support), + ) + .unwrap(); + + // Only keep the ops that successfully compiled. + let filtered_compiled_ops: Vec<_> = compiled_ops.into_iter().flatten().collect(); + + executable = Executable::new( + None, + filtered_compiled_ops, + uniform.into_gpu(gpu_device)?, + #[cfg(not(feature = "debug"))] + if debug_list.is_empty() { + None + } else { + Some(debug_list) + }, + #[cfg(feature = "debug")] + Some(Arc::new(RwLock::new(cpu_bufs))), + ); + } + + let result = self + .run_executable(&mut executable, gpu_device, false) + .await + .unwrap(); + + #[cfg(all(feature = "debug", feature = "plotting"))] + { + let cpu_bufs_guard = executable.cpu_bufs.as_ref().map(|arc| arc.read()); + + crate::plot::render_to_file( + &post_order, + &output_tensors, + &strong_counts_inplace, + cpu_bufs_guard.as_deref(), + construct_plot_filename("post_exec", self.pass_index, self.inplace_support), + ) + .unwrap(); + } + + if !is_cached && use_cache { + // We save the storage of the executable to be used in the next pass + executable.set_storage(post_order.iter().map(|t| t.storage().clone()).collect()); + } + + if self.step_log_config.is_some() { + let step_log = StepLog::from_post_order( + post_order, + result.profiling_entries, + result.gpu_bufs, + hash, + is_cached, + is_shared_realloc, + gpu_device, + ); + gpu_device.set_last_step_log(step_log); + } + + if use_cache { + // After creating/running the executable, we cache it + self.cache.insert( + hash, + CachedExecutable { + executable: Arc::new(executable), + // If we already did a shared realloc, we don't need to do it again + is_shared_realloc: is_shared_realloc || do_shared_realloc, + }, + ); + } + + self.pass_index += 1; + Ok(()) + } + + pub fn step_log_config(&self) -> Option<&StepLogConfig> { + self.step_log_config.as_ref() + } + + pub fn set_step_log_config(&mut self, config: StepLogConfig) { + let old_config_debug_selection = self + .step_log_config + .as_ref() + .map(|c| c.debug_selection.clone()); + // If the debug selection has changed, clear the cache; we'll need to recompile all the ops + let new_config_debug_selection = self + .step_log_config + .as_ref() + .map(|c| c.debug_selection.clone()); + if old_config_debug_selection != new_config_debug_selection { + log::debug!("Debug selection changed, clearing cache"); + self.cache.clear(); + } + self.step_log_config = Some(config); + } + + pub fn set_caching_enabled(&mut self, enabled: bool) { + self.caching_enabled = enabled; + } + + pub fn caching_enabled(&self) -> bool { + self.caching_enabled + } + + pub fn set_shared_object_allocation_enabled(&mut self, enabled: bool) { + self.shared_object_allocation_enabled = enabled; + } + + pub fn shared_object_allocation_enabled(&self) -> bool { + self.shared_object_allocation_enabled + } + + pub fn set_inplace_support(&mut self, enabled: bool) { + self.inplace_support = enabled; + } + + pub fn inplace_support(&self) -> bool { + self.inplace_support + } +} + +impl Default for LazyGraphExecutor { + fn default() -> Self { + Self::new(false, false, false) + } +} + +/// Constructs the plot filename with an optional "_inplace" segment. +/// +/// The resulting filename is in the format: +/// "[_inplace]_.svg" +/// +/// # Arguments +/// * `name` - The base part of the file name (e.g., "post_order"). +/// * `pass_index` - The pass index used in the file name. +/// * `inplace_support` - Flag indicating whether to add "_inplace" before the pass number. +#[cfg(feature = "plotting")] +fn construct_plot_filename(name: &str, pass_index: u64, inplace_support: bool) -> String { + if inplace_support { + format!("{}_inplace_{}", name, pass_index) + } else { + format!("{}_{}", name, pass_index) + } +} diff --git a/crates/ratchet-core/src/gpu/buffer_allocator/mod.rs b/crates/piston-core/src/gpu/buffer_allocator/mod.rs similarity index 100% rename from crates/ratchet-core/src/gpu/buffer_allocator/mod.rs rename to crates/piston-core/src/gpu/buffer_allocator/mod.rs diff --git a/crates/ratchet-core/src/gpu/buffer_allocator/tensor_usage_record.rs b/crates/piston-core/src/gpu/buffer_allocator/tensor_usage_record.rs similarity index 96% rename from crates/ratchet-core/src/gpu/buffer_allocator/tensor_usage_record.rs rename to crates/piston-core/src/gpu/buffer_allocator/tensor_usage_record.rs index 59488445..9ce547ec 100644 --- a/crates/ratchet-core/src/gpu/buffer_allocator/tensor_usage_record.rs +++ b/crates/piston-core/src/gpu/buffer_allocator/tensor_usage_record.rs @@ -11,7 +11,7 @@ pub struct TensorUsageRecord { pub last_consumer: usize, pub last_consumer_id: TensorId, pub size: usize, - pub is_variable: Option, + pub requires_grad: Option, } impl std::ops::Index for TensorUsageRecords { diff --git a/crates/ratchet-core/src/gpu/device.rs b/crates/piston-core/src/gpu/device.rs similarity index 77% rename from crates/ratchet-core/src/gpu/device.rs rename to crates/piston-core/src/gpu/device.rs index c684bf08..32c87916 100644 --- a/crates/ratchet-core/src/gpu/device.rs +++ b/crates/piston-core/src/gpu/device.rs @@ -1,5 +1,6 @@ -use crate::{gpu::*, DType, GpuCompileKey, Tensor, TensorId}; -use crate::{HashMap, TensorError}; +use crate::HashMap; +use crate::{DType, GpuCompileKey, OpTensor, TensorId, gpu::*}; +use maybe_async::maybe_async; use parking_lot::RwLock; use std::collections::BTreeMap; use std::{borrow::Cow, sync::Arc}; @@ -30,6 +31,7 @@ pub struct WgpuDevice { device_features: DeviceFeatures, device: Arc, queue: Arc, + last_step_log: Arc>>, } impl std::ops::Deref for WgpuDevice { @@ -48,7 +50,7 @@ impl std::fmt::Debug for WgpuDevice { impl PartialEq for WgpuDevice { fn eq(&self, other: &Self) -> bool { - self.ordinal == other.ordinal && self.device.global_id() == other.device.global_id() + self.ordinal == other.ordinal && self.device == other.device } } @@ -58,20 +60,17 @@ impl WgpuDevice { let adapter = Self::select_adapter().await?; #[cfg(not(target_arch = "wasm32"))] let adapter = Self::select_adapter()?; - log::info!("Adapter: {:?}", adapter.get_info()); - log::info!("Active GPU: {}", adapter.get_info().name); + log::debug!("Adapter: {:?}", adapter.get_info()); + log::debug!("Active GPU: {}", adapter.get_info().name); #[allow(unused_mut)] let mut required_features = wgpu::Features::default(); required_features |= wgpu::Features::SHADER_F16; required_features |= wgpu::Features::SUBGROUP; - #[cfg(feature = "gpu-profiling")] - { - required_features |= wgpu::Features::TIMESTAMP_QUERY; - } + required_features |= wgpu::Features::TIMESTAMP_QUERY; let mut device_descriptor = wgpu::DeviceDescriptor { - label: Some("Ratchet"), + label: Some("Piston"), required_features, required_limits: Limits { max_buffer_size: MAX_BUFFER_SIZE, @@ -80,31 +79,33 @@ impl WgpuDevice { ..Default::default() }, memory_hints: wgpu::MemoryHints::Performance, + experimental_features: wgpu::ExperimentalFeatures::disabled(), + trace: Default::default(), }; - let device_request = adapter.request_device(&device_descriptor, None).await; + let device_request = adapter.request_device(&device_descriptor).await; let (device, queue) = if let Err(e) = device_request { - log::error!("Failed to acq. device, trying with reduced limits: {:?}", e); + log::error!("Failed to acq. device, trying with reduced limits: {e:?}"); device_descriptor.required_limits = adapter.limits(); device_descriptor.required_features = adapter.features(); - adapter.request_device(&device_descriptor, None).await + adapter.request_device(&device_descriptor).await } else { device_request }?; - log::info!("Device: {:?}", device.limits()); + log::debug!("Device: {:?}", device.limits()); let limits = DeviceLimits::from(device.limits()); let mut features = DeviceFeatures::from(device.features()); - if std::env::var("RATCHET_FORCE_F32").is_ok() { + if std::env::var("PISTON_FORCE_F32").is_ok() { log::warn!("Forcing F32 precision"); features.SHADER_F16 = false; } - if std::env::var("RATCHET_DISABLE_SUBGROUPS").is_ok() { + if std::env::var("PISTON_DISABLE_SUBGROUPS").is_ok() { log::warn!("Disabling subgroup support"); features.SUBGROUP = false; } - log::warn!("Device features: {:?}", features); + log::debug!("Device features: {features:?}"); let buffer_allocator = Arc::new(BufferAllocator::new()); @@ -118,10 +119,11 @@ impl WgpuDevice { kernel_module_pool: Arc::new(KernelModulePool::new()), compute_pipeline_pool: Arc::new(ComputePipelinePool::new()), // TODO: Decide if we need some nice thing to encapsulate the lazy graph executor - lazy_graph_executor: Arc::new(RwLock::new(LazyGraphExecutor::new(true))), + lazy_graph_executor: Arc::new(RwLock::new(LazyGraphExecutor::default())), device: Arc::new(device), device_limits: limits, device_features: features, + last_step_log: Arc::new(RwLock::new(None)), }) } @@ -143,7 +145,7 @@ impl WgpuDevice { force_fallback_adapter: false, }) .await - .ok_or(DeviceError::AdapterRequestFailed) + .map_err(|_| DeviceError::AdapterRequestFailed) } #[cfg(not(target_arch = "wasm32"))] @@ -237,7 +239,7 @@ impl WgpuDevice { desc: &KernelModuleDesc, kernel: &K, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, device: &WgpuDevice, ) -> KernelModuleHandle { @@ -273,8 +275,8 @@ impl WgpuDevice { /// Additionally, allocates buffer for leaf node, the tensor upon which resolve was called. pub fn allocate_cfg( &self, - execution_order: &[&Tensor], - output_tensors: &BTreeMap, + execution_order: &[&OpTensor], + output_tensors: &BTreeMap, gpu_compile_keys: &HashMap, use_shared_buffers: bool, device: &WgpuDevice, @@ -289,7 +291,7 @@ impl WgpuDevice { } // TODO(vinhowe): Not sure if I like these calls here - pub fn register_tensor(&self, tensor: &Tensor) { + pub fn register_tensor(&self, tensor: &OpTensor) { self.lazy_graph_executor.read().register_tensor(tensor); } @@ -297,20 +299,64 @@ impl WgpuDevice { self.lazy_graph_executor.read().unregister_tensor(id); } - pub fn mark_step(&self) -> Result<(), TensorError> { + #[maybe_async] + pub async fn mark_step(&self) -> Result<(), LazyGraphExecutorError> { self.lazy_graph_executor .write() .sync_live_tensors_graph(self) + .await } - pub fn sync_tensors_graph(&self, tensors: Vec<&Tensor>) -> Result<(), TensorError> { + #[maybe_async] + pub async fn sync_tensors_graph( + &self, + tensors: Vec<&OpTensor>, + ) -> Result<(), LazyGraphExecutorError> { self.lazy_graph_executor .write() .sync_tensors_graph(tensors, self) + .await + } + + pub fn set_caching_enabled(&self, enabled: bool) { + self.lazy_graph_executor + .write() + .set_caching_enabled(enabled); + } + + pub fn caching_enabled(&self) -> bool { + self.lazy_graph_executor.read().caching_enabled() + } + + pub fn set_shared_object_allocation_enabled(&self, enabled: bool) { + self.lazy_graph_executor + .write() + .set_shared_object_allocation_enabled(enabled); + } + + pub fn shared_object_allocation_enabled(&self) -> bool { + self.lazy_graph_executor + .read() + .shared_object_allocation_enabled() + } + + pub fn set_inplace_support(&self, enabled: bool) { + self.lazy_graph_executor + .write() + .set_inplace_support(enabled); + } + + pub fn inplace_support(&self) -> bool { + self.lazy_graph_executor.read().inplace_support() + } + + pub fn set_vram_limit(&self, vram_limit: Option) { + self.buffer_allocator.set_vram_limit(vram_limit); } pub fn begin_pass(&self, pass_index: u64) { self.buffer_allocator.begin_pass(pass_index); + self.bind_group_pool.begin_pass(pass_index); } pub fn compute_features(&self) -> &DeviceFeatures { @@ -324,6 +370,26 @@ impl WgpuDevice { pub fn usage_bytes(&self) -> u64 { self.buffer_allocator.usage_bytes() } + + pub fn mark_usage_bytes_step(&self) { + self.buffer_allocator.reset_usage_peaks(); + } + + pub fn peak_usage_bytes_since_reset(&self) -> u64 { + self.buffer_allocator.peak_usage_bytes_since_reset() + } + + pub fn set_step_log_config(&self, config: StepLogConfig) { + self.lazy_graph_executor.write().set_step_log_config(config); + } + + pub fn take_step_log(&self) -> Option { + self.last_step_log.write().take() + } + + pub(crate) fn set_last_step_log(&self, log: StepLog) { + *self.last_step_log.write() = Some(log); + } } #[derive(Clone)] diff --git a/crates/piston-core/src/gpu/logging.rs b/crates/piston-core/src/gpu/logging.rs new file mode 100644 index 00000000..e121b7b1 --- /dev/null +++ b/crates/piston-core/src/gpu/logging.rs @@ -0,0 +1,223 @@ +#[cfg(target_arch = "wasm32")] +use crate::wgpu_buffer_to_cpu_buffer; +use crate::{ + DType, ExportedTensorProfilingEntry, HashMap, OpTensor, PooledGPUBuffer, Shape, TensorId, + WgpuDevice, +}; +use derive_new::new; +#[cfg(target_arch = "wasm32")] +use half::f16; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_futures::js_sys; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DebugSelection { + All, + Some(Vec), +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))] +#[derive(Default)] +pub struct StepLogConfig { + pub profiling: bool, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] + pub debug_selection: Option, +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +impl StepLogConfig { + #[wasm_bindgen(constructor, js_name = "new")] + pub fn new_js(profiling: bool, debug_selection: JsValue) -> Self { + let mut config = Self { + profiling, + debug_selection: None, + }; + + config.set_debug_selection_js(debug_selection); + + config + } + + #[wasm_bindgen(getter, js_name = "debug_selection")] + pub fn debug_selection_js(&self) -> JsValue { + match &self.debug_selection { + Some(DebugSelection::All) => JsValue::from_str("all"), + Some(DebugSelection::Some(ids)) => { + let array = js_sys::Array::new(); + for id in ids { + array.push(&JsValue::from(id)); + } + array.into() + } + None => JsValue::from_str("none"), + } + } + + #[wasm_bindgen(setter, js_name = "debug_selection")] + pub fn set_debug_selection_js(&mut self, value: JsValue) { + self.debug_selection = if let Some(s) = value.as_string() { + if s == "all" { + Some(DebugSelection::All) + } else { + None + } + } else if js_sys::Array::is_array(&value) { + let array = js_sys::Array::from(&value); + let mut ids = Vec::new(); + + for i in 0..array.length() { + if let Some(item) = array.get(i).as_string() { + ids.push(item); + } + } + + if !ids.is_empty() { + Some(DebugSelection::Some(ids)) + } else { + None + } + } else { + None + }; + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))] +#[derive(Debug, Clone, new)] +pub struct TensorLogStep { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] + pub id: TensorId, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] + pub srcs: Vec, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] + pub gpu_buf: Option, + pub kernel_name: String, + pub scope: Option, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] + pub dtype: DType, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] + pub shape: Shape, + pub profile: Option, + /// If exported, this will be a flat array of the values + pub values: Option>, + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] + pub device: Option, +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +impl TensorLogStep { + #[wasm_bindgen(getter, js_name = "id")] + pub fn id(&self) -> usize { + self.id.0 + } + + #[wasm_bindgen(getter, js_name = "srcs")] + pub fn srcs(&self) -> Vec { + self.srcs.iter().map(|id| id.0).collect() + } + + #[wasm_bindgen(getter, js_name = "dtype")] + pub fn dtype(&self) -> String { + self.dtype.as_str().to_string() + } + + #[wasm_bindgen(getter, js_name = "shape")] + pub fn shape(&self) -> Vec { + self.shape.to_vec() + } + + #[wasm_bindgen( + unchecked_return_type = "Uint8Array|Int8Array|Uint16Array|Int16Array|Uint32Array|Int32Array|Float32Array|Float64Array|null" + )] + pub async fn cpu(&self) -> JsValue { + if let Some(gpu_buf) = self.gpu_buf.as_ref() { + let cpu_buf = wgpu_buffer_to_cpu_buffer( + gpu_buf, + self.dtype.size_of(), + None, + self.device + .as_ref() + .expect("Device should exist if GPU buffer exists"), + ) + .await; + match self.dtype { + DType::F32 => JsValue::from(js_sys::Float32Array::from( + cpu_buf.to_slice::(&self.shape), + )), + DType::F16 => JsValue::from(js_sys::Float32Array::from( + cpu_buf + .to_slice::(&self.shape) + .iter() + .map(|x| x.to_f32()) + .collect::>() + .as_slice(), + )), + DType::I32 => JsValue::from(js_sys::Int32Array::from( + cpu_buf.to_slice::(&self.shape), + )), + DType::U32 => JsValue::from(js_sys::Uint32Array::from( + cpu_buf.to_slice::(&self.shape), + )), + _ => panic!("Unsupported dtype: {:?}", self.dtype), + } + } else { + JsValue::null() + } + } + + #[wasm_bindgen(getter, unchecked_return_type = "GPUBuffer|null")] + pub fn gpu_buf(&self) -> JsValue { + self.gpu_buf + .as_ref() + .map(|buf| buf.as_webgpu_buffer().into()) + .unwrap_or(JsValue::null()) + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))] +#[derive(Debug, Clone, new)] +pub struct StepLog { + pub tensors: Vec, + pub cached: bool, + pub hash: u64, + pub using_shared_buffers: bool, +} + +impl StepLog { + pub fn from_post_order( + post_order: Vec<&OpTensor>, + profiling_entries: Option>, + mut gpu_bufs: Option>, + hash: u64, + cached: bool, + using_shared_buffers: bool, + device: &WgpuDevice, + ) -> Self { + let tensors = post_order + .iter() + .map(|t| { + let gpu_buf = gpu_bufs.as_mut().and_then(|bufs| bufs.remove(&t.id())); + TensorLogStep::new( + t.id(), + t.op().srcs().iter().map(|tensor| tensor.id()).collect(), + gpu_buf.as_ref().cloned(), + t.op().name().to_string(), + t.scope().as_ref().map(|s| s.to_string()), + t.dtype(), + t.shape().clone(), + profiling_entries + .as_ref() + .and_then(|entries| entries.get(&t.id())) + .cloned(), + None, // ...for now + gpu_buf.map(|_| device.clone()), + ) + }) + .collect(); + Self::new(tensors, cached, hash, using_shared_buffers) + } +} diff --git a/crates/ratchet-core/src/gpu/mod.rs b/crates/piston-core/src/gpu/mod.rs similarity index 89% rename from crates/ratchet-core/src/gpu/mod.rs rename to crates/piston-core/src/gpu/mod.rs index cf38f4ed..80034cc0 100644 --- a/crates/ratchet-core/src/gpu/mod.rs +++ b/crates/piston-core/src/gpu/mod.rs @@ -1,25 +1,22 @@ mod align; mod buffer_allocator; mod device; +mod logging; mod pools; +mod profiler; mod uniform; mod wgsl; mod workload; -#[cfg(feature = "gpu-profiling")] -mod profiler; - pub use align::*; pub use buffer_allocator::*; pub use device::*; +pub use logging::*; pub use pools::*; +pub use profiler::*; pub use uniform::*; pub use wgsl::*; pub use workload::*; - -#[cfg(feature = "gpu-profiling")] -pub use profiler::*; - pub const MIN_STORAGE_BUFFER_SIZE: usize = 16; pub const STORAGE_BUFFER_ALIGN: usize = 256; //TODO: should be a device limit diff --git a/crates/ratchet-core/src/gpu/pools/bind_group_layout_pool.rs b/crates/piston-core/src/gpu/pools/bind_group_layout_pool.rs similarity index 96% rename from crates/ratchet-core/src/gpu/pools/bind_group_layout_pool.rs rename to crates/piston-core/src/gpu/pools/bind_group_layout_pool.rs index c2fe7244..68828deb 100644 --- a/crates/ratchet-core/src/gpu/pools/bind_group_layout_pool.rs +++ b/crates/piston-core/src/gpu/pools/bind_group_layout_pool.rs @@ -2,9 +2,9 @@ use std::hash::Hash; #[cfg(feature = "debug")] use std::hash::Hasher; -use crate::{gpu::WgpuDevice, rvec, RVec}; +use crate::{RVec, gpu::WgpuDevice, rvec}; -use super::{static_resource_pool::StaticResourcePool, StaticResourcePoolReadLockAccessor}; +use super::{StaticResourcePoolReadLockAccessor, static_resource_pool::StaticResourcePool}; pub trait BindGroupLayoutEntryExt { fn compute_storage_buffer(binding: u32, read_only: bool) -> Self; @@ -206,4 +206,8 @@ impl BindGroupLayoutPool { ) -> StaticResourcePoolReadLockAccessor<'_, BindGroupLayoutHandle, wgpu::BindGroupLayout> { self.inner.resources() } + + pub fn num_resources(&self) -> usize { + self.inner.num_resources() + } } diff --git a/crates/ratchet-core/src/gpu/pools/bind_group_pool.rs b/crates/piston-core/src/gpu/pools/bind_group_pool.rs similarity index 88% rename from crates/ratchet-core/src/gpu/pools/bind_group_pool.rs rename to crates/piston-core/src/gpu/pools/bind_group_pool.rs index 1b651d9d..8ca646d6 100644 --- a/crates/ratchet-core/src/gpu/pools/bind_group_pool.rs +++ b/crates/piston-core/src/gpu/pools/bind_group_pool.rs @@ -1,5 +1,6 @@ use super::*; -use crate::{gpu::WgpuDevice, RVec}; +use crate::{RVec, gpu::WgpuDevice}; +use parking_lot::RwLock; use std::sync::Arc; slotmap::new_key_type! { pub struct GpuBindGroupHandle; } @@ -84,12 +85,8 @@ impl DynamicResourcesDesc for BindGroupDescriptor { /// The question whether a bind groups happen to be re-usable becomes again a simple question of matching /// bind group descs which itself does not contain any ref counted objects! pub struct BindGroupPool { - // Use a DynamicResourcePool because it gives out reference counted handles - // which makes interacting with buffer/textures easier. - // - // On the flipside if someone requests the exact same bind group again as before, - // they'll get a new one which is unnecessary. But this is *very* unlikely to ever happen. - inner: DynamicResourcePool, + // Wrapped in RwLock so we can run maintenance each pass without requiring &mut self on device + inner: RwLock>, } impl Default for BindGroupPool { @@ -101,7 +98,7 @@ impl Default for BindGroupPool { impl BindGroupPool { pub fn new() -> Self { Self { - inner: DynamicResourcePool::default(), + inner: RwLock::new(DynamicResourcePool::default()), } } @@ -118,7 +115,7 @@ impl BindGroupPool { .collect() }; - let resource = self.inner.get_or_create(desc, |desc| { + let resource = self.inner.read().get_or_create(desc, |desc| { let mut buffer_index = 0; let entries = desc @@ -155,7 +152,11 @@ impl BindGroupPool { } } - pub fn begin_pass(&mut self, pass_index: u64) { - self.inner.begin_pass(pass_index, |_res| {}); + pub fn num_resources(&self) -> usize { + self.inner.read().num_resources() + } + + pub fn begin_pass(&self, pass_index: u64) { + self.inner.write().begin_pass(pass_index, |_res| {}); } } diff --git a/crates/ratchet-core/src/gpu/pools/buffer_pool.rs b/crates/piston-core/src/gpu/pools/buffer_pool.rs similarity index 79% rename from crates/ratchet-core/src/gpu/pools/buffer_pool.rs rename to crates/piston-core/src/gpu/pools/buffer_pool.rs index 97a45459..77c0bf9c 100644 --- a/crates/ratchet-core/src/gpu/pools/buffer_pool.rs +++ b/crates/piston-core/src/gpu/pools/buffer_pool.rs @@ -3,8 +3,8 @@ use std::sync::Arc; // Adapted from https://github.com/rerun-io/rerun MIT licensed use super::{DynamicResource, DynamicResourcePool, DynamicResourcesDesc, PoolError}; use crate::{ - gpu::{WgpuDevice, MIN_STORAGE_BUFFER_SIZE}, RawGPUBuffer, + gpu::{MIN_STORAGE_BUFFER_SIZE, WgpuDevice}, }; #[derive(Clone, Hash, PartialEq, Eq, Debug, derive_new::new)] @@ -38,7 +38,7 @@ impl std::ops::Deref for PooledGPUBuffer { impl PartialEq for PooledGPUBuffer { fn eq(&self, other: &Self) -> bool { - self.0.inner.global_id() == other.0.inner.global_id() + self.0.inner == other.0.inner } } @@ -48,7 +48,7 @@ impl DynamicResourcesDesc for BufferDescriptor { } fn allow_reuse(&self) -> bool { - if std::env::var("RATCHET_DEBUG").is_ok() { + if std::env::var("PISTON_DEBUG").is_ok() { false } else { !self.mapped_at_creation @@ -78,6 +78,7 @@ impl BufferPool { desc: &BufferDescriptor, device: &WgpuDevice, immediate: bool, + vram_limit: Option, ) -> PooledGPUBuffer { let size = if (desc.size as usize) < MIN_STORAGE_BUFFER_SIZE { //All buffers must be minimum 16 bytes @@ -85,7 +86,7 @@ impl BufferPool { } else { //Round all buffers to 4 bytes, as any buffer may be read back to the CPU, which //requires a copy - if desc.size % wgpu::COPY_BUFFER_ALIGNMENT == 0 { + if desc.size.is_multiple_of(wgpu::COPY_BUFFER_ALIGNMENT) { desc.size } else { desc.size + wgpu::COPY_BUFFER_ALIGNMENT - (desc.size % wgpu::COPY_BUFFER_ALIGNMENT) @@ -100,6 +101,14 @@ impl BufferPool { PooledGPUBuffer(self.inner.get_or_create(&descriptor, |descriptor| { let (size, usage, mapped_at_creation) = descriptor.fields(); + let total_size = self.inner.total_resource_size_in_bytes(); + if let Some(vram_limit) = vram_limit + && total_size + size > vram_limit { + panic!( + "VRAM limit exceeded: attempted to allocate buffer of size {size} bytes, \ + which would exceed the VRAM limit of {vram_limit} bytes (current usage: {total_size} bytes)" + ); + } let buf = device.create_buffer(&wgpu::BufferDescriptor { label: None, size, @@ -108,7 +117,7 @@ impl BufferPool { }); if immediate { device.queue().submit(None); - device.poll(wgpu::Maintain::Wait); + device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); } buf })) @@ -138,4 +147,12 @@ impl BufferPool { pub fn total_gpu_size_in_bytes(&self) -> u64 { self.inner.total_resource_size_in_bytes() } + + pub fn reset_usage_peaks(&self) { + self.inner.reset_usage_peaks(); + } + + pub fn peak_total_gpu_size_in_bytes_since_reset(&self) -> u64 { + self.inner.peak_total_resource_size_in_bytes_since_reset() + } } diff --git a/crates/ratchet-core/src/gpu/pools/dynamic_resource_pool.rs b/crates/piston-core/src/gpu/pools/dynamic_resource_pool.rs similarity index 83% rename from crates/ratchet-core/src/gpu/pools/dynamic_resource_pool.rs rename to crates/piston-core/src/gpu/pools/dynamic_resource_pool.rs index e82db987..eeceb1f9 100644 --- a/crates/ratchet-core/src/gpu/pools/dynamic_resource_pool.rs +++ b/crates/piston-core/src/gpu/pools/dynamic_resource_pool.rs @@ -5,7 +5,7 @@ use std::{ collections::hash_map::Entry, fmt::Debug, hash::Hash, - sync::{atomic::AtomicU64, Arc}, + sync::{Arc, atomic::AtomicU64}, }; use crate::HashMap; @@ -58,6 +58,8 @@ pub(super) struct DynamicResourcePool { state: RwLock>, current_pass_index: u64, total_resource_size_in_bytes: AtomicU64, + /// Peak of total_resource_size_in_bytes observed since last reset + peak_total_resource_size_since_reset: AtomicU64, } /// We cannot #derive(Default) as that would require Handle/Desc/Res to implement Default too. @@ -73,6 +75,7 @@ where }), current_pass_index: Default::default(), total_resource_size_in_bytes: AtomicU64::new(0), + peak_total_resource_size_since_reset: AtomicU64::new(0), } } } @@ -90,23 +93,41 @@ where let mut state = self.state.write(); // First check if we can reclaim a resource we have around from a previous pass. - if desc.allow_reuse() { - if let Entry::Occupied(mut entry) = state.last_pass_deallocated.entry(desc.clone()) { - let handle = entry.get_mut().pop().unwrap(); - if entry.get().is_empty() { - entry.remove(); - } - - return state.all_resources[handle].clone(); + if desc.allow_reuse() + && let Entry::Occupied(mut entry) = state.last_pass_deallocated.entry(desc.clone()) + { + let handle = entry.get_mut().pop().unwrap(); + if entry.get().is_empty() { + entry.remove(); } + + return state.all_resources[handle].clone(); } // Otherwise create a new resource let inner_resource = { constructor(desc) }; - self.total_resource_size_in_bytes.fetch_add( - desc.resource_size_in_bytes(), - std::sync::atomic::Ordering::Relaxed, - ); + let added_size = desc.resource_size_in_bytes(); + let prev_total = self + .total_resource_size_in_bytes + .fetch_add(added_size, std::sync::atomic::Ordering::Relaxed); + let new_total = prev_total + added_size; + // Update peak if this allocation increased the total beyond the current peak + let mut observed_peak = self + .peak_total_resource_size_since_reset + .load(std::sync::atomic::Ordering::Relaxed); + while new_total > observed_peak { + match self + .peak_total_resource_size_since_reset + .compare_exchange_weak( + observed_peak, + new_total, + std::sync::atomic::Ordering::Relaxed, + std::sync::atomic::Ordering::Relaxed, + ) { + Ok(_) => break, + Err(current) => observed_peak = current, + } + } let handle = state.all_resources.insert_with_key(|handle| { Arc::new(DynamicResource { @@ -155,11 +176,13 @@ where for (desc, resources) in state.last_pass_deallocated.drain() { for resource in resources { let Some(removed_resource) = state.all_resources.remove(resource) else { - debug_assert!(false, "a resource was marked as destroyed last pass that we no longer kept track of"); + debug_assert!( + false, + "a resource was marked as destroyed last pass that we no longer kept track of" + ); continue; }; update_stats(&desc); - log::debug!("Dropping resource {:?}", desc); destructor(&removed_resource); } } @@ -203,6 +226,21 @@ where self.total_resource_size_in_bytes .load(std::sync::atomic::Ordering::Relaxed) } + + /// Reset peak tracking to the current total so the window starts from baseline usage + pub fn reset_usage_peaks(&self) { + let current_total = self + .total_resource_size_in_bytes + .load(std::sync::atomic::Ordering::Relaxed); + self.peak_total_resource_size_since_reset + .store(current_total, std::sync::atomic::Ordering::Relaxed); + } + + /// Get the peak total resource size (in bytes) observed since the last reset + pub fn peak_total_resource_size_in_bytes_since_reset(&self) -> u64 { + self.peak_total_resource_size_since_reset + .load(std::sync::atomic::Ordering::Relaxed) + } } #[cfg(test)] diff --git a/crates/ratchet-core/src/gpu/pools/kernel_module_pool.rs b/crates/piston-core/src/gpu/pools/kernel_module_pool.rs similarity index 83% rename from crates/ratchet-core/src/gpu/pools/kernel_module_pool.rs rename to crates/piston-core/src/gpu/pools/kernel_module_pool.rs index 7230af1a..6978748d 100644 --- a/crates/ratchet-core/src/gpu/pools/kernel_module_pool.rs +++ b/crates/piston-core/src/gpu/pools/kernel_module_pool.rs @@ -1,4 +1,4 @@ -use crate::{Kernel, KernelKey, KernelSource, OperationError, Tensor, WgpuDevice, WorkgroupSize}; +use crate::{Kernel, KernelKey, KernelSource, OpTensor, OperationError, WgpuDevice, WorkgroupSize}; use super::static_resource_pool::{StaticResourcePool, StaticResourcePoolReadLockAccessor}; use std::hash::Hash; @@ -18,7 +18,7 @@ impl KernelModuleDesc { &self, op: &O, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { op.build_kernel(inplace, dst, workgroup_size) @@ -42,7 +42,7 @@ impl KernelModulePool { desc: &KernelModuleDesc, kernel: &K, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, device: &WgpuDevice, ) -> KernelModuleHandle { @@ -57,11 +57,16 @@ impl KernelModulePool { source: source.into(), }; - if std::env::var("RATCHET_CHECKED").is_ok() { + if std::env::var("PISTON_CHECKED").is_ok() { log::warn!("Using checked shader compilation"); device.create_shader_module(shader_module_desc) } else { - unsafe { device.create_shader_module_unchecked(shader_module_desc) } + unsafe { + device.create_shader_module_trusted( + shader_module_desc, + wgpu::ShaderRuntimeChecks::unchecked(), + ) + } } }) } diff --git a/crates/ratchet-core/src/gpu/pools/mod.rs b/crates/piston-core/src/gpu/pools/mod.rs similarity index 100% rename from crates/ratchet-core/src/gpu/pools/mod.rs rename to crates/piston-core/src/gpu/pools/mod.rs diff --git a/crates/ratchet-core/src/gpu/pools/pipeline_layout_pool.rs b/crates/piston-core/src/gpu/pools/pipeline_layout_pool.rs similarity index 97% rename from crates/ratchet-core/src/gpu/pools/pipeline_layout_pool.rs rename to crates/piston-core/src/gpu/pools/pipeline_layout_pool.rs index 05630df6..a19f492b 100644 --- a/crates/ratchet-core/src/gpu/pools/pipeline_layout_pool.rs +++ b/crates/piston-core/src/gpu/pools/pipeline_layout_pool.rs @@ -1,10 +1,10 @@ -use crate::{gpu::WgpuDevice, RVec}; +use crate::{RVec, gpu::WgpuDevice}; use super::{ + BindGroupLayoutHandle, static_resource_pool::{ StaticResourcePool, StaticResourcePoolAccessor as _, StaticResourcePoolReadLockAccessor, }, - BindGroupLayoutHandle, }; slotmap::new_key_type! { pub struct PipelineLayoutHandle; } diff --git a/crates/ratchet-core/src/gpu/pools/pipeline_pool.rs b/crates/piston-core/src/gpu/pools/pipeline_pool.rs similarity index 93% rename from crates/ratchet-core/src/gpu/pools/pipeline_pool.rs rename to crates/piston-core/src/gpu/pools/pipeline_pool.rs index 3b982a45..2c07fbc3 100644 --- a/crates/ratchet-core/src/gpu/pools/pipeline_pool.rs +++ b/crates/piston-core/src/gpu/pools/pipeline_pool.rs @@ -1,4 +1,4 @@ -use crate::{gpu::WgpuDevice, KernelKey, KernelModuleHandle}; +use crate::{KernelKey, KernelModuleHandle, gpu::WgpuDevice}; use super::{ PipelineLayoutHandle, StaticResourcePool, StaticResourcePoolAccessor, @@ -69,4 +69,8 @@ impl ComputePipelinePool { ) -> StaticResourcePoolReadLockAccessor<'_, ComputePipelineHandle, wgpu::ComputePipeline> { self.inner.resources() } + + pub fn num_resources(&self) -> usize { + self.inner.num_resources() + } } diff --git a/crates/ratchet-core/src/gpu/pools/static_resource_pool.rs b/crates/piston-core/src/gpu/pools/static_resource_pool.rs similarity index 100% rename from crates/ratchet-core/src/gpu/pools/static_resource_pool.rs rename to crates/piston-core/src/gpu/pools/static_resource_pool.rs diff --git a/crates/ratchet-core/src/gpu/profiler.rs b/crates/piston-core/src/gpu/profiler.rs similarity index 65% rename from crates/ratchet-core/src/gpu/profiler.rs rename to crates/piston-core/src/gpu/profiler.rs index 2c12e9f9..5fee58ad 100644 --- a/crates/ratchet-core/src/gpu/profiler.rs +++ b/crates/piston-core/src/gpu/profiler.rs @@ -1,17 +1,24 @@ -#![cfg(feature = "gpu-profiling")] +use crate::{HashMap, TensorId}; +#[cfg(feature = "gpu-profiling")] use itertools::Itertools; -use std::collections::HashMap; -use tabled::settings::{object::Rows, Alignment, Modify, Panel, Style}; +use maybe_async::maybe_async; +#[cfg(feature = "gpu-profiling")] +use tabled::settings::{Alignment, Modify, Panel, Style, object::Rows}; +#[cfg(feature = "gpu-profiling")] use tabled::{Table, Tabled}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; use wgpu::QuerySet; use super::WgpuDevice; +#[cfg(feature = "gpu-profiling")] //used for formatting table cells fn float2(n: &f64) -> String { format!("{:.2}", n) } +#[cfg(feature = "gpu-profiling")] #[derive(Tabled)] struct SummaryTableEntry { #[tabled(rename = "Op Type")] @@ -26,6 +33,7 @@ struct SummaryTableEntry { percent_runtime: f64, } +#[cfg(feature = "gpu-profiling")] pub fn build_summary_table( elapsed_map: HashMap, op_counts: HashMap, @@ -55,6 +63,7 @@ pub fn build_summary_table( .to_owned() } +#[cfg(feature = "gpu-profiling")] #[derive(Tabled)] struct IndividualTableEntry { #[tabled(rename = "Node ID")] @@ -67,6 +76,7 @@ struct IndividualTableEntry { percent_runtime: f64, } +#[cfg(feature = "gpu-profiling")] pub fn build_individual_table(elapsed_map: HashMap) -> Table { let total_elapsed: usize = elapsed_map.values().map(|(_, e)| e).sum(); @@ -92,6 +102,24 @@ pub fn build_individual_table(elapsed_map: HashMap) -> T .to_owned() } +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))] +#[derive(Debug, Clone)] +pub struct ExportedTensorProfilingEntry { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))] + pub id: TensorId, + pub kernel_name: String, + pub elapsed: usize, +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +impl ExportedTensorProfilingEntry { + #[wasm_bindgen(getter)] + pub fn id(&self) -> TensorId { + self.id + } +} + pub struct Profiler { device: WgpuDevice, query_set: QuerySet, @@ -99,7 +127,10 @@ pub struct Profiler { destination_buffer: wgpu::Buffer, query_index: u32, timestamp_period: f32, + #[cfg(feature = "gpu-profiling")] query_to_node: HashMap<(u32, u32), (usize, String)>, + #[cfg(not(feature = "gpu-profiling"))] + query_to_tensor: HashMap<(u32, u32), (TensorId, String)>, } impl Profiler { @@ -133,16 +164,19 @@ impl Profiler { destination_buffer, query_index: 0, timestamp_period, - query_to_node: HashMap::with_capacity(count as usize), + #[cfg(feature = "gpu-profiling")] + query_to_node: HashMap::with_capacity_and_hasher(count as usize, Default::default()), + #[cfg(not(feature = "gpu-profiling"))] + query_to_tensor: HashMap::with_capacity_and_hasher(count as usize, Default::default()), } } - #[cfg(feature = "gpu-profiling")] pub fn create_timestamp_queries( &mut self, - id: usize, + #[cfg(feature = "gpu-profiling")] id: usize, + #[cfg(not(feature = "gpu-profiling"))] id: TensorId, name: &str, - ) -> wgpu::ComputePassTimestampWrites { + ) -> wgpu::ComputePassTimestampWrites<'_> { let beginning_index = self.query_index; self.query_index += 1; let end_index = self.query_index; @@ -154,13 +188,17 @@ impl Profiler { end_of_pass_write_index: Some(end_index), }; + #[cfg(feature = "gpu-profiling")] self.query_to_node .insert((beginning_index, end_index), (id, name.to_string())); + #[cfg(not(feature = "gpu-profiling"))] + self.query_to_tensor + .insert((beginning_index, end_index), (id, name.to_string())); timestamp_writes } - pub fn resolve(&self, encoder: &mut wgpu::CommandEncoder) { + pub fn resolve(&mut self, encoder: &mut wgpu::CommandEncoder) { encoder.resolve_query_set( &self.query_set, 0..self.query_index, @@ -176,6 +214,7 @@ impl Profiler { ); } + #[cfg(feature = "gpu-profiling")] fn summary_table(&self, timestamps: &[u64]) { let mut elapsed_map = HashMap::new(); let mut op_counts = HashMap::new(); @@ -198,6 +237,7 @@ impl Profiler { println!("{}", build_summary_table(elapsed_map, op_counts)); } + #[cfg(feature = "gpu-profiling")] fn node_table(&self, timestamps: &[u64]) { let mut node_map = HashMap::new(); for (idx, (begin, end)) in timestamps.iter().tuples().enumerate() { @@ -215,11 +255,12 @@ impl Profiler { println!("{}", build_individual_table(node_map)); } - pub fn read_timestamps(&self, summary: bool) { + #[cfg(feature = "gpu-profiling")] + pub fn print_timestamps(&self, summary: bool) { self.destination_buffer .slice(..) .map_async(wgpu::MapMode::Read, |_| ()); - self.device.poll(wgpu::Maintain::Wait); + self.device.poll(wgpu::Maintain::Wait).unwrap(); let timestamp_view = self .destination_buffer .slice( @@ -235,4 +276,53 @@ impl Profiler { self.node_table(timestamps); } } + + #[cfg(not(feature = "gpu-profiling"))] + #[maybe_async] + pub async fn read_timestamps(&self) -> HashMap { + // I guess in principle we could do all of this in a shader... + #[cfg(target_arch = "wasm32")] + let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel(); + #[cfg(not(target_arch = "wasm32"))] + let (tx, rx) = std::sync::mpsc::channel(); + self.destination_buffer + .slice(..) + .map_async(wgpu::MapMode::Read, move |_| { + tx.send(()).expect("Failed to sync for profiling"); + }); + self.device + .poll(wgpu::PollType::wait_indefinitely()) + .unwrap(); + #[cfg(target_arch = "wasm32")] + rx.receive().await.unwrap(); + #[cfg(not(target_arch = "wasm32"))] + rx.recv().unwrap(); + let timestamp_view = self + .destination_buffer + .slice( + ..(std::mem::size_of::() * self.query_index as usize) as wgpu::BufferAddress, + ) + .get_mapped_range(); + + let timestamps: &[u64] = bytemuck::cast_slice(×tamp_view); + let mut exported_tensors = HashMap::default(); + for (idx, chunk) in timestamps.chunks(2).enumerate() { + if let &[begin, end] = chunk { + let elapsed_ns = (end - begin) as f64 * self.timestamp_period as f64; + let (id, name) = self + .query_to_tensor + .get(&(idx as u32 * 2, idx as u32 * 2 + 1)) + .unwrap(); + exported_tensors.insert( + *id, + ExportedTensorProfilingEntry { + id: *id, + kernel_name: name.clone(), + elapsed: elapsed_ns as usize, + }, + ); + } + } + exported_tensors + } } diff --git a/crates/ratchet-core/src/gpu/uniform.rs b/crates/piston-core/src/gpu/uniform.rs similarity index 94% rename from crates/ratchet-core/src/gpu/uniform.rs rename to crates/piston-core/src/gpu/uniform.rs index 8515b737..063f5e49 100644 --- a/crates/ratchet-core/src/gpu/uniform.rs +++ b/crates/piston-core/src/gpu/uniform.rs @@ -1,8 +1,9 @@ use std::num::NonZeroU64; use crate::{ + OperationError, gpu::{BindGroupEntry, BindGroupLayoutDescriptor}, - rvec, OperationError, + rvec, }; use super::{BindGroupDescriptor, GpuBindGroup, PooledGPUBuffer, WgpuDevice}; @@ -34,6 +35,10 @@ impl CpuUniform { self.0.into_inner() } + pub fn clone_inner(&self) -> Vec { + self.0.as_ref().clone() + } + /// Consumes the CPU repr of the uniform buffer and writes to the GPU. pub(crate) fn into_gpu(self, device: &WgpuDevice) -> Result { let buf = device.create_uniform_init(self); diff --git a/crates/ratchet-core/src/gpu/wgsl/access_granularity.rs b/crates/piston-core/src/gpu/wgsl/access_granularity.rs similarity index 100% rename from crates/ratchet-core/src/gpu/wgsl/access_granularity.rs rename to crates/piston-core/src/gpu/wgsl/access_granularity.rs diff --git a/crates/ratchet-core/src/gpu/wgsl/dtype.rs b/crates/piston-core/src/gpu/wgsl/dtype.rs similarity index 83% rename from crates/ratchet-core/src/gpu/wgsl/dtype.rs rename to crates/piston-core/src/gpu/wgsl/dtype.rs index c7aa0cd4..6dab7f29 100644 --- a/crates/ratchet-core/src/gpu/wgsl/dtype.rs +++ b/crates/piston-core/src/gpu/wgsl/dtype.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Display}; /// Supported data types in WGSL. /// -/// This can be mapped to and from the Ratchet DType. +/// This can be mapped to and from the Piston DType. pub trait WgslDType: Debug + Display + Default + Copy + num_traits::Num + num_traits::Zero { const DT: &'static str; const MIN: Self; @@ -17,7 +17,7 @@ impl WgslDType for f32 { const MIN: Self = -3e10; //ranges for wgsl and rust are diff fn render(&self) -> String { - format!("{}f", self) + format!("{self}f") } } @@ -26,7 +26,7 @@ impl WgslDType for f16 { const MIN: Self = f16::MIN; fn render(&self) -> String { - format!("{}h", self) + format!("{self}h") } } @@ -35,7 +35,7 @@ impl WgslDType for i32 { const MIN: Self = i32::MIN; fn render(&self) -> String { - format!("{}i", self) + format!("{self}i") } } @@ -44,6 +44,6 @@ impl WgslDType for u32 { const MIN: Self = u32::MIN; fn render(&self) -> String { - format!("{}u", self) + format!("{self}u") } } diff --git a/crates/ratchet-core/src/gpu/wgsl/kernel.rs b/crates/piston-core/src/gpu/wgsl/kernel.rs similarity index 92% rename from crates/ratchet-core/src/gpu/wgsl/kernel.rs rename to crates/piston-core/src/gpu/wgsl/kernel.rs index 6e042832..afe8964f 100644 --- a/crates/ratchet-core/src/gpu/wgsl/kernel.rs +++ b/crates/piston-core/src/gpu/wgsl/kernel.rs @@ -2,11 +2,12 @@ use crate::HashMap; use std::fmt::Debug; use crate::{ - BindGroupLayoutDescriptor, CpuUniform, KernelElement, KernelKey, KernelSource, OperationError, - Tensor, WgslFragment, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, UNIFORM_ALIGN, + BindGroupLayoutDescriptor, CpuUniform, KernelElement, KernelKey, KernelSource, OpTensor, + OperationError, UNIFORM_ALIGN, WgslFragment, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, + Workload, }; -use encase::{internal::WriteInto, ShaderType}; +use encase::{ShaderType, internal::WriteInto}; //Every field must be : ShaderType + WriteInto #[derive(Debug)] @@ -131,7 +132,7 @@ pub trait KernelMetadata { pub trait Kernel: KernelRenderable { /// # Metadata /// - /// Each kernel has zero or more required metadata fields (e.g shape, strides, etc). + /// Each kernel has zero or more required metadata fields (e.g shape, stride, etc). /// This is stored in a uniform buffer, for faster access. /// /// The metadata is limited to 256 bytes per kernel. @@ -160,14 +161,14 @@ pub trait Kernel: KernelRenderable { fn metadata( &self, - dst: &Tensor, + dst: &OpTensor, kernel_element: &KernelElement, ) -> Result; /// # Calculate Dispatch /// /// Determine required amount of workgroups to execute the operation. - fn calculate_dispatch(&self, dst: &Tensor) -> Result; + fn calculate_dispatch(&self, dst: &OpTensor) -> Result; /// # Kernel Key /// @@ -180,8 +181,8 @@ pub trait Kernel: KernelRenderable { &self, workgroup_size: &WorkgroupSize, inplace: bool, - srcs: &[&Tensor], - dst: &Tensor, + srcs: &[&OpTensor], + dst: &OpTensor, kernel_element: &KernelElement, ) -> KernelKey { KernelKey::new( @@ -198,12 +199,12 @@ pub trait Kernel: KernelRenderable { /// # Kernel Element /// /// Determine the largest possible unit data type that can be used (e.g f32, vec2, vec4) - fn kernel_element(&self, dst: &Tensor) -> KernelElement; + fn kernel_element(&self, dst: &OpTensor) -> KernelElement; fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result; } @@ -224,7 +225,7 @@ pub trait KernelRenderable { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result; } diff --git a/crates/ratchet-core/src/gpu/wgsl/kernel_binding.rs b/crates/piston-core/src/gpu/wgsl/kernel_binding.rs similarity index 100% rename from crates/ratchet-core/src/gpu/wgsl/kernel_binding.rs rename to crates/piston-core/src/gpu/wgsl/kernel_binding.rs diff --git a/crates/ratchet-core/src/gpu/wgsl/kernel_builder.rs b/crates/piston-core/src/gpu/wgsl/kernel_builder.rs similarity index 99% rename from crates/ratchet-core/src/gpu/wgsl/kernel_builder.rs rename to crates/piston-core/src/gpu/wgsl/kernel_builder.rs index b94cb58c..ec2b3607 100644 --- a/crates/ratchet-core/src/gpu/wgsl/kernel_builder.rs +++ b/crates/piston-core/src/gpu/wgsl/kernel_builder.rs @@ -116,7 +116,7 @@ impl WgslKernelBuilder { source.write(binding.render().0.as_str()); } source.write(self.main.0.as_str()); - if std::env::var("RATCHET_DUMP_KERNELS").is_ok() { + if std::env::var("PISTON_DUMP_KERNELS").is_ok() { log::warn!("\n{}", source.0); } Ok(source.into()) @@ -173,7 +173,7 @@ impl WgslKernelBuilder { mode: BindingMode, array: Array

, ) { - self.register_binding(BindingType::Storage, mode, name, format!("{}", array)); + self.register_binding(BindingType::Storage, mode, name, format!("{array}")); } /// Cast kernel requires this diff --git a/crates/ratchet-core/src/gpu/wgsl/mod.rs b/crates/piston-core/src/gpu/wgsl/mod.rs similarity index 89% rename from crates/ratchet-core/src/gpu/wgsl/mod.rs rename to crates/piston-core/src/gpu/wgsl/mod.rs index 2f3f779c..62df742e 100644 --- a/crates/ratchet-core/src/gpu/wgsl/mod.rs +++ b/crates/piston-core/src/gpu/wgsl/mod.rs @@ -1,5 +1,5 @@ mod access_granularity; -pub mod dtype; +pub(crate) mod dtype; mod kernel; mod kernel_binding; mod kernel_builder; diff --git a/crates/ratchet-core/src/gpu/workload.rs b/crates/piston-core/src/gpu/workload.rs similarity index 98% rename from crates/ratchet-core/src/gpu/workload.rs rename to crates/piston-core/src/gpu/workload.rs index bf3d2852..45eed314 100644 --- a/crates/ratchet-core/src/gpu/workload.rs +++ b/crates/piston-core/src/gpu/workload.rs @@ -77,7 +77,7 @@ impl WorkgroupCount { /// Divide a number by the indicated dividend, then round up to the next multiple of the dividend if there is a rest. pub fn div_ceil(num: usize, div: usize) -> usize { - num / div + (num % div != 0) as usize + num / div + !num.is_multiple_of(div) as usize } } diff --git a/crates/ratchet-core/src/lib.rs b/crates/piston-core/src/lib.rs similarity index 85% rename from crates/ratchet-core/src/lib.rs rename to crates/piston-core/src/lib.rs index 7a8d6d48..c71dab0b 100644 --- a/crates/ratchet-core/src/lib.rs +++ b/crates/piston-core/src/lib.rs @@ -12,13 +12,13 @@ mod op; mod ops; mod plot; mod quant; +mod scope; mod shape; mod storage; -mod strides; +mod stride; mod tensor; mod tensor_id; pub mod test_utils; -mod variable; pub use backprop::*; pub use compiled_op::*; @@ -33,12 +33,12 @@ pub use op::*; pub use ops::*; pub use quant::*; use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; +pub use scope::*; pub use shape::*; pub use storage::*; -pub use strides::*; +pub use stride::*; pub use tensor::*; pub use tensor_id::*; -pub use variable::*; #[cfg(feature = "plotting")] pub use plot::render_to_file; @@ -59,7 +59,7 @@ macro_rules! rvec { $crate::RVec::from_elem($elem, $n) }); ($($x:expr),*$(,)*) => ({ - let count = 0usize $(+ rvec![@one $x])*; + let count = 0usize $(+ $crate::rvec![@one $x])*; #[allow(unused_mut)] let mut vec = $crate::RVec::new(); if count <= vec.inline_size() { @@ -99,12 +99,12 @@ macro_rules! shape { } pub mod prelude { - pub use crate::{rvec, shape, Device, DeviceRequest, Tensor}; + pub use crate::{Device, DeviceRequest, OpTensor, rvec, shape}; } #[cfg(feature = "pyo3")] pub mod test_util { - use crate::{DType, Tensor}; + use crate::{DType, OpTensor, Tensor}; use half::f16; use regex::Regex; use { @@ -127,10 +127,10 @@ pub mod test_util { Python::with_gil(|py| { let prg = PyModule::from_code(py, &prg, "x.py", "x")?; - let py_tensors = tensors.iter().map(|t| match t.dt() { - DType::F32 => t.to_py::(&py).to_object(py), - DType::I32 => t.to_py::(&py).to_object(py), - DType::F16 => t.to_py::(&py).to_object(py), + let py_tensors = tensors.iter().map(|t| match t.dtype() { + DType::F32 => t.inner_or_source().to_py::(&py).to_object(py), + DType::I32 => t.inner_or_source().to_py::(&py).to_object(py), + DType::F16 => t.inner_or_source().to_py::(&py).to_object(py), _ => unimplemented!(), }); let py_args = py_tensors @@ -138,22 +138,22 @@ pub mod test_util { .collect::>(); let py_args = PyTuple::new(py, py_args); let py_result = prg.getattr(func)?.call1(py_args)?; - let result: Tensor = match dst_dtype { + let result: OpTensor = match dst_dtype { DType::F32 => py_result.extract::<&PyArrayDyn>()?.into(), DType::F16 => py_result.extract::<&PyArrayDyn>()?.into(), DType::I32 => py_result.extract::<&PyArrayDyn>()?.into(), DType::U32 => py_result.extract::<&PyArrayDyn>()?.into(), _ => unimplemented!(), }; - Ok(result) + Ok(result.wrap()) }) } pub fn run_py_prg_multiple( prg: String, - tensors: &[&Tensor], + tensors: &[&OpTensor], args: &[&dyn ToPyObject], - ) -> anyhow::Result> { + ) -> anyhow::Result> { let re = Regex::new(r"def\s+(\w+)\s*\(").unwrap(); let func = match re.captures(&prg) { Some(caps) => caps.get(1).map(|m| m.as_str()).unwrap(), @@ -161,7 +161,7 @@ pub mod test_util { }; Python::with_gil(|py| { let prg = PyModule::from_code(py, &prg, "x.py", "x")?; - let py_tensors = tensors.iter().map(|t| match t.dt() { + let py_tensors = tensors.iter().map(|t| match t.dtype() { DType::F32 => t.to_py::(&py).to_object(py), DType::I32 => t.to_py::(&py).to_object(py), _ => unimplemented!(), @@ -176,7 +176,7 @@ pub mod test_util { let mut tensors = Vec::new(); for item in tuple.iter() { let array: &PyArrayDyn = item.extract()?; - tensors.push(Tensor::from(array)); + tensors.push(OpTensor::from(array)); } Ok(tensors) }) diff --git a/crates/ratchet-core/src/ndarray_ext.rs b/crates/piston-core/src/ndarray_ext.rs similarity index 100% rename from crates/ratchet-core/src/ndarray_ext.rs rename to crates/piston-core/src/ndarray_ext.rs diff --git a/crates/ratchet-core/src/op.rs b/crates/piston-core/src/op.rs similarity index 83% rename from crates/ratchet-core/src/op.rs rename to crates/piston-core/src/op.rs index d11d3e9e..5331a9f9 100644 --- a/crates/ratchet-core/src/op.rs +++ b/crates/piston-core/src/op.rs @@ -1,15 +1,15 @@ #[cfg(feature = "debug")] +use crate::MIN_STORAGE_BUFFER_SIZE; +#[cfg(feature = "debug")] use crate::gpu::BufferUsagesExt; use crate::gpu::{ BindGroupLayoutDescriptor, ComputePipelineDescriptor, CpuUniform, PipelineLayoutDescriptor, PoolError, WgpuDevice, }; -#[cfg(feature = "debug")] -use crate::MIN_STORAGE_BUFFER_SIZE; use crate::{ - ops::*, rvec, CompiledOp, DType, HashMap, InvariantError, Kernel, KernelBuildError, - KernelMetadata, KernelModuleDesc, RVec, Shape, StorageView, Tensor, TensorId, WgslFragment, - WorkgroupSize, Workload, + CompiledOp, DType, HashMap, InvariantError, Kernel, KernelBuildError, KernelMetadata, + KernelModuleDesc, OpTensor, RVec, Shape, StorageView, TensorId, TensorTypeOrScalarEnum, + WgslFragment, WorkgroupSize, Workload, ops::*, rvec, }; #[cfg(feature = "debug")] use slotmap::Key; @@ -43,9 +43,12 @@ pub enum LazyOp { WhereCond(WhereCond), Reduce(Reduce), Gather(Gather), + Multinomial(Multinomial), + Ternary(Ternary), + Lerp(Lerp), // ---- Everything below this line shouldn't exist ---- - FillConstant(FillConstant), - FillRandn(FillRandn), + FillPointwise(FillPointwise), + Bernoulli(Bernoulli), RoPE(RoPE), Alibi(Alibi), Softmax(Softmax), @@ -55,10 +58,13 @@ pub enum LazyOp { Cache(Cache), //Should be a general class IndexAdd(IndexAdd), ScatterAdd(ScatterAdd), - Trilu(Trilu), + Trilu(TriluOp), + Eye(Eye), + OneHot(OneHot), Arange(Arange), Copy(TensorCopy), Detach(Box), //Because the entire graph is lazy, you can't actually detach something without computing the graph in parts + TopK(TopK), } impl LazyOp { @@ -78,14 +84,20 @@ impl LazyOp { LazyOp::WhereCond(w) => w.name(), LazyOp::Reduce(s) => s.name(), LazyOp::Gather(g) => g.name(), + LazyOp::OneHot(o) => o.name(), + LazyOp::Multinomial(m) => m.name(), LazyOp::Conv(c) => c.name(), + LazyOp::Ternary(t) => t.name(), + LazyOp::Lerp(l) => l.name(), LazyOp::Select(s) => s.name(), LazyOp::IndexWrite(iw) => iw.name(), LazyOp::IndexAdd(ia) => ia.name(), LazyOp::ScatterAdd(sa) => sa.name(), LazyOp::Trilu(t) => t.name(), - LazyOp::FillConstant(f) => f.name(), - LazyOp::FillRandn(f) => f.name(), + LazyOp::Eye(e) => e.name(), + LazyOp::TopK(t) => t.name(), + LazyOp::FillPointwise(f) => f.name(), + LazyOp::Bernoulli(b) => b.name(), LazyOp::Arange(a) => a.name(), LazyOp::RoPE(r) => r.name(), LazyOp::Alibi(a) => a.name(), @@ -98,7 +110,7 @@ impl LazyOp { } #[inline(always)] - pub fn srcs(&self) -> RVec<&Tensor> { + pub fn srcs(&self) -> RVec<&OpTensor> { match self { LazyOp::Binary(b) => b.srcs(), LazyOp::Cast(c) => c.srcs(), @@ -116,17 +128,24 @@ impl LazyOp { LazyOp::WhereCond(w) => w.srcs(), LazyOp::Reduce(s) => s.srcs(), LazyOp::Gather(g) => g.srcs(), + LazyOp::OneHot(o) => o.srcs(), + LazyOp::Multinomial(m) => m.srcs(), LazyOp::Conv(c) => c.srcs(), + LazyOp::Ternary(t) => t.srcs(), + LazyOp::Lerp(l) => l.srcs(), LazyOp::Select(s) => s.srcs(), LazyOp::IndexWrite(iw) => iw.srcs(), LazyOp::IndexAdd(ia) => ia.srcs(), LazyOp::ScatterAdd(sa) => sa.srcs(), LazyOp::Trilu(t) => t.srcs(), + LazyOp::Eye(e) => e.srcs(), + LazyOp::TopK(t) => t.srcs(), LazyOp::Cache(c) => c.srcs(), LazyOp::Detach(d) => d.srcs(), LazyOp::View(v) => v.srcs(), LazyOp::Copy(c) => c.srcs(), - LazyOp::FillConstant(_) | LazyOp::FillRandn(_) | LazyOp::Arange(_) | LazyOp::Const => { + LazyOp::Bernoulli(b) => b.srcs(), + LazyOp::FillPointwise(_) | LazyOp::Arange(_) | LazyOp::Const => { rvec![] } //end of the line kid } @@ -150,14 +169,20 @@ impl LazyOp { LazyOp::WhereCond(w) => w.supports_inplace(), LazyOp::Reduce(s) => s.supports_inplace(), LazyOp::Gather(g) => g.supports_inplace(), + LazyOp::OneHot(o) => o.supports_inplace(), + LazyOp::Multinomial(m) => m.supports_inplace(), LazyOp::Conv(c) => c.supports_inplace(), + LazyOp::Ternary(t) => t.supports_inplace(), + LazyOp::Lerp(l) => l.supports_inplace(), LazyOp::Select(s) => s.supports_inplace(), LazyOp::IndexWrite(iw) => iw.supports_inplace(), LazyOp::IndexAdd(ia) => ia.supports_inplace(), LazyOp::ScatterAdd(sa) => sa.supports_inplace(), LazyOp::Trilu(t) => t.supports_inplace(), - LazyOp::FillConstant(fc) => fc.supports_inplace(), - LazyOp::FillRandn(fr) => fr.supports_inplace(), + LazyOp::Eye(e) => e.supports_inplace(), + LazyOp::TopK(t) => t.supports_inplace(), + LazyOp::FillPointwise(f) => f.supports_inplace(), + LazyOp::Bernoulli(b) => b.supports_inplace(), LazyOp::Arange(a) => a.supports_inplace(), LazyOp::Cache(c) => c.supports_inplace(), LazyOp::View(_v) => true, @@ -171,6 +196,32 @@ impl LazyOp { matches!(self, LazyOp::Const) } + // We have to keep this distinct from is_const, because we use is_const to determine if + // something will be excluded from the computation graph. No good for these + // shader-generated parameters. + pub fn can_be_parameter(&self) -> bool { + matches!( + self, + LazyOp::Const | LazyOp::FillPointwise(_) | LazyOp::Eye(_) | LazyOp::Arange(_) + ) + } + + /// Returns true if this operation represents a leaf in the computation graph. + /// + /// Semantics are analogous to PyTorch's `is_leaf`: + /// - First, treat obvious parameter-capable creators as leaves (`can_be_parameter`). + /// - Also treat explicit detach boundaries as leaves (`Detach`). + /// - Finally, include any zero-input op as a leaf. + pub fn is_leaf(&self) -> bool { + if self.can_be_parameter() { + return true; + } + match self { + LazyOp::Detach(_) => true, + _ => self.srcs().is_empty(), + } + } + #[track_caller] pub fn check_invariants(&self) { match self { @@ -185,6 +236,7 @@ impl LazyOp { Reindex::Permute(p) => p.check_invariants(), Reindex::Slice(s) => s.check_invariants(), Reindex::Broadcast(b) => b.check_invariants(), + Reindex::Flip(f) => f.check_invariants(), }, LazyOp::Concat(c) => c.check_invariants(), LazyOp::Norm(n) => n.check_invariants(), @@ -194,14 +246,20 @@ impl LazyOp { LazyOp::WhereCond(w) => w.check_invariants(), LazyOp::Reduce(s) => s.check_invariants(), LazyOp::Gather(g) => g.check_invariants(), + LazyOp::OneHot(o) => o.check_invariants(), + LazyOp::Multinomial(m) => m.check_invariants(), LazyOp::Conv(c) => c.check_invariants(), + LazyOp::Ternary(t) => t.check_invariants(), + LazyOp::Lerp(l) => l.check_invariants(), LazyOp::Select(s) => s.check_invariants(), LazyOp::IndexWrite(iw) => iw.check_invariants(), LazyOp::IndexAdd(ia) => ia.check_invariants(), LazyOp::ScatterAdd(sa) => sa.check_invariants(), LazyOp::Trilu(t) => t.check_invariants(), - LazyOp::FillConstant(f) => f.check_invariants(), - LazyOp::FillRandn(f) => f.check_invariants(), + LazyOp::Eye(e) => e.check_invariants(), + LazyOp::TopK(t) => t.check_invariants(), + LazyOp::FillPointwise(f) => f.check_invariants(), + LazyOp::Bernoulli(b) => b.check_invariants(), LazyOp::Arange(a) => a.check_invariants(), LazyOp::Cache(c) => c.check_invariants(), LazyOp::View(v) => v.check_invariants(), @@ -211,7 +269,7 @@ impl LazyOp { } } - pub(crate) fn ir(&self) -> Ir { + pub fn ir(&self) -> Ir { match self { LazyOp::Binary(b) => b.ir(), LazyOp::Cast(c) => c.ir(), @@ -229,14 +287,20 @@ impl LazyOp { LazyOp::WhereCond(w) => w.ir(), LazyOp::Reduce(s) => s.ir(), LazyOp::Gather(g) => g.ir(), + LazyOp::OneHot(o) => o.ir(), + LazyOp::Multinomial(m) => m.ir(), LazyOp::Conv(c) => c.ir(), + LazyOp::Ternary(t) => t.ir(), + LazyOp::Lerp(l) => l.ir(), LazyOp::Select(s) => s.ir(), LazyOp::IndexWrite(iw) => iw.ir(), LazyOp::IndexAdd(ia) => ia.ir(), LazyOp::ScatterAdd(sa) => sa.ir(), LazyOp::Trilu(t) => t.ir(), - LazyOp::FillConstant(f) => f.ir(), - LazyOp::FillRandn(f) => f.ir(), + LazyOp::Eye(e) => e.ir(), + LazyOp::TopK(t) => t.ir(), + LazyOp::FillPointwise(f) => f.ir(), + LazyOp::Bernoulli(b) => b.ir(), LazyOp::Arange(a) => a.ir(), LazyOp::Cache(c) => c.ir(), LazyOp::View(v) => v.ir(), @@ -284,20 +348,20 @@ impl KernelKey { pub fn new( stem: &str, - inputs: &[&Tensor], - output: &Tensor, + inputs: &[&OpTensor], + output: &OpTensor, workgroup_size: &WorkgroupSize, inplace: bool, kernel_element: &KernelElement, additional: Option<&str>, ) -> Self { - let input_dts = inputs.iter().map(|t| t.dt().as_str()); + let input_dtypes = inputs.iter().map(|t| t.dtype().as_str()); let inplace_str = if inplace { "ip" } else { "oop" }; let key_parts: Vec> = vec![ Cow::Borrowed(stem), - Cow::Owned(input_dts.collect::>().join("_")), - Cow::Owned(output.dt().to_string()), + Cow::Owned(input_dtypes.collect::>().join("_")), + Cow::Owned(output.dtype().to_string()), Cow::Owned(workgroup_size.as_key()), Cow::Borrowed(inplace_str), Cow::Borrowed(additional.unwrap_or("")), @@ -430,7 +494,7 @@ fn hash_ir_value( // to be true... IrValue::Vec(vec) => { for (i, value) in vec.iter().enumerate() { - hash_key(&format!("{}", i), state); + hash_key(&format!("{i}"), state); hash_ir_value(value, state, tensor_hashes, compile_key); } } @@ -543,12 +607,12 @@ impl> From> for IrValue { } } -impl From for IrValue { - fn from(value: Tensor) -> Self { +impl From for IrValue { + fn from(value: OpTensor) -> Self { IrValue::Tensor(IrTensorValue { id: value.id(), shape: value.shape().clone(), - dtype: value.dt(), + dtype: value.dtype(), }) } } @@ -615,7 +679,7 @@ macro_rules! impl_irvalue_for_rvec { } impl_irvalue_for_rvec!(RVec>); -impl_irvalue_for_rvec!(RVec); +impl_irvalue_for_rvec!(RVec); impl_irvalue_for_rvec!(RVec); impl_irvalue_for_rvec!(Shape); @@ -637,8 +701,17 @@ impl From for IrValue { } } +impl> From> for IrValue { + fn from(value: TensorTypeOrScalarEnum) -> Self { + match value { + TensorTypeOrScalarEnum::Tensor(tensor) => tensor.into(), + TensorTypeOrScalarEnum::Scalar(scalar) => scalar.into(), + } + } +} + pub struct ComputeCompileKey<'a> { - pub dst: &'a Tensor, + pub dst: &'a OpTensor, pub workload: Workload, pub key: KernelKey, pub can_inplace: bool, @@ -646,8 +719,8 @@ pub struct ComputeCompileKey<'a> { } pub struct CopyCompileKey<'a> { - pub src: &'a Tensor, - pub dst: &'a Tensor, + pub src: &'a OpTensor, + pub dst: &'a OpTensor, } pub enum GpuCompileKey<'a> { @@ -683,11 +756,11 @@ pub trait Operation: OpGuards + IrFields + Debug + 'static { } /// # Compute View /// - /// Determine the type, shape & strides of the resultant tensor. + /// Determine the type, shape & stride of the resultant tensor. fn compute_view(&self) -> Result; /// # Source Tensors - fn srcs(&self) -> RVec<&Tensor>; + fn srcs(&self) -> RVec<&OpTensor>; /// # Supports Inplace /// @@ -723,7 +796,7 @@ pub trait GPUOperation: Operation { fn create_gpu_compile_key<'a>( &self, - dst: &'a Tensor, + dst: &'a OpTensor, can_inplace: bool, uniform: &mut CpuUniform, ) -> Result, OperationError> { @@ -744,8 +817,8 @@ pub trait GPUOperation: Operation { let metadata = kernel.metadata(dst, &kernel_element)?; let offset = metadata.write(uniform)?; - log::debug!("Kernel key: {}", key); - log::debug!("Can inplace: {}", can_inplace); + log::debug!("Kernel key: {key}"); + log::debug!("Can inplace: {can_inplace}"); Ok(ComputeCompileKey { dst, @@ -760,8 +833,8 @@ pub trait GPUOperation: Operation { &self, gpu_compile_key: &ComputeCompileKey<'a>, device: &'a WgpuDevice, - #[cfg(feature = "debug")] debug: bool, - #[cfg(not(feature = "debug"))] _debug: bool, + // TODO(vinhowe): We should remove this + _debug: bool, ) -> Result { let ComputeCompileKey { dst, @@ -833,7 +906,8 @@ pub trait GPUOperation: Operation { { if bind_group_entry.handle == other_bind_group_entry.handle { assert_eq!( - layout_entry.read_only, other_layout_entry.read_only, + layout_entry.read_only, + other_layout_entry.read_only, "Found conflicting read_only values for the same buffer handle: {:?} at index {} has read_only={} but {:?} at index {} has read_only={} ({:?})", bind_group_entry.handle, i, @@ -841,7 +915,12 @@ pub trait GPUOperation: Operation { other_bind_group_entry.handle, j, other_layout_entry.read_only, - storage_bind_group.descriptor().entries.iter().map(|e| e.handle.data()).collect::>() + storage_bind_group + .descriptor() + .entries + .iter() + .map(|e| e.handle.data()) + .collect::>() ); } } @@ -891,8 +970,9 @@ pub trait GPUOperation: Operation { storage_bind_groups, *offset as _, kernel_src_desc.key, - #[cfg(feature = "debug")] - debug_buffer, + // TODO(vinhowe): Figure out how to handle when this should be None + Some(dst.id()), + None, #[cfg(feature = "debug")] debug_input_buffers, #[cfg(feature = "debug")] diff --git a/crates/ratchet-core/src/ops/affine.rs b/crates/piston-core/src/ops/affine.rs similarity index 77% rename from crates/ratchet-core/src/ops/affine.rs rename to crates/piston-core/src/ops/affine.rs index eaad1e7e..62700264 100644 --- a/crates/ratchet-core/src/ops/affine.rs +++ b/crates/piston-core/src/ops/affine.rs @@ -2,18 +2,19 @@ use derive_new::new; use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, StorageView, - Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Vec2, + Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, }; #[derive(new, Debug, Clone, IrFields)] pub struct Affine { - pub src: Tensor, + pub src: OpTensor, pub mul: f32, pub add: f32, } @@ -30,7 +31,7 @@ impl OpGuards for Affine { fn check_dtypes(&self) { let a = &self.src; - assert!(matches!(a.dt(), crate::DType::F32)); + assert!(matches!(a.dtype(), crate::DType::F32 | crate::DType::F16)); } } @@ -44,7 +45,7 @@ impl Operation for Affine { } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.src] } @@ -85,7 +86,7 @@ impl KernelRenderable for AffineKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -103,7 +104,7 @@ impl KernelRenderable for AffineKernels { kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); let N = (P::W as u32).render(); - let dt = P::render_type(); + let dtype = P::render_type(); kernel_builder.write_main(wgsl! { let x_offset = workgroup_id.x * 64u; @@ -113,13 +114,25 @@ impl KernelRenderable for AffineKernels { } }); + let mul_rep = if dst.dtype() == DType::F16 { + wgsl!(f16(metadata.mul)) + } else { + wgsl!(metadata.mul) + }; + + let add_rep = if dst.dtype() == DType::F16 { + wgsl!(f16(metadata.add)) + } else { + wgsl!(metadata.add) + }; + let apply = if inplace { wgsl! { let val = X[index]; - X[index] = fma(val, 'dt(metadata.mul), 'dt(metadata.add)); + X[index] = fma(val, 'dtype('mul_rep), 'dtype('add_rep)); } } else { - wgsl! { Y[index] = fma(X[index], 'dt(metadata.mul), 'dt(metadata.add)); } + wgsl! { Y[index] = fma(X[index], 'dtype('mul_rep), 'dtype('add_rep)); } }; kernel_builder.write_main(apply); Ok(kernel_builder.build()?) @@ -135,7 +148,7 @@ impl Kernel for AffineKernels { } } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { let AffineKernels::Standard(inner) = self; Ok(Workload::std( inner.src.shape().numel(), @@ -154,7 +167,7 @@ impl Kernel for AffineKernels { } } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let AffineKernels::Standard(inner) = self; Ok(AffineMeta { numel: inner.src.shape().numel() as u32, @@ -163,18 +176,18 @@ impl Kernel for AffineKernels { }) } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { let AffineKernels::Standard(inner) = self; - let a_rank = inner.src.shape().rank(); + let a_rank = inner.src.shape().dim(); let N = if a_rank > 0 { inner.src.shape()[a_rank - 1] } else { 1 }; - if N % 4 == 0 { + if N.is_multiple_of(4) { KernelElement::Vec4 - } else if N % 2 == 0 { + } else if N.is_multiple_of(2) { KernelElement::Vec2 } else { KernelElement::Scalar @@ -184,12 +197,12 @@ impl Kernel for AffineKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); let AffineKernels::Standard(inner) = self; - match (inner.src.dt(), &kernel_element) { + match (inner.src.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -210,7 +223,7 @@ impl Kernel for AffineKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - inner.src.dt(), + inner.src.dtype(), kernel_element ))), } @@ -220,9 +233,9 @@ impl Kernel for AffineKernels { #[cfg(all(test, feature = "pyo3"))] mod tests { use proptest::arbitrary::any; - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; - use crate::{shape, test_util::run_py_prg, Device, DeviceRequest, Tensor}; + use crate::{Device, DeviceRequest, Tensor, randn, test_util::run_py_prg}; fn ground_truth(a: &Tensor, mul: f32, add: f32) -> anyhow::Result { let prg = r#" @@ -232,20 +245,20 @@ def affine(a, mul, add): "# .to_string(); - run_py_prg(prg.to_string(), &[a], &[&mul, &add], a.dt()) + run_py_prg(prg.to_string(), &[a], &[&mul, &add], a.dtype()) } fn run_affine_trial(problem: AffineProblem, device: Device) { let AffineProblem { B, M, N, add, mul } = problem; - let a = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); + let a = randn((B, M, N), None, None, Default::default()).unwrap(); let ground = ground_truth(&a, mul, add).unwrap(); let a_gpu = a.to(&device).unwrap(); let b = a_gpu.affine(mul, add).unwrap(); let ours = b.to(&Device::CPU).unwrap(); - println!("ours = {:?}", ours); - println!("ground = {:?}", ground); + println!("ours = {ours:?}"); + println!("ground = {ground:?}"); ground.all_close(&ours, 1e-4, 1e-4).unwrap(); } @@ -266,10 +279,7 @@ def affine(a, mul, add): #[proptest(cases = 8)] fn test_affine(prob: AffineProblem) { let AffineProblem { B, M, N, mul, add } = prob; - println!( - "B = {}, M = {}, N = {}, mul = {}, add = {}", - B, M, N, mul, add - ); + println!("B = {B}, M = {M}, N = {N}, mul = {mul}, add = {add}"); let device = Device::request_device(DeviceRequest::GPU).unwrap(); run_affine_trial(prob, device); } diff --git a/crates/ratchet-core/src/ops/alibi.rs b/crates/piston-core/src/ops/alibi.rs similarity index 86% rename from crates/ratchet-core/src/ops/alibi.rs rename to crates/piston-core/src/ops/alibi.rs index 2c70b3c3..17db16fc 100644 --- a/crates/ratchet-core/src/ops/alibi.rs +++ b/crates/piston-core/src/ops/alibi.rs @@ -1,20 +1,21 @@ use derive_new::new; use encase::ShaderType; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, + WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, gpu::{BindGroupLayoutDescriptor, WorkgroupCount}, - rvec, wgc, wgs, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, StorageView, - Tensor, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + rvec, wgc, wgs, }; /// Implements "alibi" (Attention Linear Bias) #[derive(new, Debug, Clone, IrFields)] pub struct Alibi { /// The input tensor, assumed to be rank 3: [B, n_head, seq]. - pub input: Tensor, + pub input: OpTensor, /// The maximum bias to be distributed across heads. max_bias: f32, } @@ -24,15 +25,14 @@ impl OpGuards for Alibi { let shape = self.input.shape(); assert!( shape.len() == 3, - "Alibi expects a 3D shape [B, n_head, seq], got shape={:?}", - shape + "Alibi expects a 3D shape [B, n_head, seq], got shape={shape:?}" ); } fn check_dtypes(&self) { // For simplicity, require float32 for now. assert!( - self.input.dt() == DType::F32, + self.input.dtype() == DType::F32, "Alibi only supports F32 for now" ); } @@ -48,7 +48,7 @@ impl Operation for Alibi { Ok(self.input.storage_view().clone()) } - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.input] } @@ -105,7 +105,7 @@ impl KernelRenderable for AlibiKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -122,7 +122,7 @@ impl KernelRenderable for AlibiKernels { self.register_bindings::

(&mut kernel_builder, inplace)?; kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - let dt = P::render_type(); + let dtype = P::render_type(); let kernel = if inplace { wgsl! { @@ -145,7 +145,7 @@ impl KernelRenderable for AlibiKernels { m_k = pow(metadata.m1, exponent); } - in[i] = in[i] + 'dt(col) * m_k; + in[i] = in[i] + 'dtype(col) * m_k; } } else { wgsl! { @@ -168,7 +168,7 @@ impl KernelRenderable for AlibiKernels { m_k = pow(metadata.m1, exponent); } - out[i] = in[i] + 'dt(col) * m_k; + out[i] = in[i] + 'dtype(col) * m_k; } }; @@ -197,7 +197,7 @@ impl Kernel for AlibiKernels { } } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let AlibiKernels::Standard(inner) = self; let shape = inner.input.shape(); let b = shape[0]; @@ -222,11 +222,11 @@ impl Kernel for AlibiKernels { )) } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { KernelElement::Scalar } - fn calculate_dispatch(&self, _dst: &Tensor) -> Result { + fn calculate_dispatch(&self, _dst: &OpTensor) -> Result { const WGSX: usize = 16; const WGSY: usize = 16; const WGSZ: usize = 1; @@ -254,19 +254,19 @@ impl Kernel for AlibiKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); let AlibiKernels::Standard(inner) = self; - match (inner.input.dt(), &kernel_element) { + match (inner.input.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } // TODO: extend to F16/vector types _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - inner.input.dt(), + inner.input.dtype(), kernel_element ))), } @@ -275,8 +275,8 @@ impl Kernel for AlibiKernels { #[cfg(all(test, feature = "pyo3"))] mod tests { - use crate::{shape, test_util::run_py_prg, DType, Device, DeviceRequest, Tensor}; - use test_strategy::{proptest, Arbitrary}; + use crate::{Device, DeviceRequest, Tensor, test_util::run_py_prg, zeros}; + use test_strategy::{Arbitrary, proptest}; /// Ground truth reference, computed by a Python snippet: /// @@ -318,7 +318,7 @@ def alibi(input, max_bias): out[ib, ih, c] += c * factor return out "#; - run_py_prg(prg.to_string(), &[a], &[&max_bias], a.dt()) + run_py_prg(prg.to_string(), &[a], &[&max_bias], a.dtype()) } #[derive(Arbitrary, Debug)] @@ -343,9 +343,9 @@ def alibi(input, max_bias): } = problem; // shape = [B, n_head, seq] - let shape = shape![b, n_head, seq]; - // let a_cpu = Tensor::randn::(0.0, 1.0, shape, Device::CPU); - let a_cpu = Tensor::zeros::(&shape, &Device::CPU); + let shape = (b, n_head, seq); + // let a_cpu = Tensor::randn::(0.0, 1.0, shape, Device::CPU).unwrap(); + let a_cpu = zeros(shape, Default::default()).unwrap(); let ground = ground_truth_alibi(&a_cpu, max_bias).unwrap(); let a_gpu = a_cpu.to(&device).unwrap(); @@ -353,11 +353,17 @@ def alibi(input, max_bias): let out_cpu = out_gpu.to(&Device::CPU).unwrap(); - println!("Problem: {:?}", problem); + println!("Problem: {problem:?}"); println!("Ground truth shape: {:?}", ground.shape()); println!("Output shape: {:?}", out_cpu.shape()); - println!("Ground truth: {:?}", ground.to_ndarray_view::()); - println!("Output: {:?}", out_cpu.to_ndarray_view::()); + println!( + "Ground truth: {:?}", + ground.inner().read().to_ndarray_view::() + ); + println!( + "Output: {:?}", + out_cpu.inner().read().to_ndarray_view::() + ); // Compare ground.all_close(&out_cpu, 1e-4, 1e-4).unwrap(); diff --git a/crates/ratchet-core/src/ops/arange.rs b/crates/piston-core/src/ops/arange.rs similarity index 82% rename from crates/ratchet-core/src/ops/arange.rs rename to crates/piston-core/src/ops/arange.rs index b13bc9d5..b1b7d6a5 100644 --- a/crates/ratchet-core/src/ops/arange.rs +++ b/crates/piston-core/src/ops/arange.rs @@ -1,12 +1,12 @@ use derive_new::new; use inline_wgsl::wgsl; -use ratchet_macros::IrFields; +use piston_macros::IrFields; use crate::{ - gpu::BindGroupLayoutDescriptor, rvec, shape, wgc, wgs, Array, BindingMode, BuiltIn, DType, - DynKernelMetadata, GPUOperation, Kernel, KernelElement, KernelRenderable, KernelSource, - OpGuards, Operation, OperationError, RVec, Scalar, StorageView, Strides, Tensor, - WgslKernelBuilder, WgslPrimitive, WorkgroupCount, WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, DynKernelMetadata, GPUOperation, Kernel, KernelElement, + KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, + StorageView, Stride, WgslKernelBuilder, WgslPrimitive, WorkgroupCount, WorkgroupSize, Workload, + gpu::BindGroupLayoutDescriptor, rvec, shape, wgc, wgs, }; #[derive(new, Debug, Clone, IrFields)] @@ -28,12 +28,12 @@ impl Operation for Arange { fn compute_view(&self) -> Result { let numel = self.numel(); let shape = shape![numel]; - let strides = Strides::from(&shape); - Ok(StorageView::new(shape, DType::F32, strides)) + let stride = Stride::from(&shape); + Ok(StorageView::new(shape, DType::F32, stride)) } /// The arange op has no input tensors, so return an empty array. - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![] } @@ -104,7 +104,7 @@ impl KernelRenderable for ArangeKernels { fn render( &self, _inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -120,7 +120,7 @@ impl KernelRenderable for ArangeKernels { kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - let dt = P::render_type(); + let dtype = P::render_type(); // Minimal compute shader: for each index, compute start + step * idx. // We skip any index >= metadata.numel. @@ -130,7 +130,7 @@ impl KernelRenderable for ArangeKernels { return; } - Y[idx] = metadata.start + 'dt(idx) * metadata.step; + Y[idx] = metadata.start + 'dtype(idx) * metadata.step; }); Ok(kernel_builder.build()?) @@ -148,11 +148,11 @@ impl Kernel for ArangeKernels { } } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { KernelElement::Scalar } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { let workgroup_size = wgs![256, 1, 1]; let numel = dst.shape().numel(); @@ -178,10 +178,14 @@ impl Kernel for ArangeKernels { Ok(BindGroupLayoutDescriptor::unary_inplace()) } - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { let ArangeKernels::Standard(op) = self; let mut dyn_meta = DynKernelMetadata::new(); - if dst.dt().is_float() { + if dst.dtype().is_float() { dyn_meta.add_field("start", op.start); dyn_meta.add_field("step", op.step); } else { @@ -195,12 +199,12 @@ impl Kernel for ArangeKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); // Our arange op is f32 only, but you could add more branches for f16, etc. - match (dst.dt(), &kernel_element) { + match (dst.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -209,7 +213,7 @@ impl Kernel for ArangeKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - dst.dt(), + dst.dtype(), kernel_element ))), } @@ -222,9 +226,12 @@ mod tests { use num_traits::AsPrimitive; use pyo3::ToPyObject; - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; - use crate::{test_util::run_py_prg, DType, Device, DeviceRequest, Tensor, TensorDType}; + use crate::{ + DType, Device, DeviceRequest, Tensor, TensorDType, TensorOptions, arange, + test_util::run_py_prg, + }; fn ground_truth( start: &dyn ToPyObject, @@ -249,27 +256,28 @@ def arange(start, stop, step): device: &Device, ) { fn abs>(x: T) -> T { - if x >= T::zero() { - x - } else { - -x - } + if x >= T::zero() { x } else { -x } } // Determine correct sign for step based on start/stop relationship let step = if stop >= start { abs(step) } else { -abs(step) }; - let a = Tensor::arange_step(start, stop, step, device) - .unwrap() - .cast(DType::F32) - .unwrap(); + let a = arange( + Some(start.as_()), + stop.as_(), + Some(step.as_()), + TensorOptions::new().device(device.clone()), + ) + .unwrap() + .cast(DType::F32) + .unwrap(); let ground = ground_truth(&start, &stop, &step).unwrap(); let a_gpu = a.to(device).unwrap(); let ours = a_gpu.to(&Device::CPU).unwrap(); - println!("ours = {:?}", ours); - println!("ground = {:?}", ground); + println!("ours = {ours:?}"); + println!("ground = {ground:?}"); // Compare our result with ground truth ground.all_close(&ours, 1e-6, 1e-6).unwrap(); @@ -288,7 +296,7 @@ def arange(start, stop, step): #[proptest(cases = 8)] fn test_arange_f32(prob: ArangeProblemF32) { let ArangeProblemF32 { start, stop, step } = prob; - println!("start = {}, stop = {}, step = {}", start, stop, step); + println!("start = {start}, stop = {stop}, step = {step}"); let device = Device::request_device(DeviceRequest::GPU).unwrap(); run_arange_trial::(start as f32, stop as f32, step as f32, &device); } @@ -306,7 +314,7 @@ def arange(start, stop, step): #[proptest(cases = 8)] fn test_arange_i32(prob: ArangeProblemI32) { let ArangeProblemI32 { start, stop, step } = prob; - println!("start = {}, stop = {}, step = {}", start, stop, step); + println!("start = {start}, stop = {stop}, step = {step}"); let device = Device::request_device(DeviceRequest::GPU).unwrap(); run_arange_trial::(start, stop, step, &device); } diff --git a/crates/piston-core/src/ops/bernoulli.rs b/crates/piston-core/src/ops/bernoulli.rs new file mode 100644 index 00000000..99067345 --- /dev/null +++ b/crates/piston-core/src/ops/bernoulli.rs @@ -0,0 +1,287 @@ +use derive_new::new; +use encase::ShaderType; +use half::f16; +use inline_wgsl::wgsl; +use piston_macros::{IrFields, WgslMetadata}; + +use crate::{ + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Stride, + Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::BindGroupLayoutDescriptor, rvec, +}; + +#[derive(new, Debug, Clone, IrFields)] +pub struct Bernoulli { + pub probs: OpTensor, + pub seed: Option, +} + +#[derive(Debug, derive_new::new, ShaderType, WgslMetadata)] +pub struct BernoulliMeta { + numel: u32, + seed: u32, +} + +impl Operation for Bernoulli { + fn name(&self) -> &'static str { + "Bernoulli" + } + + fn compute_view(&self) -> Result { + // Output has same shape as the input probabilities tensor + let shape = self.probs.shape().clone(); + let stride = Stride::from(&shape); + Ok(StorageView::new(shape, DType::F32, stride)) + } + + fn srcs(&self) -> RVec<&OpTensor> { + rvec![&self.probs] + } + + fn supports_inplace(&self) -> bool { + false + } +} + +impl OpGuards for Bernoulli { + fn check_shapes(&self) { + // No specific shape constraints for probabilities tensor + } + + fn check_dtypes(&self) { + // Ensure probabilities tensor has floating-point dtype + assert!( + self.probs.dtype().is_float(), + "Probabilities tensor must have floating-point dtype" + ); + } +} + +pub enum BernoulliKernels { + Standard(Bernoulli), +} + +impl GPUOperation for Bernoulli { + type KernelEnum = BernoulliKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + BernoulliKernels::Standard(self.clone()) + } +} + +impl KernelRenderable for BernoulliKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + _: bool, + ) -> Result<(), OperationError> { + builder.register_storage("X", BindingMode::ReadOnly, Array::

::default()); // Input probabilities + builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); // Output binary values + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + _: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + self.register_bindings::

(&mut kernel_builder, false)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + kernel_builder.write_global(wgsl! { + fn pcg_hash(input: u32) -> u32 { + let state = input * 747796405u + 2891336453u; + let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; + return (word >> 22u) ^ word; + } + + fn rand(seed: u32) -> f32 { + return f32(pcg_hash(seed)) / 4294967295.0; + } + }); + + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (index >= metadata.numel) { + return; + } + + let prob = X[index]; + let seed = index + metadata.seed; + let random_value = rand(seed); + + // If random value is less than probability, set to 1.0, otherwise 0.0 + Y[index] = f32(random_value < prob); + }); + + Ok(kernel_builder.build()?) + } +} + +impl Kernel for BernoulliKernels { + type Metadata = BernoulliMeta; + + fn kernel_name(&self) -> String { + match self { + BernoulliKernels::Standard(_) => "bernoulli".to_string(), + } + } + + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { + KernelElement::Scalar + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) + } + + fn storage_bind_group_layout( + &self, + _inplace: bool, + ) -> Result { + Ok(BindGroupLayoutDescriptor::unary()) + } + + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let BernoulliKernels::Standard(inner) = self; + Ok(BernoulliMeta { + numel: dst.shape().numel() as u32, + seed: inner.seed.unwrap_or(0), + }) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let kernel_element = self.kernel_element(dst); + match (dst.dtype(), &kernel_element) { + (DType::F32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {:?} or kernel element {:?}", + dst.dtype(), + kernel_element + ))), + } + } +} + +#[cfg(all(test, feature = "pyo3", feature = "rand"))] +mod tests { + use test_strategy::{Arbitrary, proptest}; + + use crate::{Device, DeviceRequest, randn}; + + fn run_bernoulli_trial(problem: BernoulliProblem, device: Device) { + let BernoulliProblem { B, M, N, seed } = problem; + + device.set_seed(seed as u64); + + // Create a tensor with random probabilities between 0 and 1 + let probs = randn((B, M, N), None, None, Default::default()).unwrap(); + let probs_gpu = probs.to(&device).unwrap(); + + // Apply Bernoulli sampling to the probabilities tensor + let a = probs_gpu.bernoulli().unwrap(); + + // Check that all values are either 0 or 1 + let values = a.to(&Device::CPU).unwrap().to_vec::().unwrap(); + for val in values.iter() { + assert!( + *val == 0.0 || *val == 1.0, + "Expected binary values (0 or 1)" + ); + } + + // Get statistics to verify that sampling distribution is reasonable + let mean = values.iter().sum::() / values.len() as f32; + let expected_mean = probs + .to(&Device::CPU) + .unwrap() + .to_vec::() + .unwrap() + .iter() + .sum::() + / values.len() as f32; + + // Calculate observed std of the binary outcomes. + let std = + (values.iter().map(|v| (v - mean).powi(2)).sum::() / values.len() as f32).sqrt(); + // For a uniformly distributed probabilities tensor, the overall variance (by total variance law) is expected to be ~0.25. + // So we square root it to get the standard deviation, 0.5. + let expected_std = 0.5; + + if (mean - expected_mean).abs() < 0.1 && (std - expected_std).abs() < 0.1 { + println!( + "\x1b[1;32mDistribution approximately bernoulli\x1b[0m - mean={mean} std={std}" + ); + } else { + (|| -> anyhow::Result<()> { + anyhow::bail!( + "\x1b[1;31mDistribution not bernoulli\x1b[0m - mean={} std={}", + mean, + std + ) + })() + .unwrap(); + } + } + + #[derive(Arbitrary, Debug)] + struct BernoulliProblem { + #[strategy(1..=64usize)] + B: usize, + #[strategy(1..=64usize)] + M: usize, + #[strategy(1..=64usize)] + N: usize, + #[strategy(0..=1000u32)] + seed: u32, + } + + #[proptest(cases = 8)] + fn test_bernoulli(prob: BernoulliProblem) { + let BernoulliProblem { B, M, N, seed } = prob; + println!("B = {B}, M = {M}, N = {N}, seed = {seed}"); + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_bernoulli_trial(prob, device); + } +} diff --git a/crates/piston-core/src/ops/binary.rs b/crates/piston-core/src/ops/binary.rs new file mode 100644 index 00000000..117a62a2 --- /dev/null +++ b/crates/piston-core/src/ops/binary.rs @@ -0,0 +1,425 @@ +use derive_new::new; +use half::f16; +use inline_wgsl::wgsl; +use piston_macros::IrFields; + +use crate::{ + Array, BindingMode, BuiltIn, DType, DynKernelMetadata, GPUOperation, InvariantError, Kernel, + KernelElement, KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, + RVec, Scalar, Shape, StorageView, Stride, TensorTypeOrScalarEnum, Vec2, Vec4, + WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, +}; +#[cfg(test)] +use test_strategy::Arbitrary; + +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Debug, Clone, Hash, IrFields)] +pub enum BinaryOp { + Add, + Sub, + Mul, + Div, + Pow, + Maximum, + Minimum, +} + +impl BinaryOp { + pub fn kernel_name(&self) -> &'static str { + match self { + BinaryOp::Add => "add", + BinaryOp::Sub => "sub", + BinaryOp::Mul => "mul", + BinaryOp::Div => "div", + BinaryOp::Pow => "pow", + BinaryOp::Maximum => "maximum", + BinaryOp::Minimum => "minimum", + } + } + + pub fn kernel_expression_tensor(&self, op1: &'static str, op2: &'static str) -> String { + match self { + BinaryOp::Add => wgsl! { 'op1 + 'op2 }, + BinaryOp::Sub => wgsl! { 'op1 - 'op2 }, + BinaryOp::Mul => wgsl! { 'op1 * 'op2 }, + BinaryOp::Div => wgsl! { 'op1 / 'op2 }, + BinaryOp::Pow => wgsl! { sign('op1) * pow(abs('op1), 'op2) }, + BinaryOp::Maximum => wgsl! { max('op1, 'op2) }, + BinaryOp::Minimum => wgsl! { min('op1, 'op2) }, + } + } + + pub fn kernel_expression_scalar(&self, dtype: &str, op1: &'static str) -> String { + match self { + BinaryOp::Add => wgsl! { fma('op1, 'dtype(1.0), 'dtype(metadata.value)) }, + BinaryOp::Sub => wgsl! { fma('op1, 'dtype(1.0), -'dtype(metadata.value)) }, + BinaryOp::Mul => wgsl! { fma('op1, 'dtype(metadata.value), 'dtype(0.0)) }, + BinaryOp::Div => wgsl! { fma('op1, 'dtype(1.0 / metadata.value), 'dtype(0.0)) }, + BinaryOp::Pow => wgsl! { sign('op1) * pow(abs('op1), 'dtype(metadata.value)) }, + BinaryOp::Maximum => panic!("Maximum with scalar is not supported"), + BinaryOp::Minimum => panic!("Minimum with scalar is not supported"), + } + } +} + +#[derive(new, Debug, Clone, IrFields)] +pub struct Binary { + pub lhs: OpTensor, + pub rhs: TensorTypeOrScalarEnum, + pub op: BinaryOp, +} + +impl KernelRenderable for BinaryKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + inplace: bool, + ) -> Result<(), OperationError> { + let BinaryKernels::Standard(inner) = self; + if inplace { + builder.register_storage("A", BindingMode::ReadWrite, Array::

::default()); + if let TensorTypeOrScalarEnum::Tensor(_) = &inner.rhs { + builder.register_storage("B", BindingMode::ReadOnly, Array::

::default()); + } + } else { + builder.register_storage("A", BindingMode::ReadOnly, Array::

::default()); + if let TensorTypeOrScalarEnum::Tensor(_) = &inner.rhs { + builder.register_storage("B", BindingMode::ReadOnly, Array::

::default()); + } + builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); + } + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + self.register_bindings::

(&mut kernel_builder, inplace)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + let N = (P::W as u32).render(); + let dtype = P::render_type(); + + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (index >= metadata.numel / 'N) { + return; + } + }); + + let BinaryKernels::Standard(inner) = self; + let apply = if inplace { + let expression = match &inner.rhs { + TensorTypeOrScalarEnum::Tensor(_) => { + inner.op.kernel_expression_tensor("val", "B[index]") + } + TensorTypeOrScalarEnum::Scalar(_) => { + inner.op.kernel_expression_scalar(&dtype, "val") + } + }; + wgsl! { + let val = A[index]; + A[index] = 'expression; + } + } else { + let expression = match &inner.rhs { + TensorTypeOrScalarEnum::Tensor(_) => { + inner.op.kernel_expression_tensor("A[index]", "B[index]") + } + TensorTypeOrScalarEnum::Scalar(_) => { + inner.op.kernel_expression_scalar(&dtype, "A[index]") + } + }; + wgsl! { Y[index] = 'expression; } + }; + kernel_builder.write_main(apply); + Ok(kernel_builder.build()?) + } +} + +impl Binary { + pub fn op(&self) -> &BinaryOp { + &self.op + } + + pub fn lhs(&self) -> &OpTensor { + &self.lhs + } + + pub fn rhs(&self) -> &TensorTypeOrScalarEnum { + &self.rhs + } +} + +impl OpGuards for Binary { + fn check_shapes(&self) { + if let TensorTypeOrScalarEnum::Tensor(rhs) = &self.rhs { + let shapes = [self.lhs.shape(), rhs.shape()]; + let broadcasted = Shape::multi_broadcast(&shapes); + assert!(broadcasted.is_some()); + } + } + + fn check_dtypes(&self) { + if let TensorTypeOrScalarEnum::Tensor(rhs) = &self.rhs { + assert_eq!(self.lhs.dtype(), rhs.dtype()); + } + } +} + +impl Operation for Binary { + fn name(&self) -> &'static str { + match self.op { + BinaryOp::Add => "Add", + BinaryOp::Sub => "Sub", + BinaryOp::Mul => "Mul", + BinaryOp::Div => "Div", + BinaryOp::Pow => "Pow", + BinaryOp::Maximum => "Maximum", + BinaryOp::Minimum => "Minimum", + } + } + + fn compute_view(&self) -> Result { + if let TensorTypeOrScalarEnum::Tensor(rhs) = &self.rhs { + let shapes = &[self.lhs.shape(), rhs.shape()]; + if self.lhs.is_scalar() || rhs.is_scalar() { + let other = if self.lhs.is_scalar() { rhs } else { &self.lhs }; + return Ok(other.storage_view().clone()); + } + let broadcasted = Shape::multi_broadcast(shapes); + if broadcasted.is_none() { + let failed = shapes.iter().map(|s| (*s).clone()).collect::>(); + return Err(InvariantError::BroadcastingFailed(failed).into()); + } + let broadcasted = broadcasted.unwrap(); + let ostride = Stride::from(&broadcasted); + Ok(StorageView::new(broadcasted, self.lhs.dtype(), ostride)) + } else { + Ok(self.lhs.storage_view().clone()) + } + } + + #[inline] + fn srcs(&self) -> RVec<&OpTensor> { + if let TensorTypeOrScalarEnum::Tensor(rhs) = &self.rhs { + rvec![&self.lhs, rhs] + } else { + rvec![&self.lhs] + } + } + + fn supports_inplace(&self) -> bool { + true + } +} + +impl GPUOperation for Binary { + type KernelEnum = BinaryKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + BinaryKernels::Standard(self.clone()) + } +} + +pub enum BinaryKernels { + Standard(Binary), +} + +impl Kernel for BinaryKernels { + type Metadata = DynKernelMetadata; + + fn storage_bind_group_layout( + &self, + inplace: bool, + ) -> Result { + let BinaryKernels::Standard(inner) = self; + match &inner.rhs { + TensorTypeOrScalarEnum::Tensor(_) => { + if inplace { + Ok(BindGroupLayoutDescriptor::binary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::binary()) + } + } + TensorTypeOrScalarEnum::Scalar(_) => { + if inplace { + Ok(BindGroupLayoutDescriptor::unary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::unary()) + } + } + } + } + + fn kernel_name(&self) -> String { + match self { + BinaryKernels::Standard(k) => k.op.kernel_name().to_string(), + } + } + + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let BinaryKernels::Standard(inner) = self; + let mut dyn_meta = DynKernelMetadata::new(); + dyn_meta.add_field("numel", dst.shape().numel() as u32); + if let TensorTypeOrScalarEnum::Scalar(value) = &inner.rhs { + if inner.lhs.dtype().is_float() { + dyn_meta.add_field("value", *value); + } else { + dyn_meta.add_field("value", *value as i32); + } + } + Ok(dyn_meta) + } + + fn kernel_element(&self, dst: &OpTensor) -> KernelElement { + let numel = dst.shape().numel(); + + if numel.is_multiple_of(4) { + KernelElement::Vec4 + } else if numel.is_multiple_of(2) { + KernelElement::Vec2 + } else { + KernelElement::Scalar + } + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let BinaryKernels::Standard(inner) = self; + let kernel_element = self.kernel_element(dst); + match (inner.lhs.dtype(), &kernel_element) { + (DType::F32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {:?} or kernel element {:?}", + inner.lhs.dtype(), + kernel_element + ))), + } + } +} + +#[cfg(all(test, feature = "pyo3"))] +mod tests { + use crate::{BinaryOp, Device, DeviceRequest, Shape, Tensor, randn, test_util::run_py_prg}; + use test_strategy::{Arbitrary, proptest}; + + #[derive(Arbitrary, Debug)] + struct BinaryProblem { + op: BinaryOp, + #[any(vec![1..=4, 1..=4, 1..=1, 1..=256])] + shape: Shape, + } + + fn ground_truth(a: &Tensor, b: &Tensor, op: &BinaryOp) -> anyhow::Result { + let kn = op.kernel_name(); + let prg = if matches!(op, BinaryOp::Pow) { + format!( + r#" +import torch +def {kn}(a, b): + a_tensor = torch.from_numpy(a) + b_tensor = torch.from_numpy(b) + sign = torch.sign(a_tensor) + return (torch.pow(torch.abs(a_tensor), b_tensor) * sign).numpy() +"#, + ) + } else { + format!( + r#" +import torch +def {kn}(a, b): + return torch.{kn}(torch.from_numpy(a), torch.from_numpy(b)).numpy() +"#, + ) + }; + run_py_prg(prg.to_string(), &[a, b], &[], a.dtype()) + } + + fn run_binary_trial(prob: BinaryProblem, device: Device) -> anyhow::Result<()> { + if device.is_cpu() && matches!(prob.op, BinaryOp::Pow) { + // Fail silently for now + return Ok(()); + } + let BinaryProblem { op, shape } = prob; + let a = randn(shape.clone(), None, None, Default::default())?; + let b = randn(shape, None, None, Default::default())?; + let ground = ground_truth(&a, &b, &op)?; + + let a = a.to(&device)?; + let b = b.to(&device)?; + + let c = match op { + BinaryOp::Add => a.add(b)?, + BinaryOp::Sub => a.sub(b)?, + BinaryOp::Mul => a.mul(b)?, + BinaryOp::Div => a.div(b)?, + BinaryOp::Pow => a.pow(b)?, + BinaryOp::Maximum => a.maximum(b)?, + BinaryOp::Minimum => a.minimum(b)?, + }; + + let d = c.to(&Device::CPU)?; + ground.all_close(&d, 1e-4, 1e-4)?; + Ok(()) + } + + #[proptest(cases = 8)] + fn test_binary_gpu(prob: BinaryProblem) { + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_binary_trial(prob, device).unwrap(); + } + + #[proptest(cases = 8)] + fn test_binary_cpu(prob: BinaryProblem) { + let device = Device::request_device(DeviceRequest::CPU).unwrap(); + run_binary_trial(prob, device).unwrap(); + } +} diff --git a/crates/ratchet-core/src/ops/cache.rs b/crates/piston-core/src/ops/cache.rs similarity index 77% rename from crates/ratchet-core/src/ops/cache.rs rename to crates/piston-core/src/ops/cache.rs index e486d40b..46bcbb5c 100644 --- a/crates/ratchet-core/src/ops/cache.rs +++ b/crates/piston-core/src/ops/cache.rs @@ -3,14 +3,14 @@ use encase::ShaderType; use glam::UVec4; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ - gpu::BindGroupLayoutDescriptor, rvec, Array, BindGroupLayoutEntryDescriptor, - BindGroupLayoutEntryExt, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, Shape, - StorageView, Strides, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, - Workload, + Array, BindGroupLayoutEntryDescriptor, BindGroupLayoutEntryExt, BindingMode, BuiltIn, DType, + GPUOperation, Kernel, KernelElement, KernelRenderable, KernelSource, OpGuards, OpTensor, + Operation, OperationError, RVec, Scalar, Shape, StorageView, Stride, Vec2, Vec4, + WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, gpu::BindGroupLayoutDescriptor, + rvec, }; /// # Cache @@ -23,8 +23,8 @@ use crate::{ /// 3. offset, where to start the write in the cache tensor, e.g [1, 5, 1024], [1, 1, 1024], offset = 5 -> [1, 6, 1024] #[derive(new, Debug, Clone, IrFields)] pub struct Cache { - cache: Tensor, - source: Tensor, + cache: OpTensor, + source: OpTensor, dim: usize, offset: usize, } @@ -50,7 +50,7 @@ impl KernelRenderable for CacheKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -117,12 +117,12 @@ pub struct CacheMeta { impl OpGuards for Cache { fn check_shapes(&self) { - assert!(self.cache.rank() >= 3); + assert!(self.cache.dim() >= 3); assert!(self.offset <= self.cache.shape()[self.dim]); } fn check_dtypes(&self) { - assert_eq!(self.cache.dt(), self.source.dt()); + assert_eq!(self.cache.dtype(), self.source.dtype()); } } @@ -132,18 +132,18 @@ impl Operation for Cache { } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.cache, &self.source] } fn compute_view(&self) -> Result { let mut result_shape = self.cache.shape().clone(); result_shape[self.dim] = self.offset + self.source.shape()[self.dim]; - let result_strides = Strides::from(&result_shape); + let result_stride = Stride::from(&result_shape); Ok(StorageView::new( result_shape, - self.cache.dt(), - result_strides, + self.cache.dtype(), + result_stride, )) } @@ -187,29 +187,33 @@ impl Kernel for CacheKernels { } } - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { let CacheKernels::Standard(inner) = self; - let original_rank = inner.cache.rank(); + let original_rank = inner.cache.dim(); let promotion = 4 - original_rank; let promoted_dim = inner.dim + promotion; let cache_shape = Shape::promote(inner.cache.shape().clone(), 4); - let cache_strides = Strides::from(&cache_shape); + let cache_stride = Stride::from(&cache_shape); let source_shape = Shape::promote(inner.source.shape().clone(), 4); - let source_strides = Strides::from(&source_shape); + let source_stride = Stride::from(&source_shape); let dst_shape = Shape::promote(dst.shape().clone(), 4); - let dst_strides = Strides::from(&dst_shape); + let dst_stride = Stride::from(&dst_shape); let cum0 = inner.offset as u32; let cum1 = cum0 + source_shape[promoted_dim] as u32; Ok(CacheMeta { - cache_stride: UVec4::from(&cache_strides), - src_stride: UVec4::from(&source_strides), - dst_stride: UVec4::from(&dst_strides), + cache_stride: UVec4::from(&cache_stride), + src_stride: UVec4::from(&source_stride), + dst_stride: UVec4::from(&dst_stride), dst_numel: dst_shape.numel() as u32, cum0, cum1, @@ -217,22 +221,22 @@ impl Kernel for CacheKernels { }) } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { KernelElement::Scalar } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) } fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); - match (dst.dt(), &kernel_element) { + match (dst.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -253,7 +257,7 @@ impl Kernel for CacheKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - dst.dt(), + dst.dtype(), kernel_element ))), } @@ -262,26 +266,29 @@ impl Kernel for CacheKernels { #[cfg(test)] mod tests { - use crate::{rvec, shape, Device, DeviceRequest, Tensor}; + use crate::{Device, DeviceRequest, cat, randn, rvec, zeros}; #[test] fn test_cache() -> anyhow::Result<()> { let device = Device::request_device(DeviceRequest::GPU).unwrap(); let populated = 2; //Create cache with 2 populated entries, and 14 blank entries - let mut dst0 = Tensor::randn::(0., 1., shape![1, 2, populated, 16], Device::CPU); - println!("PREVIOUS CACHE\n {:?}\n", dst0.to_ndarray_view::()); + let mut dst0 = randn((1, 2, populated, 16), None, None, Default::default())?; + println!( + "PREVIOUS CACHE\n {:?}\n", + dst0.inner().read().to_ndarray_view::() + ); dst0 = dst0.to(&device)?; - let dst1 = Tensor::zeros::(&shape![1, 2, 4, 16], &device); - let cur_cache = Tensor::cat(rvec![dst0.clone(), dst1], 2)?; + let dst1 = zeros((1, 2, 4, 16), Default::default())?; + let cur_cache = cat(rvec![dst0.clone(), dst1], 2)?; //This is the k or v vector we write - let mut src = Tensor::randn::(0., 1., shape![1, 2, 1, 16], Device::CPU); - println!("SRC \n {:?}\n", src.to_ndarray_view::()); + let mut src = randn((1, 2, 1, 16), None, None, Default::default())?; + println!("SRC \n {:?}\n", src.inner().read().to_ndarray_view::()); src = src.to(&device)?; //The result should be the concatenation of the cache and the source - let ground_truth = Tensor::cat(rvec![dst0.clone(), src.clone()], 2)?.to(&Device::CPU)?; + let ground_truth = cat(rvec![dst0.clone(), src.clone()], 2)?.to(&Device::CPU)?; let dim = 2; let b = cur_cache.clone().cache(src, dim, populated)?; @@ -289,11 +296,14 @@ mod tests { let cur_cache_cpu = cur_cache.to(&Device::CPU)?; println!( "CACHE RESULT \n{:?}\n", - cur_cache_cpu.to_ndarray_view::() + cur_cache_cpu.inner().read().to_ndarray_view::() ); let result = b.to(&Device::CPU)?; - println!("RESULT \n{:?}", result.to_ndarray_view::()); + println!( + "RESULT \n{:?}", + result.inner().read().to_ndarray_view::() + ); result.all_close(&ground_truth, 1e-5, 1e-5).unwrap(); Ok(()) diff --git a/crates/ratchet-core/src/ops/cast.rs b/crates/piston-core/src/ops/cast.rs similarity index 75% rename from crates/ratchet-core/src/ops/cast.rs rename to crates/piston-core/src/ops/cast.rs index b16df323..65b12707 100644 --- a/crates/ratchet-core/src/ops/cast.rs +++ b/crates/piston-core/src/ops/cast.rs @@ -2,28 +2,28 @@ use derive_new::new; use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ - gpu::BindGroupLayoutDescriptor, rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, - KernelElement, KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, - Scalar, StorageView, Strides, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, - WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Stride, + Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::BindGroupLayoutDescriptor, rvec, }; #[derive(new, Debug, Clone, IrFields)] pub struct Cast { - input: Tensor, - dst_dt: DType, + pub input: OpTensor, + pub dst_dtype: DType, } impl Cast { - pub fn input(&self) -> &Tensor { + pub fn input(&self) -> &OpTensor { &self.input } - pub fn dst_dt(&self) -> DType { - self.dst_dt + pub fn dst_dtype(&self) -> DType { + self.dst_dtype } } @@ -37,8 +37,8 @@ impl KernelRenderable for CastKernels { let CastKernels::Standard(inner) = self; //TODO: This is a bit of a hack, this match should be formalized let dst_accessor = match SP::W { - 1 => inner.dst_dt.as_wgsl().to_string(), - 2 | 4 => format!("vec{}<{}>", SP::W, inner.dst_dt.as_wgsl()), + 1 => inner.dst_dtype.as_wgsl().to_string(), + 2 | 4 => format!("vec{}<{}>", SP::W, inner.dst_dtype.as_wgsl()), _ => unimplemented!(), }; @@ -46,7 +46,7 @@ impl KernelRenderable for CastKernels { builder.register_storage_raw( "Y", BindingMode::ReadWrite, - format!("array<{}>", dst_accessor), + format!("array<{dst_accessor}>"), ) }; builder.register_uniform(); @@ -56,7 +56,7 @@ impl KernelRenderable for CastKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -86,8 +86,8 @@ impl KernelRenderable for CastKernels { //Bit of a hack let dst_accessor = match n { - 1 => inner.dst_dt.as_wgsl().to_string(), - 2 | 4 => format!("vec{}<{}>", n, inner.dst_dt.as_wgsl()), + 1 => inner.dst_dtype.as_wgsl().to_string(), + 2 | 4 => format!("vec{}<{}>", n, inner.dst_dtype.as_wgsl()), _ => unimplemented!(), }; @@ -117,12 +117,12 @@ impl Operation for Cast { fn compute_view(&self) -> Result { let shape = self.input.shape().clone(); - let strides = Strides::from(&shape); - Ok(StorageView::new(shape, self.dst_dt, strides)) + let stride = Stride::from(&shape); + Ok(StorageView::new(shape, self.dst_dtype, stride)) } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.input] } @@ -163,20 +163,24 @@ impl Kernel for CastKernels { } } - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { let numel = dst.shape().numel() as u32; Ok(CastMeta { numel }) } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) } - fn kernel_element(&self, dst: &Tensor) -> KernelElement { + fn kernel_element(&self, dst: &OpTensor) -> KernelElement { let numel = dst.shape().numel(); - if numel % 4 == 0 { + if numel.is_multiple_of(4) { KernelElement::Vec4 - } else if numel % 2 == 0 { + } else if numel.is_multiple_of(2) { KernelElement::Vec2 } else { KernelElement::Scalar @@ -186,12 +190,12 @@ impl Kernel for CastKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); let CastKernels::Standard(inner) = self; - match (inner.input.dt(), &kernel_element) { + match (inner.input.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -219,7 +223,11 @@ impl Kernel for CastKernels { (DType::I32, KernelElement::Vec4) => { self.render::>(inplace, dst, workgroup_size) } - _ => unimplemented!("Cannot cast {:?} -> {:?}", inner.input.dt(), inner.dst_dt), + _ => unimplemented!( + "Cannot cast {:?} -> {:?}", + inner.input.dtype(), + inner.dst_dtype + ), } } } @@ -227,13 +235,13 @@ impl Kernel for CastKernels { #[cfg(all(test, feature = "pyo3"))] mod tests { use half::f16; - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; - use crate::{shape, test_util::run_py_prg, DType, Device, DeviceRequest, Tensor}; + use crate::{DType, Device, DeviceRequest, Tensor, randn, test_util::run_py_prg}; #[derive(Arbitrary, Debug)] struct CastProblem { - dst_dt: DType, + dst_dtype: DType, #[strategy(1..=2usize)] B: usize, #[strategy(1..=128usize)] @@ -242,17 +250,17 @@ mod tests { N: usize, } - fn ground_truth(input: &Tensor, dst_dt: DType) -> anyhow::Result { + fn ground_truth(input: &Tensor, dst_dtype: DType) -> anyhow::Result { let prg = format!( r#" import torch def cast(a): return torch.from_numpy(a).to({}).numpy() "#, - dst_dt.as_torch() + dst_dtype.as_torch() ); - run_py_prg(prg.to_string(), &[input], &[], dst_dt) + run_py_prg(prg.to_string(), &[input], &[], dst_dtype) } fn run_cast_trial(prob: CastProblem, device: Device) -> anyhow::Result<()> { @@ -260,15 +268,15 @@ def cast(a): if matches!(device_precision, DType::F32) { return Ok(()); } - let CastProblem { dst_dt, B, M, N } = prob; - let input = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); - let ground = ground_truth(&input, dst_dt)?; + let CastProblem { dst_dtype, B, M, N } = prob; + let input = randn((B, M, N), None, None, Default::default())?; + let ground = ground_truth(&input, dst_dtype)?; let input = input.to(&device)?; - let casted = input.cast(dst_dt)?; + let casted = input.cast(dst_dtype)?; let casted = casted.to(&Device::CPU)?; - match dst_dt { + match dst_dtype { DType::F16 => { ground.all_close::(&casted, f16::from_f32(1e-4), f16::from_f32(1e-4))? } diff --git a/crates/piston-core/src/ops/cmp.rs b/crates/piston-core/src/ops/cmp.rs new file mode 100644 index 00000000..02fdce6e --- /dev/null +++ b/crates/piston-core/src/ops/cmp.rs @@ -0,0 +1,506 @@ +use derive_new::new; +use half::f16; +use inline_wgsl::wgsl; +use piston_macros::IrFields; + +use crate::{ + Array, BindingMode, BuiltIn, DType, DynKernelMetadata, GPUOperation, InvariantError, Kernel, + KernelElement, KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, + RVec, Scalar, Shape, StorageView, Stride, TensorTypeOrScalarEnum, Vec2, Vec4, + WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, +}; +#[cfg(test)] +use test_strategy::Arbitrary; + +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Debug, Clone, Hash, IrFields)] +pub enum CmpOp { + Eq, + Ne, + Le, + Ge, + Lt, + Gt, + LogicalAnd, + LogicalOr, + LogicalXor, +} + +impl CmpOp { + pub fn kernel_name(&self) -> &'static str { + match self { + CmpOp::Eq => "eq", + CmpOp::Ne => "ne", + CmpOp::Le => "le", + CmpOp::Ge => "ge", + CmpOp::Lt => "lt", + CmpOp::Gt => "gt", + CmpOp::LogicalAnd => "logical_and", + CmpOp::LogicalOr => "logical_or", + CmpOp::LogicalXor => "logical_xor", + } + } + + pub fn op_str(&self) -> &'static str { + match self { + CmpOp::Eq => "==", + CmpOp::Ne => "!=", + CmpOp::Le => "<=", + CmpOp::Ge => ">=", + CmpOp::Lt => "<", + CmpOp::Gt => ">", + CmpOp::LogicalAnd => "&&", + CmpOp::LogicalOr => "||", + CmpOp::LogicalXor => "!=", + } + } +} + +#[derive(new, Debug, Clone, IrFields)] +pub struct Cmp { + pub lhs: OpTensor, + pub rhs: TensorTypeOrScalarEnum, + pub op: CmpOp, +} + +impl KernelRenderable for CmpKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + _: bool, + ) -> Result<(), OperationError> { + builder.register_storage("A", BindingMode::ReadOnly, Array::

::default()); + let CmpKernels::Standard(inner) = self; + if let TensorTypeOrScalarEnum::Tensor(_) = &inner.rhs { + builder.register_storage("B", BindingMode::ReadOnly, Array::

::default()); + } + let CmpKernels::Standard(inner) = self; + match self.kernel_element(&inner.lhs) { + KernelElement::Scalar => { + builder.register_storage( + "Y", + BindingMode::ReadWrite, + Array::>::default(), + ); + } + KernelElement::Vec2 => { + builder.register_storage( + "Y", + BindingMode::ReadWrite, + Array::>::default(), + ); + } + KernelElement::Vec4 => { + builder.register_storage( + "Y", + BindingMode::ReadWrite, + Array::>::default(), + ); + } + } + + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + self.register_bindings::

(&mut kernel_builder, inplace)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + let CmpKernels::Standard(inner) = self; + + let N = (P::W as u32).render(); + let out_dtype = match self.kernel_element(dst) { + KernelElement::Scalar => "i32", + KernelElement::Vec2 => "vec2", + KernelElement::Vec4 => "vec4", + }; + let op = inner.op.op_str(); + let booleanify = matches!( + inner.op, + CmpOp::LogicalAnd | CmpOp::LogicalOr | CmpOp::LogicalXor + ); + + let casted_scalar_dtype = match self.kernel_element(dst) { + KernelElement::Scalar => inner.lhs.dtype().as_wgsl().to_string(), + KernelElement::Vec2 => { + format!("vec2<{}>", inner.lhs.dtype().as_wgsl()) + } + KernelElement::Vec4 => { + format!("vec4<{}>", inner.lhs.dtype().as_wgsl()) + } + }; + + let A_expr = if booleanify { + wgsl! { + (A[index] != 'casted_scalar_dtype(0.)) + } + } else { + wgsl! { + A[index] + } + }; + + let assignment_expr = match &inner.rhs { + TensorTypeOrScalarEnum::Tensor(_) => { + let B_expr = if booleanify { + wgsl! { + (B[index] != 'casted_scalar_dtype(0.)) + } + } else { + wgsl! { + B[index] + } + }; + wgsl! { + 'out_dtype('A_expr 'op 'B_expr) + } + } + TensorTypeOrScalarEnum::Scalar(_) => { + let value_expr = if booleanify { + wgsl! { + metadata.value != 0.0 + } + } else { + wgsl! { + metadata.value + } + }; + wgsl! { + 'out_dtype('A_expr 'op 'casted_scalar_dtype('value_expr)) + } + } + }; + + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (index >= metadata.numel / 'N) { + return; + } + + Y[index] = 'assignment_expr; + }); + + Ok(kernel_builder.build()?) + } +} + +impl Cmp { + pub fn op(&self) -> &CmpOp { + &self.op + } +} + +impl OpGuards for Cmp { + fn check_shapes(&self) { + if let TensorTypeOrScalarEnum::Tensor(rhs) = &self.rhs { + let shapes = [self.lhs.shape(), rhs.shape()]; + let broadcasted = Shape::multi_broadcast(&shapes); + assert!(broadcasted.is_some()); + } + } + + fn check_dtypes(&self) { + if let TensorTypeOrScalarEnum::Tensor(rhs) = &self.rhs { + assert_eq!(self.lhs.dtype(), rhs.dtype()); + } + } +} + +impl Operation for Cmp { + fn name(&self) -> &'static str { + match self.op { + CmpOp::Eq => "Eq", + CmpOp::Ne => "Ne", + CmpOp::Le => "Le", + CmpOp::Ge => "Ge", + CmpOp::Lt => "Lt", + CmpOp::Gt => "Gt", + CmpOp::LogicalAnd => "LogicalAnd", + CmpOp::LogicalOr => "LogicalOr", + CmpOp::LogicalXor => "LogicalXor", + } + } + + fn compute_view(&self) -> Result { + if let TensorTypeOrScalarEnum::Tensor(rhs) = &self.rhs { + let shapes = &[self.lhs.shape(), rhs.shape()]; + if self.lhs.is_scalar() || rhs.is_scalar() { + let other = if self.lhs.is_scalar() { rhs } else { &self.lhs }; + return Ok(other.storage_view().clone()); + } + let broadcasted = Shape::multi_broadcast(shapes); + if broadcasted.is_none() { + let failed = shapes.iter().map(|s| (*s).clone()).collect::>(); + return Err(InvariantError::BroadcastingFailed(failed).into()); + } + let broadcasted = broadcasted.unwrap(); + let ostride = Stride::from(&broadcasted); + Ok(StorageView::new(broadcasted, crate::DType::I32, ostride)) + } else { + let shape = self.lhs.shape().clone(); + let stride = Stride::from(&shape); + Ok(StorageView::new(shape, crate::DType::I32, stride)) + } + } + + #[inline] + fn srcs(&self) -> RVec<&OpTensor> { + if let TensorTypeOrScalarEnum::Tensor(rhs) = &self.rhs { + rvec![&self.lhs, rhs] + } else { + rvec![&self.lhs] + } + } +} + +pub enum CmpKernels { + Standard(Cmp), +} + +impl GPUOperation for Cmp { + type KernelEnum = CmpKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + CmpKernels::Standard(self.clone()) + } +} + +impl Kernel for CmpKernels { + type Metadata = DynKernelMetadata; + + fn kernel_name(&self) -> String { + match self { + CmpKernels::Standard(k) => k.op.kernel_name().to_string(), + } + } + + fn kernel_element(&self, dst: &OpTensor) -> KernelElement { + let numel = dst.shape().numel(); + + if numel.is_multiple_of(4) { + KernelElement::Vec4 + } else if numel.is_multiple_of(2) { + KernelElement::Vec2 + } else { + KernelElement::Scalar + } + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) + } + + fn storage_bind_group_layout( + &self, + inplace: bool, + ) -> Result { + if inplace { + panic!("Cmp cannot be done in place"); + } + let CmpKernels::Standard(inner) = self; + if let TensorTypeOrScalarEnum::Tensor(_) = &inner.rhs { + Ok(BindGroupLayoutDescriptor::binary()) + } else { + Ok(BindGroupLayoutDescriptor::unary()) + } + } + + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let CmpKernels::Standard(inner) = self; + let mut dyn_meta = DynKernelMetadata::new(); + dyn_meta.add_field("numel", dst.shape().numel() as u32); + if let TensorTypeOrScalarEnum::Scalar(value) = &inner.rhs { + if inner.lhs.dtype().is_float() { + dyn_meta.add_field("value", *value); + } else { + dyn_meta.add_field("value", *value as i32); + } + } + Ok(dyn_meta) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let CmpKernels::Standard(inner) = self; + let kernel_element = self.kernel_element(dst); + match (inner.lhs.dtype(), &kernel_element) { + (DType::F32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {:?} or kernel element {:?}", + inner.lhs.dtype(), + kernel_element + ))), + } + } +} + +#[cfg(all(test, feature = "pyo3"))] +mod tests { + use crate::{CmpOp, DType, Device, DeviceRequest, Shape, Tensor, randn, test_util::run_py_prg}; + use proptest::arbitrary::any; + use test_strategy::{Arbitrary, proptest}; + + #[derive(Arbitrary, Debug)] + struct BinaryProblem { + op: CmpOp, + #[any(vec![1..=4, 1..=4, 1..=1, 1..=256])] + shape: Shape, + } + + #[derive(Arbitrary, Debug)] + struct CmpScalarProblem { + op: CmpOp, + #[any(vec![1..=4, 1..=4, 1..=1, 1..=256])] + shape: Shape, + #[strategy(any::())] + scalar: f32, + } + + fn ground_truth(a: &Tensor, b: &Tensor, op: &CmpOp) -> anyhow::Result { + let kn = op.kernel_name(); + let prg = format!( + r#" +import torch +import numpy as np +def {kn}(a, b): + return torch.{kn}(torch.from_numpy(a), torch.from_numpy(b)).numpy().astype(np.int32) +"#, + ); + run_py_prg(prg.to_string(), &[a, b], &[], DType::I32) + } + + fn ground_truth_scalar(a: &Tensor, scalar: f32, op: &CmpOp) -> anyhow::Result { + let kn = op.kernel_name(); + let prg = format!( + r#" +import torch +import numpy as np +def {kn}(a, scalar): + return torch.{kn}(torch.from_numpy(a), scalar).numpy().astype(np.int32) +"#, + ); + run_py_prg(prg.to_string(), &[a], &[&scalar], DType::I32) + } + + fn run_cmp_trial(prob: BinaryProblem, device: Device) -> anyhow::Result<()> { + let BinaryProblem { op, shape } = prob; + let a = randn(shape.clone(), None, None, Default::default())?; + let b = randn(shape, None, None, Default::default())?; + let ground = ground_truth(&a, &b, &op)?.cast(DType::F32)?; + + let a_gpu = a.to(&device)?; + let b_gpu = b.to(&device)?; + let c_gpu = match op { + CmpOp::Eq => a_gpu.eq(b_gpu)?, + CmpOp::Ne => a_gpu.ne(b_gpu)?, + CmpOp::Le => a_gpu.le(b_gpu)?, + CmpOp::Ge => a_gpu.ge(b_gpu)?, + CmpOp::Lt => a_gpu.le(b_gpu)?, + CmpOp::Gt => a_gpu.gt(b_gpu)?, + CmpOp::LogicalAnd => a_gpu.logical_and(b_gpu)?, + CmpOp::LogicalOr => a_gpu.logical_or(b_gpu)?, + CmpOp::LogicalXor => a_gpu.logical_xor(b_gpu)?, + }; + + let d_gpu = c_gpu.to(&Device::CPU)?.cast(DType::F32)?; + ground.all_close(&d_gpu, 1e-4, 1e-4)?; + Ok(()) + } + + fn run_cmp_scalar_trial(prob: CmpScalarProblem, device: Device) -> anyhow::Result<()> { + // We'll just skip these for now + if matches!( + prob.op, + CmpOp::LogicalAnd | CmpOp::LogicalOr | CmpOp::LogicalXor + ) { + return Ok(()); + } + + let CmpScalarProblem { op, shape, scalar } = prob; + let a = randn(shape, None, None, Default::default())?; + let ground = ground_truth_scalar(&a, scalar, &op)?.cast(DType::F32)?; + + let a_gpu = a.to(&device)?; + let c_gpu = match op { + CmpOp::Eq => a_gpu.eq(scalar)?, + CmpOp::Ne => a_gpu.ne(scalar)?, + CmpOp::Le => a_gpu.le(scalar)?, + CmpOp::Ge => a_gpu.ge(scalar)?, + CmpOp::Lt => a_gpu.lt(scalar)?, + CmpOp::Gt => a_gpu.gt(scalar)?, + CmpOp::LogicalAnd => a_gpu.logical_and(scalar)?, + CmpOp::LogicalOr => a_gpu.logical_or(scalar)?, + CmpOp::LogicalXor => a_gpu.logical_xor(scalar)?, + }; + + let d_gpu = c_gpu.to(&Device::CPU)?.cast(DType::F32)?; + ground.all_close(&d_gpu, 1e-4, 1e-4)?; + Ok(()) + } + + #[proptest(cases = 32)] + fn test_binary(prob: BinaryProblem) { + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_cmp_trial(prob, device).unwrap(); + } + + #[proptest(cases = 32)] + fn test_scalar(prob: CmpScalarProblem) { + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_cmp_scalar_trial(prob, device).unwrap(); + } +} diff --git a/crates/ratchet-core/src/ops/concat.rs b/crates/piston-core/src/ops/concat.rs similarity index 71% rename from crates/ratchet-core/src/ops/concat.rs rename to crates/piston-core/src/ops/concat.rs index 046ea06f..6ce58a7d 100644 --- a/crates/ratchet-core/src/ops/concat.rs +++ b/crates/piston-core/src/ops/concat.rs @@ -2,25 +2,25 @@ use derive_new::new; use glam::UVec4; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::IrFields; +use piston_macros::IrFields; use crate::{ - gpu::BindGroupLayoutDescriptor, rvec, Array, BindingMode, BuiltIn, DType, DynKernelMetadata, - GPUOperation, Kernel, KernelElement, KernelRenderable, KernelSource, OpGuards, Operation, - OperationError, RVec, Scalar, Shape, StorageView, Strides, Tensor, Vec2, Vec4, - WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, DynKernelMetadata, GPUOperation, Kernel, KernelElement, + KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, + Shape, StorageView, Stride, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, + Workload, gpu::BindGroupLayoutDescriptor, rvec, }; #[derive(new, Debug, Clone, IrFields)] pub struct Concat { - pub inputs: RVec, + pub inputs: RVec, pub dim: usize, } const MAX_INPUTS: usize = 8; impl Concat { - pub fn inputs(&self) -> &[Tensor] { + pub fn inputs(&self) -> &[OpTensor] { &self.inputs } @@ -42,7 +42,7 @@ impl KernelRenderable for ConcatKernels { let ConcatKernels::Standard(inner) = self; for i in 0..inner.inputs.len() { - builder.register_storage(format!("X{}", i).as_str(), BindingMode::ReadOnly, arr); + builder.register_storage(format!("X{i}").as_str(), BindingMode::ReadOnly, arr); } builder.register_storage("Y", BindingMode::ReadWrite, arr); builder.register_uniform(); @@ -52,7 +52,7 @@ impl KernelRenderable for ConcatKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -93,9 +93,9 @@ impl KernelRenderable for ConcatKernels { for i in 1..inner.inputs.len() { let prevcum = format!("metadata.cum{}", i - 1); - let cum = format!("metadata.cum{}", i); - let stride = format!("metadata.x{}_stride", i); - let src = format!("X{}", i); + let cum = format!("metadata.cum{i}"); + let stride = format!("metadata.x{i}_stride"); + let src = format!("X{i}"); kernel_builder.write_main(wgsl! { if(dst_index[dim] < 'cum) { @@ -121,12 +121,12 @@ impl Operation for Concat { let stacked_dim = self.inputs.iter().map(|x| x.shape()[self.dim]).sum(); let mut output_shape = first.shape().clone(); output_shape[self.dim] = stacked_dim; - let output_strides = Strides::from(&output_shape); - Ok(StorageView::new(output_shape, first.dt(), output_strides)) + let output_stride = Stride::from(&output_shape); + Ok(StorageView::new(output_shape, first.dtype(), output_stride)) } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { self.inputs.iter().collect() } } @@ -136,28 +136,35 @@ impl OpGuards for Concat { assert!(self.inputs.len() > 1); assert!(self.inputs.len() <= MAX_INPUTS); //We only generate kernels for up to 8 inputs let first = &self.inputs[0]; - assert!(self - .inputs - .iter() - .all(|x| x.rank() == first.rank() && x.rank() <= 4)); - assert!(self.inputs.iter().all(|x| self.dim < x.rank())); + assert!( + self.inputs + .iter() + .all(|x| x.dim() == first.dim() && x.dim() <= 4) + ); + assert!(self.inputs.iter().all(|x| self.dim < x.dim())); //All tensors must have same shape, sans the concatenation dimension for axis in 0..self.dim { - assert!(self - .inputs - .iter() - .all(|x| x.shape()[axis] == first.shape()[axis])); + assert!( + self.inputs + .iter() + .all(|x| x.shape()[axis] == first.shape()[axis]) + ); } - for axis in (self.dim + 1)..first.rank() { - assert!(self - .inputs - .iter() - .all(|x| x.shape()[axis] == first.shape()[axis])); + for axis in (self.dim + 1)..first.dim() { + assert!( + self.inputs + .iter() + .all(|x| x.shape()[axis] == first.shape()[axis]) + ); } } fn check_dtypes(&self) { - assert!(self.inputs.iter().all(|x| x.dt() == self.inputs[0].dt())); + assert!( + self.inputs + .iter() + .all(|x| x.dtype() == self.inputs[0].dtype()) + ); } } @@ -182,20 +189,24 @@ impl Kernel for ConcatKernels { } } - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { let ConcatKernels::Standard(inner) = self; - let original_rank = inner.inputs[0].rank(); + let original_rank = inner.inputs[0].dim(); let promotion = 4 - original_rank; let input_shapes: Vec = inner .inputs .iter() .map(|x| Shape::promote(x.shape().clone(), 4)) .collect(); - let input_strides: Vec = input_shapes.iter().map(Strides::from).collect(); + let input_stride: Vec = input_shapes.iter().map(Stride::from).collect(); let promoted_dim = inner.dim + promotion; let dst_shape = Shape::promote(dst.shape().clone(), 4); - let dst_strides = Strides::from(&dst_shape); + let dst_stride = Stride::from(&dst_shape); let mut dyn_meta = DynKernelMetadata::new(); @@ -208,37 +219,37 @@ impl Kernel for ConcatKernels { }) .collect::>(); - for (si, strides) in input_strides.iter().enumerate() { - dyn_meta.add_field(format!("x{}_stride", si), UVec4::from(strides)); + for (si, stride) in input_stride.iter().enumerate() { + dyn_meta.add_field(format!("x{si}_stride"), UVec4::from(stride)); } - dyn_meta.add_field("dst_stride", UVec4::from(&dst_strides)); + dyn_meta.add_field("dst_stride", UVec4::from(&dst_stride)); dyn_meta.add_field("dst_numel", dst_shape.numel() as u32); for (ci, c) in cumsum.iter().enumerate() { - dyn_meta.add_field(format!("cum{}", ci), *c); + dyn_meta.add_field(format!("cum{ci}"), *c); } dyn_meta.add_field("dim", promoted_dim as u32); Ok(dyn_meta) } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) } - fn kernel_element(&self, _: &Tensor) -> KernelElement { + fn kernel_element(&self, _: &OpTensor) -> KernelElement { KernelElement::Scalar } fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); - match (dst.dt(), &kernel_element) { + match (dst.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -268,7 +279,7 @@ impl Kernel for ConcatKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - dst.dt(), + dst.dtype(), kernel_element ))), } @@ -285,7 +296,7 @@ impl GPUOperation for Concat { #[cfg(all(test, feature = "pyo3"))] mod tests { - use crate::{rvec, shape, test_util::run_py_prg, Device, DeviceRequest, RVec, Tensor}; + use crate::{Device, DeviceRequest, RVec, Tensor, cat, randn, test_util::run_py_prg}; #[derive(Debug)] struct ConcatProblem { @@ -302,17 +313,16 @@ def permute(*tensors): numpy_tensors = [] for t in tensors: numpy_tensors.append(torch.from_numpy(t)) - return np.ascontiguousarray(torch.cat(numpy_tensors, dim={}).numpy()) + return np.ascontiguousarray(torch.cat(numpy_tensors, dim={args}).numpy()) "#, - args ); - run_py_prg(prg.to_string(), to_cat, &[], to_cat[0].dt()) + run_py_prg(prg.to_string(), to_cat, &[], to_cat[0].dtype()) } fn run_concat_trial(prob: ConcatProblem, device: Device) -> anyhow::Result<()> { let ConcatProblem { tensors, dim } = prob; - let arg_str = format!("{}", dim); + let arg_str = format!("{dim}"); let ground = ground_truth( tensors.iter().collect::>().as_slice(), arg_str.as_str(), @@ -322,21 +332,21 @@ def permute(*tensors): t.to(&device)?; } let t_rvec = RVec::from(tensors); - let ours = Tensor::cat(t_rvec, dim)?; + let ours = cat(t_rvec, dim)?; let result = ours.to(&Device::CPU)?; - println!("Ground: {:?}\n", ground); - println!("Ours: {:?}", result); + println!("Ground: {ground:?}"); + println!("Ours: {result:?}"); ground.all_close(&result, 1e-5, 1e-5)?; Ok(()) } #[test] fn test_concat_gpu() { - let t0 = Tensor::randn::(0., 1., shape![4, 2, 50, 128], Device::CPU); - let t1 = Tensor::randn::(0., 1., shape![4, 2, 13, 128], Device::CPU); - let t2 = Tensor::randn::(0., 1., shape![4, 2, 77, 128], Device::CPU); - let t3 = Tensor::randn::(0., 1., shape![4, 2, 55, 128], Device::CPU); - let t4 = Tensor::randn::(0., 1., shape![4, 2, 11, 128], Device::CPU); + let t0 = randn((4, 2, 50, 128), None, None, Default::default()).unwrap(); + let t1 = randn((4, 2, 13, 128), None, None, Default::default()).unwrap(); + let t2 = randn((4, 2, 77, 128), None, None, Default::default()).unwrap(); + let t3 = randn((4, 2, 55, 128), None, None, Default::default()).unwrap(); + let t4 = randn((4, 2, 11, 128), None, None, Default::default()).unwrap(); let dim = 2; let device = Device::request_device(DeviceRequest::GPU).unwrap(); @@ -352,11 +362,11 @@ def permute(*tensors): #[test] fn test_concat_cpu() { - let t0 = Tensor::randn::(0., 1., shape![4, 2, 50, 128], Device::CPU); - let t1 = Tensor::randn::(0., 1., shape![4, 2, 13, 128], Device::CPU); - let t2 = Tensor::randn::(0., 1., shape![4, 2, 77, 128], Device::CPU); - let t3 = Tensor::randn::(0., 1., shape![4, 2, 55, 128], Device::CPU); - let t4 = Tensor::randn::(0., 1., shape![4, 2, 11, 128], Device::CPU); + let t0 = randn((4, 2, 50, 128), None, None, Default::default()).unwrap(); + let t1 = randn((4, 2, 13, 128), None, None, Default::default()).unwrap(); + let t2 = randn((4, 2, 77, 128), None, None, Default::default()).unwrap(); + let t3 = randn((4, 2, 55, 128), None, None, Default::default()).unwrap(); + let t4 = randn((4, 2, 11, 128), None, None, Default::default()).unwrap(); let dim = 2; let device = Device::request_device(DeviceRequest::CPU).unwrap(); diff --git a/crates/ratchet-core/src/ops/conv.rs b/crates/piston-core/src/ops/conv.rs similarity index 82% rename from crates/ratchet-core/src/ops/conv.rs rename to crates/piston-core/src/ops/conv.rs index 368cd867..2783f555 100644 --- a/crates/ratchet-core/src/ops/conv.rs +++ b/crates/piston-core/src/ops/conv.rs @@ -2,21 +2,22 @@ use derive_new::new; use encase::ShaderType; use half::f16; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor, WorkgroupCount}, - rvec, shape, wgc, wgs, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, StorageView, - Strides, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Stride, + Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, WorkgroupCount, dtype::WgslDType}, + rvec, shape, wgc, wgs, }; use inline_wgsl::wgsl; #[derive(new, Debug, Clone, IrFields)] pub struct Conv { - pub input: Tensor, - pub weight: Tensor, - pub bias: Option, + pub input: OpTensor, + pub weight: OpTensor, + pub bias: Option, pub stride: usize, pub padding: usize, //dilation: usize, TODO: implement dilation @@ -40,7 +41,7 @@ impl KernelRenderable for ConvKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -56,16 +57,16 @@ impl KernelRenderable for ConvKernels { self.register_bindings::

(&mut kernel_builder, inplace)?; kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - let dt = P::T::DT; + let dtype = P::T::DT; kernel_builder.write_global(wgsl! { - var F: array<'dt, 4096u>; + var F: array<'dtype, 4096u>; }); kernel_builder.write_global(wgsl! { fn inner(input_index: u32, filter_index: u32, output_index: u32, bias_index: u32, start: u32, end: u32) { - var inp = vec3<'dt>(0f); - var kernel = vec3<'dt>(0f); - var acc = vec3<'dt>(0f); + var inp = vec3<'dtype>(0f); + var kernel = vec3<'dtype>(0f); + var acc = vec3<'dtype>(0f); for(var i = 0u; i < metadata.Cin; i++) { let input_start = input_index + (i * metadata.Lin) - metadata.padding; //-1 is for padding //We only populate the input between the provided indices, used for padding @@ -138,20 +139,21 @@ pub struct ConvMeta { impl OpGuards for Conv { fn check_shapes(&self) { - assert_eq!(self.input.rank(), 3); - assert_eq!(self.weight.rank(), 3); + assert_eq!(self.input.dim(), 3); + assert_eq!(self.weight.dim(), 3); let [_, _, KS]: [usize; 3] = self.weight.shape().try_into().unwrap(); assert_eq!(KS, 3); //only have 3 kernel size for now } fn check_dtypes(&self) { - assert!(self.input.dt().is_float()); - assert!(self.weight.dt().is_float()); - assert!(self - .bias - .as_ref() - .map(|t| t.dt().is_float()) - .unwrap_or(true)); + assert!(self.input.dtype().is_float()); + assert!(self.weight.dtype().is_float()); + assert!( + self.bias + .as_ref() + .map(|t| t.dtype().is_float()) + .unwrap_or(true) + ); } } @@ -173,12 +175,12 @@ impl Operation for Conv { let L_out = calc_dim(L_in, KS, self.padding, 1, self.stride); let out_shape = shape![N, C_out, L_out]; - let out_strides = Strides::from(&out_shape); - Ok(StorageView::new(out_shape, input_t.dt(), out_strides)) + let out_stride = Stride::from(&out_shape); + Ok(StorageView::new(out_shape, input_t.dtype(), out_stride)) } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.input, &self.weight, self.bias.as_ref().unwrap()] } } @@ -203,7 +205,11 @@ impl Kernel for ConvKernels { } } - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { let ConvKernels::Threebythree(inner) = self; let [_N, Cin, Lin]: [usize; 3] = inner.input.shape().try_into()?; let [_Cout, _, KS]: [usize; 3] = inner.weight.shape().try_into()?; @@ -222,7 +228,7 @@ impl Kernel for ConvKernels { )) } - fn calculate_dispatch(&self, _: &Tensor) -> Result { + fn calculate_dispatch(&self, _: &OpTensor) -> Result { let workgroup_size = wgs![256, 1, 1]; let ConvKernels::Threebythree(inner) = self; @@ -238,19 +244,19 @@ impl Kernel for ConvKernels { }) } - fn kernel_element(&self, _: &Tensor) -> KernelElement { + fn kernel_element(&self, _: &OpTensor) -> KernelElement { KernelElement::Scalar } fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); let ConvKernels::Threebythree(inner) = self; - match (inner.input.dt(), &kernel_element) { + match (inner.input.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -271,7 +277,7 @@ impl Kernel for ConvKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - inner.input.dt(), + inner.input.dtype(), kernel_element ))), } @@ -288,10 +294,10 @@ impl GPUOperation for Conv { #[cfg(all(test, feature = "pyo3"))] mod tests { - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; use crate::test_util::run_py_prg; - use crate::{shape, Device, DeviceRequest, Tensor}; + use crate::{Device, DeviceRequest, Tensor, randn}; fn ground_truth( input: &Tensor, @@ -313,7 +319,7 @@ def conv(input, filters, bias, stride, padding): prg.to_string(), &[input, filters, bias], &[&stride, &padding], - input.dt(), + input.dtype(), ) } @@ -324,9 +330,9 @@ def conv(input, filters, bias, stride, padding): Cout, stride, } = problem; - let input = Tensor::randn::(0., 1., shape![1, Cin, Lin], Device::CPU); - let weight = Tensor::randn::(0., 1., shape![Cout, Cin, 3], Device::CPU); - let bias = Tensor::randn::(0., 1., shape![Cout], Device::CPU); + let input = randn((1, Cin, Lin), None, None, Default::default()).unwrap(); + let weight = randn((Cout, Cin, 3), None, None, Default::default()).unwrap(); + let bias = randn(Cout, None, None, Default::default()).unwrap(); let ground = ground_truth(&input, &weight, &bias, stride, 1).unwrap(); let input = input.to(device).unwrap(); @@ -335,8 +341,8 @@ def conv(input, filters, bias, stride, padding): let ours = input.conv1d(weight, Some(bias), stride, 1).unwrap(); let ours = ours.to(&Device::CPU).unwrap(); - println!("ours = {:?}", ours); - println!("ground = {:?}", ground); + println!("ours = {ours:?}"); + println!("ground = {ground:?}"); ground.all_close(&ours, 5e-3, 5e-3).unwrap(); } @@ -345,7 +351,7 @@ def conv(input, filters, bias, stride, padding): #[strategy(16..=1024usize)] Cin: usize, #[strategy(16..=1024usize)] - #[filter(#Lin % 3 == 0)] + #[filter(#Lin.is_multiple_of(3))] Lin: usize, #[strategy(16..=1024usize)] Cout: usize, @@ -362,10 +368,7 @@ def conv(input, filters, bias, stride, padding): Cout, stride, } = prob; - println!( - "Cin = {}, Lin = {}, Cout = {}, stride = {}", - Cin, Lin, Cout, stride - ); + println!("Cin = {Cin}, Lin = {Lin}, Cout = {Cout}, stride = {stride}"); run_conv_trial(&device, prob); } } diff --git a/crates/piston-core/src/ops/eye.rs b/crates/piston-core/src/ops/eye.rs new file mode 100644 index 00000000..71c2d673 --- /dev/null +++ b/crates/piston-core/src/ops/eye.rs @@ -0,0 +1,186 @@ +use derive_new::new; +use half::f16; +use inline_wgsl::wgsl; +use piston_macros::IrFields; + +use crate::{ + Array, BindingMode, BuiltIn, DType, DynKernelMetadata, GPUOperation, Kernel, KernelElement, + KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, + Shape, StorageView, Stride, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, +}; + +#[derive(new, Debug, Clone, IrFields)] +pub struct Eye { + pub shape: Shape, +} + +impl Operation for Eye { + fn name(&self) -> &'static str { + "Eye" + } + + fn compute_view(&self) -> Result { + let shape: Shape = self.shape.clone(); + let stride = Stride::from(&shape); + Ok(StorageView::new(shape, DType::F32, stride)) + } + + fn srcs(&self) -> RVec<&OpTensor> { + rvec![] + } + + fn supports_inplace(&self) -> bool { + false + } +} + +impl OpGuards for Eye { + fn check_shapes(&self) { + assert!( + self.shape.dim() == 2, + "Eye expects a 2D shape, got {:?}", + self.shape + ); + } + + fn check_dtypes(&self) {} +} + +pub enum EyeKernels { + Standard(Eye), +} + +impl GPUOperation for Eye { + type KernelEnum = EyeKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + EyeKernels::Standard(self.clone()) + } +} + +impl KernelRenderable for EyeKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + _: bool, + ) -> Result<(), OperationError> { + builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + _: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + self.register_bindings::

(&mut kernel_builder, false)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + let N = (P::W as u32).render(); + let dtype = P::render_type(); + + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (index >= metadata.numel / 'N) { + return; + } + + // Scalar-only kernel element, so 'N == 1 + let cols = metadata.cols; + let row = index / cols; + let col = index - row * cols; + if (row == col) { + Y[index] = 'dtype(1); + } else { + Y[index] = 'dtype(0); + } + }); + + Ok(kernel_builder.build()?) + } +} + +impl Kernel for EyeKernels { + type Metadata = DynKernelMetadata; + + fn kernel_name(&self) -> String { + match self { + EyeKernels::Standard(_) => "eye".to_string(), + } + } + + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { + // Keep scalar for correctness and simplicity. + KernelElement::Scalar + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) + } + + fn storage_bind_group_layout( + &self, + _inplace: bool, + ) -> Result { + Ok(BindGroupLayoutDescriptor::unary_inplace()) + } + + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let EyeKernels::Standard(op) = self; + let mut dyn_meta = DynKernelMetadata::new(); + dyn_meta.add_field("numel", dst.shape().numel() as u32); + let rows = op.shape[0] as u32; + let cols = op.shape[1] as u32; + dyn_meta.add_field("rows", rows); + dyn_meta.add_field("cols", cols); + Ok(dyn_meta) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let kernel_element = self.kernel_element(dst); + match (dst.dtype(), &kernel_element) { + (DType::F32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::U32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {:?} or kernel element {:?}", + dst.dtype(), + kernel_element + ))), + } + } +} diff --git a/crates/piston-core/src/ops/fill_pointwise.rs b/crates/piston-core/src/ops/fill_pointwise.rs new file mode 100644 index 00000000..19d06cbb --- /dev/null +++ b/crates/piston-core/src/ops/fill_pointwise.rs @@ -0,0 +1,415 @@ +use derive_new::new; +use inline_wgsl::wgsl; + +use crate::{ + Array, BindingMode, BuiltIn, DType, DynKernelMetadata, GPUOperation, Ir, IrFields, Kernel, + KernelElement, KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, + RVec, Scalar, Shape, StorageView, Stride, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, + WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, +}; + +#[derive(Debug, Clone)] +pub enum FillPointwiseKind { + Constant { + value: f32, + }, + Randn { + mean: f32, + std: f32, + seed: Option, + }, + Rand { + lo: f32, + up: f32, + seed: Option, + }, +} + +#[derive(new, Debug, Clone)] +pub struct FillPointwise { + pub shape: Shape, + pub kind: FillPointwiseKind, +} + +impl FillPointwise { + pub fn kernel_name(&self) -> &'static str { + match self.kind { + FillPointwiseKind::Constant { .. } => "fill_constant", + FillPointwiseKind::Randn { .. } => "fill_randn", + FillPointwiseKind::Rand { .. } => "fill_rand", + } + } +} + +impl Operation for FillPointwise { + fn name(&self) -> &'static str { + "FillPointwise" + } + + fn compute_view(&self) -> Result { + // The dtype is determined by the OpTensor meta at construction time. + // Returning F32 here mirrors existing fill ops and is unused in GPU path. + let shape: Shape = self.shape.clone(); + let stride = Stride::from(&shape); + Ok(StorageView::new(shape, DType::F32, stride)) + } + + fn srcs(&self) -> RVec<&OpTensor> { + rvec![] + } + + fn supports_inplace(&self) -> bool { + false + } +} + +impl OpGuards for FillPointwise { + fn check_shapes(&self) {} + fn check_dtypes(&self) {} +} + +pub enum FillPointwiseKernels { + Standard(FillPointwise), +} + +impl GPUOperation for FillPointwise { + type KernelEnum = FillPointwiseKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + FillPointwiseKernels::Standard(self.clone()) + } +} + +impl KernelRenderable for FillPointwiseKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + _: bool, + ) -> Result<(), OperationError> { + builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + _: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + self.register_bindings::

(&mut kernel_builder, false)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + let N = (P::W as u32).render(); + let dtype = P::render_type(); + + // Random helpers used by Randn/Xavier variants + let needs_rand = match self { + FillPointwiseKernels::Standard(inner) => matches!( + inner.kind, + FillPointwiseKind::Randn { .. } | FillPointwiseKind::Rand { .. } + ), + }; + + if needs_rand { + kernel_builder.write_global(wgsl! { + fn pcg_hash(input: u32) -> u32 { + let state = input * 747796405u + 2891336453u; + let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; + return (word >> 22u) ^ word; + } + + fn rand(seed: u32) -> f32 { + return f32(pcg_hash(seed)) / 4294967295.0; + } + + fn box_muller_1d(u1: f32, u2: f32) -> f32 { + let r = sqrt(-2.0 * log(u1)); + let theta = 2.0 * 3.14159265359 * u2; + return r * cos(theta); + } + }); + } + + // Common indexing prelude. Use N for vector width (1 for scalar types). + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (index >= metadata.numel / 'N) { + return; + } + }); + + match self { + FillPointwiseKernels::Standard(inner) => match inner.kind { + FillPointwiseKind::Constant { .. } => { + kernel_builder.write_main(wgsl! { + Y[index] = 'dtype(metadata.value); + }); + } + FillPointwiseKind::Randn { .. } => { + kernel_builder.write_main(wgsl! { + let seed1 = index; + let seed2 = index ^ 2747636419u; + let u1 = rand(seed1 + metadata.seed); + let u2 = rand(seed2 + metadata.seed); + let normal = box_muller_1d(u1, u2); + Y[index] = 'dtype(f32(normal) * metadata.stddev + metadata.mean); + }); + } + FillPointwiseKind::Rand { .. } => { + kernel_builder.write_main(wgsl! { + let u = rand(index + metadata.seed); + Y[index] = 'dtype(metadata.lo + (metadata.hi - metadata.lo) * u); + }); + } + }, + } + + Ok(kernel_builder.build()?) + } +} + +impl Kernel for FillPointwiseKernels { + type Metadata = DynKernelMetadata; + + fn kernel_name(&self) -> String { + match self { + FillPointwiseKernels::Standard(inner) => inner.kernel_name().to_string(), + } + } + + fn kernel_element(&self, dst: &OpTensor) -> KernelElement { + match self { + FillPointwiseKernels::Standard(inner) => match inner.kind { + FillPointwiseKind::Constant { .. } => { + // Vectorize constant fills like the original FillConstant + let rank = dst.shape().dim(); + let N = if rank > 0 { dst.shape()[rank - 1] } else { 1 }; + if N.is_multiple_of(4) { + KernelElement::Vec4 + } else if N.is_multiple_of(2) { + KernelElement::Vec2 + } else { + KernelElement::Scalar + } + } + // Randomized variants: keep scalar for simplicity/correctness + _ => KernelElement::Scalar, + }, + } + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) + } + + fn storage_bind_group_layout( + &self, + _inplace: bool, + ) -> Result { + Ok(BindGroupLayoutDescriptor::unary_inplace()) + } + + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let mut dyn_meta = DynKernelMetadata::new(); + dyn_meta.add_field("numel", dst.shape().numel() as u32); + + match self { + FillPointwiseKernels::Standard(inner) => match inner.kind { + FillPointwiseKind::Constant { value } => { + if dst.dtype().is_float() { + dyn_meta.add_field("value", value); + } else { + dyn_meta.add_field("value", value as i32); + } + } + FillPointwiseKind::Randn { mean, std, seed } => { + dyn_meta.add_field("mean", mean); + dyn_meta.add_field("stddev", std); + dyn_meta.add_field("seed", seed.unwrap_or(0)); + } + FillPointwiseKind::Rand { lo, up, seed } => { + dyn_meta.add_field("lo", lo); + dyn_meta.add_field("hi", up); + dyn_meta.add_field("seed", seed.unwrap_or(0)); + } + }, + } + Ok(dyn_meta) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let kernel_element = self.kernel_element(dst); + match (dst.dtype(), &kernel_element) { + // Floating types: all variants supported (random & constant). Random variants are Scalar-only by design. + (DType::F32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + + // Vectorized constant fills for performance + (DType::F32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + + // Integer dtype only meaningful for constant fills (random/Xavier require floats). KernelElement for random is Scalar. + (DType::I32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {:?} or kernel element {:?}", + dst.dtype(), + kernel_element + ))), + } + } +} + +impl IrFields for FillPointwise { + fn ir_fields(&self, ir: &mut Ir) { + ir.with_field("shape", self.shape.clone()); + match &self.kind { + FillPointwiseKind::Constant { value } => { + ir.with_field("kind", "Constant"); + ir.with_field("value", *value); + } + FillPointwiseKind::Randn { mean, std, seed } => { + ir.with_field("kind", "Randn"); + ir.with_field("mean", *mean); + ir.with_field("stddev", *std); + ir.with_field("seed", seed.unwrap_or(0)); + } + FillPointwiseKind::Rand { lo, up, seed } => { + ir.with_field("kind", "Rand"); + ir.with_field("lo", *lo); + ir.with_field("hi", *up); + ir.with_field("seed", seed.unwrap_or(0)); + } + } + } +} + +#[cfg(all(test, feature = "pyo3"))] +mod tests { + use test_strategy::{Arbitrary, proptest}; + + use crate::{ + DType, Device, DeviceRequest, Tensor, TensorOptions, randn, test_util::run_py_prg, + }; + + #[derive(Arbitrary, Debug)] + struct FillPointwiseProblem { + #[strategy(1..=64usize)] + B: usize, + #[strategy(1..=128usize)] + M: usize, + #[strategy(1..=128usize)] + N: usize, + } + + fn normal_parameters(output: &Tensor) -> anyhow::Result<(f32, f32)> { + let prg = r#" +import numpy as np + +def check_normal(output): + output_np = np.array(output) + mean = float(np.mean(output_np)) + std = float(np.std(output_np)) + return np.array([mean, std], dtype=np.float32) +"#; + + let params = run_py_prg(prg.to_string(), &[output], &[], DType::F32)? + .to(&Device::CPU) + .unwrap() + .to_vec::() + .unwrap(); + Ok((params[0], params[1])) + } + + #[proptest(cases = 16)] + fn test_fill_pointwise_constant(prob: FillPointwiseProblem) { + let FillPointwiseProblem { B, M, N } = prob; + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + let value: f32 = 0.5; + + let ours = Tensor::full( + (B, M, N), + value, + TensorOptions::new().device(device.clone()), + ) + .unwrap() + .to(&Device::CPU) + .unwrap(); + + // Ground truth via torch.full + let prg = r#" +import torch +def fill_constant(shape, value): + return torch.full(shape, value, dtype=torch.float32).cpu().numpy() +"#; + let ground = run_py_prg( + prg.to_string(), + &[], + &[&vec![B as i64, M as i64, N as i64], &value], + DType::F32, + ) + .unwrap(); + + ground.all_close(&ours, 1e-6, 1e-6).unwrap(); + } + + #[proptest(cases = 16)] + fn test_fill_pointwise_randn(prob: FillPointwiseProblem) { + let FillPointwiseProblem { B, M, N } = prob; + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + let a = randn((B, M, N), None, None, TensorOptions::new().device(device)) + .unwrap() + .to(&Device::CPU) + .unwrap(); + + let (mean, std) = normal_parameters(&a).unwrap(); + assert!((mean - 0.0).abs() < 0.1, "mean={mean}"); + assert!((std - 1.0).abs() < 0.1, "std={std}"); + } +} diff --git a/crates/ratchet-core/src/ops/gather.rs b/crates/piston-core/src/ops/gather.rs similarity index 70% rename from crates/ratchet-core/src/ops/gather.rs rename to crates/piston-core/src/ops/gather.rs index b03a3329..d5f02b0c 100644 --- a/crates/ratchet-core/src/ops/gather.rs +++ b/crates/piston-core/src/ops/gather.rs @@ -1,19 +1,20 @@ use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, StorageView, - Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Vec2, + Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, }; use derive_new::new; use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; #[derive(new, Debug, Clone, IrFields)] pub struct Gather { - pub src: Tensor, - pub ids: Tensor, + pub src: OpTensor, + pub ids: OpTensor, pub dim: usize, } @@ -28,11 +29,11 @@ pub struct GatherMeta { impl OpGuards for Gather { fn check_shapes(&self) { - assert!(self.src.rank() >= 1); + assert!(self.src.dim() >= 1); } fn check_dtypes(&self) { - assert!(self.ids.dt() == crate::DType::I32); + assert!(self.ids.dtype() == crate::DType::I32); } } @@ -44,13 +45,13 @@ impl Operation for Gather { fn compute_view(&self) -> Result { Ok(StorageView::new( self.ids.shape().clone(), - self.src.dt(), - self.ids.strides().clone(), + self.src.dtype(), + self.ids.stride().clone(), )) } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.src, &self.ids] } @@ -88,7 +89,7 @@ impl KernelRenderable for GatherKernels { fn render( &self, _: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -114,7 +115,7 @@ impl KernelRenderable for GatherKernels { if (index >= metadata.ids_numel / 'N) { return; } - + for (var i: u32 = index; i < metadata.ids_numel; i += 64u * num_workgroups.x) { let post = i % metadata.right_size; let idx = u32(I[i]); @@ -141,7 +142,7 @@ impl Kernel for GatherKernels { Ok(BindGroupLayoutDescriptor::binary()) } - fn calculate_dispatch(&self, _dst: &Tensor) -> Result { + fn calculate_dispatch(&self, _dst: &OpTensor) -> Result { let GatherKernels::Standard(inner) = self; Ok(Workload::std( inner.ids.shape().numel(), @@ -155,11 +156,11 @@ impl Kernel for GatherKernels { } } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { KernelElement::Scalar } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let GatherKernels::Standard(inner) = self; let src_dims = inner.src.shape().to_vec(); @@ -180,12 +181,12 @@ impl Kernel for GatherKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); let GatherKernels::Standard(inner) = self; - match (inner.src.dt(), &kernel_element) { + match (inner.src.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -204,9 +205,12 @@ impl Kernel for GatherKernels { (DType::F16, KernelElement::Vec4) => { self.render::>(inplace, dst, workgroup_size) } + (DType::I32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - inner.src.dt(), + inner.src.dtype(), kernel_element ))), } @@ -215,48 +219,71 @@ impl Kernel for GatherKernels { #[cfg(all(test, feature = "pyo3"))] mod tests { - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; use crate::test_util::run_py_prg; - use crate::{shape, DType, Device, DeviceRequest, Tensor, Var}; + use crate::{DType, Device, DeviceRequest, Tensor, randint, randn, rvec}; fn ground_truth(src: &Tensor, ids: &Tensor, dim: usize) -> anyhow::Result { let prg = format!( r#" import torch def gather(src, ids, dim): - return torch.gather(torch.from_numpy(src), {}, torch.from_numpy(ids).long()).numpy() + return torch.gather(torch.from_numpy(src), {dim}, torch.from_numpy(ids).long()).numpy() "#, - dim ); - run_py_prg(prg.to_string(), &[src, ids], &[&dim], src.dt()) + run_py_prg(prg.to_string(), &[src, ids], &[&dim], src.dtype()) } fn run_gather_trial(problem: GatherProblem, device: Device) { let GatherProblem { B, M, N, dim } = problem; - let src = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); + let src = randn((B, M, N), None, None, Default::default()).unwrap(); // Create the shape for ids tensor - let mut ids_shape = vec![B, M, N]; + let mut ids_shape = rvec![B, M, N]; ids_shape[dim] = 1; - let ids = Tensor::randint::(0, src.shape()[dim] as i32, ids_shape.into(), Device::CPU); + let ids = randint(0, src.shape()[dim] as i32, ids_shape, Default::default()).unwrap(); let ground = ground_truth(&src, &ids, dim).unwrap(); let src_gpu = src.to(&device).unwrap(); let ids_gpu = ids.to(&device).unwrap(); - let result = src_gpu.gather(ids_gpu, dim).unwrap(); + let result = src_gpu.gather(dim, ids_gpu).unwrap(); let ours = result.to(&Device::CPU).unwrap(); - log::debug!("src = {:?}", src); - log::debug!("ids = {:?}", ids); - log::debug!("ours = {:?}", ours); - log::debug!("ground = {:?}", ground); + log::debug!("src = {src:?}"); + log::debug!("ids = {ids:?}"); + log::debug!("ours = {ours:?}"); + log::debug!("ground = {ground:?}"); ground.all_close(&ours, 1e-5, 1e-5).unwrap(); } + fn run_gather_i32_trial(problem: GatherProblem, device: Device) { + let GatherProblem { B, M, N, dim } = problem; + + let src = randint(0, 1000, (B, M, N), Default::default()).unwrap(); + + // Create the shape for ids tensor + let mut ids_shape = rvec![B, M, N]; + ids_shape[dim] = 1; + let ids = randint(0, src.shape()[dim] as i32, ids_shape, Default::default()).unwrap(); + + let ground = ground_truth(&src, &ids, dim).unwrap(); + + let src_gpu = src.to(&device).unwrap(); + let ids_gpu = ids.to(&device).unwrap(); + + let result = src_gpu.gather(dim, ids_gpu).unwrap(); + + let ours = result.to(&Device::CPU).unwrap(); + // Compare in float space for equality + let ground_f32 = ground.cast(DType::F32).unwrap(); + let ours_f32 = ours.cast(DType::F32).unwrap(); + ground_f32.all_close(&ours_f32, 0.0f32, 0.0f32).unwrap(); + } + #[derive(Arbitrary, Debug)] struct GatherProblem { #[strategy(1..=3usize)] @@ -273,11 +300,18 @@ def gather(src, ids, dim): fn test_gather(prob: GatherProblem) { let _ = env_logger::builder().is_test(true).try_init(); let GatherProblem { B, M, N, dim } = prob; - log::info!("B = {}, M = {}, N = {}, dim = {}", B, M, N, dim); + log::info!("B = {B}, M = {M}, N = {N}, dim = {dim}"); let device = Device::request_device(DeviceRequest::GPU).unwrap(); run_gather_trial(prob, device); } + #[proptest(cases = 6)] + fn test_gather_i32(prob: GatherProblem) { + let _ = env_logger::builder().is_test(true).try_init(); + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_gather_i32_trial(prob, device); + } + #[derive(Arbitrary, Debug)] struct GatherBackwardProblem { #[strategy(1..=3usize)] @@ -297,11 +331,10 @@ import torch def gather_backward(src, ids): src_tensor = torch.tensor(torch.from_numpy(src), requires_grad=True) ids_tensor = torch.from_numpy(ids).long() - result = torch.gather(src_tensor, {}, ids_tensor) + result = torch.gather(src_tensor, {dim}, ids_tensor) result.backward(torch.ones_like(result)) return src_tensor.grad.numpy() -"#, - dim +"# ); run_py_prg(prg.to_string(), &[src, ids], &[], DType::F32) } @@ -309,36 +342,33 @@ def gather_backward(src, ids): fn run_gather_backward_trial(problem: GatherBackwardProblem) -> anyhow::Result<()> { let device = Device::request_device(DeviceRequest::GPU).unwrap(); let GatherBackwardProblem { B, M, N, dim } = problem; - let src = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); + let src = randn((B, M, N), None, None, Default::default())?; // Create the shape for ids tensor - let mut ids_shape = vec![B, M, N]; + let mut ids_shape = rvec![B, M, N]; ids_shape[dim] = 1; - let ids = Tensor::randint::(0, src.shape()[dim] as i32, ids_shape.into(), Device::CPU); + let ids = randint(0, src.shape()[dim] as i32, ids_shape, Default::default())?; let ground = ground_truth_backward(&src, &ids, dim)?; let src_gpu = src.to(&device)?; let ids_gpu = ids.to(&device)?; - let src_var = Var::from_tensor(&src_gpu)?; - let result_gpu = src_var - .as_tensor() - .clone() - .gather(ids_gpu, dim)?; + let src_var = src_gpu.requires_grad_(true)?; + let result_gpu = src_var.clone().gather(dim, ids_gpu)?; - let grads = result_gpu.backward()?; + result_gpu.backward()?; device.try_gpu()?.mark_step()?; - let src_grad = grads.get(src_var.as_tensor()).unwrap().clone(); + let src_grad = src_var.grad().unwrap().clone(); let ours = src_grad.to(&Device::CPU)?; let src_cpu = src.to(&Device::CPU)?; let ids_cpu = ids.to(&Device::CPU)?; - println!("src = {:?}", src_cpu); - println!("ids = {:?}", ids_cpu); - println!("ours = {:?}", ours); - println!("ground = {:?}", ground); + println!("src = {src_cpu:?}"); + println!("ids = {ids_cpu:?}"); + println!("ours = {ours:?}"); + println!("ground = {ground:?}"); ground.all_close(&ours, 1e-5, 1e-5)?; Ok(()) } @@ -347,7 +377,7 @@ def gather_backward(src, ids): fn test_gather_backward(prob: GatherBackwardProblem) { let _ = env_logger::builder().is_test(true).try_init(); let GatherBackwardProblem { B, M, N, dim } = prob; - println!("B = {}, M = {}, N = {}, dim = {}", B, M, N, dim); + println!("B = {B}, M = {M}, N = {N}, dim = {dim}"); run_gather_backward_trial(prob).unwrap(); } } diff --git a/crates/ratchet-core/src/ops/index_add.rs b/crates/piston-core/src/ops/index_add.rs similarity index 80% rename from crates/ratchet-core/src/ops/index_add.rs rename to crates/piston-core/src/ops/index_add.rs index 6d146b8b..7472220d 100644 --- a/crates/ratchet-core/src/ops/index_add.rs +++ b/crates/piston-core/src/ops/index_add.rs @@ -1,20 +1,20 @@ use crate::{ - gpu::BindGroupLayoutDescriptor, rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, - KernelElement, KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, - Scalar, StorageView, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, - Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Vec2, + Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::BindGroupLayoutDescriptor, rvec, }; use derive_new::new; use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; #[derive(new, Debug, Clone, IrFields)] pub struct IndexAdd { - pub dst: Tensor, - pub src: Tensor, - pub ids: Tensor, + pub dst: OpTensor, + pub src: OpTensor, + pub ids: OpTensor, pub dim: usize, } @@ -29,13 +29,13 @@ pub struct IndexAddMeta { impl OpGuards for IndexAdd { fn check_shapes(&self) { let (input, indices) = (&self.src, &self.ids); - assert_eq!(input.rank(), 2); - assert_eq!(indices.rank(), 1); + assert_eq!(input.dim(), 2); + assert_eq!(indices.dim(), 1); } fn check_dtypes(&self) { let indices = &self.ids; - assert_eq!(indices.dt(), DType::I32); + assert_eq!(indices.dtype(), DType::I32); } } @@ -49,7 +49,7 @@ impl Operation for IndexAdd { } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.dst, &self.src, &self.ids] } @@ -87,7 +87,7 @@ impl KernelRenderable for IndexAddKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -137,7 +137,7 @@ impl Kernel for IndexAddKernels { Ok(BindGroupLayoutDescriptor::ternary_inplace()) } - fn calculate_dispatch(&self, _: &Tensor) -> Result { + fn calculate_dispatch(&self, _: &OpTensor) -> Result { let IndexAddKernels::Standard(inner) = self; Ok(Workload::std( inner.src.shape().numel(), @@ -151,11 +151,11 @@ impl Kernel for IndexAddKernels { } } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { KernelElement::Scalar } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let IndexAddKernels::Standard(inner) = self; let dim = inner.dim; let src_shape_vec = inner.src.shape().to_vec(); @@ -176,12 +176,12 @@ impl Kernel for IndexAddKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); let IndexAddKernels::Standard(inner) = self; - match (inner.src.dt(), &kernel_element) { + match (inner.src.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -202,7 +202,7 @@ impl Kernel for IndexAddKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - inner.src.dt(), + inner.src.dtype(), kernel_element ))), } @@ -216,7 +216,7 @@ mod tests { use test_strategy::proptest; use crate::test_util::run_py_prg; - use crate::{rvec, shape, Device, DeviceRequest, Shape, Tensor}; + use crate::{Device, DeviceRequest, Shape, Tensor, randint, randn}; impl Arbitrary for IndexAddProblem { type Parameters = (); @@ -227,7 +227,7 @@ mod tests { .prop_flat_map(|input_shape| (Just(input_shape), 1..64usize)) .prop_map(|(input_shape, num_indices)| { let indices = - Tensor::randint(0, input_shape[0] as i32, shape![num_indices], Device::CPU); + randint(0, input_shape[0] as i32, num_indices, Default::default()).unwrap(); IndexAddProblem { input_shape, indices, @@ -247,11 +247,15 @@ mod tests { r#" import torch def index_add(input, source, indices): - return torch.index_add(torch.from_numpy(input),{},torch.from_numpy(indices),torch.from_numpy(source)).numpy() -"#, - dim + return torch.index_add(torch.from_numpy(input),{dim},torch.from_numpy(indices),torch.from_numpy(source)).numpy() +"# ); - run_py_prg(prg.to_string(), &[input, source, indices], &[], input.dt()) + run_py_prg( + prg.to_string(), + &[input, source, indices], + &[], + input.dtype(), + ) } fn run_index_add_trial(problem: IndexAddProblem, device: Device) { @@ -262,11 +266,13 @@ def index_add(input, source, indices): let mut source_shape = input_shape.clone(); source_shape[0] = indices.shape()[0]; - let input = Tensor::randn::(0., 1., input_shape.clone(), device.clone()) + let input = randn(input_shape.clone(), None, None, Default::default()) + .unwrap() .to(&Device::CPU) .unwrap(); - let source = Tensor::randn::(0., 1., source_shape.clone(), device.clone()) + let source = randn(source_shape.clone(), None, None, Default::default()) + .unwrap() .to(&Device::CPU) .unwrap(); @@ -279,19 +285,21 @@ def index_add(input, source, indices): let ground_truth = ground_truth(&input, &source, &indices, 0).unwrap(); - log::debug!("input = {:?}", input); - log::debug!("source = {:?}", source); - log::debug!("ground_truth = {:?}", ground_truth); - log::debug!("indices = {:?}", indices); + log::debug!("input = {input:?}"); + log::debug!("source = {source:?}"); + log::debug!("ground_truth = {ground_truth:?}"); + log::debug!("indices = {indices:?}"); let input = input.to(&device).unwrap(); let indices = indices.to(&device).unwrap(); let source = source.to(&device).unwrap(); - let result = input.index_add(indices.clone(), source.clone(), 0).unwrap(); + let result = input + .index_add_(indices.clone(), source.clone(), 0) + .unwrap(); let x = result.to(&Device::CPU).unwrap(); - log::debug!("x = {:?}", x); + log::debug!("x = {x:?}"); ground_truth.all_close(&x, 1e-1, 1e-1).unwrap(); } diff --git a/crates/ratchet-core/src/ops/index_write.rs b/crates/piston-core/src/ops/index_write.rs similarity index 76% rename from crates/ratchet-core/src/ops/index_write.rs rename to crates/piston-core/src/ops/index_write.rs index 7238f6b6..8588272a 100644 --- a/crates/ratchet-core/src/ops/index_write.rs +++ b/crates/piston-core/src/ops/index_write.rs @@ -2,19 +2,19 @@ use derive_new::new; use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ - gpu::BindGroupLayoutDescriptor, rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, - KernelElement, KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, - Scalar, Shape, StorageView, Strides, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, - WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, Shape, StorageView, + Stride, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::BindGroupLayoutDescriptor, rvec, }; #[derive(new, Debug, Clone, IrFields)] pub struct IndexWrite { - dst: Tensor, - src: Tensor, + dst: OpTensor, + src: OpTensor, write_start: RVec, } @@ -22,7 +22,7 @@ impl IndexWrite {} #[derive(Debug, derive_new::new, ShaderType, WgslMetadata)] pub struct IndexWriteMeta { - dst_strides: glam::UVec4, + dst_stride: glam::UVec4, src_numel: u32, write_start: glam::UVec4, } @@ -43,7 +43,7 @@ impl Operation for IndexWrite { } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.dst, &self.src] } @@ -80,7 +80,7 @@ impl KernelRenderable for IndexWriteKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -104,7 +104,7 @@ impl KernelRenderable for IndexWriteKernels { if (thread_offset >= metadata.src_numel) { return; } - let offset_index = ndIndexToOffset(metadata.write_start, metadata.dst_strides); + let offset_index = ndIndexToOffset(metadata.write_start, metadata.dst_stride); D[offset_index + thread_offset] = S[thread_offset]; }); @@ -131,14 +131,18 @@ impl Kernel for IndexWriteKernels { } } - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { let IndexWriteKernels::Standard(inner) = self; let padder = |mut shape: Shape| { shape.left_pad_to(1, 4); - let strides = Strides::from(&shape); - (shape, strides) + let stride = Stride::from(&shape); + (shape, stride) }; - let (_, dst_strides) = padder(dst.shape().clone()); + let (_, dst_stride) = padder(dst.shape().clone()); let (src_shape, _) = padder(inner.src.shape().clone()); let mut start = [0u32; 4]; @@ -148,13 +152,13 @@ impl Kernel for IndexWriteKernels { } Ok(IndexWriteMeta { - dst_strides: glam::UVec4::from(&dst_strides), + dst_stride: glam::UVec4::from(&dst_stride), src_numel: src_shape.numel() as u32, write_start: start.into(), }) } - fn calculate_dispatch(&self, _: &Tensor) -> Result { + fn calculate_dispatch(&self, _: &OpTensor) -> Result { let IndexWriteKernels::Standard(inner) = self; Ok(Workload::std( inner.src.shape().numel(), @@ -162,19 +166,19 @@ impl Kernel for IndexWriteKernels { )) } - fn kernel_element(&self, _: &Tensor) -> KernelElement { + fn kernel_element(&self, _: &OpTensor) -> KernelElement { KernelElement::Scalar } fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); let IndexWriteKernels::Standard(inner) = self; - match (inner.src.dt(), &kernel_element) { + match (inner.src.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -195,7 +199,7 @@ impl Kernel for IndexWriteKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - inner.src.dt(), + inner.src.dtype(), kernel_element ))), } @@ -204,23 +208,33 @@ impl Kernel for IndexWriteKernels { #[cfg(test)] mod tests { - use crate::{rvec, shape, Device, DeviceRequest, Tensor}; + use crate::{Device, DeviceRequest, Tensor, TensorOptions, rvec}; #[test] fn test_index_write() { let device = Device::request_device(DeviceRequest::GPU).unwrap(); - let dst = Tensor::from_data(vec![1., 2., 3., 4., 5., 6.], shape![3, 2], device.clone()); - let src = Tensor::from_data(vec![7., 8.], shape![1, 2], device.clone()); + let dst = Tensor::from_data( + vec![1., 2., 3., 4., 5., 6.], + (3, 2), + TensorOptions::new().device(device.clone()), + ) + .unwrap(); + let src = Tensor::from_data( + vec![7., 8.], + (1, 2), + TensorOptions::new().device(device.clone()), + ) + .unwrap(); let write_start = rvec![2, 0]; let b = dst.index_write(src, write_start).unwrap(); let result = b.to(&Device::CPU).unwrap(); let ground_truth = - Tensor::from_data(vec![1., 2., 3., 4., 7., 8.], shape![3, 2], Device::CPU); - println!("result: {:?}", result); - println!("ground_truth: {:?}", ground_truth); + Tensor::from_data(vec![1., 2., 3., 4., 7., 8.], (3, 2), TensorOptions::new()).unwrap(); + println!("result: {result:?}"); + println!("ground_truth: {ground_truth:?}"); ground_truth.all_close(&result, 1e-8, 1e-8).unwrap(); } } diff --git a/crates/piston-core/src/ops/lerp.rs b/crates/piston-core/src/ops/lerp.rs new file mode 100644 index 00000000..cc5f7fef --- /dev/null +++ b/crates/piston-core/src/ops/lerp.rs @@ -0,0 +1,373 @@ +use derive_new::new; +use half::f16; +use inline_wgsl::wgsl; +use piston_macros::IrFields; + +use crate::{ + Array, BindingMode, BuiltIn, DType, DynKernelMetadata, GPUOperation, InvariantError, Kernel, + KernelElement, KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, + RVec, Scalar, Shape, StorageView, Stride, TensorTypeOrScalarEnum, Vec2, Vec4, + WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, +}; + +#[derive(new, Debug, Clone, IrFields)] +pub struct Lerp { + pub input: OpTensor, + pub end: OpTensor, + pub weight: TensorTypeOrScalarEnum, +} + +impl KernelRenderable for LerpKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + inplace: bool, + ) -> Result<(), OperationError> { + let LerpKernels::Standard(inner) = self; + if inplace { + builder.register_storage("Input", BindingMode::ReadWrite, Array::

::default()); + builder.register_storage("End", BindingMode::ReadOnly, Array::

::default()); + if let TensorTypeOrScalarEnum::Tensor(_) = &inner.weight { + builder.register_storage("Weight", BindingMode::ReadOnly, Array::

::default()); + } + } else { + builder.register_storage("Input", BindingMode::ReadOnly, Array::

::default()); + builder.register_storage("End", BindingMode::ReadOnly, Array::

::default()); + if let TensorTypeOrScalarEnum::Tensor(_) = &inner.weight { + builder.register_storage("Weight", BindingMode::ReadOnly, Array::

::default()); + } + builder.register_storage("Output", BindingMode::ReadWrite, Array::

::default()); + } + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + self.register_bindings::

(&mut kernel_builder, inplace)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + let N = (P::W as u32).render(); + let dtype = P::render_type(); + + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (index >= metadata.numel / 'N) { + return; + } + }); + + let LerpKernels::Standard(inner) = self; + let lerp_expression = match &inner.weight { + TensorTypeOrScalarEnum::Tensor(_) => { + wgsl! { + fma(Weight[index], (End[index] - Input[index]), Input[index]) + } + } + TensorTypeOrScalarEnum::Scalar(_) => { + wgsl! { + fma('dtype(metadata.weight), (End[index] - Input[index]), Input[index]) + } + } + }; + + let assignment = if inplace { + wgsl! { + Input[index] = 'lerp_expression; + } + } else { + wgsl! { + Output[index] = 'lerp_expression; + } + }; + + kernel_builder.write_main(assignment); + Ok(kernel_builder.build()?) + } +} + +impl Lerp { + pub fn input(&self) -> &OpTensor { + &self.input + } + + pub fn end(&self) -> &OpTensor { + &self.end + } + + pub fn weight(&self) -> &TensorTypeOrScalarEnum { + &self.weight + } +} + +impl OpGuards for Lerp { + fn check_shapes(&self) { + // All tensors should be broadcastable to the same shape + let mut shapes = vec![self.input.shape(), self.end.shape()]; + if let TensorTypeOrScalarEnum::Tensor(weight) = &self.weight { + shapes.push(weight.shape()); + } + let broadcasted = Shape::multi_broadcast(&shapes); + assert!(broadcasted.is_some()); + } + + fn check_dtypes(&self) { + assert_eq!(self.input.dtype(), self.end.dtype()); + if let TensorTypeOrScalarEnum::Tensor(weight) = &self.weight { + assert_eq!(self.input.dtype(), weight.dtype()); + } + } +} + +impl Operation for Lerp { + fn name(&self) -> &'static str { + "Lerp" + } + + fn compute_view(&self) -> Result { + let mut shapes = vec![self.input.shape(), self.end.shape()]; + if let TensorTypeOrScalarEnum::Tensor(weight) = &self.weight { + shapes.push(weight.shape()); + } + + let broadcasted = Shape::multi_broadcast(&shapes); + if broadcasted.is_none() { + let failed = shapes.iter().map(|s| (*s).clone()).collect::>(); + return Err(InvariantError::BroadcastingFailed(failed).into()); + } + + let broadcasted = broadcasted.unwrap(); + let ostride = Stride::from(&broadcasted); + Ok(StorageView::new(broadcasted, self.input.dtype(), ostride)) + } + + #[inline] + fn srcs(&self) -> RVec<&OpTensor> { + if let TensorTypeOrScalarEnum::Tensor(weight) = &self.weight { + rvec![&self.input, &self.end, weight] + } else { + rvec![&self.input, &self.end] + } + } + + fn supports_inplace(&self) -> bool { + true + } +} + +impl GPUOperation for Lerp { + type KernelEnum = LerpKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + LerpKernels::Standard(self.clone()) + } +} + +pub enum LerpKernels { + Standard(Lerp), +} + +impl Kernel for LerpKernels { + type Metadata = DynKernelMetadata; + + fn storage_bind_group_layout( + &self, + inplace: bool, + ) -> Result { + let LerpKernels::Standard(inner) = self; + match &inner.weight { + TensorTypeOrScalarEnum::Tensor(_) => { + if inplace { + Ok(BindGroupLayoutDescriptor::ternary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::ternary()) + } + } + TensorTypeOrScalarEnum::Scalar(_) => { + if inplace { + Ok(BindGroupLayoutDescriptor::binary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::binary()) + } + } + } + } + + fn kernel_name(&self) -> String { + "lerp".to_string() + } + + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let LerpKernels::Standard(inner) = self; + let mut dyn_meta = DynKernelMetadata::new(); + dyn_meta.add_field("numel", dst.shape().numel() as u32); + if let TensorTypeOrScalarEnum::Scalar(value) = &inner.weight { + if inner.input.dtype().is_float() { + dyn_meta.add_field("weight", *value); + } else { + dyn_meta.add_field("weight", *value as i32); + } + } + Ok(dyn_meta) + } + + fn kernel_element(&self, dst: &OpTensor) -> KernelElement { + let numel = dst.shape().numel(); + + if numel.is_multiple_of(4) { + KernelElement::Vec4 + } else if numel.is_multiple_of(2) { + KernelElement::Vec2 + } else { + KernelElement::Scalar + } + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let LerpKernels::Standard(inner) = self; + let kernel_element = self.kernel_element(dst); + match (inner.input.dtype(), &kernel_element) { + (DType::F32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {:?} or kernel element {:?}", + inner.input.dtype(), + kernel_element + ))), + } + } +} + +#[cfg(all(test, feature = "pyo3"))] +mod tests { + use crate::{Device, DeviceRequest, Shape, Tensor, randn, test_util::run_py_prg}; + use proptest::arbitrary::any; + use test_strategy::{Arbitrary, proptest}; + + #[derive(Arbitrary, Debug)] + struct LerpProblem { + #[any(vec![1..=4, 1..=4, 1..=1, 1..=256])] + shape: Shape, + } + + #[derive(Arbitrary, Debug)] + struct LerpScalarProblem { + #[any(vec![1..=4, 1..=4, 1..=1, 1..=256])] + shape: Shape, + #[strategy(any::())] + weight: f32, + } + + fn ground_truth_tensor( + input: &Tensor, + end: &Tensor, + weight: &Tensor, + ) -> anyhow::Result { + let prg = r#" +import torch +def lerp(input, end, weight): + return torch.lerp(torch.from_numpy(input), torch.from_numpy(end), torch.from_numpy(weight)).numpy() +"#; + run_py_prg(prg.to_string(), &[input, end, weight], &[], input.dtype()) + } + + fn ground_truth_scalar(input: &Tensor, end: &Tensor, weight: f32) -> anyhow::Result { + let prg = r#" +import torch +def lerp(input, end, weight): + return torch.lerp(torch.from_numpy(input), torch.from_numpy(end), weight).numpy() +"#; + run_py_prg(prg.to_string(), &[input, end], &[&weight], input.dtype()) + } + + fn run_lerp_tensor_trial(prob: LerpProblem, device: Device) -> anyhow::Result<()> { + let LerpProblem { shape } = prob; + let input = randn(shape.clone(), None, None, Default::default())?; + let end = randn(shape.clone(), None, None, Default::default())?; + let weight = randn(shape, None, None, Default::default())?; + let ground = ground_truth_tensor(&input, &end, &weight)?; + + let input = input.to(&device)?; + let end = end.to(&device)?; + let weight = weight.to(&device)?; + + let result = input.lerp(end, weight)?; + let result = result.to(&Device::CPU)?; + ground.all_close(&result, 1e-4, 1e-4)?; + Ok(()) + } + + fn run_lerp_scalar_trial(prob: LerpScalarProblem, device: Device) -> anyhow::Result<()> { + let LerpScalarProblem { shape, weight } = prob; + let input = randn(shape.clone(), None, None, Default::default())?; + let end = randn(shape, None, None, Default::default())?; + let ground = ground_truth_scalar(&input, &end, weight)?; + + let input = input.to(&device)?; + let end = end.to(&device)?; + + let result = input.lerp(end, weight)?; + let result = result.to(&Device::CPU)?; + ground.all_close(&result, 1e-4, 1e-4)?; + Ok(()) + } + + #[proptest(cases = 8)] + fn test_lerp_tensor_gpu(prob: LerpProblem) { + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_lerp_tensor_trial(prob, device).unwrap(); + } + + #[proptest(cases = 8)] + fn test_lerp_scalar_gpu(prob: LerpScalarProblem) { + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_lerp_scalar_trial(prob, device).unwrap(); + } +} diff --git a/crates/ratchet-core/src/ops/matmul/gemm.rs b/crates/piston-core/src/ops/matmul/gemm.rs similarity index 91% rename from crates/ratchet-core/src/ops/matmul/gemm.rs rename to crates/piston-core/src/ops/matmul/gemm.rs index 366ad235..767b5610 100644 --- a/crates/ratchet-core/src/ops/matmul/gemm.rs +++ b/crates/piston-core/src/ops/matmul/gemm.rs @@ -1,20 +1,20 @@ use encase::ShaderType; use half::f16; -use ratchet_macros::WgslMetadata; +use piston_macros::WgslMetadata; use crate::{ - gpu::dtype::WgslDType, rvec, wgc, wgs, Array, BindGroupLayoutDescriptor as BGLD, BindingMode, - BuiltIn, DType, InvariantError, Kernel, KernelElement, KernelKey, KernelRenderable, - KernelSource, Matmul, MatmulSpec, OperationError, Scalar, Strides, Tensor, Vec2, Vec4, - WgslFragment, WgslKernelBuilder, WgslPrimitive, WorkgroupCount, WorkgroupSize, Workload, + Array, BindGroupLayoutDescriptor as BGLD, BindingMode, BuiltIn, DType, InvariantError, Kernel, + KernelElement, KernelKey, KernelRenderable, KernelSource, Matmul, MatmulSpec, OpTensor, + OperationError, Scalar, Stride, Vec2, Vec4, WgslFragment, WgslKernelBuilder, WgslPrimitive, + WorkgroupCount, WorkgroupSize, Workload, gpu::dtype::WgslDType, rvec, wgc, wgs, }; use glam::IVec3; use inline_wgsl::wgsl; pub struct GEMM { - lhs: Tensor, - rhs: Tensor, - bias: Option, + lhs: OpTensor, + rhs: OpTensor, + bias: Option, trans_lhs: bool, trans_rhs: bool, trans_dst: bool, @@ -47,11 +47,11 @@ impl GEMM { #[derive(Debug, Clone, ShaderType, WgslMetadata)] pub struct GEMMMeta { lhs_shape: IVec3, - lhs_strides: IVec3, + lhs_stride: IVec3, rhs_shape: IVec3, - rhs_strides: IVec3, + rhs_stride: IVec3, dst_shape: IVec3, - dst_strides: IVec3, + dst_stride: IVec3, dim_lhs_outer: i32, dim_rhs_outer: i32, dim_inner: i32, @@ -68,7 +68,7 @@ impl KernelRenderable for GEMM { let float_arr = Array::

::default(); let ro = BindingMode::ReadOnly; - match A.dt() { + match A.dtype() { DType::F32 | DType::F16 => { builder.register_storage("A", ro, float_arr); builder.register_storage("B", ro, float_arr); @@ -86,7 +86,7 @@ impl KernelRenderable for GEMM { } builder.register_storage("result", BindingMode::ReadWrite, float_arr); } - _ => return Err(InvariantError::UnsupportedDType(A.dt()).into()), + _ => return Err(InvariantError::UnsupportedDType(A.dtype()).into()), } builder.register_uniform(); @@ -96,7 +96,7 @@ impl KernelRenderable for GEMM { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -134,8 +134,8 @@ impl Kernel for GEMM { &self, workgroup_size: &WorkgroupSize, inplace: bool, - srcs: &[&Tensor], - dst: &Tensor, + srcs: &[&OpTensor], + dst: &OpTensor, kernel_element: &KernelElement, ) -> KernelKey { let (a_fit, b_fit, out_fit) = self.spec.tile_fit(); @@ -163,19 +163,19 @@ impl Kernel for GEMM { ) } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let spec = &self.spec; let mut lhs_shape = spec.lhs_shape().clone(); lhs_shape.insert(0, spec.lhs_stack()); - let lhs_strides = Strides::from(&lhs_shape); + let lhs_stride = Stride::from(&lhs_shape); let mut rhs_shape = spec.rhs_shape().clone(); rhs_shape.insert(0, spec.rhs_stack()); - let rhs_strides = Strides::from(&rhs_shape); + let rhs_stride = Stride::from(&rhs_shape); let mut dst_shape = spec.dst_shape().clone(); dst_shape.insert(0, spec.stacks()); - let dst_strides = Strides::from(&dst_shape); + let dst_stride = Stride::from(&dst_shape); let dim_lhs_outer = spec.dim_lhs_outer() as i32; let dim_rhs_outer = spec.dim_rhs_outer() as i32; @@ -183,18 +183,18 @@ impl Kernel for GEMM { Ok(GEMMMeta { lhs_shape: lhs_shape.into(), - lhs_strides: lhs_strides.into(), + lhs_stride: lhs_stride.into(), rhs_shape: rhs_shape.into(), - rhs_strides: rhs_strides.into(), + rhs_stride: rhs_stride.into(), dst_shape: dst_shape.into(), - dst_strides: dst_strides.into(), + dst_stride: dst_stride.into(), dim_lhs_outer, dim_rhs_outer, dim_inner, }) } - fn calculate_dispatch(&self, _: &Tensor) -> Result { + fn calculate_dispatch(&self, _: &OpTensor) -> Result { //GEMM let TILE_DIM = 32; let lhs_shape = self.spec.lhs_shape(); @@ -226,11 +226,11 @@ impl Kernel for GEMM { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.spec.select_kernel_element(); - match (self.lhs.dt(), kernel_element) { + match (self.lhs.dtype(), kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -253,22 +253,22 @@ impl Kernel for GEMM { (DType::Q8_0H(_), _) => self.render::>(inplace, dst, workgroup_size), (DType::Q4_KF(_), _) => self.render::>(inplace, dst, workgroup_size), (DType::Q4_KH(_), _) => self.render::>(inplace, dst, workgroup_size), - _ => Err(InvariantError::UnsupportedDType(self.lhs.dt()).into()), + _ => Err(InvariantError::UnsupportedDType(self.lhs.dtype()).into()), } } - fn kernel_element(&self, _: &Tensor) -> KernelElement { + fn kernel_element(&self, _: &OpTensor) -> KernelElement { self.spec.select_kernel_element() } fn storage_bind_group_layout(&self, _inplace: bool) -> Result { let (LHS, RHS, bias) = (&self.lhs, &self.rhs, &self.bias); - let layout = match (LHS.dt(), RHS.dt(), bias.is_some()) { + let layout = match (LHS.dtype(), RHS.dtype(), bias.is_some()) { (DType::F32 | DType::F16, DType::F32 | DType::F16, false) => BGLD::binary(), (DType::F32 | DType::F16, DType::F32 | DType::F16, true) => BGLD::ternary(), (DType::Q8_0F(_) | DType::Q8_0H(_), DType::F32 | DType::F16, false) => BGLD::ternary(), (DType::Q8_0F(_) | DType::Q8_0H(_), DType::F32 | DType::F16, true) => BGLD::nthary(4), - _ => return Err(InvariantError::UnsupportedDType(RHS.dt()).into()), + _ => return Err(InvariantError::UnsupportedDType(RHS.dtype()).into()), }; Ok(layout) } @@ -280,15 +280,15 @@ impl GEMM { let W = P::W; builder.write_global(wgsl! { fn getAIndexFromCoords3D(coords : vec3) -> i32 { - return dot(coords, metadata.lhs_strides); + return dot(coords, metadata.lhs_stride); } fn getBIndexFromCoords3D(coords : vec3) -> i32 { - return dot(coords, metadata.rhs_strides); + return dot(coords, metadata.rhs_stride); } fn getOutputIndexFromCoords(coords : vec3) -> i32 { - return dot(coords, metadata.dst_strides); + return dot(coords, metadata.dst_stride); } fn setOutputAtIndex(flatIndex : i32, value : 'accessor) { @@ -304,16 +304,16 @@ impl GEMM { fn write_getters( &self, - _: &Tensor, + _: &OpTensor, builder: &mut WgslKernelBuilder, ) -> Result<(), OperationError> { let (A, _, _) = (&self.lhs, &self.rhs, &self.bias); let accessor = P::render_type(); let W = P::W; - let dt = P::T::DT; - builder.write_unpack(A.dt()); + let dtype = P::T::DT; + builder.write_unpack(A.dtype()); - let a_getters = match A.dt() { + let a_getters = match A.dtype() { DType::F32 | DType::F16 => { wgsl! { fn getA(d0 : i32, d1 : i32, d2 : i32) -> 'accessor { @@ -323,21 +323,21 @@ impl GEMM { } DType::Q8_0F(_) | DType::Q8_0H(_) => { wgsl! { - fn getA(d0 : i32, d1 : i32, d2 : i32) -> vec4<'dt> { + fn getA(d0 : i32, d1 : i32, d2 : i32) -> vec4<'dtype> { return unpack(A[getAIndexFromCoords3D(vec3(d0,d1,d2)) / 4]); } - fn getAbsMax(d0 : i32, d1 : i32, d2 : i32) -> 'dt { + fn getAbsMax(d0 : i32, d1 : i32, d2 : i32) -> 'dtype { let abs_index = getAIndexFromCoords3D(vec3(d0,d1,d2)) / 32; return scale[abs_index]; } } } - _ => return Err(InvariantError::UnsupportedDType(A.dt()).into()), + _ => return Err(InvariantError::UnsupportedDType(A.dtype()).into()), }; builder.write_global(a_getters); - match A.dt() { + match A.dtype() { DType::F32 | DType::F16 => { builder.write_global(wgsl! { fn getB(d0 : i32, d1 : i32, d2 : i32) -> 'accessor { @@ -347,12 +347,12 @@ impl GEMM { } DType::Q8_0F(_) | DType::Q8_0H(_) => { builder.write_global(wgsl! { - fn getB(d0 : i32, d1 : i32, d2 : i32) -> 'dt { - return 'dt(B[getBIndexFromCoords3D(vec3(d0,d1,d2)) / 'W]); + fn getB(d0 : i32, d1 : i32, d2 : i32) -> 'dtype { + return 'dtype(B[getBIndexFromCoords3D(vec3(d0,d1,d2)) / 'W]); } }); } - _ => return Err(InvariantError::UnsupportedDType(A.dt()).into()), + _ => return Err(InvariantError::UnsupportedDType(A.dtype()).into()), } Ok(()) @@ -390,7 +390,7 @@ impl GEMM { } }; - let aAccessor = match self.lhs.dt() { + let aAccessor = match self.lhs.dtype() { DType::Q8_0F(_) => Vec4::::render_type(), DType::Q8_0H(_) => Vec4::::render_type(), _ => accessor.clone(), @@ -467,7 +467,7 @@ impl GEMM { const TILE_DIM: usize = 32; let accessor = P::render_type(); - let dt = P::T::DT; + let dtype = P::T::DT; let W = P::W; let T_W = TILE_DIM / W; kernel_builder.write_global(wgsl! { @@ -499,7 +499,7 @@ impl GEMM { // Loop over shared dimension. }); - let a_inner = match self.lhs.dt() { + let a_inner = match self.lhs.dtype() { DType::F32 | DType::F16 => { wgsl! { for (var innerCol = 0; innerCol < 'ROW_PER_THREAD; innerCol++) { @@ -602,7 +602,7 @@ impl GEMM { }; kernel_builder.write_main(wgsl! { - val = 'dt(acc['row]['col]) + 'bias_val; + val = 'dtype(acc['row]['col]) + 'bias_val; 'writer }); } @@ -655,7 +655,7 @@ impl GEMM { let tileRowB = localRow * 'ROW_PER_THREAD; }); - let load_a_inner = match self.lhs.dt() { + let load_a_inner = match self.lhs.dtype() { DType::F32 | DType::F16 => { wgsl! { mm_Asub[inputRow][inputCol] = mm_readA(batchA, globalRow + innerRow, kStart + inputCol * 'W); } } @@ -694,7 +694,7 @@ impl GEMM { let mut outer_body = WgslFragment::new(128); let mut inner_body = WgslFragment::new(128); for c in 0..W { - let bIdent = format!("BCached{}", c); + let bIdent = format!("BCached{c}"); inner_body.write(wgsl! { acc[i] += 'fp32_accessor('accessor(ACached['c]) * 'bIdent); }); diff --git a/crates/ratchet-core/src/ops/matmul/mod.rs b/crates/piston-core/src/ops/matmul/mod.rs similarity index 80% rename from crates/ratchet-core/src/ops/matmul/mod.rs rename to crates/piston-core/src/ops/matmul/mod.rs index c069fb86..a1a23b1f 100644 --- a/crates/ratchet-core/src/ops/matmul/mod.rs +++ b/crates/piston-core/src/ops/matmul/mod.rs @@ -4,18 +4,19 @@ mod subgroup_gemv; mod workgroup_gemv; pub use gemm::*; +use piston_macros::IrFields; pub use quantized::*; -use ratchet_macros::IrFields; pub use subgroup_gemv::*; pub use workgroup_gemv::*; use std::{cmp::Ordering, mem}; use crate::{ + DType, Device, GPUOperation, Kernel, KernelElement, KernelKey, KernelMetadata, + KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, Q4_KF, Q4_KH, + Q8_0F, Q8_0H, RVec, Shape, StorageView, Stride, WorkgroupSize, Workload, gpu::{BindGroupLayoutDescriptor, CpuUniform}, - rvec, DType, Device, GPUOperation, Kernel, KernelElement, KernelKey, KernelMetadata, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Shape, StorageView, - Strides, Tensor, WorkgroupSize, Workload, Q4_KF, Q4_KH, Q8_0F, Q8_0H, + rvec, }; //https://link.springer.com/chapter/10.1007/978-3-642-29737-3_42 @@ -48,16 +49,16 @@ impl GEMVHeuristic { #[allow(dead_code)] #[derive(Debug, Clone)] pub struct MatmulSpec { - lhs_dt: DType, - rhs_dt: DType, + lhs_dtype: DType, + rhs_dtype: DType, raw_lhs_shape: Shape, raw_rhs_shape: Shape, lhs_shape: Shape, rhs_shape: Shape, dst_shape: Shape, - lhs_strides: Strides, - rhs_strides: Strides, - dst_strides: Strides, + lhs_stride: Stride, + rhs_stride: Stride, + dst_stride: Stride, lhs_stack: usize, rhs_stack: usize, dst_stack: usize, @@ -73,8 +74,8 @@ impl MatmulSpec { pub const TILE_DIM: usize = 32; pub fn new( - LHS: &Tensor, - RHS: &Tensor, + LHS: &OpTensor, + RHS: &OpTensor, trans_lhs: bool, trans_rhs: bool, trans_dst: bool, @@ -87,19 +88,19 @@ impl MatmulSpec { let mut trans_lhs = trans_lhs; let mut trans_rhs = trans_rhs; - let lhs_dt = LHS.dt(); - let rhs_dt = RHS.dt(); + let lhs_dtype = LHS.dtype(); + let rhs_dtype = RHS.dtype(); - if (lhs_shape.rank() < 2) || (rhs_shape.rank() < 2) { + if (lhs_shape.dim() < 2) || (rhs_shape.dim() < 2) { panic!("MatMul: inputs must be at least 2D"); } - match lhs_shape.rank().cmp(&rhs_shape.rank()) { + match lhs_shape.dim().cmp(&rhs_shape.dim()) { Ordering::Less => { - lhs_shape.left_pad_to(1, rhs_shape.rank()); + lhs_shape.left_pad_to(1, rhs_shape.dim()); } Ordering::Greater => { - rhs_shape.left_pad_to(1, lhs_shape.rank()); + rhs_shape.left_pad_to(1, lhs_shape.dim()); } _ => {} }; @@ -108,7 +109,7 @@ impl MatmulSpec { Matmul::compute_dst_shape(&lhs_shape, &rhs_shape, trans_lhs, trans_rhs, trans_dst) .unwrap(); - let stack_dims = dst_shape.rank() - 2; + let stack_dims = dst_shape.dim() - 2; let stack_shape = dst_shape.slice(0..stack_dims); let lhs_stack = lhs_shape.drain(0..stack_dims).product(); @@ -121,17 +122,17 @@ impl MatmulSpec { assert!(lhs_stack == rhs_stack && rhs_stack == dst_stack); } - if lhs_shape.rank() == 1 { + if lhs_shape.dim() == 1 { lhs_shape.insert(0, 1); } - if rhs_shape.rank() == 1 { + if rhs_shape.dim() == 1 { rhs_shape.insert(0, 1); } - let mut lhs_strides = Strides::from(&lhs_shape); - let mut rhs_strides = Strides::from(&rhs_shape); - let dst_strides = Strides::from(&dst_shape); + let mut lhs_stride = Stride::from(&lhs_shape); + let mut rhs_stride = Stride::from(&rhs_shape); + let dst_stride = Stride::from(&dst_shape); let is_cpu = matches!(LHS.device(), Device::CPU); @@ -141,18 +142,18 @@ impl MatmulSpec { // This is just the xor operator (^). if trans_lhs ^ trans_dst { lhs_shape.transpose(); - lhs_strides.transpose(); + lhs_stride.transpose(); } if trans_rhs ^ trans_dst { rhs_shape.transpose(); - rhs_strides.transpose(); + rhs_stride.transpose(); } if trans_dst { // (a b)T => bT aT // aT bT has already been applied correctly above, so we can just swap. mem::swap(&mut lhs_shape, &mut rhs_shape); // strides and transposes must follow their shapes - mem::swap(&mut lhs_strides, &mut rhs_strides); + mem::swap(&mut lhs_stride, &mut rhs_stride); mem::swap(&mut trans_lhs, &mut trans_rhs); } } @@ -165,16 +166,16 @@ impl MatmulSpec { let heuristic = GEMVHeuristic::new(rhs_shape[0], rhs_shape[1]); Self { - lhs_dt, - rhs_dt, + lhs_dtype, + rhs_dtype, raw_lhs_shape, raw_rhs_shape, lhs_shape, rhs_shape, dst_shape, - lhs_strides, - rhs_strides, - dst_strides, + lhs_stride, + rhs_stride, + dst_stride, lhs_stack, rhs_stack, dst_stack, @@ -201,7 +202,7 @@ impl MatmulSpec { self.dst_shape.numel(), ]; - if checks.iter().all(|&x| x % 4 == 0) { + if checks.iter().all(|&x| x.is_multiple_of(4)) { KernelElement::Vec4 } else { KernelElement::Scalar @@ -246,16 +247,16 @@ impl MatmulSpec { &self.dst_shape } - pub fn lhs_strides(&self) -> &Strides { - &self.lhs_strides + pub fn lhs_stride(&self) -> &Stride { + &self.lhs_stride } - pub fn rhs_strides(&self) -> &Strides { - &self.rhs_strides + pub fn rhs_stride(&self) -> &Stride { + &self.rhs_stride } - pub fn dst_strides(&self) -> &Strides { - &self.dst_strides + pub fn dst_stride(&self) -> &Stride { + &self.dst_stride } pub fn dim_lhs_outer(&self) -> usize { @@ -349,8 +350,8 @@ impl MatmulSpec { self.stack_shape.numel() } - pub fn rhs_dt(&self) -> DType { - self.rhs_dt + pub fn rhs_dtype(&self) -> DType { + self.rhs_dtype } pub fn is_gemv(&self) -> bool { @@ -372,18 +373,18 @@ impl MatmulSpec { let dim_rhs_outer = self.dim_rhs_outer(); let dim_inner = self.dim_inner(); - let lhs_fit = dim_lhs_outer % Self::TILE_DIM == 0; - let rhs_fit = dim_rhs_outer % Self::TILE_DIM == 0; - let dst_fit = dim_inner % Self::TILE_DIM == 0; + let lhs_fit = dim_lhs_outer.is_multiple_of(Self::TILE_DIM); + let rhs_fit = dim_rhs_outer.is_multiple_of(Self::TILE_DIM); + let dst_fit = dim_inner.is_multiple_of(Self::TILE_DIM); (lhs_fit, rhs_fit, dst_fit) } } #[derive(derive_new::new, Debug, Clone, IrFields)] pub struct Matmul { - pub(crate) lhs: Tensor, - pub(crate) rhs: Tensor, - pub(crate) bias: Option, + pub(crate) lhs: OpTensor, + pub(crate) rhs: OpTensor, + pub(crate) bias: Option, pub(crate) trans_lhs: bool, pub(crate) trans_rhs: bool, pub(crate) trans_dst: bool, @@ -400,8 +401,8 @@ impl Matmul { let mut lhs_shape = lhs_shape.clone(); let mut rhs_shape = rhs_shape.clone(); - let implicit_m = lhs_shape.rank() < 2; - let implicit_n = rhs_shape.rank() < 2; + let implicit_m = lhs_shape.dim() < 2; + let implicit_n = rhs_shape.dim() < 2; if implicit_m { lhs_shape.insert(trans_lhs as usize, 1); } @@ -410,15 +411,15 @@ impl Matmul { } let equalize_rank = |shape: &mut Shape, target_rank: usize| { - while shape.rank() < target_rank { + while shape.dim() < target_rank { shape.insert(0, 1); } }; - equalize_rank(&mut lhs_shape, rhs_shape.rank()); - equalize_rank(&mut rhs_shape, lhs_shape.rank()); + equalize_rank(&mut lhs_shape, rhs_shape.dim()); + equalize_rank(&mut rhs_shape, lhs_shape.dim()); - let lhs_rank = lhs_shape.rank(); - let rhs_rank = rhs_shape.rank(); + let lhs_rank = lhs_shape.dim(); + let rhs_rank = rhs_shape.dim(); let (lhs_prefix, rhs_prefix) = (&lhs_shape[..lhs_rank - 2], &rhs_shape[..rhs_rank - 2]); let dst_broadcasted_prefix = Shape::multi_broadcast(&[&lhs_prefix.into(), &rhs_prefix.into()]).ok_or_else(|| { @@ -491,12 +492,12 @@ impl Operation for Matmul { self.trans_dst, ) .unwrap(); - let dst_strides = Strides::from(&dst_shape); - Ok(StorageView::new(dst_shape, self.rhs.dt(), dst_strides)) + let dst_stride = Stride::from(&dst_shape); + Ok(StorageView::new(dst_shape, self.rhs.dtype(), dst_stride)) } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { if let Some(bias) = &self.bias { rvec![&self.lhs, &self.rhs, bias] } else { @@ -527,22 +528,22 @@ impl OpGuards for Matmul { (DType::Q4_KH(Q4_KH::default()), DType::F16), ]; - if !allowed_pairs.contains(&(self.lhs.dt(), self.rhs.dt())) { + if !allowed_pairs.contains(&(self.lhs.dtype(), self.rhs.dtype())) { panic!( "DType mismatch: lhs: {:?}, rhs: {:?}", - self.lhs.dt(), - self.rhs.dt() + self.lhs.dtype(), + self.rhs.dtype() ); } - if let Some(bias) = &self.bias { - if bias.dt() != self.rhs.dt() { - panic!( - "DType mismatch: bias: {:?}, rhs: {:?}", - bias.dt(), - self.rhs.dt() - ); - } + if let Some(bias) = &self.bias + && bias.dtype() != self.rhs.dtype() + { + panic!( + "DType mismatch: bias: {:?}, rhs: {:?}", + bias.dtype(), + self.rhs.dtype() + ); } } } @@ -599,7 +600,7 @@ impl KernelRenderable for MatmulKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { match self { @@ -619,8 +620,8 @@ impl Kernel for MatmulKernels { &self, workgroup_size: &WorkgroupSize, inplace: bool, - srcs: &[&Tensor], - dst: &Tensor, + srcs: &[&OpTensor], + dst: &OpTensor, kernel_element: &KernelElement, ) -> KernelKey { match self { @@ -650,7 +651,7 @@ impl Kernel for MatmulKernels { fn metadata( &self, - dst: &Tensor, + dst: &OpTensor, kernel_element: &KernelElement, ) -> Result { match self { @@ -667,7 +668,7 @@ impl Kernel for MatmulKernels { } } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { match self { MatmulKernels::GEMM(kernel) => kernel.calculate_dispatch(dst), MatmulKernels::SubgroupGEMV(kernel) => kernel.calculate_dispatch(dst), @@ -676,7 +677,7 @@ impl Kernel for MatmulKernels { } } - fn kernel_element(&self, dst: &Tensor) -> KernelElement { + fn kernel_element(&self, dst: &OpTensor) -> KernelElement { match self { MatmulKernels::GEMM(kernel) => kernel.kernel_element(dst), MatmulKernels::SubgroupGEMV(kernel) => kernel.kernel_element(dst), @@ -688,7 +689,7 @@ impl Kernel for MatmulKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { match self { @@ -716,16 +717,16 @@ impl GPUOperation for Matmul { type KernelEnum = MatmulKernels; fn select_kernel(&self) -> Self::KernelEnum { - if !self.bias.as_ref().map_or(true, |b| b.shape().is_vector()) { + if !self.bias.as_ref().is_none_or(|b| b.shape().is_vector()) { panic!("Bias must be a vector: {:?}", self.bias); } - if self.lhs.dt().is_quantized() && self.trans_lhs { + if self.lhs.dtype().is_quantized() && self.trans_lhs { panic!("Transposed quantized inputs are not supported"); } let is_gemv = self.rhs.shape().is_vector() && !self.trans_lhs; - let is_q4 = self.lhs.dt().is_q4(); + let is_q4 = self.lhs.dtype().is_q4(); let supports_subgroup = self .lhs .device() @@ -752,11 +753,11 @@ impl GPUOperation for Matmul { #[cfg(all(test, feature = "pyo3"))] mod tests { - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; use crate::test_util::run_py_prg; - use crate::{quantize, shape, Device, DeviceRequest}; + use crate::{Device, DeviceRequest, Tensor, quantize, randn}; use super::*; @@ -781,21 +782,15 @@ mod tests { }; let inner = if bias.is_some() { - format!( - "torch.add(torch.matmul({}, {}), torch.from_numpy(bias))", - a_op, b_op - ) + format!("torch.add(torch.matmul({a_op}, {b_op}), torch.from_numpy(bias))") } else { - format!("torch.matmul({}, {})", a_op, b_op) + format!("torch.matmul({a_op}, {b_op})") }; let result_op = if trans_dst { - format!( - "np.ascontiguousarray(torch.permute({}, [0, 2, 1]).numpy())", - inner - ) + format!("np.ascontiguousarray(torch.permute({inner}, [0, 2, 1]).numpy())") } else { - format!("{}.numpy()", inner) + format!("{inner}.numpy()") }; let prg = format!( @@ -814,7 +809,7 @@ def matmul(a, b{}): vec![a, b] }; - run_py_prg(prg.to_string(), &args, &[], a.dt()) + run_py_prg(prg.to_string(), &args, &[], a.dtype()) } #[derive(Arbitrary, Clone, Debug)] @@ -866,8 +861,7 @@ def matmul(a, b{}): ref transpose, } = prob; println!( - "Running sgemm: B={} M={} N={} K={} has_bias={} transpose={:?}", - B, M, N, K, has_bias, transpose + "Running sgemm: B={B} M={M} N={N} K={K} has_bias={has_bias} transpose={transpose:?}" ); run_matmul_trial(&device, prob).unwrap(); } @@ -884,14 +878,12 @@ def matmul(a, b{}): ref transpose, } = prob; println!( - "Running sgemm: B={} M={} N={} K={} has_bias={} transpose={:?}", - B, M, N, K, has_bias, transpose + "Running sgemm: B={B} M={M} N={N} K={K} has_bias={has_bias} transpose={transpose:?}" ); run_matmul_trial(&device, prob).unwrap(); } fn run_matmul_trial(device: &Device, prob: SGEMMProblem) -> anyhow::Result<()> { - let cpu_device = Device::request_device(DeviceRequest::CPU)?; let SGEMMProblem { B, M, @@ -907,35 +899,22 @@ def matmul(a, b{}): } has_bias = false; - let lhs_shape = if trans_lhs { - shape![B, K, M] - } else { - shape![B, M, K] - }; + let lhs_shape = if trans_lhs { (B, K, M) } else { (B, M, K) }; - let rhs_shape = if trans_rhs { - shape![B, N, K] - } else { - shape![B, K, N] - }; + let rhs_shape = if trans_rhs { (B, N, K) } else { (B, K, N) }; let bias = if has_bias { - Some(Tensor::randn::( - 0.0, - 1.0, - shape![N], - cpu_device.clone(), - )) + Some(randn(N, None, None, Default::default())?) } else { None }; - println!("LHS shape: {:?}", lhs_shape); - println!("RHS shape: {:?}", rhs_shape); + println!("LHS shape: {lhs_shape:?}"); + println!("RHS shape: {rhs_shape:?}"); println!("Bias: {:?}", bias.as_ref().map(|b| b.shape())); - let a = Tensor::randn::(0.0, 1.0, lhs_shape, cpu_device.clone()); - let b = Tensor::randn::(0.0, 1.0, rhs_shape, cpu_device.clone()); + let a = randn(lhs_shape, None, None, Default::default())?; + let b = randn(rhs_shape, None, None, Default::default())?; let ground = ground_truth(&a, &b, bias.as_ref(), trans_lhs, trans_rhs, trans_dst)?; println!("Ground shape: {:?}", ground.shape()); @@ -945,8 +924,8 @@ def matmul(a, b{}): let c_gpu = a_gpu.gemm(b_gpu, bias_gpu, trans_lhs, trans_rhs, trans_dst)?; let d_gpu = c_gpu.to(&Device::CPU)?; - println!("RATCHET SGEMM\n{:?}\n", d_gpu); - println!("PYTORCH FP32:\n{:?}", ground); + println!("PISTON SGEMM\n{d_gpu:?}\n"); + println!("PYTORCH FP32:\n{ground:?}"); ground.all_close(&d_gpu, 1e-4, 1e-4)?; Ok(()) @@ -955,9 +934,8 @@ def matmul(a, b{}): #[test] fn test_qgemm() -> anyhow::Result<()> { let device = Device::request_device(DeviceRequest::GPU).unwrap(); - let cpu_device = Device::request_device(DeviceRequest::CPU)?; - let a = Tensor::randn::(0.0, 1.0, shape![6, 1500, 64], cpu_device.clone()); - let b = Tensor::randn::(0.0, 1.0, shape![6, 64, 1500], cpu_device.clone()); + let a = randn((6, 1500, 64), None, None, Default::default())?; + let b = randn((6, 64, 1500), None, None, Default::default())?; let ground = ground_truth(&a, &b, None, false, false, false)?; let aq = quantize::(&a); @@ -966,8 +944,8 @@ def matmul(a, b{}): let c_gpu = a_gpu.matmul(b_gpu, false, false)?; let ours = c_gpu.to(&Device::CPU)?; - println!("RATCHET QUANT\n{:?}\n", ours); - println!("PYTORCH FP32:\n{:?}", ground); + println!("PISTON QUANT\n{ours:?}\n"); + println!("PYTORCH FP32:\n{ground:?}"); ground.all_close(&ours, 1e1, 1e-1)?; @@ -979,15 +957,9 @@ def matmul(a, b{}): let _ = env_logger::builder().is_test(true).try_init(); let device = Device::request_device(DeviceRequest::GPU).unwrap(); - let cpu_device = Device::request_device(DeviceRequest::CPU)?; - let a = Tensor::randn::(0.0, 1.0, shape![2, 175, 240], cpu_device.clone()); - let b = Tensor::randn::(0.0, 1.0, shape![2, 240, 182], cpu_device.clone()); - let bias = Some(Tensor::randn::( - 0.0, - 1.0, - shape![182], - cpu_device.clone(), - )); + let a = randn((2, 175, 240), None, None, Default::default())?; + let b = randn((2, 240, 182), None, None, Default::default())?; + let bias = Some(randn(182, None, None, Default::default())?); let TRANS_LHS = false; let TRANS_RHS = false; @@ -1008,8 +980,14 @@ def matmul(a, b{}): let c_gpu = a_gpu.gemm(b_gpu, bias_gpu, TRANS_LHS, TRANS_RHS, TRANS_DST)?; let ours = c_gpu.to(&Device::CPU)?; - println!("RATCHET\n{:?}\n", ours.to_ndarray_view::()); - println!("PYTORCH:\n{:?}", ground.to_ndarray_view::()); + println!( + "PISTON\n{:?}\n", + ours.inner().read().to_ndarray_view::() + ); + println!( + "PYTORCH:\n{:?}", + ground.inner().read().to_ndarray_view::() + ); ground.all_close(&ours, 1e-3, 1e-3)?; Ok(()) @@ -1021,9 +999,8 @@ def matmul(a, b{}): let _ = env_logger::builder().is_test(true).try_init(); let device = Device::request_device(DeviceRequest::GPU).unwrap(); - let cpu_device = Device::request_device(DeviceRequest::CPU)?; - let a = Tensor::randn::(0.0, 1.0, shape![1, 51865, 384], cpu_device.clone()); - let b = Tensor::randn::(0.0, 1.0, shape![1, 1, 384], cpu_device.clone()); + let a = randn((1, 51865, 384), None, None, Default::default())?; + let b = randn((1, 1, 384), None, None, Default::default())?; let TRANS_LHS = false; let TRANS_RHS = true; @@ -1043,8 +1020,14 @@ def matmul(a, b{}): let c_gpu = a_gpu.gemm(b_gpu, None, TRANS_LHS, TRANS_RHS, TRANS_DST)?; let ours = c_gpu.to(&Device::CPU)?; - println!("RATCHET\n{:?}\n", ours.to_ndarray_view::()); - println!("PYTORCH:\n{:?}", ground.to_ndarray_view::()); + println!( + "PISTON\n{:?}\n", + ours.inner().read().to_ndarray_view::() + ); + println!( + "PYTORCH:\n{:?}", + ground.inner().read().to_ndarray_view::() + ); ground.all_close(&ours, 1e-3, 1e-3)?; Ok(()) diff --git a/crates/ratchet-core/src/ops/matmul/quantized.rs b/crates/piston-core/src/ops/matmul/quantized.rs similarity index 92% rename from crates/ratchet-core/src/ops/matmul/quantized.rs rename to crates/piston-core/src/ops/matmul/quantized.rs index 20a994ed..fc19c7aa 100644 --- a/crates/ratchet-core/src/ops/matmul/quantized.rs +++ b/crates/piston-core/src/ops/matmul/quantized.rs @@ -1,11 +1,11 @@ use encase::ShaderType; use half::f16; -use ratchet_macros::WgslMetadata; +use piston_macros::WgslMetadata; use crate::{ - rvec, Array, BindGroupLayoutDescriptor, BindingMode, BuiltIn, DType, InvariantError, Kernel, - KernelElement, KernelRenderable, KernelSource, Matmul, MatmulSpec, OperationError, Scalar, - Tensor, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + Array, BindGroupLayoutDescriptor, BindingMode, BuiltIn, DType, InvariantError, Kernel, + KernelElement, KernelRenderable, KernelSource, Matmul, MatmulSpec, OpTensor, OperationError, + Scalar, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, rvec, }; use inline_wgsl::wgsl; @@ -17,9 +17,9 @@ pub struct QuantizedMeta { #[derive(Debug, Clone)] pub struct QMatMul { - lhs: Tensor, - rhs: Tensor, - bias: Option, + lhs: OpTensor, + rhs: OpTensor, + bias: Option, trans_lhs: bool, trans_rhs: bool, trans_dst: bool, @@ -57,31 +57,31 @@ impl Kernel for QMatMul { fn metadata( &self, - _: &Tensor, + _: &OpTensor, _: &crate::KernelElement, ) -> Result { Ok(QuantizedMeta { dummy: 0 }) } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) } - fn kernel_element(&self, _: &Tensor) -> crate::KernelElement { + fn kernel_element(&self, _: &OpTensor) -> crate::KernelElement { KernelElement::Scalar } fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.spec.select_kernel_element(); - match (self.lhs.dt(), kernel_element) { + match (self.lhs.dtype(), kernel_element) { (DType::Q4_KF(_), _) => self.render::>(inplace, dst, workgroup_size), (DType::Q4_KH(_), _) => self.render::>(inplace, dst, workgroup_size), - _ => Err(InvariantError::UnsupportedDType(self.lhs.dt()).into()), + _ => Err(InvariantError::UnsupportedDType(self.lhs.dtype()).into()), } } @@ -90,14 +90,14 @@ impl Kernel for QMatMul { _inplace: bool, ) -> Result { let (LHS, RHS, bias) = (&self.lhs, &self.rhs, &self.bias); - let layout = match (LHS.dt(), RHS.dt(), bias.is_some()) { + let layout = match (LHS.dtype(), RHS.dtype(), bias.is_some()) { (DType::Q4_KH(_) | DType::Q4_KF(_), DType::F32 | DType::F16, false) => { BindGroupLayoutDescriptor::nthary(5) } (DType::Q4_KH(_) | DType::Q4_KF(_), DType::F32 | DType::F16, true) => { BindGroupLayoutDescriptor::nthary(6) } - _ => return Err(InvariantError::UnsupportedDType(RHS.dt()).into()), + _ => return Err(InvariantError::UnsupportedDType(RHS.dtype()).into()), }; Ok(layout) } @@ -116,7 +116,7 @@ impl KernelRenderable for QMatMul { let scalar_u32 = Array::>::default(); let ro = BindingMode::ReadOnly; - match A.dt() { + match A.dtype() { DType::Q4_KF(_) | DType::Q4_KH(_) => { builder.register_storage("A", ro, scalar_u32); builder.register_storage("scales", ro, scalar_u32); @@ -128,7 +128,7 @@ impl KernelRenderable for QMatMul { } builder.register_storage("result", BindingMode::ReadWrite, farr); } - _ => return Err(InvariantError::UnsupportedDType(A.dt()).into()), + _ => return Err(InvariantError::UnsupportedDType(A.dtype()).into()), } builder.register_uniform(); @@ -138,7 +138,7 @@ impl KernelRenderable for QMatMul { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -273,7 +273,7 @@ impl KernelRenderable for QMatMul { }); let x = kernel_builder.build()?; - println!("{}", x); + println!("{x}"); Ok(x) } } diff --git a/crates/ratchet-core/src/ops/matmul/subgroup_gemv.rs b/crates/piston-core/src/ops/matmul/subgroup_gemv.rs similarity index 87% rename from crates/ratchet-core/src/ops/matmul/subgroup_gemv.rs rename to crates/piston-core/src/ops/matmul/subgroup_gemv.rs index 084c6ddd..95458b53 100644 --- a/crates/ratchet-core/src/ops/matmul/subgroup_gemv.rs +++ b/crates/piston-core/src/ops/matmul/subgroup_gemv.rs @@ -1,22 +1,22 @@ -use crate::gpu::dtype::WgslDType; use crate::gpu::WgslPrimitive; +use crate::gpu::dtype::WgslDType; use crate::{ - rvec, wgc, wgs, Array, BindGroupLayoutDescriptor, BindingMode, BuiltIn, DType, InvariantError, - KernelElement, KernelKey, KernelRenderable, KernelSource, Matmul, MatmulSpec, OperationError, - Scalar, Tensor, Vec4, WgslFragment, WgslKernelBuilder, WorkgroupSize, Workload, + Array, BindGroupLayoutDescriptor, BindingMode, BuiltIn, DType, InvariantError, KernelElement, + KernelKey, KernelRenderable, KernelSource, Matmul, MatmulSpec, OpTensor, OperationError, + Scalar, Vec4, WgslFragment, WgslKernelBuilder, WorkgroupSize, Workload, rvec, wgc, wgs, }; use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; use num_traits::Zero; -use ratchet_macros::WgslMetadata; +use piston_macros::WgslMetadata; use crate::Kernel; pub struct SubgroupGEMV { - lhs: Tensor, - rhs: Tensor, - bias: Option, + lhs: OpTensor, + rhs: OpTensor, + bias: Option, trans_lhs: bool, trans_rhs: bool, trans_dst: bool, @@ -61,14 +61,14 @@ impl KernelRenderable for SubgroupGEMV { let bias = &self.bias; let float_arr = Array::

::default(); - if A.dt().is_float() { + if A.dtype().is_float() { builder.register_storage("mat", BindingMode::ReadOnly, float_arr); builder.register_storage("inVec", BindingMode::ReadOnly, float_arr); if bias.is_some() { builder.register_storage("bias", BindingMode::ReadOnly, float_arr); } builder.register_storage("outVec", BindingMode::ReadWrite, float_arr); - } else if A.dt().is_quantized() { + } else if A.dtype().is_quantized() { let scalar = Array::>::default(); let u32_arr = Array::>::default(); builder.register_storage("mat", BindingMode::ReadOnly, u32_arr); @@ -79,7 +79,7 @@ impl KernelRenderable for SubgroupGEMV { } builder.register_storage("outVec", BindingMode::ReadWrite, scalar); } else { - return Err(InvariantError::UnsupportedDType(A.dt()).into()); + return Err(InvariantError::UnsupportedDType(A.dtype()).into()); } builder.register_uniform(); @@ -89,7 +89,7 @@ impl KernelRenderable for SubgroupGEMV { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { const TM: usize = 4; @@ -110,31 +110,31 @@ impl KernelRenderable for SubgroupGEMV { ); kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - kernel_builder.write_unpack(self.lhs.dt()); + kernel_builder.write_unpack(self.lhs.dtype()); self.register_bindings::

(&mut kernel_builder, inplace) .unwrap(); - let dt = P::T::DT; + let dtype = P::T::DT; let work_size = BN * TN * 2; kernel_builder.write_global(wgsl! { - var tgpMemory: array<'dt, 'work_size>; + var tgpMemory: array<'dtype, 'work_size>; }); let zero = P::T::zero().render(); - let thread_locals = match self.lhs.dt() { + let thread_locals = match self.lhs.dtype() { DType::F32 | DType::F16 => { wgsl! { var result: array; - var inter: array<'dt, 'TN>; - var vCoeff: array<'dt, 'TN>; + var inter: array<'dtype, 'TN>; + var vCoeff: array<'dtype, 'TN>; } } DType::Q8_0F(_) | DType::Q8_0H(_) => { wgsl! { var result: array; - var inter = vec4<'dt>('zero); - var vCoeff = vec4<'dt>('zero); + var inter = vec4<'dtype>('zero); + var vCoeff = vec4<'dtype>('zero); } } _ => unimplemented!(), @@ -181,7 +181,7 @@ impl KernelRenderable for SubgroupGEMV { let edge_tgp_load = (0..TN) .map(|tn| { wgsl! { - tgpMemory[inVecBlockOffset + 'tn] = select('dt(0.0), inVec[inVecBatchOffset + bn + 'tn], bn + 'tn < metadata.IVL); + tgpMemory[inVecBlockOffset + 'tn] = select('dtype(0.0), inVec[inVecBatchOffset + bn + 'tn], bn + 'tn < metadata.IVL); } .into() }) @@ -227,19 +227,19 @@ impl KernelRenderable for SubgroupGEMV { .map(|tm| { if self.bias.is_some() { wgsl! { - outVec[outVecBatchOffset + outRow + 'tm] = 'dt(result['tm]) + bias[outRow + 'tm]; + outVec[outVecBatchOffset + outRow + 'tm] = 'dtype(result['tm]) + bias[outRow + 'tm]; } .into() } else { wgsl! { - outVec[outVecBatchOffset + outRow + 'tm] = 'dt(result['tm]); + outVec[outVecBatchOffset + outRow + 'tm] = 'dtype(result['tm]); } .into() } }) .collect::(); - let work_loop_inner = match self.lhs.dt() { + let work_loop_inner = match self.lhs.dtype() { DType::F32 | DType::F16 => { wgsl! { // Load for the row @@ -319,8 +319,8 @@ impl Kernel for SubgroupGEMV { &self, workgroup_size: &WorkgroupSize, inplace: bool, - srcs: &[&Tensor], - dst: &Tensor, + srcs: &[&OpTensor], + dst: &OpTensor, kernel_element: &KernelElement, ) -> KernelKey { let (a_fit, b_fit, out_fit) = self.spec.tile_fit(); @@ -348,14 +348,14 @@ impl Kernel for SubgroupGEMV { ) } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { Ok(SubgroupGEMVMeta { OVL: self.spec.new_dim_lhs_outer() as _, IVL: self.spec.new_dim_rhs_outer() as _, }) } - fn calculate_dispatch(&self, _: &Tensor) -> Result { + fn calculate_dispatch(&self, _: &OpTensor) -> Result { let tile_size = 32; Ok(Workload { workgroup_count: wgc![ @@ -367,7 +367,7 @@ impl Kernel for SubgroupGEMV { }) } - fn kernel_element(&self, _: &Tensor) -> KernelElement { + fn kernel_element(&self, _: &OpTensor) -> KernelElement { KernelElement::Scalar } @@ -376,7 +376,7 @@ impl Kernel for SubgroupGEMV { _inplace: bool, ) -> Result { let (LHS, RHS, bias) = (&self.lhs, &self.rhs, &self.bias); - let layout = match (LHS.dt(), RHS.dt(), bias.is_some()) { + let layout = match (LHS.dtype(), RHS.dtype(), bias.is_some()) { (DType::F32, DType::F32, false) => BindGroupLayoutDescriptor::binary(), (DType::F32, DType::F32, true) => BindGroupLayoutDescriptor::ternary(), (DType::F16, DType::F16, false) => BindGroupLayoutDescriptor::binary(), @@ -385,7 +385,7 @@ impl Kernel for SubgroupGEMV { (DType::Q8_0H(_), DType::F16, false) => BindGroupLayoutDescriptor::ternary(), (DType::Q8_0F(_), DType::F32, true) => BindGroupLayoutDescriptor::nthary(4), (DType::Q8_0H(_), DType::F16, true) => BindGroupLayoutDescriptor::nthary(4), - _ => return Err(InvariantError::UnsupportedDType(RHS.dt()).into()), + _ => return Err(InvariantError::UnsupportedDType(RHS.dtype()).into()), }; Ok(layout) } @@ -393,11 +393,11 @@ impl Kernel for SubgroupGEMV { fn build_kernel( &self, inplace: bool, - dst: &crate::Tensor, + dst: &crate::OpTensor, workgroup_size: &crate::WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); - match (self.lhs.dt(), kernel_element) { + match (self.lhs.dtype(), kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } diff --git a/crates/ratchet-core/src/ops/matmul/workgroup_gemv.rs b/crates/piston-core/src/ops/matmul/workgroup_gemv.rs similarity index 83% rename from crates/ratchet-core/src/ops/matmul/workgroup_gemv.rs rename to crates/piston-core/src/ops/matmul/workgroup_gemv.rs index 2fc37e6a..481066c6 100644 --- a/crates/ratchet-core/src/ops/matmul/workgroup_gemv.rs +++ b/crates/piston-core/src/ops/matmul/workgroup_gemv.rs @@ -1,12 +1,12 @@ use encase::ShaderType; use half::f16; -use ratchet_macros::WgslMetadata; +use piston_macros::WgslMetadata; use crate::{ - gpu::dtype::WgslDType, rvec, wgc, wgs, Array, BindGroupLayoutDescriptor, BindingMode, BuiltIn, - DType, InvariantError, Kernel, KernelElement, KernelKey, KernelRenderable, KernelSource, - Matmul, MatmulSpec, OperationError, Scalar, Strides, Tensor, Vec4, WgslKernelBuilder, - WgslPrimitive, WorkgroupCount, WorkgroupSize, Workload, + Array, BindGroupLayoutDescriptor, BindingMode, BuiltIn, DType, InvariantError, Kernel, + KernelElement, KernelKey, KernelRenderable, KernelSource, Matmul, MatmulSpec, OpTensor, + OperationError, Scalar, Stride, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupCount, + WorkgroupSize, Workload, gpu::dtype::WgslDType, rvec, wgc, wgs, }; use glam::IVec3; use inline_wgsl::wgsl; @@ -16,11 +16,11 @@ use num_traits::Zero; #[derive(Debug, Clone, ShaderType, WgslMetadata)] pub struct WorkgroupGEMVMeta { lhs_shape: IVec3, - lhs_strides: IVec3, + lhs_stride: IVec3, rhs_shape: IVec3, - rhs_strides: IVec3, + rhs_stride: IVec3, dst_shape: IVec3, - dst_strides: IVec3, + dst_stride: IVec3, dim_lhs_outer: i32, dim_rhs_outer: i32, dim_inner: i32, @@ -28,9 +28,9 @@ pub struct WorkgroupGEMVMeta { #[derive(Debug, Clone)] pub struct WorkgroupGEMV { - lhs: Tensor, - rhs: Tensor, - bias: Option, + lhs: OpTensor, + rhs: OpTensor, + bias: Option, trans_lhs: bool, trans_rhs: bool, trans_dst: bool, @@ -70,8 +70,8 @@ impl Kernel for WorkgroupGEMV { &self, workgroup_size: &WorkgroupSize, inplace: bool, - srcs: &[&Tensor], - dst: &Tensor, + srcs: &[&OpTensor], + dst: &OpTensor, kernel_element: &KernelElement, ) -> KernelKey { let (a_fit, b_fit, out_fit) = self.spec.tile_fit(); @@ -99,19 +99,19 @@ impl Kernel for WorkgroupGEMV { ) } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let spec = &self.spec; let mut lhs_shape = spec.lhs_shape().clone(); lhs_shape.insert(0, spec.lhs_stack()); - let lhs_strides = Strides::from(&lhs_shape); + let lhs_stride = Stride::from(&lhs_shape); let mut rhs_shape = spec.raw_rhs_shape().clone(); rhs_shape.insert(0, spec.rhs_stack()); - let rhs_strides = Strides::from(&rhs_shape); + let rhs_stride = Stride::from(&rhs_shape); let mut dst_shape = spec.dst_shape().clone(); dst_shape.insert(0, spec.stacks()); - let dst_strides = Strides::from(&dst_shape); + let dst_stride = Stride::from(&dst_shape); let dim_lhs_outer = spec.dim_lhs_outer() as i32; let dim_rhs_outer = spec.dim_rhs_outer() as i32; @@ -119,18 +119,18 @@ impl Kernel for WorkgroupGEMV { Ok(WorkgroupGEMVMeta { lhs_shape: lhs_shape.into(), - lhs_strides: lhs_strides.into(), + lhs_stride: lhs_stride.into(), rhs_shape: rhs_shape.into(), - rhs_strides: rhs_strides.into(), + rhs_stride: rhs_stride.into(), dst_shape: dst_shape.into(), - dst_strides: dst_strides.into(), + dst_stride: dst_stride.into(), dim_lhs_outer, dim_rhs_outer, dim_inner, }) } - fn calculate_dispatch(&self, _: &Tensor) -> Result { + fn calculate_dispatch(&self, _: &OpTensor) -> Result { //GEMV workgroup style let (TX, TY) = self.spec.heuristic.as_workgroup_size(); let group_x = WorkgroupCount::div_ceil(self.spec.lhs_shape()[0], TX); @@ -141,18 +141,18 @@ impl Kernel for WorkgroupGEMV { }) } - fn kernel_element(&self, _: &Tensor) -> KernelElement { + fn kernel_element(&self, _: &OpTensor) -> KernelElement { self.spec.select_kernel_element() } fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = KernelElement::Scalar; - match (self.lhs.dt(), kernel_element) { + match (self.lhs.dtype(), kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -170,7 +170,7 @@ impl Kernel for WorkgroupGEMV { _inplace: bool, ) -> Result { let (LHS, RHS, bias) = (&self.lhs, &self.rhs, &self.bias); - let layout = match (LHS.dt(), RHS.dt(), bias.is_some()) { + let layout = match (LHS.dtype(), RHS.dtype(), bias.is_some()) { (DType::F32, DType::F32, false) => BindGroupLayoutDescriptor::binary(), (DType::F32, DType::F32, true) => BindGroupLayoutDescriptor::ternary(), (DType::F16, DType::F16, false) => BindGroupLayoutDescriptor::binary(), @@ -179,7 +179,7 @@ impl Kernel for WorkgroupGEMV { (DType::Q8_0H(_), DType::F16, false) => BindGroupLayoutDescriptor::ternary(), (DType::Q8_0F(_), DType::F32, true) => BindGroupLayoutDescriptor::nthary(4), (DType::Q8_0H(_), DType::F16, true) => BindGroupLayoutDescriptor::nthary(4), - _ => return Err(InvariantError::UnsupportedDType(RHS.dt()).into()), + _ => return Err(InvariantError::UnsupportedDType(RHS.dtype()).into()), }; Ok(layout) } @@ -193,7 +193,7 @@ impl KernelRenderable for WorkgroupGEMV { ) -> Result<(), OperationError> { let (A, _, bias) = (&self.lhs, &self.rhs, &self.bias); - if A.dt().is_float() { + if A.dtype().is_float() { let float_arr = Array::

::default(); builder.register_storage("A", BindingMode::ReadOnly, float_arr); builder.register_storage("X", BindingMode::ReadOnly, float_arr); @@ -201,7 +201,7 @@ impl KernelRenderable for WorkgroupGEMV { builder.register_storage("bias", BindingMode::ReadOnly, float_arr); } builder.register_storage("result", BindingMode::ReadWrite, float_arr); - } else if A.dt().is_quantized() { + } else if A.dtype().is_quantized() { let scalar = Array::>::default(); builder.register_storage("A", BindingMode::ReadOnly, Array::>::default()); builder.register_storage("scale", BindingMode::ReadOnly, scalar); @@ -211,7 +211,7 @@ impl KernelRenderable for WorkgroupGEMV { } builder.register_storage("result", BindingMode::ReadWrite, scalar); } else { - return Err(InvariantError::UnsupportedDType(A.dt()).into()); + return Err(InvariantError::UnsupportedDType(A.dtype()).into()); } builder.register_uniform(); @@ -221,7 +221,7 @@ impl KernelRenderable for WorkgroupGEMV { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -248,7 +248,7 @@ impl KernelRenderable for WorkgroupGEMV { kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - kernel_builder.write_unpack(self.lhs.dt()); + kernel_builder.write_unpack(self.lhs.dtype()); let work_size = (workgroup_size.x * workgroup_size.y / (n as u32)).render(); kernel_builder.write_global(wgsl! { @@ -256,13 +256,13 @@ impl KernelRenderable for WorkgroupGEMV { }); let (TILE_X, _) = self.spec.heuristic.as_workgroup_size(); - let A_FIT = self.spec.lhs_shape()[1] % TILE_X == 0; + let A_FIT = self.spec.lhs_shape()[1].is_multiple_of(TILE_X); - let readA = match (A_FIT, self.lhs.dt()) { + let readA = match (A_FIT, self.lhs.dtype()) { (true, DType::F32) | (true, DType::F16) => { wgsl! { fn readA(batch: i32, row: i32, col: i32) -> 'scalar { - return A[dot(metadata.lhs_strides, vec3(batch, row, col))]; + return A[dot(metadata.lhs_stride, vec3(batch, row, col))]; } } } @@ -271,7 +271,7 @@ impl KernelRenderable for WorkgroupGEMV { fn readA(batch: i32, row: i32, col: i32) -> 'scalar { var val = 'zero; if (row <= metadata.lhs_shape.y) { - val = A[dot(metadata.lhs_strides, vec3(batch, row, col))]; + val = A[dot(metadata.lhs_stride, vec3(batch, row, col))]; } return val; } @@ -280,7 +280,7 @@ impl KernelRenderable for WorkgroupGEMV { (true, DType::Q8_0F(_)) | (true, DType::Q8_0H(_)) => { wgsl! { fn readA(batch: i32, row: i32, col: i32) -> vec4<'scalar> { - return unpack(A[dot(metadata.lhs_strides, vec3(batch, row, col))]); + return unpack(A[dot(metadata.lhs_stride, vec3(batch, row, col))]); } } } @@ -297,20 +297,20 @@ impl KernelRenderable for WorkgroupGEMV { }); kernel_builder.write_main(wgsl! { - let aOffset = metadata.lhs_strides.x * batchA / 'n; - let bOffset = metadata.rhs_strides.x * batchB / 'n; - let outOffset = metadata.dst_strides.x * batch / 'n; + let aOffset = metadata.lhs_stride.x * batchA / 'n; + let bOffset = metadata.rhs_stride.x * batchB / 'n; + let outOffset = metadata.dst_stride.x * batch / 'n; }); kernel_builder.write_main(wgsl! { var sum = 'fp32_accessor(0.0); }); kernel_builder - .write_main(wgsl! { let aIndex = aOffset + row * metadata.lhs_strides.y / 'n; }); + .write_main(wgsl! { let aIndex = aOffset + row * metadata.lhs_stride.y / 'n; }); let workgroup_size_y = workgroup_size.y; - let main_loop = match self.lhs.dt() { + let main_loop = match self.lhs.dtype() { DType::Q8_0F(_) | DType::Q8_0H(_) => { wgsl! { - let sIndex = (aOffset / 4) + row * metadata.lhs_strides.y / 32; + let sIndex = (aOffset / 4) + row * metadata.lhs_stride.y / 32; for (var k = i32(global_invocation_id.y); k < metadata.dim_inner / 4; k+='workgroup_size_y / 4) { sum += 'fp32_accessor(unpack(A[aIndex + k]) * scale[sIndex + (k/8)] * X[k]); } diff --git a/crates/ratchet-core/src/ops/mod.rs b/crates/piston-core/src/ops/mod.rs similarity index 80% rename from crates/ratchet-core/src/ops/mod.rs rename to crates/piston-core/src/ops/mod.rs index 833692bc..af5c0ea9 100644 --- a/crates/ratchet-core/src/ops/mod.rs +++ b/crates/piston-core/src/ops/mod.rs @@ -1,19 +1,23 @@ mod affine; mod alibi; mod arange; +mod bernoulli; mod binary; mod cache; mod cast; mod cmp; mod concat; mod conv; -mod fill_constant; -mod fill_randn; +mod eye; +mod fill_pointwise; mod gather; mod index_add; mod index_write; +mod lerp; mod matmul; +mod multinomial; mod norm; +mod one_hot; mod powf; mod reduce; mod reindex; @@ -21,6 +25,8 @@ mod rope; mod scatter_add; mod select; mod softmax; +mod ternary; +mod topk; mod trilu; mod unary; mod view; @@ -31,35 +37,41 @@ use std::sync::Arc; pub use affine::*; pub use alibi::*; pub use arange::*; +pub use bernoulli::*; pub use binary::*; pub use cache::*; pub use cast::*; pub use cmp::*; pub use concat::*; pub use conv::*; -pub use fill_constant::*; -pub use fill_randn::*; +pub use eye::*; +pub use fill_pointwise::*; pub use gather::*; pub use index_add::*; pub use index_write::*; +pub use lerp::*; pub use matmul::*; +pub use multinomial::*; pub use norm::*; +pub use one_hot::*; +use piston_macros::IrFields; pub use powf::*; -use ratchet_macros::IrFields; pub use reduce::*; pub use reindex::*; pub use rope::*; pub use scatter_add::*; pub use select::*; pub use softmax::*; +pub use ternary::*; +pub use topk::*; pub use trilu::*; pub use unary::*; pub use view::*; pub use where_cond::*; use crate::{ - rvec, Compiled, CompiledCopy, CopyCompileKey, OpGuards, Operation, OperationError, RVec, - Storage, StorageView, Tensor, + Compiled, CompiledCopy, CopyCompileKey, OpGuards, OpTensor, Operation, OperationError, RVec, + Storage, StorageView, rvec, }; /// # KernelElement @@ -99,16 +111,16 @@ impl From<&KernelElement> for usize { #[derive(Debug, derive_new::new, Clone, IrFields)] pub struct TensorCopy { - pub src: Tensor, - pub dst: Tensor, + pub src: OpTensor, + pub dst: OpTensor, } impl TensorCopy { - pub fn src(&self) -> &Tensor { + pub fn src(&self) -> &OpTensor { &self.src } - pub fn dst(&self) -> &Tensor { + pub fn dst(&self) -> &OpTensor { &self.dst } @@ -118,7 +130,7 @@ impl TensorCopy { panic!("copy_from only supported for GPU tensors"); } // Sanity check: shape and dtype should match. - if self.src.shape() != self.dst.shape() || self.src.dt() != self.dst.dt() { + if self.src.shape() != self.dst.shape() || self.src.dtype() != self.dst.dtype() { panic!("Shape or dtype mismatch for copy_from"); } // Retrieve the underlying GPU buffers. @@ -140,7 +152,7 @@ impl TensorCopy { ))) } - pub fn create_gpu_compile_key(&self) -> CopyCompileKey { + pub fn create_gpu_compile_key(&self) -> CopyCompileKey<'_> { CopyCompileKey { src: &self.src, dst: &self.dst, @@ -151,7 +163,7 @@ impl TensorCopy { impl OpGuards for TensorCopy { fn check_shapes(&self) { let (src_shape, dst_shape) = (self.src.shape(), self.dst.shape()); - assert_eq!(src_shape.rank(), dst_shape.rank()); + assert_eq!(src_shape.dim(), dst_shape.dim()); assert_eq!(src_shape.numel(), dst_shape.numel()); } @@ -163,7 +175,7 @@ impl Operation for TensorCopy { "TensorCopy" } - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.src] } diff --git a/crates/piston-core/src/ops/multinomial.rs b/crates/piston-core/src/ops/multinomial.rs new file mode 100644 index 00000000..18f4ac21 --- /dev/null +++ b/crates/piston-core/src/ops/multinomial.rs @@ -0,0 +1,329 @@ +use derive_new::new; +use encase::ShaderType; +use half::f16; +use inline_wgsl::wgsl; +use piston_macros::{IrFields, WgslMetadata}; + +use crate::{ + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Stride, + Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::BindGroupLayoutDescriptor, rvec, +}; + +#[derive(new, Debug, Clone, IrFields)] +pub struct Multinomial { + pub probs: OpTensor, + pub num_samples: usize, + pub replacement: bool, + pub seed: Option, +} + +#[derive(Debug, derive_new::new, ShaderType, WgslMetadata)] +pub struct MultinomialMeta { + num_rows: u32, + row_size: u32, + num_samples: u32, + replacement: u32, + seed: u32, +} + +impl Operation for Multinomial { + fn name(&self) -> &'static str { + "Multinomial" + } + + fn compute_view(&self) -> Result { + // Supports [V] -> [S] and [B, V] -> [B, S] + let shape = self.probs.shape(); + assert!( + shape.dim() == 1 || shape.dim() == 2, + "Multinomial: input must be 1D or 2D" + ); + let out_shape = if shape.dim() == 1 { + crate::Shape::from(rvec![self.num_samples]) + } else { + crate::Shape::from(rvec![shape[0], self.num_samples]) + }; + let stride = Stride::from(&out_shape); + Ok(StorageView::new(out_shape, crate::DType::I32, stride)) + } + + fn srcs(&self) -> RVec<&OpTensor> { + rvec![&self.probs] + } + + fn supports_inplace(&self) -> bool { + false + } +} + +impl OpGuards for Multinomial { + fn check_shapes(&self) { + let shape = self.probs.shape(); + assert!( + shape.dim() == 1 || shape.dim() == 2, + "Multinomial: input must be 1D or 2D" + ); + } + + fn check_dtypes(&self) { + assert!( + self.probs.dtype().is_float(), + "Multinomial: probs must be float dtype" + ); + } +} + +pub enum MultinomialKernels { + Standard(Multinomial), +} + +impl GPUOperation for Multinomial { + type KernelEnum = MultinomialKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + MultinomialKernels::Standard(self.clone()) + } +} + +impl KernelRenderable for MultinomialKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + _: bool, + ) -> Result<(), OperationError> { + builder.register_storage("X", BindingMode::ReadOnly, Array::

::default()); + builder.register_storage("Y", BindingMode::ReadWrite, Array::>::default()); + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + _: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + self.register_bindings::

(&mut kernel_builder, false)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + kernel_builder.write_global(wgsl! { + fn pcg_hash(input: u32) -> u32 { + let state = input * 747796405u + 2891336453u; + let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; + return (word >> 22u) ^ word; + } + + fn rand(seed: u32) -> f32 { + return f32(pcg_hash(seed)) / 4294967295.0; + } + + fn already_selected(row: u32, s: u32, j: u32, num_samples: u32) -> bool { + var t: u32 = 0u; + loop { + if (t >= s) { break; } + if (u32(Y[row * num_samples + t]) == j) { return true; } + t = t + 1u; + } + return false; + } + }); + + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let row = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (row >= metadata.num_rows) { + return; + } + + let base = row * metadata.row_size; + + var s: u32 = 0u; + loop { + if (s >= metadata.num_samples) { break; } + + var total: f32 = 0.0; + var j: u32 = 0u; + loop { + if (j >= metadata.row_size) { break; } + if ( + metadata.replacement == 1u + || !already_selected(row, s, j, metadata.num_samples) + ) { + total = total + X[base + j]; + } + j = j + 1u; + } + + let r = rand((row + s) ^ metadata.seed) * total; + var acc: f32 = 0.0; + var chosen: u32 = 0u; + j = 0u; + loop { + if (j >= metadata.row_size) { break; } + if ( + metadata.replacement == 1u + || !already_selected(row, s, j, metadata.num_samples) + ) { + acc = acc + X[base + j]; + if (acc >= r) { + chosen = j; + break; + } + } + j = j + 1u; + } + + Y[row * metadata.num_samples + s] = i32(chosen); + s = s + 1u; + } + }); + + Ok(kernel_builder.build()?) + } +} + +impl Kernel for MultinomialKernels { + type Metadata = MultinomialMeta; + + fn kernel_name(&self) -> String { + match self { + MultinomialKernels::Standard(_) => "multinomial".to_string(), + } + } + + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { + KernelElement::Scalar + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + let num_rows = if dst.shape().dim() == 1 { + 1 + } else { + dst.shape()[0] + }; + Ok(Workload::std(num_rows, self.kernel_element(dst))) + } + + fn storage_bind_group_layout( + &self, + _inplace: bool, + ) -> Result { + Ok(BindGroupLayoutDescriptor::unary()) + } + + fn metadata( + &self, + _dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let MultinomialKernels::Standard(inner) = self; + let shape = inner.probs.shape(); + let (num_rows, row_size) = if shape.dim() == 1 { + (1u32, shape[0] as u32) + } else { + (shape[0] as u32, shape[1] as u32) + }; + Ok(MultinomialMeta { + num_rows, + row_size, + num_samples: inner.num_samples as u32, + replacement: if inner.replacement { 1 } else { 0 }, + seed: inner.seed.unwrap_or(0), + }) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let MultinomialKernels::Standard(inner) = self; + let kernel_element = self.kernel_element(dst); + match (inner.probs.dtype(), &kernel_element) { + (DType::F32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {:?} or kernel element {:?}", + inner.probs.dtype(), + kernel_element + ))), + } + } +} + +#[cfg(all(test, feature = "pyo3"))] +mod tests { + use crate::{Device, DeviceRequest, Tensor, TensorOptions}; + + #[test] + fn multinomial_vector_single_hot_replacement_true() { + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + let probs = Tensor::from_data(vec![0.0f32, 0.0, 1.0, 0.0], 4, TensorOptions::new()) + .unwrap() + .to(&device) + .unwrap(); + // Sample 3 with replacement; should always pick index 2 + let out = probs + .multinomial(3usize, true) + .unwrap() + .to(&Device::CPU) + .unwrap(); + let vals = out.to_vec::().unwrap(); + assert!(vals.iter().all(|&x| x == 2)); + } + + #[test] + fn multinomial_matrix_rowwise_single_hot_replacement_false() { + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + // Two rows: [0,1,0] and [1,0,0] + let probs = Tensor::from_data( + vec![0.0f32, 1.0, 0.0, 1.0, 0.0, 0.0], + (2, 3), + TensorOptions::new(), + ) + .unwrap() + .to(&device) + .unwrap(); + // Sample 1 without replacement along last dim + let out = probs + .multinomial(1usize, false) + .unwrap() + .to(&Device::CPU) + .unwrap(); + let vals = out.to_vec::().unwrap(); + // Shape is [2,1] => vals[0], vals[1] + assert_eq!(vals.len(), 2); + assert_eq!(vals[0], 1); + assert_eq!(vals[1], 0); + } +} diff --git a/crates/ratchet-core/src/ops/norm/groupnorm.rs b/crates/piston-core/src/ops/norm/groupnorm.rs similarity index 78% rename from crates/ratchet-core/src/ops/norm/groupnorm.rs rename to crates/piston-core/src/ops/norm/groupnorm.rs index df2896f1..6cbb8291 100644 --- a/crates/ratchet-core/src/ops/norm/groupnorm.rs +++ b/crates/piston-core/src/ops/norm/groupnorm.rs @@ -1,5 +1,5 @@ use derive_new::new; -use ratchet_macros::IrFields; +use piston_macros::IrFields; use super::*; @@ -11,10 +11,10 @@ pub struct GroupNorm { #[cfg(all(test, feature = "pyo3"))] mod tests { - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; use crate::test_util::run_py_prg; - use crate::{rvec, shape, Device, DeviceRequest, Tensor}; + use crate::{Device, DeviceRequest, Tensor, randn, rvec}; fn ground_truth( input: &Tensor, @@ -35,7 +35,7 @@ def manual_group_norm(input, scale, bias, num_groups): Some(bias) => rvec![input, scale, bias], None => rvec![input, scale], }; - run_py_prg(prg.to_string(), &inputs, &[&num_groups], input.dt()) + run_py_prg(prg.to_string(), &inputs, &[&num_groups], input.dtype()) } fn run_norm_trial(device: &Device, problem: GroupNormProblem) -> anyhow::Result<()> { @@ -46,9 +46,9 @@ def manual_group_norm(input, scale, bias, num_groups): N, } = problem; - let input = Tensor::randn::(0., 1., shape![B, C, N], Device::CPU); - let scale = Tensor::randn::(0., 1., shape![C], Device::CPU); - let bias = Some(Tensor::randn::(0., 1., shape![C], Device::CPU)); + let input = randn((B, C, N), None, None, Default::default())?; + let scale = randn(C, None, None, Default::default())?; + let bias = Some(randn(C, None, None, Default::default())?); let ground = ground_truth(&input, &scale, bias.as_ref(), num_groups)?; @@ -56,7 +56,7 @@ def manual_group_norm(input, scale, bias, num_groups): let scale_gpu = scale.to(device)?; let bias_gpu = bias.map(|b| b.to(device)).transpose()?; - let result = input_gpu.group_norm(num_groups, scale_gpu, bias_gpu, 1e-5)?; + let result = input_gpu.group_norm(num_groups, Some(scale_gpu), bias_gpu, 1e-5)?; let ours = result.to(&Device::CPU)?; @@ -71,7 +71,7 @@ def manual_group_norm(input, scale, bias, num_groups): #[strategy(1..=1usize)] B: usize, #[strategy(2..=4usize)] - #[filter(#C % 2 != 0)] + #[filter(!#C.is_multiple_of(2))] C: usize, #[strategy(1..=1usize)] N: usize, @@ -80,7 +80,7 @@ def manual_group_norm(input, scale, bias, num_groups): #[proptest(cases = 64)] fn test_groupnorm(prob: GroupNormProblem) { let device = Device::request_device(DeviceRequest::GPU).unwrap(); - println!("prob = {:#?}", prob); + println!("prob = {prob:#?}"); run_norm_trial(&device, prob).unwrap(); } } diff --git a/crates/ratchet-core/src/ops/norm/mod.rs b/crates/piston-core/src/ops/norm/mod.rs similarity index 72% rename from crates/ratchet-core/src/ops/norm/mod.rs rename to crates/piston-core/src/ops/norm/mod.rs index 8fc814b9..6dd72adc 100644 --- a/crates/ratchet-core/src/ops/norm/mod.rs +++ b/crates/piston-core/src/ops/norm/mod.rs @@ -3,23 +3,24 @@ mod groupnorm; use encase::ShaderType; pub use groupnorm::GroupNorm; use half::f16; -use ratchet_macros::WgslMetadata; +use piston_macros::WgslMetadata; use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, wgc, wgs, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, StorageView, - Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Vec2, + Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, wgc, wgs, }; use derive_new::new; use inline_wgsl::wgsl; -use ratchet_macros::IrFields; +use piston_macros::IrFields; #[derive(new, Debug, Clone, IrFields)] pub struct Norm { - pub(crate) input: Tensor, - pub(crate) scale: Tensor, - pub(crate) bias: Option, + pub(crate) input: OpTensor, + pub(crate) scale: Option, + pub(crate) bias: Option, pub(crate) eps: f32, } @@ -29,7 +30,7 @@ impl OpGuards for NormOp { NormOp::LayerNorm(Norm { input, .. }) | NormOp::RMSNorm(Norm { input, .. }) => input, NormOp::GroupNorm(GroupNorm { norm, .. }) => &norm.input, }; - assert!(input.rank() >= 2); + assert!(input.dim() >= 2); } fn check_dtypes(&self) { @@ -41,10 +42,12 @@ impl OpGuards for NormOp { NormOp::GroupNorm(GroupNorm { norm, .. }) => (&norm.input, &norm.scale, &norm.bias), }; - input.dt().is_float(); - scale.dt().is_float(); - if bias.is_some() { - bias.as_ref().unwrap().dt().is_float(); + input.dtype().is_float(); + if let Some(scale) = scale { + scale.dtype().is_float(); + } + if let Some(bias) = bias { + bias.dtype().is_float(); } } } @@ -63,25 +66,20 @@ impl Operation for NormOp { } #[inline] - fn srcs(&self) -> RVec<&Tensor> { - match self { - NormOp::LayerNorm(Norm { - input, scale, bias, .. - }) => match bias { - Some(bias) => rvec![input, scale, bias], - None => rvec![input, scale], - }, - NormOp::RMSNorm(Norm { input, scale, .. }) => rvec![input, scale], - NormOp::GroupNorm(GroupNorm { - norm: Norm { - input, scale, bias, .. - }, - .. - }) => match bias { - Some(bias) => rvec![input, scale, bias], - None => rvec![input, scale], - }, + fn srcs(&self) -> RVec<&OpTensor> { + let norm = match self { + NormOp::LayerNorm(norm) | NormOp::RMSNorm(norm) => norm, + NormOp::GroupNorm(GroupNorm { norm, .. }) => norm, + }; + + let mut sources = rvec![&norm.input]; + if let Some(scale) = &norm.scale { + sources.push(scale); + } + if let Some(bias) = &norm.bias { + sources.push(bias); } + sources } } @@ -100,12 +98,20 @@ impl KernelRenderable for NormKernels { ) -> Result<(), OperationError> { let arr = Array::

::default(); builder.register_storage("X", BindingMode::ReadOnly, arr); - builder.register_storage("S", BindingMode::ReadOnly, arr); let NormKernels::Standard(inner) = self; - if !matches!(inner, NormOp::RMSNorm(_)) { + let norm = match inner { + NormOp::LayerNorm(norm) | NormOp::RMSNorm(norm) => norm, + NormOp::GroupNorm(GroupNorm { norm, .. }) => norm, + }; + + if norm.scale.is_some() { + builder.register_storage("S", BindingMode::ReadOnly, arr); + } + if norm.bias.is_some() { builder.register_storage("B", BindingMode::ReadOnly, arr); } + builder.register_storage("Y", BindingMode::ReadWrite, arr); builder.register_uniform(); Ok(()) @@ -114,7 +120,7 @@ impl KernelRenderable for NormKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -136,16 +142,16 @@ impl KernelRenderable for NormKernels { 1 => "metadata.N", 2 => "metadata.ND2", 4 => "metadata.ND4", - v => panic!("Invalid reduction length: {}", v), + v => panic!("Invalid reduction length: {v}"), }; - let dt = P::T::DT; + let dtype = P::T::DT; let accessor = P::render_type(); let BLOCK_SIZE = workgroup_size.x.render(); kernel_builder.write_global(wgsl! { var smem: array<'accessor, 'BLOCK_SIZE>; - var sum: 'dt; + var sum: 'dtype; }); kernel_builder.write_global(wgsl! { @@ -162,21 +168,22 @@ impl KernelRenderable for NormKernels { }); kernel_builder.write_main(wgsl! { var threadSum = 'accessor(0.); }); - if matches!(inner, NormOp::RMSNorm(_)) { - kernel_builder.write_main(wgsl! { let mu = 0.; }); - } else { + let X_i = if matches!(inner, NormOp::LayerNorm(_)) { Self::compute_mu::

( &mut kernel_builder, accessor.clone(), reduction_len, workgroup_size, ); + wgsl! { X[anchor + i] - mu } + } else { + wgsl! { X[anchor + i] } }; kernel_builder.write_main(wgsl! { threadSum = 'accessor(0.); for (var i: u32 = local_invocation_id.x; i < 'reduction_len; i += 'BLOCK_SIZE) { - let val = X[anchor + i] - mu; + let val = 'X_i; threadSum = fma(val, val, threadSum); } workgroupBarrier(); @@ -191,22 +198,28 @@ impl KernelRenderable for NormKernels { } let sigma = match P::W { - 1 => wgsl! { let sigma = smem[0] / 'dt(metadata.N); }, - 2 | 4 => wgsl! {let sigma = dot(smem[0], 'accessor(1.)) / 'dt(metadata.N); }, + 1 => wgsl! { let sigma = smem[0] / 'dtype(metadata.N); }, + 2 | 4 => wgsl! {let sigma = dot(smem[0], 'accessor(1.)) / 'dtype(metadata.N); }, _ => unreachable!(), }; kernel_builder.write_main(sigma); - let loop_core = if matches!(inner, NormOp::RMSNorm(_)) { - wgsl! { Y[anchor + i] = val * S[i]; } - } else { - wgsl! { Y[anchor + i] = fma(val, S[i], B[i]); } + let norm = match inner { + NormOp::LayerNorm(norm) | NormOp::RMSNorm(norm) => norm, + NormOp::GroupNorm(GroupNorm { norm, .. }) => norm, + }; + + let loop_core = match (norm.scale.is_some(), norm.bias.is_some()) { + (true, true) => wgsl! { Y[anchor + i] = fma(val, S[i], B[i]); }, + (true, false) => wgsl! { Y[anchor + i] = val * S[i]; }, + (false, true) => wgsl! { Y[anchor + i] = val + B[i]; }, + (false, false) => wgsl! { Y[anchor + i] = val; }, }; kernel_builder.write_main(wgsl! { let denom = inverseSqrt(sigma + 'accessor(metadata.eps)); for(var i: u32 = local_invocation_id.x; i < 'reduction_len; i += 'BLOCK_SIZE) { - let val = (X[anchor + i] - mu) * denom; + let val = ('X_i) * denom; 'loop_core } }); @@ -222,7 +235,7 @@ impl NormKernels { workgroup_size: &WorkgroupSize, ) { let BLOCK_SIZE = workgroup_size.x.render(); - let dt = P::T::DT; + let dtype = P::T::DT; kernel_builder.write_main(wgsl! { for (var i: u32 = local_invocation_id.x; i < 'reduction_len; i += 'BLOCK_SIZE) { threadSum += X[anchor + i]; @@ -239,8 +252,8 @@ impl NormKernels { } let mu = match P::W { - 1 => wgsl! { let mu = smem[0] / 'dt(metadata.N); }, - 2 | 4 => wgsl! {let mu = dot(smem[0], 'accessor(1.)) / 'dt(metadata.N); }, + 1 => wgsl! { let mu = smem[0] / 'dtype(metadata.N); }, + 2 | 4 => wgsl! {let mu = dot(smem[0], 'accessor(1.)) / 'dtype(metadata.N); }, _ => unreachable!(), }; kernel_builder.write_main(mu); @@ -274,10 +287,10 @@ impl Kernel for NormKernels { } } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let NormKernels::Standard(inner) = self; let input = inner.srcs()[0]; - let rank = input.rank(); + let rank = input.dim(); let meta = match inner { NormOp::RMSNorm(n) | NormOp::LayerNorm(n) => { let M = input.shape()[rank - 2] as u32; @@ -302,11 +315,11 @@ impl Kernel for NormKernels { Ok(meta) } - fn calculate_dispatch(&self, _: &Tensor) -> Result { + fn calculate_dispatch(&self, _: &OpTensor) -> Result { let NormKernels::Standard(inner) = self; let input = inner.srcs()[0]; - let rank = input.rank(); + let rank = input.dim(); let stacks = input.shape().slice(0..rank - 2).numel(); let workgroup_count = match inner { @@ -326,12 +339,12 @@ impl Kernel for NormKernels { }) } - fn kernel_element(&self, dst: &Tensor) -> KernelElement { - let rank = dst.rank(); + fn kernel_element(&self, dst: &OpTensor) -> KernelElement { + let rank = dst.dim(); let N = dst.shape()[rank - 1] as u32; - if N % 4 == 0 { + if N.is_multiple_of(4) { KernelElement::Vec4 - } else if N % 2 == 0 { + } else if N.is_multiple_of(2) { KernelElement::Vec2 } else { KernelElement::Scalar @@ -341,11 +354,11 @@ impl Kernel for NormKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); - match (dst.dt(), &kernel_element) { + match (dst.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -366,7 +379,7 @@ impl Kernel for NormKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - dst.dt(), + dst.dtype(), kernel_element ))), } @@ -377,16 +390,21 @@ impl Kernel for NormKernels { _inplace: bool, ) -> Result { let NormKernels::Standard(inner) = self; - match inner { - NormOp::LayerNorm(l) => match l.bias { - Some(_) => Ok(BindGroupLayoutDescriptor::ternary()), - None => Ok(BindGroupLayoutDescriptor::binary()), - }, - NormOp::RMSNorm(_) => Ok(BindGroupLayoutDescriptor::binary()), - NormOp::GroupNorm(l) => match l.norm.bias { - Some(_) => Ok(BindGroupLayoutDescriptor::ternary()), - None => Ok(BindGroupLayoutDescriptor::binary()), - }, + let norm = match inner { + NormOp::LayerNorm(norm) | NormOp::RMSNorm(norm) => norm, + NormOp::GroupNorm(GroupNorm { norm, .. }) => norm, + }; + + let num_input_buffers = 1 + // X (input) + if norm.scale.is_some() { 1 } else { 0 } + // S (scale) + if norm.bias.is_some() { 1 } else { 0 }; // B (bias) + + // +1 for output buffer Y + match num_input_buffers { + 1 => Ok(BindGroupLayoutDescriptor::unary()), // Only X and Y + 2 => Ok(BindGroupLayoutDescriptor::binary()), // X + (S or B) + Y + 3 => Ok(BindGroupLayoutDescriptor::ternary()), // X + S + B + Y + _ => unreachable!("Invalid number of input buffers"), } } } @@ -401,15 +419,15 @@ impl GPUOperation for NormOp { #[cfg(all(test, feature = "pyo3"))] mod tests { - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; use crate::test_util::run_py_prg; - use crate::{rvec, shape, Device, DeviceRequest, Tensor}; + use crate::{Device, DeviceRequest, Tensor, randn, rvec}; fn ground_truth( var: NormVariant, input: &Tensor, - scale: &Tensor, + scale: Option<&Tensor>, bias: Option<&Tensor>, ) -> anyhow::Result { let ln_prg = r#" @@ -433,27 +451,30 @@ def manual_rms_norm(input, scale): NormVariant::RMSNorm => rms_prg, }; - let inputs = match bias { - Some(bias) => rvec![input, scale, bias], - None => rvec![input, scale], - }; + let mut inputs = rvec![input]; + if let Some(scale) = scale { + inputs.push(scale); + } + if let Some(bias) = bias { + inputs.push(bias); + } - run_py_prg(prg.to_string(), &inputs, &[], input.dt()) + run_py_prg(prg.to_string(), &inputs, &[], input.dtype()) } fn run_norm_trial(device: &Device, problem: NormProblem) -> anyhow::Result<()> { let NormProblem { var, B, M, N } = problem; - let input = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); - let scale = Tensor::randn::(0., 1., shape![N], Device::CPU); + let input = randn((B, M, N), None, None, Default::default())?; + let scale = randn(N, None, None, Default::default())?; let bias = match var { - NormVariant::LayerNorm => Some(Tensor::randn::(0., 1., shape![N], Device::CPU)), + NormVariant::LayerNorm => Some(randn(N, None, None, Default::default())?), NormVariant::RMSNorm => None, }; let ground = match var { - NormVariant::LayerNorm => ground_truth(var, &input, &scale, bias.as_ref())?, - NormVariant::RMSNorm => ground_truth(var, &input, &scale, None)?, + NormVariant::LayerNorm => ground_truth(var, &input, Some(&scale), bias.as_ref())?, + NormVariant::RMSNorm => ground_truth(var, &input, Some(&scale), None)?, }; let input_gpu = input.to(device)?; @@ -461,8 +482,8 @@ def manual_rms_norm(input, scale): let bias_gpu = bias.map(|b| b.to(device)).transpose()?; let result = match var { - NormVariant::LayerNorm => input_gpu.layer_norm(scale_gpu, bias_gpu, 1e-5)?, - NormVariant::RMSNorm => input_gpu.rms_norm(scale_gpu, 1e-5)?, + NormVariant::LayerNorm => input_gpu.layer_norm(Some(scale_gpu), bias_gpu, 1e-5)?, + NormVariant::RMSNorm => input_gpu.rms_norm(Some(scale_gpu), 1e-5)?, }; let ours = result.to(&Device::CPU)?; @@ -496,7 +517,7 @@ def manual_rms_norm(input, scale): M: 57, N: 1001, }; - println!("prob = {:#?}", prob); + println!("prob = {prob:#?}"); run_norm_trial(&device, prob).unwrap(); } diff --git a/crates/piston-core/src/ops/one_hot.rs b/crates/piston-core/src/ops/one_hot.rs new file mode 100644 index 00000000..dd25cac7 --- /dev/null +++ b/crates/piston-core/src/ops/one_hot.rs @@ -0,0 +1,246 @@ +use crate::{ + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, Shape, StorageView, + Stride, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::BindGroupLayoutDescriptor, rvec, +}; +use derive_new::new; +use encase::ShaderType; +use inline_wgsl::wgsl; +use piston_macros::{IrFields, WgslMetadata}; + +#[derive(new, Debug, Clone, IrFields)] +pub struct OneHot { + pub indices: OpTensor, + pub num_classes: usize, +} + +#[derive(Debug, derive_new::new, ShaderType, WgslMetadata)] +pub struct OneHotMeta { + input_numel: u32, + num_classes: u32, +} + +impl OpGuards for OneHot { + fn check_shapes(&self) {} + + fn check_dtypes(&self) { + assert!( + matches!(self.indices.dtype(), DType::I32), + "one_hot: indices must be I32" + ); + } + + fn check_custom(&self) { + assert!(self.num_classes > 0, "one_hot: num_classes must be > 0"); + } +} + +impl Operation for OneHot { + fn name(&self) -> &'static str { + "OneHot" + } + + fn compute_view(&self) -> Result { + let mut out_shape = self.indices.shape().to_vec(); + out_shape.push(self.num_classes); + let shape: Shape = out_shape.into(); + let stride = Stride::from(&shape); + Ok(StorageView::new(shape, DType::I32, stride)) + } + + fn srcs(&self) -> RVec<&OpTensor> { + rvec![&self.indices] + } + + fn supports_inplace(&self) -> bool { + false + } +} + +pub enum OneHotKernels { + Standard(OneHot), +} + +impl GPUOperation for OneHot { + type KernelEnum = OneHotKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + OneHotKernels::Standard(self.clone()) + } +} + +impl KernelRenderable for OneHotKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + _: bool, + ) -> Result<(), OperationError> { + builder.register_storage("X", BindingMode::ReadOnly, Array::>::default()); + builder.register_storage("Y", BindingMode::ReadWrite, Array::>::default()); + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + _: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + self.register_bindings::

(&mut kernel_builder, false)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + let total = metadata.input_numel * metadata.num_classes; + if (index >= total) { + return; + } + + for (var i: u32 = index; i < total; i += 64u * num_workgroups.x) { + let sample_idx = i / metadata.num_classes; + let class_idx = i % metadata.num_classes; + let idx_val = i32(X[sample_idx]); + if (idx_val == i32(class_idx)) { + Y[i] = 1; + } else { + Y[i] = 0; + } + } + }); + + Ok(kernel_builder.build()?) + } +} + +impl Kernel for OneHotKernels { + type Metadata = OneHotMeta; + + fn storage_bind_group_layout( + &self, + inplace: bool, + ) -> Result { + if inplace { + panic!("Only non-inplace one_hot is supported"); + } + Ok(BindGroupLayoutDescriptor::unary()) + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + Ok(Workload::std(dst.shape().numel(), KernelElement::Scalar)) + } + + fn kernel_name(&self) -> String { + match self { + OneHotKernels::Standard(_) => "one_hot".to_string(), + } + } + + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { + KernelElement::Scalar + } + + fn metadata( + &self, + _dst: &OpTensor, + _ke: &KernelElement, + ) -> Result { + let OneHotKernels::Standard(inner) = self; + Ok(OneHotMeta { + input_numel: inner.indices.shape().numel() as u32, + num_classes: inner.num_classes as u32, + }) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + match dst.dtype() { + DType::I32 => self.render::>(inplace, dst, workgroup_size), + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {:?} for one_hot", + dst.dtype() + ))), + } + } +} + +#[cfg(all(test, feature = "pyo3"))] +mod tests { + use crate::test_util::run_py_prg; + use crate::{DType, Device, DeviceRequest, Tensor, TensorOptions, randint}; + use test_strategy::{Arbitrary, proptest}; + + fn ground_truth(indices: &Tensor, num_classes: usize) -> anyhow::Result { + let prg = r#" +import torch +def one_hot(indices, num_classes): + t = torch.from_numpy(indices).long() + out = torch.nn.functional.one_hot(t, num_classes) + return out.to(torch.int32).numpy() +"#; + run_py_prg(prg.to_string(), &[indices], &[&num_classes], DType::I32) + } + + #[derive(Arbitrary, Debug)] + struct OneHotProblem { + #[strategy(1..=3usize)] + B: usize, + #[strategy(1..=16usize)] + M: usize, + #[strategy(1..=8usize)] + N: usize, + #[strategy(2..=16usize)] + num_classes: usize, + #[strategy(1..=3usize)] + rank: usize, + } + + fn shape_for(prob: &OneHotProblem) -> crate::Shape { + match prob.rank { + 1 => crate::Shape::from(vec![prob.M]), + 2 => crate::Shape::from(vec![prob.B, prob.M]), + _ => crate::Shape::from(vec![prob.B, prob.M, prob.N]), + } + } + + #[proptest(cases = 8)] + fn test_one_hot_matches_pytorch(prob: OneHotProblem) { + let _ = env_logger::builder().is_test(true).try_init(); + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + let shape = shape_for(&prob); + + let indices = randint(0, prob.num_classes as i32, shape, TensorOptions::new()).unwrap(); + + let ground = ground_truth(&indices, prob.num_classes).unwrap(); + + let ours = indices + .clone() + .to(&device) + .unwrap() + .one_hot(prob.num_classes) + .unwrap() + .to(&crate::Device::CPU) + .unwrap(); + + let ground_f32 = ground.cast(DType::F32).unwrap(); + let ours_f32 = ours.cast(DType::F32).unwrap(); + ground_f32.all_close(&ours_f32, 0.0f32, 0.0f32).unwrap(); + } +} diff --git a/crates/piston-core/src/ops/powf.rs b/crates/piston-core/src/ops/powf.rs new file mode 100644 index 00000000..99c18012 --- /dev/null +++ b/crates/piston-core/src/ops/powf.rs @@ -0,0 +1,384 @@ +use derive_new::new; +use half::f16; +use inline_wgsl::wgsl; +use piston_macros::IrFields; + +use crate::{ + Array, BindingMode, BuiltIn, DType, DynKernelMetadata, GPUOperation, InvariantError, Kernel, + KernelElement, KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, + RVec, Scalar, Shape, StorageView, Stride, TensorTypeOrScalarEnum, Vec2, Vec4, + WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, +}; + +#[derive(new, Debug, Clone, IrFields)] +pub struct Powf { + pub src: OpTensor, + pub e: TensorTypeOrScalarEnum, +} + +impl OpGuards for Powf { + fn check_shapes(&self) { + if let TensorTypeOrScalarEnum::Tensor(e) = &self.e { + let shapes = [self.src.shape(), e.shape()]; + let broadcasted = Shape::multi_broadcast(&shapes); + assert!(broadcasted.is_some()); + } + } + + fn check_dtypes(&self) { + let a = &self.src; + assert!(matches!(a.dtype(), crate::DType::F32)); + if let TensorTypeOrScalarEnum::Tensor(e) = &self.e { + assert_eq!(self.src.dtype(), e.dtype()); + } + } +} + +impl Operation for Powf { + fn name(&self) -> &'static str { + "Powf" + } + + fn compute_view(&self) -> Result { + if let TensorTypeOrScalarEnum::Tensor(e) = &self.e { + let shapes = &[self.src.shape(), e.shape()]; + if self.src.is_scalar() || e.is_scalar() { + let other = if self.src.is_scalar() { e } else { &self.src }; + return Ok(other.storage_view().clone()); + } + let broadcasted = Shape::multi_broadcast(shapes); + if broadcasted.is_none() { + let failed = shapes.iter().map(|s| (*s).clone()).collect::>(); + return Err(InvariantError::BroadcastingFailed(failed).into()); + } + let broadcasted = broadcasted.unwrap(); + let ostride = Stride::from(&broadcasted); + Ok(StorageView::new(broadcasted, self.src.dtype(), ostride)) + } else { + Ok(self.src.storage_view().clone()) + } + } + + #[inline] + fn srcs(&self) -> RVec<&OpTensor> { + if let TensorTypeOrScalarEnum::Tensor(e) = &self.e { + rvec![&self.src, e] + } else { + rvec![&self.src] + } + } + + fn supports_inplace(&self) -> bool { + true + } +} + +pub enum PowfKernels { + Standard(Powf), +} + +impl GPUOperation for Powf { + type KernelEnum = PowfKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + PowfKernels::Standard(self.clone()) + } +} + +impl KernelRenderable for PowfKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + inplace: bool, + ) -> Result<(), OperationError> { + let arr = Array::

::default(); + let PowfKernels::Standard(inner) = self; + + if inplace { + builder.register_storage("X", BindingMode::ReadWrite, arr); + } else { + builder.register_storage("X", BindingMode::ReadOnly, arr); + } + + if let TensorTypeOrScalarEnum::Tensor(_) = &inner.e { + builder.register_storage("E", BindingMode::ReadOnly, arr); + } + + if !inplace { + builder.register_storage("Y", BindingMode::ReadWrite, arr); + } + + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + self.register_bindings::

(&mut kernel_builder, inplace)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + let N = (P::W as u32).render(); + let dtype = P::render_type(); + + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (index >= metadata.numel / 'N) { + return; + } + + let val = X[index]; + }); + + let PowfKernels::Standard(inner) = self; + + // pow(x, e) is undefined for x < 0 in Dawn, but apparently not in wgpu, + // but only when the compiler doesn't have enough information to coerce + // e into an integer. We supply e through the metadata, so, at compile-time, + // its type is unknown. + // + // Multiplying by the sign is a fix to make this shader work correctly in Chrome. + let exponent_expr = match &inner.e { + TensorTypeOrScalarEnum::Tensor(_) => "E[index]".to_string(), + TensorTypeOrScalarEnum::Scalar(_) => wgsl! { 'dtype(metadata.e) }, + }; + + let apply = if inplace { + wgsl! { + X[index] = sign(val) * pow(abs(val), 'exponent_expr); + } + } else { + wgsl! { Y[index] = sign(val) * pow(abs(val), 'exponent_expr); } + }; + + kernel_builder.write_main(apply); + Ok(kernel_builder.build()?) + } +} + +impl Kernel for PowfKernels { + type Metadata = DynKernelMetadata; + + fn kernel_name(&self) -> String { + match self { + PowfKernels::Standard(_) => "powf".to_string(), + } + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + let PowfKernels::Standard(_) = self; + Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) + } + + fn storage_bind_group_layout( + &self, + inplace: bool, + ) -> Result { + let PowfKernels::Standard(inner) = self; + match &inner.e { + TensorTypeOrScalarEnum::Tensor(_) => { + if inplace { + Ok(BindGroupLayoutDescriptor::binary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::binary()) + } + } + TensorTypeOrScalarEnum::Scalar(_) => { + if inplace { + Ok(BindGroupLayoutDescriptor::unary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::unary()) + } + } + } + } + + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let PowfKernels::Standard(inner) = self; + let mut dyn_meta = DynKernelMetadata::new(); + dyn_meta.add_field("numel", dst.shape().numel() as u32); + if let TensorTypeOrScalarEnum::Scalar(value) = &inner.e { + dyn_meta.add_field("e", *value); + } + Ok(dyn_meta) + } + + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { + let PowfKernels::Standard(inner) = self; + let a_rank = inner.src.shape().dim(); + let N = if a_rank > 0 { + inner.src.shape()[a_rank - 1] + } else { + 1 + }; + + if N.is_multiple_of(4) { + KernelElement::Vec4 + } else if N.is_multiple_of(2) { + KernelElement::Vec2 + } else { + KernelElement::Scalar + } + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let kernel_element = self.kernel_element(dst); + let PowfKernels::Standard(inner) = self; + match (inner.src.dtype(), &kernel_element) { + (DType::F32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {:?} or kernel element {:?}", + inner.src.dtype(), + kernel_element + ))), + } + } +} + +#[cfg(all(test, feature = "pyo3"))] +mod tests { + use test_strategy::{Arbitrary, proptest}; + + use crate::test_util::run_py_prg; + use crate::{Device, DeviceRequest, Tensor, randn}; + + fn ground_truth(a: &Tensor, e: f32) -> anyhow::Result { + let func_prg = r#" +import torch +def powf(a, e): + a_tensor = torch.from_numpy(a) + sign = torch.sign(a_tensor) + return (torch.pow(torch.abs(a_tensor), e) * sign).numpy() +"# + .to_string(); + + let prg = func_prg; + + run_py_prg(prg.to_string(), &[a], &[&e], a.dtype()) + } + + fn ground_truth_tensor(a: &Tensor, e: &Tensor) -> anyhow::Result { + let func_prg = r#" +import torch +def powf(a, e): + a_tensor = torch.from_numpy(a) + e_tensor = torch.from_numpy(e) + sign = torch.sign(a_tensor) + return (torch.pow(torch.abs(a_tensor), e_tensor) * sign).numpy() +"# + .to_string(); + + let prg = func_prg; + + run_py_prg(prg.to_string(), &[a, e], &[], a.dtype()) + } + + fn run_powf_trial(problem: PowfProblem, device: Device) { + let PowfProblem { B, M, N, e } = problem; + let a = randn((B, M, N), None, None, Default::default()).unwrap(); + let ground = ground_truth(&a, e).unwrap(); + + let a_gpu = a.to(&device).unwrap(); + let b = a_gpu.pow(e).unwrap(); + + let ours = b.to(&Device::CPU).unwrap(); + + ground.all_close(&ours, 1e-5, 1e-5).unwrap(); + } + + #[derive(Arbitrary, Debug)] + struct PowfProblem { + #[strategy(1..=128usize)] + B: usize, + #[strategy(1..=128usize)] + M: usize, + #[strategy(1..=128usize)] + N: usize, + #[strategy(-10.0f32..=10.0f32)] + e: f32, + } + + #[derive(Arbitrary, Debug)] + struct PowfTensorProblem { + #[strategy(1..=32usize)] + B: usize, + #[strategy(1..=32usize)] + M: usize, + #[strategy(1..=32usize)] + N: usize, + } + + fn run_powf_tensor_trial(problem: PowfTensorProblem, device: Device) { + let PowfTensorProblem { B, M, N } = problem; + let a = randn((B, M, N), None, None, Default::default()).unwrap(); + let e = randn((B, M, N), None, None, Default::default()).unwrap(); + let ground = ground_truth_tensor(&a, &e).unwrap(); + + let a_gpu = a.to(&device).unwrap(); + let e_gpu = e.to(&device).unwrap(); + let b = a_gpu.pow(e_gpu).unwrap(); + + let ours = b.to(&Device::CPU).unwrap(); + + ground.all_close(&ours, 1e-4, 1e-4).unwrap(); + } + + #[proptest(cases = 16)] + fn test_powf_scalar(prob: PowfProblem) { + let PowfProblem { B, M, N, e } = prob; + println!("B = {B}, M = {M}, N = {N}, e = {e}"); + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_powf_trial(prob, device); + } + + #[proptest(cases = 8)] + fn test_powf_tensor(prob: PowfTensorProblem) { + let PowfTensorProblem { B, M, N } = prob; + println!("B = {B}, M = {M}, N = {N}"); + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_powf_tensor_trial(prob, device); + } +} diff --git a/crates/ratchet-core/src/ops/reduce.rs b/crates/piston-core/src/ops/reduce.rs similarity index 80% rename from crates/ratchet-core/src/ops/reduce.rs rename to crates/piston-core/src/ops/reduce.rs index fef1ac4b..69a196dc 100644 --- a/crates/ratchet-core/src/ops/reduce.rs +++ b/crates/piston-core/src/ops/reduce.rs @@ -1,13 +1,14 @@ use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, wgc, wgs, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, Shape, - StorageView, Strides, Tensor, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, Shape, StorageView, + Stride, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, wgc, wgs, }; #[cfg(test)] @@ -21,6 +22,7 @@ pub enum ReduceOp { Max, ArgMin, ArgMax, + Norm2, } impl ReduceOp { @@ -31,13 +33,14 @@ impl ReduceOp { ReduceOp::Max => "max", ReduceOp::ArgMin => "argmin", ReduceOp::ArgMax => "argmax", + ReduceOp::Norm2 => "norm2", } } } #[derive(Debug, Clone, IrFields)] pub struct Reduce { - pub input: Tensor, + pub input: OpTensor, pub reduced_shape: Shape, pub keepdim: bool, pub op: ReduceOp, @@ -45,31 +48,31 @@ pub struct Reduce { src_numel: usize, dst_numel: usize, dims: RVec, - strides: RVec, + stride: RVec, el_to_reduce_per_block: usize, } impl Reduce { - pub fn new(input: Tensor, op: ReduceOp, reduce_dims: RVec, keepdim: bool) -> Self { + pub fn new(input: OpTensor, op: ReduceOp, reduce_dims: RVec, keepdim: bool) -> Self { // TODO: These are common to all reduce operations; we should make this more general - let src_stride = input.strides().to_vec(); + let src_stride = input.stride().to_vec(); let src_dims = input.shape().to_vec(); let src_numel = src_dims.iter().product(); let mut dims = rvec![]; - let mut strides = rvec![]; + let mut stride = rvec![]; let mut dst_numel: usize = 1; for (dim_idx, &d) in src_dims.iter().enumerate() { if !reduce_dims.contains(&dim_idx) { dst_numel *= d; dims.push(d); - strides.push(src_stride[dim_idx] as usize); + stride.push(src_stride[dim_idx] as usize); } } for &dim_idx in reduce_dims.iter() { dims.push(src_dims[dim_idx]); - strides.push(src_stride[dim_idx] as usize); + stride.push(src_stride[dim_idx] as usize); } // I _think_ this stays the same if we split on the right dimension @@ -90,7 +93,7 @@ impl Reduce { src_numel, dst_numel, dims, - strides, + stride, el_to_reduce_per_block: el_to_sum_per_block, } } @@ -108,7 +111,7 @@ impl Reduce { }; let smem_update = match self.op { - ReduceOp::Sum => wgsl! { + ReduceOp::Sum | ReduceOp::Norm2 => wgsl! { smem[index] += smem[index + stride]; }, ReduceOp::ArgMax | ReduceOp::ArgMin => wgsl! { @@ -144,12 +147,12 @@ impl Reduce { kernel_builder: &mut WgslKernelBuilder, ) -> Result<(), OperationError> { kernel_builder.write_global(wgsl! { - fn get_strided_index(idx: u32, num_dims: u32, shape: vec4, strides: vec4) -> u32 { + fn get_strided_index(idx: u32, num_dims: u32, shape: vec4, stride: vec4) -> u32 { var strided_i: u32 = 0; var idx_: u32 = idx; for (var d: u32 = 0; d < num_dims; d++) { var dim_idx: u32 = num_dims - 1 - d; - strided_i += (idx_ % shape[dim_idx]) * strides[dim_idx]; + strided_i += (idx_ % shape[dim_idx]) * stride[dim_idx]; idx_ /= shape[dim_idx]; } return strided_i; @@ -168,22 +171,20 @@ pub struct ReduceMeta { el_to_reduce_per_block: u32, // Hard limit of summing along 4 dimensions; we might be able to improve this. shape: glam::UVec4, - strides: glam::UVec4, + stride: glam::UVec4, } impl OpGuards for Reduce { fn check_shapes(&self) { let input = &self.input; - let rank = input.rank(); + let rank = input.dim(); for &dim in self.reduce_dims.iter() { assert!(dim < rank); } } fn check_dtypes(&self) { - let input = &self.input; assert!(self.reduce_dims.len() <= 4); - assert!(input.dt() == crate::DType::F32); } } @@ -195,6 +196,7 @@ impl Operation for Reduce { ReduceOp::Max => "Max", ReduceOp::ArgMin => "Argmin", ReduceOp::ArgMax => "Argmax", + ReduceOp::Norm2 => "Norm2", } } @@ -208,7 +210,7 @@ impl Operation for Reduce { } let output_dtype = match self.op { - ReduceOp::Sum | ReduceOp::Min | ReduceOp::Max => DType::F32, + ReduceOp::Sum | ReduceOp::Min | ReduceOp::Max | ReduceOp::Norm2 => DType::F32, ReduceOp::ArgMin | ReduceOp::ArgMax => DType::I32, }; @@ -218,12 +220,12 @@ impl Operation for Reduce { } let output_shape = Shape::from(output_shape_vec); - let strides = Strides::from(&output_shape); - Ok(StorageView::new(output_shape, output_dtype, strides)) + let stride = Stride::from(&output_shape); + Ok(StorageView::new(output_shape, output_dtype, stride)) } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.input] } @@ -253,9 +255,9 @@ impl Kernel for ReduceKernels { } } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { // let input = self.srcs()[0]; - // let rank = input.rank(); + // let rank = input.dim(); // let shape = input.shape(); // let mut min_N = 4; // for &dim in self.sum_dims.iter() { @@ -278,7 +280,7 @@ impl Kernel for ReduceKernels { KernelElement::Scalar } - fn calculate_dispatch(&self, _dst: &Tensor) -> Result { + fn calculate_dispatch(&self, _dst: &OpTensor) -> Result { let ReduceKernels::Standard(inner) = self; // This one is a little tricky let sum_dim_size = if inner.reduce_dims.len() == inner.dims.len() { @@ -304,14 +306,14 @@ impl Kernel for ReduceKernels { Ok(BindGroupLayoutDescriptor::unary()) } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let ReduceKernels::Standard(inner) = self; let mut shape = [0; 4]; for (i, &dim) in inner.dims.iter().enumerate() { shape[i] = dim as u32; } let mut strides = [0; 4]; - for (i, &stride) in inner.strides.iter().enumerate() { + for (i, &stride) in inner.stride.iter().enumerate() { strides[i] = stride as u32; } @@ -321,28 +323,31 @@ impl Kernel for ReduceKernels { num_reduce_dims: inner.reduce_dims.len() as u32, el_to_reduce_per_block: inner.el_to_reduce_per_block as u32, shape: shape.into(), - strides: strides.into(), + stride: strides.into(), }) } fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let ReduceKernels::Standard(inner) = self; let kernel_element = self.kernel_element(dst); - match (inner.input.dt(), &kernel_element) { + match (inner.input.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } (DType::F16, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } + (DType::I32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - inner.input.dt(), + inner.input.dtype(), kernel_element ))), } @@ -377,7 +382,7 @@ impl KernelRenderable for ReduceKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -396,13 +401,13 @@ impl KernelRenderable for ReduceKernels { let ReduceKernels::Standard(inner) = self; - let dt = P::T::DT; + let dtype = P::T::DT; let op = inner.op.kernel_name(); kernel_builder.write_global(wgsl! { const BLOCK_SIZE: u32 = 256u; const maxFloat: f32 = 3.402823e+38f; - var smem: array<'dt, BLOCK_SIZE>; //max 16kb + var smem: array<'dtype, BLOCK_SIZE>; //max 16kb }); match inner.op { @@ -430,14 +435,14 @@ impl KernelRenderable for ReduceKernels { }); let smem_initialize = match inner.op { - ReduceOp::Sum => wgsl! { - smem[thread_id] = 'dt(0.0); + ReduceOp::Sum | ReduceOp::Norm2 => wgsl! { + smem[thread_id] = 'dtype(0.0); }, ReduceOp::Max | ReduceOp::ArgMax => wgsl! { - smem[thread_id] = 'dt(-maxFloat); + smem[thread_id] = 'dtype(-maxFloat); }, ReduceOp::Min | ReduceOp::ArgMin => wgsl! { - smem[thread_id] = 'dt(maxFloat); + smem[thread_id] = 'dtype(maxFloat); }, }; @@ -463,6 +468,9 @@ impl KernelRenderable for ReduceKernels { ReduceOp::Sum => wgsl! { smem[thread_id] += X[strided_i]; }, + ReduceOp::Norm2 => wgsl! { + smem[thread_id] += X[strided_i] * X[strided_i]; + }, ReduceOp::Max | ReduceOp::Min => wgsl! { smem[thread_id] = 'op(smem[thread_id], X[strided_i]); }, @@ -484,7 +492,7 @@ impl KernelRenderable for ReduceKernels { kernel_builder.write_main(wgsl! { while (idx < stop_idx) { - let strided_i = get_strided_index(idx, metadata.num_dims, metadata.shape, metadata.strides); + let strided_i = get_strided_index(idx, metadata.num_dims, metadata.shape, metadata.stride); 'smem_update idx += BLOCK_SIZE; } @@ -505,6 +513,9 @@ impl KernelRenderable for ReduceKernels { ReduceOp::ArgMax | ReduceOp::ArgMin => wgsl! { Y[destination_id] = smem_index[0]; }, + ReduceOp::Norm2 => wgsl! { + Y[destination_id] = sqrt(smem[0]); + }, _ => wgsl! { Y[destination_id] = smem[0]; }, @@ -522,12 +533,10 @@ impl KernelRenderable for ReduceKernels { #[cfg(all(test, feature = "pyo3"))] mod tests { - use proptest::prelude::Just; - use proptest::prop_oneof; - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; use crate::test_util::run_py_prg; - use crate::{shape, DType, Device, DeviceRequest, Tensor, Var}; + use crate::{AllDims, DType, Device, DeviceRequest, NormOrd, Tensor, randn}; use super::ReduceOp; @@ -537,27 +546,27 @@ mod tests { dim: Option, ) -> anyhow::Result { let dim_str = match dim { - Some(d) => format!(", dim={}", d), + Some(d) => format!(", dim={d}"), None => "".to_string(), }; + let kernel_name = match op { + ReduceOp::Norm2 => "norm", + _ => op.kernel_name(), + }; let prg = match op { ReduceOp::Max | ReduceOp::Min => format!( r#" import torch def reduce(a): - return torch.{}(torch.from_numpy(a){}).values.float().numpy() + return torch.{kernel_name}(torch.from_numpy(a){dim_str}).values.float().numpy() "#, - op.kernel_name(), - dim_str ), _ => format!( r#" import torch def reduce(a): - return torch.{}(torch.from_numpy(a){}).float().numpy() + return torch.{kernel_name}(torch.from_numpy(a){dim_str}).float().numpy() "#, - op.kernel_name(), - dim_str ), }; // let out_dtype = match op { @@ -575,45 +584,43 @@ def reduce(a): dim: Option, device: Device, ) -> anyhow::Result<()> { - let a = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); + let a = randn((B, M, N), None, None, Default::default())?; let mut ground = ground_truth_forward(&a, op, dim)?; if dim.is_none() { - ground = ground.view(shape![1])?; + ground = ground.view(1)?; } let a_gpu = a.to(&device)?; let b_gpu = match dim { Some(dim) => match op { - ReduceOp::Sum => a_gpu.sum(&[dim]), - ReduceOp::Min => a_gpu.min(dim), - ReduceOp::Max => a_gpu.max(dim), - ReduceOp::ArgMin => a_gpu.argmin(dim)?.cast(DType::I32), - ReduceOp::ArgMax => a_gpu.argmax(dim)?.cast(DType::I32), + ReduceOp::Sum => a_gpu.sum(dim, false), + ReduceOp::Min => a_gpu.min(dim, false), + ReduceOp::Max => a_gpu.max(dim, false), + ReduceOp::ArgMin => a_gpu.argmin(dim, false), + ReduceOp::ArgMax => a_gpu.argmax(dim, false), + ReduceOp::Norm2 => a_gpu.norm(Some(NormOrd::Frobenius), dim, false), }, None => match op { - ReduceOp::Sum => a_gpu.sum_all(), + ReduceOp::Sum => a_gpu.sum(AllDims, false), + ReduceOp::Norm2 => a_gpu.norm(Some(NormOrd::Frobenius), AllDims, false), _ => panic!("All * not supported"), }, }?; - let ours = b_gpu.to(&Device::CPU)?.cast(DType::F32)?; - // println!("input = {:?}", a); - // println!("input strides = {:?}", a.strides()); - // println!("ours = {:?}", ours); - // println!("ground = {:?}", ground); - ground.all_close(&ours, 1e-5, 1e-5)?; + let ours = b_gpu.cast(DType::F32)?.to(&Device::CPU)?; + // println!("input = {a:?}"); + // println!("input stride = {:?}", a.stride()); + // println!("ours = {ours:?}"); + // println!("ground = {ground:?}"); + ground.all_close(&ours, 3e-5, 1e-5)?; Ok(()) } #[derive(Arbitrary, Debug)] struct ReduceProblem { - // argmin and argmax don't seem to work right now, and the reason why is probably subtle - #[strategy(prop_oneof![ - Just(ReduceOp::Sum), - Just(ReduceOp::Min), - Just(ReduceOp::Max) - ])] + #[strategy(0..=2usize)] + dim: usize, op: ReduceOp, #[strategy(1..=3usize)] B: usize, @@ -621,8 +628,6 @@ def reduce(a): M: usize, #[strategy(1..=256usize)] N: usize, - #[strategy(0..=2usize)] - dim: usize, } #[proptest(cases = 256)] @@ -659,7 +664,7 @@ def reduce(a): #[proptest(cases = 16)] fn test_sum_all(prob: SumAllProblem) { let SumAllProblem { B, M, N } = prob; - println!("B = {}, M = {}, N = {}", B, M, N); + println!("B = {B}, M = {M}, N = {N}"); let device = Device::request_device(DeviceRequest::GPU).unwrap(); run_reduce_forward_trial(B, M, N, &ReduceOp::Sum, None, device).unwrap(); } @@ -693,20 +698,19 @@ def reduce_backward(a): ) -> anyhow::Result<()> { let ReduceBackwardProblem { B, M, N } = problem; let gpu_device = device.try_gpu()?; - let a = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); + let a = randn((B, M, N), None, None, Default::default())?; let ground = ground_truth_backward(&a)?; - let a_gpu = a.to(&device)?; - let a_var = Var::from_tensor(&a_gpu)?; - let b_gpu = a_var.as_tensor().clone().sum_all()?; + let a_gpu = a.to(&device)?.requires_grad_(true)?; + let b_gpu = a_gpu.clone().sum(AllDims, false)?; - let grads = b_gpu.backward()?; + b_gpu.backward()?; gpu_device.mark_step()?; - let a_grad = grads.get(a_var.as_tensor()).unwrap().clone(); + let a_grad = a_gpu.grad().unwrap().clone(); let ours = a_grad.to(&Device::CPU)?; - println!("ours = {:?}", ours); - println!("ground = {:?}", ground); + println!("ours = {ours:?}"); + println!("ground = {ground:?}"); ground.all_close(&ours, 1e-5, 1e-5)?; Ok(()) } @@ -714,7 +718,7 @@ def reduce_backward(a): #[proptest(cases = 8)] fn test_reduce_backward(prob: ReduceBackwardProblem) { let ReduceBackwardProblem { B, M, N } = prob; - println!("B = {}, M = {}, N = {}", B, M, N); + println!("B = {B}, M = {M}, N = {N}"); let device = Device::request_device(DeviceRequest::GPU).unwrap(); run_sum_backward_trial(prob, device).unwrap(); } diff --git a/crates/ratchet-core/src/ops/reindex/broadcast.rs b/crates/piston-core/src/ops/reindex/broadcast.rs similarity index 75% rename from crates/ratchet-core/src/ops/reindex/broadcast.rs rename to crates/piston-core/src/ops/reindex/broadcast.rs index b5c8fc60..702d6cd6 100644 --- a/crates/ratchet-core/src/ops/reindex/broadcast.rs +++ b/crates/piston-core/src/ops/reindex/broadcast.rs @@ -1,9 +1,11 @@ use derive_new::new; use encase::ShaderType; -use ratchet_macros::WgslMetadata; +use piston_macros::WgslMetadata; -use crate::{rvec, OpGuards, Operation, OperationError, RVec, Shape, StorageView, Strides, Tensor}; -use ratchet_macros::IrFields; +use crate::{ + OpGuards, OpTensor, Operation, OperationError, RVec, Shape, StorageView, Stride, rvec, +}; +use piston_macros::IrFields; #[derive(Debug, WgslMetadata, ShaderType, derive_new::new)] pub struct BroadcastMeta { @@ -17,7 +19,7 @@ pub struct BroadcastMeta { #[derive(new, Debug, Clone, IrFields)] pub struct Broadcast { - pub src: Tensor, + pub src: OpTensor, to: Shape, } @@ -33,13 +35,10 @@ impl OpGuards for Broadcast { let src_shape = self.src.shape(); let to_shape = &self.to; - let sr = src_shape.rank(); - let dr = to_shape.rank(); + let sr = src_shape.dim(); + let dr = to_shape.dim(); if sr > dr { - panic!( - "Source shape cannot have more dimensions than target shape: {} > {}", - sr, dr - ); + panic!("Source shape cannot have more dimensions than target shape: {sr} > {dr}"); } let src_iter = src_shape.iter().rev(); @@ -48,15 +47,14 @@ impl OpGuards for Broadcast { for (src_dim, to_dim) in src_iter.zip(to_iter) { if *src_dim != 1 && *src_dim != *to_dim { panic!( - "Invalid broadcast: source dimension {} cannot be broadcast to {}", - src_dim, to_dim + "Invalid broadcast: source dimension {src_dim} cannot be broadcast to {to_dim}" ); } } } fn check_dtypes(&self) { - assert!(!self.src.dt().is_quantized()); + assert!(!self.src.dtype().is_quantized()); } } @@ -73,11 +71,11 @@ impl Operation for Broadcast { return Ok(self.src.storage_view().clone()); } - let strides = Strides::from(&self.to); - Ok(StorageView::new(self.to.clone(), self.src.dt(), strides)) + let stride = Stride::from(&self.to); + Ok(StorageView::new(self.to.clone(), self.src.dtype(), stride)) } - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.src] } } @@ -90,7 +88,9 @@ mod tests { }; use test_strategy::proptest; - use crate::{shape, test_util::run_py_prg, Broadcast, Device, DeviceRequest, Shape, Tensor}; + use crate::{ + Broadcast, Device, DeviceRequest, Shape, Tensor, randn, shape, test_util::run_py_prg, + }; impl Arbitrary for BroadcastProblem { type Parameters = (); @@ -117,7 +117,10 @@ mod tests { }) .prop_map(|(original_shape, to)| BroadcastProblem { op: Broadcast::new( - Tensor::randn::(0., 1., original_shape, Device::CPU), + randn(original_shape, None, None, Default::default()) + .unwrap() + .inner_or_source() + .clone(), to, ), }) @@ -137,17 +140,16 @@ import torch import numpy as np def slice(a): torch_a = torch.from_numpy(a) - return np.ascontiguousarray(torch_a.broadcast_to({}).numpy()) + return np.ascontiguousarray(torch_a.broadcast_to({args}).numpy()) "#, - args ); - run_py_prg(prg.to_string(), &[a], &[], a.dt()) + run_py_prg(prg.to_string(), &[a], &[], a.dtype()) } fn run_reindex_trial(prob: BroadcastProblem, device: Device) -> anyhow::Result<()> { - println!("\n\nBroadcast problem: {:?}", prob); + println!("\n\nBroadcast problem: {prob:?}"); let BroadcastProblem { op } = prob; - let a = op.src.clone(); + let a = op.src.wrap(); let a_gpu = a.to(&device)?; let ground = ground_truth(&a, &op.to.as_torch())?; @@ -174,7 +176,10 @@ def slice(a): let device = Device::request_device(DeviceRequest::GPU).unwrap(); let prob = BroadcastProblem { op: Broadcast::new( - Tensor::randn::(0., 1., shape![1], Device::CPU), + randn(1, None, None, Default::default()) + .unwrap() + .inner_or_source() + .clone(), shape![4, 32, 128, 128], ), }; diff --git a/crates/piston-core/src/ops/reindex/flip.rs b/crates/piston-core/src/ops/reindex/flip.rs new file mode 100644 index 00000000..63a72dab --- /dev/null +++ b/crates/piston-core/src/ops/reindex/flip.rs @@ -0,0 +1,57 @@ +use encase::ShaderType; +use piston_macros::{IrFields, WgslMetadata}; + +use crate::{OpGuards, OpTensor, Operation, OperationError, RVec, StorageView, Stride, rvec}; + +#[derive(Debug, WgslMetadata, ShaderType, derive_new::new)] +pub struct FlipMeta { + src_shape: glam::UVec4, + dst_shape: glam::UVec4, + src_stride: glam::UVec4, + dst_stride: glam::UVec4, + src_numel: u32, + dst_numel: u32, + flip_mask: glam::UVec4, +} + +#[derive(derive_new::new, Debug, Clone, IrFields)] +pub struct Flip { + pub src: OpTensor, + pub dims: RVec, +} + +impl Flip { + /// Promote dims to 4D indexing space by offsetting with pad length + pub fn promote(&self) -> RVec { + let pad_len = 4 - self.src.shape().dim(); + self.dims.iter().map(|&d| d + pad_len).collect() + } +} + +impl OpGuards for Flip { + fn check_shapes(&self) { + let rank = self.src.shape().dim(); + assert!(self.dims.iter().all(|&d| d < rank)); + // No duplicate dims + let mut seen = std::collections::HashSet::new(); + assert!(self.dims.iter().all(|d| seen.insert(*d))); + } + + fn check_dtypes(&self) {} +} + +impl Operation for Flip { + fn name(&self) -> &'static str { + "Flip" + } + + fn compute_view(&self) -> Result { + let shape = self.src.shape().clone(); + let stride = Stride::from(&shape); + Ok(StorageView::new(shape, self.src.dtype(), stride)) + } + + fn srcs(&self) -> RVec<&OpTensor> { + rvec![&self.src] + } +} diff --git a/crates/ratchet-core/src/ops/reindex/mod.rs b/crates/piston-core/src/ops/reindex/mod.rs similarity index 80% rename from crates/ratchet-core/src/ops/reindex/mod.rs rename to crates/piston-core/src/ops/reindex/mod.rs index 1b7ad10a..bd459963 100644 --- a/crates/ratchet-core/src/ops/reindex/mod.rs +++ b/crates/piston-core/src/ops/reindex/mod.rs @@ -1,13 +1,16 @@ mod broadcast; +mod flip; mod permute; mod slice; pub use broadcast::Broadcast; use broadcast::BroadcastMeta; +pub use flip::Flip; +use flip::FlipMeta; use half::f16; pub use permute::Permute; use permute::PermuteMeta; -use ratchet_macros::IrFields; +use piston_macros::IrFields; pub use slice::Slice; use derive_new::new; @@ -15,10 +18,11 @@ use inline_wgsl::wgsl; use slice::SliceMeta; use crate::{ + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelMetadata, + KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, + Shape, Stride, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, gpu::{BindGroupLayoutDescriptor, CpuUniform}, - rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelMetadata, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, Shape, - Strides, Tensor, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + rvec, }; use glam::UVec4; @@ -27,6 +31,7 @@ pub enum Reindex { Permute(Permute), Slice(Slice), Broadcast(Broadcast), + Flip(Flip), } pub enum ReindexKernels { @@ -49,7 +54,7 @@ impl KernelRenderable for ReindexKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu().unwrap(); @@ -107,6 +112,10 @@ impl KernelRenderable for ReindexKernels { // Broadcasting is valid if dims are equal, or if one of the dims is 1 var src_index = select(dst_index, vec4(0u), metadata.src_shape == vec4(1u)); }, + Reindex::Flip(_) => wgsl! { + let flipped = (metadata.src_shape - vec4(1u)) - dst_index; + var src_index = select(dst_index, flipped, metadata.flip_mask != vec4(0u)); + }, }; kernel_builder.write_main(body); @@ -134,22 +143,22 @@ impl Kernel for ReindexKernels { } } - fn kernel_element(&self, _: &Tensor) -> KernelElement { + fn kernel_element(&self, _: &OpTensor) -> KernelElement { KernelElement::Scalar } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) } fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); - match (dst.dt(), &kernel_element) { + match (dst.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -161,13 +170,17 @@ impl Kernel for ReindexKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - dst.dt(), + dst.dtype(), kernel_element ))), } } - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { let ReindexKernels::Standard(inner) = self; let srcs = inner.srcs(); let src = srcs.first().unwrap(); @@ -177,11 +190,11 @@ impl Kernel for ReindexKernels { let src_numel = src_shape.numel() as u32; let dst_numel = dst_shape.numel() as u32; - let src_strides = Strides::from(&src_shape); - let dst_strides = Strides::from(&dst_shape); + let src_stride = Stride::from(&src_shape); + let dst_stride = Stride::from(&dst_shape); - let src_stride = UVec4::from(&src_strides); - let dst_stride = UVec4::from(&dst_strides); + let src_stride = UVec4::from(&src_stride); + let dst_stride = UVec4::from(&dst_stride); let src_shape = UVec4::from(&src_shape); let dst_shape = UVec4::from(&dst_shape); @@ -222,6 +235,20 @@ impl Kernel for ReindexKernels { Reindex::Broadcast(_) => Ok(ReindexMeta::Broadcast(BroadcastMeta::new( src_shape, dst_shape, src_stride, dst_stride, src_numel, dst_numel, ))), + Reindex::Flip(f) => { + // Build flip mask: 1 for flipped axes in promoted 4D space + let promoted = f.promote(); + let mut mask = [0u32; 4]; + for &d in promoted.iter() { + if d < 4 { + mask[d] = 1; + } + } + let flip_mask = UVec4::from(mask); + Ok(ReindexMeta::Flip(FlipMeta::new( + src_shape, dst_shape, src_stride, dst_stride, src_numel, dst_numel, flip_mask, + ))) + } } } @@ -237,6 +264,7 @@ pub enum ReindexMeta { Permute(PermuteMeta), Slice(SliceMeta), Broadcast(BroadcastMeta), + Flip(FlipMeta), } impl KernelMetadata for ReindexMeta { @@ -245,6 +273,7 @@ impl KernelMetadata for ReindexMeta { ReindexMeta::Permute(p) => p.render_meta(), ReindexMeta::Slice(s) => s.render_meta(), ReindexMeta::Broadcast(b) => b.render_meta(), + ReindexMeta::Flip(f) => f.render_meta(), } } @@ -253,6 +282,7 @@ impl KernelMetadata for ReindexMeta { ReindexMeta::Permute(p) => p.write(uniform), ReindexMeta::Slice(s) => s.write(uniform), ReindexMeta::Broadcast(b) => b.write(uniform), + ReindexMeta::Flip(f) => f.write(uniform), } } } @@ -263,6 +293,7 @@ impl OpGuards for Reindex { Reindex::Permute(p) => p.check_shapes(), Reindex::Slice(s) => s.check_shapes(), Reindex::Broadcast(b) => b.check_shapes(), + Reindex::Flip(f) => f.check_shapes(), } } @@ -271,6 +302,7 @@ impl OpGuards for Reindex { Reindex::Permute(p) => p.check_dtypes(), Reindex::Slice(s) => s.check_dtypes(), Reindex::Broadcast(b) => b.check_dtypes(), + Reindex::Flip(f) => f.check_dtypes(), } } } @@ -281,6 +313,7 @@ impl Operation for Reindex { Reindex::Permute(_) => "Permute", Reindex::Slice(_) => "Slice", Reindex::Broadcast(_) => "Broadcast", + Reindex::Flip(_) => "Flip", } } @@ -289,15 +322,17 @@ impl Operation for Reindex { Reindex::Permute(p) => p.compute_view(), Reindex::Slice(s) => s.compute_view(), Reindex::Broadcast(b) => b.compute_view(), + Reindex::Flip(f) => f.compute_view(), } } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { match self { Reindex::Permute(p) => p.srcs(), Reindex::Slice(s) => s.srcs(), Reindex::Broadcast(b) => b.srcs(), + Reindex::Flip(f) => f.srcs(), } } } diff --git a/crates/ratchet-core/src/ops/reindex/permute.rs b/crates/piston-core/src/ops/reindex/permute.rs similarity index 78% rename from crates/ratchet-core/src/ops/reindex/permute.rs rename to crates/piston-core/src/ops/reindex/permute.rs index d6e2fe43..93176087 100644 --- a/crates/ratchet-core/src/ops/reindex/permute.rs +++ b/crates/piston-core/src/ops/reindex/permute.rs @@ -2,10 +2,10 @@ use std::collections::HashSet; use derive_new::new; use encase::ShaderType; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ - rvec, InvariantError, OpGuards, Operation, OperationError, RVec, StorageView, Strides, Tensor, + InvariantError, OpGuards, OpTensor, Operation, OperationError, RVec, StorageView, Stride, rvec, }; #[derive(Debug, derive_new::new, WgslMetadata, ShaderType)] @@ -21,7 +21,7 @@ pub struct PermuteMeta { #[derive(new, Debug, Clone, IrFields)] pub struct Permute { - pub src: Tensor, + pub src: OpTensor, pub dims: RVec, } @@ -51,21 +51,21 @@ impl Operation for Permute { } let mut output_shape = input_shape.clone(); - for i in 0..input_shape.rank() { + for i in 0..input_shape.dim() { output_shape[i] = input_shape[self.dims[i]]; } - let strides = Strides::from(&output_shape); - Ok(StorageView::new(output_shape, self.src.dt(), strides)) + let stride = Stride::from(&output_shape); + Ok(StorageView::new(output_shape, self.src.dtype(), stride)) } - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.src] } } impl OpGuards for Permute { fn check_shapes(&self) { - assert!(self.src.shape().rank() == self.dims.len()); + assert!(self.src.shape().dim() == self.dims.len()); assert!(self.dims.iter().all(|&x| x < 4)); //Only support 4D for now } @@ -74,9 +74,9 @@ impl OpGuards for Permute { #[cfg(all(test, feature = "pyo3"))] mod tests { - use crate::{test_util::run_py_prg, Device, DeviceRequest, Permute, Shape, Tensor}; + use crate::{Device, DeviceRequest, Permute, Shape, Tensor, randn, test_util::run_py_prg}; use proptest::prelude::*; - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; impl Arbitrary for Permute { type Parameters = (); @@ -88,7 +88,10 @@ mod tests { .prop_flat_map(|shape| (Just(shape.clone()), Just(vec![0, 1, 2, 3]).prop_shuffle())) .prop_map(|(shape, perm)| { Permute::new( - Tensor::randn::(0., 1., shape, Device::CPU), + randn(shape, None, None, Default::default()) + .unwrap() + .inner_or_source() + .clone(), perm.into(), ) }) @@ -107,20 +110,19 @@ mod tests { import torch import numpy as np def permute(a): - return np.ascontiguousarray(torch.permute(torch.from_numpy(a), {}).numpy()) + return np.ascontiguousarray(torch.permute(torch.from_numpy(a), {args}).numpy()) "#, - args ); - run_py_prg(prg.to_string(), &[a], &[], a.dt()) + run_py_prg(prg.to_string(), &[a], &[], a.dtype()) } fn run_reindex_trial(prob: PermuteProblem, device: Device) -> anyhow::Result<()> { let PermuteProblem { op } = prob; - let a = op.src.clone(); + let a = op.src.wrap(); let a_gpu = a.to(&device)?; let ground = ground_truth(&a, format!("{:?}", op.dims).as_str())?; - let ours = a_gpu.permute(&op.dims)?; + let ours = a_gpu.permute(op.dims)?; let d_gpu = ours.to(&Device::CPU)?; ground.all_close(&d_gpu, 1e-5, 1e-5)?; Ok(()) diff --git a/crates/ratchet-core/src/ops/reindex/slice.rs b/crates/piston-core/src/ops/reindex/slice.rs similarity index 85% rename from crates/ratchet-core/src/ops/reindex/slice.rs rename to crates/piston-core/src/ops/reindex/slice.rs index b77ce514..4b267021 100644 --- a/crates/ratchet-core/src/ops/reindex/slice.rs +++ b/crates/piston-core/src/ops/reindex/slice.rs @@ -1,7 +1,7 @@ use encase::ShaderType; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; -use crate::{prelude::*, OpGuards, OperationError, StorageView, Strides}; +use crate::{OpGuards, OperationError, StorageView, Stride, prelude::*}; use crate::{Operation, RVec}; use std::ops::Range; @@ -21,7 +21,7 @@ pub struct SliceMeta { /// This is a temporary, user hostile implementation. #[derive(derive_new::new, Debug, Clone, IrFields)] pub struct Slice { - pub src: Tensor, + pub src: OpTensor, pub indices: RVec>, } @@ -59,11 +59,11 @@ impl Operation for Slice { .map(|range| range.end - range.start) .collect::>() .into(); - let strides = Strides::from(&output_shape); - Ok(StorageView::new(output_shape, self.src.dt(), strides)) + let stride = Stride::from(&output_shape); + Ok(StorageView::new(output_shape, self.src.dtype(), stride)) } - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.src] } } @@ -72,7 +72,7 @@ impl Operation for Slice { mod tests { use std::ops::Range; - use crate::{test_util::run_py_prg, Device, DeviceRequest, Tensor}; + use crate::{Device, DeviceRequest, Tensor, randn, test_util::run_py_prg}; use crate::{Shape, Slice}; use proptest::prelude::*; use test_strategy::proptest; @@ -134,7 +134,10 @@ mod tests { let indices = sub_slices.into_iter().map(|sub| sub.0).collect(); SliceProblem { op: Slice::new( - Tensor::randn::(0., 1., shape.clone(), Device::CPU), + randn(shape.clone(), None, None, Default::default()) + .unwrap() + .inner_or_source() + .clone(), indices, ), } @@ -151,17 +154,16 @@ import torch import numpy as np def slice(a): torch_a = torch.from_numpy(a) - return np.ascontiguousarray(torch_a{}) + return np.ascontiguousarray(torch_a{args}).numpy() "#, - args ); - run_py_prg(prg.to_string(), &[a], &[], a.dt()) + run_py_prg(prg.to_string(), &[a], &[], a.dtype()) } fn run_reindex_trial(prob: SliceProblem, device: Device) -> anyhow::Result<()> { let SliceProblem { op } = prob; - println!("SLICE PROBLEM: {:?}", op); - let a = op.src.clone(); + println!("SLICE PROBLEM: {op:?}"); + let a = op.src.clone().wrap(); let a_gpu = a.to(&device)?; let ground = ground_truth(&a, &op.as_torch())?; diff --git a/crates/ratchet-core/src/ops/rope.rs b/crates/piston-core/src/ops/rope.rs similarity index 78% rename from crates/ratchet-core/src/ops/rope.rs rename to crates/piston-core/src/ops/rope.rs index 0fb61370..83d7c3fc 100644 --- a/crates/ratchet-core/src/ops/rope.rs +++ b/crates/piston-core/src/ops/rope.rs @@ -1,21 +1,22 @@ use derive_new::new; use encase::ShaderType; use half::f16; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::gpu::dtype::WgslDType; use crate::{ + Array, BindingMode, BuiltIn, DType, KernelElement, KernelSource, OpGuards, OpTensor, Operation, + OperationError, RVec, Scalar, StorageView, Stride, Vec2, Vec4, WgslKernelBuilder, + WgslPrimitive, WorkgroupSize, Workload, gpu::{BindGroupLayoutDescriptor, WorkgroupCount}, - rvec, wgc, wgs, Array, BindingMode, BuiltIn, DType, KernelElement, KernelSource, OpGuards, - Operation, OperationError, RVec, Scalar, StorageView, Strides, Tensor, Vec2, Vec4, - WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + rvec, wgc, wgs, }; use crate::{GPUOperation, Kernel, KernelRenderable}; use inline_wgsl::wgsl; #[derive(new, Debug, Clone, IrFields)] pub struct RoPE { - pub(crate) input: Tensor, + pub(crate) input: OpTensor, pub(crate) dim: usize, pub(crate) base: f32, pub(crate) offset: usize, @@ -25,7 +26,7 @@ pub struct RoPE { } impl RoPE { - pub fn input(&self) -> &Tensor { + pub fn input(&self) -> &OpTensor { &self.input } @@ -48,8 +49,8 @@ impl RoPE { #[derive(Debug, derive_new::new, ShaderType, WgslMetadata)] pub struct RoPEMeta { - in_strides: glam::UVec3, - out_strides: glam::UVec3, + in_stride: glam::UVec3, + out_stride: glam::UVec3, seq_len: u32, offset: u32, base: f32, @@ -60,14 +61,14 @@ impl OpGuards for RoPE { fn check_shapes(&self) { let input = &self.input; //TODO: overly restrictive - assert!(input.rank() == 4); + assert!(input.dim() == 4); assert!(input.shape()[3] >= self.dim); - assert!(self.dim % 8 == 0); + assert!(self.dim.is_multiple_of(8)); } fn check_dtypes(&self) { let input = &self.input; - assert!(input.dt().is_float()); + assert!(input.dtype().is_float()); } } @@ -81,7 +82,7 @@ impl Operation for RoPE { } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.input] } @@ -127,11 +128,13 @@ impl KernelRenderable for RoPEKernels { builder: &mut WgslKernelBuilder, inplace: bool, ) -> Result<(), OperationError> { - if !inplace { - panic!("Only inplace rope is supported"); - } let arr = Array::

::default(); - builder.register_storage("in", BindingMode::ReadWrite, arr); + if inplace { + builder.register_storage("in", BindingMode::ReadWrite, arr); + } else { + builder.register_storage("in", BindingMode::ReadOnly, arr); + builder.register_storage("out", BindingMode::ReadWrite, arr); + } builder.register_uniform(); Ok(()) } @@ -139,7 +142,7 @@ impl KernelRenderable for RoPEKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu().unwrap(); @@ -154,7 +157,7 @@ impl KernelRenderable for RoPEKernels { ); self.register_bindings::

(&mut kernel_builder, inplace)?; - let (is_backward, inner) = match self { + let (is_backward, _inner) = match self { RoPEKernels::Forward(x) => (false, x), RoPEKernels::Backward(x) => (true, x), }; @@ -163,7 +166,19 @@ impl KernelRenderable for RoPEKernels { let body_code = rope_body(is_backward); - let dt = P::T::DT; + let dtype = P::T::DT; + let write_operations = if inplace { + wgsl! { + in[out_index_1] = rx1; + in[out_index_2] = rx2; + } + } else { + wgsl! { + out[out_index_1] = rx1; + out[out_index_2] = rx2; + } + }; + kernel_builder.write_main(wgsl! { if(global_invocation_id.y >= metadata.seq_len) { return; @@ -171,26 +186,25 @@ impl KernelRenderable for RoPEKernels { let grid = vec3(num_workgroups.x * 8u, num_workgroups.y * 8u, num_workgroups.z * 1u); - let out_index_1 = dot(global_invocation_id, vec3(metadata.out_strides[2], metadata.out_strides[1], metadata.out_strides[0])); - let out_index_2 = out_index_1 + grid.x * metadata.out_strides[2]; + let out_index_1 = dot(global_invocation_id, vec3(metadata.out_stride[2], metadata.out_stride[1], metadata.out_stride[0])); + let out_index_2 = out_index_1 + grid.x * metadata.out_stride[2]; - let in_index_1 = dot(global_invocation_id, vec3(metadata.in_strides[2], metadata.in_strides[1], metadata.in_strides[0])); - let in_index_2 = in_index_1 + grid.x * metadata.in_strides[2]; + let in_index_1 = dot(global_invocation_id, vec3(metadata.in_stride[2], metadata.in_stride[1], metadata.in_stride[0])); + let in_index_2 = in_index_1 + grid.x * metadata.in_stride[2]; let L = metadata.scale * f32(global_invocation_id.y + metadata.offset); let d = f32(global_invocation_id.x) / f32(grid.x); let theta = L * exp2(-d * metadata.base); - let costheta = 'dt(cos(theta)); - let sintheta = 'dt(sin(theta)); + let costheta = 'dtype(cos(theta)); + let sintheta = 'dtype(sin(theta)); let x1 = in[in_index_1]; let x2 = in[in_index_2]; 'body_code - in[out_index_1] = rx1; - in[out_index_2] = rx2; + 'write_operations }); Ok(kernel_builder.build()?) @@ -212,12 +226,17 @@ impl Kernel for RoPEKernels { inplace: bool, ) -> Result { if inplace { - return Ok(BindGroupLayoutDescriptor::unary_inplace()); + Ok(BindGroupLayoutDescriptor::unary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::unary()) } - panic!("RoPE does not support out-of-place operation"); } - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { let inner = match self { RoPEKernels::Forward(x) => x, RoPEKernels::Backward(x) => x, @@ -228,11 +247,11 @@ impl Kernel for RoPEKernels { let mut out_shape = dst.shape().clone(); input_shape.remove(0); out_shape.remove(0); - let in_strides = Strides::from(&input_shape); - let out_strides = Strides::from(&out_shape); + let in_stride = Stride::from(&input_shape); + let out_stride = Stride::from(&out_shape); Ok(RoPEMeta::new( - (&in_strides).into(), - (&out_strides).into(), + (&in_stride).into(), + (&out_stride).into(), SL as u32, inner.offset as u32, f32::log2(inner.base), @@ -240,11 +259,11 @@ impl Kernel for RoPEKernels { )) } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { KernelElement::Scalar } - fn calculate_dispatch(&self, _dst: &Tensor) -> Result { + fn calculate_dispatch(&self, _dst: &OpTensor) -> Result { const WGSX: usize = 8; const WGSY: usize = 8; const WGSZ: usize = 1; @@ -274,7 +293,7 @@ impl Kernel for RoPEKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); @@ -282,7 +301,7 @@ impl Kernel for RoPEKernels { RoPEKernels::Forward(x) => x, RoPEKernels::Backward(x) => x, }; - match (inner.input.dt(), &kernel_element) { + match (inner.input.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -303,7 +322,7 @@ impl Kernel for RoPEKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - inner.input.dt(), + inner.input.dtype(), kernel_element ))), } @@ -312,10 +331,10 @@ impl Kernel for RoPEKernels { #[cfg(all(test, feature = "pyo3", target_os = "macos"))] mod tests { - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; use crate::test_util::run_py_prg; - use crate::{shape, Device, DeviceRequest, Tensor}; + use crate::{Device, DeviceRequest, Tensor, randn}; fn ground_truth(a: &Tensor, dim: usize, offset: usize) -> anyhow::Result { let prg = r#" @@ -330,7 +349,7 @@ def mlx_rope(input, dim, offset): mx.eval(y) return np.array(y) "#; - run_py_prg(prg.to_string(), &[a], &[&dim, &offset], a.dt()) + run_py_prg(prg.to_string(), &[a], &[&dim, &offset], a.dtype()) } fn run_rope_trial(problem: RoPEProblem, device: Device) { @@ -342,12 +361,12 @@ def mlx_rope(input, dim, offset): dim, offset, } = problem; - let shape = shape![BS, NH, SL, HD]; - let a = Tensor::randn::(0., 1., shape, Device::CPU); + let shape = (BS, NH, SL, HD); + let a = randn(shape, None, None, Default::default()).unwrap(); let ground = ground_truth(&a, dim, offset).unwrap(); let a = a.to(&device).unwrap(); - let b = a.rope(dim, 10000.0, offset).unwrap(); + let b = a.rope_(dim, 10000.0, offset).unwrap(); let ours = b.to(&Device::CPU).unwrap(); //println!("ours = \n{:#?}\n", ours.to_ndarray_view::()); @@ -365,10 +384,10 @@ def mlx_rope(input, dim, offset): #[strategy(1..=256usize)] SL: usize, #[strategy(32..=128usize)] - #[filter(#HD % 16 == 0)] + #[filter(#HD.is_multiple_of(16))] HD: usize, #[strategy(32..=#HD)] - #[filter(#dim % 32 == 0)] + #[filter(#dim.is_multiple_of(32))] dim: usize, #[strategy(0..=#SL)] offset: usize, @@ -384,10 +403,7 @@ def mlx_rope(input, dim, offset): dim, offset, } = prob; - println!( - "BS = {}, NH = {}, SL = {}, HD = {}, rope_dim = {}, offset = {}", - BS, NH, SL, HD, dim, offset - ); + println!("BS = {BS}, NH = {NH}, SL = {SL}, HD = {HD}, rope_dim = {dim}, offset = {offset}"); let device = Device::request_device(DeviceRequest::GPU).unwrap(); run_rope_trial(prob, device); @@ -403,10 +419,7 @@ def mlx_rope(input, dim, offset): dim, offset, } = prob; - println!( - "BS = {}, NH = {}, SL = {}, HD = {}, rope_dim = {}, offset = {}", - BS, NH, SL, HD, dim, offset - ); + println!("BS = {BS}, NH = {NH}, SL = {SL}, HD = {HD}, rope_dim = {dim}, offset = {offset}"); let device = Device::request_device(DeviceRequest::CPU).unwrap(); run_rope_trial(prob, device); diff --git a/crates/ratchet-core/src/ops/scatter_add.rs b/crates/piston-core/src/ops/scatter_add.rs similarity index 83% rename from crates/ratchet-core/src/ops/scatter_add.rs rename to crates/piston-core/src/ops/scatter_add.rs index 73c1130c..2b41c86f 100644 --- a/crates/ratchet-core/src/ops/scatter_add.rs +++ b/crates/piston-core/src/ops/scatter_add.rs @@ -2,20 +2,20 @@ use derive_new::new; use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ - gpu::BindGroupLayoutDescriptor, rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, - KernelElement, KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, - Scalar, StorageView, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, - Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Vec2, + Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::BindGroupLayoutDescriptor, rvec, }; #[derive(new, Debug, Clone, IrFields)] pub struct ScatterAdd { - pub dst: Tensor, - pub src: Tensor, - pub ids: Tensor, + pub dst: OpTensor, + pub src: OpTensor, + pub ids: OpTensor, pub dim: usize, } @@ -29,15 +29,15 @@ pub struct ScatterAddMeta { impl OpGuards for ScatterAdd { fn check_shapes(&self) { - assert!(self.src.rank() >= 1); + assert!(self.src.dim() >= 1); assert_eq!(self.src.shape().len(), self.ids.shape().len()); assert_eq!(self.src.shape().len(), self.dst.shape().len()); } fn check_dtypes(&self) { - assert!(self.ids.dt() == crate::DType::I32); - assert!(self.dst.dt() == crate::DType::F32); - assert!(self.src.dt() == crate::DType::F32); + assert!(self.ids.dtype() == crate::DType::I32); + assert!(self.dst.dtype() == crate::DType::F32); + assert!(self.src.dtype() == crate::DType::F32); } } @@ -51,7 +51,7 @@ impl Operation for ScatterAdd { } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.dst, &self.src, &self.ids] } @@ -88,7 +88,7 @@ impl KernelRenderable for ScatterAddKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -133,11 +133,11 @@ impl Kernel for ScatterAddKernels { } } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { KernelElement::Scalar } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { let ScatterAddKernels::Standard(inner) = self; let dim = inner.dim; let src_shape_vec = inner.src.shape().to_vec(); @@ -157,7 +157,7 @@ impl Kernel for ScatterAddKernels { Ok(BindGroupLayoutDescriptor::ternary_inplace()) } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let ScatterAddKernels::Standard(inner) = self; let dim = inner.dim; let src_shape_vec = inner.src.shape().to_vec(); @@ -178,12 +178,12 @@ impl Kernel for ScatterAddKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let ScatterAddKernels::Standard(inner) = self; let kernel_element = self.kernel_element(dst); - match (inner.src.dt(), &kernel_element) { + match (inner.src.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -204,7 +204,7 @@ impl Kernel for ScatterAddKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - inner.src.dt(), + inner.src.dtype(), kernel_element ))), } @@ -215,10 +215,10 @@ impl Kernel for ScatterAddKernels { mod tests { use std::cmp::max; - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; use crate::test_util::run_py_prg; - use crate::{shape, Device, DeviceRequest, Tensor}; + use crate::{Device, DeviceRequest, Tensor, ones, randint, shape, zeros}; fn ground_truth( dst: &Tensor, @@ -233,12 +233,11 @@ def scatter_add(dst, src, ids): dst_tensor = torch.from_numpy(dst) src_tensor = torch.from_numpy(src) ids_tensor = torch.from_numpy(ids).to(torch.long) - result = dst_tensor.scatter_add({}, ids_tensor, src_tensor) + result = dst_tensor.scatter_add({dim}, ids_tensor, src_tensor) return result.numpy() "#, - dim ); - run_py_prg(prg.to_string(), &[dst, src, ids], &[], src.dt()) + run_py_prg(prg.to_string(), &[dst, src, ids], &[], src.dtype()) } fn run_scatter_add_trial(problem: ScatterAddProblem, device: Device) { @@ -248,11 +247,17 @@ def scatter_add(dst, src, ids): let mut src_shape = vec![B, M, N]; src_shape[dim] = max(M.min(N) / 2, 1); // Make src dimension smaller than dst - let dst = Tensor::zeros::(&dst_shape, &Device::CPU); - let src = Tensor::ones::(&src_shape.into(), &Device::CPU); + let dst = zeros(dst_shape.clone(), Default::default()).unwrap(); + let src = ones(src_shape, Default::default()).unwrap(); // Create ids tensor with the same shape as src, but with values in range [0, dst_shape[dim]) - let ids = Tensor::randint(0, dst_shape[dim] as i32, src.shape().clone(), Device::CPU); + let ids = randint( + 0, + dst_shape[dim] as i32, + src.shape().clone(), + Default::default(), + ) + .unwrap(); let ground = ground_truth(&dst, &src, &ids, dim).unwrap(); diff --git a/crates/ratchet-core/src/ops/select.rs b/crates/piston-core/src/ops/select.rs similarity index 82% rename from crates/ratchet-core/src/ops/select.rs rename to crates/piston-core/src/ops/select.rs index 1551a6a4..19d00cb8 100644 --- a/crates/ratchet-core/src/ops/select.rs +++ b/crates/piston-core/src/ops/select.rs @@ -1,29 +1,30 @@ use derive_new::new; use encase::ShaderType; use half::f16; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Stride, + Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, gpu::{BindGroupLayoutDescriptor, WorkgroupCount}, - rvec, wgc, wgs, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, StorageView, - Strides, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + rvec, wgc, wgs, }; use inline_wgsl::wgsl; #[derive(new, Debug, Clone, IrFields)] pub struct IndexSelect { - pub src: Tensor, - pub indices: Tensor, + pub src: OpTensor, + pub indices: OpTensor, pub dim: usize, } impl IndexSelect { - pub fn src(&self) -> &Tensor { + pub fn src(&self) -> &OpTensor { &self.src } - pub fn indices(&self) -> &Tensor { + pub fn indices(&self) -> &OpTensor { &self.indices } @@ -51,16 +52,16 @@ impl Operation for IndexSelect { let mut output_shape = input_shape.clone(); output_shape[self.dim] = indices_shape[0]; - let strides = Strides::from(&output_shape); + let stride = Stride::from(&output_shape); Ok(StorageView::new( output_shape, - self.src.dt().activation_dt(), - strides, + self.src.dtype().activation_dtype(), + stride, )) } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.src, &self.indices] } } @@ -68,14 +69,29 @@ impl Operation for IndexSelect { impl OpGuards for IndexSelect { fn check_shapes(&self) { let (input, indices) = (&self.src, &self.indices); - assert_eq!(input.rank(), 2); - assert_eq!(indices.rank(), 1); + assert_eq!( + input.dim(), + 2, + "Input must be a 2D tensor, got {:?}", + input.shape() + ); + assert_eq!( + indices.dim(), + 1, + "Indices must be a 1D tensor, got {:?}", + indices.shape() + ); } fn check_dtypes(&self) { let indices = &self.indices; //TODO: support others - assert_eq!(indices.dt(), DType::I32); + assert_eq!( + indices.dtype(), + DType::I32, + "Indices must be of type I32, got {:?}", + indices.dtype() + ); } } @@ -99,7 +115,7 @@ impl KernelRenderable for IndexSelectKernels { ) -> Result<(), OperationError> { let index_arr = Array::>::default(); let IndexSelectKernels::Standard(inner) = self; - match inner.src.dt() { + match inner.src.dtype() { DType::F16 | DType::F32 => { builder.register_storage("E", BindingMode::ReadOnly, Array::

::default()); builder.register_storage("I", BindingMode::ReadOnly, index_arr); @@ -123,7 +139,7 @@ impl KernelRenderable for IndexSelectKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -141,9 +157,9 @@ impl KernelRenderable for IndexSelectKernels { let IndexSelectKernels::Standard(inner) = self; //TODO: REFACTOR - match inner.src.dt() { + match inner.src.dtype() { DType::Q8_0H(_) | DType::Q8_0F(_) => { - kernel_builder.write_unpack(inner.src.dt()); + kernel_builder.write_unpack(inner.src.dtype()); kernel_builder.write_main(wgsl! { let tid = workgroup_id.x * 64u + local_invocation_index; @@ -199,14 +215,18 @@ impl Kernel for IndexSelectKernels { _: bool, ) -> Result { let IndexSelectKernels::Standard(inner) = self; - match inner.src.dt() { + match inner.src.dtype() { DType::F32 | DType::F16 => Ok(BindGroupLayoutDescriptor::binary()), DType::Q8_0H(_) | DType::Q8_0F(_) => Ok(BindGroupLayoutDescriptor::ternary()), _ => unimplemented!(), } } - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { let IndexSelectKernels::Standard(inner) = self; let dst_numel = dst.shape().numel() as u32; @@ -224,10 +244,10 @@ impl Kernel for IndexSelectKernels { }) } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { let workgroup_size = wgs![8, 8, 1]; let IndexSelectKernels::Standard(inner) = self; - let numel = match inner.src.dt() { + let numel = match inner.src.dtype() { DType::F32 | DType::F16 => dst.shape().numel(), DType::Q8_0H(_) | DType::Q8_0F(_) => dst.shape().numel() / 4, _ => unimplemented!(), @@ -239,19 +259,19 @@ impl Kernel for IndexSelectKernels { }) } - fn kernel_element(&self, _: &Tensor) -> KernelElement { + fn kernel_element(&self, _: &OpTensor) -> KernelElement { KernelElement::Scalar } fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); let IndexSelectKernels::Standard(inner) = self; - match (inner.src.dt(), &kernel_element) { + match (inner.src.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -278,7 +298,7 @@ impl Kernel for IndexSelectKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - inner.src.dt(), + inner.src.dtype(), kernel_element ))), } @@ -292,7 +312,10 @@ mod tests { use test_strategy::proptest; use crate::test_util::run_py_prg; - use crate::{quantize, rvec, shape, Device, DeviceRequest, Shape, Tensor, Q8_0F}; + use crate::{ + Device, DeviceRequest, Q8_0F, Shape, Tensor, TensorOptions, quantize, randint, randn, rvec, + shape, + }; impl Arbitrary for IndexSelectProblem { type Parameters = (); @@ -303,7 +326,7 @@ mod tests { .prop_flat_map(|input_shape| (Just(input_shape), 1..64usize)) .prop_map(|(input_shape, num_indices)| { let indices = - Tensor::randint(0, input_shape[0] as i32, shape![num_indices], Device::CPU); + randint(0, input_shape[0] as i32, num_indices, Default::default()).unwrap(); IndexSelectProblem { input_shape, indices, @@ -318,11 +341,10 @@ mod tests { r#" import torch def index_select(input, indices): - return torch.index_select(torch.from_numpy(input),{},torch.from_numpy(indices)).numpy() -"#, - dim + return torch.index_select(torch.from_numpy(input),{dim},torch.from_numpy(indices)).numpy() +"# ); - run_py_prg(prg.to_string(), &[input, indices], &[], input.dt()) + run_py_prg(prg.to_string(), &[input, indices], &[], input.dtype()) } fn run_index_select_trial(problem: IndexSelectProblem, device: Device, quant: bool) { @@ -330,7 +352,7 @@ def index_select(input, indices): input_shape, indices, } = problem; - let mut input = Tensor::randn::(0., 1., input_shape, Device::CPU); + let mut input = randn(input_shape, None, None, Default::default()).unwrap(); let ground_truth = ground_truth(&input, &indices, 0).unwrap(); if quant { @@ -342,8 +364,8 @@ def index_select(input, indices): let result = input.index_select(indices, 0).unwrap(); let x = result.to(&Device::CPU).unwrap(); - println!("X: {:?}", x); - println!("Ground Truth: {:?}", ground_truth); + println!("X: {x:?}"); + println!("Ground Truth: {ground_truth:?}"); ground_truth.all_close(&x, 1e-1, 1e-1).unwrap(); } @@ -351,7 +373,12 @@ def index_select(input, indices): fn test_qindex_select() { let prob = IndexSelectProblem { input_shape: shape![256, 32], - indices: Tensor::from_data(vec![64, 192, 255], shape![3], Device::CPU), + indices: Tensor::from_data( + vec![64, 192, 255], + 3, + TensorOptions::new().device(Device::CPU), + ) + .unwrap(), }; let device = Device::request_device(DeviceRequest::GPU).unwrap(); run_index_select_trial(prob.clone(), device, true); diff --git a/crates/ratchet-core/src/ops/softmax.rs b/crates/piston-core/src/ops/softmax.rs similarity index 87% rename from crates/ratchet-core/src/ops/softmax.rs rename to crates/piston-core/src/ops/softmax.rs index 52157a40..e7aa9543 100644 --- a/crates/ratchet-core/src/ops/softmax.rs +++ b/crates/piston-core/src/ops/softmax.rs @@ -2,18 +2,19 @@ use derive_new::new; use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, wgc, wgs, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, StorageView, - Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Vec2, + Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, wgc, wgs, }; #[derive(new, Debug, Clone, IrFields)] pub struct Softmax { - pub(crate) input: Tensor, + pub(crate) input: OpTensor, pub(crate) dim: usize, } @@ -28,13 +29,13 @@ pub struct SoftmaxMeta { impl OpGuards for Softmax { fn check_shapes(&self) { let input = &self.input; - assert!(input.rank() >= 2); - assert!(self.dim < input.rank()); + assert!(input.dim() >= 2); + assert!(self.dim < input.dim()); } fn check_dtypes(&self) { let input = &self.input; - assert!(input.dt().is_float()); + assert!(input.dtype().is_float()); } } @@ -55,7 +56,7 @@ impl KernelRenderable for SoftmaxKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -71,7 +72,7 @@ impl KernelRenderable for SoftmaxKernels { self.register_bindings::

(&mut kernel_builder, inplace)?; kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - let dt = P::T::DT; + let dtype = P::T::DT; let accessor = P::render_type(); let BLOCK_SIZE = workgroup_size.x.render(); @@ -79,8 +80,8 @@ impl KernelRenderable for SoftmaxKernels { kernel_builder.write_global(wgsl! { var smem: array<'accessor, 'BLOCK_SIZE>; - var maximum: 'dt; - var sum: 'dt; + var maximum: 'dtype; + var sum: 'dtype; }); kernel_builder.write_global(wgsl! { @@ -106,7 +107,7 @@ impl KernelRenderable for SoftmaxKernels { _ => { return Err(OperationError::CompileError( "Invalid dimension".to_string(), - ))? + ))?; } }; @@ -190,7 +191,7 @@ impl Operation for Softmax { } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.input] } @@ -208,15 +209,15 @@ impl Kernel for SoftmaxKernels { } } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { let inner = match self { Self::Standard(op) => op, }; let input = &inner.input; let N = input.shape()[inner.dim] as u32; - if N % 4 == 0 { + if N.is_multiple_of(4) { KernelElement::Vec4 - } else if N % 2 == 0 { + } else if N.is_multiple_of(2) { KernelElement::Vec2 } else { KernelElement::Scalar @@ -226,11 +227,11 @@ impl Kernel for SoftmaxKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); - match (dst.dt(), &kernel_element) { + match (dst.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -251,13 +252,13 @@ impl Kernel for SoftmaxKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - dst.dt(), + dst.dtype(), kernel_element ))), } } - fn calculate_dispatch(&self, _dst: &Tensor) -> Result { + fn calculate_dispatch(&self, _dst: &OpTensor) -> Result { let inner = match self { Self::Standard(op) => op, }; @@ -271,7 +272,7 @@ impl Kernel for SoftmaxKernels { }) } - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let inner = match self { Self::Standard(op) => op, }; @@ -308,10 +309,10 @@ impl GPUOperation for Softmax { #[cfg(all(test, feature = "pyo3"))] mod tests { - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; use crate::test_util::run_py_prg; - use crate::{shape, Device, DeviceRequest, Tensor}; + use crate::{Device, DeviceRequest, Tensor, randn}; fn ground_truth(a: &Tensor) -> anyhow::Result { let prg = r#" @@ -320,12 +321,12 @@ import torch.nn.functional as F def softmax(a): return F.softmax(torch.from_numpy(a), dim=-1).numpy() "#; - run_py_prg(prg.to_string(), &[a], &[], a.dt()) + run_py_prg(prg.to_string(), &[a], &[], a.dtype()) } fn run_softmax_trial(problem: SoftmaxProblem, device: Device) { let SoftmaxProblem { B, M, N } = problem; - let a = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); + let a = randn((B, M, N), None, None, Default::default()).unwrap(); let ground = ground_truth(&a).unwrap(); let a_gpu = a.to(&device).unwrap(); diff --git a/crates/piston-core/src/ops/ternary.rs b/crates/piston-core/src/ops/ternary.rs new file mode 100644 index 00000000..c4e068ad --- /dev/null +++ b/crates/piston-core/src/ops/ternary.rs @@ -0,0 +1,376 @@ +use derive_new::new; +use encase::ShaderType; +use half::f16; +use inline_wgsl::wgsl; +use piston_macros::{IrFields, WgslMetadata}; + +use crate::{ + Array, BindingMode, BuiltIn, DType, GPUOperation, InvariantError, Kernel, KernelElement, + KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, + Shape, StorageView, Stride, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, + Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, +}; +#[cfg(test)] +use test_strategy::Arbitrary; + +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Debug, Clone, Hash, IrFields)] +pub enum TernaryOp { + Addcdiv, + Addcmul, +} + +impl TernaryOp { + pub fn kernel_name(&self) -> &'static str { + match self { + TernaryOp::Addcdiv => "addcdiv", + TernaryOp::Addcmul => "addcmul", + } + } + + pub fn kernel_expression(&self) -> String { + match self { + TernaryOp::Addcdiv => wgsl! { + input + metadata.value * (tensor1 / tensor2) + }, + TernaryOp::Addcmul => wgsl! { + input + metadata.value * (tensor1 * tensor2) + }, + } + } +} + +#[derive(new, Debug, Clone, IrFields)] +pub struct Ternary { + pub input: OpTensor, + pub tensor1: OpTensor, + pub tensor2: OpTensor, + pub value: f32, + pub op: TernaryOp, +} + +impl KernelRenderable for TernaryKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + inplace: bool, + ) -> Result<(), OperationError> { + if inplace { + builder.register_storage("Input", BindingMode::ReadWrite, Array::

::default()); + builder.register_storage("Tensor1", BindingMode::ReadOnly, Array::

::default()); + builder.register_storage("Tensor2", BindingMode::ReadOnly, Array::

::default()); + } else { + builder.register_storage("Input", BindingMode::ReadOnly, Array::

::default()); + builder.register_storage("Tensor1", BindingMode::ReadOnly, Array::

::default()); + builder.register_storage("Tensor2", BindingMode::ReadOnly, Array::

::default()); + builder.register_storage("Output", BindingMode::ReadWrite, Array::

::default()); + } + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + self.register_bindings::

(&mut kernel_builder, inplace)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + let N = (P::W as u32).render(); + + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (index >= metadata.numel / 'N) { + return; + } + }); + + let TernaryKernels::Standard(inner) = self; + let expression = inner.op.kernel_expression(); + let assignment_expression = if inplace { + wgsl! { + Input[index] = 'expression; + } + } else { + wgsl! { + Output[index] = 'expression; + } + }; + + kernel_builder.write_main(wgsl! { + let input = Input[index]; + let tensor1 = Tensor1[index]; + let tensor2 = Tensor2[index]; + 'assignment_expression + }); + + Ok(kernel_builder.build()?) + } +} + +impl Ternary { + pub fn op(&self) -> &TernaryOp { + &self.op + } + + pub fn input(&self) -> &OpTensor { + &self.input + } + + pub fn tensor1(&self) -> &OpTensor { + &self.tensor1 + } + + pub fn tensor2(&self) -> &OpTensor { + &self.tensor2 + } + + pub fn value(&self) -> f32 { + self.value + } +} + +#[derive(Debug, ShaderType, WgslMetadata)] +pub struct TernaryMeta { + numel: u32, + value: f32, +} + +impl OpGuards for Ternary { + fn check_shapes(&self) { + let shapes = [ + self.input.shape(), + self.tensor1.shape(), + self.tensor2.shape(), + ]; + let broadcasted = Shape::multi_broadcast(&shapes); + assert!(broadcasted.is_some()); + } + + fn check_dtypes(&self) { + assert_eq!(self.input.dtype(), self.tensor1.dtype()); + assert_eq!(self.input.dtype(), self.tensor2.dtype()); + } +} + +impl Operation for Ternary { + fn name(&self) -> &'static str { + match self.op { + TernaryOp::Addcdiv => "Addcdiv", + TernaryOp::Addcmul => "Addcmul", + } + } + + fn compute_view(&self) -> Result { + let input = &self.input; + let tensor1 = &self.tensor1; + let tensor2 = &self.tensor2; + let shapes = &[input.shape(), tensor1.shape(), tensor2.shape()]; + + let broadcasted = Shape::multi_broadcast(shapes); + if broadcasted.is_none() { + let failed = shapes.iter().map(|s| (*s).clone()).collect::>(); + return Err(InvariantError::BroadcastingFailed(failed).into()); + } + + let broadcasted = broadcasted.unwrap(); + let ostride = Stride::from(&broadcasted); + Ok(StorageView::new(broadcasted, input.dtype(), ostride)) + } + + #[inline] + fn srcs(&self) -> RVec<&OpTensor> { + rvec![&self.input, &self.tensor1, &self.tensor2] + } + + fn supports_inplace(&self) -> bool { + true + } +} + +impl GPUOperation for Ternary { + type KernelEnum = TernaryKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + TernaryKernels::Standard(self.clone()) + } +} + +pub enum TernaryKernels { + Standard(Ternary), +} + +impl Kernel for TernaryKernels { + type Metadata = TernaryMeta; + + fn storage_bind_group_layout( + &self, + inplace: bool, + ) -> Result { + if inplace { + Ok(BindGroupLayoutDescriptor::ternary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::ternary()) + } + } + + fn kernel_name(&self) -> String { + match self { + TernaryKernels::Standard(k) => k.op.kernel_name().to_string(), + } + } + + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let numel = dst.shape().numel() as _; + let TernaryKernels::Standard(inner) = self; + Ok(TernaryMeta { + numel, + value: inner.value, + }) + } + + fn kernel_element(&self, dst: &OpTensor) -> KernelElement { + let numel = dst.shape().numel(); + + if numel.is_multiple_of(4) { + KernelElement::Vec4 + } else if numel.is_multiple_of(2) { + KernelElement::Vec2 + } else { + KernelElement::Scalar + } + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let TernaryKernels::Standard(inner) = self; + let kernel_element = self.kernel_element(dst); + match (inner.input.dtype(), &kernel_element) { + (DType::F32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {:?} or kernel element {:?}", + inner.input.dtype(), + kernel_element + ))), + } + } +} + +#[cfg(all(test, feature = "pyo3"))] +#[cfg(test)] +mod tests { + use crate::{Device, DeviceRequest, Shape, Tensor, TernaryOp, randn, test_util::run_py_prg}; + use test_strategy::{Arbitrary, proptest}; + + #[derive(Arbitrary, Debug)] + struct TernaryProblem { + op: TernaryOp, + #[any(vec![1..=4, 1..=4, 1..=1, 1..=256])] + shape: Shape, + } + + fn ground_truth( + input: &Tensor, + tensor1: &Tensor, + tensor2: &Tensor, + value: f32, + op: &TernaryOp, + ) -> anyhow::Result { + let kn = op.kernel_name(); + let prg = match op { + TernaryOp::Addcdiv => format!( + r#" +import torch +def {kn}(input, tensor1, tensor2): + return torch.addcdiv(torch.from_numpy(input), torch.from_numpy(tensor1), torch.from_numpy(tensor2), value={value}).numpy() +"#, + ), + TernaryOp::Addcmul => format!( + r#" +import torch +def {kn}(input, tensor1, tensor2): + return torch.addcmul(torch.from_numpy(input), torch.from_numpy(tensor1), torch.from_numpy(tensor2), value={value}).numpy() +"#, + ), + }; + run_py_prg( + prg.to_string(), + &[input, tensor1, tensor2], + &[], + input.dtype(), + ) + } + + fn run_ternary_trial(prob: TernaryProblem, device: Device) -> anyhow::Result<()> { + let TernaryProblem { op, shape } = prob; + let input = randn(shape.clone(), None, None, Default::default())?; + let tensor1 = randn(shape.clone(), None, None, Default::default())?; + let tensor2 = randn(shape, None, None, Default::default())?; + let value = 0.5; + let ground = ground_truth(&input, &tensor1, &tensor2, value, &op)?; + + let input = input.to(&device)?; + let tensor1 = tensor1.to(&device)?; + let tensor2 = tensor2.to(&device)?; + + let c = match op { + TernaryOp::Addcdiv => input.addcdiv(tensor1, tensor2, value)?, + TernaryOp::Addcmul => input.addcmul(tensor1, tensor2, value)?, + }; + + let d = c.to(&Device::CPU)?; + ground.all_close(&d, 1e-4, 1e-4)?; + Ok(()) + } + + #[proptest(cases = 8)] + fn test_ternary_gpu(prob: TernaryProblem) { + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_ternary_trial(prob, device).unwrap(); + } +} diff --git a/crates/piston-core/src/ops/topk.rs b/crates/piston-core/src/ops/topk.rs new file mode 100644 index 00000000..22c96f3a --- /dev/null +++ b/crates/piston-core/src/ops/topk.rs @@ -0,0 +1,530 @@ +use derive_new::new; +use half::f16; +use inline_wgsl::wgsl; +use piston_macros::IrFields; + +use crate::{ + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, Shape, StorageView, + Stride, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::BindGroupLayoutDescriptor, +}; + +const MAX_TOPK: usize = 256; + +#[derive(new, Debug, Clone, IrFields)] +pub struct TopK { + pub input: OpTensor, + pub k: usize, + pub dim: usize, + pub largest: bool, + pub sorted: bool, +} + +impl OpGuards for TopK { + fn check_shapes(&self) { + let rank = self.input.dim(); + assert!(rank > 0, "topk: input must have at least 1 dimension"); + assert!(self.dim < rank, "topk: dim out of range"); + assert!(self.k > 0, "topk: k must be > 0"); + assert!( + self.k <= self.input.shape()[self.dim], + "topk: k must be <= dim size" + ); + } + + fn check_dtypes(&self) { + let dt = self.input.dtype(); + assert!( + dt.is_float() || matches!(dt, DType::I32), + "topk: only F32/F16/I32 supported" + ); + assert!( + self.k <= MAX_TOPK, + "topk: k={} exceeds MAX_TOPK={} (temporary limitation)", + self.k, + MAX_TOPK + ); + assert!(!self.sorted, "topk: sorted=true not implemented"); + } +} + +impl Operation for TopK { + fn name(&self) -> &'static str { + "TopK" + } + + fn compute_view(&self) -> Result { + // Output are indices along dim: replace dim size with k, dtype I32 + let mut out_shape = self.input.shape().to_vec(); + out_shape[self.dim] = self.k; + let out_shape = Shape::from(out_shape); + let stride = Stride::from(&out_shape); + Ok(StorageView::new(out_shape, DType::I32, stride)) + } + + #[inline] + fn srcs(&self) -> RVec<&OpTensor> { + crate::rvec![&self.input] + } + + fn supports_inplace(&self) -> bool { + false + } +} + +pub enum TopKKernels { + Standard(TopK), +} + +impl GPUOperation for TopK { + type KernelEnum = TopKKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + TopKKernels::Standard(self.clone()) + } +} + +#[derive(Debug, derive_new::new, encase::ShaderType, piston_macros::WgslMetadata)] +pub struct TopKMeta { + rank: u32, + dim: u32, + k: u32, + num_slices: u32, + shape: glam::UVec4, + stride: glam::UVec4, + out_stride: glam::UVec4, +} + +impl Kernel for TopKKernels { + type Metadata = TopKMeta; + + fn kernel_name(&self) -> String { + match self { + TopKKernels::Standard(inner) => if inner.largest { + "topk_largest" + } else { + "topk_smallest" + } + .to_string(), + } + } + + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { + KernelElement::Scalar + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + // One invocation per slice (all dims except target) + let TopKKernels::Standard(inner) = self; + let total_slices = dst.shape().numel() / inner.k; + Ok(Workload::std(total_slices, KernelElement::Scalar)) + } + + fn storage_bind_group_layout( + &self, + inplace: bool, + ) -> Result { + if inplace { + return Err(OperationError::InplaceError( + "TopK cannot be done in place".to_string(), + )); + } + Ok(BindGroupLayoutDescriptor::unary()) + } + + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let TopKKernels::Standard(inner) = self; + let rank = inner.input.dim() as u32; + let mut shape = [1u32; 4]; + let mut stride = [0u32; 4]; + for (i, &d) in inner.input.shape().iter().enumerate() { + shape[i] = d as u32; + stride[i] = inner.input.stride()[i] as u32; + } + let mut out_stride = [0u32; 4]; + for (i, &s) in dst.stride().iter().enumerate() { + out_stride[i] = s as u32; + } + let num_slices = (inner.input.shape().numel() / inner.input.shape()[inner.dim]) as u32; + Ok(TopKMeta { + rank, + dim: inner.dim as u32, + k: inner.k as u32, + num_slices, + shape: shape.into(), + stride: stride.into(), + out_stride: out_stride.into(), + }) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let TopKKernels::Standard(inner) = self; + match inner.input.dtype() { + DType::F32 => self.render::>(inplace, dst, workgroup_size), + DType::F16 => self.render::>(inplace, dst, workgroup_size), + DType::I32 => self.render::>(inplace, dst, workgroup_size), + _ => Err(OperationError::CompileError( + "TopK only supports F32/F16/I32".to_string(), + )), + } + } +} + +impl KernelRenderable for TopKKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + _: bool, + ) -> Result<(), OperationError> { + builder.register_storage("X", BindingMode::ReadOnly, Array::

::default()); + builder.register_storage("Y", BindingMode::ReadWrite, Array::>::default()); + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + crate::rvec![ + BuiltIn::WorkgroupId, + BuiltIn::LocalInvocationIndex, + BuiltIn::NumWorkgroups + ], + device.compute_features().clone(), + ); + + let TopKKernels::Standard(inner) = self; + + self.register_bindings::

(&mut kernel_builder, inplace)?; + kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); + + let dtype = P::render_type(); + // Helpers + kernel_builder.write_global(wgsl! { + const MAX_TOPK: u32 = 'MAX_TOPK; + + fn coords_from_slice_id( + idx_in: u32, + rank: u32, + shape: vec4, + exclude_dim: u32, + ) -> vec4 { + var coords: vec4 = vec4(0u); + var idx: u32 = idx_in; + for (var d: u32 = 0u; d < rank; d++) { + let dim_idx = rank - 1u - d; + if (dim_idx == exclude_dim) { continue; } + let base = shape[dim_idx]; + coords[dim_idx] = idx % base; + idx = idx / base; + } + return coords; + } + + fn linear_from_coords(coords: vec4, stride: vec4, rank: u32) -> u32 { + var off: u32 = 0u; + for (var d: u32 = 0u; d < rank; d++) { + off += coords[d] * stride[d]; + } + return off; + } + }); + + let loop_body = if inner.largest { + wgsl! { + while (pos < limit && top_vals[pos] > v) { pos += 1u; } + } + } else { + wgsl! { + while (pos < limit && top_vals[pos] < v) { pos += 1u; } + } + }; + + // Main + kernel_builder.write_main(wgsl! { + // One invocation per slice + let x_offset = workgroup_id.x * 64u; + let slice_id = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (slice_id >= metadata.num_slices) { return; } + + // Guard Max K + if (metadata.k > MAX_TOPK) { return; } + + let rank = metadata.rank; + let dim = metadata.dim; + let n_dim = metadata.shape[dim]; + + var coords = coords_from_slice_id(slice_id, rank, metadata.shape, dim); + // Ensure target dim index 0 for base + coords[dim] = 0u; + let base = linear_from_coords(coords, metadata.stride, rank); + let step = metadata.stride[dim]; + + // Local buffers + var top_vals: array<'dtype, MAX_TOPK>; + var top_idxs: array; + var count: u32 = 0u; + + // Iterate across the reduction dimension + for (var j: u32 = 0u; j < n_dim; j++) { + let idx = base + j * step; + let v = X[idx]; + + // Determine insertion position + var pos: u32 = 0u; + let limit = count; + 'loop_body + + if (count < metadata.k) { + // Make room + var i: i32 = i32(count); + while (i > i32(pos)) { + top_vals[u32(i)] = top_vals[u32(i - 1)]; + top_idxs[u32(i)] = top_idxs[u32(i - 1)]; + i -= 1; + } + top_vals[pos] = v; + top_idxs[pos] = i32(j); + count += 1u; + } else if (pos < metadata.k) { + // Insert and drop the last element + var i: i32 = i32(metadata.k - 1u); + while (i > i32(pos)) { + top_vals[u32(i)] = top_vals[u32(i - 1)]; + top_idxs[u32(i)] = top_idxs[u32(i - 1)]; + i -= 1; + } + top_vals[pos] = v; + top_idxs[pos] = i32(j); + } + } + + // Write out indices for this slice across k outputs + // coords currently has target dim = 0; update per r + for (var r: u32 = 0u; r < metadata.k; r++) { + coords[dim] = r; + let out_off = linear_from_coords(coords, metadata.out_stride, rank); + Y[out_off] = top_idxs[r]; + } + }); + + Ok(kernel_builder.build()?) + } +} + +#[cfg(all(test, feature = "pyo3"))] +mod tests { + use crate::randint; + use crate::{DType, Device, DeviceRequest, Tensor, randn, test_util::run_py_prg}; + use test_strategy::{Arbitrary, proptest}; + + #[derive(Arbitrary, Debug)] + struct TopKProblem { + #[strategy(1..=3usize)] + B: usize, + #[strategy(1..=64usize)] + M: usize, + #[strategy(1..=64usize)] + N: usize, + #[strategy(0..=2usize)] + dim: usize, + #[strategy(1..=4usize)] + k: usize, + largest: bool, + } + + fn ground_truth_values( + a: &Tensor, + k: usize, + dim: usize, + largest: bool, + ) -> anyhow::Result { + let prg = r#" +import torch +def topk_vals(a, k, dim, largest): + return torch.topk(torch.from_numpy(a), k, dim=dim, largest=largest)[0].float().numpy() +"# + .to_string(); + run_py_prg( + prg, + &[a], + &[&(k as i32), &(dim as i32), &largest], + DType::F32, + ) + } + + fn ground_truth_indices( + a: &Tensor, + k: usize, + dim: usize, + largest: bool, + ) -> anyhow::Result { + let prg = r#" +import torch +import numpy as np +def topk_idx(a, k, dim, largest): + return torch.topk(torch.from_numpy(a), k, dim=dim, largest=largest)[1].to(torch.int32).numpy().astype(np.int32) +"#.to_string(); + run_py_prg( + prg, + &[a], + &[&(k as i32), &(dim as i32), &largest], + DType::I32, + ) + } + + #[proptest(cases = 8)] + fn test_topk(prob: TopKProblem) { + let TopKProblem { + B, + M, + N, + dim, + k, + largest, + } = prob; + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + let a = randn((B, M, N), None, None, Default::default()).unwrap(); + let (_dim_len, k_use) = { + let dims = a.shape().to_vec(); + let dlen = dims[dim]; + (dlen, k.min(dims[dim].max(1))) + }; + let a_vals = ground_truth_values(&a, k_use, dim, largest).unwrap(); + let a_idx = ground_truth_indices(&a, k_use, dim, largest).unwrap(); + + let a_gpu = a.to(&device).unwrap(); + let ours = crate::topk(a_gpu, k_use, Some(dim), Some(largest), Some(false)).unwrap(); + let values = ours[0].clone().to(&Device::CPU).unwrap(); + let indices = ours[1].clone().to(&Device::CPU).unwrap(); + let a_idx_f32 = a_idx + .clone() + .cast(DType::F32) + .unwrap() + .to(&Device::CPU) + .unwrap(); + let indices_f32 = indices + .clone() + .cast(DType::F32) + .unwrap() + .to(&Device::CPU) + .unwrap(); + + a_vals.all_close(&values, 3e-5f32, 1e-5f32).unwrap(); + assert_eq!(a_idx.dtype(), DType::I32); + assert_eq!(indices.dtype(), DType::I32); + a_idx_f32.all_close(&indices_f32, 0.0f32, 0.0f32).unwrap(); + } + + #[derive(Arbitrary, Debug)] + struct TopKIntProblem { + #[strategy(1..=3usize)] + B: usize, + #[strategy(1..=32usize)] + M: usize, + #[strategy(1..=32usize)] + N: usize, + #[strategy(0..=2usize)] + dim: usize, + #[strategy(1..=4usize)] + k: usize, + largest: bool, + } + + fn ground_truth_values_i32( + a: &Tensor, + k: usize, + dim: usize, + largest: bool, + ) -> anyhow::Result { + let prg = r#" +import torch +def topk_vals_i32(a, k, dim, largest): + return torch.topk(torch.from_numpy(a).to(torch.int32), k, dim=dim, largest=largest)[0].to(torch.int32).numpy() +"#.to_string(); + run_py_prg( + prg, + &[a], + &[&(k as i32), &(dim as i32), &largest], + DType::I32, + ) + } + + fn ground_truth_indices_i32( + a: &Tensor, + k: usize, + dim: usize, + largest: bool, + ) -> anyhow::Result { + let prg = r#" +import torch +import numpy as np +def topk_idx_i32(a, k, dim, largest): + return torch.topk(torch.from_numpy(a).to(torch.int32), k, dim=dim, largest=largest)[1].to(torch.int32).numpy().astype(np.int32) +"#.to_string(); + run_py_prg( + prg, + &[a], + &[&(k as i32), &(dim as i32), &largest], + DType::I32, + ) + } + + #[proptest(cases = 6)] + fn test_topk_i32(prob: TopKIntProblem) { + let TopKIntProblem { + B, + M, + N, + dim, + k, + largest, + } = prob; + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + + // Generate integer tensor in a safe range [0, 1000) + let a = randint(0, 1000, (B, M, N), Default::default()).unwrap(); + let (_dim_len, k_use) = { + let dims = a.shape().to_vec(); + let dlen = dims[dim]; + (dlen, k.min(dims[dim].max(1))) + }; + + let a_vals = ground_truth_values_i32(&a, k_use, dim, largest).unwrap(); + let a_idx = ground_truth_indices_i32(&a, k_use, dim, largest).unwrap(); + + let a_gpu = a.to(&device).unwrap(); + let ours = crate::topk(a_gpu, k_use, Some(dim), Some(largest), Some(false)).unwrap(); + let values = ours[0].clone().to(&Device::CPU).unwrap(); + let indices = ours[1].clone().to(&Device::CPU).unwrap(); + + assert_eq!(values.dtype(), DType::I32); + assert_eq!(indices.dtype(), DType::I32); + + // Compare values and indices against PyTorch ground truth + // Convert to F32 for all_close API that expects floats + let a_vals_f32 = a_vals.clone().cast(DType::F32).unwrap(); + let values_f32 = values.clone().cast(DType::F32).unwrap(); + a_vals_f32.all_close(&values_f32, 0.0f32, 0.0f32).unwrap(); + + let a_idx_f32 = a_idx.clone().cast(DType::F32).unwrap(); + let indices_f32 = indices.clone().cast(DType::F32).unwrap(); + a_idx_f32.all_close(&indices_f32, 0.0f32, 0.0f32).unwrap(); + } +} diff --git a/crates/ratchet-core/src/ops/trilu.rs b/crates/piston-core/src/ops/trilu.rs similarity index 85% rename from crates/ratchet-core/src/ops/trilu.rs rename to crates/piston-core/src/ops/trilu.rs index 12685389..d5b29c9d 100644 --- a/crates/ratchet-core/src/ops/trilu.rs +++ b/crates/piston-core/src/ops/trilu.rs @@ -1,23 +1,30 @@ +#![allow(clippy::doc_overindented_list_items)] use derive_new::new; use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use crate::{ - gpu::BindGroupLayoutDescriptor, rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, - KernelElement, KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, - Scalar, Shape, StorageView, Strides, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, - WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, Shape, StorageView, + Stride, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::BindGroupLayoutDescriptor, rvec, }; #[derive(new, Debug, Clone, IrFields)] -pub struct Trilu { - pub src: Tensor, +pub struct TriluOp { + pub src: OpTensor, pub upper: bool, pub k: Option, } +impl TriluOp { + fn kernel_name(&self) -> &str { + if self.upper { "triu" } else { "tril" } + } +} + #[derive(Debug, derive_new::new, ShaderType, WgslMetadata)] pub struct TriluMeta { k: i32, @@ -27,18 +34,18 @@ pub struct TriluMeta { numel: u32, } -impl Operation for Trilu { +impl Operation for TriluOp { fn name(&self) -> &'static str { - "Trilu" + if self.upper { "Triu" } else { "Tril" } } fn compute_view(&self) -> Result { let shape: Shape = self.src.shape().clone(); - let strides = Strides::from(&shape); - Ok(StorageView::new(shape, crate::DType::F32, strides)) + let stride = Stride::from(&shape); + Ok(StorageView::new(shape, crate::DType::F32, stride)) } - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.src] } @@ -47,17 +54,17 @@ impl Operation for Trilu { } } -impl OpGuards for Trilu { +impl OpGuards for TriluOp { fn check_shapes(&self) {} fn check_dtypes(&self) {} } pub enum TriluKernels { - Standard(Trilu), + Standard(TriluOp), } -impl GPUOperation for Trilu { +impl GPUOperation for TriluOp { type KernelEnum = TriluKernels; fn select_kernel(&self) -> Self::KernelEnum { @@ -84,7 +91,7 @@ impl KernelRenderable for TriluKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -104,16 +111,16 @@ impl KernelRenderable for TriluKernels { kernel_builder.write_main(wgsl! { let x_offset = workgroup_id.x * 64u; let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; - + if (index >= metadata.numel) { return; } - + let batch_size = metadata.rows * metadata.cols * metadata.stride; let batch_index = index / batch_size; - + let local_index = index % batch_size; - + let col = local_index % metadata.cols; let row = (local_index / metadata.cols) % metadata.rows; }); @@ -155,15 +162,15 @@ impl Kernel for TriluKernels { fn kernel_name(&self) -> String { match self { - TriluKernels::Standard(_) => "trilu".to_string(), + TriluKernels::Standard(inner) => inner.kernel_name().to_string(), } } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { KernelElement::Scalar } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) } @@ -178,11 +185,7 @@ impl Kernel for TriluKernels { } } - fn metadata( - &self, - _: &Tensor, - _: &KernelElement, - ) -> Result { + fn metadata(&self, _: &OpTensor, _: &KernelElement) -> Result { let TriluKernels::Standard(inner) = self; let shape = inner.src.shape(); let ndim = shape.len(); @@ -198,11 +201,11 @@ impl Kernel for TriluKernels { fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); - match (dst.dt(), &kernel_element) { + match (dst.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -223,7 +226,7 @@ impl Kernel for TriluKernels { } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - dst.dt(), + dst.dtype(), kernel_element ))), } @@ -232,9 +235,11 @@ impl Kernel for TriluKernels { #[cfg(all(test, feature = "pyo3"))] mod tests { - use crate::{shape, test_util::run_py_prg, DType, Device, DeviceRequest, Tensor}; + use crate::{ + DType, Device, DeviceRequest, Tensor, TensorOptions, ones, shape, test_util::run_py_prg, + }; use proptest::prelude::any; - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; /// Generates the ground truth tensor using NumPy's triu or tril functions. /// @@ -317,7 +322,7 @@ def trilu(shape, upper, k): // Define the shape of the tensor. let shape = shape![B, M, N]; - let src = Tensor::ones::(&shape, &device); + let src = ones(shape.clone(), TensorOptions::new().device(device)).unwrap(); // Generate the ground truth using NumPy. let ground = ground_truth(&shape, upper, Some(k)) @@ -332,8 +337,8 @@ def trilu(shape, upper, k): .to(&Device::CPU) .unwrap(); - println!("Ours: {:?}", ours); - println!("Ground: {:?}", ground); + println!("Ours: {ours:?}"); + println!("Ground: {ground:?}"); // Compare the GPU result with the ground truth. ground diff --git a/crates/ratchet-core/src/ops/unary.rs b/crates/piston-core/src/ops/unary.rs similarity index 56% rename from crates/ratchet-core/src/ops/unary.rs rename to crates/piston-core/src/ops/unary.rs index 560d74b4..5a405a47 100644 --- a/crates/ratchet-core/src/ops/unary.rs +++ b/crates/piston-core/src/ops/unary.rs @@ -5,14 +5,15 @@ use derive_new::new; use encase::ShaderType; use half::f16; use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; +use piston_macros::{IrFields, WgslMetadata}; use strum_macros::EnumIter; use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, StorageView, - Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, KernelRenderable, + KernelSource, OpGuards, OpTensor, Operation, OperationError, RVec, Scalar, StorageView, Stride, + Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, }; #[cfg(test)] @@ -39,6 +40,9 @@ pub enum UnaryOp { Silu, Sigmoid, Swiglu, + LogicalNot, + IsNan, + IsInf, } impl UnaryOp { @@ -62,6 +66,9 @@ impl UnaryOp { UnaryOp::Silu => "silu".into(), UnaryOp::Sigmoid => "sigmoid".into(), UnaryOp::Swiglu => "swiglu".into(), + UnaryOp::LogicalNot => "logical_not".into(), + UnaryOp::IsNan => "isnan".into(), + UnaryOp::IsInf => "isinf".into(), } } @@ -77,7 +84,7 @@ impl UnaryOp { #[derive(new, Debug, Clone, IrFields)] pub struct Unary { - pub input: Tensor, + pub input: OpTensor, pub op: UnaryOp, } @@ -91,7 +98,37 @@ impl KernelRenderable for UnaryKernels { builder.register_storage("X", BindingMode::ReadWrite, Array::

::default()); } else { builder.register_storage("X", BindingMode::ReadOnly, Array::

::default()); - builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); + let UnaryKernels::Standard(inner) = self; + if matches!( + inner.op, + UnaryOp::LogicalNot | UnaryOp::IsNan | UnaryOp::IsInf + ) { + match self.kernel_element(&inner.input) { + KernelElement::Scalar => { + builder.register_storage( + "Y", + BindingMode::ReadWrite, + Array::>::default(), + ); + } + KernelElement::Vec2 => { + builder.register_storage( + "Y", + BindingMode::ReadWrite, + Array::>::default(), + ); + } + KernelElement::Vec4 => { + builder.register_storage( + "Y", + BindingMode::ReadWrite, + Array::>::default(), + ); + } + } + } else { + builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); + } } builder.register_uniform(); Ok(()) @@ -100,7 +137,7 @@ impl KernelRenderable for UnaryKernels { fn render( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let device = dst.device().try_gpu()?; @@ -148,6 +185,15 @@ impl KernelRenderable for UnaryKernels { kernel_builder.write_global(Unary::render_sigmoid::

()); kernel_builder.write_global(Unary::render_swiglu::

()); } + UnaryOp::LogicalNot => { + kernel_builder.write_global(Unary::render_logical_not::

()); + } + UnaryOp::IsNan => { + kernel_builder.write_global(Unary::render_isnan::

(inner.input.dtype())); + } + UnaryOp::IsInf => { + kernel_builder.write_global(Unary::render_isinf::

(inner.input.dtype())); + } _ => {} }; @@ -185,7 +231,7 @@ impl Unary { &self.op } - pub fn input(&self) -> &Tensor { + pub fn input(&self) -> &OpTensor { &self.input } @@ -272,6 +318,102 @@ impl Unary { } } } + + // fn input_accessor(input_dtype: DType) -> String { + // match P::W { + // 1 => input_dtype.as_wgsl().to_string(), + // 2 => format!("vec2<{}>", input_dtype.as_wgsl()), + // 4 => format!("vec4<{}>", input_dtype.as_wgsl()), + // _ => panic!("Unsupported W for i32_equivalent_accessor: {:?}", P::W), + // } + // } + + fn equivalent_accessor(dtype: String) -> String { + match P::W { + 1 => dtype, + 2 => format!("vec2<{}>", dtype), + 4 => format!("vec4<{}>", dtype), + _ => panic!("Unsupported W for equivalent_accessor: {:?}", P::W), + } + } + + fn render_logical_not() -> String { + let accessor = P::render_type(); + let output_accessor = Self::equivalent_accessor::

("i32".to_string()); + + wgsl! { + fn logical_not(val: 'accessor) -> 'output_accessor { + return 'output_accessor(val == 'accessor(0.)); + } + } + } + + fn render_isnan(dtype: DType) -> String { + let accessor = P::render_type(); + let output_accessor = Self::equivalent_accessor::

("i32".to_string()); + + // wgsl! { + // let other_max + // fn isnan(val: 'accessor) -> 'output_accessor { + // return 'output_accessor(max()); + // } + // } + match dtype { + DType::F16 => { + wgsl! { + fn isnan(val: 'accessor) -> 'output_accessor { + let packed_val = pack2x16float(vec2(val, 0.0h)); + + let bits = packed_val & 0x0000ffffu; + + return 'output_accessor((bits & 0x7fffu) > 0x7C00u); + } + } + } + DType::F32 => { + let u32_accessor = Self::equivalent_accessor::

("u32".to_string()); + wgsl! { + fn isnan(val: 'accessor) -> 'output_accessor { + return 'output_accessor((bitcast<'u32_accessor>(val) & 'u32_accessor(0x7fffffffu)) > 'u32_accessor(0x7f800000u)); + } + } + } + _ => { + panic!("Unsupported dtype for isinf: {:?}", dtype); + } + } + } + + fn render_isinf(dtype: DType) -> String { + let accessor = P::render_type(); + let output_accessor = Self::equivalent_accessor::

("i32".to_string()); + + match dtype { + // TODO: F16 arm has never been tested + DType::F16 => { + wgsl! { + fn isinf(val: 'accessor) -> 'output_accessor { + let packed_val = pack2x16float(vec2(val, 0.0h)); + + let bits = packed_val & 0x0000ffffu; + + return 'output_accessor((bits & 0x7fffu) == 0x7C00u); + } + } + } + DType::F32 => { + let u32_accessor = Self::equivalent_accessor::

("u32".to_string()); + wgsl! { + fn isinf(val: 'accessor) -> 'output_accessor { + return 'output_accessor((bitcast<'u32_accessor>(val) & 'u32_accessor(0x7fffffffu)) == 'u32_accessor(0x7f800000u)); + } + } + } + _ => { + panic!("Unsupported dtype for isinf: {:?}", dtype); + } + } + } } #[derive(Debug, ShaderType, WgslMetadata)] @@ -306,20 +448,35 @@ impl Operation for Unary { UnaryOp::Silu => "Silu", UnaryOp::Sigmoid => "Sigmoid", UnaryOp::Swiglu => "Swiglu", + UnaryOp::LogicalNot => "LogicalNot", + UnaryOp::IsNan => "IsNan", + UnaryOp::IsInf => "IsInf", } } fn compute_view(&self) -> Result { - Ok(self.input.storage_view().clone()) + if matches!( + self.op, + UnaryOp::LogicalNot | UnaryOp::IsNan | UnaryOp::IsInf + ) { + let shape = self.input.shape().clone(); + let stride = Stride::from(&shape); + Ok(StorageView::new(shape, DType::I32, stride)) + } else { + Ok(self.input.storage_view().clone()) + } } #[inline] - fn srcs(&self) -> RVec<&Tensor> { + fn srcs(&self) -> RVec<&OpTensor> { rvec![&self.input] } fn supports_inplace(&self) -> bool { - true + !matches!( + self.op, + UnaryOp::LogicalNot | UnaryOp::IsNan | UnaryOp::IsInf + ) } } @@ -355,33 +512,46 @@ impl Kernel for UnaryKernels { } } - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { let UnaryKernels::Standard(inner) = self; - let a_rank = &inner.input.shape().rank(); + let a_rank = &inner.input.shape().dim(); let N = &inner.input.shape()[a_rank - 1]; - if N % 4 == 0 { + if N.is_multiple_of(4) { KernelElement::Vec4 - } else if N % 2 == 0 { + } else if N.is_multiple_of(2) { KernelElement::Vec2 } else { KernelElement::Scalar } } - fn calculate_dispatch(&self, dst: &Tensor) -> Result { + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) } fn build_kernel( &self, inplace: bool, - dst: &Tensor, + dst: &OpTensor, workgroup_size: &WorkgroupSize, ) -> Result { let kernel_element = self.kernel_element(dst); - match (dst.dt(), &kernel_element) { + let UnaryKernels::Standard(inner) = self; + match inner.op { + UnaryOp::LogicalNot | UnaryOp::IsNan | UnaryOp::IsInf => { + if dst.dtype() == DType::F32 { + panic!( + "Unsupported dtype for unary operation {:?} with boolean output: {:?}", + inner.name(), + dst.dtype() + ); + } + } + _ => {} + } + match (inner.input.dtype(), &kernel_element) { (DType::F32, KernelElement::Scalar) => { self.render::>(inplace, dst, workgroup_size) } @@ -400,15 +570,28 @@ impl Kernel for UnaryKernels { (DType::F16, KernelElement::Vec4) => { self.render::>(inplace, dst, workgroup_size) } + (DType::I32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } _ => Err(OperationError::CompileError(format!( "Unsupported dtype {:?} or kernel element {:?}", - dst.dt(), + dst.dtype(), kernel_element ))), } } - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { Ok(UnaryMeta { numel: dst.shape().numel() as u32, }) @@ -417,9 +600,11 @@ impl Kernel for UnaryKernels { #[cfg(all(test, feature = "pyo3"))] mod tests { - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; - use crate::{shape, test_util::run_py_prg, Device, DeviceRequest, Tensor, UnaryOp}; + use crate::{ + Device, DeviceRequest, Tensor, TensorOptions, UnaryOp, randn, test_util::run_py_prg, + }; #[derive(Arbitrary, Debug)] struct UnaryProblem { @@ -436,32 +621,69 @@ mod tests { r#" import torch import torch.nn.functional as F -def {}(a): - return F.{}(torch.from_numpy(a), {}).numpy() +def {kn}(a): + return F.{kn}(torch.from_numpy(a), {args}).numpy() "#, - kn, kn, args, ); let imp_prg = format!( r#" import torch -def {}(a): - return torch.{}(torch.from_numpy(a), {}).numpy() +def {kn}(a): + return torch.{kn}(torch.from_numpy(a), {args}).numpy() +"#, + ); + + let imp_with_cast_prg = format!( + r#" +import torch +def {kn}(a): + return torch.{kn}(torch.from_numpy(a), {args}).float().numpy() +"#, + ); + + let swiglu_prg = format!( + r#" +import torch +def {kn}(a): + x = torch.from_numpy(a) + return (x * x * torch.sigmoid(x)).numpy() +"#, + ); + + let relu2_prg = format!( + r#" +import torch +import torch.nn.functional as F +def {kn}(a): + return (F.relu(torch.from_numpy(a), {args})**2).numpy() "#, - kn, kn, args, ); let prg = match op { - UnaryOp::Gelu | UnaryOp::Silu | UnaryOp::Sigmoid => func_prg, + UnaryOp::Gelu | UnaryOp::Silu | UnaryOp::Sigmoid | UnaryOp::Relu => func_prg, + UnaryOp::Swiglu => swiglu_prg, + UnaryOp::Relu2 => relu2_prg, + UnaryOp::LogicalNot | UnaryOp::IsNan | UnaryOp::IsInf => imp_with_cast_prg, _ => imp_prg, }; - run_py_prg(prg.to_string(), &[a], &[], a.dt()) + run_py_prg(prg.to_string(), &[a], &[], a.dtype()) } fn run_unary_trial(prob: UnaryProblem, device: Device) -> anyhow::Result<()> { + // Not implemented on CPU for now + if device.is_cpu() + && matches!( + prob.op, + UnaryOp::LogicalNot | UnaryOp::IsNan | UnaryOp::IsInf + ) + { + return Ok(()); + } + let UnaryProblem { op, B, M } = prob; - let a = Tensor::randn::(0., 1., shape![B, M], Device::CPU); + let a = randn((B, M), None, None, Default::default())?; let args = match op { UnaryOp::Gelu => "approximate=\"tanh\"", @@ -489,6 +711,9 @@ def {}(a): UnaryOp::Silu => a.silu()?, UnaryOp::Sigmoid => a.sigmoid()?, UnaryOp::Swiglu => a.swiglu()?, + UnaryOp::LogicalNot => a.logical_not()?.cast(crate::DType::F32)?, + UnaryOp::IsNan => a.isnan()?.cast(crate::DType::F32)?, + UnaryOp::IsInf => a.isinf()?.cast(crate::DType::F32)?, }; let (atol, rtol) = match op { @@ -514,4 +739,36 @@ def {}(a): let device = Device::request_device(DeviceRequest::CPU).unwrap(); run_unary_trial(prob, device).unwrap(); } + + #[test] + fn test_isnan_detection_f32() -> anyhow::Result<()> { + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + let t = Tensor::from_data( + vec![0.0f32, 1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY], + 5, + TensorOptions::new(), + )? + .to(&device)? + .isnan()?; + + let out = t.to(&Device::CPU)?.to_vec::()?; + assert_eq!(out, vec![0, 0, 1, 0, 0]); + Ok(()) + } + + #[test] + fn test_isinf_detection_f32() -> anyhow::Result<()> { + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + let t = Tensor::from_data( + vec![0.0f32, 1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY], + 5, + TensorOptions::new(), + )? + .to(&device)? + .isinf()?; + + let out = t.to(&Device::CPU)?.to_vec::()?; + assert_eq!(out, vec![0, 0, 0, 1, 1]); + Ok(()) + } } diff --git a/crates/ratchet-core/src/ops/view.rs b/crates/piston-core/src/ops/view.rs similarity index 56% rename from crates/ratchet-core/src/ops/view.rs rename to crates/piston-core/src/ops/view.rs index 80aa08d0..b99d7cdd 100644 --- a/crates/ratchet-core/src/ops/view.rs +++ b/crates/piston-core/src/ops/view.rs @@ -1,15 +1,15 @@ -use crate::{rvec, OpGuards, Operation, Shape, StorageView, Strides, Tensor}; +use crate::{OpGuards, OpTensor, Operation, Shape, StorageView, Stride, rvec}; -use ratchet_macros::IrFields; +use piston_macros::IrFields; #[derive(Debug, derive_new::new, Clone, IrFields)] pub struct View { - pub(crate) src: Tensor, + pub(crate) src: OpTensor, pub(crate) shape: Shape, } impl View { - pub fn input(&self) -> &Tensor { + pub fn input(&self) -> &OpTensor { &self.src } } @@ -17,7 +17,7 @@ impl View { impl OpGuards for View { fn check_shapes(&self) { let (src_shape, dst_shape) = (self.src.shape(), &self.shape); - assert_eq!(src_shape.rank(), dst_shape.rank()); + assert_eq!(src_shape.dim(), dst_shape.dim()); assert_eq!(src_shape.numel(), dst_shape.numel()); } @@ -30,12 +30,16 @@ impl Operation for View { } fn compute_view(&self) -> Result { - let strides = Strides::from(&self.shape); - Ok(StorageView::new(self.shape.clone(), self.src.dt(), strides)) + let stride = Stride::from(&self.shape); + Ok(StorageView::new( + self.shape.clone(), + self.src.dtype(), + stride, + )) } #[inline] - fn srcs(&self) -> crate::RVec<&Tensor> { + fn srcs(&self) -> crate::RVec<&OpTensor> { rvec![&self.src] } } diff --git a/crates/piston-core/src/ops/where_cond.rs b/crates/piston-core/src/ops/where_cond.rs new file mode 100644 index 00000000..240e5a19 --- /dev/null +++ b/crates/piston-core/src/ops/where_cond.rs @@ -0,0 +1,492 @@ +use derive_new::new; +use half::f16; +use inline_wgsl::wgsl; +use piston_macros::IrFields; + +use crate::{ + Array, BindingMode, BuiltIn, DType, DynKernelMetadata, GPUOperation, InvariantError, Kernel, + KernelElement, KernelRenderable, KernelSource, OpGuards, OpTensor, Operation, OperationError, + RVec, Scalar, Shape, StorageView, Stride, TensorTypeOrScalar, TensorTypeOrScalarEnum, Vec2, + Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, + gpu::{BindGroupLayoutDescriptor, dtype::WgslDType}, + rvec, +}; + +#[derive(new, Debug, Clone, IrFields)] +pub struct WhereCond { + pub condition: OpTensor, + pub on_true: TensorTypeOrScalarEnum, + pub on_false: TensorTypeOrScalarEnum, +} + +impl WhereCond { + pub fn dtype(&self) -> Result { + let tensor_dtype = self + .on_true + .map_tensor(|t| t.dtype()) + .or_else(|_| self.on_false.map_tensor(|t| t.dtype()))?; + match tensor_dtype { + TensorTypeOrScalarEnum::Tensor(t) => Ok(t), + TensorTypeOrScalarEnum::Scalar(_) => Ok(DType::F32), + } + } +} + +impl OpGuards for WhereCond { + fn check_shapes(&self) { + let (a, b, c) = (&self.condition, &self.on_true, &self.on_false); + if let TensorTypeOrScalarEnum::Tensor(b) = b { + assert_eq!(a.shape(), b.shape()); + } + if let TensorTypeOrScalarEnum::Tensor(c) = c { + assert_eq!(a.shape(), c.shape()); + } + } + + fn check_dtypes(&self) { + let (a, b, c) = (&self.condition, &self.on_true, &self.on_false); + assert!(matches!(a.dtype(), crate::DType::F32 | crate::DType::I32)); + if let TensorTypeOrScalarEnum::Tensor(b) = b { + assert!(matches!(b.dtype(), crate::DType::F32 | crate::DType::I32)); + } + if let TensorTypeOrScalarEnum::Tensor(c) = c { + assert!(matches!(c.dtype(), crate::DType::F32 | crate::DType::I32)); + } + if let TensorTypeOrScalarEnum::Tensor(b) = b + && let TensorTypeOrScalarEnum::Tensor(c) = c + { + assert!(b.dtype() == c.dtype()) + } + } +} + +impl Operation for WhereCond { + fn name(&self) -> &'static str { + "WhereCond" + } + + fn compute_view(&self) -> Result { + let on_true_shape = self + .on_true + .tensor() + .map(|t| t.shape().clone()) + .unwrap_or(Shape::scalar()); + let on_false_shape = self + .on_false + .tensor() + .map(|t| t.shape().clone()) + .unwrap_or(Shape::scalar()); + let shapes = &[self.condition.shape(), &on_true_shape, &on_false_shape]; + let broadcasted = Shape::multi_broadcast(shapes); + if broadcasted.is_none() { + let failed = shapes.iter().map(|s| (*s).clone()).collect::>(); + return Err(InvariantError::BroadcastingFailed(failed).into()); + } + let broadcasted = broadcasted.unwrap(); + let ostride = Stride::from(&broadcasted); + Ok(StorageView::new(broadcasted, self.dtype()?, ostride)) + } + + #[inline] + fn srcs(&self) -> RVec<&OpTensor> { + let mut srcs = rvec![&self.condition]; + if let TensorTypeOrScalarEnum::Tensor(on_true) = &self.on_true { + srcs.push(on_true); + } + if let TensorTypeOrScalarEnum::Tensor(on_false) = &self.on_false { + srcs.push(on_false); + } + srcs + } + + fn supports_inplace(&self) -> bool { + // For inplace, the on_{true,false} tensors must be the same dtype as the condition tensor + self.on_true + .tensor() + .is_none_or(|t| t.dtype() == self.condition.dtype()) + && self + .on_false + .tensor() + .is_none_or(|t| t.dtype() == self.condition.dtype()) + } +} + +pub enum WhereCondKernels { + Standard(WhereCond), +} + +impl GPUOperation for WhereCond { + type KernelEnum = WhereCondKernels; + + fn select_kernel(&self) -> Self::KernelEnum { + WhereCondKernels::Standard(self.clone()) + } +} + +impl Kernel for WhereCondKernels { + type Metadata = DynKernelMetadata; + + fn kernel_name(&self) -> String { + match self { + WhereCondKernels::Standard(_) => "where_cond".to_string(), + } + } + + fn kernel_element(&self, _dst: &OpTensor) -> KernelElement { + let WhereCondKernels::Standard(inner) = self; + let a_rank = inner.condition.shape().dim(); + let N = if a_rank > 0 { + inner.condition.shape()[a_rank - 1] + } else { + 1 + }; + + if N.is_multiple_of(4) { + KernelElement::Vec4 + } else if N.is_multiple_of(2) { + KernelElement::Vec2 + } else { + KernelElement::Scalar + } + } + + fn calculate_dispatch(&self, dst: &OpTensor) -> Result { + let WhereCondKernels::Standard(inner) = self; + Ok(Workload::std( + inner.condition.shape().numel(), + self.kernel_element(dst), + )) + } + + fn storage_bind_group_layout( + &self, + inplace: bool, + ) -> Result { + let WhereCondKernels::Standard(inner) = self; + let tensor_count = 1 + // condition is always a tensor + if matches!(inner.on_true, TensorTypeOrScalarEnum::Tensor(_)) { 1 } else { 0 } + + if matches!(inner.on_false, TensorTypeOrScalarEnum::Tensor(_)) { 1 } else { 0 }; + + match tensor_count { + 1 => { + // Only condition is a tensor, both on_true and on_false are scalars + if inplace { + Ok(BindGroupLayoutDescriptor::unary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::unary()) + } + } + 2 => { + // Condition + one of on_true/on_false is a tensor + if inplace { + Ok(BindGroupLayoutDescriptor::binary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::binary()) + } + } + 3 => { + // All three are tensors + if inplace { + Ok(BindGroupLayoutDescriptor::ternary_inplace()) + } else { + Ok(BindGroupLayoutDescriptor::ternary()) + } + } + _ => unreachable!("Invalid tensor count for WhereCond"), + } + } + + fn metadata( + &self, + dst: &OpTensor, + _: &KernelElement, + ) -> Result { + let WhereCondKernels::Standard(inner) = self; + let mut dyn_meta = DynKernelMetadata::new(); + dyn_meta.add_field("numel", dst.shape().numel() as u32); + + // Add scalar values to metadata + if let TensorTypeOrScalarEnum::Scalar(value) = &inner.on_true { + if dst.dtype().is_float() { + dyn_meta.add_field("on_true_value", *value); + } else { + dyn_meta.add_field("on_true_value", *value as i32); + } + } + + if let TensorTypeOrScalarEnum::Scalar(value) = &inner.on_false { + if dst.dtype().is_float() { + dyn_meta.add_field("on_false_value", *value); + } else { + dyn_meta.add_field("on_false_value", *value as i32); + } + } + + Ok(dyn_meta) + } + + fn build_kernel( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let kernel_element = self.kernel_element(dst); + let WhereCondKernels::Standard(inner) = self; + let dtype = inner.dtype()?; + match (dtype, &kernel_element) { + (DType::F32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::F16, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Scalar) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Vec2) => { + self.render::>(inplace, dst, workgroup_size) + } + (DType::I32, KernelElement::Vec4) => { + self.render::>(inplace, dst, workgroup_size) + } + _ => Err(OperationError::CompileError(format!( + "Unsupported dtype {dtype:?} or kernel element {kernel_element:?}" + ))), + } + } +} + +impl KernelRenderable for WhereCondKernels { + fn register_bindings( + &self, + builder: &mut WgslKernelBuilder, + inplace: bool, + ) -> Result<(), OperationError> { + let WhereCondKernels::Standard(inner) = self; + let arr = Array::

::default(); + builder.register_storage( + "A", + if inplace { + BindingMode::ReadWrite + } else { + BindingMode::ReadOnly + }, + arr, + ); + + // Only register storage for tensor inputs, not scalars + if matches!(inner.on_true, TensorTypeOrScalarEnum::Tensor(_)) { + builder.register_storage("B", BindingMode::ReadOnly, Array::

::default()); + } + if matches!(inner.on_false, TensorTypeOrScalarEnum::Tensor(_)) { + builder.register_storage("C", BindingMode::ReadOnly, Array::

::default()); + } + + if !inplace { + builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); + } + builder.register_uniform(); + Ok(()) + } + + fn render( + &self, + inplace: bool, + dst: &OpTensor, + workgroup_size: &WorkgroupSize, + ) -> Result { + let device = dst.device().try_gpu()?; + let mut kernel_builder = WgslKernelBuilder::new( + workgroup_size.clone(), + rvec![ + BuiltIn::WorkgroupId, + BuiltIn::NumWorkgroups, + BuiltIn::LocalInvocationIndex + ], + device.compute_features().clone(), + ); + + let kernel_element = self.kernel_element(dst); + + self.register_bindings::

(&mut kernel_builder, inplace)?; + kernel_builder.render_metadata(&self.metadata(dst, &kernel_element)?); + + let N = (P::W as u32).render(); + + kernel_builder.write_main(wgsl! { + let x_offset = workgroup_id.x * 64u; + let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; + if (index >= metadata.numel / 'N) { + return; + } + }); + + let dtype = P::T::DT; + + let kernel_element_str = match kernel_element { + KernelElement::Scalar => dtype.to_string(), + KernelElement::Vec2 => format!("{}<{}>", kernel_element.as_str(), dtype), + KernelElement::Vec4 => format!("{}<{}>", kernel_element.as_str(), dtype), + }; + + let WhereCondKernels::Standard(inner) = self; + + // Determine how to access on_true and on_false values + let on_true_expr = match &inner.on_true { + TensorTypeOrScalarEnum::Tensor(_) => "B[index]".to_string(), + TensorTypeOrScalarEnum::Scalar(_) => { + let casted_scalar_dtype = match kernel_element { + KernelElement::Scalar => dtype.to_string(), + KernelElement::Vec2 => format!("{}<{}>", kernel_element.as_str(), dtype), + KernelElement::Vec4 => format!("{}<{}>", kernel_element.as_str(), dtype), + }; + format!("{casted_scalar_dtype}(metadata.on_true_value)") + } + }; + + let on_false_expr = match &inner.on_false { + TensorTypeOrScalarEnum::Tensor(_) => "C[index]".to_string(), + TensorTypeOrScalarEnum::Scalar(_) => { + let casted_scalar_dtype = match kernel_element { + KernelElement::Scalar => dtype.to_string(), + KernelElement::Vec2 => format!("{}<{}>", kernel_element.as_str(), dtype), + KernelElement::Vec4 => format!("{}<{}>", kernel_element.as_str(), dtype), + }; + format!("{casted_scalar_dtype}(metadata.on_false_value)") + } + }; + + let apply = if inplace { + wgsl! { + let val = A[index]; + A[index] = select('on_false_expr, 'on_true_expr, val != 'kernel_element_str(0)); + } + } else { + wgsl! { Y[index] = select('on_false_expr, 'on_true_expr, A[index] != 'kernel_element_str(0)); } + }; + kernel_builder.write_main(apply); + Ok(kernel_builder.build()?) + } +} + +#[cfg(all(test, feature = "pyo3"))] +mod tests { + use proptest::arbitrary::any; + use test_strategy::{Arbitrary, proptest}; + + use crate::test_util::run_py_prg; + use crate::{Device, DeviceRequest, Tensor, randn}; + + fn ground_truth(a: &Tensor, b: &Tensor, c: &Tensor) -> anyhow::Result { + let prg = r#" +import torch +def where_cond(a, b, c): + return torch.where(torch.from_numpy(a) != 0, torch.from_numpy(b), torch.from_numpy(c)).numpy() +"#; + run_py_prg(prg.to_string(), &[a, b, c], &[], b.dtype()) + } + + fn ground_truth_scalar(a: &Tensor, b: &Tensor, scalar: f32) -> anyhow::Result { + let prg = r#" +import torch +def where_cond_scalar(a, b, scalar): + return torch.where(torch.from_numpy(a) != 0, torch.from_numpy(b), scalar).numpy() +"#; + run_py_prg(prg.to_string(), &[a, b], &[&scalar], b.dtype()) + } + + fn run_where_cond_trial(problem: WhereCondProblem, device: Device) { + let WhereCondProblem { B, M, N } = problem; + // Put through a ReLU so some of its entries are 0 + let a = randn((B, M, N), None, None, Default::default()) + .unwrap() + .relu() + .unwrap(); + let b = randn((B, M, N), None, None, Default::default()).unwrap(); + let c = randn((B, M, N), None, None, Default::default()).unwrap(); + let ground = ground_truth(&b, &a, &c).unwrap(); + + let a_gpu = a.to(&device).unwrap(); + let b_gpu = b.to(&device).unwrap(); + let c_gpu = c.to(&device).unwrap(); + let b = a_gpu.where_cond(b_gpu, c_gpu).unwrap(); + + let ours = b.to(&Device::CPU).unwrap(); + + log::debug!("ours = {ours:?}"); + log::debug!("ground = {ground:?}"); + + ground.all_close(&ours, 1e-6, 1e-6).unwrap(); + } + + fn run_where_cond_scalar_trial(problem: WhereCondScalarProblem, device: Device) { + let WhereCondScalarProblem { B, M, N, scalar } = problem; + // Put through a ReLU so some of its entries are 0 + let a = randn((B, M, N), None, None, Default::default()) + .unwrap() + .relu() + .unwrap(); + let b = randn((B, M, N), None, None, Default::default()).unwrap(); + let ground = ground_truth_scalar(&b, &a, scalar).unwrap(); + + let a_gpu = a.to(&device).unwrap(); + let b_gpu = b.to(&device).unwrap(); + let result = a_gpu.where_cond(b_gpu, scalar).unwrap(); + + let ours = result.to(&Device::CPU).unwrap(); + + log::debug!("ours = {ours:?}"); + log::debug!("ground = {ground:?}"); + + ground.all_close(&ours, 1e-6, 1e-6).unwrap(); + } + + #[derive(Arbitrary, Debug)] + struct WhereCondProblem { + #[strategy(1..=3usize)] + B: usize, + #[strategy(1..=256usize)] + M: usize, + #[strategy(1..=256usize)] + N: usize, + } + + #[derive(Arbitrary, Debug)] + struct WhereCondScalarProblem { + #[strategy(1..=3usize)] + B: usize, + #[strategy(1..=256usize)] + M: usize, + #[strategy(1..=256usize)] + N: usize, + #[strategy(any::())] + scalar: f32, + } + + #[proptest(cases = 8)] + fn test_where_cond(prob: WhereCondProblem) { + let _ = env_logger::builder().try_init(); + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_where_cond_trial(prob, device); + } + + #[proptest(cases = 8)] + fn test_where_cond_scalar(prob: WhereCondScalarProblem) { + let _ = env_logger::builder().try_init(); + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + run_where_cond_scalar_trial(prob, device); + } +} diff --git a/crates/ratchet-core/src/plot.rs b/crates/piston-core/src/plot.rs similarity index 80% rename from crates/ratchet-core/src/plot.rs rename to crates/piston-core/src/plot.rs index 1bd1e6e8..dde946ae 100644 --- a/crates/ratchet-core/src/plot.rs +++ b/crates/piston-core/src/plot.rs @@ -1,7 +1,5 @@ #![cfg(feature = "plotting")] -use crate::{ - CPUBuffer, DeviceStorage, GpuCompileKey, GradStore, HashSet, LazyOp, Tensor, TensorId, -}; +use crate::{CPUBuffer, DeviceStorage, GpuCompileKey, HashSet, LazyOp, OpTensor, TensorId}; use derive_new::new; use std::{ borrow::Cow, @@ -117,8 +115,8 @@ impl RenderableGraph { } fn build_graph( - post_order: &Vec<&Tensor>, - outputs: &BTreeMap, + post_order: &Vec<&OpTensor>, + outputs: &BTreeMap, strong_counts_inplace: &crate::HashMap, cpu_bufs: Option<&crate::HashMap>, ) -> anyhow::Result { @@ -146,7 +144,7 @@ impl RenderableGraph { t.plot_fmt(), cpu_bufs .and_then(|bufs| bufs.get(&t.id())) - .map(|buf| buf.dump(t.dt(), (buf.n_bytes() / 4) <= 8)) + .map(|buf| buf.dump(t.dtype(), (buf.n_bytes() / 4) <= 8)) .unwrap_or_default() ) } else { @@ -187,7 +185,7 @@ impl RenderableGraph { strong_count, cpu_bufs .and_then(|bufs| bufs.get(&src_t.id())) - .map(|buf| buf.dump(src_t.dt(), (buf.n_bytes() / 4) <= 8)) + .map(|buf| buf.dump(src_t.dtype(), (buf.n_bytes() / 4) <= 8)) .unwrap_or_default() )), *src_id, @@ -202,47 +200,6 @@ impl RenderableGraph { Ok(g) } - fn build_backward_graph(store: &GradStore) -> anyhow::Result { - log::warn!("Rendering plot"); - let mut g = RenderableGraph::new(); - - let mut graph_index_map = HashMap::new(); - for (id, grad) in store.iter() { - let execution_order = grad.execution_order(); - for t in execution_order.iter() { - if graph_index_map.contains_key(&t.id()) { - continue; - } - let renderable_node = g.create_node(t.id(), Cow::Owned(t.op().name().to_string())); - let can_inplace = t.op().supports_inplace() && t.strong_count() == 2; - match t.op() { - crate::LazyOp::Const => renderable_node.style_as_const(), - _ => renderable_node.style_as_op(), - } - if can_inplace { - renderable_node.style_as_inplace() - } - - let node_graph_id = renderable_node.plot_id; - graph_index_map.insert(t.id(), renderable_node.plot_id); - t.op().srcs().iter().for_each(|src_t| { - if let Some(src_id) = graph_index_map.get(&src_t.id()) { - let e = g.create_edge(Cow::Owned(src_t.plot_fmt()), *src_id, node_graph_id); - } else { - panic!("Source tensor not found in graph index map"); - } - }); - } - g.create_node( - grad.id(), - Cow::Owned(format!("{}\nGrad for #{:?}", grad.op().name(), *id)), - ) - .style_as_grad(); - } - - Ok(g) - } - fn plot_to_file(self, fname: impl AsRef) -> anyhow::Result<()> { let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); d.push(fname.as_ref()); @@ -340,8 +297,8 @@ impl<'a> dot3::GraphWalk<'a, Nd, PlotEdge> for RenderableGraph { } pub fn render_to_file( - post_order: &Vec<&Tensor>, - outputs: &BTreeMap, + post_order: &Vec<&OpTensor>, + outputs: &BTreeMap, strong_counts_inplace: &crate::HashMap, cpu_bufs: Option<&crate::HashMap>, fname: impl AsRef, @@ -351,7 +308,3 @@ pub fn render_to_file( fname, ) } - -pub fn render_backward_to_file(g: &GradStore, fname: impl AsRef) -> anyhow::Result<()> { - RenderableGraph::plot_to_file(RenderableGraph::build_backward_graph(&g)?, fname) -} diff --git a/crates/ratchet-core/src/quant.rs b/crates/piston-core/src/quant.rs similarity index 57% rename from crates/ratchet-core/src/quant.rs rename to crates/piston-core/src/quant.rs index 16557551..cd0392d9 100644 --- a/crates/ratchet-core/src/quant.rs +++ b/crates/piston-core/src/quant.rs @@ -1,6 +1,9 @@ use crate::{ - dtype::Quantized, gpu::STORAGE_BUFFER_ALIGN, DType, Device, Tensor, Q4_KF, Q4_KH, Q8_0F, Q8_0H, + DType, Device, Q4_KF, Q4_KH, Q8_0F, Q8_0H, Tensor, TensorOptions, dtype::Quantized, + gpu::STORAGE_BUFFER_ALIGN, }; +use anyhow::Result; +use maybe_async::maybe_async; use num::integer::div_floor; use num_traits::{AsPrimitive, Float, FromPrimitive, Zero}; @@ -8,7 +11,7 @@ use num_traits::{AsPrimitive, Float, FromPrimitive, Zero}; fn storage_align(n: usize) -> usize { let size_t = core::mem::size_of::(); let nbytes = n * size_t; - let aligned = if nbytes % STORAGE_BUFFER_ALIGN != 0 { + let aligned = if !nbytes.is_multiple_of(STORAGE_BUFFER_ALIGN) { nbytes + STORAGE_BUFFER_ALIGN - nbytes % STORAGE_BUFFER_ALIGN } else { nbytes @@ -28,7 +31,7 @@ pub fn quantize_inner(matrix: &[Q::FP], elements: usize) -> Vec(matrix: &[Q::FP], elements: usize) -> Vec, Vec>(d_matrix) }); quantized_matrix } -pub fn quantize(tensor: &Tensor) -> Tensor { - match (tensor.dt(), Q::dt()) { +#[maybe_async] +pub async fn quantize(tensor: &Tensor) -> Tensor { + match (tensor.dtype(), Q::dtype()) { (DType::F32, DType::Q8_0F(_)) => { - let matrix = tensor.to_vec::().unwrap(); - unsafe { - Tensor::from_quantized( - quantize_inner::(&matrix, tensor.shape().numel()), - DType::Q8_0F(Q8_0F::default()), - tensor.shape().clone(), - Device::CPU, - ) - } + let matrix = tensor.to_vec::().await.unwrap(); + Tensor::from_quantized( + quantize_inner::(&matrix, tensor.shape().numel()), + DType::Q8_0F(Q8_0F::default()), + tensor.shape().clone(), + Device::CPU, + ) } (DType::F32, DType::Q4_KF(_)) => { - let matrix = tensor.to_vec::().unwrap(); - unsafe { - Tensor::from_quantized( - quantize_inner::(&matrix, tensor.shape().numel()), - DType::Q4_KF(Q4_KF::default()), - tensor.shape().clone(), - Device::CPU, - ) - } + let matrix = tensor.to_vec::().await.unwrap(); + Tensor::from_quantized( + quantize_inner::(&matrix, tensor.shape().numel()), + DType::Q4_KF(Q4_KF::default()), + tensor.shape().clone(), + Device::CPU, + ) } (DType::F16, DType::Q8_0H(_)) => { - let matrix = tensor.to_vec::().unwrap(); - unsafe { - Tensor::from_quantized( - quantize_inner::(&matrix, tensor.shape().numel()), - DType::Q8_0H(Q8_0H::default()), - tensor.shape().clone(), - Device::CPU, - ) - } + let matrix = tensor.to_vec::().await.unwrap(); + Tensor::from_quantized( + quantize_inner::(&matrix, tensor.shape().numel()), + DType::Q8_0H(Q8_0H::default()), + tensor.shape().clone(), + Device::CPU, + ) } (DType::F16, DType::Q4_KH(_)) => { - let matrix = tensor.to_vec::().unwrap(); - unsafe { - Tensor::from_quantized( - quantize_inner::(&matrix, tensor.shape().numel()), - DType::Q4_KH(Q4_KH::default()), - tensor.shape().clone(), - Device::CPU, - ) - } + let matrix = tensor.to_vec::().await.unwrap(); + Tensor::from_quantized( + quantize_inner::(&matrix, tensor.shape().numel()), + DType::Q4_KH(Q4_KH::default()), + tensor.shape().clone(), + Device::CPU, + ) } - (dt, qdt) => panic!("Unsupported dtype combination {dt}, {qdt}"), + (dtype, q_dtype) => panic!("Unsupported dtype combination {dtype}, {q_dtype}"), } } @@ -129,44 +125,61 @@ pub fn dequantize_inner(quantized: &[u8], elements: usize) -> Vec< dequantized } -pub fn dequantize(quantized: Tensor) -> Tensor { - match quantized.dt() { +pub fn dequantize(quantized: Tensor) -> Result { + let quantized_requires_grad = quantized.requires_grad(); + match quantized.dtype() { DType::Q8_0F(_) => { let elements = quantized.shape().numel(); let original_shape = quantized.shape().clone(); - let raw_bytes = unsafe { quantized.into_bytes().unwrap() }; + let raw_bytes = quantized.into_bytes().unwrap(); let dequantized = dequantize_inner::(&raw_bytes, elements); - Tensor::from_data(&dequantized, original_shape, Device::CPU) + Tensor::from_data( + &dequantized, + original_shape, + TensorOptions::new().requires_grad(quantized_requires_grad), + ) } DType::Q4_KF(_) => { let elements = quantized.shape().numel(); let original_shape = quantized.shape().clone(); - let raw_bytes = unsafe { quantized.into_bytes().unwrap() }; + let raw_bytes = quantized.into_bytes().unwrap(); let dequantized = dequantize_inner::(&raw_bytes, elements); - Tensor::from_data(&dequantized, original_shape, Device::CPU) + Tensor::from_data( + &dequantized, + original_shape, + TensorOptions::new().requires_grad(quantized_requires_grad), + ) } DType::Q8_0H(_) => { let elements = quantized.shape().numel(); let original_shape = quantized.shape().clone(); - let raw_bytes = unsafe { quantized.into_bytes().unwrap() }; + let raw_bytes = quantized.into_bytes().unwrap(); let dequantized = dequantize_inner::(&raw_bytes, elements); - Tensor::from_data(&dequantized, original_shape, Device::CPU) + Tensor::from_data( + &dequantized, + original_shape, + TensorOptions::new().requires_grad(quantized_requires_grad), + ) } DType::Q4_KH(_) => { let elements = quantized.shape().numel(); let original_shape = quantized.shape().clone(); - let raw_bytes = unsafe { quantized.into_bytes().unwrap() }; + let raw_bytes = quantized.into_bytes().unwrap(); let dequantized = dequantize_inner::(&raw_bytes, elements); - Tensor::from_data(&dequantized, original_shape, Device::CPU) + Tensor::from_data( + &dequantized, + original_shape, + TensorOptions::new().requires_grad(quantized_requires_grad), + ) } - dt => panic!("Unsupported dtype {dt}"), + dtype => panic!("Unsupported dtype {dtype}"), } } #[cfg(test)] mod tests { use crate::{ - dequantize, quantize, shape, Device, Quantized, Tensor, Q4_KF, Q4_KH, Q8_0F, Q8_0H, + Q4_KF, Q4_KH, Q8_0F, Q8_0H, Quantized, TensorOptions, dequantize, quantize, randn, }; use half::f16; @@ -175,10 +188,10 @@ mod tests { where Q::FP: std::fmt::Display + num_traits::Float + Default, { - let ground = Tensor::randn::(0., 1., shape![64, 64], Device::CPU); + let ground = randn((4, 64), None, None, TensorOptions::new().dtype(Q::dtype())).unwrap(); let q = quantize::(&ground); let dq = dequantize(q); - ground.all_close(&dq, atol, rtol).unwrap(); + ground.all_close(&dq.unwrap(), atol, rtol).unwrap(); } #[test] diff --git a/crates/piston-core/src/scope.rs b/crates/piston-core/src/scope.rs new file mode 100644 index 00000000..5035bbed --- /dev/null +++ b/crates/piston-core/src/scope.rs @@ -0,0 +1,106 @@ +use crate::HashMap; +use std::cell::RefCell; + +// Each scope entry stores a name and the next id that was saved. +#[derive(Debug)] +struct ScopeEntry { + name: String, +} + +// The scope context holds a stack of scope entries and a counter for the next id. +#[derive(Debug)] +struct ScopeContext { + scopes: Vec, + duplicate_counter: Vec>, +} + +// Create a thread-local ScopeContext using RefCell for interior mutability. +thread_local! { + static SCOPE_CONTEXT: RefCell = RefCell::new(ScopeContext { + scopes: Vec::new(), + duplicate_counter: vec![HashMap::default()], + }); +} + +/// Returns the current scope as a concatenated string of the scope names, +/// separated by slashes. +pub fn get_current_scope() -> String { + SCOPE_CONTEXT.with(|ctx| { + let ctx = ctx.borrow(); + ctx.scopes + .iter() + .map(|entry| entry.name.clone()) + .collect::>() + .join("/") + }) +} + +/// Push a new scope with the given name. +/// This function formats the name with an id to ensure uniqueness. +fn push_scope(name: &str) { + SCOPE_CONTEXT.with(|cell| { + let mut ctx = cell.borrow_mut(); + let name_count = ctx + .duplicate_counter + .last_mut() + .unwrap() + .entry(name.to_string()) + .or_insert(0); + let formatted_name = format!("{}.{}", name, *name_count); + *name_count += 1; + // Save the current next_id (incremented by one) in the entry. + ctx.scopes.push(ScopeEntry { + name: formatted_name, + }); + ctx.duplicate_counter.push(HashMap::default()); + }); +} + +/// Pop the most recent scope off the stack. +/// Panics if there are no scopes to pop. +fn pop_scope() { + SCOPE_CONTEXT.with(|ctx| { + let mut ctx = ctx.borrow_mut(); + if !ctx.scopes.is_empty() { + ctx.duplicate_counter.pop(); + ctx.scopes.pop(); + } else { + panic!("Attempted to pop scope from an empty stack"); + } + }); +} + +/// Resets the scope context, ensuring that there are no remaining scopes. +/// Panics if the scope stack is not empty. +pub fn reset_scope_context() { + SCOPE_CONTEXT.with(|ctx| { + let mut ctx = ctx.borrow_mut(); + if !ctx.scopes.is_empty() || ctx.duplicate_counter.len() > 1 { + panic!( + "Expecting scope to be empty but it is '{}'", + get_current_scope() + ); + } + ctx.duplicate_counter.last_mut().unwrap().clear(); + }); +} + +/// A RAII-style scope pusher that pushes a scope on creation and pops it +/// when dropped. +pub struct ScopePusher; + +impl ScopePusher { + /// Create a new scope pusher that pushes the given scope. + pub fn new(name: &str) -> Self { + push_scope(name); + ScopePusher + } +} + +// When a ScopePusher goes out of scope, its Drop implementation will automatically +// pop the scope. +impl Drop for ScopePusher { + fn drop(&mut self) { + pop_scope(); + } +} diff --git a/crates/piston-core/src/shape.rs b/crates/piston-core/src/shape.rs new file mode 100644 index 00000000..4de514fb --- /dev/null +++ b/crates/piston-core/src/shape.rs @@ -0,0 +1,775 @@ +use crate::{RVec, Stride, rvec, shape}; +use anyhow::Result; +use encase::impl_wrapper; +use smallvec::ToSmallVec; +use std::{ + ops::{RangeFrom, RangeTo}, + slice::Iter, +}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +#[derive(Clone, PartialEq, Eq, Hash, Default)] +pub struct Shape(RVec); + +impl_wrapper!(Shape; using); + +impl Shape { + pub fn scalar() -> Self { + // TODO(vinhowe): Move to an empty scalar shape, once I have time to debug + Self(rvec![1]) + } + + pub fn new(shape: RVec) -> Self { + Self(shape) + } + + pub fn inner(&self) -> &RVec { + &self.0 + } + + pub fn get(&self, index: usize) -> Option<&usize> { + self.0.get(index) + } + + pub fn insert(&mut self, index: usize, dim: usize) { + self.0.insert(index, dim); + } + + pub fn numel(&self) -> usize { + self.0.iter().product() + } + + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + + pub fn iter(&self) -> Iter<'_, usize> { + self.0.iter() + } + + pub fn reverse(&mut self) { + self.0.reverse(); + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn dim(&self) -> usize { + self.len() + } + + pub fn push(&mut self, dim: usize) { + self.0.push(dim); + } + + pub fn remove(&mut self, index: usize) -> usize { + self.0.remove(index) + } + + pub fn is_scalar(&self) -> bool { + self.0.iter().all(|&x| x == 1) + } + + pub fn is_vector(&self) -> bool { + let mut shape = self.clone(); + shape.squeeze(None); + shape.dim() <= 1 + } + + #[inline] + pub fn left_pad_to(&mut self, scalar: usize, rank: usize) { + while self.0.len() < rank { + self.0.insert(0, scalar); + } + } + + #[inline] + pub fn right_pad_to(&mut self, scalar: usize, rank: usize) { + while self.0.len() < rank { + self.0.push(scalar); + } + } + + #[inline] + pub fn promote(shape: Shape, rank: usize) -> Shape { + let mut shape = shape; + shape.left_pad_to(1, rank); + shape + } + + #[inline] + pub fn squeeze(&mut self, dims: Option>) { + if let Some(dims) = dims { + // Create a sorted copy of the dims in descending order + // This way, removing elements won't affect the indices of elements we haven't processed + // yet + let mut sorted_dims = dims.to_vec(); + sorted_dims.sort_by(|a, b| b.cmp(a)); + + for dim in sorted_dims { + if dim < self.0.len() { + self.0.remove(dim); + } + } + } else { + self.0.retain(|x| *x != 1); + } + } + + #[inline] + pub fn unsqueeze(&mut self, usize: usize) { + self.0.insert(usize, 1); + } + + pub fn drain(&mut self, range: R) -> smallvec::Drain<'_, [usize; 4]> + where + R: std::ops::RangeBounds, + { + self.0.drain(range) + } + + pub fn slice(&self, range: R) -> Self + where + R: std::ops::RangeBounds + std::slice::SliceIndex<[usize], Output = [usize]>, + { + Shape(self.0[range].to_vec().into()) + } + + pub fn as_slice(&self) -> &[usize] { + &self.0 + } + + pub fn multi_broadcast(shapes: &[&Shape]) -> Option { + let max_rank = shapes.iter().map(|shape| shape.dim()).max()?; + let mut shape: Shape = shape![]; + for i in 0..max_rank { + let mut current_dim_size = 1; + for shape in shapes { + let len = shape.dim(); + let dim = if i < len { &shape[len - i - 1] } else { &1 }; + if dim != &1 { + if current_dim_size != 1 && dim != ¤t_dim_size { + return None; + } + current_dim_size = *dim; + } + } + shape.0.insert(0, current_dim_size) + } + Some(shape) + } + + /// Returns true if the stride is C contiguous (aka row major). + pub fn is_contiguous(&self, stride: &Stride) -> bool { + let stride_vec = stride.to_vec(); + if self.0.len() != stride_vec.len() { + return false; + } + let mut acc = 1; + for (&stride, &dim) in stride_vec.iter().zip(self.0.iter()).rev() { + if dim > 1 && stride != acc { + return false; + } + acc *= dim as isize; + } + true + } + + pub fn transpose(&mut self) { + let rank = self.dim(); + if rank < 2 { + return; + } + self.0.swap(rank - 2, rank - 1); + } +} + +impl core::ops::Deref for Shape { + type Target = [usize]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::fmt::Debug for Shape { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut shape = format!("[{}", self.0.first().unwrap_or(&0)); + for dim in self.0.iter().skip(1) { + shape.push_str(&format!("x{dim}")); + } + write!(f, "{shape}]") + } +} + +impl std::fmt::Display for Shape { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl std::ops::Index for Shape { + type Output = usize; + + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} + +impl std::ops::IndexMut for Shape { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.0[index] + } +} + +impl std::ops::Index> for Shape { + type Output = [usize]; + + fn index(&self, index: RangeFrom) -> &Self::Output { + &self.0[index] + } +} + +impl std::ops::Index> for Shape { + type Output = [usize]; + + fn index(&self, index: RangeTo) -> &Self::Output { + &self.0[index] + } +} + +impl From<&[usize; C]> for Shape { + fn from(dims: &[usize; C]) -> Self { + Self(dims.to_smallvec()) + } +} + +impl From<&[usize]> for Shape { + fn from(dims: &[usize]) -> Self { + Self(dims.into()) + } +} + +impl From<&Shape> for Shape { + fn from(shape: &Shape) -> Self { + Self(shape.0.to_smallvec()) + } +} + +impl From<()> for Shape { + fn from(_: ()) -> Self { + Self(rvec![]) + } +} + +impl std::iter::Iterator for Shape { + type Item = usize; + + fn next(&mut self) -> Option { + self.0.pop() + } +} + +impl std::iter::DoubleEndedIterator for Shape { + fn next_back(&mut self) -> Option { + self.0.pop() + } +} + +impl From<&Shape> for glam::UVec4 { + fn from(shape: &Shape) -> Self { + glam::UVec4::new( + shape[0] as u32, + shape[1] as u32, + shape[2] as u32, + shape[3] as u32, + ) + } +} + +impl From for glam::UVec4 { + fn from(shape: Shape) -> Self { + (&shape).into() + } +} + +impl From<&Shape> for glam::IVec3 { + fn from(shape: &Shape) -> Self { + glam::IVec3::new(shape[0] as i32, shape[1] as i32, shape[2] as i32) + } +} + +impl From for glam::IVec3 { + fn from(shape: Shape) -> Self { + (&shape).into() + } +} + +impl From for RVec { + fn from(shape: Shape) -> Self { + shape.0 + } +} + +impl From for Shape { + fn from(d1: usize) -> Self { + Self(rvec![d1]) + } +} + +macro_rules! impl_try_into_arr_for_shape { + ($($N:expr),*) => { + $( + impl TryInto<[usize; $N]> for &Shape { + type Error = anyhow::Error; + + fn try_into(self) -> Result<[usize; $N], Self::Error> { + if self.0.len() == $N { + let mut arr = [0; $N]; + for (i, &item) in self.0.iter().enumerate().take($N) { + arr[i] = item; + } + Ok(arr) + } else { + Err(anyhow::anyhow!("Shape has length {} but expected {}", self.0.len(), $N)) + } + } + } + )* + }; +} + +impl_try_into_arr_for_shape!(1, 2, 3, 4); + +macro_rules! impl_from_tuple { + ($tuple:ty, $($index:tt),+) => { + impl From<$tuple> for Shape { + fn from(d: $tuple) -> Self { + Self(rvec![$(d.$index,)+]) + } + } + } +} + +impl_from_tuple!((usize,), 0); +impl_from_tuple!((usize, usize), 0, 1); +impl_from_tuple!((usize, usize, usize), 0, 1, 2); +impl_from_tuple!((usize, usize, usize, usize), 0, 1, 2, 3); +impl_from_tuple!((usize, usize, usize, usize, usize), 0, 1, 2, 3, 4); +impl_from_tuple!((usize, usize, usize, usize, usize, usize), 0, 1, 2, 3, 4, 5); + +impl From> for Shape { + fn from(dims: RVec) -> Self { + Self(dims) + } +} + +impl From> for Shape { + fn from(dims: Vec) -> Self { + Self(dims.into()) + } +} + +macro_rules! extract_dims { + ($fn_name:ident, $cnt:tt, $dims:expr, $out_type:ty) => { + pub fn $fn_name(dims: &[usize]) -> Result<$out_type> { + if dims.len() != $cnt { + Err(anyhow::anyhow!( + "Unexpected number of dimensions: expected {}, got {}, shape: {:?}", + $cnt, + dims.len(), + Shape::from(dims) + )) + } else { + Ok($dims(dims)) + } + } + + impl Shape { + pub fn $fn_name(&self) -> Result<$out_type> { + $fn_name(self.0.as_slice()) + } + } + + impl crate::OpTensor { + pub fn $fn_name(&self) -> Result<$out_type> { + self.shape().$fn_name() + } + } + + impl std::convert::TryInto<$out_type> for Shape { + type Error = anyhow::Error; + fn try_into(self) -> anyhow::Result<$out_type> { + self.$fn_name() + } + } + }; +} + +#[cfg(test)] +mod tests { + use crate::Shape; + use proptest::prelude::*; + use std::ops::RangeInclusive; + + impl Arbitrary for Shape { + type Parameters = Vec>; + type Strategy = BoxedStrategy; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + args.prop_map(Into::::into).boxed() + } + } + + impl Shape { + pub fn as_torch(&self) -> String { + let mut shape = format!("({}", self[0]); + for dim in self.iter().skip(1) { + shape.push_str(&format!(", {dim}")); + } + shape.push(')'); + shape + } + } +} + +pub trait Dim { + fn to_index(&self, shape: &Shape, op: &'static str) -> Result; + fn to_index_plus_one(&self, shape: &Shape, op: &'static str) -> Result; +} + +impl Dim for usize { + fn to_index(&self, shape: &Shape, op: &'static str) -> Result { + let rank = shape.dim(); + if *self >= rank { + Err(anyhow::anyhow!("Dimension out of range for op: {}", op)) + } else { + Ok(*self) + } + } + + fn to_index_plus_one(&self, shape: &Shape, op: &'static str) -> Result { + let rank = shape.dim(); + if *self > rank { + Err(anyhow::anyhow!("Dimension out of range for op: {}", op)) + } else { + Ok(*self) + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum D { + Minus1, + Minus2, + Minus(usize), +} + +impl D { + fn out_of_range(&self, _: &Shape, op: &'static str) -> anyhow::Error { + let dim = match self { + Self::Minus1 => -1, + Self::Minus2 => -2, + Self::Minus(u) => -(*u as i32), + }; + // TODO(vinhowe): include shape + anyhow::anyhow!("Dimension {} out of range for op: {}", dim, op) + } +} + +impl Dim for D { + fn to_index(&self, shape: &Shape, op: &'static str) -> Result { + let rank = shape.dim(); + match self { + Self::Minus1 if rank >= 1 => Ok(rank - 1), + Self::Minus2 if rank >= 2 => Ok(rank - 2), + Self::Minus(u) if *u > 0 && rank >= *u => Ok(rank - *u), + _ => Err(self.out_of_range(shape, op)), + } + } + + fn to_index_plus_one(&self, shape: &Shape, op: &'static str) -> Result { + let rank = shape.dim(); + match self { + Self::Minus1 => Ok(rank), + Self::Minus2 if rank >= 1 => Ok(rank - 1), + Self::Minus(u) if *u > 0 && rank + 1 >= *u => Ok(rank + 1 - *u), + _ => Err(self.out_of_range(shape, op)), + } + } +} + +pub trait Dims: Sized { + fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result>; + + fn to_indexes(self, shape: &Shape, op: &'static str) -> Result> { + let dims = self.to_indexes_internal(shape, op)?; + for (i, &dim) in dims.iter().enumerate() { + if dims[..i].contains(&dim) { + anyhow::bail!("Duplicate dimension index: {}", dim) + } + if dim >= shape.dim() { + anyhow::bail!("Dimension out of range: {}", dim) + } + } + Ok(dims) + } +} + +impl Dims for T { + fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result> { + let dim = self.to_index(shape, op)?; + Ok(rvec![dim]) + } +} + +impl Dims for RVec { + fn to_indexes_internal(self, _: &Shape, _: &'static str) -> Result> { + Ok(self) + } +} + +impl Dims for [usize; N] { + fn to_indexes_internal(self, _: &Shape, _: &'static str) -> Result> { + Ok(self.to_vec().into()) + } +} + +impl Dims for &[usize] { + fn to_indexes_internal(self, _: &Shape, _: &'static str) -> Result> { + Ok(self.to_vec().into()) + } +} + +impl Dims for () { + fn to_indexes_internal(self, _: &Shape, _: &'static str) -> Result> { + Ok(rvec![]) + } +} + +impl Dims for (D,) { + fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result> { + let dim = self.0.to_index(shape, op)?; + Ok(rvec![dim]) + } +} + +impl Dims for (D1, D2) { + fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result> { + let d0 = self.0.to_index(shape, op)?; + let d1 = self.1.to_index(shape, op)?; + Ok(rvec![d0, d1]) + } +} + +impl Dims for (D1, D2, D3) { + fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result> { + let d0 = self.0.to_index(shape, op)?; + let d1 = self.1.to_index(shape, op)?; + let d2 = self.2.to_index(shape, op)?; + Ok(rvec![d0, d1, d2]) + } +} + +impl Dims for (D1, D2, D3, D4) { + fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result> { + let d0 = self.0.to_index(shape, op)?; + let d1 = self.1.to_index(shape, op)?; + let d2 = self.2.to_index(shape, op)?; + let d3 = self.3.to_index(shape, op)?; + Ok(rvec![d0, d1, d2, d3]) + } +} + +impl Dims for (D1, D2, D3, D4, D5) { + fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result> { + let d0 = self.0.to_index(shape, op)?; + let d1 = self.1.to_index(shape, op)?; + let d2 = self.2.to_index(shape, op)?; + let d3 = self.3.to_index(shape, op)?; + let d4 = self.4.to_index(shape, op)?; + Ok(rvec![d0, d1, d2, d3, d4]) + } +} + +impl Dims for (D1, D2, D3, D4, D5, D6) { + fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> Result> { + let d0 = self.0.to_index(shape, op)?; + let d1 = self.1.to_index(shape, op)?; + let d2 = self.2.to_index(shape, op)?; + let d3 = self.3.to_index(shape, op)?; + let d4 = self.4.to_index(shape, op)?; + let d5 = self.5.to_index(shape, op)?; + Ok(rvec![d0, d1, d2, d3, d4, d5]) + } +} + +pub struct AllDims; + +impl Dims for AllDims { + fn to_indexes_internal(self, shape: &Shape, _: &'static str) -> Result> { + Ok((0..shape.dim()).collect()) + } +} + +extract_dims!(dims0, 0, |_: &[usize]| (), ()); +extract_dims!(dims1, 1, |d: &[usize]| d[0], usize); +extract_dims!(dims2, 2, |d: &[usize]| (d[0], d[1]), (usize, usize)); +extract_dims!( + dims3, + 3, + |d: &[usize]| (d[0], d[1], d[2]), + (usize, usize, usize) +); +extract_dims!( + dims4, + 4, + |d: &[usize]| (d[0], d[1], d[2], d[3]), + (usize, usize, usize, usize) +); +extract_dims!( + dims5, + 5, + |d: &[usize]| (d[0], d[1], d[2], d[3], d[4]), + (usize, usize, usize, usize, usize) +); + +pub trait ShapeWithOneHole { + fn into_shape(self, el_count: usize) -> Result; +} + +impl> ShapeWithOneHole for S { + fn into_shape(self, _el_count: usize) -> Result { + Ok(self.into()) + } +} + +impl ShapeWithOneHole for ((),) { + fn into_shape(self, el_count: usize) -> Result { + Ok(el_count.into()) + } +} + +pub fn hole_size(el_count: usize, prod_d: usize, s: &dyn std::fmt::Debug) -> Result { + if prod_d == 0 { + anyhow::bail!("cannot reshape tensor of {el_count} elements to {s:?}") + } + if !el_count.is_multiple_of(prod_d) { + anyhow::bail!("shape.hole_size: cannot reshape tensor with {el_count} elements to {s:?}") + } + Ok(el_count / prod_d) +} + +impl ShapeWithOneHole for ((), usize) { + fn into_shape(self, el_count: usize) -> Result { + let ((), d1) = self; + Ok((hole_size(el_count, d1, &self)?, d1).into()) + } +} + +impl ShapeWithOneHole for (usize, ()) { + fn into_shape(self, el_count: usize) -> Result { + let (d1, ()) = self; + Ok((d1, hole_size(el_count, d1, &self)?).into()) + } +} + +impl ShapeWithOneHole for ((), usize, usize) { + fn into_shape(self, el_count: usize) -> Result { + let ((), d1, d2) = self; + Ok((hole_size(el_count, d1 * d2, &self)?, d1, d2).into()) + } +} + +impl ShapeWithOneHole for (usize, (), usize) { + fn into_shape(self, el_count: usize) -> Result { + let (d1, (), d2) = self; + Ok((d1, hole_size(el_count, d1 * d2, &self)?, d2).into()) + } +} + +impl ShapeWithOneHole for (usize, usize, ()) { + fn into_shape(self, el_count: usize) -> Result { + let (d1, d2, ()) = self; + Ok((d1, d2, hole_size(el_count, d1 * d2, &self)?).into()) + } +} + +impl ShapeWithOneHole for ((), usize, usize, usize) { + fn into_shape(self, el_count: usize) -> Result { + let ((), d1, d2, d3) = self; + let d = hole_size(el_count, d1 * d2 * d3, &self)?; + Ok((d, d1, d2, d3).into()) + } +} + +impl ShapeWithOneHole for (usize, (), usize, usize) { + fn into_shape(self, el_count: usize) -> Result { + let (d1, (), d2, d3) = self; + let d = hole_size(el_count, d1 * d2 * d3, &self)?; + Ok((d1, d, d2, d3).into()) + } +} + +impl ShapeWithOneHole for (usize, usize, (), usize) { + fn into_shape(self, el_count: usize) -> Result { + let (d1, d2, (), d3) = self; + let d = hole_size(el_count, d1 * d2 * d3, &self)?; + Ok((d1, d2, d, d3).into()) + } +} + +impl ShapeWithOneHole for (usize, usize, usize, ()) { + fn into_shape(self, el_count: usize) -> Result { + let (d1, d2, d3, ()) = self; + let d = hole_size(el_count, d1 * d2 * d3, &self)?; + Ok((d1, d2, d3, d).into()) + } +} + +impl ShapeWithOneHole for ((), usize, usize, usize, usize) { + fn into_shape(self, el_count: usize) -> Result { + let ((), d1, d2, d3, d4) = self; + let d = hole_size(el_count, d1 * d2 * d3 * d4, &self)?; + Ok((d, d1, d2, d3, d4).into()) + } +} + +impl ShapeWithOneHole for (usize, (), usize, usize, usize) { + fn into_shape(self, el_count: usize) -> Result { + let (d1, (), d2, d3, d4) = self; + let d = hole_size(el_count, d1 * d2 * d3 * d4, &self)?; + Ok((d1, d, d2, d3, d4).into()) + } +} + +impl ShapeWithOneHole for (usize, usize, (), usize, usize) { + fn into_shape(self, el_count: usize) -> Result { + let (d1, d2, (), d3, d4) = self; + let d = hole_size(el_count, d1 * d2 * d3 * d4, &self)?; + Ok((d1, d2, d, d3, d4).into()) + } +} + +impl ShapeWithOneHole for (usize, usize, usize, (), usize) { + fn into_shape(self, el_count: usize) -> Result { + let (d1, d2, d3, (), d4) = self; + let d = hole_size(el_count, d1 * d2 * d3 * d4, &self)?; + Ok((d1, d2, d3, d, d4).into()) + } +} + +impl ShapeWithOneHole for (usize, usize, usize, usize, ()) { + fn into_shape(self, el_count: usize) -> Result { + let (d1, d2, d3, d4, ()) = self; + let d = hole_size(el_count, d1 * d2 * d3 * d4, &self)?; + Ok((d1, d2, d3, d4, d).into()) + } +} diff --git a/crates/ratchet-core/src/storage/cpu_buffer.rs b/crates/piston-core/src/storage/cpu_buffer.rs similarity index 84% rename from crates/ratchet-core/src/storage/cpu_buffer.rs rename to crates/piston-core/src/storage/cpu_buffer.rs index c0a225ac..d093c966 100644 --- a/crates/ratchet-core/src/storage/cpu_buffer.rs +++ b/crates/piston-core/src/storage/cpu_buffer.rs @@ -1,14 +1,26 @@ use bytemuck::{NoUninit, Pod}; use half::f16; -use crate::{storage::DeviceStorage, DType, Device, DeviceError, GPUBuffer, Shape, TensorDType}; +use crate::{DType, Device, DeviceError, GPUBuffer, Shape, TensorDType, storage::DeviceStorage}; use maybe_async::maybe_async; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::{alloc::Layout, fmt::Debug, mem::MaybeUninit, sync::Arc}; #[derive(derive_new::new, Debug, PartialEq, Eq)] pub struct RawCPUBuffer(*mut u8, Layout); +static ALIVE_CPUBUF_BYTES: AtomicUsize = AtomicUsize::new(0); +static ALIVE_CPUBUF_COUNT: AtomicUsize = AtomicUsize::new(0); + +pub fn alive_cpu_bytes() -> usize { + ALIVE_CPUBUF_BYTES.load(Ordering::Relaxed) +} + +pub fn alive_cpu_count() -> usize { + ALIVE_CPUBUF_COUNT.load(Ordering::Relaxed) +} + impl RawCPUBuffer { pub fn into_raw_parts(&self) -> (*mut u8, Layout) { (self.0, self.1) @@ -41,6 +53,10 @@ impl RawCPUBuffer { assert!(!ptr.is_null()); ptr } as *mut u8; + if size > 0 { + ALIVE_CPUBUF_BYTES.fetch_add(size, Ordering::Relaxed); + ALIVE_CPUBUF_COUNT.fetch_add(1, Ordering::Relaxed); + } Self(data, layout) } } @@ -56,6 +72,8 @@ impl Clone for RawCPUBuffer { impl Drop for RawCPUBuffer { fn drop(&mut self) { if !self.0.is_null() && self.1.size() > 0 { + ALIVE_CPUBUF_BYTES.fetch_sub(self.1.size(), Ordering::Relaxed); + ALIVE_CPUBUF_COUNT.fetch_sub(1, Ordering::Relaxed); unsafe { std::alloc::dealloc(self.0, self.1) } } } @@ -73,6 +91,7 @@ unsafe impl Sync for CPUBuffer {} impl CPUBuffer { pub fn new(inner: RawCPUBuffer) -> Self { Self { + #[allow(clippy::arc_with_non_send_sync)] inner: Arc::new(inner), } } @@ -99,21 +118,21 @@ impl CPUBuffer { } pub fn zeros(shape: &Shape) -> Self { - let n_bytes = shape.numel() * T::dt().size_of(); + let n_bytes = shape.numel() * T::dtype().size_of(); let mut raw = RawCPUBuffer::uninitialized(n_bytes, std::mem::align_of::()); raw.as_bytes_mut().fill(0); Self::new(raw) } pub fn ones(shape: &Shape) -> Self { - match T::dt() { + match T::dtype() { DType::Q8_0H(_) => Self::from_slice(&vec![1u8; shape.numel()], shape), DType::Q8_0F(_) => Self::from_slice(&vec![1u8; shape.numel()], shape), DType::F16 => Self::from_slice(&vec![f16::from_f32(1.0); shape.numel()], shape), DType::I32 => Self::from_slice(&vec![1i32; shape.numel()], shape), DType::F32 => Self::from_slice(&vec![1.0f32; shape.numel()], shape), DType::U32 => Self::from_slice(&vec![1u32; shape.numel()], shape), - _ => unimplemented!("Unable to create ones for {:?}", T::dt()), + _ => unimplemented!("Unable to create ones for {:?}", T::dtype()), } } @@ -121,8 +140,8 @@ impl CPUBuffer { reader: &mut R, shape: &Shape, ) -> Result { - let dt = T::dt(); - let n_bytes = shape.numel() * dt.size_of(); + let dtype = T::dtype(); + let n_bytes = shape.numel() * dtype.size_of(); let mut buf: Vec> = Vec::with_capacity(n_bytes); unsafe { buf.set_len(n_bytes); @@ -133,7 +152,7 @@ impl CPUBuffer { let buf = unsafe { std::mem::transmute::>, std::vec::Vec>(buf) }; - Ok(Self::from_bytes(&buf, dt.size_of())) + Ok(Self::from_bytes(&buf, dtype.size_of())) } pub fn inner(&self) -> &RawCPUBuffer { @@ -183,7 +202,7 @@ impl DeviceStorage for CPUBuffer { fn dump_inner(data: &[T], full: bool) -> String { let length = if data.len() < 64 { data.len() } else { 64 }; if full { - format!("{:?}", data) + format!("{data:?}") } else { format!( "{:?} ... {:?}", @@ -197,7 +216,7 @@ impl DeviceStorage for CPUBuffer { DType::I32 => dump_inner(bytemuck::cast_slice::(bytes), full), DType::U32 => dump_inner(bytemuck::cast_slice::(bytes), full), DType::F16 => dump_inner(bytemuck::cast_slice::(bytes), full), - dt => format!("[{:?} dump not yet supported]", dt), + dtype => format!("[{dtype:?} dump not yet supported]"), } } } diff --git a/crates/ratchet-core/src/storage/gpu_buffer.rs b/crates/piston-core/src/storage/gpu_buffer.rs similarity index 85% rename from crates/ratchet-core/src/storage/gpu_buffer.rs rename to crates/piston-core/src/storage/gpu_buffer.rs index 52f70d81..d0acccc9 100644 --- a/crates/ratchet-core/src/storage/gpu_buffer.rs +++ b/crates/piston-core/src/storage/gpu_buffer.rs @@ -1,8 +1,8 @@ use crate::{ + Device, DeviceError, MIN_STORAGE_BUFFER_SIZE, Shape, TensorDType, gpu::{BufferDescriptor, WgpuDevice}, gpu::{BufferUsagesExt, PooledGPUBuffer}, storage::{CPUBuffer, DeviceStorage}, - Device, DeviceError, Shape, TensorDType, MIN_STORAGE_BUFFER_SIZE, }; use bytemuck::NoUninit; @@ -33,14 +33,14 @@ impl GPUBuffer { //ensure that the buffer is zeroed pub fn zeros(shape: &Shape, device: &WgpuDevice) -> Self { Self::from_bytes( - vec![0; shape.numel() * T::dt().size_of()].as_slice(), - T::dt().size_of(), + vec![0; shape.numel() * T::dtype().size_of()].as_slice(), + T::dtype().size_of(), device, ) } pub fn ones(shape: &Shape, device: &WgpuDevice) -> Self { - match T::dt() { + match T::dtype() { DType::Q8_0H(_) => Self::from_slice(&vec![1u8; shape.numel()], shape, device), DType::Q8_0F(_) => Self::from_slice(&vec![1u8; shape.numel()], shape, device), DType::F16 => Self::from_slice(&vec![f16::from_f32(1.0); shape.numel()], shape, device), @@ -83,7 +83,7 @@ impl GPUBuffer { ) .unwrap(); device.queue().submit(None); - device.poll(wgpu::Maintain::Wait); + device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); Self { inner, alignment, @@ -119,7 +119,7 @@ impl GPUBuffer { device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); encoder.copy_buffer_to_buffer(&self.inner, 0, &clone, 0, self.inner.size()); device.queue().submit(Some(encoder.finish())); - device.poll(wgpu::Maintain::Wait); + device.poll(wgpu::PollType::wait_indefinitely()); Self { inner: clone, alignment: self.alignment, @@ -136,22 +136,22 @@ impl GPUBuffer { CPUBuffer::from_disk::(reader, shape)?.to_device(device) } - pub fn trim_id(id: wgpu::Id) -> Option { - let id = format!("{:?}", id); - let trimmed = id.trim_start_matches("Id(").trim_end_matches(')'); - if trimmed.len() > 12 && trimmed.chars().all(|c| c.is_numeric()) { - Some(trimmed[12..].to_string()) - } else { - None - } - } + // pub fn trim_id(id: wgpu::Id) -> Option { + // let id = format!("{id:?}"); + // let trimmed = id.trim_start_matches("Id(").trim_end_matches(')'); + // if trimmed.len() > 12 && trimmed.chars().all(|c| c.is_numeric()) { + // Some(trimmed[12..].to_string()) + // } else { + // None + // } + // } #[cfg(feature = "plotting")] pub fn plot_fmt(&self) -> String { - let id_string = Self::trim_id(self.inner().global_id()).unwrap_or_default(); + // let id_string = Self::trim_id(self.inner().global_id()).unwrap_or_default(); format!( - "GPU:#{}\n({:?})\n{} bytes", - id_string, + "GPU:\n({:?})\n{} bytes", + // id_string, self.inner.handle, self.inner.size() ) @@ -178,8 +178,8 @@ impl DeviceStorage for GPUBuffer { fn dump(&self, _: DType, _: bool) -> String { let mut result = String::new(); - let id_string = Self::trim_id(self.inner().global_id()).unwrap_or_default(); - result.push_str(&format!("GPU Buffer #{}\n", id_string)); + // let id_string = Self::trim_id(self.inner().global_id()).unwrap_or_default(); + // result.push_str(&format!("GPU Buffer #{id_string}\n")); result.push_str(&format!("Size: {} bytes\n", self.inner.size())); result } @@ -208,7 +208,7 @@ pub async fn wgpu_buffer_to_cpu_buffer( }) .expect("Failed to send result of read_buffer"); }); - device.poll(wgpu::Maintain::Wait); + device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); #[cfg(target_arch = "wasm32")] return rx.receive().await.unwrap().unwrap(); #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/ratchet-core/src/storage/mod.rs b/crates/piston-core/src/storage/mod.rs similarity index 95% rename from crates/ratchet-core/src/storage/mod.rs rename to crates/piston-core/src/storage/mod.rs index 0f5e5c15..b713c6bd 100644 --- a/crates/ratchet-core/src/storage/mod.rs +++ b/crates/piston-core/src/storage/mod.rs @@ -77,10 +77,10 @@ impl Storage { } } - pub fn dump(&self, dt: DType, full: bool) -> String { + pub fn dump(&self, dtype: DType, full: bool) -> String { match self { - Storage::CPU(c) => c.dump(dt, full), - Storage::GPU(g) => g.dump(dt, full), + Storage::CPU(c) => c.dump(dtype, full), + Storage::GPU(g) => g.dump(dtype, full), } } @@ -142,5 +142,5 @@ pub trait DeviceStorage: std::fmt::Debug + Clone + 'static { /// Creates a copy of the device buffer on the CPU async fn to_cpu(&self, device: &Device) -> Result; fn n_bytes(&self) -> usize; - fn dump(&self, dt: DType, full: bool) -> String; + fn dump(&self, dtype: DType, full: bool) -> String; } diff --git a/crates/ratchet-core/src/strides.rs b/crates/piston-core/src/stride.rs similarity index 54% rename from crates/ratchet-core/src/strides.rs rename to crates/piston-core/src/stride.rs index 2ca653f9..4604255f 100644 --- a/crates/ratchet-core/src/strides.rs +++ b/crates/piston-core/src/stride.rs @@ -1,15 +1,15 @@ use std::ops::{Index, IndexMut, RangeFrom, RangeTo}; use std::slice::Iter; -use crate::{rvec, RVec, Shape}; +use crate::{RVec, Shape, rvec}; use encase::impl_wrapper; #[derive(Clone, PartialEq, Eq, Default, Hash)] -pub struct Strides(RVec); +pub struct Stride(RVec); -impl_wrapper!(Strides; using); +impl_wrapper!(Stride; using); -impl Strides { +impl Stride { pub fn to_vec(&self) -> Vec { self.0.to_vec() } @@ -35,24 +35,24 @@ impl Strides { } } -impl std::fmt::Debug for Strides { +impl std::fmt::Debug for Stride { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut shape = format!("[{}", self.0.first().unwrap_or(&0)); for dim in self.0.iter().skip(1) { - shape.push_str(&format!("x{}", dim)); + shape.push_str(&format!("x{dim}")); } - write!(f, "{}]", shape) + write!(f, "{shape}]") } } -impl core::ops::Deref for Strides { +impl core::ops::Deref for Stride { type Target = [isize]; fn deref(&self) -> &Self::Target { &self.0 } } -impl Index for Strides { +impl Index for Stride { type Output = isize; fn index(&self, index: usize) -> &Self::Output { @@ -60,13 +60,13 @@ impl Index for Strides { } } -impl IndexMut for Strides { +impl IndexMut for Stride { fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.0[index] } } -impl Index> for Strides { +impl Index> for Stride { type Output = [isize]; fn index(&self, index: RangeFrom) -> &Self::Output { @@ -74,7 +74,7 @@ impl Index> for Strides { } } -impl Index> for Strides { +impl Index> for Stride { type Output = [isize]; fn index(&self, index: RangeTo) -> &Self::Output { @@ -82,7 +82,7 @@ impl Index> for Strides { } } -impl From<&Shape> for Strides { +impl From<&Shape> for Stride { fn from(shape: &Shape) -> Self { let mut strides = rvec![]; let mut stride = 1; @@ -95,74 +95,74 @@ impl From<&Shape> for Strides { } } -impl From> for Strides { - fn from(strides: Vec) -> Self { - Self(strides.into()) +impl From> for Stride { + fn from(stride: Vec) -> Self { + Self(stride.into()) } } -impl From<&[isize]> for Strides { - fn from(strides: &[isize]) -> Self { - Self(strides.into()) +impl From<&[isize]> for Stride { + fn from(stride: &[isize]) -> Self { + Self(stride.into()) } } -impl From<&Strides> for [u32; 3] { - fn from(strides: &Strides) -> Self { - assert!(strides.0.len() <= 3); +impl From<&Stride> for [u32; 3] { + fn from(stride: &Stride) -> Self { + assert!(stride.0.len() <= 3); let mut array = [0; 3]; - for (i, &stride) in strides.0.iter().enumerate() { + for (i, &stride) in stride.0.iter().enumerate() { array[i] = stride as u32; } array } } -impl From<&Strides> for glam::UVec3 { - fn from(strides: &Strides) -> Self { - let array: [u32; 3] = strides.into(); +impl From<&Stride> for glam::UVec3 { + fn from(stride: &Stride) -> Self { + let array: [u32; 3] = stride.into(); glam::UVec3::from(array) } } -impl From<&Strides> for [u32; 4] { - fn from(strides: &Strides) -> Self { - assert!(strides.0.len() <= 4); +impl From<&Stride> for [u32; 4] { + fn from(stride: &Stride) -> Self { + assert!(stride.0.len() <= 4); let mut array = [0; 4]; - for (i, &stride) in strides.0.iter().enumerate() { + for (i, &stride) in stride.0.iter().enumerate() { array[i] = stride as u32; } array } } -impl From<&Strides> for [usize; 4] { - fn from(strides: &Strides) -> Self { - assert!(strides.0.len() <= 4); +impl From<&Stride> for [usize; 4] { + fn from(stride: &Stride) -> Self { + assert!(stride.0.len() <= 4); let mut array = [0; 4]; - for (i, &stride) in strides.0.iter().enumerate() { + for (i, &stride) in stride.0.iter().enumerate() { array[i] = stride as usize; } array } } -impl From<&Strides> for glam::UVec4 { - fn from(strides: &Strides) -> Self { - let array: [u32; 4] = strides.into(); +impl From<&Stride> for glam::UVec4 { + fn from(stride: &Stride) -> Self { + let array: [u32; 4] = stride.into(); glam::UVec4::from(array) } } -impl From for glam::IVec3 { - fn from(strides: Strides) -> Self { - (&strides).into() +impl From for glam::IVec3 { + fn from(stride: Stride) -> Self { + (&stride).into() } } -impl From<&Strides> for glam::IVec3 { - fn from(strides: &Strides) -> Self { - glam::IVec3::new(strides.0[0] as _, strides.0[1] as _, strides.0[2] as _) +impl From<&Stride> for glam::IVec3 { + fn from(stride: &Stride) -> Self { + glam::IVec3::new(stride.0[0] as _, stride.0[1] as _, stride.0[2] as _) } } @@ -171,10 +171,10 @@ mod tests { use crate::shape; #[test] - fn test_strides() { + fn test_stride() { use super::*; let shape = shape![2, 3, 4]; - let strides = Strides::from(&shape); - assert_eq!(strides.to_vec(), vec![12, 4, 1]); + let stride = Stride::from(&shape); + assert_eq!(stride.to_vec(), vec![12, 4, 1]); } } diff --git a/crates/piston-core/src/tensor.rs b/crates/piston-core/src/tensor.rs new file mode 100644 index 00000000..003ae3ed --- /dev/null +++ b/crates/piston-core/src/tensor.rs @@ -0,0 +1,4171 @@ +use crate::gpu::{BindGroupEntry, CpuUniform, WgpuDevice}; +#[cfg(not(feature = "debug"))] +use crate::{BufferDescriptor, BufferUsagesExt, GPUBuffer}; +use crate::{ + BufferSegment, CPUBuffer, Compiled, CompiledOp, ComputeCompileKey, DType, Device, + DeviceStorage, Dim, Dims, GPUOperation, GpuCompileKey, InvariantError, LazyGraphExecutorError, + LazyOp, Operation, OperationError, RVec, RawCPUBuffer, ScopePusher, Shape, Storage, Stride, + TensorDType, TensorId, cpu, get_current_scope, ops::*, rvec, +}; +use anyhow::Result; +use bitvec::prelude::*; +use derive_new::new; +use half::{bf16, f16}; +use maybe_async::maybe_async; +use npyz::WriterBuilder; +use num_traits::AsPrimitive; +use num_traits::NumCast; +use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use piston_macros::tensor_op; +use std::borrow::Cow; +use std::io::{BufRead, Seek}; +use std::mem::ManuallyDrop; +use std::ops::Bound; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; + +#[cfg(feature = "rand")] +use { + rand::prelude::*, + rand_distr::{Normal, Uniform}, +}; + +#[cfg(feature = "testing")] +use ndarray::{ArrayD, ArrayViewD, Dimension}; + +#[cfg(all(not(target_arch = "wasm32"), feature = "pyo3"))] +use numpy::PyArrayDyn; + +// thiserror error for Tensor +#[derive(thiserror::Error, Debug)] +pub enum TensorError { + #[error("Tensor is not resolved")] + NotResolved, + #[error("Tensor {0:?} is missing storage")] + NoStorage(TensorId), + #[error(transparent)] + DeviceError(#[from] crate::DeviceError), + #[error("Failed to transfer data to host")] + TransferError, + #[error(transparent)] + OperationError(#[from] OperationError), + #[error(transparent)] + LazyGraphExecutorError(#[from] Box), + #[error(transparent)] + UnknownError(#[from] anyhow::Error), +} + +/// A multi-dimensional array of data. +/// +/// A tensor is a lazy representation of an operation. The nodes required to compute it's +/// value and it's own value will not be computed until `resolve` is called. +#[derive(Clone)] +pub struct OpTensor { + pub(crate) inner: Arc, +} + +unsafe impl Send for OpTensor {} + +macro_rules! ensure_resolved_sync { + ($self:ident) => { + #[cfg(not(target_arch = "wasm32"))] + if !$self.resolved() { + $self + .apply_pending_graph() + .expect("Failed to apply pending graph"); + } + }; +} + +macro_rules! ensure_resolved { + ($self:ident) => { + if !$self.resolved() { + #[cfg(target_arch = "wasm32")] + { + $self + .apply_pending_graph() + .await + .expect("Failed to apply pending graph"); + } + #[cfg(not(target_arch = "wasm32"))] + { + $self + .apply_pending_graph() + .expect("Failed to apply pending graph"); + } + } + }; +} + +impl OpTensor { + fn register_with_device(&self) { + if let Device::GPU(inner) = self.device() { + log::trace!( + "Attempting to register tensor {:?} with op {:?} requires_grad={}", + self.id(), + self.op().name(), + self.requires_grad() + ); + inner.register_tensor(self); + } + } + + pub fn new( + op: LazyOp, + meta: StorageView, + storage: Option, + device: Device, + requires_grad: bool, + ) -> Result { + let _scope_guard = ScopePusher::new(op.name()); + let value = Self { + inner: Arc::new(Inner::new( + op, + Some(get_current_scope()), + meta, + storage, + device.clone(), + requires_grad, + )?), + }; + value.register_with_device(); + Ok(value) + } + + pub fn wrap(self) -> Tensor { + Tensor::wrap(self) + } + + pub(crate) fn full_impl, S: Into>( + shape: S, + value: T, + options: TensorOptions, + ) -> Result { + let device = options.device_or_default(); + let requires_grad = options.requires_grad_or_default(); + let shape = shape.into(); + let meta = StorageView { + shape: shape.clone(), + dtype: T::dtype(), + stride: Stride::from(&shape.clone()), + }; + Self::new( + LazyOp::FillPointwise(FillPointwise { + kind: FillPointwiseKind::Constant { value: value.as_() }, + shape: shape.clone(), + }), + meta, + None, + device.clone(), + requires_grad, + ) + } + + pub fn full, S: Into>( + shape: S, + value: T, + options: TensorOptions, + ) -> Result { + let shape = shape.into(); + let device = options.device_or_default(); + if device.is_cpu() { + let mut data = Vec::with_capacity(shape.numel()); + data.resize(shape.numel(), value); + Self::from_data(data, shape, options) + } else { + Self::full_impl::(shape, value, options) + } + } + + #[track_caller] + fn lazy(op: LazyOp, meta: StorageView, device: Device, requires_grad: bool) -> Result { + op.check_invariants(); + Self::new(op, meta, None, device, requires_grad) + } + + pub fn shallow( + op: LazyOp, + meta: StorageView, + storage: Arc>>, + device: Device, + requires_grad: bool, + ) -> Result { + let _scope_guard = ScopePusher::new(op.name()); + let value = Self { + inner: Arc::new(Inner::from_shallow( + op, + Some(get_current_scope()), + meta, + storage, + device, + requires_grad, + )?), + }; + value.register_with_device(); + Ok(value) + } + + pub fn strong_count(&self) -> usize { + Arc::strong_count(&self.inner) + } + + pub(crate) fn update_storage(&self, storage: Storage) { + *self.inner.storage.write() = Some(storage); + } + + // Helper method to handle broadcasting for multiple tensors + fn broadcast_tensors(tensors: RVec) -> Result, OperationError> { + if tensors.is_empty() { + return Ok(rvec![]); + } + + let shapes: RVec = tensors.iter().map(|t| t.shape().clone()).collect(); + let shape_refs: RVec<&Shape> = shapes.iter().collect(); + let broadcasted = Shape::multi_broadcast(&shape_refs); + + if broadcasted.is_none() { + return Err(InvariantError::BroadcastingFailed(shapes.to_vec()).into()); + } + + let broadcasted = broadcasted.unwrap(); + let mut result = RVec::with_capacity(tensors.len()); + + for (tensor, shape) in tensors.into_iter().zip(shapes.iter()) { + if shape != &broadcasted { + result.push(broadcast_to_kernel(tensor, broadcasted.clone())?); + } else { + result.push(tensor); + } + } + + Ok(result) + } + + // Convenience method for binary operations + fn broadcast_for_binary_op(self, other: Self) -> Result<(Self, Self), OperationError> { + let mut result = Self::broadcast_tensors(rvec![self, other])?; + let rhs = result.pop().unwrap(); + let lhs = result.pop().unwrap(); + Ok((lhs, rhs)) + } + + // Convenience method for ternary operations + fn broadcast_for_ternary_op( + self, + tensor1: Self, + tensor2: Self, + ) -> Result<(Self, Self, Self), OperationError> { + let mut result = Self::broadcast_tensors(rvec![self, tensor1, tensor2])?; + let t2 = result.pop().unwrap(); + let t1 = result.pop().unwrap(); + let input = result.pop().unwrap(); + Ok((input, t1, t2)) + } +} + +impl std::fmt::Debug for OpTensor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.device() { + Device::CPU => match self.dtype() { + DType::F32 => self.to_ndarray_view::().fmt(f), + _ => { + let storage_fmt = self.storage().as_ref().map(|s| s.dump(self.dtype(), false)); + let (id, op) = (self.id(), self.op()); + f.debug_struct("Tensor") + .field("id", &id) + .field("shape", &self.shape()) + .field("dtype", &self.dtype()) + .field("op", &op) + .field("storage", &storage_fmt) + .finish() + } + }, + Device::GPU(_) => { + let storage_fmt = self.storage().as_ref().map(|s| s.dump(self.dtype(), false)); + let (id, op) = (self.id(), self.op()); + f.debug_struct("Tensor") + .field("id", &id) + .field("shape", &self.shape()) + .field("dtype", &self.dtype()) + .field("op", &op) + .field("storage", &storage_fmt) + .finish() + } + } + } +} + +impl PartialEq for OpTensor { + fn eq(&self, other: &Self) -> bool { + self.inner.id == other.inner.id + } +} + +impl std::ops::Deref for OpTensor { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + self.inner.as_ref() + } +} + +/// Tensors are just an view into their underlying byte storage. +#[derive(new, Debug, Clone)] +pub struct StorageView { + shape: Shape, + dtype: DType, + stride: Stride, +} + +impl StorageView { + pub fn is_contiguous(&self) -> bool { + self.shape.is_contiguous(&self.stride) + } +} + +impl Drop for Inner { + fn drop(&mut self) { + if let Device::GPU(inner) = &self.device { + log::trace!("Attempting to unregister tensor {:?}", self.id); + inner.unregister_tensor(self.id); + } + + unsafe { + ManuallyDrop::drop(&mut self.storage); + } + } +} + +#[derive(Debug)] +pub struct Inner { + id: TensorId, + scope: Option, + op: LazyOp, + device: Device, + view: StorageView, + requires_grad: bool, + retains_grad: RwLock, + storage: ManuallyDrop>>>, + grad: Arc>>, + #[cfg(not(feature = "debug"))] + debug_tensor: Arc>>, + inplace: RwLock, +} + +impl AsRef for Inner { + fn as_ref(&self) -> &Inner { + self + } +} + +impl Inner { + fn new( + op: LazyOp, + scope: Option, + meta: StorageView, + storage: Option, + device: Device, + requires_grad: bool, + ) -> Result { + if !op.can_be_parameter() && requires_grad { + return Err(anyhow::anyhow!("Cannot require grad for non-const tensor")); + } + + Ok(Self { + id: TensorId::new(), + scope, + view: meta, + op, + device, + storage: ManuallyDrop::new(Arc::new(RwLock::new(storage))), + grad: Arc::new(RwLock::new(None)), + requires_grad, + retains_grad: RwLock::new(false), + #[cfg(not(feature = "debug"))] + debug_tensor: Arc::new(RwLock::new(None)), + inplace: RwLock::new(false), + }) + } + + fn from_shallow( + op: LazyOp, + scope: Option, + meta: StorageView, + storage: Arc>>, + device: Device, + requires_grad: bool, + ) -> Result { + if !op.can_be_parameter() && requires_grad { + return Err(anyhow::anyhow!("Cannot require grad for non-const tensor")); + } + + Ok(Self { + id: TensorId::new(), + scope, + view: meta, + op, + device, + storage: ManuallyDrop::new(storage), + grad: Arc::new(RwLock::new(None)), + requires_grad, + retains_grad: RwLock::new(false), + #[cfg(not(feature = "debug"))] + debug_tensor: Arc::new(RwLock::new(None)), + inplace: RwLock::new(false), + }) + } +} + +impl OpTensor { + pub fn id(&self) -> TensorId { + self.inner.id + } + + pub fn storage_view(&self) -> &StorageView { + &self.view + } + + pub fn dim(&self) -> usize { + self.view.shape.dim() + } + + pub fn dtype(&self) -> DType { + self.view.dtype + } + + pub fn shape(&self) -> &Shape { + &self.view.shape + } + + pub fn stride(&self) -> &Stride { + &self.view.stride + } + + //WARNING: very wrong for quantized types! + pub fn num_bytes(&self) -> usize { + self.view.shape.numel() * self.view.dtype.size_of() + } + + pub fn device(&self) -> &Device { + &self.device + } + + pub fn storage(&'_ self) -> RwLockReadGuard<'_, Option> { + self.inner.storage.read() + } + + pub fn resolved(&self) -> bool { + self.storage().is_some() + } + + pub fn op(&self) -> &LazyOp { + &self.inner.op + } + + pub fn scope(&self) -> &Option { + &self.inner.scope + } + + pub fn is_scalar(&self) -> bool { + self.shape().is_scalar() + } + + pub fn requires_grad(&self) -> bool { + self.inner.requires_grad + } + + pub fn retains_grad(&self) -> bool { + *self.inner.retains_grad.read() + } + + pub fn is_inplace(&self) -> bool { + *self.inner.inplace.read() + } + + // TODO: Get rid of these + /// Sets the content of the inner tensor, this does not require a mutable reference as inner + /// mutability is used. + pub fn set_sync(&self, src: Self) -> Result<()> { + if self.same_storage(&src) { + panic!("cannot set a variable to a tensor that is derived from its value"); + } + if self.shape() != src.shape() { + panic!( + "shape mismatch: {:?} != {:?} (target id: {:?}, source id: {:?})", + self.shape(), + src.shape(), + self.id(), + src.id() + ); + } + self.update_storage(Storage::GPU(GPUBuffer { + inner: src.storage().as_ref().unwrap().try_gpu()?.inner.clone(), + alignment: self.dtype().size_of(), + cpu_size: Some(self.num_bytes()), + })); + Ok(()) + } + + pub fn set(self, src: Self) -> Self { + copy_kernel(self, src).unwrap() + } + + #[cfg(feature = "plotting")] + pub fn plot_fmt(&self) -> String { + let shape = self.shape(); + let dtype = self.dtype(); + let storage = self.storage(); + let storage_fmt = storage + .as_ref() + .map(|s| s.plot_fmt()) + .unwrap_or_else(|| "Unresolved".to_string()); + let references = self.strong_count(); + format!( + "#{:?}-{:?}-{:?}{}\n{:#?}\n{}\n{:?} references", + self.id(), + dtype, + shape, + if self.requires_grad() { " (param)" } else { "" }, + self.op().ir().fields(), + storage_fmt, + references + ) + } +} + +macro_rules! impl_binary_op { + ($method_name:ident, $op:expr) => { + #[tensor_op(variants = [function, method, method_inplace])] + pub fn $method_name>( + input: OpTensor, + other: T, + ) -> Result { + let device = input.device().clone(); + let (input, other) = crate::promoted_cast(input, other)?; + let (lhs, rhs) = match other.tensor_or_scalar() { + Ok(TensorTypeOrScalarEnum::Tensor(other)) => { + let (lhs, rhs) = input.broadcast_for_binary_op(other.tensor().unwrap())?; + (lhs, TensorTypeOrScalarEnum::Tensor(rhs)) + } + Ok(TensorTypeOrScalarEnum::Scalar(other)) => { + (input, TensorTypeOrScalarEnum::Scalar(other)) + } + Err(e) => return Err(e), + }; + let binary = Binary::new(lhs, rhs, $op); + let new_view = binary.compute_view()?; + OpTensor::lazy(LazyOp::Binary(binary), new_view, device, false) + } + }; +} + +macro_rules! impl_binary_op_tensor_only { + ($method_name:ident, $op:expr) => { + #[tensor_op(variants = [function, method, method_inplace])] + pub fn $method_name(input: OpTensor, other: OpTensor) -> Result { + let device = input.device().clone(); + let (input, other) = crate::promoted_cast(input, other)?; + let (lhs, rhs) = input.broadcast_for_binary_op(other)?; + let binary = Binary::new(lhs, TensorTypeOrScalarEnum::Tensor(rhs), $op); + let new_view = binary.compute_view()?; + OpTensor::lazy(LazyOp::Binary(binary), new_view, device, false) + } + }; +} + +macro_rules! impl_ternary_op { + ($method_name:ident, $op:expr) => { + #[tensor_op(variants = [function, method, method_inplace])] + pub fn $method_name( + input: OpTensor, + tensor1: OpTensor, + tensor2: OpTensor, + value: f32, + ) -> Result { + let device = input.device().clone(); + // Promote dtypes across all three tensors and cast prior to broadcast + let (input, t1, t2) = crate::promoted_cast_ternary::<_, OpTensor, _, OpTensor, _>( + input, tensor1, tensor2, + )?; + let (input, t1, t2) = input.broadcast_for_ternary_op(t1, t2)?; + let ternary = Ternary::new(input, t1, t2, value, $op); + let new_view = ternary.compute_view()?; + OpTensor::lazy(LazyOp::Ternary(ternary), new_view, device, false) + } + }; +} + +macro_rules! impl_cmp_op { + ($method_name:ident, $op:expr) => { + #[tensor_op(variants = [function, method, method_inplace])] + pub fn $method_name>( + input: OpTensor, + other: T, + ) -> Result { + let device = input.device().clone(); + let (input, other) = crate::promoted_cast(input, other)?; + match other.tensor_or_scalar() { + Ok(TensorTypeOrScalarEnum::Tensor(other)) => { + let (lhs, rhs) = input.broadcast_for_binary_op(other)?; + let cmp = Cmp::new(lhs, TensorTypeOrScalarEnum::Tensor(rhs), $op); + let new_view = cmp.compute_view()?; + OpTensor::lazy(LazyOp::Cmp(cmp), new_view, device, false) + } + Ok(TensorTypeOrScalarEnum::Scalar(other)) => { + let device = input.device.clone(); + let cmp = Cmp::new(input, TensorTypeOrScalarEnum::Scalar(other), $op); + let new_view = cmp.compute_view()?; + OpTensor::lazy(LazyOp::Cmp(cmp), new_view, device, false) + } + Err(e) => Err(e), + } + } + }; +} + +macro_rules! impl_unary_op { + ($method_name:ident, $op:expr) => { + #[tensor_op(variants = [function, method, method_inplace])] + pub fn $method_name(input: OpTensor) -> Result { + let device = input.device().clone(); + let unary = Unary::new(input, $op); + let new_view = unary.compute_view()?; + OpTensor::lazy(LazyOp::Unary(unary), new_view, device, false) + } + }; +} + +impl_binary_op!(add, BinaryOp::Add); +impl_binary_op!(sub, BinaryOp::Sub); +impl_binary_op!(mul, BinaryOp::Mul); +impl_binary_op!(div, BinaryOp::Div); +impl_binary_op!(pow, BinaryOp::Pow); +impl_binary_op_tensor_only!(maximum, BinaryOp::Maximum); +impl_binary_op_tensor_only!(minimum, BinaryOp::Minimum); + +impl_ternary_op!(addcdiv, TernaryOp::Addcdiv); +impl_ternary_op!(addcmul, TernaryOp::Addcmul); + +impl_cmp_op!(eq, CmpOp::Eq); +impl_cmp_op!(ne, CmpOp::Ne); +impl_cmp_op!(le, CmpOp::Le); +impl_cmp_op!(ge, CmpOp::Ge); +impl_cmp_op!(lt, CmpOp::Lt); +impl_cmp_op!(gt, CmpOp::Gt); +impl_cmp_op!(logical_and, CmpOp::LogicalAnd); +impl_cmp_op!(logical_or, CmpOp::LogicalOr); +impl_cmp_op!(logical_xor, CmpOp::LogicalXor); + +impl_unary_op!(gelu, UnaryOp::Gelu); +impl_unary_op!(tanh, UnaryOp::Tanh); +impl_unary_op!(exp, UnaryOp::Exp); +impl_unary_op!(log, UnaryOp::Log); +impl_unary_op!(sin, UnaryOp::Sin); +impl_unary_op!(cos, UnaryOp::Cos); +impl_unary_op!(abs, UnaryOp::Abs); +impl_unary_op!(sqrt, UnaryOp::Sqrt); +impl_unary_op!(relu, UnaryOp::Relu); +impl_unary_op!(relu2, UnaryOp::Relu2); +impl_unary_op!(floor, UnaryOp::Floor); +impl_unary_op!(ceil, UnaryOp::Ceil); +impl_unary_op!(neg, UnaryOp::Neg); +impl_unary_op!(sigmoid, UnaryOp::Sigmoid); +impl_unary_op!(swiglu, UnaryOp::Swiglu); +impl_unary_op!(silu, UnaryOp::Silu); +impl_unary_op!(square, UnaryOp::Square); +impl_unary_op!(recip, UnaryOp::Reciprocal); +impl_unary_op!(logical_not, UnaryOp::LogicalNot); +impl_unary_op!(isnan, UnaryOp::IsNan); +impl_unary_op!(isinf, UnaryOp::IsInf); + +#[tensor_op(variants = [function, method])] +pub fn cast(input: OpTensor, dst_dtype: DType) -> Result { + if input.dtype() == dst_dtype { + return Ok(input); + } + + let device = input.device().clone(); + let cast = Cast::new(input, dst_dtype); + let new_view = cast.compute_view()?; + OpTensor::lazy(LazyOp::Cast(cast), new_view, device, false) +} + +/// Cast a tensor to full precision (IEEE 754 32-bit floating point). +#[tensor_op(variants = [method])] +pub fn float(input: OpTensor) -> Result { + cast_kernel(input, DType::F32) +} + +/// Cast a tensor to half precision (IEEE 754 16-bit floating point). +#[tensor_op(variants = [method])] +pub fn half(input: OpTensor) -> Result { + cast_kernel(input, DType::F16) +} + +#[tensor_op(variants = [function, method])] +pub fn group_norm( + input: OpTensor, + num_groups: usize, + weight: Option, + bias: Option, + eps: f32, +) -> Result { + let device = input.device().clone(); + let group_norm = GroupNorm::new(Norm::new(input, weight, bias, eps), num_groups); + let norm_op = NormOp::GroupNorm(group_norm); + let new_view = norm_op.compute_view()?; + OpTensor::lazy(LazyOp::Norm(norm_op), new_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn layer_norm( + input: OpTensor, + weight: Option, + bias: Option, + eps: f32, +) -> Result { + let device = input.device().clone(); + let layer_norm = Norm::new(input, weight, bias, eps); + let op = NormOp::LayerNorm(layer_norm); + let new_view = op.compute_view()?; + OpTensor::lazy(LazyOp::Norm(op), new_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn rms_norm(input: OpTensor, weight: Option, eps: f32) -> Result { + let device = input.device().clone(); + let rms = Norm::new(input, weight, None, eps); + let op = NormOp::RMSNorm(rms); + let new_view = op.compute_view()?; + OpTensor::lazy(LazyOp::Norm(op), new_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn conv1d( + input: OpTensor, + weight: OpTensor, + bias: Option, + stride: usize, + padding: usize, +) -> Result { + let device = input.device().clone(); + let conv = Conv::new(input, weight, bias, stride, padding); + let new_view = conv.compute_view()?; + OpTensor::lazy(LazyOp::Conv(conv), new_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn softmax(input: OpTensor, dim: D) -> Result { + let device = input.device().clone(); + let dim = dim.to_index(input.shape(), "softmax")?; + let softmax = Softmax::new(input, dim); + let new_view = softmax.compute_view()?; + OpTensor::lazy(LazyOp::Softmax(softmax), new_view, device, false) +} + +fn rope_impl( + input: OpTensor, + dim: usize, + base: f32, + offset: usize, + is_backward: bool, +) -> Result { + let device = input.device().clone(); + let rope = RoPE::new(input, dim, base, offset, is_backward); + let new_view = rope.compute_view()?; + OpTensor::lazy(LazyOp::RoPE(rope), new_view, device, false) +} + +#[tensor_op(variants = [function, method_inplace])] +pub fn rope(input: OpTensor, dim: usize, base: f32, offset: usize) -> Result { + rope_impl(input, dim, base, offset, false) +} + +#[tensor_op(variants = [function, method_inplace])] +pub fn rope_backward(input: OpTensor, dim: usize, base: f32, offset: usize) -> Result { + rope_impl(input, dim, base, offset, true) +} + +#[tensor_op(variants = [function, method])] +pub fn alibi(input: OpTensor, max_bias: f32) -> Result { + let device = input.device().clone(); + let alibi = Alibi::new(input, max_bias); + let new_view = alibi.compute_view()?; + OpTensor::lazy(LazyOp::Alibi(alibi), new_view, device, false) +} + +//TODO (vinhowe): figure out how to make this interface more like pytorch +#[tensor_op(variants = [function, method])] +pub fn matmul( + input: OpTensor, + rhs: OpTensor, + trans_lhs: bool, + trans_rhs: bool, +) -> Result { + let device = input.device().clone(); + let matmul = Matmul::new(input, rhs, None, trans_lhs, trans_rhs, false); + let new_view = matmul.compute_view()?; + OpTensor::lazy(LazyOp::Matmul(matmul), new_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn gemm( + input: OpTensor, + rhs: OpTensor, + bias: Option, + trans_lhs: bool, + trans_rhs: bool, + trans_out: bool, +) -> Result { + let device = input.device().clone(); + let gemm = Matmul::new(input, rhs, bias, trans_lhs, trans_rhs, trans_out); + let new_view = gemm.compute_view()?; + OpTensor::lazy(LazyOp::Matmul(gemm), new_view, device, false) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn affine(input: OpTensor, mul: f32, add: f32) -> Result { + let device = input.device().clone(); + let affine = Affine::new(input, mul, add); + let new_view = affine.compute_view()?; + OpTensor::lazy(LazyOp::Affine(affine), new_view, device, false) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn lerp>( + input: OpTensor, + end: OpTensor, + weight: T, +) -> Result { + let weight = weight.tensor_or_scalar()?; + let device = input.device().clone(); + let lerp = Lerp::new(input, end, weight); + let new_view = lerp.compute_view()?; + OpTensor::lazy(LazyOp::Lerp(lerp), new_view, device, false) +} + +fn reduce_impl( + input: OpTensor, + dims: RVec, + keepdim: bool, + op: ReduceOp, +) -> Result { + let device = input.device().clone(); + let reduce = Reduce::new(input, op, dims, keepdim); + let new_view = reduce.compute_view()?; + OpTensor::lazy(LazyOp::Reduce(reduce), new_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn sum(input: OpTensor, dim: D, keepdim: bool) -> Result { + let sum_dims = dim.to_indexes(input.shape(), "sum")?; + let device = input.device().clone(); + let sum = Reduce::new(input, ReduceOp::Sum, sum_dims, keepdim); + let new_view = sum.compute_view()?; + OpTensor::lazy(LazyOp::Reduce(sum), new_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn mean(input: OpTensor, dim: D, keepdim: bool) -> Result { + let dim = dim.to_indexes(input.shape(), "mean")?; + let reduced_dim: usize = dim.iter().map(|i| input.shape()[*i]).product(); + let scale = 1f32 / (reduced_dim as f32); + mul_kernel::<_, _, OpTensor>(sum_kernel(input, dim, keepdim)?, scale) +} + +#[tensor_op(variants = [function, method])] +pub fn var(input: OpTensor, dim: D, keepdim: bool) -> Result { + let dim = dim.to_indexes(input.shape(), "var_keepdim")?; + let n: usize = dim.iter().map(|&i| input.shape()[i]).product(); + let mean = mean_kernel(input.clone(), dim.clone(), true)?; + let squares = square_kernel(sub_kernel(input, mean)?)?; + div_kernel::<_, _, OpTensor>(sum_kernel(squares, dim, keepdim)?, n as f32 - 1.0) +} + +#[tensor_op(variants = [function, method])] +pub fn max(input: OpTensor, dim: D, keepdim: bool) -> Result { + let max_dims = dim.to_indexes(input.shape(), "max")?; + reduce_impl(input, max_dims, keepdim, ReduceOp::Max) +} + +#[tensor_op(variants = [function, method])] +pub fn min(input: OpTensor, dim: D, keepdim: bool) -> Result { + let min_dims = dim.to_indexes(input.shape(), "min")?; + reduce_impl(input, min_dims, keepdim, ReduceOp::Min) +} + +#[tensor_op(variants = [function, method])] +pub fn argmax(input: OpTensor, dim: D, keepdim: bool) -> Result { + let dim = dim.to_indexes(input.shape(), "argmax")?; + reduce_impl(input, dim, keepdim, ReduceOp::ArgMax) +} + +#[tensor_op(variants = [function, method])] +pub fn argmin(input: OpTensor, dim: D, keepdim: bool) -> Result { + let dim = dim.to_indexes(input.shape(), "argmin")?; + reduce_impl(input, dim, keepdim, ReduceOp::ArgMin) +} + +#[tensor_op(variants = [function, method])] +pub fn norm( + input: OpTensor, + ord: Option, + dim: D, + keepdim: bool, +) -> Result { + let device = input.device().clone(); + + // Default ord is Frobenius/2-norm + let ord = ord.unwrap_or_default(); + + // Default dim is all dimensions (flatten) + let dim = dim.to_indexes(input.shape(), "norm")?; + + match ord { + NormOrd::Frobenius | NormOrd::P(2.0) => { + // Use existing Norm2 reduce operation (sqrt(sum of squares)) + let reduce = Reduce::new(input, ReduceOp::Norm2, dim.clone(), keepdim); + let new_view = reduce.compute_view()?; + OpTensor::lazy(LazyOp::Reduce(reduce), new_view, device, false) + } + NormOrd::Inf => { + // Vector ∞-norm : max(|x|) + // Matrix ∞-norm : max(row_sums(|A|)) + let abs_tensor = abs_kernel(input)?; + let result = if dim.len() == 1 { + max_kernel(abs_tensor, dim.clone(), keepdim)? + } else if dim.len() == 2 { + // Matrix case – rows are dim[0], columns are dim[1] + let row_dim = dim[0]; + let col_dim = dim[1]; + let row_sums = sum_kernel(abs_tensor, col_dim, keepdim)?; + let row_dim_adj = if !keepdim && col_dim < row_dim { + row_dim - 1 + } else { + row_dim + }; + max_kernel(row_sums, row_dim_adj, keepdim)? + } else { + // Fallback – reduce max over all specified dims. + reduce_impl(abs_tensor, dim.clone(), keepdim, ReduceOp::Max)? + }; + Ok(result) + } + NormOrd::NegInf => { + // Vector −∞-norm : min(|x|) + // Matrix −∞-norm : min(row_sums(|A|)) + let abs_tensor = abs_kernel(input)?; + let result = if dim.len() == 1 { + min_kernel(abs_tensor, dim.clone(), keepdim)? + } else if dim.len() == 2 { + let row_dim = dim[0]; + let col_dim = dim[1]; + let row_sums = sum_kernel(abs_tensor, col_dim, keepdim)?; + let row_dim_adj = if !keepdim && col_dim < row_dim { + row_dim - 1 + } else { + row_dim + }; + min_kernel(row_sums, row_dim_adj, keepdim)? + } else { + reduce_impl(abs_tensor, dim.clone(), keepdim, ReduceOp::Min)? + }; + Ok(result) + } + NormOrd::Zero => { + // 0-norm : number of non-zero elements (vector only) + if dim.len() != 1 { + anyhow::bail!("0-norm is only defined for 1-D tensors"); + } + sum_kernel(ne_kernel::<_, _, OpTensor>(input, 0.0f32)?, dim, keepdim) + } + NormOrd::One => { + // Vector 1-norm : sum(|x|) + // Matrix 1-norm : max(column_sums(|A|)) + let abs_tensor = abs_kernel(input)?; + let result = if dim.len() == 1 { + sum_kernel(abs_tensor, dim, keepdim)? + } else if dim.len() == 2 { + let row_dim = dim[0]; + let col_dim = dim[1]; + let col_sums = sum_kernel(abs_tensor, row_dim, keepdim)?; + let col_dim_adj = if !keepdim && row_dim < col_dim { + col_dim - 1 + } else { + col_dim + }; + max_kernel(col_sums, col_dim_adj, keepdim)? + } else { + anyhow::bail!("1-norm for tensors with more than 2 dims is not supported"); + }; + Ok(result) + } + NormOrd::NegOne => { + // Vector (−1)-norm : harmonic mean norm + // Matrix (−1)-norm : min(column_sums(|A|)) + let abs_tensor = abs_kernel(input)?; + let result = if dim.len() == 1 { + let count = abs_tensor.shape()[dim[0]] as f32; + let sum_recip = sum_kernel(recip_kernel(abs_tensor)?, dim, keepdim)?; + // count / sum(1/|x|) + mul_kernel::<_, _, OpTensor>(recip_kernel(sum_recip)?, count)? + } else if dim.len() == 2 { + let row_dim = dim[0]; + let col_dim = dim[1]; + let col_sums = sum_kernel(abs_tensor, row_dim, keepdim)?; + let col_dim_adj = if !keepdim && row_dim < col_dim { + col_dim - 1 + } else { + col_dim + }; + min_kernel(col_sums, col_dim_adj, keepdim)? + } else { + anyhow::bail!("-1 norm for tensors with more than 2 dims is not supported"); + }; + Ok(result) + } + NormOrd::P(p) => { + if dim.len() != 1 { + anyhow::bail!("p-norm is only defined for 1-D tensors"); + } + // General p-norm ‑ (sum(|x|^p))^(1/p) + let powered = pow_kernel::<_, _, OpTensor>(abs_kernel(input)?, p)?; + let summed = sum_kernel(powered, dim, keepdim)?; + pow_kernel::<_, _, OpTensor>(summed, 1.0 / p) + } + } +} + +#[tensor_op(variants = [function, method])] +pub fn flatten(input: OpTensor, start_dim: D1, end_dim: D2) -> Result { + if input.dim() == 0 { + view_kernel(input, 1) + } else { + let start_dim = start_dim.to_index(input.shape(), "flatten")?; + let end_dim = end_dim.to_index(input.shape(), "flatten")?; + if start_dim < end_dim { + let dims = input.shape(); + let mut dst_dims = dims[..start_dim].to_vec(); + dst_dims.push( + dims.to_vec()[start_dim..end_dim + 1] + .iter() + .product::(), + ); + if end_dim + 1 < dims.len() { + dst_dims.extend(&dims[end_dim + 1..]); + } + view_kernel(input, dst_dims) + } else { + Ok(input) + } + } +} + +/// # Slice +/// +/// Current slice implementation requires specification of all dimensions. +/// Currently very user hostile, but will be improved. +/// TODO: should allow mixed range types +#[tensor_op(variants = [function, method])] +pub fn slice>(input: OpTensor, ranges: &[D]) -> Result { + let device = input.device().clone(); + let mut resolved_ranges = rvec![]; + + for (ridx, r) in ranges.iter().enumerate() { + let start = match r.start_bound() { + Bound::Included(&s) => s, + Bound::Excluded(&s) => s + 1, + Bound::Unbounded => 0, + }; + let end = match r.end_bound() { + Bound::Included(&e) => e + 1, + Bound::Excluded(&e) => e, + Bound::Unbounded => input.shape()[ridx], + }; + resolved_ranges.push(start..end); + } + + let slice = Slice::new(input, resolved_ranges); + let out_view = slice.compute_view()?; + let op = LazyOp::Reindex(Reindex::Slice(slice)); + OpTensor::lazy(op, out_view, device, false) +} + +pub enum SplitArg { + SplitSize(usize), + Sizes(RVec), +} + +/// Splits the tensor along dimension `dim`. When given a `SplitArg::SplitSize(n)`, +/// the tensor is split into chunks of size `n` (last chunk may be smaller). When +/// given `SplitArg::Sizes(sizes)`, it is split according to the explicit sizes. +#[tensor_op(variants = [function, method])] +pub fn split(input: OpTensor, arg: SplitArg, dim: D) -> Result> { + let dim = dim.to_index(input.shape(), "split")?; + let dim_len = input.shape()[dim]; + + match arg { + SplitArg::SplitSize(split_size) => { + if split_size == 0 { + anyhow::bail!("split: split_size must be > 0"); + } + if dim_len == 0 { + return Ok(rvec![]); + } + let mut start = 0usize; + let mut outputs: RVec = RVec::with_capacity(dim_len.div_ceil(split_size)); + while start < dim_len { + let len = (dim_len - start).min(split_size); + outputs.push(narrow_kernel(input.clone(), dim, start, len)?); + start += len; + } + Ok(outputs) + } + SplitArg::Sizes(sizes) => { + let total: usize = sizes.iter().sum(); + if total != dim_len { + anyhow::bail!( + "split_with_sizes: sizes {:?} must sum to dim length {} (dim {})", + sizes, + dim_len, + dim + ); + } + let mut start = 0usize; + let mut outputs: RVec = RVec::with_capacity(sizes.len()); + for &len in sizes.iter() { + outputs.push(narrow_kernel(input.clone(), dim, start, len)?); + start += len; + } + Ok(outputs) + } + } +} + +/// Splits the tensor into `chunks` parts along dimension `dim`. +/// The first `dim_len % chunks` chunks will have size `ceil(dim_len / chunks)`, +/// and the remainder will have size `floor(dim_len / chunks)`. Zero-length chunks +/// are produced if `chunks > dim_len`. +#[tensor_op(variants = [function, method])] +pub fn chunk(input: OpTensor, chunks: usize, dim: D) -> Result> { + let dim = dim.to_index(input.shape(), "chunk")?; + let dim_len = input.shape()[dim]; + + if chunks == 0 { + anyhow::bail!("chunk: chunks must be > 0"); + } + + let base = dim_len / chunks; + let rem = dim_len % chunks; + + // Build explicit sizes and reuse split_with_sizes implementation + let mut sizes = RVec::with_capacity(chunks); + for i in 0..chunks { + sizes.push(base + ((i < rem) as usize)); + } + split_kernel(input, SplitArg::Sizes(sizes), dim) +} + +/// Returns a new tensor that is a narrowed version of the input, the dimension `dim` +/// ranges from `start` to `start + len`. +/// This calls `slice` internally. +#[tensor_op(variants = [function, method])] +pub fn narrow(input: OpTensor, dim: D, start: usize, len: usize) -> Result { + let dims = input.shape().as_slice(); + let device = input.device().clone(); + let dim = dim.to_index(input.shape(), "narrow")?; + let err = |msg| { + anyhow::bail!( + "invalid narrow args: shape {:?}, dim {}, start {}, len {}, {}", + input.shape(), + dim, + start, + len, + msg + ) + }; + if start > dims[dim] { + err("start > dim_len")? + } + if start.saturating_add(len) > dims[dim] { + err("start + len > dim_len")? + } + if start == 0 && dims[dim] == len { + Ok(input) + } else { + // Create ranges for all dimensions, using full range for non-target dimensions + let mut ranges = rvec![]; + dims.iter().enumerate().for_each(|(i, &_d)| { + if i == dim { + ranges.push(start..start + len); + } else { + ranges.push(0..dims[i]); + } + }); + + let slice = Slice::new(input, ranges); + let out_view = slice.compute_view()?; + let op = LazyOp::Reindex(Reindex::Slice(slice)); + OpTensor::lazy(op, out_view, device, false) + } +} + +/// # View +/// +/// Creates a new tensor with the same data, but a different shape. +/// The new shape must have the same number of elements as the original shape. +#[tensor_op(variants = [function, method])] +pub fn view(input: OpTensor, shape: S) -> Result { + let shape = shape.into_shape(input.shape().numel())?; + if input.shape().numel() != shape.numel() { + anyhow::bail!( + "view: cannot reshape tensor with {} elements to shape {:?} ({} elements)", + input.shape().numel(), + shape, + shape.numel() + ); + } + let device = input.device().clone(); + let storage = Arc::clone(&input.storage); + let op = View::new(input, shape); + let out_view = op.compute_view()?; + + OpTensor::shallow(LazyOp::View(op), out_view, storage, device, false) +} + +// Use view to add a singleton dimension +#[tensor_op(variants = [function, method, method_inplace])] +pub fn unsqueeze(input: OpTensor, dim: D) -> Result { + let dim = dim.to_index_plus_one(input.shape(), "unsqueeze")?; + let mut new_shape = input.shape().clone(); + new_shape.unsqueeze(dim); + view_kernel(input, new_shape) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn squeeze(input: OpTensor, dims: D) -> Result { + let mut new_shape = input.shape().clone(); + let dims = dims.to_indexes(input.shape(), "squeeze")?; + // Special case for empty dims, which means squeeze all dimensions + // Relative to PyTorch, this is a terrible hack because we don't really have optional + // params. Oh well. + if dims.is_empty() { + new_shape.squeeze(None); + } else { + new_shape.squeeze(Some(dims)); + } + view_kernel(input, new_shape) +} + +fn cat_inner(tensors: RVec, dim: usize) -> Result { + let device = tensors[0].device().clone(); + let cat = Concat::new(tensors, dim); + let new_view = cat.compute_view()?; + OpTensor::lazy(LazyOp::Concat(cat), new_view, device, false) +} + +fn cat_impl(tensors: RVec, dim: usize) -> Result { + match tensors.len() { + 0 => anyhow::bail!("Cannot cat empty list of tensors"), + 1 => Ok(tensors[0].clone()), + len => { + let device = tensors[0].device().clone(); + assert!( + tensors.iter().all(|t| t.device == device), + "cat: mixed devices" + ); + + if len <= 4 { + return cat_inner(tensors, dim); + } + + // Process tensors in chunks of 4 recursively + let mut current_level = tensors; + + while current_level.len() > 1 { + let mut next_level = RVec::with_capacity(current_level.len().div_ceil(4)); + + for chunk in current_level.chunks(4) { + let chunk_vec = chunk.iter().cloned().collect(); + let reduced = cat_impl(chunk_vec, dim)?; + next_level.push(reduced); + } + + current_level = next_level; + } + + Ok(current_level.into_iter().next().unwrap()) + } + } +} + +#[tensor_op(variants = [function])] +pub fn cat(tensors: RVec, dim: D) -> Result { + let dim = dim.to_index(tensors[0].shape(), "cat")?; + cat_impl(tensors, dim) +} + +#[tensor_op(variants = [function])] +pub fn stack(tensors: RVec, dim: D) -> Result { + let dim = dim.to_index_plus_one(tensors[0].shape(), "stack")?; + match tensors.len() { + 0 => anyhow::bail!("Cannot stack empty list of tensors"), + 1 => Ok(unsqueeze_kernel(tensors[0].clone(), dim)?), + _ => { + // Preserve stack-specific device error message + let device = tensors[0].device().clone(); + assert!( + tensors.iter().all(|t| t.device == device), + "stack: mixed devices" + ); + + let tensors = tensors + .iter() + .map(|t| unsqueeze_kernel(t.clone(), dim)) + .collect::>>()?; + cat_impl(tensors, dim) + } + } +} + +#[tensor_op(variants = [function, method])] +pub fn permute(input: OpTensor, dims: D) -> Result { + let dims = dims.to_indexes(input.shape(), "permute")?; + let device = input.device().clone(); + let permute = Permute::new(input, dims); + let out_view = permute.compute_view()?; + + let op = LazyOp::Reindex(Reindex::Permute(permute)); + OpTensor::lazy(op, out_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn flip(input: OpTensor, dims: D) -> Result { + let dims = dims.to_indexes(input.shape(), "flip")?; + let device = input.device().clone(); + let flip = Flip::new(input, dims); + let out_view = flip.compute_view()?; + + let op = LazyOp::Reindex(Reindex::Flip(flip)); + OpTensor::lazy(op, out_view, device, false) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn transpose(input: OpTensor, dim0: D, dim1: D) -> Result { + let dim0 = dim0.to_index(input.shape(), "transpose")?; + let dim1 = dim1.to_index(input.shape(), "transpose")?; + let mut dims: RVec = (0..input.dim()).collect(); + dims.swap(dim0, dim1); + permute_kernel(input, dims) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn t(input: OpTensor) -> Result { + if input.dim() > 2 { + anyhow::bail!( + "t() can only be applied to tensors with 2 or fewer dimensions, got tensor with {} dimensions", + input.dim() + ); + } + transpose_kernel(input, 0, 1) +} + +/// Returns a tensor with the last two dimensions transposed. +/// For 2D tensors, equivalent to transpose(0, 1). +/// For tensors with any other number of dimensions, throws an error. +#[tensor_op(variants = [function, method, method_inplace])] +pub fn T(input: OpTensor) -> Result { + match input.dim() { + 2 => transpose_kernel(input, 0, 1), + _ => anyhow::bail!( + "T() can only be applied to 2D tensors, got tensor with {} dimensions", + input.dim() + ), + } +} + +/// Matrix transpose. Returns a tensor with the last two dimensions transposed. +/// For tensors with fewer than 2 dimensions, returns the tensor unchanged. +#[tensor_op(variants = [function, method, method_inplace])] +pub fn mT(input: OpTensor) -> Result { + match input.dim() { + 0 | 1 => Ok(input), + _ => { + let dim = input.dim(); + transpose_kernel(input, dim - 2, dim - 1) + } + } +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn cache(input: OpTensor, source: OpTensor, dim: D, offset: usize) -> Result { + let dim = dim.to_index(input.shape(), "cache")?; + let device = input.device().clone(); + let cache = Cache::new(input, source, dim, offset); + let new_view = cache.compute_view()?; + OpTensor::lazy(LazyOp::Cache(cache), new_view, device, false) +} + +/// Returns a new tensor duplicating data from the original tensor. New dimensions are inserted +/// on the left. +#[tensor_op(variants = [function, method])] +pub fn broadcast_left>(input: OpTensor, left_shape: S) -> Result { + let mut dims = left_shape.into().to_vec(); + dims.extend(input.shape().to_vec()); + broadcast_to_kernel(input, Shape::from(dims)) +} + +#[tensor_op(variants = [function, method])] +pub fn broadcast_to>(input: OpTensor, shape: S) -> Result { + let device = input.device().clone(); + let broadcast = Broadcast::new(input, shape.into()); + let new_view = broadcast.compute_view()?; + + let op = LazyOp::Reindex(Reindex::Broadcast(broadcast)); + OpTensor::lazy(op, new_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn index_select(input: OpTensor, indices: OpTensor, dim: D) -> Result { + let dim = dim.to_index(input.shape(), "index_select")?; + let device = input.device().clone(); + let index_select = IndexSelect::new(input, indices, dim); + let new_view = index_select.compute_view()?; + OpTensor::lazy(LazyOp::Select(index_select), new_view, device, false) +} + +// TODO(vinhowe): Make this API more like PyTorch's +#[tensor_op(variants = [function, method, method_inplace])] +pub fn index_write(input: OpTensor, src: OpTensor, write_start: D) -> Result { + let write_start = write_start.to_indexes(input.shape(), "index_write")?; + let device = input.device().clone(); + let index_write = IndexWrite::new(input, src, write_start); + let new_view = index_write.compute_view()?; + let op = LazyOp::IndexWrite(index_write); + OpTensor::lazy(op, new_view, device, false) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn where_cond>( + input: OpTensor, + condition: OpTensor, + on_false: T, +) -> Result { + let device = condition.device().clone(); + let (input, condition, on_false) = crate::promoted_cast_ternary(input, condition, on_false)?; + let where_cond = WhereCond::new(condition, TensorTypeOrScalarEnum::Tensor(input), on_false); + let new_view = where_cond.compute_view()?; + OpTensor::lazy(LazyOp::WhereCond(where_cond), new_view, device, false) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn clamp, T2: TensorTypeOrScalar>( + input: OpTensor, + min: Option, + max: Option, +) -> Result { + if min.is_none() && max.is_none() { + return Ok(input); + } + + let min_val = min + .as_ref() + .map(|t| t.map_tensor(|t| t.into())) + .transpose()? + .unwrap_or(TensorTypeOrScalarEnum::Scalar(0.0)); + let max_val = max + .as_ref() + .map(|t| t.map_tensor(|t| t.into())) + .transpose()? + .unwrap_or(TensorTypeOrScalarEnum::Scalar(1.0)); + let (mut input, min_cast, max_cast) = crate::promoted_cast_ternary(input, min_val, max_val)?; + if min.is_some() { + input = where_cond_kernel(input.clone(), ge_kernel(input, min_cast.clone())?, min_cast)?; + } + if max.is_some() { + input = where_cond_kernel(input.clone(), le_kernel(input, max_cast.clone())?, max_cast)?; + } + Ok(input) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn scatter_add( + input: OpTensor, + indices: OpTensor, + source: OpTensor, + dim: D, +) -> Result { + let dim = dim.to_index(input.shape(), "scatter_add")?; + let source_dims = source.shape().to_vec(); + let self_dims = input.shape().to_vec(); + let mismatch = if source_dims.len() != self_dims.len() { + true + } else { + let mut mismatch = false; + for (i, (&d1, &d2)) in self_dims.iter().zip(source_dims.iter()).enumerate() { + if i != dim && d1 != d2 { + mismatch = true; + break; + } + } + mismatch + }; + if mismatch { + Err(InvariantError::ShapeMismatchBinaryOp { + op: "scatter-add (self, src)", + lhs: input.shape().clone(), + rhs: source.shape().clone(), + })? + } + if indices.shape() != source.shape() { + Err(InvariantError::ShapeMismatchBinaryOp { + op: "scatter-add (indexes, src)", + lhs: indices.shape().clone(), + rhs: source.shape().clone(), + })? + } + let device = input.device().clone(); + let scatter_add = ScatterAdd::new(input, source, indices, dim); + let new_view = scatter_add.compute_view()?; + OpTensor::lazy(LazyOp::ScatterAdd(scatter_add), new_view, device, false) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn index_add( + input: OpTensor, + indices: OpTensor, + source: OpTensor, + dim: D, +) -> Result { + let dim = dim.to_index(input.shape(), "index_add")?; + let source_dims = source.shape().to_vec(); + let self_dims = input.shape().to_vec(); + let mismatch = if source_dims.len() != self_dims.len() { + true + } else { + let mut mismatch = false; + for (i, (&d1, &d2)) in self_dims.iter().zip(source_dims.iter()).enumerate() { + if i != dim && d1 != d2 { + mismatch = true; + break; + } + } + mismatch + }; + if mismatch { + Err(InvariantError::ShapeMismatchBinaryOp { + op: "index_add", + lhs: input.shape().clone(), + rhs: source.shape().clone(), + })? + } + if indices.dim() != 1 { + Err(InvariantError::RankMismatch { + accepted: 1..=1, + actual: indices.dim(), + })? + } + let indices_len = indices.shape()[0]; + if source_dims[dim] != indices_len { + Err(InvariantError::ShapeMismatchBinaryOp { + op: "index_add", + lhs: indices.shape().clone(), + rhs: source.shape().clone(), + })? + } + let device = input.device().clone(); + let index_add = IndexAdd::new(input, source, indices, dim); + let new_view = index_add.compute_view()?; + OpTensor::lazy(LazyOp::IndexAdd(index_add), new_view, device, false) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn gather(input: OpTensor, dim: D, index: OpTensor) -> Result { + let dim = dim.to_index(input.shape(), "gather")?; + let self_dims = input.shape().to_vec(); + let indices_dims = index.shape().to_vec(); + let mismatch = if indices_dims.len() != self_dims.len() { + true + } else { + let mut mismatch = false; + for (i, (&d1, &d2)) in self_dims.iter().zip(indices_dims.iter()).enumerate() { + if i != dim && d1 != d2 { + mismatch = true; + break; + } + } + mismatch + }; + if mismatch { + Err(InvariantError::ShapeMismatchBinaryOp { + op: "gather", + lhs: input.shape().clone(), + rhs: index.shape().clone(), + })? + } + let device = input.device().clone(); + let gather = Gather::new(input, index, dim); + let new_view = gather.compute_view()?; + OpTensor::lazy(LazyOp::Gather(gather), new_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn multinomial(input: OpTensor, num_samples: usize, replacement: bool) -> Result { + let shape = input.shape(); + if !(shape.dim() == 1 || shape.dim() == 2) { + anyhow::bail!("multinomial: input must be 1D [V] or 2D [B, V]"); + } + let device = input.device().clone(); + let rng = device.get_rng(); + let seed = rng.write().next_u32(); + let op = Multinomial::new(input, num_samples, replacement, Some(seed)); + let new_view = op.compute_view()?; + OpTensor::lazy(LazyOp::Multinomial(op), new_view, device, false) +} + +#[tensor_op(variants = [function, method])] +pub fn topk( + input: OpTensor, + k: usize, + dim: Option, + largest: Option, + sorted: Option, +) -> Result> { + let dim_idx = match dim { + Some(d) => d.to_index(input.shape(), "topk")?, + None => input.dim().saturating_sub(1), + }; + let largest = largest.unwrap_or(true); + let sorted = sorted.unwrap_or(false); + + let device = input.device().clone(); + let op = TopK::new(input.clone(), k, dim_idx, largest, sorted); + let indices_view = op.compute_view()?; + let indices = OpTensor::lazy(LazyOp::TopK(op), indices_view, device, false)?; + + let values = gather_kernel(input, dim_idx, indices.clone())?; + Ok(rvec![values, indices]) +} + +// +// TensorOptions for factory functions +// + +/// Options for tensor creation using the factory pattern. +/// +/// # Examples +/// +/// Creating tensors with different configurations using the builder pattern: +/// +/// ```rust +/// // Basic usage with defaults +/// let tensor = zeros([2, 3], TensorOptions::default())?; +/// +/// // Using the factory pattern to set specific options +/// let tensor = zeros([2, 3], TensorOptions::new() +/// .device(Device::GPU(gpu_device)) +/// .dtype(DType::F16) +/// .requires_grad(true) +/// .build())?; +/// +/// // Chaining methods in different orders +/// let tensor = ones([4, 4], TensorOptions::new() +/// .dtype(DType::I32) +/// .device(Device::CPU))?; +/// +/// // Using only some options +/// let tensor = full([3, 3], 5.0, TensorOptions::new() +/// .requires_grad(true))?; +/// ``` +#[derive(Debug, Clone)] +pub struct TensorOptions { + pub device: Option, + pub dtype: Option, + pub requires_grad: Option, +} + +impl TensorOptions { + /// Create a new TensorOptions builder with default values + pub fn new() -> Self { + Self { + device: None, + dtype: None, + requires_grad: None, + } + } + + pub fn from_tensor(tensor: &Tensor) -> Self { + Self { + device: Some(tensor.device()), + dtype: Some(tensor.dtype()), + requires_grad: Some(tensor.requires_grad()), + } + } + + /// Set the device for tensor creation + pub fn device(mut self, device: Device) -> Self { + self.device = Some(device); + self + } + + /// Set the dtype for tensor creation + pub fn dtype(mut self, dtype: DType) -> Self { + self.dtype = Some(dtype); + self + } + + /// Set whether gradients are required for tensor creation + pub fn requires_grad(mut self, requires_grad: bool) -> Self { + self.requires_grad = Some(requires_grad); + self + } + + pub fn device_or_default(&self) -> Device { + self.device.clone().unwrap_or(Device::CPU) + } + + pub fn dtype_or_default(&self) -> DType { + self.dtype.unwrap_or(DType::F32) + } + + pub fn dtype_or(&self, dtype: DType) -> DType { + self.dtype.unwrap_or(dtype) + } + + pub fn requires_grad_or_default(&self) -> bool { + self.requires_grad.unwrap_or(false) + } +} + +impl Default for TensorOptions { + fn default() -> Self { + Self::new() + } +} + +// +// Dispatch macros to adapt potentially user-specified dtypes in factory methods +// + +/// Dispatch to floating point types +macro_rules! dispatch_floating_types { + ($dtype:expr, $func:ident $(, $args:expr)*) => {{ + match $dtype { + DType::F32 => $func::($($args,)*), + DType::F16 => $func::($($args,)*), + DType::BF16 => $func::($($args,)*), + _ => anyhow::bail!("dtype {:?} not supported for floating point operations", $dtype), + } + }}; +} + +/// Dispatch to integer types +macro_rules! dispatch_int_types { + ($dtype:expr, $func:ident $(, $args:expr)*) => {{ + match $dtype { + DType::I32 => $func::($($args,)*), + DType::U32 => $func::($($args,)*), + _ => anyhow::bail!("dtype {:?} not supported for integer operations", $dtype), + } + }}; +} + +/// Dispatch to all numeric types +macro_rules! dispatch_all_types { + ($dtype:expr, $func:ident $(, $args:expr)*) => {{ + match $dtype { + DType::F32 => $func::($($args,)*), + DType::F16 => $func::($($args,)*), + DType::BF16 => $func::($($args,)*), + DType::I32 => $func::($($args,)*), + DType::U32 => $func::($($args,)*), + _ => anyhow::bail!("dtype {:?} not supported", $dtype), + } + }}; +} + +fn arange_impl>( + start: T, + end: T, + step: T, + shape_len: usize, + options: TensorOptions, +) -> Result { + let device = options.device_or_default(); + if device.is_cpu() { + let mut data = Vec::with_capacity(shape_len); + let mut current = start; + if step >= T::zero() { + while current < end { + data.push(current); + current = current + step; + } + } else { + while current > end { + data.push(current); + current = current + step; + } + } + let len = data.len(); + OpTensor::from_data(data, len, options) + } else { + let arange = Arange::new(start.as_(), end.as_(), step.as_()); + let numel = arange.numel(); + let op = LazyOp::Arange(arange); + + let meta = StorageView { + shape: numel.into(), + dtype: T::dtype(), + stride: Stride::from(&Shape::from(numel)), + }; + + OpTensor::lazy(op, meta, device.clone(), options.requires_grad_or_default()) + } +} + +fn arange_from_f32_impl>( + start_f32: f32, + end_f32: f32, + step_f32: f32, + shape_len: usize, + options: TensorOptions, +) -> Result { + let start: T = + NumCast::from(start_f32).ok_or_else(|| anyhow::anyhow!("Failed to cast start value"))?; + let end: T = + NumCast::from(end_f32).ok_or_else(|| anyhow::anyhow!("Failed to cast end value"))?; + let step: T = + NumCast::from(step_f32).ok_or_else(|| anyhow::anyhow!("Failed to cast step value"))?; + arange_impl::(start, end, step, shape_len, options) +} + +/// Creates a new 1D tensor with values from the interval `[start, end)` taken with a common +/// difference `step` from `start`. +#[tensor_op(variants = [function])] +pub fn arange( + start: Option, + end: f32, + step: Option, + options: TensorOptions, +) -> Result { + let dtype = options.dtype_or_default(); + + let start = start.unwrap_or(0.0); + let step = step.unwrap_or(1.0); + + if step == 0.0 { + anyhow::bail!("step cannot be zero") + } + + // Calculate length for pre-allocation + let len = if step > 0.0 { + ((end - start) / step).ceil() as usize + } else { + ((start - end) / (-step)).ceil() as usize + }; + let len = len.max(0); + + dispatch_all_types!(dtype, arange_from_f32_impl, start, end, step, len, options) +} + +#[cfg(feature = "rand")] +/// Private implementation for randint +fn randint_impl( + low: T, + high: T, + shape: &Shape, + device: &Device, + requires_grad: bool, +) -> Result { + let rng = device.get_rng(); + let data = (0..shape.numel()) + .map(|_| { + let sample: T = rng.write().gen_range(low..high); + sample + }) + .collect::>(); + + let stride = Stride::from(shape); + let meta = StorageView::new(shape.clone(), T::dtype(), stride); + let storage = Storage::from_slice(&data, shape, device); + + OpTensor::new( + LazyOp::Const, + meta, + Some(storage), + device.clone(), + requires_grad, + ) +} + +#[cfg(feature = "rand")] +fn randint_from_i32_impl< + T: TensorDType + NumCast + rand_distr::uniform::SampleUniform + PartialOrd, +>( + low_i32: i32, + high_i32: i32, + shape: &Shape, + device: &Device, + requires_grad: bool, +) -> Result { + let low: T = + NumCast::from(low_i32).ok_or_else(|| anyhow::anyhow!("Failed to cast low value"))?; + let high: T = + NumCast::from(high_i32).ok_or_else(|| anyhow::anyhow!("Failed to cast high value"))?; + randint_impl::(low, high, shape, device, requires_grad) +} + +#[cfg(feature = "rand")] +#[tensor_op(variants = [function])] +pub fn randint>( + low: i32, + high: i32, + shape: S, + options: TensorOptions, +) -> Result { + let shape = shape.into(); + let device = options.device_or_default(); + let dtype = options.dtype_or(DType::I32); + + dispatch_int_types!( + dtype, + randint_from_i32_impl, + low, + high, + &shape, + &device, + options.requires_grad_or_default() + ) +} + +#[cfg(feature = "rand")] +/// Private implementation for randn +fn randn_impl>( + shape: &Shape, + mean: T, + std: T, + device: &Device, + requires_grad: bool, +) -> Result { + let rng = device.get_rng(); + + if device.is_cpu() { + let distr = Normal::new(mean.to_f64().unwrap(), std.to_f64().unwrap()).unwrap(); + let data = (0..shape.numel()) + .map(|_| { + let sample: f64 = distr.sample(&mut *rng.write()); + T::from(sample as f32).expect("Failed to convert sample") + }) + .collect::>(); + let stride = Stride::from(shape); + let meta = StorageView::new(shape.clone(), T::dtype(), stride); + let storage = Storage::from_slice(&data, shape, device); + OpTensor::new( + LazyOp::Const, + meta, + Some(storage), + device.clone(), + requires_grad, + ) + } else { + let meta = StorageView { + shape: shape.clone(), + dtype: T::dtype(), + stride: Stride::from(shape), + }; + OpTensor::new( + LazyOp::FillPointwise(FillPointwise { + shape: shape.clone(), + kind: FillPointwiseKind::Randn { + mean: mean.as_(), + std: std.as_(), + seed: Some(rng.write().next_u32()), + }, + }), + meta, + None, + device.clone(), + requires_grad, + ) + } +} + +#[cfg(feature = "rand")] +fn randn_from_f32_impl>( + shape: &Shape, + mean_f32: f32, + std_f32: f32, + device: &Device, + requires_grad: bool, +) -> Result { + let mean: T = + NumCast::from(mean_f32).ok_or_else(|| anyhow::anyhow!("Failed to cast mean value"))?; + let std: T = + NumCast::from(std_f32).ok_or_else(|| anyhow::anyhow!("Failed to cast std value"))?; + randn_impl::(shape, mean, std, device, requires_grad) +} + +#[cfg(feature = "rand")] +#[tensor_op(variants = [function])] +pub fn randn>( + shape: S, + mean: Option, + std: Option, + options: TensorOptions, +) -> Result { + let shape = shape.into(); + let device = options.device_or_default(); + let dtype = options.dtype_or_default(); + + let mean = mean.unwrap_or(0.0); + let std = std.unwrap_or(1.0); + + dispatch_floating_types!( + dtype, + randn_from_f32_impl, + &shape, + mean, + std, + &device, + options.requires_grad_or_default() + ) +} + +#[cfg(feature = "rand")] +/// Private implementation for rand +fn rand_impl>( + shape: &Shape, + lo: T, + up: T, + device: &Device, + requires_grad: bool, +) -> Result { + let rng = device.get_rng(); + + if device.is_cpu() { + let distr = Uniform::new(lo.as_(), up.as_()); + let data = (0..shape.numel()) + .map(|_| { + let sample: f32 = distr.sample(&mut *rng.write()); + T::from(sample).expect("Failed to convert sample") + }) + .collect::>(); + + let stride = Stride::from(shape); + let meta = StorageView::new(shape.clone(), T::dtype(), stride); + let storage = Storage::from_slice(&data, shape, device); + + OpTensor::new( + LazyOp::Const, + meta, + Some(storage), + device.clone(), + requires_grad, + ) + } else { + let meta = StorageView { + shape: shape.clone(), + dtype: T::dtype(), + stride: Stride::from(shape), + }; + OpTensor::new( + LazyOp::FillPointwise(FillPointwise { + shape: shape.clone(), + kind: FillPointwiseKind::Rand { + lo: lo.as_(), + up: up.as_(), + seed: Some(rng.write().next_u32()), + }, + }), + meta, + None, + device.clone(), + requires_grad, + ) + } +} + +#[cfg(feature = "rand")] +fn rand_from_f32_impl>( + shape: &Shape, + lo_f32: f32, + up_f32: f32, + device: &Device, + requires_grad: bool, +) -> Result { + let lo: T = NumCast::from(lo_f32).ok_or_else(|| anyhow::anyhow!("Failed to cast lo value"))?; + let up: T = NumCast::from(up_f32).ok_or_else(|| anyhow::anyhow!("Failed to cast up value"))?; + rand_impl::(shape, lo, up, device, requires_grad) +} + +#[cfg(feature = "rand")] +#[tensor_op(variants = [function])] +pub fn rand>( + shape: S, + lo: Option, + up: Option, + options: TensorOptions, +) -> Result { + let shape = shape.into(); + let device = options.device_or_default(); + let dtype = options.dtype_or_default(); + + let lo = lo.unwrap_or(0.0); + let up = up.unwrap_or(1.0); + + dispatch_floating_types!( + dtype, + rand_from_f32_impl, + &shape, + lo, + up, + &device, + options.requires_grad_or_default() + ) +} + +#[derive(Debug, Clone)] +pub enum NormalOrUniform { + Normal, + Uniform, +} + +#[derive(Debug, Clone)] +pub enum KaimingFan { + FanIn, + FanOut, +} + +#[derive(Debug, Clone)] +pub enum KaimingNonLinearity { + ReLU, + Linear, + Sigmoid, + Tanh, + SELU, + LeakyReLU, + ExplicitGain(f32), +} + +pub fn calculate_fan_in_out(shape: &Shape) -> (usize, usize) { + let dims = shape.to_vec(); + let receptive_field_size: usize = dims.iter().skip(2).product(); + + let fan_in = if dims.len() < 2 { + 1 + } else { + dims[1] * receptive_field_size + }; + + let fan_out = if dims.is_empty() { + 1 + } else { + dims[0] * receptive_field_size + }; + + (fan_in, fan_out) +} + +pub fn xavier_impl>( + shape: S, + dist: NormalOrUniform, + gain: Option, + options: TensorOptions, +) -> Result { + let shape = shape.into(); + let dtype = options.dtype_or_default(); + let gain = gain.unwrap_or(1.0); + + if !dtype.is_float() { + return Err(anyhow::anyhow!( + "xavier_init: dtype {:?} is not floating point", + dtype + )); + } + + let (fan_in, fan_out) = calculate_fan_in_out(&shape); + let denom = (fan_in + fan_out) as f32; + + match dist { + NormalOrUniform::Uniform => { + let bound = (6.0f32).sqrt() * gain / denom.sqrt(); + rand_kernel(shape, Some(-bound), Some(bound), options) + } + NormalOrUniform::Normal => { + let std = (2.0f32 / denom).sqrt() * gain; + randn_kernel(shape, Some(0.0), Some(std), options) + } + } +} + +#[tensor_op(variants = [function])] +pub fn xavier_uniform>( + shape: S, + gain: Option, + options: TensorOptions, +) -> Result { + xavier_impl(shape, NormalOrUniform::Uniform, gain, options) +} + +#[tensor_op(variants = [function])] +pub fn xavier_normal>( + shape: S, + gain: Option, + options: TensorOptions, +) -> Result { + xavier_impl(shape, NormalOrUniform::Normal, gain, options) +} + +pub fn kaiming_impl>( + shape: S, + dist: NormalOrUniform, + fan: KaimingFan, + non_linearity: KaimingNonLinearity, + a: Option, + options: TensorOptions, +) -> Result { + let shape = shape.into(); + let dtype = options.dtype_or_default(); + + if !dtype.is_float() { + return Err(anyhow::anyhow!( + "kaiming_init: dtype {:?} is not floating point", + dtype + )); + } + + // Compute params on CPU and fall back to rand/randn + let (fan_in, fan_out) = calculate_fan_in_out(&shape); + let fan_val = match fan { + KaimingFan::FanIn => fan_in as f32, + KaimingFan::FanOut => fan_out as f32, + }; + let gain = match non_linearity { + KaimingNonLinearity::ReLU => 2f32.sqrt(), + KaimingNonLinearity::Tanh => 5.0 / 3.0, + KaimingNonLinearity::Linear | KaimingNonLinearity::Sigmoid => 1.0, + KaimingNonLinearity::SELU => 0.75, + KaimingNonLinearity::LeakyReLU => (2. / (1. + a.unwrap_or(0.01).powi(2))).sqrt(), + KaimingNonLinearity::ExplicitGain(g) => g, + }; + let std = gain / fan_val.sqrt(); + + match dist { + NormalOrUniform::Uniform => { + let bound = std * 3f32.sqrt(); + rand_kernel(shape, Some(-bound), Some(bound), options) + } + NormalOrUniform::Normal => randn_kernel(shape, Some(0.0), Some(std), options), + } +} + +#[tensor_op(variants = [function])] +pub fn kaiming_uniform>( + shape: S, + a: Option, + mode: KaimingFan, + nonlinearity: KaimingNonLinearity, + options: TensorOptions, +) -> Result { + kaiming_impl( + shape, + NormalOrUniform::Uniform, + mode, + nonlinearity, + a, + options, + ) +} + +#[tensor_op(variants = [function])] +pub fn kaiming_normal>( + shape: S, + a: Option, + mode: KaimingFan, + nonlinearity: KaimingNonLinearity, + options: TensorOptions, +) -> Result { + kaiming_impl( + shape, + NormalOrUniform::Normal, + mode, + nonlinearity, + a, + options, + ) +} + +#[tensor_op(variants = [function])] +pub fn orthogonal>( + shape: S, + gain: Option, + options: TensorOptions, +) -> Result { + const EPS: f32 = 1e-6; + const STEPS: usize = 6; + let gain = gain.unwrap_or(1.0); + + let shape = shape.into(); + let dtype = options.dtype_or_default(); + + if !dtype.is_float() { + return Err(anyhow::anyhow!( + "orthogonal: dtype {:?} is not floating point", + dtype + )); + } + + if shape.dim() != 2 { + anyhow::bail!( + "orthogonal: expected 2D shape, got {}D: {:?}", + shape.dim(), + shape + ); + } + + let m = shape[0]; + let n = shape[1]; + + // A ~ N(0, 1) + let a = randn_kernel(shape.clone(), Some(0.0), Some(1.0), options.clone())?; + + // Build symmetric Gram matrix G and set identity size + let (g, d): (OpTensor, usize) = if m >= n { + // G = A^T A (n x n) + let g_raw = matmul_kernel(a.clone(), a.clone(), true, false)?; + let g_sym = mul_kernel::<_, _, OpTensor>( + add_kernel(g_raw.clone(), t_kernel(g_raw.clone())?)?, + 0.5, + )?; + (g_sym, n) + } else { + // G = A A^T (m x m) + let g_raw = matmul_kernel(a.clone(), a.clone(), false, true)?; + let g_sym = mul_kernel::<_, _, OpTensor>( + add_kernel(g_raw.clone(), t_kernel(g_raw.clone())?)?, + 0.5, + )?; + (g_sym, m) + }; + + // mu = ||G||_F + eps + let mu = add_kernel::<_, _, OpTensor>( + norm_kernel(g.clone(), Some(NormOrd::Frobenius), crate::AllDims, false)?, + EPS, + )?; + + let i = eye_kernel(d, None, options)?; + + // X0 = I / sqrt(mu) + let inv_sqrt_mu = recip_kernel(sqrt_kernel(mu.clone())?)?; + let mut x = mul_kernel(i.clone(), inv_sqrt_mu)?; + + // Newton–Schulz iteration (6 iters) + // X <- 0.5 * X @ (3I - G @ (X @ X)) + let three_i = mul_kernel::<_, _, OpTensor>(i.clone(), 3.0)?; + for _ in 0..STEPS { + let x2 = matmul_kernel(x.clone(), x.clone(), false, false)?; + let gx2 = matmul_kernel(g.clone(), x2, false, false)?; + let inner = sub_kernel(three_i.clone(), gx2)?; + let x_inner = matmul_kernel(x.clone(), inner, false, false)?; + x = mul_kernel::<_, _, OpTensor>(x_inner, 0.5)?; + } + + // Orthonormalize columns (m >= n) or rows (m < n) + let q = if m >= n { + matmul_kernel(a.clone(), x.clone(), false, false)? + } else { + matmul_kernel(x.clone(), a.clone(), false, false)? + }; + + mul_kernel::<_, _, OpTensor>(q, gain) +} + +#[tensor_op(variants = [function])] +pub fn eye(n: usize, m: Option, options: TensorOptions) -> Result { + let m = m.unwrap_or(n); + let shape = Shape::from(vec![n, m]); + let device = options.device_or_default(); + let dtype = options.dtype_or_default(); + + if device.is_cpu() { + match dtype { + DType::F32 => { + let mut data = vec![0f32; n * m]; + let d = n.min(m); + for i in 0..d { + data[i * m + i] = 1.0; + } + return OpTensor::from_data(data, shape, options); + } + DType::F16 => { + let mut data = vec![half::f16::from_f32(0.0); n * m]; + let d = n.min(m); + for i in 0..d { + data[i * m + i] = half::f16::from_f32(1.0); + } + return OpTensor::from_data(data, shape, options); + } + DType::I32 => { + let mut data = vec![0i32; n * m]; + let d = n.min(m); + for i in 0..d { + data[i * m + i] = 1; + } + return OpTensor::from_data(data, shape, options); + } + DType::U32 => { + let mut data = vec![0u32; n * m]; + let d = n.min(m); + for i in 0..d { + data[i * m + i] = 1u32; + } + return OpTensor::from_data(data, shape, options); + } + _ => anyhow::bail!("dtype {:?} not supported for eye", dtype), + } + } + + let meta = StorageView { + shape: shape.clone(), + dtype, + stride: Stride::from(&shape), + }; + OpTensor::new( + LazyOp::Eye(Eye { shape }), + meta, + None, + device, + options.requires_grad_or_default(), + ) +} + +#[tensor_op(variants = [function, method])] +pub fn one_hot(input: OpTensor, num_classes: usize) -> Result { + let device = input.device().clone(); + let op = crate::ops::OneHot::new(input, num_classes); + let view = op.compute_view()?; + OpTensor::lazy(LazyOp::OneHot(op), view, device, false) +} + +/// Private implementation for full +fn full_impl>( + shape: &Shape, + value: T, + device: &Device, + requires_grad: bool, +) -> Result { + let meta = StorageView { + shape: shape.clone(), + dtype: T::dtype(), + stride: Stride::from(shape), + }; + OpTensor::new( + LazyOp::FillPointwise(FillPointwise { + shape: shape.clone(), + kind: FillPointwiseKind::Constant { value: value.as_() }, + }), + meta, + None, + device.clone(), + requires_grad, + ) +} + +fn full_from_f32_impl>( + shape: &Shape, + value_f32: f32, + device: &Device, + requires_grad: bool, +) -> Result { + let value: T = + NumCast::from(value_f32).ok_or_else(|| anyhow::anyhow!("Failed to cast value"))?; + full_impl::(shape, value, device, requires_grad) +} + +#[tensor_op(variants = [function])] +pub fn full>(shape: S, value: f32, options: TensorOptions) -> Result { + let shape = shape.into(); + let device = options.device_or_default(); + let dtype = options.dtype_or_default(); + + dispatch_all_types!( + dtype, + full_from_f32_impl, + &shape, + value, + &device, + options.requires_grad_or_default() + ) +} + +/// Private implementation for zeros +fn zeros_impl>( + shape: &Shape, + device: &Device, + requires_grad: bool, +) -> Result { + if device.is_cpu() { + let storage = Storage::zeros::(shape, device); + let stride = Stride::from(shape); + let meta = StorageView::new(shape.clone(), T::dtype(), stride); + OpTensor::new( + LazyOp::Const, + meta, + Some(storage), + device.clone(), + requires_grad, + ) + } else { + full_impl::(shape, T::zero(), device, requires_grad) + } +} + +#[tensor_op(variants = [function])] +pub fn zeros>(shape: S, options: TensorOptions) -> Result { + let shape = shape.into(); + let device = options.device_or_default(); + let dtype = options.dtype_or_default(); + + dispatch_all_types!( + dtype, + zeros_impl, + &shape, + &device, + options.requires_grad_or_default() + ) +} + +#[tensor_op(variants = [function, method])] +pub fn zeros_like(input: OpTensor, options: TensorOptions) -> Result { + let dtype = options.dtype.unwrap_or_else(|| input.dtype()); + let device = options.device.as_ref().unwrap_or_else(|| input.device()); + + dispatch_all_types!( + dtype, + zeros_impl, + input.shape(), + device, + options.requires_grad_or_default() + ) +} + +fn ones_impl>( + shape: &Shape, + device: &Device, + requires_grad: bool, +) -> Result { + if device.is_cpu() { + let storage = Storage::ones::(shape, device); + let stride = Stride::from(shape); + let meta = StorageView::new(shape.clone(), T::dtype(), stride); + OpTensor::new( + LazyOp::Const, + meta, + Some(storage), + device.clone(), + requires_grad, + ) + } else { + full_impl::(shape, T::one(), device, requires_grad) + } +} + +#[tensor_op(variants = [function])] +pub fn ones>(shape: S, options: TensorOptions) -> Result { + let shape = shape.into(); + let device = options.device_or_default(); + let dtype = options.dtype_or_default(); + + dispatch_all_types!( + dtype, + ones_impl, + &shape, + &device, + options.requires_grad_or_default() + ) +} + +#[tensor_op(variants = [function, method])] +pub fn ones_like(input: OpTensor, options: TensorOptions) -> Result { + let dtype = options.dtype.unwrap_or_else(|| input.dtype()); + let device = options.device.as_ref().unwrap_or_else(|| input.device()); + + dispatch_all_types!( + dtype, + ones_impl, + input.shape(), + device, + options.requires_grad_or_default() + ) +} + +#[tensor_op(variants = [method_inplace])] +pub fn zero(input: OpTensor) -> Result { + mul_kernel::<_, _, OpTensor>(input, 0.) +} + +#[cfg(feature = "rand")] +#[tensor_op(variants = [function, method, method_inplace])] +pub fn bernoulli(input: OpTensor) -> Result { + let rng = input.device().get_rng(); + let seed = rng.write().next_u32(); + let shape = input.shape(); + let device = input.device().clone(); + + let meta = StorageView { + shape: shape.clone(), + dtype: DType::F32, + stride: Stride::from(shape), + }; + + OpTensor::new( + LazyOp::Bernoulli(Bernoulli::new(input, Some(seed))), + meta, + None, + device, + false, + ) +} + +// TODO(vinhowe): Add inplace +fn trilu_kernel>(input: T, upper: bool, k: Option) -> Result { + let input = input.into(); + let device = input.device().clone(); + let trilu = TriluOp::new(input, upper, k); + let new_view = trilu.compute_view()?; + OpTensor::lazy(LazyOp::Trilu(trilu), new_view, device, false) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn triu(input: OpTensor, k: Option) -> Result { + trilu_kernel(input, true, k) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn tril(input: OpTensor, k: Option) -> Result { + trilu_kernel(input, false, k) +} + +#[tensor_op(variants = [function, method, method_inplace])] +pub fn copy(src: OpTensor, dst: OpTensor) -> Result { + OpTensor::new( + LazyOp::Copy(TensorCopy { + src: src.clone(), + dst: dst.clone(), + }), + src.view.clone(), + None, + src.device.clone(), + false, + ) +} + +/// Returns a tensor that is in row major order. This is the same as the original tensor if it +/// was already contiguous, otherwise a copy is triggered. +#[tensor_op(variants = [function, method, method_inplace])] +pub fn contiguous(input: OpTensor) -> Result { + if input.is_contiguous() { + Ok(input.clone()) + } else { + let storage_guard = input.storage(); + let storage = storage_guard.as_ref().unwrap(); + let cloned_storage = storage.deep_clone(input.device()).unwrap(); + OpTensor::new( + LazyOp::Const, + input.view.clone(), + Some(cloned_storage), + input.device.clone(), + false, + ) + } +} + +impl OpTensor { + /// Returns true if the data is stored in a C contiguous (aka row major) way. + pub fn is_contiguous(&self) -> bool { + self.view.is_contiguous() + } + + pub fn has_nan(&self) -> bool { + assert!(self.device().is_cpu()); + let self_nd = self.to_ndarray_view::(); + self_nd.iter().any(|&x| !x.is_finite()) + } + + /// Creates a new tensor from a chunk of data. + /// + /// The Tensor is instantly resolved. + /// If a non-CPU device is specified, the data will be copied to the device. + pub fn from_data, S: Into>( + data: U, + shape: S, + options: TensorOptions, + ) -> Result { + let shape = shape.into(); + let device = options.device_or_default(); + let requires_grad = options.requires_grad_or_default(); + let storage = Storage::from_slice(data.as_ref(), &shape, &device); + let stride = Stride::from(&shape); + let meta = StorageView::new(shape, T::dtype(), stride); + Self::new(LazyOp::Const, meta, Some(storage), device, requires_grad) + } + + pub fn from_bytes>( + data: &[u8], + shape: S, + options: TensorOptions, + ) -> Result { + let shape = shape.into(); + let device = options.device_or_default(); + let requires_grad = options.requires_grad_or_default(); + let dtype = options.dtype_or_default(); + let storage = Storage::from_bytes(data, dtype.size_of(), &device); + let stride = Stride::from(&shape); + let meta = StorageView::new(shape, dtype, stride); + Self::new(LazyOp::Const, meta, Some(storage), device, requires_grad) + } + + /// Create a parameter based on the values currently stored in a tensor. The storage is always + /// copied. + pub fn requires_grad_(&self, requires_grad: bool) -> Result { + if self.requires_grad == requires_grad { + Ok(self.clone()) + } else { + let device = self.device.clone(); + let storage = Arc::clone(&self.storage); + Self::shallow( + self.op().clone(), + self.view.clone(), + storage, + device, + requires_grad, + ) + } + } + + pub fn retain_grad(&self) -> Result<()> { + if self.op.is_leaf() { + if !self.requires_grad { + return Err(anyhow::anyhow!( + "can't retain_grad on Tensor that has requires_grad=false" + )); + } + // No-op either way on constant tensor, but if requires_grad is true, we'll + // automatically retain the grad anyway + return Ok(()); + } + *self.inner.retains_grad.write() = true; + Ok(()) + } + + pub fn is_leaf(&self) -> bool { + self.op.is_leaf() + } + + /// Returns a new tensor detached from the current graph, gradient are not propagated through + /// this new node. The storage of this tensor is shared with the initial tensor. + /// + /// If the tensor is already detached from the computation graph, the same tensor is returned. + pub fn detach(&self) -> Result { + match self.op { + LazyOp::Const if !self.requires_grad => Ok(self.clone()), + _ => { + let storage_guard = self.storage(); + let storage = storage_guard.as_ref().cloned(); + Self::new( + LazyOp::Detach(Box::new(self.op().clone())), + self.view.clone(), + storage, + self.device.clone(), + false, + ) + } + } + } + + pub(crate) fn same_storage(&self, rhs: &Self) -> bool { + match (self.storage().as_ref(), rhs.storage().as_ref()) { + (Some(lhs), Some(rhs)) => std::ptr::eq(lhs, rhs), + _ => false, + } + } + + /// # Safety + /// + /// If the tensor has more than 1 reference, you die. + /// If the tensor has no storage, you die. + pub fn into_bytes(self) -> anyhow::Result> { + let mut inner = Arc::try_unwrap(self.inner).map_err(|_| { + anyhow::anyhow!("Cannot convert tensor into bytes with multiple references.") + })?; + let storage = unsafe { ManuallyDrop::take(&mut inner.storage) }; + let storage = Arc::try_unwrap(storage).unwrap().into_inner().unwrap(); + Ok(storage.into_bytes()) + } + + pub fn from_quantized, S: Into>( + data: U, + dtype: DType, + shape: S, + device: Device, + ) -> Self { + let shape = shape.into(); + let storage = unsafe { Storage::from_quantized(data.as_ref(), &device) }; + let stride = Stride::from(&shape); + let meta = StorageView::new(shape, dtype, stride); + Self::new(LazyOp::Const, meta, Some(storage), device, false) + .expect("Shouldn't be an error because requires_grad is false") + } + + pub fn from_disk>( + reader: &mut R, + shape: S, + device: Device, + ) -> Result { + let shape = shape.into(); + let storage = Storage::from_disk::(reader, &shape, &device)?; + let stride = Stride::from(&shape); + let meta = StorageView::new(shape, T::dtype(), stride); + Self::new(LazyOp::Const, meta, Some(storage), device, false) + } + + #[maybe_async] + pub async fn item(&self) -> T { + assert!(self.is_scalar()); + ensure_resolved!(self); + let storage_guard = self.storage(); + let buffer = storage_guard.as_ref().unwrap().try_cpu().unwrap(); + buffer.to_slice::(self.shape())[0] + } + + /// # Bindings + /// + /// Only applicable to GPU tensors. + /// Generates the bind group entries required to bind the tensor to a kernel. + /// Quantized tensors may use multiple bind groups. + /// Unquantized tensors should only use a single bind group. + pub(crate) fn bind_group_entries(&self) -> RVec { + assert!(self.device().is_gpu()); + let storage_guard = self.storage(); + let storage = storage_guard + .as_ref() + .unwrap_or_else(|| panic!("Storage missing for {:?}", self.id())); + let gpu_buf = storage.try_gpu().unwrap(); + let handle = gpu_buf.inner().handle; + self.segments() + .iter() + .fold(rvec![], |mut entries, segment| { + let (offset, size) = (segment.offset, segment.size); + entries.push(BindGroupEntry { + handle, + offset, + size: Some(size), + }); + entries + }) + } + + /// # Segments + /// + /// In Piston, a tensor may be split into multiple segments. + /// This is due to our quantization scheme allowing multiple quantized components to be packed + /// and stored in a single tensor. + pub(crate) fn segments(&self) -> RVec { + self.dtype().segments(self.shape().numel()) + } + + /// Converts the tensor into a 1D vector. + /// + /// The 1D vector contains the data from the tensor, as it was laid out in memory. + #[maybe_async] + pub async fn to_vec(&self) -> anyhow::Result> { + ensure_resolved!(self); + let storage_guard = self.storage(); + let buffer = storage_guard.as_ref().unwrap().try_cpu()?; + let slice = buffer.to_slice::(self.shape()); + Ok(slice.to_vec()) + } + + pub(crate) fn execution_order(&self) -> Vec<&Self> { + let mut done = BitVec::::repeat(false, self.id().0 + 1); + let mut pending = BitVec::::repeat(false, self.id().0 + 1); + let mut order = Vec::new(); + + let mut stack: Vec<(&OpTensor, usize)> = vec![(self, 0)]; + while let Some((cur_t, cur_src)) = stack.pop() { + let all_deps_done = cur_src == cur_t.op().srcs().len(); + + if all_deps_done { + done.set(cur_t.id().0, true); + pending.set(cur_t.id().0, false); + order.push(cur_t); + continue; + } + + let (srcs_with_deps, srcs_without_deps): (Vec<_>, Vec<_>) = cur_t + .op() + .srcs() + .iter() + .partition(|s| s.op().srcs().is_empty()); + + let all_srcs = srcs_with_deps + .into_iter() + .chain(srcs_without_deps) + .collect::>(); + + let precursor: &OpTensor = all_srcs[cur_src]; + let precursor_id = precursor.id().0; + + if done[precursor_id] { + stack.push((cur_t, cur_src + 1)); + } else if pending[precursor_id] { + panic!( + "Cycle detected whilst computing topological order: {precursor_id:?}. Try plotting with feature `plotting`." + ); + } else { + pending.set(precursor_id, true); + stack.push((cur_t, cur_src)); + stack.push((precursor, 0)); + } + } + + order + } + + #[maybe_async] + pub async fn cpu_apply(self, dst: Self) -> Option { + cpu::apply_operation(self.op().clone(), dst).await.ok() + } + + fn gpu_compile_key_for_op<'a>( + &'a self, + op: &'a LazyOp, + can_inplace: bool, + uniform: &mut CpuUniform, + ) -> Option> { + match op { + LazyOp::Binary(b) => b.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Ternary(t) => t.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Lerp(l) => l.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Cast(c) => c.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Matmul(m) => m.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Softmax(s) => s.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::RoPE(r) => r.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Alibi(a) => a.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Unary(u) => u.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Reindex(r) => r.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Concat(c) => c.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Norm(n) => n.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Affine(a) => a.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Cmp(c) => c.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Powf(p) => p.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::WhereCond(w) => w.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Conv(c) => c.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Select(i) => i.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::IndexWrite(i) => i.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::IndexAdd(i) => i.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::ScatterAdd(s) => s.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Trilu(t) => t.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Cache(c) => c.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Reduce(s) => s.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Detach(d) => self.gpu_compile_key_for_op(d, can_inplace, uniform), + LazyOp::Gather(g) => g.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::OneHot(o) => o.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Multinomial(m) => m.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::FillPointwise(f) => f.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Bernoulli(b) => b.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Arange(a) => a.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Eye(e) => e.create_gpu_compile_key(self, can_inplace, uniform).ok(), + LazyOp::Copy(_) | LazyOp::View(_) | LazyOp::Const => None, + LazyOp::TopK(t) => t.create_gpu_compile_key(self, can_inplace, uniform).ok(), + } + } + + pub(crate) fn gpu_compile_key( + &self, + can_inplace: bool, + uniform: &mut CpuUniform, + ) -> Option> { + match self.op() { + LazyOp::Copy(c) => Some(GpuCompileKey::Copy(c.create_gpu_compile_key())), + _ => self + .gpu_compile_key_for_op(self.op(), can_inplace, uniform) + .map(GpuCompileKey::Compute), + } + } + + pub(crate) fn compile_gpu<'a>( + &'a self, + gpu_compile_key: &GpuCompileKey<'a>, + gpu_device: &'a WgpuDevice, + debug: bool, + ) -> Option { + match gpu_compile_key { + GpuCompileKey::Copy(_) => { + if let LazyOp::Copy(c) = self.op() { + c.compile_gpu().ok() + } else { + None + } + } + GpuCompileKey::Compute(compute_key) => { + compile_gpu_for_op(self.op(), compute_key, gpu_device, debug).map(Compiled::Compute) + } + } + } + + #[maybe_async] + async fn resolve_cpu(self) -> Result { + let mut tensor = self.clone(); + let execution_order = self.execution_order(); + + for t in execution_order.into_iter() { + log::debug!("Running: {:?}", t.op().name()); + assert!(t.device().is_cpu()); + if t.resolved() { + continue; + } + tensor = tensor.cpu_apply(t.clone()).await.unwrap(); + } + + Ok(tensor) + } + + /// Applies the pending graph to the tensor. + #[maybe_async] + async fn apply_pending_graph(&self) -> Result { + if self.resolved() { + return Ok(self.clone()); + } + + match self.device() { + Device::GPU(gpu_device) => { + #[cfg(target_arch = "wasm32")] + { + Box::pin(gpu_device.sync_tensors_graph(vec![&self])) + .await + .map_err(|e| TensorError::LazyGraphExecutorError(Box::new(e)))?; + } + #[cfg(not(target_arch = "wasm32"))] + { + gpu_device + .sync_tensors_graph(vec![&self]) + .map_err(|e| TensorError::LazyGraphExecutorError(Box::new(e)))?; + } + Ok(self.clone()) + } + Device::CPU => { + #[cfg(target_arch = "wasm32")] + { + Box::pin(self.clone().resolve_cpu()).await?; + } + #[cfg(not(target_arch = "wasm32"))] + { + self.clone().resolve_cpu()?; + } + Ok(self.clone()) + } + } + } + + #[maybe_async] + async fn to_gpu(&self, dst_device: &Device) -> Result { + ensure_resolved!(self); + let storage_guard = self.storage(); + let cpu_buf = storage_guard + .as_ref() + .ok_or(TensorError::TransferError)? + .try_cpu()?; + let gpu_buf = cpu_buf.to_device(dst_device)?; + + let wgpu_device = dst_device.try_gpu()?; + Ok(Self::new( + LazyOp::Const, + self.view.clone(), + Some(Storage::GPU(gpu_buf)), + Device::GPU(wgpu_device.clone()), + false, + )?) + } + + #[maybe_async] + pub async fn deep_clone(&self) -> Result { + ensure_resolved!(self); + let storage_guard = self.storage(); + let storage = storage_guard.as_ref().unwrap(); + let cloned_storage = storage.deep_clone(self.device()).unwrap(); + Self::new( + LazyOp::Const, + self.view.clone(), + Some(cloned_storage), + self.device.clone(), + false, + ) + } + + #[maybe_async] + async fn to_cpu(&self) -> Result { + ensure_resolved!(self); + + if self.device().is_cpu() { + return Ok(self.clone()); + } + let storage_guard = self.storage().clone(); + let gpu_buf = storage_guard + .as_ref() + .ok_or(TensorError::TransferError)? + .try_gpu()?; + let cpu_buf = gpu_buf.to_cpu(&self.device).await?; + + Ok(Self::new( + LazyOp::Const, + self.view.clone(), + Some(Storage::CPU(cpu_buf)), + Device::CPU, + false, + )?) + } + + /// Transfers the tensor to the specified device. + /// + /// If the tensor is already on the specified device, it will be returned as-is, + /// and the underlying storage will not be copied. + /// If the tensor is on a different device, it will be copied to the specified device. + #[maybe_async] + pub async fn to(&self, device: &Device) -> Result { + match (self.device(), device) { + (Device::GPU(_), Device::CPU) => self.to_cpu().await, + (Device::CPU, Device::GPU(_)) => self.to_gpu(device).await, + _ => Ok(self.clone()), + } + } + + #[cfg(feature = "pyo3")] + pub fn to_py<'s, 'p: 's, T: TensorDType + numpy::Element>( + &'s self, + py: &'p pyo3::Python<'p>, + ) -> &'s PyArrayDyn { + use numpy::PyArray; + assert!( + self.device().is_cpu(), + "Cannot convert non-CPU tensor to numpy array" + ); + PyArray::from_owned_array(*py, self.deep_clone().unwrap().into_ndarray::()) + } + + #[cfg(not(feature = "debug"))] + pub fn debug_tensor(&self) -> Option { + self.debug_tensor.read().as_ref().cloned() + } + + #[cfg(not(feature = "debug"))] + pub fn get_or_create_debug_tensor(&self) -> Result { + if self.debug_tensor.read().is_some() { + return Ok(self.debug_tensor.read().as_ref().unwrap().clone()); + } + + let gpu_device = self.device().try_gpu()?; + let buffer = gpu_device.get_or_create_buffer( + &BufferDescriptor { + size: self.num_bytes() as _, + // If we want the values in CPU land, we'll eventually have to copy again to a + // buffer with a usage of COPY_DST | MAP_READ. + usage: wgpu::BufferUsages::standard(), + mapped_at_creation: false, + }, + false, + )?; + let tensor = Self::new( + LazyOp::Const, + self.view.clone(), + Some(Storage::GPU(GPUBuffer { + inner: buffer, + alignment: self.dtype().size_of(), + cpu_size: Some(self.num_bytes()), + })), + Device::GPU(gpu_device.clone()), + false, + )?; + *self.debug_tensor.write() = Some(tensor.clone()); + Ok(tensor) + } + + pub fn grad(&self) -> Option { + self.grad.read().as_ref().cloned() + } + + pub fn set_grad(&self, grad: Option) { + log::trace!( + "Setting grad for {:?}: {:?}", + self.id(), + grad.as_ref().map(|_| "Some").unwrap_or("None") + ); + *self.grad.write() = grad; + } + + pub fn take_grad(&self) -> Option { + log::trace!("Taking grad for {:?}", self.id()); + self.grad.write().take() + } +} + +#[derive(Debug, Clone, Default)] +pub enum NormOrd { + /// Frobenius norm (default for matrices, same as 2-norm) + #[default] + Frobenius, + /// Infinity norm (max of absolute values) + Inf, + /// Negative infinity norm (min of absolute values) + NegInf, + /// 0-norm (count non-zero elements, for vectors only) + Zero, + /// 1-norm (sum of absolute values) + One, + /// -1-norm (harmonic mean norm) + NegOne, + /// p-norm for arbitrary p (for vectors only) + P(f32), +} + +impl FromStr for NormOrd { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + match s { + "fro" => Ok(Self::Frobenius), + "inf" => Ok(Self::Inf), + "-inf" => Ok(Self::NegInf), + "0" => Ok(Self::Zero), + "1" => Ok(Self::One), + "-1" => Ok(Self::NegOne), + _ => Ok(Self::P(s.parse::()?)), + } + } +} + +pub fn compile_gpu_for_op( + op: &LazyOp, + gpu_compile_key: &ComputeCompileKey, + gpu_device: &WgpuDevice, + debug: bool, +) -> Option { + match op { + LazyOp::Binary(b) => b.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Ternary(t) => t.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Lerp(l) => l.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Cast(c) => c.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Matmul(m) => m.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Softmax(s) => s.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::RoPE(r) => r.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Alibi(a) => a.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Unary(u) => u.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Reindex(r) => r.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Concat(c) => c.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Norm(n) => n.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Affine(a) => a.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Cmp(c) => c.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Powf(p) => p.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::WhereCond(w) => w.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Conv(c) => c.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Select(i) => i.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::IndexWrite(i) => i.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::IndexAdd(i) => i.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::ScatterAdd(s) => s.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Trilu(t) => t.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Cache(c) => c.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Reduce(s) => s.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::OneHot(o) => o.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Detach(d) => compile_gpu_for_op(d, gpu_compile_key, gpu_device, debug), + LazyOp::Gather(g) => g.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Multinomial(m) => m.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::FillPointwise(f) => f.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Eye(e) => e.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Bernoulli(b) => b.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::Arange(a) => a.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::TopK(t) => t.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), + LazyOp::View(_) | LazyOp::Const => None, + LazyOp::Copy(_) => panic!("Copy should not have a gpu_compile_key"), + } +} + +#[cfg(feature = "pyo3")] +impl From<&PyArrayDyn> for OpTensor { + fn from(array: &PyArrayDyn) -> Self { + Self::from(array.to_owned_array()) + } +} + +#[cfg(feature = "testing")] +#[derive(Default)] +struct CloseStats { + total_error: T, + max_abs_error: T, + max_abs_error_idxs: Option>, + element_count: usize, + fail_count: usize, + atol: T, + rtol: T, +} + +#[cfg(feature = "testing")] +impl CloseStats { + fn new(atol: T, rtol: T) -> Self { + Self { + atol, + rtol, + ..Default::default() + } + } + + fn update(&mut self, a: &T, b: &T, index: ndarray::IxDyn) { + let abs_diff = (*a - *b).abs(); + self.total_error = self.total_error + abs_diff; + self.element_count += 1; + + if abs_diff > self.max_abs_error { + self.max_abs_error = abs_diff; + self.max_abs_error_idxs = Some(index.slice().into()); + } + + if !self.is_close(a, b, abs_diff) { + self.fail_count += 1; + } + } + + fn avg_error(&self) -> T { + self.total_error / T::from(self.element_count).expect("Failed to convert") + } + + fn is_close(&self, a: &T, b: &T, abs_diff: T) -> bool { + (a.is_nan() && b.is_nan()) + || (a.is_infinite() && b.is_infinite() && a.signum() == b.signum()) + || abs_diff <= self.atol + self.rtol * b.abs() + } +} + +#[cfg(feature = "testing")] +impl OpTensor { + pub fn read_npy(path: P, options: TensorOptions) -> Result + where + T: TensorDType + npyz::Deserialize, + P: AsRef, + { + Self::from_npy_bytes::(&std::fs::read(path)?, options) + } + + pub fn write_npy(&self, path: P) -> anyhow::Result<()> + where + T: TensorDType + npyz::Serialize, + P: AsRef, + { + let mut out_buf = vec![]; + let shape = self + .shape() + .to_vec() + .iter() + .map(|x| *x as u64) + .collect::>(); + let mut writer = { + npyz::WriteOptions::new() + .dtype(self.dtype().into()) + .shape(&shape) + .writer(&mut out_buf) + .begin_nd()? + }; + let ndarray = self.to_ndarray_view::(); + ndarray.iter().for_each(|x| { + writer.push(x).unwrap(); + }); + writer.finish()?; + std::fs::write(path, out_buf)?; + Ok(()) + } + + pub fn from_npy_bytes( + bytes: &[u8], + options: TensorOptions, + ) -> Result { + let reader = npyz::NpyFile::new(bytes)?; + let shape = reader + .shape() + .iter() + .map(|&x| x as usize) + .collect::>(); + let data = reader.into_vec::()?; + OpTensor::from_data(data, shape, options) + } + + pub fn into_ndarray(self) -> ArrayD { + self.to_ndarray_view().into_owned() + } + + pub fn to_ndarray_view(&self) -> ArrayViewD<'_, T> { + ensure_resolved_sync!(self); + assert!(self.device().is_cpu()); + assert!(self.dtype() == T::dtype()); + let shape = self.shape().to_vec(); + if self.num_bytes() != 0 { + let storage_guard = self.storage(); + let buffer = storage_guard.as_ref().unwrap().try_cpu().unwrap(); + let (ptr, _) = buffer.inner().into_raw_parts(); + unsafe { ArrayViewD::from_shape_ptr(shape, ptr as *const T) } + } else { + ArrayViewD::from_shape(shape, &[]).unwrap() + } + } + + pub fn all_close(&self, other: &Self, atol: T, rtol: T) -> anyhow::Result<()> + where + T: TensorDType + std::fmt::Display + num_traits::Float + Default, + { + if self.shape() != other.shape() { + anyhow::bail!("Shape mismatch {:?} != {:?}", self.shape(), other.shape()) + } + assert!( + self.dtype() == other.dtype(), + "DType mismatch {:?} != {:?}", + self.dtype(), + other.dtype() + ); + assert!( + self.dtype() == T::dtype(), + "DType mismatch {:?} != {:?}", + self.dtype(), + T::dtype() + ); + + let self_nd = self.to_ndarray_view::(); + let other_nd = other.to_ndarray_view::(); + + let mut stats = CloseStats::new(atol, rtol); + ndarray::indices_of(&self_nd).into_iter().for_each(|idx| { + let (a, b) = (self_nd[&idx], other_nd[&idx]); + stats.update(&a, &b, idx); + }); + + let idx_fmt = stats.max_abs_error_idxs.as_ref(); + if stats.fail_count > 0 { + anyhow::bail!( + "\x1b[1;31m{} samples not close \x1b[0m - AVGE={} MAE={} at {:?}", + stats.fail_count, + stats.avg_error(), + stats.max_abs_error, + idx_fmt + ); + } else { + println!( + "\x1b[1;32mAll close \x1b[0m - AVGE={} MAE={} at {:?}", + stats.avg_error(), + stats.max_abs_error, + idx_fmt + ); + Ok(()) + } + } +} + +#[cfg(feature = "testing")] +impl Tensor { + pub fn all_close(&self, other: &Self, atol: T, rtol: T) -> anyhow::Result<()> + where + T: TensorDType + std::fmt::Display + num_traits::Float + Default, + { + self.inner_or_source() + .all_close(&other.inner_or_source(), atol, rtol) + } +} +impl From> for OpTensor { + fn from(it: ArrayD) -> Self { + if it.as_slice().is_some() { + let layout = std::alloc::Layout::from_size_align( + it.len() * std::mem::size_of::(), + std::mem::align_of::(), + ) + .unwrap(); + let shape = it.shape().to_vec().into(); + let stride = Stride::from(&shape); + let vec = it.into_raw_vec().into_boxed_slice(); + let ptr = Box::into_raw(vec) as *mut u8; + + let raw_buf = RawCPUBuffer::new(ptr, layout); + let meta = StorageView::new(shape, T::dtype(), stride); + OpTensor::new( + LazyOp::Const, + meta, + Some(Storage::CPU(CPUBuffer::new(raw_buf))), + Device::CPU, + false, + ) + .unwrap() + } else { + panic!("Cannot convert numpy array with non-contiguous memory layout to tensor"); + } + } +} + +impl safetensors::View for &OpTensor { + fn dtype(&self) -> safetensors::Dtype { + match OpTensor::dtype(self) { + DType::F32 => safetensors::Dtype::F32, + DType::U32 => safetensors::Dtype::U32, + DType::I32 => safetensors::Dtype::I32, + DType::F16 => safetensors::Dtype::F16, + DType::Q8_0F(_) | DType::Q8_0H(_) => safetensors::Dtype::U8, + DType::BF16 => safetensors::Dtype::BF16, + DType::Q4_KF(_) | DType::Q4_KH(_) => todo!(), + } + } + + fn shape(&self) -> &[usize] { + OpTensor::shape(self).inner() + } + + fn data(&self) -> Cow<'_, [u8]> { + assert!( + self.device().is_cpu(), + "Cannot convert non-CPU tensor to safetensors" + ); + let storage_guard = self.storage(); + let buffer = storage_guard.as_ref().unwrap().try_cpu().unwrap(); + let (ptr, _) = buffer.inner().into_raw_parts(); + Cow::from(unsafe { std::slice::from_raw_parts(ptr, self.num_bytes()) }) + } + + fn data_len(&self) -> usize { + self.num_bytes() + } +} + +// Most of the actual work is done in OpTensor; this very manually wraps OpTensor so we can do +// inplace operations similar to PyTorch, which is written in C++, and does inner mutability more +// straightforwardly (/less safely). +#[derive(Clone)] +pub struct Tensor { + inner: Arc>, + inplace_source: Arc>>, +} + +impl Tensor { + pub fn shallow( + op: LazyOp, + meta: StorageView, + storage: Arc>>, + device: Device, + requires_grad: bool, + ) -> Result { + OpTensor::shallow(op, meta, storage, device, requires_grad).map(Self::wrap) + } + + pub fn inner_or_source(&self) -> RwLockWriteGuard<'_, OpTensor> { + let mut inplace_source = self.inplace_source.write(); + let mut inner = self.inner.write(); + if let Some(inplace_source) = inplace_source.take_if(|_| inner.resolved()) { + *inner = inplace_source.clone(); + } + inner + } + + pub fn wrap(op_tensor: OpTensor) -> Self { + Self { + inner: Arc::new(RwLock::new(op_tensor)), + inplace_source: Arc::new(RwLock::new(None)), + } + } + + fn wrap_inplace_impl(&self, op_tensor: OpTensor, track: bool) -> Self { + if track { + let mut inplace_source = self.inplace_source.write(); + if inplace_source.is_none() { + *inplace_source = Some(self.inner.read().clone()); + } + } + + *op_tensor.inplace.write() = true; + *self.inner.write() = op_tensor; + self.clone() + } + + fn wrap_inplace_untracked(&self, op_tensor: OpTensor) -> Self { + // This is important for making sure we don't erase gradient information. + // If we wanted to be really careful, we would move this to the tensor right after the + // inplace source, set the inplace source to the new tensor, then replay all other inplace + // operations on that tensor. + self.wrap_inplace_impl(op_tensor, false) + } + + fn wrap_inplace(&self, op_tensor: OpTensor) -> Self { + self.wrap_inplace_impl(op_tensor, true) + } + + pub fn inner(&self) -> &Arc> { + &self.inner + } + + pub fn new( + op: LazyOp, + meta: StorageView, + storage: Option, + device: Device, + requires_grad: bool, + ) -> Result { + OpTensor::new(op, meta, storage, device, requires_grad).map(Self::wrap) + } + + pub fn full, S: Into>( + shape: S, + value: T, + options: TensorOptions, + ) -> Result { + OpTensor::full(shape, value, options).map(Self::wrap) + } +} + +// We special-case a bunch of initialization methods here to have an initialization API more like +// PyTorch. +pub fn init_uniform_(tensor: &Tensor, low: Option, high: Option) -> Result { + Ok(tensor.wrap_inplace_untracked(rand_kernel( + tensor.shape(), + low, + high, + TensorOptions::from_tensor(tensor), + )?)) +} + +pub fn init_normal_(tensor: &Tensor, mean: Option, std: Option) -> Result { + Ok(tensor.wrap_inplace_untracked(randn_kernel( + tensor.shape(), + mean, + std, + TensorOptions::from_tensor(tensor), + )?)) +} + +pub fn init_constant_(tensor: &Tensor, value: f32) -> Result { + Ok(tensor.wrap_inplace_untracked(full_kernel( + tensor.shape(), + value, + TensorOptions::from_tensor(tensor), + )?)) +} + +pub fn init_ones_(tensor: &Tensor) -> Result { + Ok(tensor.wrap_inplace_untracked(ones_kernel( + tensor.shape(), + TensorOptions::from_tensor(tensor), + )?)) +} + +pub fn init_zeros_(tensor: &Tensor) -> Result { + Ok(tensor.wrap_inplace_untracked(zeros_kernel( + tensor.shape(), + TensorOptions::from_tensor(tensor), + )?)) +} + +pub fn init_eye_(tensor: &Tensor) -> Result { + let (n, m) = tensor.shape().dims2()?; + Ok(tensor.wrap_inplace_untracked(eye_kernel(n, Some(m), TensorOptions::from_tensor(tensor))?)) +} + +pub fn init_xavier_uniform_(tensor: &Tensor, gain: Option) -> Result { + Ok(tensor.wrap_inplace_untracked(xavier_uniform_kernel( + tensor.shape(), + gain, + TensorOptions::from_tensor(tensor), + )?)) +} + +pub fn init_xavier_normal_(tensor: &Tensor, gain: Option) -> Result { + Ok(tensor.wrap_inplace_untracked(xavier_normal_kernel( + tensor.shape(), + gain, + TensorOptions::from_tensor(tensor), + )?)) +} + +pub fn init_kaiming_uniform_( + tensor: &Tensor, + a: Option, + mode: KaimingFan, + nonlinearity: KaimingNonLinearity, +) -> Result { + Ok(tensor.wrap_inplace_untracked(kaiming_uniform_kernel( + tensor.shape(), + a, + mode, + nonlinearity, + TensorOptions::from_tensor(tensor), + )?)) +} + +pub fn init_kaiming_normal_( + tensor: &Tensor, + a: Option, + mode: KaimingFan, + nonlinearity: KaimingNonLinearity, +) -> Result { + Ok(tensor.wrap_inplace_untracked(kaiming_normal_kernel( + tensor.shape(), + a, + mode, + nonlinearity, + TensorOptions::from_tensor(tensor), + )?)) +} + +pub fn init_orthogonal_(tensor: &Tensor, gain: Option) -> Result { + Ok(tensor.wrap_inplace_untracked(orthogonal_kernel( + tensor.shape(), + gain, + TensorOptions::from_tensor(tensor), + )?)) +} + +impl std::fmt::Debug for Tensor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.inner_or_source()) + } +} + +impl PartialEq for Tensor { + fn eq(&self, other: &Self) -> bool { + let self_tensor = self.inner_or_source(); + let other_tensor = other.inner_or_source(); + *self_tensor == *other_tensor + } +} + +// No deref + +impl Tensor { + pub fn id(&self) -> TensorId { + self.inner_or_source().id() + } + + pub fn dim(&self) -> usize { + self.inner_or_source().dim() + } + + pub fn dtype(&self) -> DType { + self.inner_or_source().dtype() + } + + pub fn shape(&self) -> Shape { + self.inner_or_source().shape().clone() + } + + pub fn stride(&self) -> Stride { + self.inner_or_source().stride().clone() + } + + pub fn device(&self) -> Device { + self.inner_or_source().device().clone() + } + + pub fn resolved(&self) -> bool { + self.inner_or_source().resolved() + } + + pub fn op(&self) -> LazyOp { + self.inner_or_source().op().clone() + } + + pub fn scope(&self) -> Option { + self.inner_or_source().scope().clone() + } + + pub fn is_scalar(&self) -> bool { + self.inner_or_source().is_scalar() + } + + pub fn requires_grad(&self) -> bool { + self.inner_or_source().requires_grad() + } + + pub fn is_leaf(&self) -> bool { + self.inner_or_source().is_leaf() + } + + pub fn set_sync(&self, src: Self) -> Result<()> { + self.inner_or_source() + .clone() + .set(src.inner_or_source().clone()); + Ok(()) + } + + pub fn set(&self, src: Self) -> Self { + self.inner_or_source() + .clone() + .set(src.inner_or_source().clone()); + self.clone() + } + + pub fn num_bytes(&self) -> usize { + self.inner_or_source().num_bytes() + } + + pub fn strong_count(&self) -> usize { + self.inner_or_source().strong_count() + } +} + +impl Tensor { + /// Returns true if the data is stored in a C contiguous (aka row major) way. + pub fn is_contiguous(&self) -> bool { + self.inner_or_source().clone().is_contiguous() + } + + pub fn has_nan(&self) -> bool { + self.inner_or_source().clone().has_nan::() + } + + /// Creates a new tensor from a chunk of data. + /// + /// The Tensor is instantly resolved. + /// If a non-CPU device is specified, the data will be copied to the device. + pub fn from_data, S: Into>( + data: U, + shape: S, + options: TensorOptions, + ) -> Result { + OpTensor::from_data(data, shape, options).map(Self::wrap) + } + + pub fn from_bytes>( + data: &[u8], + shape: S, + options: TensorOptions, + ) -> Result { + OpTensor::from_bytes(data, shape, options).map(Self::wrap) + } + + /// Create a parameter based on the values currently stored in a tensor. The storage is always + /// copied. + pub fn requires_grad_(&self, requires_grad: bool) -> Result { + self.inner_or_source() + .requires_grad_(requires_grad) + .map(Self::wrap) + } + + pub fn retain_grad(&self) -> Result<()> { + self.inner_or_source().retain_grad() + } + + pub fn retains_grad(&self) -> bool { + self.inner_or_source().retains_grad() + } + + /// Returns a new tensor detached from the current graph, gradient are not propagated through + /// this new node. The storage of this tensor is shared with the initial tensor. + /// + /// If the tensor is already detached from the computation graph, the same tensor is returned. + pub fn detach(&self) -> Result { + self.inner_or_source().detach().map(Self::wrap) + } + + pub fn detach_(&self) -> Result { + let inner = self.inner_or_source().clone(); + Ok(self.wrap_inplace_untracked(inner.detach()?)) + } + + /// # Safety + /// + /// If the tensor has more than 1 reference, you die. + /// If the tensor has no storage, you die. + pub fn into_bytes(self) -> anyhow::Result> { + self.inner_or_source().clone().into_bytes() + } + + pub fn from_quantized, S: Into>( + data: U, + dtype: DType, + shape: S, + device: Device, + ) -> Self { + Self::wrap(OpTensor::from_quantized(data, dtype, shape, device)) + } + + pub fn from_disk>( + reader: &mut R, + shape: S, + device: Device, + ) -> Result { + OpTensor::from_disk::(reader, shape, device).map(Self::wrap) + } + + #[maybe_async] + pub async fn item(&self) -> T { + let inner = self.inner_or_source().clone(); + inner.item::().await + } + + /// Converts the tensor into a 1D vector. + /// + /// The 1D vector contains the data from the tensor, as it was laid out in memory. + #[maybe_async] + pub async fn to_vec(&self) -> anyhow::Result> { + let inner = self.inner_or_source().clone(); + inner.to_vec::().await + } + + #[maybe_async] + pub async fn cpu_apply(self, dst: Self) -> Option { + let inner_clone = self.inner_or_source().clone(); + let dst_inner_clone = dst.inner_or_source().clone(); + + inner_clone.cpu_apply(dst_inner_clone).await.map(Self::wrap) + } + + #[maybe_async] + pub async fn deep_clone(&self) -> Result { + let inner_clone = self.inner_or_source().clone(); + Ok(Self::wrap(inner_clone.deep_clone().await?)) + } + + /// Transfers the tensor to the specified device. + /// + /// If the tensor is already on the specified device, it will be returned as-is, + /// and the underlying storage will not be copied. + /// If the tensor is on a different device, it will be copied to the specified device. + #[maybe_async] + pub async fn to(&self, device: &Device) -> Result { + let inner_clone = self.inner_or_source().clone(); + inner_clone.to(device).await.map(Self::wrap) + } + + #[cfg(not(feature = "debug"))] + pub fn debug_tensor(&self) -> Option { + self.inner_or_source().debug_tensor().map(Self::wrap) + } + + #[cfg(not(feature = "debug"))] + pub fn get_or_create_debug_tensor(&self) -> Result { + self.inner_or_source() + .get_or_create_debug_tensor() + .map(Self::wrap) + } + + pub fn grad(&self) -> Option { + self.inner_or_source().grad() + } + + pub fn set_grad(&self, grad: Option) { + self.inner_or_source().set_grad(grad); + } + + pub fn take_grad(&self) -> Option { + self.inner_or_source().take_grad() + } +} + +impl From> for Tensor { + fn from(it: ArrayD) -> Self { + Self::wrap(OpTensor::from(it)) + } +} + +macro_rules! bin_trait_wrapper { + ($trait:ident, $fn1:ident, $mul:expr, $add:expr) => { + impl std::ops::$trait for Tensor { + type Output = Result; + + fn $fn1(self, rhs: Tensor) -> Self::Output { + Tensor::$fn1(self, rhs) + } + } + + impl std::ops::$trait for Result { + type Output = Result; + + fn $fn1(self, rhs: Tensor) -> Self::Output { + Tensor::$fn1(self?, rhs) + } + } + + impl std::ops::$trait> for Tensor { + type Output = Result; + + fn $fn1(self, rhs: Result) -> Self::Output { + Tensor::$fn1(self, rhs?) + } + } + + impl std::ops::$trait for Tensor { + type Output = Result; + + fn $fn1(self, rhs: f32) -> Self::Output { + self.affine($mul(rhs), $add(rhs)) + } + } + }; +} + +bin_trait_wrapper!(Add, add, |_| 1., |v| v); +bin_trait_wrapper!(Sub, sub, |_| 1., |v: f32| -v); +bin_trait_wrapper!(Mul, mul, |v| v, |_| 0.); +bin_trait_wrapper!(Div, div, |v| 1. / v, |_| 0.); + +impl std::ops::Add for f32 { + type Output = Result; + + fn add(self, rhs: Tensor) -> Self::Output { + add_kernel::<_, _, Tensor>(rhs, self).map(Tensor::wrap) + } +} + +impl std::ops::Mul for f32 { + type Output = Result; + + fn mul(self, rhs: Tensor) -> Self::Output { + mul_kernel::<_, _, Tensor>(rhs, self).map(Tensor::wrap) + } +} + +impl std::ops::Sub for f32 { + type Output = Result; + + fn sub(self, rhs: Tensor) -> Self::Output { + rhs.affine(-1., self) + } +} + +impl std::ops::Div for f32 { + type Output = Result; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn div(self, rhs: Tensor) -> Self::Output { + rhs.recip()? * self + } +} + +// +// Tensor <-> OpTensor conversion +// + +impl From for OpTensor { + fn from(t: Tensor) -> Self { + t.inner_or_source().clone() + } +} + +impl From<&Tensor> for OpTensor { + fn from(t: &Tensor) -> Self { + t.inner_or_source().clone() + } +} + +impl From for Tensor { + fn from(t: OpTensor) -> Self { + Self::wrap(t) + } +} + +// +// TensorTypeOrScalar +// + +#[derive(Debug, Clone)] +pub enum TensorTypeOrScalarEnum { + Tensor(T), + Scalar(f32), +} + +pub trait TensorTypeOrScalar { + fn tensor_or_scalar(&self) -> Result>; + fn tensor(&self) -> Option { + self.tensor_or_scalar().ok().and_then(|x| match x { + TensorTypeOrScalarEnum::Tensor(t) => Some(t), + _ => None, + }) + } + fn map_tensor ReturnTensorType>( + &self, + f: F, + ) -> Result> { + match self.tensor_or_scalar()? { + TensorTypeOrScalarEnum::Tensor(t) => Ok(TensorTypeOrScalarEnum::Tensor(f(t))), + TensorTypeOrScalarEnum::Scalar(s) => Ok(TensorTypeOrScalarEnum::Scalar(s)), + } + } +} + +impl TensorTypeOrScalarEnum> { + pub fn transpose(self) -> Result> { + match self { + TensorTypeOrScalarEnum::Tensor(t) => Ok(TensorTypeOrScalarEnum::Tensor(t?)), + TensorTypeOrScalarEnum::Scalar(s) => Ok(TensorTypeOrScalarEnum::Scalar(s)), + } + } +} + +impl TensorTypeOrScalar for OpTensor { + fn tensor_or_scalar(&self) -> Result> { + Ok(TensorTypeOrScalarEnum::Tensor(self.clone())) + } +} + +impl TensorTypeOrScalar for Tensor { + fn tensor_or_scalar(&self) -> Result> { + Ok(TensorTypeOrScalarEnum::Tensor(self.clone())) + } +} + +impl TensorTypeOrScalar for Result> { + fn tensor_or_scalar(&self) -> Result> { + match self { + Ok(value) => Ok(value.clone()), + Err(e) => Err(anyhow::anyhow!("{}", e)), + } + } +} + +impl TensorTypeOrScalar for U { + fn tensor_or_scalar(&self) -> Result> { + Ok(TensorTypeOrScalarEnum::Scalar( + self.to_f32() + .ok_or(anyhow::anyhow!("Could not convert to f32"))?, + )) + } +} + +impl TensorTypeOrScalar for TensorTypeOrScalarEnum { + fn tensor_or_scalar(&self) -> Result> { + Ok(self.clone()) + } +} + +impl From> for OpTensor { + fn from(t: TensorTypeOrScalarEnum) -> Self { + match t { + TensorTypeOrScalarEnum::Tensor(t) => t, + TensorTypeOrScalarEnum::Scalar(s) => { + panic!("Scalar {s} cannot be converted to OpTensor") + } + } + } +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use crate::{Device, Tensor, TensorOptions, cat, randn, rvec}; + + #[test] + fn has_nan_works() -> anyhow::Result<()> { + let device = Device::request_device(crate::DeviceRequest::GPU).unwrap(); + let rand = randn((1, 1500, 384), None, None, TensorOptions::new()).unwrap(); + let nans = Tensor::from_data( + vec![f32::NAN; 1500 * 384], + (1, 1500, 384), + TensorOptions::new(), + )?; + + let bingo = cat(rvec![rand, nans], 2).unwrap(); + + let result = bingo.to(&Device::CPU).unwrap(); + println!("RESULT: {result:?}"); + assert!(result.has_nan::()); + Ok(()) + } +} diff --git a/crates/ratchet-core/src/tensor_id.rs b/crates/piston-core/src/tensor_id.rs similarity index 84% rename from crates/ratchet-core/src/tensor_id.rs rename to crates/piston-core/src/tensor_id.rs index e3691705..61a415cf 100644 --- a/crates/ratchet-core/src/tensor_id.rs +++ b/crates/piston-core/src/tensor_id.rs @@ -1,6 +1,10 @@ +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + /// Unique identifier for tensors. #[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct TensorId(pub(crate) usize); +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +pub struct TensorId(pub usize); impl std::fmt::Debug for TensorId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/crates/ratchet-core/src/test_utils.rs b/crates/piston-core/src/test_utils.rs similarity index 76% rename from crates/ratchet-core/src/test_utils.rs rename to crates/piston-core/src/test_utils.rs index 436b8cce..503c22d2 100644 --- a/crates/ratchet-core/src/test_utils.rs +++ b/crates/piston-core/src/test_utils.rs @@ -4,14 +4,14 @@ use maybe_async::maybe_async; #[maybe_async] pub async fn to_vec0_round(t: &Tensor, digits: i32) -> anyhow::Result { let b = 10f32.powi(digits); - let t = t.to(&Device::CPU).await?.to_vec::()?[0]; + let t = t.to(&Device::CPU).await?.to_vec::().await?[0]; Ok(f32::round(t * b) / b) } #[maybe_async] pub async fn to_vec1_round(t: &Tensor, digits: i32) -> anyhow::Result> { let b = 10f32.powi(digits); - let t = t.to(&Device::CPU).await?.to_vec::()?; + let t = t.to(&Device::CPU).await?.to_vec::().await?; let t = t.iter().map(|t| f32::round(t * b) / b).collect(); Ok(t) } diff --git a/crates/ratchet-core/tests/attn_tests.rs b/crates/piston-core/tests/attn_tests.rs similarity index 79% rename from crates/ratchet-core/tests/attn_tests.rs rename to crates/piston-core/tests/attn_tests.rs index 506a8c0b..b884b03c 100644 --- a/crates/ratchet-core/tests/attn_tests.rs +++ b/crates/piston-core/tests/attn_tests.rs @@ -1,6 +1,6 @@ #[cfg(all(test, feature = "pyo3"))] mod tests { - use ratchet::{shape, test_util::run_py_prg, Device, DeviceRequest, Tensor}; + use piston::{Device, DeviceRequest, Tensor, TensorOptions, randn, test_util::run_py_prg}; #[derive(Debug, derive_new::new)] struct AttentionTest { @@ -42,7 +42,7 @@ def scaled_dot_product_attention(input, qw, kw, vw) -> torch.Tensor: prg.to_string(), &[&case.input, &case.qw, &case.kw, &case.vw], &[], - case.input.dt(), + case.input.dtype(), ) } @@ -58,8 +58,12 @@ def scaled_dot_product_attention(input, qw, kw, vw) -> torch.Tensor: let v_proj = input.matmul(vw, false, false)?; let scale_factor = 1f64 / (q_proj.shape()[2] as f64).sqrt(); - let scale_factor = Tensor::from_data([scale_factor as f32], shape![1], device); - let kt = k_proj.permute(&[0, 2, 1])?; + let scale_factor = Tensor::from_data( + [scale_factor as f32], + 1, + TensorOptions::new().device(device), + )?; + let kt = k_proj.permute((0, 2, 1))?; let logits = q_proj.matmul(kt, false, false)?.mul(scale_factor)?; logits.softmax(2)?.matmul(v_proj, false, false) @@ -68,10 +72,10 @@ def scaled_dot_product_attention(input, qw, kw, vw) -> torch.Tensor: #[test] pub fn test_sdpa() -> anyhow::Result<()> { let _ = env_logger::builder().is_test(true).try_init(); - let input = Tensor::randn::(0., 1., shape![1, 128, 256], Device::CPU); - let qw = Tensor::randn::(0., 1., shape![256, 256], Device::CPU); - let kw = Tensor::randn::(0., 1., shape![256, 256], Device::CPU); - let vw = Tensor::randn::(0., 1., shape![256, 256], Device::CPU); + let input = randn((1, 128, 256), None, None, Default::default())?; + let qw = randn((256, 256), None, None, Default::default())?; + let kw = randn((256, 256), None, None, Default::default())?; + let vw = randn((256, 256), None, None, Default::default())?; let cpu_test_case = AttentionTest::new(input, qw, kw, vw, None); let ground = sdpa_ground(&cpu_test_case)?; @@ -115,7 +119,7 @@ def qkv_attention(input, qw, kw, vw, n_heads): prg.to_string(), &[&case.input, &case.qw, &case.kw, &case.vw], &[&case.n_heads.unwrap()], - case.input.dt(), + case.input.dtype(), ) } @@ -133,35 +137,35 @@ def qkv_attention(input, qw, kw, vw, n_heads): let n_heads = case.n_heads.unwrap(); let qdim = q_proj.shape()[2]; let scale = ((qdim / n_heads) as f32).powf(-0.25); - let scale = Tensor::from_data([scale], shape![1], device); + let scale = Tensor::from_data([scale], 1, TensorOptions::new().device(device))?; let hdim = qdim / n_heads; let q = q_proj - .view(shape![1, hdim, n_heads, hdim])? - .permute(&[0, 2, 1, 3])? + .view((1, hdim, n_heads, hdim))? + .permute((0, 2, 1, 3))? .mul(scale.clone())?; let k = k_proj - .view(shape![1, hdim, n_heads, hdim])? - .permute(&[0, 2, 3, 1])? + .view((1, hdim, n_heads, hdim))? + .permute((0, 2, 3, 1))? .mul(scale.clone())?; let v = v_proj - .view(shape![1, hdim, n_heads, hdim])? - .permute(&[0, 2, 1, 3])?; + .view((1, hdim, n_heads, hdim))? + .permute((0, 2, 1, 3))?; let qk = q.matmul(k, false, false)?; let attn = qk.softmax(3)?; attn.matmul(v, false, false)? - .permute(&[0, 2, 1, 3])? - .view(shape![1, hdim, qdim]) + .permute((0, 2, 1, 3))? + .view((1, hdim, qdim)) } #[test] pub fn test_mha() -> anyhow::Result<()> { let _ = env_logger::builder().is_test(true).try_init(); - let input = Tensor::randn::(0., 1., shape![1, 64, 384], Device::CPU); - let qw = Tensor::randn::(0., 1., shape![1, 384, 384], Device::CPU); - let kw = Tensor::randn::(0., 1., shape![1, 384, 384], Device::CPU); - let vw = Tensor::randn::(0., 1., shape![1, 384, 384], Device::CPU); + let input = randn((1, 64, 384), None, None, Default::default())?; + let qw = randn((1, 384, 384), None, None, Default::default())?; + let kw = randn((1, 384, 384), None, None, Default::default())?; + let vw = randn((1, 384, 384), None, None, Default::default())?; let cpu_test_case = AttentionTest::new(input, qw, kw, vw, Some(6)); let ground = mha_ground(&cpu_test_case)?; diff --git a/crates/piston-core/tests/caching.rs b/crates/piston-core/tests/caching.rs new file mode 100644 index 00000000..309a5a51 --- /dev/null +++ b/crates/piston-core/tests/caching.rs @@ -0,0 +1,45 @@ +use piston::{Device, DeviceRequest, Tensor, TensorOptions}; + +// TODO(vinhowe): Remove or motivate this test +#[test] +fn test_simple_caching() -> anyhow::Result<()> { + let _ = env_logger::builder().is_test(true).try_init(); + + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + let t1 = Tensor::from_data(vec![1f32], 1, TensorOptions::new())? + .to(&device)? + .square()?; + println!("t1: {:?}", t1.to(&Device::CPU)?.to_vec::()?); + + assert_eq!(t1.to(&Device::CPU)?.to_vec::()?, vec![1f32]); + + let t2 = Tensor::from_data(vec![2f32], 1, TensorOptions::new())? + .to(&device)? + .square()?; + println!("t2: {:?}", t2.to(&Device::CPU)?.to_vec::()?); + + assert_eq!(t2.to(&Device::CPU)?.to_vec::()?, vec![4f32]); + + let t3 = Tensor::from_data(vec![3f32], 1, TensorOptions::new())? + .to(&device)? + .square()?; + println!("t3: {:?}", t3.to(&Device::CPU)?.to_vec::()?); + + assert_eq!(t3.to(&Device::CPU)?.to_vec::()?, vec![9f32]); + + let t4 = Tensor::from_data(vec![4f32], 1, TensorOptions::new())? + .to(&device)? + .square()?; + println!("t4: {:?}", t4.to(&Device::CPU)?.to_vec::()?); + + assert_eq!(t4.to(&Device::CPU)?.to_vec::()?, vec![16f32]); + + let t5 = Tensor::from_data(vec![5f32], 1, TensorOptions::new())? + .to(&device)? + .square()?; + println!("t5: {:?}", t5.to(&Device::CPU)?.to_vec::()?); + + assert_eq!(t5.to(&Device::CPU)?.to_vec::()?, vec![25f32]); + + Ok(()) +} diff --git a/crates/ratchet-datasets/Cargo.toml b/crates/piston-datasets/Cargo.toml similarity index 68% rename from crates/ratchet-datasets/Cargo.toml rename to crates/piston-datasets/Cargo.toml index 79f7f358..3952f1b2 100644 --- a/crates/ratchet-datasets/Cargo.toml +++ b/crates/piston-datasets/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "ratchet-datasets" +name = "piston-datasets" version = "0.1.0" -edition = "2021" +edition = "2024" license = "MIT" description = "A web-first, cross-platform ML framework." keywords = [ @@ -13,7 +13,7 @@ keywords = [ "machine-learning", "deep-learning", ] -repository = "https://github.com/FL33TW00D/ratchet" +repository = "https://github.com/vinhowe/piston" [package.metadata.wasm-pack.profile.dev.wasm-bindgen] debug-js-glue = true @@ -25,12 +25,7 @@ wasm-opt = ['-O3', '--enable-simd'] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ratchet = { path = "../ratchet-core" } -ratchet-loader = { path = "../ratchet-loader" } -log.workspace = true -tokenizers = { version = "0.19.1", default-features = false, features = [ - "unstable_wasm", -] } +piston = { path = "../piston-core" } anyhow.workspace = true memmap2.workspace = true rand.workspace = true diff --git a/crates/ratchet-datasets/src/batcher.rs b/crates/piston-datasets/src/batcher.rs similarity index 94% rename from crates/ratchet-datasets/src/batcher.rs rename to crates/piston-datasets/src/batcher.rs index 1827a445..333809d4 100644 --- a/crates/ratchet-datasets/src/batcher.rs +++ b/crates/piston-datasets/src/batcher.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use ratchet::{RVec, Tensor}; +use piston::{RVec, Tensor, stack}; pub struct Batcher { inner: I, @@ -86,7 +86,7 @@ impl> Iterator for Batcher> { } } } - Some(Tensor::stack(items, 0)) + Some(stack(items, 0)) } } @@ -110,8 +110,8 @@ impl> Iterator for Batcher> { } } } - let xs = Tensor::stack(xs, 0); - let ys = Tensor::stack(ys, 0); + let xs = stack(xs, 0); + let ys = stack(ys, 0); Some(xs.and_then(|xs| ys.map(|ys| (xs, ys)))) } } @@ -136,7 +136,7 @@ impl>> Iterator for Batcher> { } } let items = items.into_iter().collect::>>(); - Some(items.and_then(|items| Tensor::stack(items, 0))) + Some(items.and_then(|items| stack(items, 0))) } } @@ -165,8 +165,8 @@ impl>> Iterator for Batcher, @@ -20,10 +20,10 @@ impl Dataset { let mut bin_files = vec![]; for file in std::fs::read_dir(dir)?.flatten() { let file = file.path(); - if let Some(extension) = file.extension() { - if extension == "bin" { - bin_files.push(file) - } + if let Some(extension) = file.extension() + && extension == "bin" + { + bin_files.push(file) } } if bin_files.len() < 2 { @@ -91,7 +91,7 @@ impl<'a> DatasetRandomIter<'a> { } } -impl<'a> Iterator for DatasetRandomIter<'a> { +impl Iterator for DatasetRandomIter<'_> { type Item = Result<(Tensor, Tensor)>; fn next(&mut self) -> Option { @@ -119,8 +119,18 @@ impl<'a> Iterator for DatasetRandomIter<'a> { return Some(Err(err.into())); } let tokens = tokens.into_iter().map(|v| v as i32).collect::>(); - let inputs = Tensor::from_data(&tokens[..seq_len], shape![seq_len], self.device.clone()); - let targets = Tensor::from_data(&tokens[1..], shape![seq_len], self.device.clone()); + let inputs = Tensor::from_data( + &tokens[..seq_len], + (seq_len,), + TensorOptions::new().device(self.device.clone()), + ) + .expect("Shouldn't be an error because requires_grad is false"); + let targets = Tensor::from_data( + &tokens[1..], + (seq_len,), + TensorOptions::new().device(self.device.clone()), + ) + .expect("Shouldn't be an error because requires_grad is false"); Some(Ok((inputs, targets))) } } diff --git a/crates/ratchet-macros/Cargo.toml b/crates/piston-macros/Cargo.toml similarity index 56% rename from crates/ratchet-macros/Cargo.toml rename to crates/piston-macros/Cargo.toml index e3b884fc..7d42bc50 100644 --- a/crates/ratchet-macros/Cargo.toml +++ b/crates/piston-macros/Cargo.toml @@ -1,11 +1,10 @@ [package] -name = "ratchet-macros" -authors = ["Chris Fleetwood "] +name = "piston-macros" version = "0.1.0" keywords = [] -edition = "2021" +edition = "2024" readme.workspace = true -repository = "https://github.com/huggingface/ratchet" +repository = "https://github.com/vinhowe/piston" [lib] proc-macro = true @@ -17,5 +16,8 @@ std = [] [dependencies] proc-macro2 = { version = "1.0" } quote = { version = "1.0" } -syn = { version = "2.0" } +syn = { version = "2.0", features = ["full"] } heck = { version = "0.5.0" } + +[dev-dependencies] +prettyplease = "0.2" \ No newline at end of file diff --git a/crates/ratchet-macros/src/ir_fields.rs b/crates/piston-macros/src/ir_fields.rs similarity index 98% rename from crates/ratchet-macros/src/ir_fields.rs rename to crates/piston-macros/src/ir_fields.rs index 4a30937e..6e13a040 100644 --- a/crates/ratchet-macros/src/ir_fields.rs +++ b/crates/piston-macros/src/ir_fields.rs @@ -1,7 +1,7 @@ use heck::ToSnakeCase; use proc_macro2::TokenStream; use quote::quote; -use syn::{parse2, DeriveInput}; +use syn::{DeriveInput, parse2}; pub fn derive(input: TokenStream) -> TokenStream { let input = parse2::(input).unwrap(); diff --git a/crates/piston-macros/src/js_tensor_web_op.rs b/crates/piston-macros/src/js_tensor_web_op.rs new file mode 100644 index 00000000..ab2d1c40 --- /dev/null +++ b/crates/piston-macros/src/js_tensor_web_op.rs @@ -0,0 +1,1724 @@ +use heck::{ToLowerCamelCase, ToUpperCamelCase}; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Expr, FnArg, Ident, ItemFn, LitStr, Result as SynResult, Token, Type}; + +fn to_lower_camel_case_with_underscore(ident: &str) -> String { + let leading_count = ident.chars().take_while(|&c| c == '_').count(); + let trailing_count = ident.chars().rev().take_while(|&c| c == '_').count(); + + if leading_count + trailing_count >= ident.len() { + return ident.to_string(); + } + + format!( + "{}{}{}", + &ident[..leading_count], + &ident[leading_count..ident.len() - trailing_count].to_lower_camel_case(), + &ident[ident.len() - trailing_count..] + ) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum VariantKind { + Function, + Method, + MethodInplace, +} + +impl VariantKind { + fn as_js_export_name(self, op_name_pascal: &str) -> String { + match self { + // Function exports should be lowerCamelCase of the op name (e.g., zerosLike) + VariantKind::Function => op_name_pascal.to_lower_camel_case(), + VariantKind::Method => format!("opMethod{op_name_pascal}"), + VariantKind::MethodInplace => format!("opMethod{op_name_pascal}_"), + } + } +} + +#[derive(Default)] +pub struct JsTensorWebOpAttr { + name: String, + variants: Vec, + dtype_generic: Option>, // parsed but not used initially + getter: bool, + setter: bool, + js_name: Option, + target: Option, +} + +impl Parse for JsTensorWebOpAttr { + fn parse(input: ParseStream) -> SynResult { + let mut name: Option = None; + let mut variants: Option> = None; + let mut dtype_generic: Option> = None; + let mut getter: bool = false; + let mut setter: bool = false; + let mut js_name: Option = None; + let mut target: Option = None; + + let parser = syn::punctuated::Punctuated::::parse_terminated; + // Try a simple name-value list first, but only if it consumes the entire input + let fork = input.fork(); + let list_res = parser(&fork); + if let Ok(list) = list_res + && fork.is_empty() + { + // consume + let _ = parser(input)?; + for nv in list { + if nv.path.is_ident("name") { + match nv.value { + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(ls), + .. + }) => { + name = Some(ls.value()); + } + syn::Expr::Path(ref p) => { + // Allow identifiers (e.g., passed from macro_rules) like name = Add + if let Some(seg) = p.path.segments.last() { + name = Some(seg.ident.to_string()); + } else { + return Err(syn::Error::new_spanned( + &nv.value, + "invalid ident for name", + )); + } + } + _ => { + return Err(syn::Error::new_spanned( + nv, + "name must be a string literal", + )); + } + } + } else if nv.path.is_ident("variants") { + // Expect array literal like [function, method] + match nv.value { + syn::Expr::Array(arr) => { + let mut vs = Vec::new(); + for elem in arr.elems { + match elem { + syn::Expr::Path(p) => { + let ident = + p.path.segments.last().unwrap().ident.to_string(); + let v = match ident.as_str() { + "function" => VariantKind::Function, + "method" => VariantKind::Method, + "method_inplace" => VariantKind::MethodInplace, + other => { + return Err(syn::Error::new_spanned( + p, + format!("Unknown variant '{other}'"), + )); + } + }; + vs.push(v); + } + other => { + return Err(syn::Error::new_spanned( + other, + "variants expects an array of identifiers", + )); + } + } + } + variants = Some(vs); + } + other => { + return Err(syn::Error::new_spanned( + other, + "variants must be an array literal, e.g. [function, method]", + )); + } + } + } else if nv.path.is_ident("dtype_generic") { + // dtype_generic() or dtype_generic(f32, f16) + match nv.value { + syn::Expr::Tuple(t) => { + let mut dts = Vec::new(); + for elem in t.elems { + if let syn::Expr::Path(p) = elem { + dts.push(p.path.segments.last().unwrap().ident.clone()); + } + } + dtype_generic = Some(dts); + } + syn::Expr::Path(_) => { + dtype_generic = Some(Vec::new()); + } + other => { + return Err(syn::Error::new_spanned( + other, + "dtype_generic must be like dtype_generic or dtype_generic(f32, f16)", + )); + } + } + } else if nv.path.is_ident("js_name") { + match nv.value { + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(ls), + .. + }) => { + js_name = Some(ls.value()); + } + syn::Expr::Path(ref p) => { + if let Some(seg) = p.path.segments.last() { + js_name = Some(seg.ident.to_string()); + } else { + return Err(syn::Error::new_spanned( + &nv.value, + "invalid ident for js_name", + )); + } + } + _ => { + return Err(syn::Error::new_spanned( + nv, + "js_name must be a string literal", + )); + } + } + } else if nv.path.is_ident("target") { + match nv.value { + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(ls), + .. + }) => { + target = Some(ls.value()); + } + syn::Expr::Path(ref p) => { + if let Some(seg) = p.path.segments.last() { + target = Some(seg.ident.to_string()); + } else { + return Err(syn::Error::new_spanned( + &nv.value, + "invalid ident for target", + )); + } + } + _ => { + return Err(syn::Error::new_spanned( + nv, + "target must be a string literal", + )); + } + } + } else if nv.path.is_ident("getter") { + match nv.value { + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Bool(b), + .. + }) => { + getter = b.value; + } + _ => return Err(syn::Error::new_spanned(nv, "getter must be a boolean")), + } + } else if nv.path.is_ident("setter") { + match nv.value { + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Bool(b), + .. + }) => { + setter = b.value; + } + _ => return Err(syn::Error::new_spanned(nv, "setter must be a boolean")), + } + } else { + return Err(syn::Error::new_spanned( + nv, + "Unknown attribute key for js_tensor_web_op", + )); + } + } + } else { + // Named arguments using nested meta, e.g. name = "Addcdiv", variants = [...] + let mut parsed_any = false; + while !input.is_empty() { + parsed_any = true; + let lookahead = input.lookahead1(); + if lookahead.peek(syn::Ident) { + let ident: Ident = input.parse()?; + if ident == "getter" { + getter = true; + let _ = input.parse::(); + continue; + } else if ident == "setter" { + setter = true; + let _ = input.parse::(); + continue; + } + input.parse::()?; + if ident == "name" { + // Accept either a string literal or a bare ident here + if input.peek(LitStr) { + let lit: LitStr = input.parse()?; + name = Some(lit.value()); + } else if input.peek(Ident) { + let id2: Ident = input.parse()?; + name = Some(id2.to_string()); + } else { + return Err(syn::Error::new( + Span::call_site(), + "name must be a string literal or ident", + )); + } + } else if ident == "js_name" { + if input.peek(LitStr) { + let lit: LitStr = input.parse()?; + js_name = Some(lit.value()); + } else if input.peek(Ident) { + let id2: Ident = input.parse()?; + js_name = Some(id2.to_string()); + } else { + return Err(syn::Error::new( + Span::call_site(), + "js_name must be a string literal or ident", + )); + } + } else if ident == "variants" { + let content; + syn::bracketed!(content in input); + let elems = + syn::punctuated::Punctuated::::parse_terminated( + &content, + )?; + let mut vs = Vec::new(); + for elem in elems { + if let syn::Expr::Path(p) = elem { + let id = p.path.segments.last().unwrap().ident.to_string(); + let v = match id.as_str() { + "function" => VariantKind::Function, + "method" => VariantKind::Method, + "method_inplace" => VariantKind::MethodInplace, + _ => return Err(syn::Error::new_spanned(p, "Unknown variant")), + }; + vs.push(v); + } else { + return Err(syn::Error::new_spanned( + elem, + "variants expects identifiers", + )); + } + } + variants = Some(vs); + } else if ident == "dtype_generic" { + // Accept optional parens with items + if input.peek(syn::token::Paren) { + let content; + syn::parenthesized!(content in input); + let elems = + syn::punctuated::Punctuated::::parse_terminated( + &content, + )?; + dtype_generic = Some(elems.into_iter().collect()); + } else { + dtype_generic = Some(Vec::new()); + } + } else if ident == "target" { + if input.peek(LitStr) { + let lit: LitStr = input.parse()?; + target = Some(lit.value()); + } else if input.peek(Ident) { + let id2: Ident = input.parse()?; + target = Some(id2.to_string()); + } else { + return Err(syn::Error::new( + Span::call_site(), + "target must be a string literal or ident", + )); + } + } else { + return Err(syn::Error::new_spanned(ident, "Unknown key")); + } + // Optional trailing comma + let _ = input.parse::>(); + } else { + return Err(lookahead.error()); + } + } + if !parsed_any { + return Err(syn::Error::new( + Span::call_site(), + "Expected attributes for js_tensor_web_op", + )); + } + } + + let name = name.ok_or_else(|| { + syn::Error::new( + Span::call_site(), + "js_tensor_web_op requires `name = \"PascalCase\"`", + ) + })?; + let variants = variants.unwrap_or_else(|| vec![VariantKind::Function, VariantKind::Method]); + if getter && setter { + return Err(syn::Error::new( + Span::call_site(), + "js_tensor_web_op: cannot set both getter and setter", + )); + } + Ok(Self { + name, + variants, + dtype_generic, + getter, + setter, + js_name, + target, + }) + } +} + +#[derive(Default, Clone)] +struct OpParamMeta { + default_expr: Option, + keyword: bool, + raw_js: bool, + ts_type_override: Option, + name: Option, +} + +fn parse_op_param_meta(attrs: &[Attribute]) -> SynResult { + let mut meta = OpParamMeta::default(); + for attr in attrs { + if attr.path().is_ident("op") { + attr.parse_nested_meta(|nested| { + if nested.path.is_ident("default") { + let value: Expr = nested.value()?.parse()?; + meta.default_expr = Some(value); + Ok(()) + } else if nested.path.is_ident("keyword") { + meta.keyword = true; + Ok(()) + } else if nested.path.is_ident("raw_js") { + meta.raw_js = true; + Ok(()) + } else if nested.path.is_ident("unchecked_type") || nested.path.is_ident("type") { + let lit: LitStr = nested.value()?.parse()?; + meta.ts_type_override = Some(lit.value()); + Ok(()) + } else if nested.path.is_ident("name") { + let lit: LitStr = nested.value()?.parse()?; + meta.name = Some(lit.value()); + Ok(()) + } else { + Err(nested.error("Unknown op(...) attribute key")) + } + })?; + } + } + Ok(meta) +} + +#[derive(Clone)] +struct ParamInfo { + ident: Ident, + ty: Type, + is_option: bool, + is_self_tensor: bool, + meta: OpParamMeta, +} + +fn is_type(ty: &Type, expected: &str) -> bool { + if let Type::Path(tp) = ty + && tp.qself.is_none() + && tp.path.segments.len() == 1 + { + return tp.path.segments[0].ident == expected; + } + false +} + +fn get_inner_type<'a>(ty: &'a Type, container: &str) -> Option<(&'a Type, &'a Type)> { + if let Type::Path(type_path) = ty + && let Some(segment) = type_path.path.segments.first() + && segment.ident == container + && let syn::PathArguments::AngleBracketed(angle_bracketed) = &segment.arguments + && let Some(syn::GenericArgument::Type(inner_ty)) = angle_bracketed.args.first() + { + return Some((inner_ty, ty)); + } + None +} + +fn is_optional_of_type(ty: &Type, expected: &str) -> bool { + if let Some((Type::Path(type_path), _)) = get_inner_type(ty, "Option") + && type_path.qself.is_none() + && type_path.path.segments.len() == 1 + { + return type_path.path.segments[0].ident == expected; + } + false +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum ParamKind { + Tensor, + VecTensor, + TensorOrScalar, + Shape, + ShapeWithOneHole, + Dims, + Dim, + NormOrd, + DType, + Device, + Bool, + Usize, + I32, + U32, + F32, + Unknown, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +struct KindMeta { + optional: bool, +} + +fn classify_param_kind(ty: &Type) -> (ParamKind, KindMeta) { + // Unwrap Option + if let Some((inner, _)) = get_inner_type(ty, "Option") { + let (kind, _meta) = classify_param_kind(inner); + return (kind, KindMeta { optional: true }); + } + // Vec + if let Some((inner, _)) = get_inner_type(ty, "Vec") + && is_type(inner, "Tensor") + { + return (ParamKind::VecTensor, KindMeta { optional: false }); + } + // Base kinds + let kind = if is_type(ty, "Tensor") { + ParamKind::Tensor + } else if is_type(ty, "TensorOrScalar") { + ParamKind::TensorOrScalar + } else if is_type(ty, "Shape") { + ParamKind::Shape + } else if is_type(ty, "ShapeWithOneHole") { + ParamKind::ShapeWithOneHole + } else if is_type(ty, "Dims") { + ParamKind::Dims + } else if is_type(ty, "Dim") { + ParamKind::Dim + } else if is_type(ty, "NormOrd") { + ParamKind::NormOrd + } else if is_type(ty, "DType") { + ParamKind::DType + } else if is_type(ty, "Device") { + ParamKind::Device + } else if is_type(ty, "bool") { + ParamKind::Bool + } else if is_type(ty, "usize") { + ParamKind::Usize + } else if is_type(ty, "i32") { + ParamKind::I32 + } else if is_type(ty, "u32") { + ParamKind::U32 + } else if is_type(ty, "f32") { + ParamKind::F32 + } else { + ParamKind::Unknown + }; + (kind, KindMeta { optional: false }) +} + +fn is_return_type_option(func: &ItemFn) -> bool { + // Returns (is_option, inner_is_js_tensor) + // Look at the function signature return type and detect if Ok type is Option or Option + use syn::ReturnType; + let mut ty_opt: Option<&Type> = None; + if let ReturnType::Type(_, ty) = &func.sig.output { + ty_opt = Some(ty); + } + if let Some(ty) = ty_opt { + // Unwrap Result -> Ok + let ok_ty = if let Some((inner, _)) = get_inner_type(ty, "Result") { + inner + } else { + ty + }; + // Check Option<...> + return is_optional_of_type(ok_ty, "JsTensor"); + } + false +} + +fn is_return_type_vec_tensor(func: &ItemFn) -> bool { + use syn::ReturnType; + let mut ty_opt: Option<&Type> = None; + if let ReturnType::Type(_, ty) = &func.sig.output { + ty_opt = Some(ty); + } + if let Some(ty) = ty_opt { + // Unwrap Result -> Ok + let ok_ty = if let Some((inner, _)) = get_inner_type(ty, "Result") { + inner + } else { + ty + }; + // Vec + if let Some((inner, _)) = get_inner_type(ok_ty, "Vec") { + return is_type(inner, "JsTensor") || is_type(inner, "Tensor"); + } + } + false +} + +fn param_unchecked_ts_type(ty: &Type, optional: bool) -> String { + let (kind, meta) = classify_param_kind(ty); + let is_optional = optional || meta.optional; + let mut s = match kind { + ParamKind::Tensor => "Tensor".to_string(), + ParamKind::VecTensor => "Tensor[]".to_string(), + ParamKind::TensorOrScalar => "Tensor | number".to_string(), + ParamKind::NormOrd => { + "'fro' | 'inf' | '-inf' | '0' | '1' | '-1' | '2' | number".to_string() + } + ParamKind::Shape => "Uint32Array | number[] | number".to_string(), + ParamKind::ShapeWithOneHole | ParamKind::Dims => { + "Int32Array | number[] | number".to_string() + } + ParamKind::Dim => "number".to_string(), + ParamKind::Device => "Device | 'cpu' | 'gpu' | 'webgpu'".to_string(), + ParamKind::DType => "DType".to_string(), + ParamKind::Bool => "boolean".to_string(), + ParamKind::Usize | ParamKind::I32 | ParamKind::U32 | ParamKind::F32 => "number".to_string(), + ParamKind::Unknown => "unknown".to_string(), + }; + if is_optional { + s.push_str(" | null | undefined"); + } + s +} + +fn options_field_ts_type_for_ty(ty: &Type) -> TokenStream2 { + let (kind, _meta) = classify_param_kind(ty); + match kind { + ParamKind::Tensor => quote! { crate::tensor::JsTensor }, + ParamKind::TensorOrScalar => quote! { crate::tensor::JsTensorOrScalar }, + ParamKind::Shape => quote! { Vec }, + ParamKind::NormOrd | ParamKind::ShapeWithOneHole | ParamKind::Dims | ParamKind::Dim => { + quote! { JsValue } + } + ParamKind::DType => quote! { crate::dtype::JsDType }, + ParamKind::Device => quote! { crate::device::JsDevice }, + _ => quote! { #ty }, + } +} + +fn make_param_info_list(func: &ItemFn) -> SynResult> { + let mut params = Vec::new(); + for (idx, arg) in func.sig.inputs.iter().enumerate() { + match arg { + FnArg::Receiver(_) => { + return Err(syn::Error::new_spanned( + arg, + "js_tensor_web_op must be used on a free function (no self)", + )); + } + FnArg::Typed(pt) => { + let ident = match &*pt.pat { + syn::Pat::Ident(pat_ident) => pat_ident.ident.clone(), + _ => format_ident!("arg{idx}"), + }; + let ty = (*pt.ty).clone(); + let meta = parse_op_param_meta(&pt.attrs)?; + let is_option = get_inner_type(&ty, "Option").is_some(); + // Heuristic: The first param is considered self in method variant generation. + let is_self_tensor = + idx == 0 && (is_type(&ty, "Tensor") || is_optional_of_type(&ty, "Tensor")); + params.push(ParamInfo { + ident, + ty, + is_option, + is_self_tensor, + meta, + }); + } + } + } + Ok(params) +} + +fn find_pack_start(params: &[ParamInfo]) -> Option { + let mut pack_start: Option = None; + for (i, p) in params.iter().enumerate() { + if p.meta.keyword && pack_start.is_none() { + pack_start = Some(i); + } + if p.is_option && pack_start.is_none() { + pack_start = Some(i); + } + if p.meta.default_expr.is_some() && pack_start.is_none() { + pack_start = Some(i); + } + } + pack_start +} + +fn last_param_is_tensor_options(params: &[ParamInfo]) -> bool { + if let Some(last) = params.last() { + return is_type(&last.ty, "TensorOptions"); + } + false +} + +fn build_options_struct( + op_name_pascal: &str, + params: &[ParamInfo], + pack_start: usize, + include_tensor_options: bool, +) -> TokenStream2 { + let options_ident = format_ident!("{}Options", op_name_pascal); + let mut fields_ts = Vec::::new(); + + for (idx, p) in params.iter().enumerate() { + if idx < pack_start { + continue; + } + // If trailing TensorOptions sentinel, we don't add it as raw field; we'll inject special fields later + if include_tensor_options && idx == params.len() - 1 && is_type(&p.ty, "TensorOptions") { + continue; + } + let name_ident = p.ident.clone(); + let name_str = name_ident.to_string(); + let camel = if let Some(ref custom) = p.meta.name { + custom.clone() + } else { + name_str.to_lower_camel_case() + }; + // Required fields remain non-Option; optional fields become Option<...> + let is_optional = p.is_option || p.meta.default_expr.is_some(); + // Avoid Option> in the generated options struct by mapping Option to T first + let ty_for_mapping: &Type = if p.is_option { + if let Some((inner, _)) = get_inner_type(&p.ty, "Option") { + inner + } else { + &p.ty + } + } else { + &p.ty + }; + let rust_field_ty = options_field_ts_type_for_ty(ty_for_mapping); + let field_ty_tokens = if is_optional && rust_field_ty.to_string() != "JsValue" { + quote! { Option<#rust_field_ty> } + } else { + quote! { #rust_field_ty } + }; + let mut attrs = Vec::::new(); + + // Rename if camelCase differs from snake_case + if camel != name_str { + let rename_lit = syn::LitStr::new(&camel, Span::call_site()); + attrs.push(quote! { #[serde(rename = #rename_lit)] }); + } + + // Override tsify(type=...) mapping if provided + let lit = if let Some(ref ts_override) = p.meta.ts_type_override { + syn::LitStr::new(ts_override, Span::call_site()) + } else { + // Provide a default tsify(type=...) mapping based on Rust type when it is a JsValue-backed field + let ts_str = param_unchecked_ts_type(&p.ty, p.is_option); + // We don't want to add redundant "| null | undefined" for required fields in struct typing + let ts_clean = if is_optional { + ts_str + } else { + // TODO(vinhowe): Not sure I love this + ts_str.replace(" | null | undefined", "") + }; + syn::LitStr::new(&ts_clean, Span::call_site()) + }; + + attrs.push(quote! { #[tsify(type = #lit)] }); + + // Determine serde `with` strategy for JsValue-backed fields. + // - For Tensor and TensorOrScalar (and their Option variants), use + // crate::js_util::try_from_js_value_preserve + // - For other JsValue-backed types (Dims, Dim, ShapeWithOneHole, NormOrd), use + // serde_wasm_bindgen::preserve + let is_tensor_like = is_type(&p.ty, "Tensor") + || is_optional_of_type(&p.ty, "Tensor") + || is_type(&p.ty, "TensorOrScalar") + || is_optional_of_type(&p.ty, "TensorOrScalar"); + + let is_other_jsvalue_field = is_type(&p.ty, "ShapeWithOneHole") + || is_optional_of_type(&p.ty, "ShapeWithOneHole") + || is_type(&p.ty, "Dims") + || is_optional_of_type(&p.ty, "Dims") + || is_type(&p.ty, "Dim") + || is_optional_of_type(&p.ty, "Dim") + || is_type(&p.ty, "NormOrd") + || is_optional_of_type(&p.ty, "NormOrd"); + + if is_optional { + attrs.push(quote! { #[tsify(optional)] }); + } + + let with_mod_lit: Option = if is_tensor_like { + Some(syn::LitStr::new( + "crate::js_util::try_from_js_value_preserve", + Span::call_site(), + )) + } else if is_other_jsvalue_field { + Some(syn::LitStr::new( + "serde_wasm_bindgen::preserve", + Span::call_site(), + )) + } else { + None + }; + + if let Some(with_mod) = with_mod_lit { + if is_optional { + attrs.push(quote! { #[serde(default, with = #with_mod)] }); + } else { + attrs.push(quote! { #[serde(with = #with_mod)] }); + } + } else if is_optional { + attrs.push(quote! { #[serde(default)] }); + } + + fields_ts.push(quote! { + #(#attrs)* + pub #name_ident: #field_ty_tokens + }); + } + + if include_tensor_options { + fields_ts.push(quote! { + #[serde(default, with = "crate::js_util::try_from_js_value_preserve")] + #[tsify(optional, type = "DType")] + pub dtype: Option + }); + fields_ts.push(quote! { + #[serde(default, with = "crate::js_util::try_from_js_value_preserve")] + #[tsify(optional, type = "Device")] + pub device: Option + }); + fields_ts.push(quote! { + #[serde(default, rename = "requiresGrad")] + #[tsify(optional)] + pub requires_grad: Option + }); + } + + quote! { + #[derive(tsify::Tsify, serde::Serialize, serde::Deserialize, Default)] + #[tsify(into_wasm_abi, from_wasm_abi)] + pub struct #options_ident { + #(#fields_ts,)* + } + } +} + +fn conversion_from_jsvalue( + ident: &Ident, + ty: &Type, + _has_self: bool, + fn_name: &str, +) -> (TokenStream2, TokenStream2) { + // Returns (prelude code, call expr) for building typed variable named ident + let (kind, meta) = classify_param_kind(ty); + match kind { + ParamKind::Tensor => { + if meta.optional { + ( + quote! { + let #ident: Option = if #ident.is_undefined() || #ident.is_null() { + None + } else { + Some(crate::tensor::JsTensor::try_from(#ident.clone())?.inner()) + }; + }, + quote! { #ident }, + ) + } else { + ( + quote! { let #ident: piston::Tensor = crate::tensor::JsTensor::try_from(#ident.clone())?.inner(); }, + quote! { #ident }, + ) + } + } + ParamKind::VecTensor => { + if meta.optional { + ( + quote! { + let #ident: Option> = if #ident.is_undefined() || #ident.is_null() { + None + } else if #ident.is_array() { + let array = #ident.dyn_into::().map_err(|_| wasm_bindgen::JsError::new("Expected an array of Tensors"))?; + Some(array + .iter() + .map(|v| crate::tensor::JsTensor::try_from(v.clone()).map(|t| t.inner())) + .collect::, wasm_bindgen::JsError>>()?) + } else { + return Err(wasm_bindgen::JsError::new("Expected an array of Tensors")); + }; + }, + quote! { #ident }, + ) + } else { + ( + quote! { + let #ident: Vec = if #ident.is_array() { + let array = #ident.dyn_into::().map_err(|_| wasm_bindgen::JsError::new("Expected an array of Tensors"))?; + array + .iter() + .map(|v| crate::tensor::JsTensor::try_from(v.clone()).map(|t| t.inner())) + .collect::, wasm_bindgen::JsError>>()? + } else { + return Err(wasm_bindgen::JsError::new("Expected an array of Tensors")); + }; + }, + quote! { #ident }, + ) + } + } + ParamKind::TensorOrScalar => ( + quote! { let #ident: crate::tensor::JsTensorOrScalar = crate::tensor::JsTensorOrScalar { inner: #ident.clone() }; }, + quote! { #ident.tensor_or_scalar().map_err(|e| e.into_js_error())? }, + ), + ParamKind::Shape => { + if meta.optional { + ( + quote! { + let #ident: Option = crate::shape::FromJsVecUsize::from_js_value(#ident.clone())?.map(|v| v.into()); + }, + quote! { #ident }, + ) + } else { + ( + quote! { + let #ident: piston::Shape = crate::shape::FromJsVecUsize::from_js_value(#ident.clone())?.ok_or(wasm_bindgen::JsError::new("Missing required dims"))?.into(); + }, + quote! { #ident }, + ) + } + } + ParamKind::ShapeWithOneHole | ParamKind::Dims => { + if meta.optional { + ( + quote! { let #ident: Option = crate::shape::FromJsVecISize::from_js_value(#ident.clone())?; }, + quote! { #ident }, + ) + } else { + ( + quote! { let #ident: crate::shape::FromJsVecISize = crate::shape::FromJsVecISize::from_js_value(#ident.clone())?.ok_or(wasm_bindgen::JsError::new("Missing required dims"))?; }, + quote! { #ident }, + ) + } + } + ParamKind::NormOrd => ( + quote! { let #ident: Option = crate::tensor::js_value_to_norm_ord(#ident.clone())?; }, + quote! { #ident }, + ), + ParamKind::Dim => ( + quote! { let #ident = crate::shape::FromJsDim(#ident.as_f64().ok_or(wasm_bindgen::JsError::new(format!("dim must be a number; got {:?} (in {})", #ident, #fn_name).as_str()))? as isize); }, + quote! { #ident }, + ), + ParamKind::Bool => ( + quote! { let #ident: bool = #ident.as_bool().unwrap_or(false); }, + quote! { #ident }, + ), + ParamKind::Usize => ( + quote! { let #ident: usize = #ident.as_f64().ok_or(wasm_bindgen::JsError::new("expected number"))? as usize; }, + quote! { #ident }, + ), + ParamKind::I32 => ( + quote! { let #ident: i32 = #ident.as_f64().ok_or(wasm_bindgen::JsError::new("expected number"))? as i32; }, + quote! { #ident }, + ), + ParamKind::U32 => ( + quote! { let #ident: u32 = #ident.as_f64().ok_or(wasm_bindgen::JsError::new("expected number"))? as u32; }, + quote! { #ident }, + ), + ParamKind::F32 => ( + quote! { let #ident: f32 = #ident.as_f64().ok_or(wasm_bindgen::JsError::new("expected number"))? as f32; }, + quote! { #ident }, + ), + ParamKind::DType => ( + quote! { let #ident: piston::DType = crate::dtype::JsDType::try_from(#ident.clone())?.dtype; }, + quote! { #ident }, + ), + ParamKind::Device => ( + quote! { let #ident: piston::Device = crate::device::JsDevice::try_from(#ident.clone())?.inner; }, + quote! { #ident }, + ), + ParamKind::Unknown => (quote! {}, quote! { #ident }), + } +} + +fn build_positional_param_defs( + params: &[ParamInfo], + pack_start: usize, + exported_param_idents: &[Ident], + is_method: bool, + fn_name: &str, +) -> (TokenStream2, Vec, Vec) { + // Returns (prelude, typed_arg_exprs, overloaded_js_args) + let mut prelude = Vec::::new(); + let mut typed_args = Vec::::new(); + let mut overloaded_js_args = Vec::::new(); + + let mut js_idx = if is_method { 1 } else { 0 }; + for (idx, p) in params.iter().enumerate() { + if idx >= pack_start { + break; + } + // For method variants, the first Rust param is the JS `input`/self and is handled separately. + if is_method && idx == 0 { + // Do not generate a typed `input` here to avoid shadowing the JS `input` &JsValue. + // Also, do not include it in typed positional args; the call uses `self_tensor` built later. + continue; + } + let js_ident = &exported_param_idents[js_idx]; + let sym_ident = format_ident!("{}_js", p.ident); + let ident = &p.ident; + prelude.push(quote! { let #sym_ident = #js_ident.clone(); }); + // Convert + let (conv_prelude, typed_call_expr) = + conversion_from_jsvalue(ident, &p.ty, is_method, fn_name); + // But conv_prelude uses the same ident; we need to shadow variable names accordingly. + // We'll create a new block to avoid collisions + prelude.push(quote! { let #ident = #sym_ident; }); + prelude.push(conv_prelude); + typed_args.push(typed_call_expr); + + // For overloaded args, include this positional arg + overloaded_js_args.push(quote! { &#sym_ident }); + js_idx += 1; + } + (quote! { #(#prelude)* }, typed_args, overloaded_js_args) +} + +fn build_options_parsing( + op_name_pascal: &str, + params: &[ParamInfo], + pack_start: usize, + include_tensor_options: bool, + _has_self: bool, +) -> (TokenStream2, Vec) { + let options_ident = format_ident!("{}Options", op_name_pascal); + if pack_start >= params.len() && !include_tensor_options { + return (quote! {}, Vec::new()); + } + let opts_ident = format_ident!("opts"); + let mut prelude = Vec::::new(); + prelude.push(quote! { let #opts_ident: #options_ident = serde_wasm_bindgen::from_value(options.clone()).unwrap_or_default(); }); + + let mut typed_fields = Vec::::new(); + // For each packed param, generate a local variable with the typed value (respecting defaults) + for (idx, p) in params.iter().enumerate() { + if idx < pack_start { + continue; + } + if include_tensor_options && idx == params.len() - 1 && is_type(&p.ty, "TensorOptions") { + continue; + } + let field_ident = &p.ident; + let default_expr = p.meta.default_expr.as_ref().map(|e| quote! { #e }); + let (kind, _meta) = classify_param_kind(&p.ty); + + if p.meta.raw_js { + prelude.push(quote! { + let #field_ident: wasm_bindgen::JsValue = crate::js_util::to_option(#opts_ident.#field_ident.clone()).unwrap_or(wasm_bindgen::JsValue::UNDEFINED); + }); + typed_fields.push(quote! { #field_ident }); + continue; + } + + match kind { + ParamKind::Tensor => { + if p.is_option { + prelude.push(quote! { let #field_ident: Option = #opts_ident.#field_ident.map(|tensor| tensor.inner()); }); + } else { + prelude.push(quote! { let #field_ident: piston::Tensor = #opts_ident.#field_ident.inner(); }); + } + typed_fields.push(quote! { #field_ident }); + } + ParamKind::TensorOrScalar => { + if p.is_option { + prelude.push(quote! { + let #field_ident: Option = #opts_ident.#field_ident.clone(); + }); + typed_fields.push(quote! { + #field_ident + .map(|v| v.tensor_or_scalar()) + .transpose() + .map_err(|e| e.into_js_error())? + }); + } else { + prelude.push(quote! { + let __js = crate::js_util::to_option(#opts_ident.#field_ident.clone()).unwrap_or(wasm_bindgen::JsValue::UNDEFINED); + let #field_ident: crate::tensor::JsTensorOrScalar = crate::tensor::JsTensorOrScalar { inner: __js }; + }); + typed_fields.push( + quote! { #field_ident.tensor_or_scalar().map_err(|e| e.into_js_error())? }, + ); + } + } + ParamKind::Shape => { + if p.is_option || p.meta.default_expr.is_some() { + prelude.push(quote! { let #field_ident: Vec = #opts_ident.#field_ident.unwrap_or_default(); }); + } else { + prelude + .push(quote! { let #field_ident: Vec = #opts_ident.#field_ident; }); + } + typed_fields.push(quote! { #field_ident }); + } + ParamKind::ShapeWithOneHole => { + prelude.push(quote! { + let __js = crate::js_util::to_option(#opts_ident.#field_ident.clone()).unwrap_or(wasm_bindgen::JsValue::UNDEFINED); + let #field_ident: crate::shape::FromJsVecISize = crate::shape::FromJsVecISize::from_js_value(__js)?.ok_or(wasm_bindgen::JsError::new("Missing required dims"))?; + }); + typed_fields.push(quote! { #field_ident }); + } + ParamKind::Dims => { + if let Some(def) = default_expr { + prelude.push(quote! { + let __js = #opts_ident.#field_ident.clone(); + let #field_ident: crate::shape::FromJsVecISize = match crate::shape::FromJsVecISize::from_js_value(__js) { + Ok(Some(v)) => v, + _ => { #def } + }; + }); + } else if p.is_option { + prelude.push(quote! { + let __js = #opts_ident.#field_ident.clone(); + let #field_ident: Option = crate::shape::FromJsVecISize::from_js_value(__js)?; + }); + } else { + prelude.push(quote! { + let __js = #opts_ident.#field_ident.clone(); + let #field_ident: crate::shape::FromJsVecISize = crate::shape::FromJsVecISize::from_js_value(__js)?.ok_or(wasm_bindgen::JsError::new("Missing required dims"))?; + }); + } + typed_fields.push(quote! { #field_ident }); + } + ParamKind::NormOrd => { + prelude.push(quote! { + let __js = crate::js_util::to_option(#opts_ident.#field_ident.clone()).unwrap_or(wasm_bindgen::JsValue::UNDEFINED); + let #field_ident: Option = crate::tensor::js_value_to_norm_ord(__js)?; + }); + typed_fields.push(quote! { #field_ident }); + } + ParamKind::Dim => { + if let Some(def) = default_expr { + prelude.push(quote! { + let __js = #opts_ident.#field_ident.clone(); + let #field_ident = crate::shape::FromJsDim( + crate::js_util::to_option(__js) + .and_then(|v| v.as_f64()) + .unwrap_or(#def as f64) as isize, + ); + }); + } else if p.is_option { + prelude.push(quote! { + let __js = #opts_ident.#field_ident.clone(); + let #field_ident: Option = crate::js_util::to_option(__js) + .and_then(|v| v.as_f64()) + .map(|n| crate::shape::FromJsDim(n as isize)); + }); + let default = quote! { crate::shape::FromJsDim(0) }; + typed_fields.push(quote! { #field_ident.unwrap_or(#default) }); + continue; + } else { + prelude.push(quote! { + let __js = #opts_ident.#field_ident.clone(); + let #field_ident = crate::shape::FromJsDim( + crate::js_util::to_option(__js) + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) as isize, + ); + }); + } + typed_fields.push(quote! { #field_ident }); + } + ParamKind::Bool + | ParamKind::Usize + | ParamKind::I32 + | ParamKind::U32 + | ParamKind::F32 => { + let ty = &p.ty; + if p.is_option { + prelude.push(quote! { let #field_ident: #ty = #opts_ident.#field_ident; }); + } else if let Some(def) = default_expr { + prelude.push(quote! { let #field_ident: #ty = #opts_ident.#field_ident.unwrap_or(#def); }); + } else { + prelude.push(quote! { let #field_ident: #ty = #opts_ident.#field_ident; }); + } + typed_fields.push(quote! { #field_ident }); + } + ParamKind::DType => { + // In options we keep JsDType and convert where needed in call site; here pass through + let ty = &p.ty; + prelude.push(quote! { let #field_ident: #ty = #opts_ident.#field_ident; }); + typed_fields.push(quote! { #field_ident }); + } + ParamKind::Device => { + let ty = &p.ty; + prelude.push(quote! { let #field_ident: #ty = #opts_ident.#field_ident; }); + typed_fields.push(quote! { #field_ident }); + } + ParamKind::Unknown => { + let rhs = if let Some(def) = default_expr { + quote! { #opts_ident.#field_ident.unwrap_or(#def) } + } else { + quote! { #opts_ident.#field_ident.unwrap_or_default() } + }; + prelude.push(quote! { let #field_ident = #rhs; }); + typed_fields.push(quote! { #field_ident }); + } + ParamKind::VecTensor => { + // Options struct stores Vec or Option<...>? We expect Vec requested at call site + prelude.push(quote! { + let #field_ident: Vec = if #opts_ident.#field_ident.is_array() { + let array = #opts_ident.#field_ident.dyn_into::().map_err(|_| wasm_bindgen::JsError::new("Expected an array of Tensors"))?; + array + .iter() + .map(|v| crate::tensor::JsTensor::try_from(v.clone()).map(|t| t.inner())) + .collect::, wasm_bindgen::JsError>>()? + } else { + return Err(wasm_bindgen::JsError::new("Expected an array of Tensors")); + }; + }); + typed_fields.push(quote! { #field_ident }); + } + } + } + + // TensorOptions sentinel + if include_tensor_options { + prelude.push(quote! { + let options: piston::TensorOptions = piston::TensorOptions { + dtype: #opts_ident.dtype.map(|d| d.dtype), + device: #opts_ident.device.map(|d| d.inner), + requires_grad: #opts_ident.requires_grad, + }; + }); + typed_fields.push(quote! { options }); + } + + (quote! { #(#prelude)* }, typed_fields) +} + +fn build_js_param_list( + params: &[ParamInfo], + pack_start: usize, + variant: VariantKind, + has_options: bool, +) -> Vec { + let mut js_param_idents = Vec::::new(); + + if matches!(variant, VariantKind::Method | VariantKind::MethodInplace) { + // Expose the first Rust param (self) as `input` in JS for better ergonomics + let name = format_ident!("input"); + js_param_idents.push(name); + } + + let is_method = matches!(variant, VariantKind::Method | VariantKind::MethodInplace); + let last_is_tensor_options = params + .last() + .map(|p| is_type(&p.ty, "TensorOptions")) + .unwrap_or(false) + && has_options; + for (idx, p) in params.iter().enumerate() { + if idx >= pack_start { + break; + } + // For methods, skip the first Rust parameter (it becomes `self_js`) + if is_method && idx == 0 { + continue; + } + // Skip trailing TensorOptions sentinel from positional list; it will be represented by the generated Options object param + if last_is_tensor_options && idx == params.len() - 1 { + continue; + } + js_param_idents.push(p.ident.clone()); + } + + if has_options { + let name = format_ident!("options"); + // The caller will append correct attribute for options param since it needs op_name_pascal. + js_param_idents.push(name); + } + js_param_idents +} + +pub fn process_js_tensor_web_op(attr: JsTensorWebOpAttr, item: ItemFn) -> SynResult { + // Extract simple info + let op_rust_name = item.sig.ident.clone(); + let op_name_pascal = attr.name.clone(); + // Determine target (alternative external name) if provided + let target_name_ident = attr + .target + .as_ref() + .map(|s| format_ident!("{}", s)) + .unwrap_or_else(|| op_rust_name.clone()); + let target_camel = to_lower_camel_case_with_underscore(&target_name_ident.to_string()); + let target_pascal = target_name_ident.to_string().to_upper_camel_case(); + let params = make_param_info_list(&item)?; + let pack_start = find_pack_start(¶ms).unwrap_or(params.len()); + let include_tensor_options = last_param_is_tensor_options(¶ms); + let has_options = pack_start < params.len() || include_tensor_options; + let is_async = item.sig.asyncness.is_some(); + let async_token = if is_async { + quote! { async } + } else { + quote! {} + }; + let output_is_option = is_return_type_option(&item); + let output_is_vec_tensor = is_return_type_vec_tensor(&item); + let unchecked_ret_ts = if output_is_option { + syn::LitStr::new("Tensor | undefined", Span::call_site()) + } else if output_is_vec_tensor { + syn::LitStr::new("Tensor[]", Span::call_site()) + } else { + syn::LitStr::new("Tensor", Span::call_site()) + }; + + // Build Options struct + let options_struct_tokens = if has_options { + build_options_struct(&op_name_pascal, ¶ms, pack_start, include_tensor_options) + } else { + quote! {} + }; + + let mut exported_fns = Vec::::new(); + let mut wrapper_methods = Vec::::new(); + for variant in attr.variants.iter().copied() { + let rust_pascal = op_rust_name.to_string().to_upper_camel_case(); + let export_js_name = match variant { + VariantKind::Function => variant.as_js_export_name(&target_pascal), + VariantKind::Method => format!("opMethod{rust_pascal}"), + VariantKind::MethodInplace => format!("opMethod{rust_pascal}_"), + }; + let is_method = matches!(variant, VariantKind::Method | VariantKind::MethodInplace); + let is_inplace = matches!(variant, VariantKind::MethodInplace); + + // JS parameter list and attributes (skip the first param for method variants, becomes self) + let js_param_idents = build_js_param_list(¶ms, pack_start, variant, has_options); + + // Early function-mode dispatch setup + // Build overloaded args slice: positional js args after first (for method) or all (for function), excluding options + let mut overloaded_js_args = Vec::::new(); + if !params.is_empty() { + // For methods, skip self_js; for function, skip the first param if it's an input tensor + let first_is_tensor = params + .first() + .map(|p| is_type(&p.ty, "Tensor") || is_optional_of_type(&p.ty, "Tensor")) + .unwrap_or(false); + let start_index = if is_method || first_is_tensor { 1 } else { 0 }; + let end_index = js_param_idents.len() - if has_options { 1 } else { 0 }; + js_param_idents + .iter() + .skip(start_index) + .take(end_index) + .for_each(|id| { + overloaded_js_args.push(quote! { &#id }); + }); + } + // Build local arrays for overloaded args and positional args to avoid borrowing temporaries + let mut dispatch_prelude = Vec::::new(); + let overloaded_slice_ident = format_ident!("__overloaded_slice"); + if overloaded_js_args.is_empty() { + dispatch_prelude + .push(quote! { let #overloaded_slice_ident: [&wasm_bindgen::JsValue; 0] = []; }); + } else { + let len = overloaded_js_args.len(); + dispatch_prelude.push(quote! { let #overloaded_slice_ident: [&wasm_bindgen::JsValue; #len] = [ #(#overloaded_js_args),* ]; }); + } + + // When TensorOptions sentinel is present, exclude it from positional args + let pack_start_positional = if include_tensor_options { + pack_start.min(params.len().saturating_sub(1)) + } else { + pack_start + }; + + let pre_pack_count = pack_start_positional.min(params.len()); + let args_items: Vec = if is_method { + (1..(pre_pack_count)) + .map(|i| { + let id = &js_param_idents[i]; + quote! { &#id } + }) + .collect() + } else { + (0..pre_pack_count) + .map(|i| { + let id = &js_param_idents[i]; + quote! { &#id } + }) + .collect() + }; + let args_array_ident = format_ident!("__positional_args"); + if args_items.is_empty() { + dispatch_prelude + .push(quote! { let #args_array_ident: [&wasm_bindgen::JsValue; 0] = []; }); + } else { + let k = args_items.len(); + dispatch_prelude.push(quote! { let #args_array_ident: [&wasm_bindgen::JsValue; #k] = [ #(#args_items),* ]; }); + } + + // Function-mode call + // For handle_piston_function `function_name`: + // - Methods: use the exported method name (e.g., opMethodRequiresGrad or opMethodRequiresGrad_) + // - Functions: use the exported function name (lowerCamelCase op name) + let function_name_str = &export_js_name; + let named_arg_tokens: TokenStream2 = if has_options { + quote! { options } + } else { + quote! { &wasm_bindgen::JsValue::UNDEFINED } + }; + + let self_arg = if is_method { + quote! { Some(&input) } + } else { + quote! { None } + }; + + let handle_call = quote! { + #(#dispatch_prelude)* + let overloaded_args = crate::function::get_overloaded_args(&#overloaded_slice_ident); + if !overloaded_args.is_empty() || crate::function::is_function_mode_active() { + return crate::function::handle_piston_function( + #self_arg, + #function_name_str, + &overloaded_args, + &#args_array_ident, + #named_arg_tokens, + ); + } + }; + + // Build conversion prelude and call + let (pos_prelude, typed_positional_args, _ol_js) = build_positional_param_defs( + ¶ms, + pack_start_positional, + &js_param_idents, + is_method, + function_name_str, + ); + let (opts_prelude, typed_options_args) = build_options_parsing( + &op_name_pascal, + ¶ms, + pack_start, + include_tensor_options, + is_method, + ); + + let mut typed_args_for_call = Vec::::new(); + if is_method { + typed_args_for_call.extend(typed_positional_args.clone()); + } else if typed_positional_args.len() > 1 { + typed_args_for_call.extend(typed_positional_args.into_iter().skip(1)); + } + typed_args_for_call.extend(typed_options_args.clone()); + + let invocation_tokens = if item.block.stmts.is_empty() { + // Direct call to core op (method on Tensor) + let base_ident = target_name_ident.clone(); + let method_ident = if is_inplace { + format_ident!("{}_", base_ident) + } else { + base_ident + }; + + let first_ident = if is_method { + syn::Ident::new("input", Span::call_site()) + } else { + params + .first() + .map(|p| p.ident.clone()) + .unwrap_or_else(|| format_ident!("self")) + }; + + quote! { + #first_ident.#method_ident( #(#typed_args_for_call),* ) + } + } else { + // Custom body: inline the user's function body with typed params + let user_block = &item.block; + quote! { + #user_block + } + }; + + let call_result_binding = if output_is_option { + quote! { + let result_opt = #invocation_tokens; + Ok(result_opt + .map(|js_t| js_t.into()) + .unwrap_or(wasm_bindgen::JsValue::UNDEFINED)) + } + } else if output_is_vec_tensor { + quote! { + let result = #invocation_tokens.map_err(|e| e.into_js_error())?; + let array = js_sys::Array::new(); + for t in result.into_iter() { + array.push(&crate::tensor::JsTensor::new(t).into()); + } + Ok(array.into()) + } + } else { + quote! { + let result = #invocation_tokens.map_err(|e| e.into_js_error())?; + Ok(crate::tensor::JsTensor::new(result).into()) + } + }; + + let call_tokens = if is_method { + quote! { + let input = crate::tensor::JsTensor::try_from(input.clone())?._clone_weak_js().inner(); + #call_result_binding + } + } else { + call_result_binding + }; + + // Build full exported function + let export_ident = format_ident!("{}", export_js_name); + // Compute js_name to expose. Allow override for methods; append '_' for inplace when overridden + let js_name_effective = if is_method { + // For method exports, keep opMethod unchanged regardless of target + export_js_name.clone() + } else { + // For function exports, if target provided, use it exactly; else use the default + if let Some(ref tgt) = attr.target { + tgt.clone() + } else { + export_js_name.clone() + } + }; + let js_name_lit = syn::LitStr::new(&js_name_effective, Span::call_site()); + let mut param_decls = Vec::::new(); + let last_is_tensor_options = params + .last() + .map(|p| is_type(&p.ty, "TensorOptions")) + .unwrap_or(false) + && has_options; + for (i, ident) in js_param_idents.iter().enumerate() { + // Attach attributes + if matches!(variant, VariantKind::Method | VariantKind::MethodInplace) && i == 0 { + param_decls.push(quote! { #[wasm_bindgen(unchecked_param_type = "Tensor")] #ident: &wasm_bindgen::JsValue }); + } else if has_options && i == js_param_idents.len() - 1 { + // We need the type name string, not ident token + let ts = syn::LitStr::new(&format!("{op_name_pascal}Options?"), Span::call_site()); + param_decls.push(quote! { #[wasm_bindgen(unchecked_param_type = #ts)] #ident: &wasm_bindgen::JsValue }); + } else { + let mut param_idx = i; + if last_is_tensor_options && param_idx >= params.len() - 1 { + param_idx = params.len() - 2; + } + let p = ¶ms[param_idx]; + let name_str = p.ident.to_string(); + let camel = if let Some(ref custom) = p.meta.name { + custom.clone() + } else { + name_str.to_lower_camel_case() + }; + let mut attrs_tokens: Vec = Vec::new(); + // js_name override when camelCase differs + if camel != name_str { + let jsname_lit = syn::LitStr::new(&camel, Span::call_site()); + attrs_tokens.push(quote! { #[wasm_bindgen(js_name = #jsname_lit)] }); + } + // unchecked type override or derived + if let Some(ref ts_override) = p.meta.ts_type_override { + let lit = syn::LitStr::new(ts_override, Span::call_site()); + attrs_tokens.push(quote! { #[wasm_bindgen(unchecked_param_type = #lit)] }); + } else { + let ts = param_unchecked_ts_type(&p.ty, p.is_option); + let ts_lit = syn::LitStr::new(&ts, Span::call_site()); + attrs_tokens.push(quote! { #[wasm_bindgen(unchecked_param_type = #ts_lit)] }); + } + param_decls.push(quote! { #(#attrs_tokens)* #ident: &wasm_bindgen::JsValue }); + } + } + + let wasm_fn = { + quote! { + #[wasm_bindgen(js_name = #js_name_lit, unchecked_return_type = #unchecked_ret_ts)] + pub #async_token fn #export_ident( #(#param_decls),* ) -> Result { + #handle_call + #pos_prelude + #opts_prelude + #call_tokens + } + } + }; + + exported_fns.push(wasm_fn); + + // Generate instance method passthrough on JsTensor for method variants + if is_method { + // Build wrapper param list: skip self/input, keep other pre-pack params and options if present + let mut wrapper_param_decls = Vec::::new(); + // Determine positional count like earlier + let pack_start_positional = if include_tensor_options { + pack_start.min(params.len().saturating_sub(1)) + } else { + pack_start + }; + let pre_pack_count = pack_start_positional.min(params.len()); + params.iter().take(pre_pack_count).skip(1).for_each(|p| { + let name = p.ident.clone(); + // camelCase rename for param if different + let name_str = name.to_string(); + let camel = if let Some(ref custom) = p.meta.name { + custom.clone() + } else { + name_str.to_lower_camel_case() + }; + let mut attrs = Vec::::new(); + if camel != name_str { + let jsname_lit = syn::LitStr::new(&camel, Span::call_site()); + attrs.push(quote! { #[wasm_bindgen(js_name = #jsname_lit)] }); + } + // type attr + let ts_lit = if let Some(ref ts_override) = p.meta.ts_type_override { + syn::LitStr::new(ts_override, Span::call_site()) + } else { + let ts = param_unchecked_ts_type(&p.ty, p.is_option); + syn::LitStr::new(&ts, Span::call_site()) + }; + attrs.push(quote! { #[wasm_bindgen(unchecked_param_type = #ts_lit)] }); + wrapper_param_decls.push(quote! { #(#attrs)* #name: &wasm_bindgen::JsValue }); + }); + if has_options { + let ts = syn::LitStr::new(&format!("{op_name_pascal}Options?"), Span::call_site()); + wrapper_param_decls.push(quote! { #[wasm_bindgen(unchecked_param_type = #ts)] options: &wasm_bindgen::JsValue }); + } + + // Compute JS method name + // If a target is provided, use it EXACTLY (preserve case) for JS name; otherwise use lowerCamelCase of the Rust name. + let base_js_method_name = if let Some(ref tgt) = attr.target { + tgt.clone() + } else if let Some(ref js_name) = attr.js_name { + js_name.clone() + } else { + target_camel.clone() + }; + let mut js_method_name = base_js_method_name.clone(); + if is_inplace && !js_method_name.ends_with('_') { + js_method_name.push('_'); + } + let js_method_name_lit = syn::LitStr::new(&js_method_name, Span::call_site()); + + // Choose attribute key: js_name (normal) or getter/setter when set + let method_name_key = syn::Ident::new( + if attr.getter { + "getter" + } else if attr.setter { + "setter" + } else { + "js_name" + }, + Span::call_site(), + ); + let method_name_attr = quote! { #[wasm_bindgen(#method_name_key = #js_method_name_lit, unchecked_return_type = #unchecked_ret_ts)] }; + + // Build call to exported free function + let free_fn_ident = format_ident!("{}", export_js_name); + let mut call_args: Vec = Vec::new(); + call_args.push(quote! { &input_js }); + params.iter().take(pre_pack_count).skip(1).for_each(|p| { + let id = &p.ident; + call_args.push(quote! { #id }); + }); + if has_options { + call_args.push(quote! { options }); + } + + let maybe_async_await = if is_async { + quote! { .await } + } else { + quote! {} + }; + let call_tokens = quote! { #free_fn_ident( #(#call_args),* ) #maybe_async_await }; + + // Name the Rust method after the original Rust function ident to avoid collisions (e.g., `T_upper`), + // and append '_' for inplace variants. + let wrapper_fn_ident = if is_inplace { + format_ident!("{}_", op_rust_name) + } else { + op_rust_name.clone() + }; + wrapper_methods.push(quote! { + #[wasm_bindgen(js_class = Tensor)] + impl JsTensor { + #method_name_attr + #[allow(non_snake_case)] + pub #async_token fn #wrapper_fn_ident(&self, #(#wrapper_param_decls),*) -> Result { + let input_js = self.js_value(); + #call_tokens + } + } + }); + } + } + + // Do not re-emit the original function. Its signature and optional custom body are + // incorporated into the generated exports. Re-emitting would leak internal attrs (e.g. #[op]). + let tokens = quote! { + #options_struct_tokens + #(#exported_fns)* + #(#wrapper_methods)* + }; + Ok(tokens) +} + +#[cfg(test)] +mod tests { + use super::*; + use quote::quote; + + #[test] + fn parses_attr_with_variants() { + let attr: JsTensorWebOpAttr = syn::parse_quote!( + name = "Addcdiv", + variants = [function, method, method_inplace] + ); + assert_eq!(attr.name, "Addcdiv"); + assert_eq!(attr.variants.len(), 3); + } + + #[test] + fn generates_options_struct_and_exports() { + let item: ItemFn = syn::parse2(quote! { + pub fn addcdiv(input: Tensor, tensor1: Tensor, tensor2: Tensor, #[op(default = 1.0)] value: f32) -> Result {} + }).unwrap(); + let attr: JsTensorWebOpAttr = syn::parse_quote!( + name = "Addcdiv", + variants = [function, method, method_inplace] + ); + let out = process_js_tensor_web_op(attr, item).unwrap(); + let s = out.to_string(); + assert!(s.contains("struct AddcdivOptions")); + assert!(s.contains("_opFnAddcdiv")); + assert!(s.contains("opMethodAddcdiv")); + assert!(s.contains("opMethodAddcdiv_")); + assert!(s.contains("unchecked_return_type = \"Tensor\"")); + } + + #[test] + fn packs_tensor_options_fields() { + let item: ItemFn = syn::parse2(quote! { + pub fn arange(#[op(keyword)] end: f32, options: TensorOptions) -> Result {} + }) + .unwrap(); + let attr: JsTensorWebOpAttr = syn::parse_quote!(name = "Arange", variants = [function]); + let out = process_js_tensor_web_op(attr, item).unwrap(); + let s = out.to_string(); + assert!(s.contains("struct ArangeOptions")); + assert!(s.contains("requiresGrad")); + assert!(s.contains("JsDType")); + assert!(s.contains("JsDevice")); + } +} diff --git a/crates/piston-macros/src/lib.rs b/crates/piston-macros/src/lib.rs new file mode 100644 index 00000000..c5962828 --- /dev/null +++ b/crates/piston-macros/src/lib.rs @@ -0,0 +1,73 @@ +mod ir_fields; +mod js_tensor_web_op; +mod ops; +mod scoped_module; +mod wgsl_metadata; + +use proc_macro::TokenStream; +use syn::parse_macro_input; + +/// Derives the `OpMetadata` trait implementation for a struct. +/// +/// Generates a `.render()` method that converts a Rust struct into a WGSL struct. +#[proc_macro_derive(WgslMetadata, attributes(builder))] +pub fn derive_wgsl_metadata(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input); + wgsl_metadata::derive(input).into() +} + +/// Derives the `IrFields` trait implementation for a struct. +/// +/// Generates a `.ir_fields()` method we use for hashing compute graphs. +#[proc_macro_derive(IrFields, attributes(builder))] +pub fn derive_ir_fields(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input); + ir_fields::derive(input).into() +} + +/// Derives the `ScopedModule` trait implementation for a struct. +/// +/// Automatically adds scoping to Module implementations +#[proc_macro_attribute] +pub fn scoped_module(_attr: TokenStream, item: TokenStream) -> TokenStream { + scoped_module::scoped_module(item) +} + +/// Generates tensor operation variants from an OpTensor kernel function. +/// +/// This macro takes a function that operates on OpTensor and generates: +/// - A kernel function with OT generics for Into parameters +/// - Optional function, method, and method_inplace variants +/// +/// # Example +/// ```rust +/// #[tensor_op(variants = [function, method, method_inplace])] +/// fn add>(lhs: OpTensor, rhs: T) -> Result { +/// // implementation +/// } +/// ``` +#[proc_macro_attribute] +pub fn tensor_op(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = parse_macro_input!(attr as ops::TensorOpAttr); + let item = parse_macro_input!(item as syn::ItemFn); + + match ops::process_tensor_op(attr, item) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } +} + +/// Generates wasm-bindgen-exposed functions for Tensor ops in the web crate. +/// +/// See `docs/js_tensor_web_op.md` for detailed behavior. This macro is attached to a +/// typed Rust function and generates free-function exports: function/method/method_inplace. +#[proc_macro_attribute] +pub fn js_tensor_web_op(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = parse_macro_input!(attr as js_tensor_web_op::JsTensorWebOpAttr); + let item = parse_macro_input!(item as syn::ItemFn); + + match js_tensor_web_op::process_js_tensor_web_op(attr, item) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } +} diff --git a/crates/piston-macros/src/ops.rs b/crates/piston-macros/src/ops.rs new file mode 100644 index 00000000..05f7fd76 --- /dev/null +++ b/crates/piston-macros/src/ops.rs @@ -0,0 +1,1618 @@ +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{ToTokens, quote}; +use std::collections::HashMap; +use syn::{ + Attribute, Error, Expr, FnArg, GenericParam, Ident, ItemFn, Pat, PatIdent, PatType, Path, + Result, ReturnType, Token, Type, + parse::{Parse, ParseStream}, + punctuated::Punctuated, +}; + +/// Represents the variants that can be generated from a tensor operation +#[derive(Debug, Clone, PartialEq)] +pub enum TensorOpVariant { + Function, + Method, + MethodInplace, +} + +impl Parse for TensorOpVariant { + fn parse(input: ParseStream) -> Result { + let ident: Ident = input.parse()?; + match ident.to_string().as_str() { + "function" => Ok(TensorOpVariant::Function), + "method" => Ok(TensorOpVariant::Method), + "method_inplace" => Ok(TensorOpVariant::MethodInplace), + _ => Err(Error::new_spanned( + ident, + "Expected 'function', 'method', or 'method_inplace'", + )), + } + } +} + +/// Represents a default parameter assignment +#[derive(Clone)] +pub struct DefaultParam { + pub name: String, + pub value: Expr, +} + +impl std::fmt::Debug for DefaultParam { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DefaultParam") + .field("name", &self.name) + .field("value", &"") + .finish() + } +} + +/// Represents the parsed contents of a #[tensor_op(...)] attribute +#[derive(Debug, Clone)] +pub struct TensorOpAttr { + pub variants: Vec, +} + +impl Parse for TensorOpAttr { + fn parse(input: ParseStream) -> Result { + let mut variants = Vec::new(); + + if input.is_empty() { + // Default variants if none specified + return Ok(TensorOpAttr { + variants: vec![TensorOpVariant::Function, TensorOpVariant::Method], + }); + } + + while !input.is_empty() { + let ident: Ident = input.parse()?; + match ident.to_string().as_str() { + "variants" => { + let _eq: Token![=] = input.parse()?; + let content; + syn::bracketed!(content in input); + let variant_list: Punctuated = + content.parse_terminated(TensorOpVariant::parse, Token![,])?; + variants.extend(variant_list); + } + _ => return Err(Error::new_spanned(ident, "Unknown tensor_op attribute")), + } + + if !input.is_empty() { + let _comma: Token![,] = input.parse()?; + } + } + + if variants.is_empty() { + variants = vec![TensorOpVariant::Function, TensorOpVariant::Method]; + } + + Ok(TensorOpAttr { variants }) + } +} + +/// Represents a function parameter with its default value if any +#[derive(Clone)] +pub struct ParamInfo { + pub name: String, + pub pat_type: PatType, + pub default: Option, + pub is_op_tensor: bool, + pub mentions_op_tensor: bool, +} + +impl std::fmt::Debug for ParamInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ParamInfo") + .field("name", &self.name) + .field("pat_type", &"") + .field("default", &self.default.as_ref().map(|_| "")) + .field("is_op_tensor", &self.is_op_tensor) + .field("mentions_op_tensor", &self.mentions_op_tensor) + .finish() + } +} + +/// Generic allocator for OT1, OT2, etc. +struct OtGen { + counter: usize, +} + +impl OtGen { + fn new() -> Self { + Self { counter: 1 } + } + + fn next(&mut self) -> Ident { + let ident = Ident::new(&format!("OT{}", self.counter), Span::call_site()); + self.counter += 1; + ident + } +} + +/// Check if a path ends with "OpTensor" +fn is_op_tensor_path(path: &Path) -> bool { + path.segments + .last() + .map(|seg| seg.ident == "OpTensor") + .unwrap_or(false) +} + +/// Check if a type mentions OpTensor anywhere +fn mentions_op_tensor(ty: &Type) -> bool { + match ty { + Type::Path(type_path) => { + if is_op_tensor_path(&type_path.path) { + return true; + } + // Check in generic arguments + for segment in &type_path.path.segments { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + for arg in &args.args { + if let syn::GenericArgument::Type(inner_ty) = arg + && mentions_op_tensor(inner_ty) + { + return true; + } + } + } + } + false + } + Type::Reference(type_ref) => mentions_op_tensor(&type_ref.elem), + Type::Tuple(type_tuple) => type_tuple.elems.iter().any(mentions_op_tensor), + _ => false, + } +} + +/// Replace OpTensor with a new type in a Type +fn replace_op_tensor_in_type(ty: &mut Type, replacement: &Path) { + match ty { + Type::Path(type_path) => { + if is_op_tensor_path(&type_path.path) { + type_path.path = replacement.clone(); + } else { + // Check in generic arguments + for segment in &mut type_path.path.segments { + if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments { + for arg in &mut args.args { + if let syn::GenericArgument::Type(inner_ty) = arg { + replace_op_tensor_in_type(inner_ty, replacement); + } + } + } + } + } + } + Type::Reference(type_ref) => { + replace_op_tensor_in_type(&mut type_ref.elem, replacement); + } + Type::Tuple(type_tuple) => { + for elem in &mut type_tuple.elems { + replace_op_tensor_in_type(elem, replacement); + } + } + _ => {} + } +} + +/// Replace OpTensor with Tensor or Self in a Type +fn replace_op_tensor_with_tensor(ty: &mut Type, use_self: bool) { + let replacement = if use_self { + syn::parse_quote!(Self) + } else { + syn::parse_quote!(Tensor) + }; + replace_op_tensor_in_type(ty, &replacement); +} + +/// Replace OpTensor in a Type using the constraint_ot_map to find the right replacement +/// The current_generic parameter tells us which generic constraint we're currently processing +fn replace_op_tensor_in_type_for_constraints( + ty: &mut Type, + constraint_ot_map: &HashMap, + current_generic: &str, +) { + match ty { + Type::Path(type_path) => { + if is_op_tensor_path(&type_path.path) { + // Replace OpTensor with the OT generic assigned to this constraint + if let Some(ot_ident) = constraint_ot_map.get(current_generic) { + type_path.path = syn::parse_quote!(#ot_ident); + } + } else { + replace_op_tensor_in_path_for_constraints( + &mut type_path.path, + constraint_ot_map, + current_generic, + ); + } + } + Type::Reference(type_ref) => { + replace_op_tensor_in_type_for_constraints( + &mut type_ref.elem, + constraint_ot_map, + current_generic, + ); + } + Type::Tuple(type_tuple) => { + for elem in &mut type_tuple.elems { + replace_op_tensor_in_type_for_constraints(elem, constraint_ot_map, current_generic); + } + } + _ => {} + } +} + +/// Replace OpTensor in a Path using the constraint_ot_map +fn replace_op_tensor_in_path_for_constraints( + path: &mut Path, + constraint_ot_map: &HashMap, + current_generic: &str, +) { + for segment in &mut path.segments { + if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments { + for arg in &mut args.args { + if let syn::GenericArgument::Type(ty) = arg { + replace_op_tensor_in_type_for_constraints( + ty, + constraint_ot_map, + current_generic, + ); + } + } + } + } +} + +/// Parse default attribute from parameter attributes +fn parse_default_attr(attrs: &[Attribute]) -> Result> { + for attr in attrs { + if attr.path().is_ident("default") { + return attr.parse_args::().map(Some); + } + } + Ok(None) +} + +/// Parse function parameters and extract information +fn parse_parameters(inputs: &Punctuated) -> Result> { + let mut params = Vec::new(); + let mut seen_default = false; + + for input in inputs { + if let FnArg::Typed(pat_type) = input + && let Pat::Ident(PatIdent { ident, .. }) = &*pat_type.pat + { + let default = parse_default_attr(&pat_type.attrs)?; + + // Validate default ordering + if seen_default && default.is_none() { + return Err(Error::new_spanned( + pat_type, + "Parameter without default follows parameter with default", + )); + } + if default.is_some() { + seen_default = true; + } + + let is_op_tensor = if let Type::Path(type_path) = &*pat_type.ty { + is_op_tensor_path(&type_path.path) + } else { + false + }; + + let mentions_op_tensor = mentions_op_tensor(&pat_type.ty); + + params.push(ParamInfo { + name: ident.to_string(), + pat_type: pat_type.clone(), + default, + is_op_tensor, + mentions_op_tensor, + }); + } + } + + Ok(params) +} + +/// Enhanced parameter analysis that considers generic constraints +fn analyze_parameters_with_generics( + inputs: &Punctuated, + generics: &syn::Generics, +) -> Result> { + let mut params = Vec::new(); + let mut seen_default = false; + + for input in inputs { + if let FnArg::Typed(pat_type) = input + && let Pat::Ident(PatIdent { ident, .. }) = &*pat_type.pat + { + let default = parse_default_attr(&pat_type.attrs)?; + + // Validate default ordering + if seen_default && default.is_none() { + return Err(Error::new_spanned( + pat_type, + "Parameter without default follows parameter with default", + )); + } + if default.is_some() { + seen_default = true; + } + + let is_op_tensor = if let Type::Path(type_path) = &*pat_type.ty { + is_op_tensor_path(&type_path.path) + } else { + false + } || is_option_with_op_tensor(&pat_type.ty) + || is_container_type_with_op_tensor(&pat_type.ty); + + let mut mentions_op_tensor = mentions_op_tensor(&pat_type.ty); + + // Check if this parameter's type is a generic that has OpTensor constraints + if !mentions_op_tensor + && let Type::Path(type_path) = &*pat_type.ty + && type_path.path.segments.len() == 1 + { + let param_type_name = &type_path.path.segments[0].ident; + // Check if this generic has OpTensor in its constraints + for param in &generics.params { + if let GenericParam::Type(type_param) = param + && type_param.ident == *param_type_name + { + let has_op_tensor_constraint = type_param.bounds.iter().any(|bound| { + if let syn::TypeParamBound::Trait(trait_bound) = bound { + contains_op_tensor_in_path(&trait_bound.path) + } else { + false + } + }); + if has_op_tensor_constraint { + mentions_op_tensor = true; + break; + } + } + } + } + + params.push(ParamInfo { + name: ident.to_string(), + pat_type: pat_type.clone(), + default, + is_op_tensor, + mentions_op_tensor, + }); + } + } + + Ok(params) +} + +/// Build a mapping from OpTensor occurrences in constraints to unique OT generics +fn build_constraint_ot_map( + generics: &syn::Generics, + op_tensor_map: &HashMap, +) -> HashMap { + let mut constraint_ot_map = HashMap::new(); + let mut ot_gen = OtGen::new(); + + // Skip existing OT generics to avoid conflicts + for _ in 0..op_tensor_map.len() { + ot_gen.next(); + } + + // For each generic parameter that has OpTensor in its constraints, + // assign it a unique OT generic + for param in &generics.params { + if let GenericParam::Type(type_param) = param { + let generic_name = type_param.ident.to_string(); + + // Check if this generic has OpTensor in its bounds + let has_op_tensor_constraint = type_param.bounds.iter().any(|bound| { + if let syn::TypeParamBound::Trait(trait_bound) = bound { + contains_op_tensor_in_path(&trait_bound.path) + } else { + false + } + }); + + if has_op_tensor_constraint { + let new_ot = ot_gen.next(); + constraint_ot_map.insert(generic_name, new_ot); + } + } + } + + constraint_ot_map +} + +/// Check if a path contains OpTensor anywhere in its arguments +fn contains_op_tensor_in_path(path: &Path) -> bool { + for segment in &path.segments { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + for arg in &args.args { + if let syn::GenericArgument::Type(ty) = arg + && mentions_op_tensor(ty) + { + return true; + } + } + } + } + false +} + +/// Check if a type is TensorTypeOrScalar<...> +fn is_tensor_type_or_scalar(ty: &Type) -> bool { + if let Type::Path(type_path) = ty { + type_path + .path + .segments + .last() + .map(|seg| seg.ident == "TensorTypeOrScalar") + .unwrap_or(false) + } else { + false + } +} + +/// Check if a type is Option where T contains OpTensor +fn is_option_with_op_tensor(ty: &Type) -> bool { + if let Type::Path(type_path) = ty + && let Some(last_segment) = type_path.path.segments.last() + && last_segment.ident == "Option" + && let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments + { + return args.args.iter().any(|arg| { + if let syn::GenericArgument::Type(inner_ty) = arg { + mentions_op_tensor(inner_ty) + } else { + false + } + }); + } + false +} + +/// Check if a type is Option or Option +fn is_option_with_tensor_or_self(ty: &Type) -> bool { + if let Type::Path(type_path) = ty + && let Some(last_segment) = type_path.path.segments.last() + && last_segment.ident == "Option" + && let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments + { + return args.args.iter().any(|arg| { + if let syn::GenericArgument::Type(Type::Path(inner_path)) = arg { + inner_path.path.segments.len() == 1 + && (inner_path.path.segments[0].ident == "Tensor" + || inner_path.path.segments[0].ident == "Self") + } else { + false + } + }); + } + false +} + +/// Check if a type is a container type that needs iter().map().collect() conversion +/// These types need special handling for OpTensor conversion +fn is_container_type_with_op_tensor(ty: &Type) -> bool { + if let Type::Path(type_path) = ty + && let Some(last_segment) = type_path.path.segments.last() + { + // Check for container types like RVec, Vec, etc. that contain OpTensor + let container_names = ["RVec", "Vec", "Array", "SmallVec"]; + if container_names.contains(&last_segment.ident.to_string().as_str()) + && let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments + { + return args.args.iter().any(|arg| { + if let syn::GenericArgument::Type(inner_ty) = arg { + mentions_op_tensor(inner_ty) + } else { + false + } + }); + } + } + false +} + +/// Generate the kernel function with OT generics +fn generate_kernel_function( + original_fn: &ItemFn, + params: &[ParamInfo], + op_tensor_map: &HashMap, +) -> Result { + let mut kernel_fn = original_fn.clone(); + + // Rename to {name}_kernel + let kernel_name = Ident::new( + &format!("{}_kernel", original_fn.sig.ident), + original_fn.sig.ident.span(), + ); + kernel_fn.sig.ident = kernel_name; + + // Make it pub(crate) + kernel_fn.vis = syn::parse_quote!(pub(crate)); + + // Build constraint OT mapping for unique OpTensor replacements in constraints + let constraint_ot_map = build_constraint_ot_map(&original_fn.sig.generics, op_tensor_map); + + // Add OT generics and replace OpTensor in original generics + let mut new_generics = original_fn.sig.generics.clone(); + + // First, replace OpTensor in existing generic bounds + for param in &mut new_generics.params { + if let GenericParam::Type(type_param) = param { + let generic_name = type_param.ident.to_string(); + for bound in &mut type_param.bounds { + if let syn::TypeParamBound::Trait(trait_bound) = bound { + replace_op_tensor_in_path_for_constraints( + &mut trait_bound.path, + &constraint_ot_map, + &generic_name, + ); + } + } + } + } + + // Replace OpTensor in where clauses + if let Some(where_clause) = &mut new_generics.where_clause { + for predicate in &mut where_clause.predicates { + if let syn::WherePredicate::Type(type_predicate) = predicate { + // Extract the generic name from the bounded type + let generic_name = if let Type::Path(type_path) = &type_predicate.bounded_ty { + type_path + .path + .segments + .first() + .map(|seg| seg.ident.to_string()) + .unwrap_or_default() + } else { + String::new() + }; + + for bound in &mut type_predicate.bounds { + if let syn::TypeParamBound::Trait(trait_bound) = bound { + replace_op_tensor_in_path_for_constraints( + &mut trait_bound.path, + &constraint_ot_map, + &generic_name, + ); + } + } + } + } + } + + // Add OT generics (both from parameters and constraints) + for ot_ident in op_tensor_map.values() { + let generic_param: GenericParam = syn::parse_quote!(#ot_ident: Into); + new_generics.params.push(generic_param); + } + + // Add constraint OT generics + for ot_ident in constraint_ot_map.values() { + let generic_param: GenericParam = syn::parse_quote!(#ot_ident: Into); + new_generics.params.push(generic_param); + } + + // Replace OpTensor in parameters with OT generics + let mut new_inputs = Punctuated::new(); + for input in &original_fn.sig.inputs { + if let FnArg::Typed(mut pat_type) = input.clone() { + // Remove default attributes from kernel function + pat_type + .attrs + .retain(|attr| !attr.path().is_ident("default")); + + if let Pat::Ident(PatIdent { ident, .. }) = &*pat_type.pat + && let Some(ot_ident) = op_tensor_map.get(&ident.to_string()) + { + let ot_path: Path = syn::parse_quote!(#ot_ident); + replace_op_tensor_in_type(&mut pat_type.ty, &ot_path); + } + new_inputs.push(FnArg::Typed(pat_type)); + } else { + new_inputs.push(input.clone()); + } + } + + kernel_fn.sig.inputs = new_inputs; + kernel_fn.sig.generics = new_generics; + + // Generate parameter conversions at the beginning of the function + let mut conversions = Vec::new(); + + for param in params { + let param_name = Ident::new(¶m.name, Span::call_site()); + + if param.is_op_tensor || param.mentions_op_tensor { + // Check the actual type to determine the right conversion + if is_container_type_with_op_tensor(¶m.pat_type.ty) { + // Container types like RVec, Vec need iter().map().collect() conversion + if let Type::Path(type_path) = &*param.pat_type.ty + && let Some(last_segment) = type_path.path.segments.last() + { + let container_name = &last_segment.ident; + conversions.push(quote! { + let #param_name = #param_name.into_iter().map(|inner| inner.into()).collect::<#container_name>(); + }); + } + } else if is_option_with_op_tensor(¶m.pat_type.ty) { + conversions.push(quote! { + let #param_name = #param_name.map(|inner| inner.into()); + }); + } else if is_tensor_type_or_scalar(¶m.pat_type.ty) { + conversions.push(quote! { + let #param_name = #param_name.map_tensor(|inner| inner.into())?; + }); + } else if param.is_op_tensor { + // Direct OpTensor parameter: let param = param.into(); + conversions.push(quote! { + let #param_name = #param_name.into(); + }); + } else { + // This might be a generic type constrained by TensorTypeOrScalar + conversions.push(quote! { + let #param_name = #param_name.map_tensor(|inner| inner.into())?; + }); + } + } + } + + // Preserve the original function body but add conversions at the beginning + let original_block = &original_fn.block; + kernel_fn.block = syn::parse_quote!({ + #(#conversions)* + #original_block + }); + + Ok(kernel_fn) +} + +/// Generate function variant +fn generate_function_variant( + original_fn: &ItemFn, + params: &[ParamInfo], + kernel_name: &Ident, +) -> Result { + let mut func = original_fn.clone(); + + // Replace OpTensor with Tensor in signature + let mut new_inputs = Punctuated::new(); + for input in &original_fn.sig.inputs { + if let FnArg::Typed(mut pat_type) = input.clone() { + // Remove default attributes + pat_type + .attrs + .retain(|attr| !attr.path().is_ident("default")); + replace_op_tensor_with_tensor(&mut pat_type.ty, false); + new_inputs.push(FnArg::Typed(pat_type)); + } else { + new_inputs.push(input.clone()); + } + } + func.sig.inputs = new_inputs; + + // Replace OpTensor with Tensor in return type, including containers like RVec or Vec + if let ReturnType::Type(_, ref mut ty) = func.sig.output { + replace_op_tensor_with_tensor(ty, false); + } + + // Replace OpTensor with Tensor in generics + let mut new_generics = original_fn.sig.generics.clone(); + for param in &mut new_generics.params { + if let GenericParam::Type(type_param) = param { + for bound in &mut type_param.bounds { + if let syn::TypeParamBound::Trait(trait_bound) = bound { + for segment in &mut trait_bound.path.segments { + if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments { + for arg in &mut args.args { + if let syn::GenericArgument::Type(ty) = arg { + replace_op_tensor_with_tensor(ty, false); + } + } + } + } + } + } + } + } + func.sig.generics = new_generics; + + // Generate call arguments + let mut call_args = Vec::new(); + for param in params { + let param_name = Ident::new(¶m.name, Span::call_site()); + // if param.is_op_tensor || param.mentions_op_tensor { + // call_args.push(quote!(#param_name)); + // } else { + // call_args.push(quote!(#param_name)); + // } + call_args.push(quote!(#param_name)); + } + + // Detect if return type is Result> where Container is RVec or Vec + let returns_container_of_optensor = (|| { + if let ReturnType::Type(_, ty) = &original_fn.sig.output { + // Unwrap Result to Ok + let ok_ty: &Type = if let Type::Path(tp) = &**ty + && tp.path.segments.last().map(|s| s.ident.to_string()) + == Some("Result".to_string()) + && matches!( + tp.path.segments.last().unwrap().arguments, + syn::PathArguments::AngleBracketed(_) + ) { + if let syn::PathArguments::AngleBracketed(args) = + &tp.path.segments.last().unwrap().arguments + { + if let Some(syn::GenericArgument::Type(t)) = args.args.first() { + t + } else { + ty + } + } else { + ty + } + } else { + ty + }; + + if let Type::Path(tp2) = ok_ty + && let Some(seg) = tp2.path.segments.last() + { + let name = seg.ident.to_string(); + if (name == "RVec" || name == "Vec") + && let syn::PathArguments::AngleBracketed(args) = &seg.arguments + && let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() + && let Type::Path(inner_path) = inner_ty + { + return is_op_tensor_path(&inner_path.path); + } + } + } + // Fallback: string match on signature tokens + let sig = original_fn.sig.output.to_token_stream().to_string(); + sig.contains("RVec < OpTensor >") || sig.contains("Vec < OpTensor >") + })(); + + if returns_container_of_optensor { + func.block = syn::parse_quote!({ + #kernel_name(#(#call_args),*).map(|v| v.into_iter().map(Tensor::wrap).collect()) + }); + } else { + func.block = syn::parse_quote!({ + #kernel_name(#(#call_args),*).map(Tensor::wrap) + }); + } + + Ok(func) +} + +/// Generate method variant +fn generate_method_variant( + original_fn: &ItemFn, + params: &[ParamInfo], + kernel_name: &Ident, +) -> Result { + let method_name = &original_fn.sig.ident; + let mut generics = original_fn.sig.generics.clone(); + + // Replace OpTensor with Self in generics + for param in &mut generics.params { + if let GenericParam::Type(type_param) = param { + for bound in &mut type_param.bounds { + if let syn::TypeParamBound::Trait(trait_bound) = bound { + for segment in &mut trait_bound.path.segments { + if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments { + for arg in &mut args.args { + if let syn::GenericArgument::Type(ty) = arg { + replace_op_tensor_with_tensor(ty, true); + } + } + } + } + } + } + } + } + + // Generate method parameters (skip first parameter which becomes self) + let mut method_params = Vec::new(); + let mut call_args = Vec::new(); + call_args.push(quote!(self.inner_or_source().clone())); + + // Skip the first parameter since it becomes self + for param in params.iter().skip(1) { + let param_name = Ident::new(¶m.name, Span::call_site()); + let mut param_type = param.pat_type.ty.clone(); + replace_op_tensor_with_tensor(&mut param_type, true); + + method_params.push(quote!(#param_name: #param_type)); + + if is_option_with_tensor_or_self(¶m_type) { + call_args.push(quote!(#param_name.map(|t| t.inner_or_source().clone()))); + } else if param.is_op_tensor { + call_args.push(quote!(#param_name.inner_or_source().clone())); + } else { + call_args.push(quote!(#param_name)); + } + } + + // Generate return type + let mut return_type = original_fn.sig.output.clone(); + if let ReturnType::Type(_, ref mut ty) = return_type { + replace_op_tensor_with_tensor(ty, true); + } + + // Detect if return type is Result> where Container is RVec or Vec + let returns_container_of_optensor = (|| { + if let ReturnType::Type(_, ty) = &original_fn.sig.output { + // Unwrap Result to Ok + let ok_ty: &Type = if let Type::Path(tp) = &**ty + && tp.path.segments.last().map(|s| s.ident.to_string()) + == Some("Result".to_string()) + && matches!( + tp.path.segments.last().unwrap().arguments, + syn::PathArguments::AngleBracketed(_) + ) { + if let syn::PathArguments::AngleBracketed(args) = + &tp.path.segments.last().unwrap().arguments + { + if let Some(syn::GenericArgument::Type(t)) = args.args.first() { + t + } else { + ty + } + } else { + ty + } + } else { + ty + }; + + if let Type::Path(tp2) = ok_ty + && let Some(seg) = tp2.path.segments.last() + { + let name = seg.ident.to_string(); + if (name == "RVec" || name == "Vec") + && let syn::PathArguments::AngleBracketed(args) = &seg.arguments + && let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() + && let Type::Path(inner_path) = inner_ty + { + return is_op_tensor_path(&inner_path.path); + } + } + } + let sig = original_fn.sig.output.to_token_stream().to_string(); + sig.contains("RVec < OpTensor >") || sig.contains("Vec < OpTensor >") + })(); + + let method = if returns_container_of_optensor { + quote! { + pub fn #method_name #generics (self, #(#method_params),*) #return_type { + #kernel_name(#(#call_args),*).map(|v| v.into_iter().map(Self::wrap).collect()) + } + } + } else { + quote! { + pub fn #method_name #generics (self, #(#method_params),*) #return_type { + #kernel_name(#(#call_args),*).map(Self::wrap) + } + } + }; + + Ok(method) +} + +/// Generate inplace method variant +fn generate_method_inplace_variant( + original_fn: &ItemFn, + params: &[ParamInfo], + kernel_name: &Ident, +) -> Result { + let method_name = Ident::new( + &format!("{}_", original_fn.sig.ident), + original_fn.sig.ident.span(), + ); + let mut generics = original_fn.sig.generics.clone(); + + // Replace OpTensor with Self in generics + for param in &mut generics.params { + if let GenericParam::Type(type_param) = param { + for bound in &mut type_param.bounds { + if let syn::TypeParamBound::Trait(trait_bound) = bound { + for segment in &mut trait_bound.path.segments { + if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments { + for arg in &mut args.args { + if let syn::GenericArgument::Type(ty) = arg { + replace_op_tensor_with_tensor(ty, true); + } + } + } + } + } + } + } + } + + // Generate method parameters (skip first parameter which becomes self) + let mut method_params = Vec::new(); + let mut call_args = Vec::new(); + call_args.push(quote!(inner)); + + // We'll collect guard bindings for converted arguments here + let mut pre_bindings: Vec = Vec::new(); + + // Skip the first parameter since it becomes self + for param in params.iter().skip(1) { + let param_name = Ident::new(¶m.name, Span::call_site()); + let mut param_type = param.pat_type.ty.clone(); + replace_op_tensor_with_tensor(&mut param_type, true); + + method_params.push(quote!(#param_name: #param_type)); + + // For inplace methods, pull out conversions into guard variables instead of inlining + if is_option_with_tensor_or_self(¶m_type) { + let inner_name = Ident::new(&format!("{}_inner", param.name), Span::call_site()); + pre_bindings.push( + quote! { let #inner_name = #param_name.map(|t| t.inner_or_source().clone()); }, + ); + call_args.push(quote!(#inner_name)); + } else if param.is_op_tensor { + let inner_name = Ident::new(&format!("{}_inner", param.name), Span::call_site()); + pre_bindings.push(quote! { let #inner_name = #param_name.inner_or_source().clone(); }); + call_args.push(quote!(#inner_name)); + } else { + call_args.push(quote!(#param_name)); + } + } + + // Generate return type + let mut return_type = original_fn.sig.output.clone(); + if let ReturnType::Type(_, ref mut ty) = return_type { + replace_op_tensor_with_tensor(ty, true); + } + + let method = quote! { + pub fn #method_name #generics (self, #(#method_params),*) #return_type { + let inner = self.inner_or_source().clone(); + #(#pre_bindings)* + Ok(self.wrap_inplace(#kernel_name(#(#call_args),*)?)) + } + }; + + Ok(method) +} + +/// Main function to process a tensor operation +pub fn process_tensor_op(attr: TensorOpAttr, item: ItemFn) -> Result { + // Parse parameters with generic constraints consideration + let params = analyze_parameters_with_generics(&item.sig.inputs, &item.sig.generics)?; + + // Build OpTensor mapping - only for direct OpTensor parameters + let mut op_tensor_map = HashMap::new(); + let mut ot_gen = OtGen::new(); + + for param in ¶ms { + if param.is_op_tensor { + op_tensor_map.insert(param.name.clone(), ot_gen.next()); + } + } + + // Generate kernel function + let kernel_name = Ident::new(&format!("{}_kernel", item.sig.ident), item.sig.ident.span()); + let kernel_fn = generate_kernel_function(&item, ¶ms, &op_tensor_map)?; + + let mut output = quote!(#kernel_fn); + + // Generate variants + for variant in &attr.variants { + match variant { + TensorOpVariant::Function => { + let func = generate_function_variant(&item, ¶ms, &kernel_name)?; + output.extend(quote!(#func)); + } + TensorOpVariant::Method => { + let method = generate_method_variant(&item, ¶ms, &kernel_name)?; + output.extend(quote! { + impl Tensor { + #method + } + }); + } + TensorOpVariant::MethodInplace => { + let method = generate_method_inplace_variant(&item, ¶ms, &kernel_name)?; + output.extend(quote! { + impl Tensor { + #method + } + }); + } + } + } + + Ok(output) +} + +#[cfg(test)] +mod tests { + use super::*; + use quote::ToTokens; + use syn::parse_quote; + + #[test] + fn test_parse_tensor_op_attr_empty() { + let attr: TensorOpAttr = syn::parse_str("").unwrap(); + assert_eq!(attr.variants.len(), 2); + assert!(attr.variants.contains(&TensorOpVariant::Function)); + assert!(attr.variants.contains(&TensorOpVariant::Method)); + } + + #[test] + fn test_parse_tensor_op_attr_with_variants() { + let attr: TensorOpAttr = + syn::parse_str("variants = [function, method, method_inplace]").unwrap(); + assert_eq!(attr.variants.len(), 3); + assert!(attr.variants.contains(&TensorOpVariant::Function)); + assert!(attr.variants.contains(&TensorOpVariant::Method)); + assert!(attr.variants.contains(&TensorOpVariant::MethodInplace)); + } + + #[test] + fn test_is_op_tensor_path() { + let path: Path = parse_quote!(OpTensor); + assert!(is_op_tensor_path(&path)); + + let path: Path = parse_quote!(crate::tensor::OpTensor); + assert!(is_op_tensor_path(&path)); + + let path: Path = parse_quote!(Tensor); + assert!(!is_op_tensor_path(&path)); + } + + #[test] + fn test_mentions_op_tensor() { + let ty: Type = parse_quote!(OpTensor); + assert!(mentions_op_tensor(&ty)); + + let ty: Type = parse_quote!(TensorTypeOrScalar); + assert!(mentions_op_tensor(&ty)); + + let ty: Type = parse_quote!(f64); + assert!(!mentions_op_tensor(&ty)); + + let ty: Type = parse_quote!(TensorTypeOrScalar); + assert!(!mentions_op_tensor(&ty)); + } + + #[test] + fn test_replace_op_tensor_with_tensor() { + let mut ty: Type = parse_quote!(OpTensor); + replace_op_tensor_with_tensor(&mut ty, false); + assert_eq!(ty.to_token_stream().to_string(), "Tensor"); + + let mut ty: Type = parse_quote!(TensorTypeOrScalar); + replace_op_tensor_with_tensor(&mut ty, true); + assert_eq!( + ty.to_token_stream().to_string(), + "TensorTypeOrScalar < Self >" + ); + } + + #[test] + fn test_parse_parameters_with_defaults() { + let inputs: Punctuated = parse_quote!( + lhs: OpTensor, + rhs: OpTensor, + #[default(1.0)] alpha: f64 + ); + + let params = parse_parameters(&inputs).unwrap(); + assert_eq!(params.len(), 3); + assert_eq!(params[0].name, "lhs"); + assert!(params[0].is_op_tensor); + assert!(params[0].default.is_none()); + assert_eq!(params[2].name, "alpha"); + assert!(params[2].default.is_some()); + } + + #[test] + fn test_parse_parameters_invalid_default_order() { + let inputs: Punctuated = parse_quote!( + #[default(1.0)] alpha: f64, + beta: f64 + ); + + let result = parse_parameters(&inputs); + assert!(result.is_err()); + } + + #[test] + fn test_generate_kernel_function() { + let original_fn: ItemFn = parse_quote! { + fn add>(lhs: OpTensor, rhs: T) -> Result { + // implementation + } + }; + + let params = parse_parameters(&original_fn.sig.inputs).unwrap(); + let mut op_tensor_map = HashMap::new(); + let mut ot_gen = OtGen::new(); + + for param in ¶ms { + if param.is_op_tensor { + op_tensor_map.insert(param.name.clone(), ot_gen.next()); + } + } + + let kernel_fn = generate_kernel_function(&original_fn, ¶ms, &op_tensor_map).unwrap(); + + assert_eq!(kernel_fn.sig.ident, "add_kernel"); + assert!(kernel_fn.sig.generics.params.len() >= 2); // Should have OT1, OT2 generics + + let kernel_str = kernel_fn.to_token_stream().to_string(); + assert!(kernel_str.contains("pub (crate)")); + assert!(kernel_str.contains("OT1 : Into < OpTensor >")); + assert!(kernel_str.contains("OT2 : Into < OpTensor >")); + + let formatted_output = prettyplease::unparse(&syn::File { + shebang: None, + attrs: vec![], + items: vec![syn::Item::Fn(kernel_fn)], + }); + + println!("{formatted_output}"); + } + + #[test] + fn test_generate_function_variant() { + let original_fn: ItemFn = parse_quote! { + fn add(lhs: OpTensor, rhs: OpTensor) -> Result { + // implementation + } + }; + + let params = parse_parameters(&original_fn.sig.inputs).unwrap(); + let kernel_name = Ident::new("add_kernel", Span::call_site()); + + let func = generate_function_variant(&original_fn, ¶ms, &kernel_name).unwrap(); + + let func_str = func.to_token_stream().to_string(); + assert!(func_str.contains("fn add")); + assert!(func_str.contains("lhs : Tensor")); + assert!(func_str.contains("rhs : Tensor")); + assert!(func_str.contains("Result < Tensor >")); + assert!(func_str.contains("add_kernel")); + assert!(func_str.contains(". map (Tensor :: wrap)")); + + // let formatted_output = prettyplease::unparse(&syn::File { + // shebang: None, + // attrs: vec![], + // items: vec![syn::Item::Fn(func)], + // }); + + // println!("{formatted_output}"); + } + + #[test] + fn test_generate_method_variant() { + let original_fn: ItemFn = parse_quote! { + fn add(lhs: OpTensor, rhs: OpTensor) -> Result { + // implementation + } + }; + + let params = parse_parameters(&original_fn.sig.inputs).unwrap(); + let kernel_name = Ident::new("add_kernel", Span::call_site()); + + let method = generate_method_variant(&original_fn, ¶ms, &kernel_name).unwrap(); + + let method_str = method.to_token_stream().to_string(); + assert!(method_str.contains("pub fn add")); + assert!(method_str.contains("self")); + assert!(method_str.contains("Result < Self >")); + assert!(method_str.contains("inner_or_source")); + assert!(method_str.contains("Self :: wrap")); + } + + #[test] + fn test_generate_method_inplace_variant() { + let original_fn: ItemFn = parse_quote! { + fn add(lhs: OpTensor, rhs: OpTensor) -> Result { + // implementation + } + }; + + let params = parse_parameters(&original_fn.sig.inputs).unwrap(); + let kernel_name = Ident::new("add_kernel", Span::call_site()); + + let method = generate_method_inplace_variant(&original_fn, ¶ms, &kernel_name).unwrap(); + + let method_str = method.to_token_stream().to_string(); + assert!(method_str.contains("pub fn add_")); + assert!(method_str.contains("self")); + assert!(method_str.contains("wrap_inplace")); + } + + #[test] + fn test_process_tensor_op_complete() { + let attr = TensorOpAttr { + variants: vec![TensorOpVariant::Function, TensorOpVariant::Method], + }; + + let item: ItemFn = parse_quote! { + fn add>(lhs: OpTensor, rhs: T) -> Result { + // implementation + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Should contain kernel function + assert!(result_str.contains("pub (crate) fn add_kernel")); + + // Should contain function variant + assert!(result_str.contains("fn add")); + assert!(result_str.contains("lhs : Tensor")); + + // Should contain method variant + assert!(result_str.contains("impl Tensor")); + assert!(result_str.contains("pub fn add")); + } + + #[test] + fn test_process_tensor_op_with_generics() { + let attr = TensorOpAttr { + variants: vec![TensorOpVariant::Function], + }; + + let item: ItemFn = parse_quote! { + fn scatter_add(input: OpTensor, indices: OpTensor, source: OpTensor, dim: D) -> Result { + // implementation + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Should preserve original generics + assert!(result_str.contains("D : Dim")); + + // Should add OT generics + assert!(result_str.contains("OT1 : Into < OpTensor >")); + assert!(result_str.contains("OT2 : Into < OpTensor >")); + assert!(result_str.contains("OT3 : Into < OpTensor >")); + + // Should handle non-tensor parameters correctly + assert!(result_str.contains("dim : D")); + } + + #[test] + fn test_process_tensor_op_with_defaults() { + let attr = TensorOpAttr { + variants: vec![TensorOpVariant::Function], + }; + + let item: ItemFn = parse_quote! { + fn add(lhs: OpTensor, rhs: OpTensor, #[default(1.0)] alpha: f64) -> Result { + // implementation + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Kernel should not have default attributes + assert!(result_str.contains("alpha : f64")); + + // Function variant should not have default attributes either + // (defaults would be handled at a higher level) + assert!(!result_str.contains("#[default")); + } + + #[test] + fn test_process_tensor_op_with_generic_constraints() { + let attr = TensorOpAttr { + variants: vec![TensorOpVariant::Function], + }; + + let item: ItemFn = parse_quote! { + fn add, T2: TensorTypeOrScalar>( + lhs: OpTensor, + rhs: T, + rhs2: T2, + ) -> Result { + // implementation + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Should contain kernel function with proper generic constraint replacement + assert!(result_str.contains("pub (crate) fn add_kernel")); + + // The generic constraints should be updated to use unique OT generics + // T: TensorTypeOrScalar should become T: TensorTypeOrScalar + // T2: TensorTypeOrScalar should become T2: TensorTypeOrScalar + assert!(result_str.contains("T : TensorTypeOrScalar < OT2 >")); + assert!(result_str.contains("T2 : TensorTypeOrScalar < OT3 >")); + + // Should have OT generics (OT1 for lhs, OT2-OT3 for constraints) + assert!(result_str.contains("OT1 : Into < OpTensor >")); + assert!(result_str.contains("OT2 : Into < OpTensor >")); + assert!(result_str.contains("OT3 : Into < OpTensor >")); + } + + #[test] + fn test_kernel_function_body_conversions() { + let attr = TensorOpAttr { + variants: vec![TensorOpVariant::Function], + }; + + let item: ItemFn = parse_quote! { + fn complex_op>( + input: OpTensor, + param: T, + optional: Option, + ) -> Result { + // Some original implementation + let result = input + param.unwrap_or_default(); + Ok(result) + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Should contain the parameter conversions + assert!(result_str.contains("let input = input . into ()")); + assert!(result_str.contains("let param = param . map_tensor (| inner | inner . into ())")); + assert!(result_str.contains("let optional = optional . map (| inner | inner . into ())")); + + // Should contain the original implementation + assert!(result_str.contains("let result = input + param . unwrap_or_default ()")); + assert!(result_str.contains("Ok (result)")); + } + + #[test] + fn test_method_first_parameter_becomes_self() { + let attr = TensorOpAttr { + variants: vec![TensorOpVariant::Method, TensorOpVariant::MethodInplace], + }; + + let item: ItemFn = parse_quote! { + pub fn eq2>(input: OpTensor, other: T) -> Result { + // Some implementation + Ok(input) + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Should contain correct method signatures (only 'other' parameter, not 'input') + assert!( + result_str.contains("pub fn eq2 < T : TensorTypeOrScalar < T > > (self , other : T)") + ); + assert!( + result_str.contains("pub fn eq2_ < T : TensorTypeOrScalar < T > > (self , other : T)") + ); + + // Should NOT contain 'input' in method parameters + assert!(!result_str.contains("(self , input")); + + // Should have correct kernel call arguments (self replaces first parameter) + assert!(result_str.contains("eq2_kernel (self . inner_or_source () . clone () , other")); + } + + #[test] + fn test_your_eq2_example() { + let attr = TensorOpAttr { + variants: vec![ + TensorOpVariant::Function, + TensorOpVariant::Method, + TensorOpVariant::MethodInplace, + ], + }; + + let item: ItemFn = parse_quote! { + pub fn eq2>(input: OpTensor, other: T) -> Result { + let device = input.device().clone(); + match other.tensor_or_scalar() { + Ok(TensorTypeOrScalarEnum::Tensor(other)) => { + let (lhs, rhs) = input.broadcast_for_binary_op(other)?; + let cmp = Cmp::new(lhs, TensorTypeOrScalarEnum::Tensor(rhs), (CmpOp::Eq)); + let new_view = cmp.compute_view()?; + Ok(OpTensor::lazy(LazyOp::Cmp(cmp), new_view, device, false)) + } + Ok(TensorTypeOrScalarEnum::Scalar(other)) => { + let device = input.device.clone(); + let cmp = Cmp::new(input, TensorTypeOrScalarEnum::Scalar(other), (CmpOp::Eq)); + let new_view = cmp.compute_view()?; + Ok(OpTensor::lazy(LazyOp::Cmp(cmp), new_view, device, false)) + } + Err(e) => Err(e), + } + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Should have kernel with original implementation + assert!(result_str.contains("pub (crate) fn eq2_kernel")); + assert!(result_str.contains("let input = input . into ()")); + assert!(result_str.contains("let device = input . device () . clone ()")); + + // Should have function variant + assert!(result_str.contains( + "pub fn eq2 < T : TensorTypeOrScalar < Tensor > > (input : Tensor , other : T)" + )); + + // Should have method variants with correct signatures (input becomes self) + assert!( + result_str + .contains("pub fn eq2 < T : TensorTypeOrScalar < Self > > (self , other : T)") + ); + assert!( + result_str + .contains("pub fn eq2_ < T : TensorTypeOrScalar < Self > > (self , other : T)") + ); + + // Should NOT have extra input parameter in methods + assert!(!result_str.contains("(self , input : Self , other")); + } + + #[test] + fn test_option_op_tensor_parameters() { + let attr = TensorOpAttr { + variants: vec![TensorOpVariant::Function], + }; + + let item: ItemFn = parse_quote! { + pub fn group_norm( + input: OpTensor, + num_groups: usize, + weight: Option, + bias: Option, + eps: f32, + ) -> Result { + // implementation + Ok(input) + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Should have kernel with Option generics + assert!(result_str.contains("pub (crate) fn group_norm_kernel")); + assert!(result_str.contains("weight : Option < OT2 >")); + assert!(result_str.contains("bias : Option < OT3 >")); + + // Should have OT generics for all OpTensor parameters including Options + assert!(result_str.contains("OT1 : Into < OpTensor >")); // input + assert!(result_str.contains("OT2 : Into < OpTensor >")); // weight + assert!(result_str.contains("OT3 : Into < OpTensor >")); // bias + + // Should have correct conversions + assert!(result_str.contains("let input = input . into ()")); + assert!(result_str.contains("let weight = weight . map (| inner | inner . into ())")); + assert!(result_str.contains("let bias = bias . map (| inner | inner . into ())")); + } + + #[test] + fn test_option_tensor_in_methods() { + let attr = TensorOpAttr { + variants: vec![TensorOpVariant::Method, TensorOpVariant::MethodInplace], + }; + + let item: ItemFn = parse_quote! { + pub fn group_norm( + input: OpTensor, + num_groups: usize, + weight: Option, + bias: Option, + eps: f32, + ) -> Result { + // implementation + Ok(input) + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Should have method signatures with Option + assert!(result_str.contains("weight : Option < Self >")); + assert!(result_str.contains("bias : Option < Self >")); + + // Should have correct method calls with .map for Option parameters + assert!(result_str.contains("weight . map (| t | t . inner_or_source () . clone ())")); + assert!(result_str.contains("bias . map (| t | t . inner_or_source () . clone ())")); + + // Should NOT have .inner_or_source().clone() directly on Option parameters + assert!(!result_str.contains("weight . inner_or_source () . clone ()")); + assert!(!result_str.contains("bias . inner_or_source () . clone ()")); + } + + #[test] + fn test_option_tensor_in_function() { + let attr = TensorOpAttr { + variants: vec![TensorOpVariant::Function], + }; + + let item: ItemFn = parse_quote! { + pub fn group_norm( + input: OpTensor, + num_groups: usize, + weight: Option, + bias: Option, + eps: f32, + ) -> Result { + // implementation + Ok(input) + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Should have function signature with Option + assert!(result_str.contains("weight : Option < Tensor >")); + assert!(result_str.contains("bias : Option < Tensor >")); + + // Should have simple parameter passing (no .map needed for function variant) + assert!( + result_str.contains("group_norm_kernel (input , num_groups , weight , bias , eps)") + ); + } + + #[test] + fn test_container_type_correct_automatic_conversion() { + let attr = TensorOpAttr { + variants: vec![TensorOpVariant::Function], + }; + + let item: ItemFn = parse_quote! { + pub fn cat(tensors: RVec, dim: D) -> Result { + let dim = dim.to_index(tensors[0].shape(), "cat")?; + let device = tensors[0].device().clone(); + assert!(tensors.iter().all(|t| t.device == device), "Mixed devices"); + + let cat = Concat::new(tensors, dim); + let new_view = cat.compute_view()?; + Ok(OpTensor::lazy(LazyOp::Concat(cat), new_view, device, false)) + } + }; + + let result = process_tensor_op(attr, item).unwrap(); + let result_str = result.to_token_stream().to_string(); + + // Should NOT contain wrong conversion type (map_tensor) for RVec + assert!(!result_str.contains("let tensors = tensors . map_tensor")); + + // Should contain correct automatic conversion for container types + assert!(result_str.contains( + "let tensors = tensors . into_iter () . map (| inner | inner . into ()) . collect :: < RVec < OpTensor > > ()" + )); + + // Should have function signature with RVec + assert!(result_str.contains("tensors : RVec < Tensor >")); + + // Should have kernel signature with RVec where OT1: Into + assert!(result_str.contains("tensors : RVec < OT1 >")); + assert!(result_str.contains("OT1 : Into < OpTensor >")); + + // Should contain kernel function call + assert!(result_str.contains("cat_kernel (tensors , dim)")); + } +} diff --git a/crates/piston-macros/src/scoped_module.rs b/crates/piston-macros/src/scoped_module.rs new file mode 100644 index 00000000..a6ac936e --- /dev/null +++ b/crates/piston-macros/src/scoped_module.rs @@ -0,0 +1,70 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{ImplItem, ItemImpl, Type, parse2}; + +pub fn scoped_module(item: TokenStream) -> TokenStream { + let input = proc_macro2::TokenStream::from(item); + scoped_module_impl(input).into() +} + +fn scoped_module_impl(input: TokenStream2) -> TokenStream2 { + let mut impl_block = parse2::(input).expect("Expected impl block"); + let self_ty = &impl_block.self_ty; + + let mut has_module_name = false; + + for item in &impl_block.items { + if let ImplItem::Fn(method) = item + && method.sig.ident == "module_name" + { + has_module_name = true; + break; + } + } + + for item in &mut impl_block.items { + if let ImplItem::Fn(method) = item + && method.sig.ident == "schedule" + { + let original_body = method.block.clone(); + + method.block = syn::parse2(quote! { + { + let _scope_guard = piston::ScopePusher::new(&format!("mod:{}", self.module_name())); + + #original_body + } + }) + .unwrap(); + + break; + } + } + + // Generate a default module_name if not present + if !has_module_name { + let type_name = match self_ty.as_ref() { + Type::Path(type_path) => { + if let Some(segment) = type_path.path.segments.last() { + segment.ident.to_string() + } else { + "unknown".to_string() + } + } + _ => "unknown".to_string(), + }; + + let module_name_method = quote! { + fn module_name(&self) -> &str { + #type_name + } + }; + + impl_block + .items + .push(syn::parse2(module_name_method).unwrap()); + } + + quote! { #impl_block } +} diff --git a/crates/ratchet-macros/src/wgsl_metadata.rs b/crates/piston-macros/src/wgsl_metadata.rs similarity index 98% rename from crates/ratchet-macros/src/wgsl_metadata.rs rename to crates/piston-macros/src/wgsl_metadata.rs index f78efba2..563d403b 100644 --- a/crates/ratchet-macros/src/wgsl_metadata.rs +++ b/crates/piston-macros/src/wgsl_metadata.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{parse2, DeriveInput}; +use syn::{DeriveInput, parse2}; pub fn derive(input: TokenStream) -> TokenStream { let _input = parse2::(input).unwrap(); diff --git a/crates/ratchet-models/Cargo.toml b/crates/piston-models/Cargo.toml similarity index 61% rename from crates/ratchet-models/Cargo.toml rename to crates/piston-models/Cargo.toml index 7a672dbe..5ff9971e 100644 --- a/crates/ratchet-models/Cargo.toml +++ b/crates/piston-models/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "ratchet-models" +name = "piston-models" version = "0.1.0" -edition = "2021" +edition = "2024" resolver = "2" [features] default = ["pyo3"] ci = [] pyo3 = [] -plotting = ["ratchet/plotting"] -debug = ["ratchet-nn/debug", "ratchet/debug"] +plotting = ["piston/plotting"] +debug = ["piston-nn/debug", "piston/debug"] [lib] crate-type = ["cdylib", "lib"] @@ -23,46 +23,33 @@ dwarf-debug-info = true wasm-opt = ['-O3', '--enable-simd'] [dependencies] -ratchet = { path = "../ratchet-core" } -ratchet-nn = { path = "../ratchet-nn" } -ratchet-loader = { path = "../ratchet-loader" } -ratchet-datasets = { path = "../ratchet-datasets" } -byteorder.workspace = true +piston = { path = "../piston-core" } +piston-nn = { path = "../piston-nn" } +piston-datasets = { path = "../piston-datasets" } +piston-macros = { path = "../piston-macros" } anyhow.workspace = true thiserror.workspace = true derive-new = { workspace = true } log.workspace = true ndarray-stats = { workspace = true } -num = { workspace = true } -realfft = { workspace = true } ndarray = { workspace = true } -cfg-if = { workspace = true } -serde = { workspace = true } tokenizers = { version = "0.19.1", default-features = false, features=["unstable_wasm"] } -lazy_static = { workspace = true } -web-time = { workspace = true } clap = { workspace = true, features = ["derive"]} -serde_json.workspace = true -half.workspace = true -image = { workspace = true } -pollster.workspace = true -wasm-bindgen-futures = "0.4.42" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { workspace = true } serde-wasm-bindgen = "0.4.5" -ratchet-hub = { path = "../ratchet-hub" } tsify = "0.4.5" js-sys = { workspace = true } maybe-async = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ratchet = { path = "../ratchet-core", features = ["pyo3"] } +piston = { path = "../piston-core", features = ["pyo3"] } maybe-async = { workspace = true, features = ["is_sync"] } hf-hub.workspace = true [dev-dependencies] -ratchet = { path = "../ratchet-core" } +piston = { path = "../piston-core" } console_error_panic_hook = { workspace = true } console_log = { workspace = true } wasm-bindgen-test = { workspace = true } @@ -74,7 +61,7 @@ env_logger = { workspace = true } rand = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -ratchet = { path = "../ratchet-core", features = ["pyo3"] } -pyo3 = "0.20.2" -numpy = "0.20.0" +piston = { path = "../piston-core", features = ["pyo3"] } +pyo3 = { workspace = true } +numpy = { workspace = true } diff --git a/crates/ratchet-models/src/gpt2/attn.rs b/crates/piston-models/src/gpt2/attn.rs similarity index 55% rename from crates/ratchet-models/src/gpt2/attn.rs rename to crates/piston-models/src/gpt2/attn.rs index f92481ca..77649093 100644 --- a/crates/ratchet-models/src/gpt2/attn.rs +++ b/crates/piston-models/src/gpt2/attn.rs @@ -1,9 +1,11 @@ use std::{cell::RefCell, rc::Rc}; use maybe_async::maybe_async; -use ratchet::{prelude::shape, rvec, Tensor}; -use ratchet_nn::{ - AlibiEmbedding, AlibiInput, KVCache, Linear, Module, RotaryEmbedding, RotaryInput, VarBuilder, +use piston::{Tensor, TensorOptions}; +use piston_macros::scoped_module; +use piston_nn::{ + AlibiEmbedding, AlibiInput, Dropout, KVCache, Linear, Module, RotaryEmbedding, RotaryInput, + VarBuilder, }; use super::{ @@ -16,12 +18,13 @@ use super::{ pub struct GPT2SelfAttention { c_attn: Linear, c_proj: Linear, - softmax_scale: Tensor, n_embd: usize, n_head: usize, h_dim: usize, rope: Option, alibi: Option, + attn_dropout: Option, + resid_dropout: Option, } impl GPT2SelfAttention { @@ -32,8 +35,6 @@ impl GPT2SelfAttention { linear_gpt2_residual(cfg.n_embd, cfg.n_embd, cfg.n_layer, vb.pp("c_proj")).await?; let h_dim = cfg.head_dim(); - let softmax_scale = Tensor::full(&shape![1], 1.0 / (h_dim as f32).sqrt(), vb.device()); - let (rope, alibi) = match cfg.positional_encoding { PositionalEncoding::RoPE => { let rope_base = 10000.0f32; @@ -46,18 +47,32 @@ impl GPT2SelfAttention { None, Some(AlibiEmbedding::new(cfg.n_head, 8.0)), // max_bias=8.0 is a common default ), - PositionalEncoding::Learned | PositionalEncoding::Sinusoidal => (None, None), + PositionalEncoding::Learned + | PositionalEncoding::Sinusoidal + | PositionalEncoding::None => (None, None), + }; + + let attn_dropout = if cfg.attn_pdrop > 0.0 { + Some(Dropout::new(cfg.attn_pdrop)) + } else { + None + }; + let resid_dropout = if cfg.resid_pdrop > 0.0 { + Some(Dropout::new(cfg.resid_pdrop)) + } else { + None }; Ok(Self { c_attn, c_proj, - softmax_scale, n_head: cfg.n_head, n_embd: cfg.n_embd, h_dim, rope, alibi, + attn_dropout, + resid_dropout, }) } } @@ -69,6 +84,7 @@ pub struct GPT2AttnInput { pub cache: Rc>, } +#[scoped_module] impl Module for GPT2SelfAttention { type Input = GPT2AttnInput; type Output = (Tensor, Tensor); @@ -76,11 +92,11 @@ impl Module for GPT2SelfAttention { fn schedule(&self, input: Self::Input) -> anyhow::Result { let GPT2AttnInput { input, - index_pos, + index_pos: _, block_idx, - mut cache, + cache, } = input; - let [batch_size, q_len, _]: [usize; 3] = input.shape().try_into()?; + let (batch_size, q_len, _) = input.shape().dims3()?; let qkv = self.c_attn.schedule(input)?; @@ -98,25 +114,38 @@ impl Module for GPT2SelfAttention { qkv.clone() .slice(&[0..batch_size, 0..q_len, value_pos..value_pos + self.n_embd])?; - let qkv_shape = shape![batch_size as _, q_len, self.n_head, self.h_dim]; + let qkv_shape = (batch_size, q_len, self.n_head, self.h_dim); - let mut k = k.view(qkv_shape.clone())?.permute(&[0, 2, 1, 3])?; - let mut q = q.view(qkv_shape.clone())?.permute(&[0, 2, 1, 3])?; - let v = v.view(qkv_shape.clone())?.permute(&[0, 2, 1, 3])?; + let mut k = k.view(qkv_shape)?.permute((0, 2, 1, 3))?; + let mut q = q.view(qkv_shape)?.permute((0, 2, 1, 3))?; + let v = v.view(qkv_shape)?.permute((0, 2, 1, 3))?; + let cache_entry = if cache.borrow_mut().use_kv_cache() { + let cache_ref = cache.borrow_mut(); + let entry = cache_ref[block_idx].clone(); + Some(entry) + } else { + None + }; + let offset = cache_entry.as_ref().map(|kv| kv.entries).unwrap_or(0); // Apply RoPE if enabled if let Some(rope) = &self.rope { - q = rope.schedule(RotaryInput { - input: q, - offset: index_pos, - })?; - k = rope.schedule(RotaryInput { - input: k, - offset: index_pos, - })?; + q = rope.schedule(RotaryInput { input: q, offset })?; + k = rope.schedule(RotaryInput { input: k, offset })?; } - let mut att = q.matmul(k, false, true)?.mul(self.softmax_scale.clone())?; + let (k, v) = if let Some(cache_entry) = cache_entry { + let k_cache = cache_entry.k_cache.cache(k, 2, offset)?; + let v_cache = cache_entry.v_cache.cache(v, 2, offset)?; + (k_cache, v_cache) + } else { + (k, v) + }; + + let mut att = q + .clone() + .matmul(k.clone(), false, true)? + .affine(1f32 / (self.h_dim as f32).sqrt(), 0f32)?; // Apply ALiBi if enabled if let Some(alibi) = &self.alibi { @@ -134,13 +163,22 @@ impl Module for GPT2SelfAttention { }; let att = att.softmax(3)?; + let att = match &self.attn_dropout { + Some(dropout) => dropout.schedule(att)?, + None => att, + }; + let y = att .clone() - .matmul(v, false, false)? - .permute(&[0, 2, 1, 3])? - .view(shape![batch_size as _, q_len, self.n_embd])?; + .matmul(v.clone(), false, false)? + .permute((0, 2, 1, 3))? + .view((batch_size, q_len, self.n_embd))?; let y = self.c_proj.schedule(y)?; + let y = match &self.resid_dropout { + Some(dropout) => dropout.schedule(y)?, + None => y, + }; Ok((y, att)) } @@ -148,7 +186,11 @@ impl Module for GPT2SelfAttention { fn masked_fill(on_false: &Tensor, mask: &Tensor, on_true: f32) -> anyhow::Result { let shape = mask.shape(); - let on_true = Tensor::full(shape, on_true, on_false.device()); - let m = mask.clone().where_cond(on_true, on_false.clone())?; + let on_true = Tensor::full( + shape, + on_true, + TensorOptions::new().device(on_false.device()), + )?; + let m = on_true.where_cond(mask.clone(), on_false.clone())?; Ok(m) } diff --git a/crates/piston-models/src/gpt2/embedding.rs b/crates/piston-models/src/gpt2/embedding.rs new file mode 100644 index 00000000..dda3b398 --- /dev/null +++ b/crates/piston-models/src/gpt2/embedding.rs @@ -0,0 +1,21 @@ +use maybe_async::maybe_async; +use piston_nn::{Embedding, Init, VarBuilder}; + +#[maybe_async] +pub async fn embedding_gpt2( + in_size: usize, + out_size: usize, + vb: VarBuilder<'_>, +) -> anyhow::Result { + let embeddings = vb + .get_with_hints( + (in_size, out_size), + "weight", + Init::Randn { + mean: 0., + stdev: 0.02, + }, + ) + .await?; + Ok(Embedding::new(embeddings, out_size)) +} diff --git a/crates/piston-models/src/gpt2/generate.rs b/crates/piston-models/src/gpt2/generate.rs new file mode 100644 index 00000000..cf221c01 --- /dev/null +++ b/crates/piston-models/src/gpt2/generate.rs @@ -0,0 +1,120 @@ +use crate::gpt2::{GPT2, GPT2Input}; +use maybe_async::maybe_async; +use ndarray::{Array3, Axis, Ix3}; +use ndarray_stats::QuantileExt; +use piston::{Device, Tensor, TensorOptions}; +use piston_nn::{Module, ModuleMode, ModuleModeGuard}; + +#[maybe_async] +pub async fn generate( + model: &mut GPT2, + prompt: Vec, + // The callback now receives: + // - A Vec of the tokens fed in this pass (either the prompt, or the latest token) + // - An ndarray (shape: [1, seq_len, vocab_size]) containing the logits for that pass. + // - An ndarray (shape: [1, seq_len, seq_len]) containing the attention probabilities for that pass. + callback: impl Fn(Vec, Array3, Vec), + max_tokens: usize, +) -> anyhow::Result>> { + let _eval_mode_guard = ModuleModeGuard::new(ModuleMode::Eval); + + // Preserve original cache setting and enable kv-cache for generation. + let use_kv_cache = model.cache_mut().use_kv_cache(); + // TODO: We'll replace this with training mode in the JS api. + model.cache_mut().set_use_kv_cache(true)?; + + // This vector will accumulate the logits (as ndarray on CPU) from each model call. + let mut all_logits_ndarray: Vec> = Vec::new(); + + // all_tokens holds the entire context (prompt + generated tokens). + let mut all_tokens = prompt.clone(); + // Count only the tokens that are generated (not in the original prompt) + let mut generated_count = 0; + + while generated_count < max_tokens && all_tokens[all_tokens.len() - 1] != 256 { + // For the first pass, feed the entire prompt. + // For subsequent passes, feed only the latest generated token. + let tokens_to_feed = if generated_count == 0 { + &all_tokens[..] + } else { + &all_tokens[all_tokens.len() - 1..] + }; + + // Build the input tensor from tokens_to_feed. + let input = Tensor::from_data( + tokens_to_feed, + (1, tokens_to_feed.len()), + TensorOptions::new().device(model.device.clone()), + )?; + + // The index_pos is the total length of the context so far. + let (result, attn_probs) = model.schedule(GPT2Input { + x: input, + index_pos: all_tokens.len(), + })?; + + // Bring the logits to the CPU. + let logits_cpu = result.to(&Device::CPU).await?; + + // Update the kv-cache: + // - For the first pass, update with the full length. + // - For subsequent passes, update only with the new token. + if generated_count == 0 { + model.cache_mut().update(all_tokens.len() + 1); + } else { + model.cache_mut().update(1); + } + + // Convert the logits to an owned ndarray. + // The logits have shape [1, seq_len, vocab_size] where: + // - seq_len == prompt.len() on first pass, or 1 on subsequent passes. + let logits_nd = logits_cpu + .inner() + .read() + .to_ndarray_view::() + .into_owned() + .into_dimensionality::() + .unwrap(); + // Store them in our accumulator. + all_logits_ndarray.push(logits_nd.clone()); + + let attn_probs_cpu = attn_probs + .to(&Device::CPU) + .await + .map_err(|e| e.to_string()) + .unwrap(); + let attn_probs_data = attn_probs_cpu + .to_vec::() + .await + .map_err(|e| e.to_string()) + .unwrap(); + + // *** Stream the current pass via the callback *** + // Pass the tokens that were fed (as a Vec) and the corresponding logits ndarray. + callback(tokens_to_feed.to_vec(), logits_nd.clone(), attn_probs_data); + + // Extract the logits for the last token in this pass: + // - For the first pass, that's at index (prompt.len() - 1). + // - For later passes, the only token is at index 0. + let seq_len_this_pass = tokens_to_feed.len(); + let last_logits = logits_nd.index_axis(Axis(1), seq_len_this_pass - 1); + // last_logits has shape [1, vocab_size]; take the 0th row. + let vocab_logits = last_logits.index_axis(Axis(0), 0); + // Use argmax_skipnan from ndarray_stats to get the next token id. + let next_token_id = vocab_logits.argmax_skipnan().unwrap() as i32; + + // Stop if the end-of-text token is generated. + if next_token_id == 50256 { + break; + } + + // Append the generated token to the full context. + all_tokens.push(next_token_id); + generated_count += 1; + } + + // Clean up: reset model state and restore the original kv-cache setting. + model.reset(); + model.cache_mut().set_use_kv_cache(use_kv_cache)?; + Ok(all_logits_ndarray) +} diff --git a/crates/ratchet-models/src/gpt2/linear.rs b/crates/piston-models/src/gpt2/linear.rs similarity index 63% rename from crates/ratchet-models/src/gpt2/linear.rs rename to crates/piston-models/src/gpt2/linear.rs index 42322383..465c3e1f 100644 --- a/crates/ratchet-models/src/gpt2/linear.rs +++ b/crates/piston-models/src/gpt2/linear.rs @@ -1,6 +1,5 @@ use maybe_async::maybe_async; -use ratchet::prelude::shape; -use ratchet_nn::{Linear, VarBuilder}; +use piston_nn::{Linear, VarBuilder}; #[maybe_async] pub async fn linear_gpt2( @@ -8,15 +7,15 @@ pub async fn linear_gpt2( out_dim: usize, vb: VarBuilder<'_>, ) -> anyhow::Result { - let init_ws = ratchet_nn::Init::Randn { + let init_ws = piston_nn::Init::Randn { mean: 0.0, stdev: 0.02, }; let ws = vb - .get_with_hints(shape![out_dim, in_dim], "weight", init_ws) + .get_with_hints((out_dim, in_dim), "weight", init_ws) .await?; - let init_bs = ratchet_nn::Init::Const(0.0); - let bs = vb.get_with_hints(shape![out_dim], "bias", init_bs).await?; + let init_bs = piston_nn::Init::Const(0.0); + let bs = vb.get_with_hints(out_dim, "bias", init_bs).await?; Ok(Linear::new(ws, Some(bs))) } @@ -26,12 +25,12 @@ pub async fn linear_no_bias_gpt2( out_dim: usize, vb: VarBuilder<'_>, ) -> anyhow::Result { - let init_ws = ratchet_nn::Init::Randn { + let init_ws = piston_nn::Init::Randn { mean: 0.0, stdev: 0.02, }; let ws = vb - .get_with_hints(shape![out_dim, in_dim], "weight", init_ws) + .get_with_hints((out_dim, in_dim), "weight", init_ws) .await?; Ok(Linear::new(ws, None)) } @@ -57,14 +56,14 @@ pub async fn linear_gpt2_residual( n_layer: usize, vb: VarBuilder<'_>, ) -> anyhow::Result { - let init_ws = ratchet_nn::Init::Randn { + let init_ws = piston_nn::Init::Randn { mean: 0.0, stdev: 0.02 / (2f32 * (n_layer as f32)).sqrt(), }; let ws = vb - .get_with_hints(shape![out_dim, in_dim], "weight", init_ws) + .get_with_hints((out_dim, in_dim), "weight", init_ws) .await?; - let init_bs = ratchet_nn::Init::Const(0.0); - let bs = vb.get_with_hints(shape![out_dim], "bias", init_bs).await?; + let init_bs = piston_nn::Init::Const(0.0); + let bs = vb.get_with_hints(out_dim, "bias", init_bs).await?; Ok(Linear::new(ws, Some(bs))) } diff --git a/crates/ratchet-models/src/gpt2/mlp.rs b/crates/piston-models/src/gpt2/mlp.rs similarity index 84% rename from crates/ratchet-models/src/gpt2/mlp.rs rename to crates/piston-models/src/gpt2/mlp.rs index 422e0b49..96fcd09c 100644 --- a/crates/ratchet-models/src/gpt2/mlp.rs +++ b/crates/piston-models/src/gpt2/mlp.rs @@ -1,6 +1,7 @@ use maybe_async::maybe_async; -use ratchet::Tensor; -use ratchet_nn::{Linear, Module, VarBuilder}; +use piston::Tensor; +use piston_macros::scoped_module; +use piston_nn::{Linear, Module, VarBuilder}; use super::{ linear::{linear_gpt2, linear_gpt2_residual}, @@ -11,7 +12,7 @@ use super::{ pub struct MLP { c_fc: Linear, c_proj: Linear, - hidden_act: ratchet_nn::Activation, + hidden_act: piston_nn::Activation, } impl MLP { @@ -26,6 +27,7 @@ impl MLP { } } +#[scoped_module] impl Module for MLP { type Input = Tensor; type Output = Tensor; diff --git a/crates/ratchet-models/src/gpt2/mod.rs b/crates/piston-models/src/gpt2/mod.rs similarity index 69% rename from crates/ratchet-models/src/gpt2/mod.rs rename to crates/piston-models/src/gpt2/mod.rs index da60582c..74a34d29 100644 --- a/crates/ratchet-models/src/gpt2/mod.rs +++ b/crates/piston-models/src/gpt2/mod.rs @@ -1,14 +1,14 @@ mod attn; -// mod generate; +mod embedding; +mod generate; mod linear; mod mlp; mod model; pub use model::Config; +pub use model::GPT2; pub use model::GPT2Input; pub use model::LayerNormPosition; pub use model::PositionalEncoding; -pub use model::GPT2; -// #[cfg(target_arch = "wasm32")] -// pub use generate::generate; +pub use generate::generate; diff --git a/crates/ratchet-models/src/gpt2/model.rs b/crates/piston-models/src/gpt2/model.rs similarity index 53% rename from crates/ratchet-models/src/gpt2/model.rs rename to crates/piston-models/src/gpt2/model.rs index e3f12af7..35ff758b 100644 --- a/crates/ratchet-models/src/gpt2/model.rs +++ b/crates/piston-models/src/gpt2/model.rs @@ -1,16 +1,39 @@ -/// This is not a true GPT2 model, in that we probably couldn't load the weights from a GGML file. +/// GPT2 model, state-dict-compatible with minGPT, except for "bias" buffer, which is easy enough +/// to add back in: +/// +/// ```python +/// from safetensors.torch import load_file +/// n_layer = 6 +/// state_dict = load_file("our-gpt2.safetensors") +/// bias_buffer = torch.tril(torch.ones(24, 24)).view(1, 1, 24, 24) +/// state_dict["lm_head.bias"] = bias_buffer +/// for i in range(n_layer): +/// state_dict[f"transformer.h.{i}.attn.bias"] = bias_buffer +/// ``` +/// +/// You can then load the state_dict into minGPT with: +/// +/// ```python +/// from mingpt.model import GPT +/// config = GPT.get_default_config() +/// # ... set config appropriately ... +/// model = GPT(config) +/// model.load_state_dict(state_dict) +/// ``` use std::{cell::RefCell, rc::Rc}; use super::{ attn::{GPT2AttnInput, GPT2SelfAttention}, + embedding::embedding_gpt2, linear::linear_no_bias_gpt2, mlp::MLP, }; use maybe_async::maybe_async; -use ratchet::{shape, Device, Tensor}; -use ratchet_nn::{ - embedding, layer_norm, Embedding, KVCache, LayerNorm, Linear, Module, SinusoidalEmbedding, - SinusoidalInput, VarBuilder, +use piston::{D, DType, Device, Tensor, TensorOptions, arange, stack}; +use piston_macros::scoped_module; +use piston_nn::{ + Dropout, Embedding, KVCache, LayerNorm, Linear, Module, SinusoidalEmbedding, SinusoidalInput, + VarBuilder, layer_norm, }; #[derive(Debug, Clone, PartialEq)] @@ -19,19 +42,21 @@ pub enum PositionalEncoding { RoPE, ALiBi, Sinusoidal, + None, } #[derive(Debug, Clone, PartialEq)] pub enum LayerNormPosition { Pre, Post, + None, } // https://huggingface.co/microsoft/Phi-3-mini-4k-instruct/blob/main/config.json #[derive(Debug, Clone)] pub struct Config { pub vocab_size: usize, - pub hidden_act: ratchet_nn::Activation, + pub hidden_act: piston_nn::Activation, pub n_embd: usize, pub n_layer: usize, pub n_head: usize, @@ -39,6 +64,9 @@ pub struct Config { pub attention_only: bool, pub positional_encoding: PositionalEncoding, pub layernorm_position: LayerNormPosition, + pub embd_pdrop: f32, + pub attn_pdrop: f32, + pub resid_pdrop: f32, } impl Config { @@ -49,37 +77,38 @@ impl Config { #[derive(Debug)] pub struct DecoderLayer { - input_norm: LayerNorm, - self_attn: GPT2SelfAttention, - ffn_norm: LayerNorm, + ln_1: LayerNorm, + attn: GPT2SelfAttention, + ln_2: LayerNorm, mlp: MLP, attention_only: bool, layernorm_position: LayerNormPosition, + dropout: Option, } impl DecoderLayer { #[maybe_async] async fn new(cfg: &Config, vb: VarBuilder<'_>) -> anyhow::Result { - let self_attn = GPT2SelfAttention::new(cfg, vb.pp("self_attn")).await?; + let attn = GPT2SelfAttention::new(cfg, vb.pp("attn")).await?; - let input_norm = - layer_norm(cfg.n_embd, Default::default(), vb.pp("input_layernorm")).await?; - let ffn_norm = layer_norm( - cfg.n_embd, - Default::default(), - vb.pp("post_attention_layernorm"), - ) - .await?; + let ln_1 = layer_norm(cfg.n_embd, Default::default(), vb.pp("ln_1")).await?; + let ln_2 = layer_norm(cfg.n_embd, Default::default(), vb.pp("ln_2")).await?; let mlp = MLP::new(cfg, vb.pp("mlp")).await?; + let dropout = if cfg.resid_pdrop > 0.0 { + Some(Dropout::new(cfg.resid_pdrop)) + } else { + None + }; Ok(Self { - self_attn, + attn, mlp, - input_norm, - ffn_norm, + ln_1, + ln_2, attention_only: cfg.attention_only, layernorm_position: cfg.layernorm_position.clone(), + dropout, }) } } @@ -92,6 +121,7 @@ pub struct DecoderLayerInput { pub cache: Rc>, } +#[scoped_module] impl Module for DecoderLayer { type Input = DecoderLayerInput; type Output = (Tensor, Tensor); @@ -105,46 +135,42 @@ impl Module for DecoderLayer { } = input; let residual = x.clone(); - let (attn_output, attn_masks) = match self.layernorm_position { - LayerNormPosition::Pre => { - let xs = self.input_norm.schedule(x)?; - let (attn_output, attn_masks) = self.self_attn.schedule(GPT2AttnInput { - input: xs, - index_pos, - block_idx, - cache, - })?; - (residual.add(attn_output)?, attn_masks) - } - LayerNormPosition::Post => { - let (attn_output, attn_masks) = self.self_attn.schedule(GPT2AttnInput { - input: x, - index_pos, - block_idx, - cache, - })?; - let xs = self.input_norm.schedule(residual.add(attn_output)?)?; - (xs, attn_masks) - } + let x = match self.layernorm_position { + LayerNormPosition::Pre => self.ln_1.schedule(x)?, + LayerNormPosition::Post | LayerNormPosition::None => x, + }; + let (attn_output, attn_masks) = self.attn.schedule(GPT2AttnInput { + input: x, + index_pos, + block_idx, + cache, + })?; + let x = residual.add(attn_output)?; + let x = match self.layernorm_position { + LayerNormPosition::Post => self.ln_1.schedule(x)?, + LayerNormPosition::Pre | LayerNormPosition::None => x, }; // Skip the feed-forward network if attention_only is true if !self.attention_only { - let residual = attn_output.clone(); - let xs = match self.layernorm_position { - LayerNormPosition::Pre => { - let xs = self.ffn_norm.schedule(attn_output)?; - let xs = self.mlp.schedule(xs)?; - residual.add(xs)? - } - LayerNormPosition::Post => { - let xs = self.mlp.schedule(self.ffn_norm.schedule(attn_output)?)?; - self.ffn_norm.schedule(residual.add(xs)?)? - } + let residual = x.clone(); + let x = match self.layernorm_position { + LayerNormPosition::Pre => self.ln_2.schedule(x)?, + LayerNormPosition::Post | LayerNormPosition::None => x, }; - Ok((xs, attn_masks)) + let x = self.mlp.schedule(x)?; + let x = match &self.dropout { + Some(dropout) => dropout.schedule(x)?, + None => x, + }; + let x = residual.add(x)?; + let x = match self.layernorm_position { + LayerNormPosition::Post => self.ln_2.schedule(x)?, + LayerNormPosition::Pre | LayerNormPosition::None => x, + }; + Ok((x, attn_masks)) } else { - Ok((attn_output, attn_masks)) + Ok((x, attn_masks)) } } } @@ -155,8 +181,9 @@ pub struct GPT2 { pub wpe: Option, pub sinusoidal: Option, pub layers: Vec, - pub ln_post: LayerNorm, + pub ln_f: LayerNorm, pub lm_head: Linear, + pub embd_dropout: Option, pub kv_cache: Rc>, pub device: Device, } @@ -166,21 +193,27 @@ pub struct GPT2Input { pub index_pos: usize, } +#[scoped_module] impl Module for GPT2 { type Input = GPT2Input; type Output = (Tensor, Tensor); fn schedule(&self, input: Self::Input) -> anyhow::Result { let GPT2Input { x, index_pos } = input; - let [b_size, seq_len]: [usize; 2] = x.shape().try_into()?; + let (b_size, seq_len) = x.shape().dims2()?; let mut x = self.wte.schedule(x)?; // Add positional embeddings based on the type if let Some(wpe) = &self.wpe { // Learned embeddings - let pos = Tensor::arange(0, seq_len as i32, x.device())?; - let pos = pos.unsqueeze(0)?.broadcast_to(shape![b_size, seq_len])?; + let pos = arange( + Some(0.0), + seq_len as f32, + None, + TensorOptions::new().device(x.device()).dtype(DType::I32), + )?; + let pos = pos.unsqueeze(0)?.broadcast_to((b_size, seq_len))?; let position_embeds = wpe.schedule(pos)?; x = x.add(position_embeds)?; } else if let Some(sinusoidal) = &self.sinusoidal { @@ -192,6 +225,11 @@ impl Module for GPT2 { } // For RoPE and ALiBi, positional encoding is handled in the attention layer + let mut x = match &self.embd_dropout { + Some(dropout) => dropout.schedule(x)?, + None => x, + }; + let mut attn_masks = vec![]; for (block_idx, layer) in self.layers.iter().enumerate() { @@ -203,11 +241,11 @@ impl Module for GPT2 { }; let (layer_output, layer_attn_masks) = layer.schedule(input)?; x = layer_output; - attn_masks.push(layer_attn_masks.flatten_from(2)?); + attn_masks.push(layer_attn_masks.flatten(2, D::Minus1)?); } - let attn_masks = Tensor::stack(attn_masks.into(), 0)?; - x = self.ln_post.schedule(x)?; + let attn_masks = stack(attn_masks.into(), 0)?; + x = self.ln_f.schedule(x)?; let logits = self.lm_head.schedule(x)?; Ok((logits, attn_masks)) } @@ -217,14 +255,14 @@ impl Module for GPT2 { impl GPT2 { const MAX_CACHE: usize = 4096; - pub async fn new(cfg: &Config, vb: VarBuilder<'_>) -> anyhow::Result { - let vb_m = vb.pp("model"); - let wte = embedding(cfg.vocab_size, cfg.n_embd, vb_m.pp("wte")).await?; + pub async fn new(cfg: &Config, vb: VarBuilder<'_>, use_kv_cache: bool) -> anyhow::Result { + let vb_m = vb.pp("transformer"); + let wte = embedding_gpt2(cfg.vocab_size, cfg.n_embd, vb_m.pp("wte")).await?; // Initialize positional encoding based on the type let (wpe, sinusoidal) = match cfg.positional_encoding { PositionalEncoding::Learned => ( - Some(embedding(cfg.block_size, cfg.n_embd, vb_m.pp("wpe")).await?), + Some(embedding_gpt2(cfg.block_size, cfg.n_embd, vb_m.pp("wpe")).await?), None, ), PositionalEncoding::Sinusoidal => ( @@ -232,57 +270,70 @@ impl GPT2 { Some(SinusoidalEmbedding::new(cfg.n_embd, vb.device())?), ), PositionalEncoding::RoPE | PositionalEncoding::ALiBi => (None, None), + PositionalEncoding::None => (None, None), }; let n_layers = cfg.n_layer as _; let mut layers = Vec::with_capacity(n_layers); - let vb_l = vb_m.pp("layers"); + let vb_l = vb_m.pp("h"); for layer_idx in 0..n_layers { let layer = DecoderLayer::new(cfg, vb_l.pp(layer_idx)).await?; layers.push(layer) } - let ln_post = layer_norm(cfg.n_embd, Default::default(), vb_m.pp("norm")).await?; + let ln_f = layer_norm(cfg.n_embd, Default::default(), vb_m.pp("ln_f")).await?; let lm_head = linear_no_bias_gpt2(cfg.n_embd, cfg.vocab_size, vb.pp("lm_head")).await?; + let embd_dropout = if cfg.embd_pdrop > 0.0 { + Some(Dropout::new(cfg.embd_pdrop)) + } else { + None + }; - let cache_shape = shape![1, n_layers as _, Self::MAX_CACHE, cfg.head_dim() as _]; + let cache_shape = (1, cfg.n_head as _, Self::MAX_CACHE, cfg.head_dim() as _); Ok(Self { wte, wpe, sinusoidal, layers, - ln_post, + ln_f, lm_head, - kv_cache: Rc::new(RefCell::new(KVCache::new::( + embd_dropout, + kv_cache: Rc::new(RefCell::new(KVCache::new::( n_layers as _, - false, + use_kv_cache, cache_shape, vb.device(), - ))), + )?)), device: vb.device().clone(), }) } + + pub fn reset(&mut self) { + self.kv_cache.borrow_mut().reset(); + } + + pub fn cache_mut(&mut self) -> std::cell::RefMut<'_, KVCache> { + (*self.kv_cache).borrow_mut() + } } #[cfg(all(test, not(target_arch = "wasm32"), feature = "pyo3"))] mod tests { - use ratchet::{prelude::shape, DType, Device, DeviceRequest, Tensor, Var}; - use ratchet_datasets::{ - nlp::{ - tinystories::{Dataset, DatasetRandomIter}, - toy::{ToyTaskIter, TwoSumTask}, - }, + use piston::{DType, Device, DeviceRequest, TensorOptions, zeros}; + use piston_datasets::{ Batcher, + nlp::tinystories::{Dataset, DatasetRandomIter}, }; - use ratchet_nn::{ - clip_grad_norm, cross_entropy, AdamW, ConstantLR, LRScheduler, Module, Optimizer, - ParamsAdamW, VarBuilder, VarMap, - }; + use piston_nn::{AdamW, Module, Optimizer, ParamsAdamW, VarBuilder, VarMap, cross_entropy}; use super::GPT2; - use crate::gpt2::model::{Config, GPT2Input, PositionalEncoding}; + use crate::gpt2::{ + LayerNormPosition, + // generate, + model::{Config, GPT2Input, PositionalEncoding}, + }; #[test] #[cfg_attr(feature = "ci", ignore)] @@ -295,7 +346,7 @@ mod tests { let config = Config { vocab_size: VOCAB_SIZE, - hidden_act: ratchet_nn::Activation::Relu2, + hidden_act: piston_nn::Activation::Relu2, n_embd: 128, n_layer: 4, n_head: 4, @@ -303,12 +354,15 @@ mod tests { attention_only: false, positional_encoding: PositionalEncoding::Learned, layernorm_position: LayerNormPosition::Pre, + embd_pdrop: 0.1, + attn_pdrop: 0.1, + resid_pdrop: 0.1, }; let varmap = VarMap::new(); - let vb = VarBuilder::from_varmap(&varmap, ratchet::DType::F32, &device.clone()); + let vb = VarBuilder::from_varmap(&varmap, piston::DType::F32, &device.clone()); - let model = GPT2::new(&config, vb)?; + let model = GPT2::new(&config, vb, false)?; let params = ParamsAdamW { lr: 1e-4, @@ -317,33 +371,37 @@ mod tests { ..Default::default() }; - let mut opt = AdamW::new( - varmap - .all_labeled_vars() - .iter() - .map(|(label, var)| (Some(label.to_owned()), var.to_owned())) - .collect::, Var)>>(), - params, - )?; + let mut opt = AdamW::new(varmap.all_vars(), params)?; const BATCH_SIZE: usize = 1; - for step in 0..100 { - let input = Tensor::zeros::(&shape![BATCH_SIZE, config.block_size], &device); - let tgt = Tensor::zeros::(&shape![BATCH_SIZE, config.block_size], &device); + for step in 0..3 { + let input = zeros( + (BATCH_SIZE, config.block_size), + TensorOptions::new() + .device(device.clone()) + .dtype(DType::I32), + )?; + let tgt = zeros( + (BATCH_SIZE, config.block_size), + TensorOptions::new() + .device(device.clone()) + .dtype(DType::I32), + )?; let (logits, _) = model.schedule(GPT2Input { x: input, index_pos: 0, })?; - let loss = cross_entropy(logits.flatten_to(1)?, tgt.flatten_to(1)?)?; + let loss = cross_entropy(logits.flatten(0, 1)?, tgt.flatten(0, 1)?)?; - let grads = loss.backward()?; + loss.backward()?; // clip_grad_norm(&mut grads, 1.0f32, &device)?; + opt.zero_grad(false)?; - opt.step(&grads, &device)?; + opt.step(&device)?; let loss_vec = loss.clone().to(&Device::CPU)?.to_vec::()?; @@ -364,7 +422,7 @@ mod tests { let config = Config { vocab_size: VOCAB_SIZE, - hidden_act: ratchet_nn::Activation::Relu2, + hidden_act: piston_nn::Activation::Relu2, n_embd: 128, n_layer: 1, n_head: 1, @@ -372,25 +430,34 @@ mod tests { attention_only: false, positional_encoding: PositionalEncoding::Learned, layernorm_position: LayerNormPosition::Pre, + embd_pdrop: 0.1, + attn_pdrop: 0.1, + resid_pdrop: 0.1, }; let varmap = VarMap::new(); - let vb = VarBuilder::from_varmap(&varmap, ratchet::DType::F32, &device.clone()); + let vb = VarBuilder::from_varmap(&varmap, piston::DType::F32, &device.clone()); - let model = GPT2::new(&config, vb)?; + let model = GPT2::new(&config, vb, false)?; const BATCH_SIZE: usize = 10; for batch_index in 0..10 { - let input = Tensor::zeros::(&shape![BATCH_SIZE, config.block_size], &device); - let tgt = Tensor::zeros::(&shape![BATCH_SIZE, config.block_size], &device); + let input = zeros( + (BATCH_SIZE, config.block_size), + TensorOptions::new().device(device.clone()), + )?; + let tgt = zeros( + (BATCH_SIZE, config.block_size), + TensorOptions::new().device(device.clone()), + )?; let (logits, _) = model.schedule(GPT2Input { x: input, index_pos: 0, })?; - let loss = cross_entropy(logits.flatten_to(1)?, tgt.flatten_to(1)?)?; + let loss = cross_entropy(logits.flatten(0, 1)?, tgt.flatten(0, 1)?)?; device.try_gpu()?.mark_step()?; @@ -402,82 +469,6 @@ mod tests { Ok(()) } - #[test] - #[cfg_attr(feature = "ci", ignore)] - fn train_2_sum() -> anyhow::Result<()> { - let _ = env_logger::builder().is_test(true).try_init(); - - const VOCAB_SIZE: usize = 256; - - const BATCH_SIZE: usize = 8; - const SEQUENCE_LENGTH: usize = 24; - - let config = Config { - vocab_size: VOCAB_SIZE, - hidden_act: ratchet_nn::Activation::Relu2, - n_embd: 768, - n_layer: 4, - n_head: 4, - block_size: SEQUENCE_LENGTH, - attention_only: false, - positional_encoding: PositionalEncoding::Sinusoidal, - layernorm_position: LayerNormPosition::Pre, - }; - - let device = Device::request_device(ratchet::DeviceRequest::GPU).unwrap(); - - let varmap = VarMap::new(); - let vb = VarBuilder::from_varmap(&varmap, DType::F32, &device.clone()); - - let model = GPT2::new(&config, vb).unwrap(); - - let params = ParamsAdamW { - lr: 1e-4, - // lr: 0.0, - // weight_decay: 0.1, - ..Default::default() - }; - - let opt = AdamW::new( - varmap - .all_labeled_vars() - .iter() - .map(|(label, var)| (Some(label.to_owned()), var.to_owned())) - .collect::, Var)>>(), - params, - )?; - - let mut lr_scheduler = ConstantLR::new(opt, 1.0, 100); - - let task = TwoSumTask::new(5, 5, Some(10)); - let dataset_iter = ToyTaskIter::new(task, device.clone()); - let batch_iter = Batcher::new_r2(dataset_iter).batch_size(BATCH_SIZE); - - for (batch_index, batch) in batch_iter.enumerate() { - if batch_index > 10 { - break; - } - - let (input, tgt) = batch?; - - let (logits, _) = model.schedule(GPT2Input { - x: input, - index_pos: 0, - })?; - - let loss = cross_entropy(logits.flatten_to(1)?, tgt.flatten_to(1)?)?; - - let grads = loss.backward()?; - - lr_scheduler.step(&grads, &device)?; - - let loss_vec = loss.clone().to(&Device::CPU)?.to_vec::()?; - - println!("{:?} loss: {:?}, norm: {:?}", batch_index, loss_vec[0], "?"); - } - Ok(()) - } - #[test] #[cfg_attr(feature = "ci", ignore)] fn train_tinystories() -> anyhow::Result<()> { @@ -499,12 +490,12 @@ mod tests { const BATCH_SIZE: usize = 1; let varmap = VarMap::new(); - let vb = VarBuilder::from_varmap(&varmap, ratchet::DType::F32, &device.clone()); + let vb = VarBuilder::from_varmap(&varmap, piston::DType::F32, &device.clone()); // distilgpt2-sized let config = Config { vocab_size: VOCAB_SIZE, - hidden_act: ratchet_nn::Activation::Relu2, + hidden_act: piston_nn::Activation::Relu2, n_embd: 768, n_layer: 12, n_head: 12, @@ -512,12 +503,15 @@ mod tests { attention_only: false, positional_encoding: PositionalEncoding::Learned, layernorm_position: LayerNormPosition::Pre, + embd_pdrop: 0.1, + attn_pdrop: 0.1, + resid_pdrop: 0.1, }; let iter = DatasetRandomIter::new(&dataset, false, config.block_size, device.clone()); let batch_iter = Batcher::new_r2(iter).batch_size(BATCH_SIZE); - let model = GPT2::new(&config, vb)?; + let model = GPT2::new(&config, vb, false)?; let params = ParamsAdamW { lr: 0.0001, @@ -526,14 +520,7 @@ mod tests { ..Default::default() }; - let mut opt = AdamW::new( - varmap - .all_labeled_vars() - .iter() - .map(|(label, var)| (Some(label.to_owned()), var.to_owned())) - .collect::, Var)>>(), - params, - )?; + let mut opt = AdamW::new(varmap.all_vars(), params)?; for (batch_index, batch) in batch_iter.enumerate() { let (input, tgt) = batch?; @@ -542,12 +529,14 @@ mod tests { index_pos: 0, })?; - let loss = cross_entropy(logits.flatten_to(1)?, tgt.flatten_to(1)?)?; + let loss = cross_entropy(logits.flatten(0, 1)?, tgt.flatten(0, 1)?)?; // This is something of a hack; we add references to all tensors that need to be backpropped - let grads = loss.backward()?; + loss.backward()?; - opt.step(&grads, &device)?; + opt.zero_grad(false)?; + + opt.step(&device)?; let loss_vec = loss.to(&Device::CPU)?.to_vec::()?; @@ -556,4 +545,45 @@ mod tests { Ok(()) } + + #[test] + fn generate_from_initialization() -> anyhow::Result<()> { + let _ = env_logger::builder().is_test(true).try_init(); + + let device = Device::request_device(DeviceRequest::GPU).unwrap(); + + let varmap = VarMap::new(); + let vb = VarBuilder::from_varmap(&varmap, piston::DType::F32, &device.clone()); + + let config = Config { + vocab_size: 256, + hidden_act: piston_nn::Activation::Relu2, + n_embd: 128, + n_layer: 1, + n_head: 1, + block_size: 20, + attention_only: false, + positional_encoding: PositionalEncoding::Learned, + layernorm_position: LayerNormPosition::Pre, + embd_pdrop: 0.1, + attn_pdrop: 0.1, + resid_pdrop: 0.1, + }; + + let mut model = GPT2::new(&config, vb, false)?; + + // Uncomment this to generate a uninitialized sequence from the model: + // generate( + // &mut model, + // "Hello, world".chars().map(|c| c as i32).collect(), + // |s, logits| { + // println!("{:?}", s); + // println!("{:?}", logits); + // }, + // 24, + // ) + // .unwrap(); + + Ok(()) + } } diff --git a/crates/piston-models/src/lib.rs b/crates/piston-models/src/lib.rs new file mode 100644 index 00000000..d4082238 --- /dev/null +++ b/crates/piston-models/src/lib.rs @@ -0,0 +1,4 @@ +#![allow(clippy::upper_case_acronyms)] +pub mod gpt2; +mod token_stream; +pub use token_stream::TokenOutputStream; diff --git a/crates/ratchet-models/src/token_stream.rs b/crates/piston-models/src/token_stream.rs similarity index 100% rename from crates/ratchet-models/src/token_stream.rs rename to crates/piston-models/src/token_stream.rs diff --git a/crates/ratchet-nn/Cargo.toml b/crates/piston-nn/Cargo.toml similarity index 78% rename from crates/ratchet-nn/Cargo.toml rename to crates/piston-nn/Cargo.toml index 0a9f3ea8..350bc87b 100644 --- a/crates/ratchet-nn/Cargo.toml +++ b/crates/piston-nn/Cargo.toml @@ -1,24 +1,23 @@ [package] -name = "ratchet-nn" +name = "piston-nn" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -pyo3 = ["ratchet/pyo3"] -plotting = ["ratchet/plotting"] -debug = ["ratchet/debug"] +pyo3 = ["piston/pyo3"] +plotting = ["piston/plotting"] +debug = ["piston/debug"] [dependencies] anyhow.workspace = true log.workspace = true derive-new = { workspace = true } -half = { workspace = true } -ratchet = { path = "../ratchet-core" } +piston = { path = "../piston-core" } +piston-macros = { path = "../piston-macros" } wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } -wasm-bindgen-test = { workspace = true } safetensors = { workspace = true } thiserror = { workspace = true } ndarray = { workspace = true } @@ -36,11 +35,11 @@ maybe-async = { workspace = true } proptest = { workspace = true } test-strategy = { workspace = true } # hf-hub = { workspace = true } -ratchet-loader = { path = "../ratchet-loader" } tokenizers = { version = "0.19.1", default-features = false, features = [ "unstable_wasm", ] } +wasm-bindgen-test = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] -features = ["Blob", "HtmlAnchorElement", "CssStyleDeclaration"] +features = ["Blob", "Url"] workspace = true diff --git a/crates/ratchet-nn/src/activation.rs b/crates/piston-nn/src/activation.rs similarity index 93% rename from crates/ratchet-nn/src/activation.rs rename to crates/piston-nn/src/activation.rs index 580360c8..aa938aaf 100644 --- a/crates/ratchet-nn/src/activation.rs +++ b/crates/piston-nn/src/activation.rs @@ -1,6 +1,6 @@ -use ratchet::Tensor; - use crate::Module; +use piston::Tensor; +use piston_macros::scoped_module; #[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum Activation { @@ -13,6 +13,7 @@ pub enum Activation { Swiglu, } +#[scoped_module] impl Module for Activation { type Input = Tensor; type Output = Tensor; diff --git a/crates/ratchet-nn/src/alibi.rs b/crates/piston-nn/src/alibi.rs similarity index 68% rename from crates/ratchet-nn/src/alibi.rs rename to crates/piston-nn/src/alibi.rs index 621d1b8e..ed88dc73 100644 --- a/crates/ratchet-nn/src/alibi.rs +++ b/crates/piston-nn/src/alibi.rs @@ -1,4 +1,5 @@ -use ratchet::Tensor; +use piston::{Tensor, TensorOptions, zeros}; +use piston_macros::scoped_module; use crate::Module; @@ -12,6 +13,7 @@ pub struct AlibiInput { pub input: Tensor, } +#[scoped_module] impl Module for AlibiEmbedding { type Input = AlibiInput; type Output = Tensor; @@ -19,7 +21,10 @@ impl Module for AlibiEmbedding { fn schedule(&self, input: Self::Input) -> anyhow::Result { let AlibiInput { input } = input; // To make broadcasting work... - let alibi = Tensor::zeros::(&input.shape()[..3].into(), input.device()); + let alibi = zeros( + &input.shape()[..3], + TensorOptions::new().device(input.device()), + )?; input + alibi.alibi(self.max_bias)?.unsqueeze(3) } } diff --git a/crates/piston-nn/src/dropout.rs b/crates/piston-nn/src/dropout.rs new file mode 100644 index 00000000..87218efc --- /dev/null +++ b/crates/piston-nn/src/dropout.rs @@ -0,0 +1,44 @@ +use crate::{Module, ModuleMode, current_module_mode}; +use derive_new::new; +use piston::{Tensor, TensorOptions}; +use piston_macros::scoped_module; + +/// Dropout layer that randomly zeroes some of the elements of the input tensor with probability `p` +/// during training, and rescales the remaining elements by a factor of `1/(1-p)`. +/// +/// This is a common regularization technique used in neural networks to prevent overfitting. +#[derive(Debug, Clone, new)] +pub struct Dropout { + /// Probability of an element to be zeroed. Value range: (0, 1) + p: f32, +} + +#[scoped_module] +impl Module for Dropout { + type Input = Tensor; + type Output = Tensor; + + fn schedule(&self, input: Self::Input) -> anyhow::Result { + if self.p <= 0.0 || current_module_mode() == ModuleMode::Eval { + return Ok(input); + } + + // Create a tensor of probabilities (1-p) to keep elements + let keep_prob = 1.0 - self.p; + let probs = Tensor::full::( + input.shape(), + keep_prob, + TensorOptions::new().device(input.device()), + )?; + + // Apply bernoulli sampling to get binary mask + let mask = probs.bernoulli()?; + + // Scale the mask by 1/(1-p) to maintain the expected value of the output + let scale = 1.0 / keep_prob; + let scaled_mask = mask.affine(scale, 0.0)?; + + // Apply the mask by multiplying element-wise with the input + input.mul(scaled_mask) + } +} diff --git a/crates/ratchet-nn/src/embedding.rs b/crates/piston-nn/src/embedding.rs similarity index 80% rename from crates/ratchet-nn/src/embedding.rs rename to crates/piston-nn/src/embedding.rs index 6e8eaab2..fb189537 100644 --- a/crates/ratchet-nn/src/embedding.rs +++ b/crates/piston-nn/src/embedding.rs @@ -1,5 +1,7 @@ use crate::Module; -use ratchet::{shape, Shape, Tensor}; +use maybe_async::maybe_async; +use piston::{D, DType, Shape, Tensor}; +use piston_macros::scoped_module; /// # Embedding /// @@ -21,21 +23,23 @@ impl Embedding { } } +#[scoped_module] impl Module for Embedding { type Input = Tensor; type Output = Tensor; fn schedule(&self, input: Self::Input) -> anyhow::Result { + assert_eq!(input.dtype(), DType::I32); let mut final_dims = input.shape().to_vec(); final_dims.push(self.hidden_size); - let indexes = input.flatten_all()?; + let indexes = input.flatten(0, D::Minus1)?; let values = self.weight.clone().index_select(indexes.clone(), 0)?; let values = values.view(Shape::from(final_dims))?; Ok(values) } } -#[cfg(target_arch = "wasm32")] +#[maybe_async] pub async fn embedding( in_size: usize, out_size: usize, @@ -43,7 +47,7 @@ pub async fn embedding( ) -> anyhow::Result { let embeddings = vb .get_with_hints( - shape![in_size, out_size], + (in_size, out_size), "weight", crate::Init::Randn { mean: 0., @@ -54,34 +58,16 @@ pub async fn embedding( Ok(Embedding::new(embeddings, out_size)) } -#[cfg(not(target_arch = "wasm32"))] -pub fn embedding( - in_size: usize, - out_size: usize, - vb: crate::VarBuilder, -) -> anyhow::Result { - let embeddings = vb.get_with_hints( - shape![in_size, out_size], - "weight", - crate::Init::Randn { - mean: 0., - stdev: 1., - }, - )?; - Ok(Embedding::new(embeddings, out_size)) -} - #[cfg(all(test, feature = "pyo3"))] mod tests { use hf_hub::api::sync::Api; use proptest::arbitrary::Arbitrary; use proptest::strategy::{BoxedStrategy, Just, Strategy}; - use ratchet_loader::gguf::gguf::Header; use test_strategy::proptest; use tokenizers::Tokenizer; - use ratchet::test_util::run_py_prg; - use ratchet::{rvec, shape, Device, DeviceRequest, Shape, Tensor}; + use piston::test_util::run_py_prg; + use piston::{Device, DeviceRequest, Shape, Tensor, rvec, shape}; use crate::{Embedding, Module}; @@ -97,7 +83,7 @@ mod tests { .prop_flat_map(|vocab_shape| (Just(vocab_shape), 1..64usize)) .prop_map(|(vocab_shape, num_indices)| { let indices = - Tensor::randint(0, vocab_shape[0] as i32, shape![num_indices], Device::CPU); + Tensor::randint(0, vocab_shape[0] as i32, num_indices, Device::CPU).unwrap(); EmbeddingProblem { vocab_shape, indices, @@ -119,7 +105,7 @@ def embedding(weight, indices): "#, arg ); - run_py_prg(prg.to_string(), &[weight, indices], &[], weight.dt()) + run_py_prg(prg.to_string(), &[weight, indices], &[], weight.dtype()) } fn run_embedding_trial(problem: EmbeddingProblem) { @@ -129,7 +115,7 @@ def embedding(weight, indices): vocab_shape, indices, } = problem; - let weight = Tensor::randn::(vocab_shape, Device::CPU); + let weight = Tensor::randn::(vocab_shape, Device::CPU).unwrap(); let ground_truth = ground_truth(&weight, &indices).unwrap(); @@ -152,7 +138,7 @@ def embedding(weight, indices): fn debug_embedding() { let prob = EmbeddingProblem { vocab_shape: shape![10000, 384], - indices: Tensor::from_data([400i32, 9001i32, 5555i32], shape![1, 3], Device::CPU), + indices: Tensor::from_data([400i32, 9001i32, 5555i32], (1, 3), Device::CPU), }; run_embedding_trial(prob); } diff --git a/crates/ratchet-nn/src/groupnorm.rs b/crates/piston-nn/src/groupnorm.rs similarity index 90% rename from crates/ratchet-nn/src/groupnorm.rs rename to crates/piston-nn/src/groupnorm.rs index 4be45f2b..e82e76c9 100644 --- a/crates/ratchet-nn/src/groupnorm.rs +++ b/crates/piston-nn/src/groupnorm.rs @@ -1,4 +1,5 @@ -use ratchet::Tensor; +use piston::Tensor; +use piston_macros::scoped_module; #[derive(Debug, Clone, Copy, PartialEq)] pub struct GroupNormConfig { @@ -42,13 +43,14 @@ impl GroupNorm { } } +#[scoped_module] impl crate::Module for GroupNorm { type Input = Tensor; type Output = Tensor; fn schedule(&self, input: Self::Input) -> anyhow::Result { input.group_norm( self.num_groups, - self.weight.clone(), + Some(self.weight.clone()), self.bias.clone(), self.eps, ) diff --git a/crates/ratchet-nn/src/init.rs b/crates/piston-nn/src/init.rs similarity index 74% rename from crates/ratchet-nn/src/init.rs rename to crates/piston-nn/src/init.rs index 316272bb..2727f1df 100644 --- a/crates/ratchet-nn/src/init.rs +++ b/crates/piston-nn/src/init.rs @@ -1,7 +1,7 @@ -//! Variable initialization. +//! Parameter initialization. // This is based on: // https://github.com/pytorch/pytorch/blob/07107919297db3f8ab37f11c12666b6d6d5f692e/torch/nn/init.py# -use ratchet::{Device, Shape, Tensor, Var}; +use piston::{Device, Shape, Tensor, TensorOptions, full, ones, rand, randn, zeros}; /// Number of features as input or output of a layer. /// In Kaiming initialization, choosing `FanIn` preserves @@ -110,13 +110,23 @@ pub const DEFAULT_KAIMING_NORMAL: Init = Init::Kaiming { impl Init { /// Creates a new tensor with the specified shape, device, and initialization. - pub fn var(&self, s: &Shape, device: Device) -> anyhow::Result { + pub fn var(&self, s: &Shape, device: Device) -> anyhow::Result { match self { - Self::Const(v) if *v == 0. => Ok(Var::zeros::(s, &device)), - Self::Const(v) if *v == 1. => Ok(Var::ones::(s, &device)), - Self::Const(cst) => Ok(Var::full(s, *cst, &device)), - Self::Uniform { lo, up } => Ok(Var::rand::(*lo, *up, s.clone(), device)), - Self::Randn { mean, stdev } => Ok(Var::randn::(*mean, *stdev, s.clone(), device)), + Self::Const(v) if *v == 0. => Ok(zeros(s, TensorOptions::new().device(device))?), + Self::Const(v) if *v == 1. => Ok(ones(s, TensorOptions::new().device(device))?), + Self::Const(cst) => Ok(full(s, *cst, TensorOptions::new().device(device))?), + Self::Uniform { lo, up } => Ok(rand( + s.clone(), + Some(*lo), + Some(*up), + TensorOptions::new().device(device), + )?), + Self::Randn { mean, stdev } => Ok(randn( + s.clone(), + Some(*mean), + Some(*stdev), + TensorOptions::new().device(device), + )?), Self::Kaiming { dist, fan, @@ -128,9 +138,19 @@ impl Init { match dist { NormalOrUniform::Uniform => { let bound = 3f32.sqrt() * std; - Ok(Var::rand::(-bound, bound, s.clone(), device)) + Ok(rand( + s.clone(), + Some(-bound), + Some(bound), + TensorOptions::new().device(device), + )?) } - NormalOrUniform::Normal => Ok(Var::randn::(0., std, s.clone(), device)), + NormalOrUniform::Normal => Ok(randn( + s.clone(), + Some(0.), + Some(std), + TensorOptions::new().device(device), + )?), } } } diff --git a/crates/piston-nn/src/kv_cache.rs b/crates/piston-nn/src/kv_cache.rs new file mode 100644 index 00000000..120bdd49 --- /dev/null +++ b/crates/piston-nn/src/kv_cache.rs @@ -0,0 +1,119 @@ +use anyhow::Result; +use num_traits::AsPrimitive; +use piston::{Device, HashMap, Shape, Tensor, TensorDType, TensorOptions, ones, zeros}; + +#[derive(Clone, Debug)] +pub struct KVEntry { + pub k_cache: Tensor, + pub v_cache: Tensor, + pub entries: usize, +} + +impl KVEntry { + pub fn allocate>( + shape: &Shape, + device: &Device, + ) -> Result { + Ok(KVEntry { + k_cache: zeros(shape, TensorOptions::new().device(device.clone()))?, + v_cache: zeros(shape, TensorOptions::new().device(device.clone()))?, + entries: 0, + }) + } +} + +#[derive(Clone, Debug)] +pub struct KVCache { + entries: Vec, + use_kv_cache: bool, + masks: HashMap, + device: Device, + n_layers: usize, + allocated: bool, + shape: Shape, +} + +impl std::ops::Index for KVCache { + type Output = KVEntry; + + fn index(&self, index: usize) -> &Self::Output { + &self.entries[index] + } +} + +impl KVCache { + pub fn new, S: Into>( + n_layers: i32, + use_kv_cache: bool, + shape: S, + device: &Device, + ) -> Result { + let shape: Shape = shape.into(); + let mut entries = Vec::with_capacity(n_layers as _); + // TODO: This is really bad; look at actual patterns for how people do KV caches + let mut allocated = false; + if use_kv_cache { + for _ in 0..n_layers { + entries.push(KVEntry::allocate::(&shape, device)?); + } + allocated = true; + } + Ok(KVCache { + entries, + masks: HashMap::default(), + device: device.clone(), + n_layers: n_layers as _, + use_kv_cache, + allocated, + shape, + }) + } + + pub fn update(&mut self, offset: usize) { + for entry in &mut self.entries { + entry.entries += offset; + } + } + + pub fn entries(&self, layer: usize) -> usize { + self.entries[layer].entries + } + + pub fn reset(&mut self) { + for entry in &mut self.entries { + entry.entries = 0; + } + } + + pub fn use_kv_cache(&self) -> bool { + self.use_kv_cache + } + + pub fn set_use_kv_cache(&mut self, use_kv_cache: bool) -> Result<()> { + self.use_kv_cache = use_kv_cache; + if !use_kv_cache && self.allocated { + self.entries.clear(); + self.allocated = false; + } else if use_kv_cache && !self.allocated { + for _ in 0..self.n_layers { + self.entries + .push(KVEntry::allocate::(&self.shape, &self.device)?); + } + self.allocated = true; + } + Ok(()) + } + + pub fn mask(&mut self, t: usize) -> Result { + if let Some(mask) = self.masks.get(&t) { + log::debug!("Using existing mask for {t:?}"); + Ok(mask.clone()) + } else { + log::debug!("Creating mask for {t:?}"); + let ones = ones((t, t), TensorOptions::new().device(self.device.clone()))?; + let mask = ones.tril(None)?; + self.masks.insert(t, mask.clone()); + Ok(mask) + } + } +} diff --git a/crates/ratchet-nn/src/lib.rs b/crates/piston-nn/src/lib.rs similarity index 90% rename from crates/ratchet-nn/src/lib.rs rename to crates/piston-nn/src/lib.rs index e9433e05..a0d815f2 100644 --- a/crates/ratchet-nn/src/lib.rs +++ b/crates/piston-nn/src/lib.rs @@ -1,5 +1,6 @@ mod activation; mod alibi; +mod dropout; mod embedding; mod groupnorm; mod init; @@ -11,12 +12,14 @@ mod norm; mod optim; mod rope; mod sinusoidal; +mod training_context; mod util; mod var_builder; mod var_map; pub use activation::*; pub use alibi::*; +pub use dropout::*; pub use embedding::*; pub use groupnorm::*; pub use init::*; @@ -28,11 +31,12 @@ pub use norm::*; pub use optim::*; pub use rope::*; pub use sinusoidal::*; +pub use training_context::*; pub use util::*; pub use var_builder::*; pub use var_map::*; -use ratchet::Tensor; +use piston::Tensor; /// # Module /// @@ -45,6 +49,8 @@ use ratchet::Tensor; pub trait Module { type Input; type Output; + + fn module_name(&self) -> &str; fn schedule(&self, input: Self::Input) -> anyhow::Result; } diff --git a/crates/ratchet-nn/src/linear.rs b/crates/piston-nn/src/linear.rs similarity index 86% rename from crates/ratchet-nn/src/linear.rs rename to crates/piston-nn/src/linear.rs index 7734168f..fb562816 100644 --- a/crates/ratchet-nn/src/linear.rs +++ b/crates/piston-nn/src/linear.rs @@ -1,5 +1,6 @@ use maybe_async::maybe_async; -use ratchet::{shape, Tensor}; +use piston::Tensor; +use piston_macros::scoped_module; use crate::Module; @@ -13,21 +14,22 @@ pub struct Linear { b: Option, } +#[scoped_module] impl Module for Linear { type Input = Tensor; type Output = Tensor; fn schedule(&self, input: Self::Input) -> anyhow::Result { let w = match *input.shape().to_vec() { - [b1, b2, _, _] => self.w.clone().broadcast_left(shape![b1, b2])?, - [bsize, _, _] => self.w.clone().broadcast_left(shape![bsize])?, + [b1, b2, _, _] => self.w.clone().broadcast_left((b1, b2))?, + [bsize, _, _] => self.w.clone().broadcast_left(bsize)?, _ => self.w.clone(), }; let x = input.matmul(w, false, true)?; match &self.b { None => Ok(x), - Some(b) => x.clone() + b.clone().cast(x.dt())?, + Some(b) => x.clone() + b.clone().cast(x.dtype())?, } } } @@ -40,14 +42,14 @@ pub async fn linear( ) -> anyhow::Result { let init_ws = crate::init::DEFAULT_KAIMING_NORMAL; let ws = vb - .get_with_hints(shape![out_dim, in_dim], "weight", init_ws) + .get_with_hints((out_dim, in_dim), "weight", init_ws) .await?; let bound = 1. / (in_dim as f32).sqrt(); let init_bs = crate::Init::Uniform { lo: -bound, up: bound, }; - let bs = vb.get_with_hints(shape![out_dim], "bias", init_bs).await?; + let bs = vb.get_with_hints(out_dim, "bias", init_bs).await?; Ok(Linear::new(ws, Some(bs))) } @@ -60,7 +62,7 @@ pub async fn linear_no_bias( ) -> anyhow::Result { let init_ws = crate::init::DEFAULT_KAIMING_NORMAL; let ws = vb - .get_with_hints(shape![out_dim, in_dim], "weight", init_ws) + .get_with_hints((out_dim, in_dim), "weight", init_ws) .await?; Ok(Linear::new(ws, None)) } @@ -79,15 +81,15 @@ pub async fn linear_b( } } -#[cfg(test)] +#[cfg(all(test, feature = "pyo3"))] mod tests { use crate::{Module, VarBuilder, VarMap}; - use super::{linear, linear_no_bias, Linear}; - use ratchet::{ - prelude::shape, test_util::run_py_prg_multiple, Device, DeviceRequest, Tensor, Var, + use super::{Linear, linear, linear_no_bias}; + use piston::{ + Device, DeviceRequest, Parameter, Tensor, prelude::shape, test_util::run_py_prg_multiple, }; - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; thread_local! { static GPU_DEVICE: Device = Device::request_device(DeviceRequest::GPU).unwrap(); @@ -128,9 +130,9 @@ def linear(x, w): with_bias: bool, ) -> anyhow::Result<()> { let device = GPU_DEVICE.with(|d| d.clone()); - let x = Tensor::randn::(0., 1., shape![batch_size, in_features], Device::CPU); + let x = Tensor::randn::(0., 1., (batch_size, in_features), Device::CPU)?; let varmap = VarMap::new(); - let vb = VarBuilder::from_varmap(&varmap, ratchet::DType::F32, &device.clone()); + let vb = VarBuilder::from_varmap(&varmap, piston::DType::F32, &device.clone()); let linear = if with_bias { linear(in_features, out_features, vb.clone())? @@ -235,11 +237,11 @@ def linear_backward(x, w): with_bias, } = problem; - let x = Tensor::randn::(0., 1., shape![batch_size, in_features], Device::CPU); + let x = Tensor::randn::(0., 1., (batch_size, in_features), Device::CPU)?; let x_gpu = x.to(&device)?; - let x_var = Var::from_tensor(&x_gpu)?; + let x_var = Parameter::from_tensor(&x_gpu)?; let varmap = VarMap::new(); - let vb = VarBuilder::from_varmap(&varmap, ratchet::DType::F32, &device.clone()); + let vb = VarBuilder::from_varmap(&varmap, piston::DType::F32, &device.clone()); let linear = if with_bias { linear(in_features, out_features, vb.clone())? @@ -257,13 +259,13 @@ def linear_backward(x, w): let result_gpu = linear.schedule(x_var.as_tensor().clone())?; - let grads = result_gpu.backward()?; + result_gpu.backward()?; device.try_gpu()?.mark_step()?; - let x_grad = grads.get(x_var.as_tensor()).unwrap().to(&Device::CPU)?; - let w_grad = grads.get(&linear.w).unwrap().to(&Device::CPU)?; + let x_grad = x_var.as_tensor().grad().unwrap().to(&Device::CPU)?; + let w_grad = linear.w.grad().unwrap().to(&Device::CPU)?; let b_grad = match &linear.b { - Some(b) => Some(grads.get(b).unwrap().to(&Device::CPU)?), + Some(b) => Some(b.grad().unwrap().to(&Device::CPU)?), None => None, }; diff --git a/crates/piston-nn/src/loss.rs b/crates/piston-nn/src/loss.rs new file mode 100644 index 00000000..a2677eba --- /dev/null +++ b/crates/piston-nn/src/loss.rs @@ -0,0 +1,259 @@ +use piston::{AllDims, DType, ScopePusher, Tensor, TensorOptions}; + +pub fn nll(inp: Tensor, target: Tensor) -> anyhow::Result { + let _scope_guard = ScopePusher::new("loss:nll"); + let b_sz = match &target.shape().to_vec()[..] { + &[b_sz] => b_sz, + dims => anyhow::bail!("the target tensor should have a single dimension ({dims:?})"), + }; + match &inp.shape().to_vec()[..] { + &[inp_b_sz, _] => { + if inp_b_sz != b_sz { + anyhow::bail!("batch size mismatch between inp ({inp_b_sz}) and target ({b_sz})") + } + } + dims => anyhow::bail!("the target tensor should have two dimensions ({dims:?})"), + } + inp.gather(1, target.clone().unsqueeze(1)?)? + .affine(-1f32 / b_sz as f32, 0.) +} + +pub fn nll_masked(inp: Tensor, target: Tensor) -> anyhow::Result { + let _scope_guard = ScopePusher::new("loss:nll_masked"); + let b_sz = match &target.shape().to_vec()[..] { + &[b_sz] => b_sz, + dims => anyhow::bail!("the target tensor should have a single dimension ({dims:?})"), + }; + match &inp.shape().to_vec()[..] { + &[inp_b_sz, _] => { + if inp_b_sz != b_sz { + anyhow::bail!("batch size mismatch between inp ({inp_b_sz}) and target ({b_sz})") + } + } + dims => anyhow::bail!("the target tensor should have two dimensions ({dims:?})"), + } + + // We do a bunch of work here to allow ignoring tokens in the target. + let ignore_index = -100; + let mask = target.clone().ne(Tensor::full( + target.shape(), + ignore_index, + TensorOptions::new().device(target.device()), + )?)?; + + // Note here that we seem to be able to get away with passing negative indices to gather. + // If we were more careful about this, we'd replace the indices with 0s where the mask is 0, + // before passing them to gather. + + let per_sample_loss = inp + .gather(1, target.clone().unsqueeze(1)?)? + .affine(-1f32, 0.)?; + let mask_unsqueezed = mask.clone().unsqueeze(1)?; + let masked_loss = per_sample_loss + .clone() + .mul(mask_unsqueezed.cast(DType::F32)?)?; + + let valid_count = mask.cast(DType::F32)?.sum(AllDims, false)?; + + masked_loss.div(valid_count.cast(DType::F32)?) +} + +/// Computes label-smoothed cross-entropy for a flattened `[batch_size, vocab_size]` log-softmax +/// tensor `log_probs`, together with a corresponding `target` of shape `[batch_size]`. +/// +/// `alpha` is the smoothing parameter in `[0, 1]`. If `alpha == 0.0`, this is just ordinary NLL. +pub fn label_smoothed_nll(log_probs: Tensor, target: Tensor, alpha: f32) -> anyhow::Result { + let _scope_guard = ScopePusher::new("loss:nll_label_smoothed"); + let b_sz = match &target.shape().to_vec()[..] { + &[b_sz] => b_sz, + dims => { + anyhow::bail!("label_smoothed_nll: target must be [batch_size], got shape {dims:?}") + } + }; + let shape_lp = log_probs.shape().to_vec(); + if shape_lp.len() != 2 { + anyhow::bail!( + "label_smoothed_nll: log_probs must be rank-2 [batch_size, vocab_size], got {shape_lp:?}" + ); + } + let (inp_b_sz, vocab_size) = (shape_lp[0], shape_lp[1]); + if inp_b_sz != b_sz { + anyhow::bail!( + "label_smoothed_nll: batch size mismatch between log_probs ({inp_b_sz}) and target ({b_sz})" + ); + } + + // Check for ignored tokens (often `-100` in NLP). + let ignore_index = -100; + let mask = target + .clone() + .ne(Tensor::full( + target.shape(), + ignore_index, + TensorOptions::new().device(target.device()), + )?)? + .cast(piston::DType::F32)?; + + // Gather the negative log-prob for the correct class: + // nll_loss[i] = -log_probs[i, target[i]] (for each token i). + let nll_gathered = log_probs + .clone() + .gather(1, target.clone().unsqueeze(1)?)? // shape [batch_size, 1] + .affine(-1.0, 0.0)?; // multiply by -1 + + // Mask out ignored tokens (multiply by 0 where masked=0). + let nll_masked = nll_gathered.mul(mask.clone().unsqueeze(1)?)?; + + // We'll also average over only the valid tokens: + let valid_count = mask.clone().sum(AllDims, false)?; // shape [] + + // (1) Ordinary cross-entropy term (averaged). + let nll_loss = nll_masked.div(valid_count.clone())?; + + // (2) Uniform penalty term, also masked. + // + // For label smoothing, we pretend a small fraction α of the time + // we want the “average” log-prob over all classes, not just the correct one. + // i.e. uniform_loss = - average over (vocab_size) of log_probs, restricted to masked positions. + let all_probs_masked = log_probs.mul(mask.unsqueeze(1)?)?; + let sum_log_probs = all_probs_masked.sum(1, false)?; + // Now shape [batch_size], each entry is sum_{v in vocab} log_probs[i, v]. + // Negative average per token: + let neg_avg_log_prob = sum_log_probs.affine(-1.0 / vocab_size as f32, 0.0)?; + let uniform_loss = neg_avg_log_prob.sum(AllDims, false)?.div(valid_count)?; + + // Combine with alpha + // final = (1 - alpha) * nll + alpha * uniform_term + let final_loss = nll_loss + .affine(1.0 - alpha, 0.0)? + .add(uniform_loss.affine(alpha, 0.0)?)?; + + Ok(final_loss) +} + +pub fn log_softmax(xs: Tensor, d: usize) -> anyhow::Result { + let _scope_guard = ScopePusher::new("loss:log_softmax"); + let max = xs.clone().max(d, true)?; + let diff = xs.clone().sub(max)?; + let sum_exp = diff.clone().exp()?.sum(d, true)?; + let log_sm = diff.sub(sum_exp.log()?)?; + Ok(log_sm) +} + +pub fn cross_entropy(inp: Tensor, target: Tensor) -> anyhow::Result { + let _scope_guard = ScopePusher::new("loss:cross_entropy"); + if inp.dim() != 2 { + anyhow::bail!("cross_entropy expects an input tensor of rank 2") + } + let inp = log_softmax(inp, 1)?; + nll(inp, target)?.sum(AllDims, false) +} + +#[cfg(all(test, feature = "pyo3"))] +mod tests { + use super::*; + use anyhow::Result; + use piston::{DType, Device, DeviceRequest, Parameter}; + use test_strategy::{Arbitrary, proptest}; + + thread_local! { + static GPU_DEVICE: Device = Device::request_device(DeviceRequest::GPU).unwrap(); + } + + fn ground_truth_cross_entropy(input: &Tensor, target: &Tensor) -> Result<(Tensor, Tensor)> { + let grad_prg = r#" +import torch +import torch.nn.functional as F + +def cross_entropy_with_grad(input, target): + input_tensor = torch.tensor(torch.from_numpy(input), requires_grad=True) + target_tensor = torch.tensor(torch.from_numpy(target), dtype=torch.long) + loss = F.cross_entropy(input_tensor, target_tensor, reduction='mean') + loss.backward() + return input_tensor.grad.numpy() +"#; + let grad = + piston::test_util::run_py_prg(grad_prg.to_string(), &[input, target], &[], DType::F32)?; + + let loss_prg = r#" +import torch +import torch.nn.functional as F + +def cross_entropy(input, target): + input_tensor = torch.tensor(torch.from_numpy(input)) + target_tensor = torch.tensor(torch.from_numpy(target), dtype=torch.long) + return F.cross_entropy(input_tensor, target_tensor, reduction='mean').numpy() +"#; + let loss = + piston::test_util::run_py_prg(loss_prg.to_string(), &[input, target], &[], DType::F32)?; + + Ok((loss, grad)) + } + + #[derive(Arbitrary, Debug)] + struct CrossEntropyProblem { + #[strategy(1..=32usize)] + batch_size: usize, + #[strategy(2..=10usize)] + num_classes: usize, + } + + fn run_cross_entropy_trial(problem: CrossEntropyProblem) -> Result<()> { + let device = GPU_DEVICE.with(|d| d.clone()); + let CrossEntropyProblem { + batch_size, + num_classes, + } = problem; + + // Generate random input and target tensors + let input = Tensor::randn::(0., 1., (batch_size, num_classes), Device::CPU)?; + let target = Tensor::randint(0, num_classes as i32, batch_size, Device::CPU)?; + + // Compute ground truth + let (ground_loss, ground_grad) = ground_truth_cross_entropy(&input, &target)?; + + // Compute our implementation + let input_gpu = input.to(&device)?; + let target_gpu = target.to(&device)?; + let input_param = Parameter::from_tensor(&input_gpu)?; + let our_loss = cross_entropy(input_param.as_tensor().clone(), target_gpu)?; + + // Compute gradients + our_loss.backward()?; + device.try_gpu()?.mark_step()?; + let our_grad = input_param.as_tensor().grad().unwrap().clone(); + + // Compare results + let our_loss_cpu = our_loss.to(&Device::CPU)?; + let our_grad_cpu = our_grad.to(&Device::CPU)?; + let ground_grad = ground_grad.to(&Device::CPU)?; + let ground_loss = ground_loss.to(&Device::CPU)?; + + println!("Input shape: {:?}", input.shape()); + println!("Target shape: {:?}", target.shape()); + println!("Our loss: {:?}", our_loss_cpu.to_vec::()); + println!("Ground truth loss: {:?}", ground_loss.to_vec::()); + println!("Our grad: {:?}", our_grad_cpu.to_vec::()); + println!("Ground truth grad: {:?}", ground_grad.to_vec::()); + println!("Our grad shape: {:?}", our_grad_cpu.shape()); + println!("Ground truth grad shape: {:?}", ground_grad.shape()); + + ground_loss.all_close(&our_loss_cpu, 1e-5, 1e-5)?; + ground_grad.all_close(&our_grad_cpu, 1e-5, 1e-5)?; + + Ok(()) + } + + #[proptest(cases = 10)] + fn test_cross_entropy(prob: CrossEntropyProblem) { + let CrossEntropyProblem { + batch_size, + num_classes, + } = prob; + println!( + "Testing with batch_size = {}, num_classes = {}", + batch_size, num_classes + ); + run_cross_entropy_trial(prob).unwrap(); + } +} diff --git a/crates/ratchet-nn/src/lr_scheduler.rs b/crates/piston-nn/src/lr_scheduler.rs similarity index 97% rename from crates/ratchet-nn/src/lr_scheduler.rs rename to crates/piston-nn/src/lr_scheduler.rs index f4c6ea97..ba79536f 100644 --- a/crates/ratchet-nn/src/lr_scheduler.rs +++ b/crates/piston-nn/src/lr_scheduler.rs @@ -1,6 +1,6 @@ use anyhow::Result; use maybe_async::maybe_async; -use ratchet::{Device, GradStore}; +use piston::Device; use crate::Optimizer; @@ -23,10 +23,10 @@ pub trait LRScheduler: Send + Sync { fn compute_lr(&self) -> f64; /// Advances the scheduler by one step. - async fn step(&mut self, grads: &GradStore, device: &Device) -> Result<()> { + async fn step(&mut self, device: &Device) -> Result<()> { let lr = self.compute_lr(); self.optimizer_mut().set_learning_rate(lr); - self.optimizer_mut().step(grads, device).await?; + self.optimizer_mut().step(device).await?; self.set_step_count(self.step_count() + 1); Ok(()) } diff --git a/crates/ratchet-nn/src/norm.rs b/crates/piston-nn/src/norm.rs similarity index 75% rename from crates/ratchet-nn/src/norm.rs rename to crates/piston-nn/src/norm.rs index 3e53f9fd..16447122 100644 --- a/crates/ratchet-nn/src/norm.rs +++ b/crates/piston-nn/src/norm.rs @@ -1,5 +1,6 @@ use maybe_async::maybe_async; -use ratchet::{shape, DType, Tensor}; +use piston::Tensor; +use piston_macros::scoped_module; #[derive(Debug, Clone, Copy, PartialEq)] pub struct LayerNormConfig { @@ -52,38 +53,13 @@ impl LayerNorm { } } +#[scoped_module] impl crate::Module for LayerNorm { type Input = Tensor; type Output = Tensor; - // Shader-accelerated implementation that I don't know how to broadcast - // correctly - // fn schedule(&self, input: Self::Input) -> anyhow::Result { - // input.layer_norm(self.weight.clone(), self.bias.clone(), self.eps) - // } - - fn schedule(&self, x: Self::Input) -> anyhow::Result { - let x_dtype = x.dt(); - let internal_dtype = match x_dtype { - DType::F16 => DType::F32, - d => d, - }; - let hidden_size = x.shape()[x.rank() - 1]; - let last_dim = x.rank() - 1; - let x = x.cast(internal_dtype)?; - let x = if self.remove_mean { - let mean_x = (x.clone().sum_keepdim(&[last_dim])? / hidden_size as f32)?; - x.clone().sub(mean_x.clone())? - } else { - x - }; - let norm_x = (x.clone().square()?.sum_keepdim(&[last_dim])? / hidden_size as f32)?; - let x_normed = x.clone().div((norm_x + self.eps)?.sqrt()?)?; - let x = x_normed.cast(x_dtype)?.mul(self.weight.clone())?; - match &self.bias { - None => Ok(x), - Some(bias) => x.add(bias.clone()), - } + fn schedule(&self, input: Self::Input) -> anyhow::Result { + input.layer_norm(Some(self.weight.clone()), self.bias.clone(), self.eps) } } @@ -94,10 +70,10 @@ pub async fn layer_norm( vb: crate::VarBuilder<'_>, ) -> anyhow::Result { let weight = vb - .get_with_hints(shape![size], "weight", crate::Init::Const(1.)) + .get_with_hints(size, "weight", crate::Init::Const(1.)) .await?; let bias = vb - .get_with_hints(shape![size], "bias", crate::Init::Const(0.)) + .get_with_hints(size, "bias", crate::Init::Const(0.)) .await?; Ok(LayerNorm { weight, @@ -122,37 +98,38 @@ impl RMSNorm { } } +#[scoped_module] impl crate::Module for RMSNorm { type Input = Tensor; type Output = Tensor; fn schedule(&self, input: Self::Input) -> anyhow::Result { - let src_dt = input.dt(); + let src_dtype = input.dtype(); input .float()? - .rms_norm(self.weight.clone(), self.eps)? - .cast(src_dt) + .rms_norm(Some(self.weight.clone()), self.eps)? + .cast(src_dtype) } } #[maybe_async] pub async fn rms_norm(size: usize, eps: f32, vb: crate::VarBuilder<'_>) -> anyhow::Result { let weight = vb - .get_with_hints(shape![size], "weight", crate::Init::Const(1.)) + .get_with_hints(size, "weight", crate::Init::Const(1.)) .await?; Ok(RMSNorm::new(weight, eps)) } -#[cfg(test)] +#[cfg(feature = "pyo3")] mod tests { - use super::{layer_norm, LayerNorm, LayerNormConfig}; + use super::{LayerNorm, LayerNormConfig, layer_norm}; use crate::{Module, VarBuilder, VarMap}; - use ratchet::{ + use piston::{ + Device, DeviceRequest, Parameter, Tensor, prelude::shape, test_util::{run_py_prg, run_py_prg_multiple}, - Device, DeviceRequest, Tensor, Var, }; - use test_strategy::{proptest, Arbitrary}; + use test_strategy::{Arbitrary, proptest}; thread_local! { static GPU_DEVICE: Device = Device::request_device(DeviceRequest::GPU).unwrap(); @@ -187,8 +164,8 @@ def layer_norm(x, weight, bias=None): ); let result = match bias { - Some(b) => run_py_prg(prg, &[x, weight, b], &[], x.dt())?, - None => run_py_prg(prg, &[x, weight], &[], x.dt())?, + Some(b) => run_py_prg(prg, &[x, weight, b], &[], x.dtype())?, + None => run_py_prg(prg, &[x, weight], &[], x.dtype())?, }; Ok(result) } @@ -259,14 +236,9 @@ def layer_norm_backward(x, weight, bias = None): eps, } = problem; - let x = Tensor::randn::( - 0., - 1., - shape![batch_size, seq_len, hidden_size], - Device::CPU, - ); + let x = Tensor::randn::(0., 1., (batch_size, seq_len, hidden_size), Device::CPU)?; let varmap = VarMap::new(); - let vb = VarBuilder::from_varmap(&varmap, ratchet::DType::F32, &device); + let vb = VarBuilder::from_varmap(&varmap, piston::DType::F32, &device); let config = LayerNormConfig { eps, @@ -304,16 +276,11 @@ def layer_norm_backward(x, weight, bias = None): eps, } = problem; - let x = Tensor::randn::( - 0., - 1., - shape![batch_size, seq_len, hidden_size], - Device::CPU, - ); + let x = Tensor::randn::(0., 1., (batch_size, seq_len, hidden_size), Device::CPU)?; let x_gpu = x.to(&device)?; - let x_var = Var::from_tensor(&x_gpu)?; + let x_var = Parameter::from_tensor(&x_gpu)?; let varmap = VarMap::new(); - let vb = VarBuilder::from_varmap(&varmap, ratchet::DType::F32, &device); + let vb = VarBuilder::from_varmap(&varmap, piston::DType::F32, &device); let config = LayerNormConfig { eps, @@ -331,14 +298,14 @@ def layer_norm_backward(x, weight, bias = None): let result_gpu = layer_norm.schedule(x_var.as_tensor().clone())?; - let grads = result_gpu.backward()?; + result_gpu.backward()?; device.try_gpu()?.mark_step()?; - let x_grad = grads.get(&x_var.as_tensor()).unwrap().to(&Device::CPU)?; - let weight_grad = grads.get(weight).unwrap().to(&Device::CPU)?; + let x_grad = x_var.as_tensor().grad().unwrap().to(&Device::CPU)?; + let weight_grad = weight.grad().unwrap().to(&Device::CPU)?; let bias_grad = bias .as_ref() - .map(|b| grads.get(b).unwrap().to(&Device::CPU).unwrap()); + .map(|b| b.grad().unwrap().to(&Device::CPU).unwrap()); ground_x_grad.all_close(&x_grad, 1e-4, 1e-4)?; ground_weight_grad.all_close(&weight_grad, 1e-4, 1e-4)?; diff --git a/crates/piston-nn/src/optim.rs b/crates/piston-nn/src/optim.rs new file mode 100644 index 00000000..e9578877 --- /dev/null +++ b/crates/piston-nn/src/optim.rs @@ -0,0 +1,258 @@ +use maybe_async::maybe_async; +use piston::{DType, Device, ScopePusher, Tensor, TensorOptions, zeros}; + +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +pub trait Optimizer: Sized { + type Config: Sized; + + fn new(vars: Vec, config: Self::Config) -> anyhow::Result; + + async fn step(&mut self, device: &Device) -> anyhow::Result<()>; + + fn learning_rate(&self) -> f64; + + fn set_learning_rate(&mut self, lr: f64); + + fn parameters(&self) -> Vec<&Tensor>; + + fn zero_grad(&self, set_to_none: bool) -> anyhow::Result<()> { + for var in self.parameters() { + if set_to_none { + var.set_grad(None); + } else { + var.set_grad(Some( + var.clone() + .zeros_like(TensorOptions::new().device(var.device()))?, + )); + } + } + Ok(()) + } + + fn empty(config: Self::Config) -> anyhow::Result { + Self::new(vec![], config) + } + + async fn backward_step(&mut self, device: &Device) -> anyhow::Result<()> { + self.step(device).await + } + + fn from_slice(vars: &[&Tensor], config: Self::Config) -> anyhow::Result { + Self::new(vars.iter().cloned().cloned().collect(), config) + } +} + +#[derive(Debug)] +pub struct SGD { + vars: Vec, + learning_rate: f64, +} + +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +impl Optimizer for SGD { + type Config = f64; + + fn new(vars: Vec, learning_rate: f64) -> anyhow::Result { + let vars = vars.into_iter().filter(|v| v.dtype().is_float()).collect(); + Ok(Self { + vars, + learning_rate, + }) + } + + fn learning_rate(&self) -> f64 { + self.learning_rate + } + + fn set_learning_rate(&mut self, lr: f64) { + self.learning_rate = lr; + } + + async fn step(&mut self, device: &Device) -> anyhow::Result<()> { + let mut updates = Vec::new(); + { + let _scope_guard = ScopePusher::new("optim:SGD"); + for var in &self.vars { + let _scope_guard = optim_var_scope_guard(var); + if let Some(grad) = var.grad() { + let update = (var.clone() - (grad.clone() * self.learning_rate as f32)?)?; + updates.push(var.set(update)); + } + } + } + + if let Ok(gpu_device) = device.try_gpu() { + gpu_device.mark_step().await?; + } + + Ok(()) + } + + fn parameters(&self) -> Vec<&Tensor> { + self.vars.iter().collect() + } +} + +#[derive(Clone, Debug)] +pub struct ParamsAdamW { + pub lr: f64, + pub beta1: f64, + pub beta2: f64, + pub eps: f64, + pub weight_decay: f64, +} + +impl Default for ParamsAdamW { + fn default() -> Self { + Self { + lr: 0.001, + beta1: 0.9, + beta2: 0.999, + eps: 1e-8, + weight_decay: 0.01, + } + } +} + +#[derive(Debug)] +struct VarAdamW { + var: Tensor, + first_moment: Tensor, + second_moment: Tensor, +} + +#[derive(Debug)] +pub struct AdamW { + vars: Vec, + step_t: usize, + params: ParamsAdamW, +} + +#[maybe_async(AFIT)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] +impl Optimizer for AdamW { + type Config = ParamsAdamW; + + fn new(vars: Vec, params: ParamsAdamW) -> anyhow::Result { + let vars = vars + .into_iter() + .filter(|var| var.dtype().is_float()) + .map(|var| { + let dtype = var.dtype(); + let shape = var.shape(); + let device = var.device(); + let (first_moment, second_moment) = match dtype { + DType::F32 => ( + zeros(shape.clone(), TensorOptions::new().device(device.clone()))?, + zeros(shape, TensorOptions::new().device(device.clone()))?, + ), + DType::F16 => ( + zeros(shape.clone(), TensorOptions::new().device(device.clone()))?, + zeros(shape, TensorOptions::new().device(device.clone()))?, + ), + DType::BF16 => ( + zeros(shape.clone(), TensorOptions::new().device(device.clone()))?, + zeros(shape, TensorOptions::new().device(device))?, + ), + _ => return Err(anyhow::anyhow!("Unsupported dtype for AdamW: {:?}", dtype)), + }; + Ok(VarAdamW { + var, + first_moment, + second_moment, + }) + }) + .collect::>>()?; + Ok(Self { + vars, + params, + step_t: 0, + }) + } + fn learning_rate(&self) -> f64 { + self.params.lr + } + + fn set_learning_rate(&mut self, lr: f64) { + self.params.lr = lr + } + + async fn step(&mut self, device: &Device) -> anyhow::Result<()> { + // This makes sure we keep references to the copy tensors. + let mut updates = Vec::new(); + { + let _scope_guard = ScopePusher::new("optim:AdamW"); + self.step_t += 1; + let lr = self.params.lr; + let lambda = self.params.weight_decay; + let lr_lambda = lr * lambda; + let beta1 = self.params.beta1; + let beta2 = self.params.beta2; + let scale_m = 1f64 / (1f64 - beta1.powi(self.step_t as i32)); + let scale_v = 1f64 / (1f64 - beta2.powi(self.step_t as i32)); + + for var in self.vars.iter_mut() { + let _scope_guard = optim_var_scope_guard(&var.var); + let theta = &var.var; + let m = &var.first_moment; + let v = &var.second_moment; + + if let Some(g) = theta.grad() { + let next_m = + ((m.clone() * beta1 as f32)? + (g.clone() * (1.0 - beta1 as f32))?)?; + let next_v = ((v.clone() * beta2 as f32)? + + (g.clone().square()? * (1.0 - beta2 as f32))?)?; + let m_hat = (next_m.clone() * scale_m as f32)?; + let v_hat = (next_v.clone() * scale_v as f32)?; + let next_theta = (theta.clone() * (1f32 - lr_lambda as f32))?; + let adjusted_grad = (m_hat / (v_hat.sqrt()? + self.params.eps as f32)?)?; + let next_theta = (next_theta - (adjusted_grad.clone() * lr as f32)?)?; + + // This ensures we keep references to the copy tensors. + updates.push((theta.set(next_theta), m.set(next_m), v.set(next_v))); + } + } + } + + // Finalize all the tensors we just built above. + if let Ok(gpu) = device.try_gpu() { + gpu.mark_step().await?; + } + + Ok(()) + } + + fn parameters(&self) -> Vec<&Tensor> { + self.vars.iter().map(|v| &v.var).collect() + } +} + +impl AdamW { + pub fn new_lr(vars: Vec, learning_rate: f64) -> anyhow::Result { + let params = ParamsAdamW { + lr: learning_rate, + ..ParamsAdamW::default() + }; + Self::new(vars, params) + } + + pub fn params(&self) -> &ParamsAdamW { + &self.params + } + + pub fn set_params(&mut self, params: ParamsAdamW) { + self.params = params; + } +} + +fn optim_var_scope_guard(var: &Tensor) -> ScopePusher { + ScopePusher::new( + format!( + "for:({})", + var.scope().as_ref().unwrap_or(&"unknown".to_string()) + ) + .as_str(), + ) +} diff --git a/crates/ratchet-nn/src/rope.rs b/crates/piston-nn/src/rope.rs similarity index 92% rename from crates/ratchet-nn/src/rope.rs rename to crates/piston-nn/src/rope.rs index 7e06a42d..dd68648e 100644 --- a/crates/ratchet-nn/src/rope.rs +++ b/crates/piston-nn/src/rope.rs @@ -1,4 +1,5 @@ -use ratchet::Tensor; +use piston::Tensor; +use piston_macros::scoped_module; use crate::Module; @@ -33,12 +34,13 @@ pub struct RotaryInput { pub offset: usize, } +#[scoped_module] impl Module for RotaryEmbedding { type Input = RotaryInput; type Output = Tensor; fn schedule(&self, input: Self::Input) -> anyhow::Result { let RotaryInput { input, offset } = input; - input.rope(self.dim, self.base, offset) + input.rope_(self.dim, self.base, offset) } } diff --git a/crates/ratchet-nn/src/sinusoidal.rs b/crates/piston-nn/src/sinusoidal.rs similarity index 74% rename from crates/ratchet-nn/src/sinusoidal.rs rename to crates/piston-nn/src/sinusoidal.rs index 92157d4b..d70f22eb 100644 --- a/crates/ratchet-nn/src/sinusoidal.rs +++ b/crates/piston-nn/src/sinusoidal.rs @@ -1,10 +1,12 @@ -use ratchet::{rvec, shape, DType, Tensor}; +use piston::{Tensor, TensorOptions, arange, cat, rvec}; +use piston_macros::scoped_module; use crate::Module; /// Implements sinusoidal positional encodings as described in "Attention Is All You Need". #[derive(Clone, Debug)] pub struct SinusoidalEmbedding { + #[allow(dead_code)] dim: usize, inv_freq: Tensor, } @@ -15,7 +17,7 @@ pub struct SinusoidalInput { } impl SinusoidalEmbedding { - pub fn new(dim: usize, device: &ratchet::Device) -> anyhow::Result { + pub fn new(dim: usize, device: &piston::Device) -> anyhow::Result { // Create position frequencies let mut freqs = Vec::with_capacity(dim / 2); for i in (0..dim).step_by(2) { @@ -23,12 +25,14 @@ impl SinusoidalEmbedding { } // Create inverse frequency tensor - let inv_freq = Tensor::from_data(&freqs, shape![dim / 2], device.clone()); + let inv_freq = + Tensor::from_data(&freqs, dim / 2, TensorOptions::new().device(device.clone()))?; Ok(Self { dim, inv_freq }) } } +#[scoped_module] impl Module for SinusoidalEmbedding { type Input = SinusoidalInput; type Output = Tensor; @@ -40,8 +44,12 @@ impl Module for SinusoidalEmbedding { let seq_len = input.shape()[1]; // Create position sequence [0, 1, 2, ..., seq_len-1] - let pos_seq = - Tensor::arange::(offset as f32, (offset + seq_len) as f32, input.device())?; + let pos_seq = arange( + Some(offset as f32), + (offset + seq_len) as f32, + None, + TensorOptions::new().device(input.device()), + )?; // Compute outer product between positions and frequencies let sinusoid_inp = @@ -55,7 +63,7 @@ impl Module for SinusoidalEmbedding { let last_dim = sin.shape().len() - 1; // Interleave sin and cos values - let pos_emb = Tensor::cat(rvec![sin, cos], last_dim)?; + let pos_emb = cat(rvec![sin, cos], last_dim)?; // Add batch dimension if needed let pos_emb = pos_emb.unsqueeze(0)?; diff --git a/crates/piston-nn/src/training_context.rs b/crates/piston-nn/src/training_context.rs new file mode 100644 index 00000000..22860810 --- /dev/null +++ b/crates/piston-nn/src/training_context.rs @@ -0,0 +1,43 @@ +use std::cell::RefCell; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ModuleMode { + Train, + Eval, +} + +thread_local! { + static CURRENT_MODE: RefCell = const { RefCell::new(ModuleMode::Train) }; +} + +// Functions to get/set the mode +pub fn set_train_mode() { + CURRENT_MODE.with(|mode| *mode.borrow_mut() = ModuleMode::Train); +} + +pub fn set_eval_mode() { + CURRENT_MODE.with(|mode| *mode.borrow_mut() = ModuleMode::Eval); +} + +pub fn current_module_mode() -> ModuleMode { + CURRENT_MODE.with(|mode| *mode.borrow()) +} + +/// Context manager for scoped mode changes +pub struct ModuleModeGuard { + previous: ModuleMode, +} + +impl ModuleModeGuard { + pub fn new(mode: ModuleMode) -> Self { + let previous = current_module_mode(); + CURRENT_MODE.with(|m| *m.borrow_mut() = mode); + Self { previous } + } +} + +impl Drop for ModuleModeGuard { + fn drop(&mut self) { + CURRENT_MODE.with(|mode| *mode.borrow_mut() = self.previous); + } +} diff --git a/crates/piston-nn/src/util.rs b/crates/piston-nn/src/util.rs new file mode 100644 index 00000000..452c6897 --- /dev/null +++ b/crates/piston-nn/src/util.rs @@ -0,0 +1,29 @@ +use piston::{AllDims, Device, Tensor, TensorOptions, ones}; + +// This seems to work, but it is very slow. Norming and adding all the gradients is probaby quite slow; +// there are probably some easy wins here, like maybe a fused norm op? +pub fn clip_grad_norm(vars: Vec, max_norm: f32, device: &Device) -> anyhow::Result { + let mut total_norm = Tensor::full(1, 0., TensorOptions::new().device(device.clone()))?; + let mut any_grads = false; + + for var in vars.iter() { + total_norm = (total_norm + var.grad().unwrap().norm(None, AllDims, false)?)?; + any_grads = true; + } + + if !any_grads { + return Ok(total_norm); + } + + let clip_coef = (max_norm / (total_norm.clone() + 1e-6)?)?; + let ones_max = ones(1, TensorOptions::new().device(device.clone()))?; + let clip_coef = clip_coef + .clone() + .where_cond(clip_coef.clone().lt(ones_max.clone())?, ones_max)?; + + for var in vars.iter() { + var.set_grad(Some(var.grad().unwrap().mul(clip_coef.clone())?)); + } + + Ok(total_norm) +} diff --git a/crates/ratchet-nn/src/var_builder.rs b/crates/piston-nn/src/var_builder.rs similarity index 94% rename from crates/ratchet-nn/src/var_builder.rs rename to crates/piston-nn/src/var_builder.rs index 322fb352..9fd733e9 100644 --- a/crates/ratchet-nn/src/var_builder.rs +++ b/crates/piston-nn/src/var_builder.rs @@ -3,8 +3,8 @@ //! for training, e.g. using `VarBuilder::from_varmap`. use crate::VarMap; use async_trait::async_trait; -use ratchet::HashMap; -use ratchet::{DType, Device, OperationError, Shape, Tensor}; +use piston::{DType, Device, OperationError, Shape, Tensor, zeros}; +use piston::{HashMap, TensorOptions}; use std::sync::Arc; use maybe_async::maybe_async; @@ -22,6 +22,8 @@ pub enum VarBuilderError { shape: Shape, tensor_shape: Shape, }, + #[error(transparent)] + Other(#[from] anyhow::Error), } /// A structure used to retrieve variables, these variables can either come from storage or be @@ -34,7 +36,7 @@ pub struct VarBuilderArgs<'a, B: Backend> { _phantom: std::marker::PhantomData<&'a B>, } -impl<'a, B: Backend> Clone for VarBuilderArgs<'a, B> { +impl Clone for VarBuilderArgs<'_, B> { fn clone(&self) -> Self { Self { data: self.data.clone(), @@ -92,7 +94,7 @@ pub trait SimpleBackend: Send + Sync { } #[maybe_async] -impl<'a> Backend for Box { +impl Backend for Box { // impl<'a> Backend for Box { type Hints = crate::Init; @@ -112,7 +114,7 @@ impl<'a> Backend for Box { } #[maybe_async] -impl<'a, B: Backend> VarBuilderArgs<'a, B> { +impl VarBuilderArgs<'_, B> { pub fn new_with_args(backend: B, dtype: DType, dev: Device) -> Self { let data = TensorData { backend, @@ -193,9 +195,9 @@ impl<'a, B: Backend> VarBuilderArgs<'a, B> { } /// Retrieve the tensor associated with the given name at the current path. - pub async fn get_with_hints( + pub async fn get_with_hints>( &self, - s: Shape, + s: S, name: &str, hints: B::Hints, ) -> anyhow::Result { @@ -214,7 +216,11 @@ impl<'a, B: Backend> VarBuilderArgs<'a, B> { // } /// Retrieve the tensor associated with the given name at the current path. - pub async fn get(&self, s: Shape, name: &str) -> anyhow::Result { + pub async fn get>( + &self, + s: S, + name: &str, + ) -> anyhow::Result { self.get_with_hints(s, name, Default::default()).await } @@ -259,7 +265,7 @@ impl SimpleBackend for Zeros { _: crate::Init, dev: Device, ) -> anyhow::Result { - Ok(Tensor::zeros::(&s, &dev)) + Ok(zeros(s, TensorOptions::new().device(dev.clone()))?) } fn contains_tensor(&self, _name: &str) -> bool { @@ -282,7 +288,7 @@ impl SimpleBackend for HashMap { path: name.to_string(), })? .clone(); - if tensor.shape() != &s { + if tensor.shape() != s { return Err(VarBuilderError::ShapeMismatch { path: name.to_string(), shape: s, @@ -394,7 +400,7 @@ pub struct Rename<'a, R: Renamer> { } #[maybe_async] -impl<'a, R: Renamer + Send + Sync> SimpleBackend for Rename<'a, R> { +impl SimpleBackend for Rename<'_, R> { async fn get( &self, s: Shape, diff --git a/crates/ratchet-nn/src/var_map.rs b/crates/piston-nn/src/var_map.rs similarity index 59% rename from crates/ratchet-nn/src/var_map.rs rename to crates/piston-nn/src/var_map.rs index f6c9dd0d..88ce0fce 100644 --- a/crates/ratchet-nn/src/var_map.rs +++ b/crates/piston-nn/src/var_map.rs @@ -1,16 +1,15 @@ -use ratchet::HashMap; +use piston::HashMap; use std::sync::{Arc, Mutex}; -use ratchet::{Device, GradStore, Shape, Tensor, Var}; +use piston::{Device, Shape, Tensor}; #[cfg(target_arch = "wasm32")] use { - wasm_bindgen::JsCast, wasm_bindgen::JsValue, + wasm_bindgen_futures::JsFuture, wasm_bindgen_futures::future_to_promise, wasm_bindgen_futures::js_sys, - wasm_bindgen_futures::JsFuture, - web_sys::{Blob, HtmlAnchorElement, Url}, + web_sys::{Blob, Url}, }; /// A `VarMap` is a store that holds named variables. Variables can be retrieved from the stores @@ -19,7 +18,7 @@ use { /// `VarMap` structures can be serialized in the safetensors format. #[derive(Clone)] pub struct VarMap { - data: Arc>>, + data: Arc>>, } impl VarMap { @@ -31,54 +30,52 @@ impl VarMap { } /// Retrieve all the variables currently stored in the map. - pub fn all_vars(&self) -> Vec { + pub fn all_vars(&self) -> Vec { let tensor_data = self.data.lock().unwrap(); #[allow(clippy::map_clone)] tensor_data.values().map(|c| c.clone()).collect::>() } - pub fn all_labeled_vars(&self) -> Vec<(String, Var)> { - let tensor_data = self.data.lock().unwrap(); - tensor_data - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect() - } - /// Save the map in the safetensors format. #[cfg(not(target_arch = "wasm32"))] pub fn save>(&self, path: P) -> anyhow::Result<()> { - let tensor_data = self.data.lock().unwrap(); - let data = tensor_data.iter().map(|(k, v)| (k, v.as_tensor())); - safetensors::tensor::serialize_to_file(data, &None, path.as_ref())?; - Ok(()) - } + use piston::OpTensor; - #[cfg(not(target_arch = "wasm32"))] - pub fn save_grads>( - &self, - path: P, - grads: &GradStore, - ) -> anyhow::Result<()> { let tensor_data = self.data.lock().unwrap(); - let data = tensor_data + // Create a temporary Vec to hold the tensors and ensure they remain valid + let tensors: Vec<(String, OpTensor)> = tensor_data .iter() - .map(|(k, v)| (k, grads.get(v.as_tensor()).unwrap())); - safetensors::tensor::serialize_to_file(data, &None, path.as_ref())?; + .map(|(k, v)| (k.clone(), v.inner().read().clone())) + .collect(); + + safetensors::tensor::serialize_to_file( + tensors + .iter() + .map(|(k, tensor)| (k.as_str(), tensor as &OpTensor)), + &None, + path.as_ref(), + )?; Ok(()) } #[cfg(target_arch = "wasm32")] - async fn download_safetensors( + async fn create_safetensors_download_url( &self, - file_name: &str, data: Vec<(String, Tensor)>, - ) -> anyhow::Result<(), JsValue> { + ) -> anyhow::Result { + use piston::OpTensor; // Convert (String, Tensor) -> (&String, &Tensor) for serialization - let data_ref: Vec<(&String, &Tensor)> = data.iter().map(|(k, v)| (k, v)).collect(); + + let data_ref: Vec<(&String, OpTensor)> = data + .iter() + .map(|(k, v)| (k, v.inner().read().clone())) + .collect::>(); // Safetensors serialization - let serialized = safetensors::tensor::serialize(data_ref, &None).unwrap(); + let serialized = + // TODO(vinhowe): This probably looks really dumb; there's a better more rust-like way + // to do this + safetensors::tensor::serialize(data_ref.iter().map(|(k, v)| (k, v)), &None).unwrap(); // Create a Blob from the serialized data let uint8_array = js_sys::Uint8Array::from(&serialized[..]); @@ -88,62 +85,13 @@ impl VarMap { // Create a URL for the Blob let url = Url::create_object_url_with_blob(&blob)?; - // Create an anchor element and trigger the download - let document = web_sys::window().unwrap().document().unwrap(); - let a: HtmlAnchorElement = document.create_element("a")?.dyn_into()?; - a.set_href(&url); - a.set_download(file_name); - a.style().set_property("display", "none")?; - document.body().unwrap().append_child(&a)?; - a.click(); - document.body().unwrap().remove_child(&a)?; - - // Revoke the object URL to free memory - Url::revoke_object_url(&url)?; - - Ok(()) - } - - /// Download the gradients as a safetensors file using web APIs. - #[cfg(target_arch = "wasm32")] - pub async fn download_grads( - &self, - file_name: &str, - grads: &GradStore, - ) -> anyhow::Result<(), JsValue> { - let tensor_data = self.data.lock().unwrap(); - let data = Arc::new(Mutex::new(Vec::new())); - let mut futures = Vec::new(); - - // For each variable, move the corresponding grad to CPU asynchronously. - for (k, v) in tensor_data.iter() { - let k = k.clone(); - let grad = grads.get(&v.as_tensor()).unwrap().clone(); - let data_ref = Arc::clone(&data); - futures.push(future_to_promise(async move { - let grad_cpu = grad.to(&Device::CPU).await.unwrap(); - data_ref.lock().unwrap().push((k, grad_cpu)); - Ok(JsValue::undefined()) - })); - } - - // Wait for all futures to complete using Promise.all - let promise_array = js_sys::Array::from_iter(futures.iter()); - JsFuture::from(js_sys::Promise::all(&promise_array)).await?; - - // Collect the results - let data = Arc::try_unwrap(data).unwrap().into_inner().unwrap(); - - // Use the shared helper to download the safetensors data - self.download_safetensors(file_name, data).await?; - - Ok(()) + Ok(url) } /// Download the variables (instead of grads) as a safetensors file using web APIs. /// This avoids logic duplication by calling the same `_download_safetensors` helper. #[cfg(target_arch = "wasm32")] - pub async fn download(&self, file_name: &str) -> anyhow::Result<(), JsValue> { + pub async fn download_url(&self) -> anyhow::Result { let tensor_data = self.data.lock().unwrap(); let data = Arc::new(Mutex::new(Vec::new())); let mut futures = Vec::new(); @@ -151,7 +99,7 @@ impl VarMap { // For each Var, move the Tensor to CPU asynchronously. for (k, var) in tensor_data.iter() { let k = k.clone(); - let t = var.as_tensor().clone(); + let t = var.clone(); let data_ref = Arc::clone(&data); futures.push(future_to_promise(async move { let t_cpu = t.to(&Device::CPU).await.unwrap(); @@ -168,9 +116,7 @@ impl VarMap { let data = Arc::try_unwrap(data).unwrap().into_inner().unwrap(); // Use the shared helper - self.download_safetensors(file_name, data).await?; - - Ok(()) + self.create_safetensors_download_url(data).await } /// Set a named variable to some value. @@ -233,20 +179,20 @@ impl VarMap { ) -> anyhow::Result { let mut tensor_data = self.data.lock().unwrap(); if let Some(tensor) = tensor_data.get(path) { - let tensor_shape = tensor.as_tensor().shape(); - if &shape != tensor_shape { + let tensor_shape = tensor.shape(); + if shape != tensor_shape { // candle::bail!("shape mismatch on {path}: {shape:?} <> {tensor_shape:?}") panic!("shape mismatch on {path}: {shape:?} <> {tensor_shape:?}") } - return Ok(tensor.as_tensor().clone()); + return Ok(tensor.clone()); } let var = init.var(&shape, device)?; - let tensor = var.as_tensor().clone(); + let tensor = var.clone(); tensor_data.insert(path.to_string(), var); Ok(tensor) } - pub fn data(&self) -> &Mutex> { + pub fn data(&self) -> &Mutex> { &self.data } } diff --git a/crates/ratchet-nn/tests/optim.rs b/crates/piston-nn/tests/optim.rs similarity index 51% rename from crates/ratchet-nn/tests/optim.rs rename to crates/piston-nn/tests/optim.rs index b4197218..52da5928 100644 --- a/crates/ratchet-nn/tests/optim.rs +++ b/crates/piston-nn/tests/optim.rs @@ -1,51 +1,48 @@ -use ratchet::{ - shape, +use piston::{ + Device, DeviceRequest, Tensor, TensorOptions, test_utils::{to_vec0_round, to_vec1_round}, - Device, DeviceRequest, Tensor, Var, + zeros, }; -use ratchet_nn::{AdamW, Linear, Module, Optimizer, ParamsAdamW, SGD}; +use piston_nn::{AdamW, Linear, Module, Optimizer, ParamsAdamW, SGD}; -type OptimizerFactory = fn(Vec<(Option, Var)>) -> anyhow::Result; +type OptimizerFactory = fn(Vec) -> anyhow::Result; fn run_linear_regression(optimizer: OptimizerFactory) -> anyhow::Result<()> { let _ = env_logger::builder().is_test(true).try_init(); let device = Device::request_device(DeviceRequest::GPU).unwrap(); - let w_gen = Tensor::from_data(vec![3f32, 1.], shape![1, 2], Device::CPU).to(&device)?; - let b_gen = Tensor::from_data(vec![-2f32], shape![1, 1], Device::CPU).to(&device)?; - let gen = Linear::new(w_gen, Some(b_gen)); + let w_gen = Tensor::from_data(vec![3f32, 1.], (1, 2), TensorOptions::new())?.to(&device)?; + let b_gen = Tensor::from_data(vec![-2f32], (1, 1), TensorOptions::new())?.to(&device)?; + let r#gen = Linear::new(w_gen, Some(b_gen)); let sample_xs = Tensor::from_data( vec![2f32, 1., 7., 4., -4., 12., 5., 8.], - shape![4, 2], - Device::CPU, - ); + (4, 2), + TensorOptions::new(), + )?; let sample_xs = sample_xs.to(&device)?; - let sample_ys = gen.schedule(sample_xs.clone())?; + let sample_ys = r#gen.schedule(sample_xs.clone())?; // Now use backprop to run a linear regression between samples and get the coefficients back. - let w = Var::zeros::(&shape![1, 2], &device); - let b = Var::zeros::(&shape![1, 1], &device); - let mut opt = optimizer(vec![ - (Some(String::from("b")), b.clone()), - (Some(String::from("w")), w.clone()), - ])?; - let lin = Linear::new(w.as_tensor().clone(), Some(b.as_tensor().clone())); + let w = zeros((1, 2), TensorOptions::new().device(device.clone()))?; + let b = zeros((1, 1), TensorOptions::new().device(device.clone()))?; + let mut opt = optimizer(vec![w.clone(), b.clone()])?; + let lin = Linear::new(w.clone(), Some(b.clone())); for _step in 0..100 { let ys = lin.schedule(sample_xs.clone())?; - let loss = ys.sub(sample_ys.clone())?.square()?.sum(&[0])?; - let mut grads = loss.backward()?; - opt.backward_step(&mut grads, &device)?; + let loss = ys.sub(sample_ys.clone())?.square()?.sum(0, false)?; + loss.backward()?; + opt.backward_step(&device)?; // device.try_gpu().unwrap().mark_step().unwrap(); - let b = b.as_tensor().to(&Device::CPU)?; - let w = w.as_tensor().to(&Device::CPU)?; + let b = b.to(&Device::CPU)?; + let w = w.to(&Device::CPU)?; println!("b: {:?}, w: {:?}", b.to_vec::(), w.to_vec::()); let loss_cpu = loss.clone().to(&Device::CPU)?; let loss_vec = loss_cpu.to_vec::()?; println!("loss: {:?}", loss_vec[0]); } - let b = b.as_tensor().to(&Device::CPU)?; - let w = w.as_tensor().to(&Device::CPU)?; + let b = b.to(&Device::CPU)?; + let w = w.to(&Device::CPU)?; println!("b: {:?}, w: {:?}", b.to_vec::(), w.to_vec::()); assert_eq!(to_vec0_round(&b, 4)?, 0.7872); assert_eq!(to_vec1_round(&w, 4)?, &[2.7257, 0.7097]); @@ -54,7 +51,7 @@ fn run_linear_regression(optimizer: OptimizerFactory) -> anyhow #[test] fn sgd_linear_regression() -> anyhow::Result<()> { - fn optimizer(vars: Vec<(Option, Var)>) -> anyhow::Result { + fn optimizer(vars: Vec) -> anyhow::Result { SGD::new(vars, 0.001) } run_linear_regression(optimizer) @@ -62,7 +59,7 @@ fn sgd_linear_regression() -> anyhow::Result<()> { #[test] fn adamw_linear_regression() -> anyhow::Result<()> { - fn optimizer(vars: Vec<(Option, Var)>) -> anyhow::Result { + fn optimizer(vars: Vec) -> anyhow::Result { let params = ParamsAdamW { lr: 0.5, ..Default::default() @@ -76,30 +73,30 @@ fn gradient_descent(optimizer: OptimizerFactory) -> anyhow::Resu let _ = env_logger::builder().is_test(true).try_init(); let device = Device::request_device(DeviceRequest::GPU).unwrap(); - let target = Tensor::from_data(vec![5.0], shape![1], Device::CPU).to(&device)?; + let target = Tensor::from_data(vec![5.0], 1, TensorOptions::new())?.to(&device)?; // Initialize variable at 0.0 (shape is scalar) - let w = Var::from_tensor(&Tensor::from_data(vec![0.0], shape![1], Device::CPU).to(&device)?)?; + let w = Tensor::from_data(vec![0.0], 1, TensorOptions::new())?.to(&device)?; - let mut opt = optimizer(vec![(Some("w".into()), w.clone())])?; + let mut opt = optimizer(vec![w.clone()])?; for step in 0..100 { // Compute loss = (w - target)^2 - let loss = w.as_tensor().clone().sub(target.clone())?.square()?; + let loss = w.clone().sub(target.clone())?.square()?; // Backpropagate - let mut grads = loss.backward()?; - opt.backward_step(&mut grads, &device)?; + loss.backward()?; + opt.backward_step(&device)?; // Print debug info - let current_w = w.as_tensor().to(&Device::CPU)?.to_vec::()?; + let current_w = w.to(&Device::CPU)?.to_vec::()?; let current_loss = loss.to(&Device::CPU)?.to_vec::()?; #[cfg(feature = "plotting")] println!( "Step {step}: w = {:.4}, loss = {:.4} (fmt: {})", current_w[0], current_loss[0], - w.as_tensor().to(&Device::CPU)?.plot_fmt() + w.to(&Device::CPU)?.plot_fmt() ); println!( "Step {step}: w = {:.4}, loss = {:.4}", @@ -107,7 +104,7 @@ fn gradient_descent(optimizer: OptimizerFactory) -> anyhow::Resu ); } - let final_w = w.as_tensor().to(&Device::CPU)?.to_vec::()?; + let final_w = w.to(&Device::CPU)?.to_vec::()?; assert!( (final_w[0] - target.to(&Device::CPU)?.to_vec::()?[0]).abs() < 0.1, "Final w should be close to 5.0" @@ -117,7 +114,7 @@ fn gradient_descent(optimizer: OptimizerFactory) -> anyhow::Resu #[test] fn sgd_gradient_descent() -> anyhow::Result<()> { - fn optimizer(vars: Vec<(Option, Var)>) -> anyhow::Result { + fn optimizer(vars: Vec) -> anyhow::Result { SGD::new(vars, 0.1) } gradient_descent(optimizer) @@ -125,7 +122,7 @@ fn sgd_gradient_descent() -> anyhow::Result<()> { #[test] fn adamw_gradient_descent() -> anyhow::Result<()> { - fn optimizer(vars: Vec<(Option, Var)>) -> anyhow::Result { + fn optimizer(vars: Vec) -> anyhow::Result { let params = ParamsAdamW { lr: 0.2, ..Default::default() @@ -138,25 +135,22 @@ fn adamw_gradient_descent() -> anyhow::Result<()> { #[test] fn test_intermediate() -> anyhow::Result<()> { let device = Device::request_device(DeviceRequest::GPU).unwrap(); - let w_gen = Tensor::from_data(vec![3f32, 1.], shape![1, 2], Device::CPU).to(&device)?; - let b_gen = Tensor::from_data(vec![-2f32], shape![1, 1], Device::CPU).to(&device)?; - let gen = Linear::new(w_gen.clone(), Some(b_gen.clone())); + let w_gen = Tensor::from_data(vec![3f32, 1.], (1, 2), TensorOptions::new())?.to(&device)?; + let b_gen = Tensor::from_data(vec![-2f32], (1, 1), TensorOptions::new())?.to(&device)?; + let r#gen = Linear::new(w_gen.clone(), Some(b_gen.clone())); let sample_xs = Tensor::from_data( vec![2f32, 1., 7., 4., -4., 12., 5., 8.], - shape![4, 2], - Device::CPU, - ); + (4, 2), + TensorOptions::new(), + )?; let sample_xs = sample_xs.to(&device)?; - let sample_ys = gen.schedule(sample_xs.clone())?; + let sample_ys = r#gen.schedule(sample_xs.clone())?; // Now use backprop to run a linear regression between samples and get the coefficients back. - let w = Var::from_tensor( - &Tensor::from_data(vec![0f32, 0.], shape![1, 2], Device::CPU).to(&device)?, - )?; - // let b = Var::from_data(vec![0f32], shape![1], Device::CPU); - let b = - Var::from_tensor(&Tensor::from_data(vec![0f32], shape![1, 1], Device::CPU).to(&device)?)?; - let lin = Linear::new(w.as_tensor().clone(), Some(b.as_tensor().clone())); + let w = Tensor::from_data(vec![0f32, 0.], (1, 2), TensorOptions::new())?.to(&device)?; + // let b = Parameter::from_data(vec![0f32], 1, Device::CPU); + let b = Tensor::from_data(vec![0f32], (1, 1), TensorOptions::new())?.to(&device)?; + let lin = Linear::new(w.clone(), Some(b.clone())); let ys = lin.schedule(sample_xs.clone())?; let loss = ys.sub(sample_ys.clone())?.square()?; @@ -164,15 +158,17 @@ fn test_intermediate() -> anyhow::Result<()> { // Print loss println!("loss: {:?}", loss.to(&Device::CPU)?.to_vec::()?); - let gen_grads = loss.backward()?; - for (i, (_id, g)) in gen_grads.iter().enumerate() { - let g_clone = g.clone(); - println!( - "gen_grads[{}]: (op {:?}) {:?}", - i, - g_clone.clone().op().name(), - g_clone.to(&Device::CPU)?.to_vec::()? - ); - } + loss.backward()?; + // TODO(vinhowe): we'd need to make this a concern of the optimizer or otherwise extract the + // list of variables. Should be a simple pytorchish interface for this. + // for (i, (_id, g)) in gen_grads.iter().enumerate() { + // let g_clone = g.clone(); + // println!( + // "gen_grads[{}]: (op {:?}) {:?}", + // i, + // g_clone.clone().op().name(), + // g_clone.to(&Device::CPU)?.to_vec::()? + // ); + // } Ok(()) } diff --git a/crates/piston-web/Cargo.toml b/crates/piston-web/Cargo.toml new file mode 100644 index 00000000..b2603092 --- /dev/null +++ b/crates/piston-web/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "piston-web" +description = "Train small neural networks in your browser with WebGPU" +version = "0.0.0" +edition = "2024" +license = "MIT" +repository = "https://github.com/vinhowe/piston" + +[lib] +crate-type = ["cdylib", "rlib"] + +[package.metadata.docs.rs] +default-target = "wasm32-unknown-unknown" + + +[profile.release] +opt-level = 3 +lto = "fat" +strip = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +piston-macros = { path = "../piston-macros" } +piston = { path = "../piston-core" } +half = { workspace = true } +wasm-bindgen = { workspace = true } +safetensors = { workspace = true } +wasm-bindgen-futures = { workspace = true } +js-sys = { workspace = true } +anyhow.workspace = true +serde = { workspace = true } +serde-wasm-bindgen = { workspace = true } +console_error_panic_hook = { workspace = true } +console_log = { workspace = true } +parking_lot = { workspace = true } +log.workspace = true +fern = { workspace = true } +futures = "0.3.30" +tsify = { workspace = true } + +[dependencies.web-sys] +features = ['console', 'Window', 'Navigator'] +workspace = true + +[dev-dependencies] +wasm-bindgen-test.workspace = true +chrono = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { workspace = true, features = ["js"] } + +[package.metadata.cargo-machete] +ignored = ["tsify"] diff --git a/crates/piston-web/LICENSE b/crates/piston-web/LICENSE new file mode 100644 index 00000000..70ff3f1c --- /dev/null +++ b/crates/piston-web/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Thomas Vincent Howe +Copyright (c) 2024 Christopher Fleetwood + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/piston-web/README.md b/crates/piston-web/README.md new file mode 100644 index 00000000..daf019a8 --- /dev/null +++ b/crates/piston-web/README.md @@ -0,0 +1,2 @@ +# piston-web + diff --git a/crates/piston-web/cast.js b/crates/piston-web/cast.js new file mode 100644 index 00000000..df9b17a3 --- /dev/null +++ b/crates/piston-web/cast.js @@ -0,0 +1,5 @@ +// We use this to cast JsValue to the correct type in rustland. I'm still not +// sure I understand why we need this, but it does work. +export const cast = (value) => { + return value; +}; diff --git a/crates/piston-web/package.json b/crates/piston-web/package.json new file mode 100644 index 00000000..c2b89977 --- /dev/null +++ b/crates/piston-web/package.json @@ -0,0 +1,17 @@ +{ + "name": "@piston-ml/piston-web-wasm", + "type": "module", + "description": "Train small neural networks in your browser with WebGPU", + "version": "0.0.0", + "license": "MIT", + "files": [ + "piston-web_bg.wasm", + "piston-web.js", + "piston-web.d.ts" + ], + "main": "piston-web.js", + "types": "piston-web.d.ts", + "sideEffects": [ + "./snippets/*" + ] + } \ No newline at end of file diff --git a/crates/piston-web/src/device.rs b/crates/piston-web/src/device.rs new file mode 100644 index 00000000..5818871e --- /dev/null +++ b/crates/piston-web/src/device.rs @@ -0,0 +1,198 @@ +use std::cell::RefCell; + +use piston::{Device, DeviceRequest}; +use serde::de::Error as DeError; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use wasm_bindgen::prelude::*; + +use crate::js_util::downcast_from_ptr; + +#[wasm_bindgen(js_name = Device)] +#[derive(Clone)] +pub struct JsDevice { + #[wasm_bindgen(skip)] + pub(crate) inner: Device, +} + +#[wasm_bindgen(js_class = Device)] +impl JsDevice { + // Marker function for downcasting from a JS object + #[wasm_bindgen] + pub fn __wbg_piston_device() {} + + // This is used so we can keep a global device instance in the js lib without it being moved + // into various methods. Probably not the best way to do this. + #[wasm_bindgen(js_name = _clone)] + pub fn _clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + + #[wasm_bindgen(js_name = markStep)] + pub async fn mark_step(&self) -> Result<(), JsValue> { + crate::function::handle_mark_step(self) + .await + .map_err(|e| e.into()) + } + + #[wasm_bindgen(js_name = beginPass)] + pub fn begin_pass(&self) { + self.inner.try_gpu().unwrap().begin_pass(0); + } + + #[wasm_bindgen(js_name = setSharedObjectAllocationEnabled)] + pub fn set_shared_object_allocation_enabled(&self, enabled: bool) { + self.inner + .try_gpu() + .unwrap() + .set_shared_object_allocation_enabled(enabled); + } + + #[wasm_bindgen(js_name = setCachingEnabled)] + pub fn set_caching_enabled(&self, enabled: bool) { + self.inner.try_gpu().unwrap().set_caching_enabled(enabled); + } + + #[wasm_bindgen(js_name = setInplaceSupport)] + pub fn set_inplace_support(&self, enabled: bool) { + self.inner.try_gpu().unwrap().set_inplace_support(enabled); + } + + #[wasm_bindgen(getter)] + pub fn name(&self) -> String { + match self.inner { + Device::CPU => "cpu".to_string(), + Device::GPU(_) => "webgpu".to_string(), + } + } + + #[wasm_bindgen(js_name = usageBytes)] + pub fn usage_bytes(&self) -> u64 { + self.inner.try_gpu().unwrap().usage_bytes() + } + + #[wasm_bindgen(js_name = markUsageBytesStep)] + pub fn mark_usage_bytes_step(&self) { + self.inner.try_gpu().unwrap().mark_usage_bytes_step(); + } + + #[wasm_bindgen(js_name = peakUsageBytes)] + pub fn peak_usage_bytes(&self) -> u64 { + self.inner.try_gpu().unwrap().peak_usage_bytes_since_reset() + } + + #[wasm_bindgen(js_name = setVRAMLimit)] + pub fn set_vram_limit(&self, #[wasm_bindgen(js_name = vramLimit)] vram_limit: Option) { + self.inner.try_gpu().unwrap().set_vram_limit(vram_limit); + } + + #[wasm_bindgen(js_name = asWebGPUDevice)] + pub fn as_webgpu_device(&self) -> Option { + match &self.inner { + Device::GPU(gpu) => Some( + gpu.as_webgpu_device() + .dyn_into::() + .unwrap(), + ), + Device::CPU => None, + } + } +} + +thread_local! { + pub static GPU_DEVICE: RefCell> = const { RefCell::new(None) }; +} + +pub async fn gpu() -> Result { + // First check if the device is already initialized + let maybe_device = GPU_DEVICE.with(|refcell| refcell.borrow().clone()); + + if let Some(device) = maybe_device { + return Ok(device); + } + + // Otherwise, initialize it + let device = Device::request_device(DeviceRequest::GPU) + .await + .map_err(|e| e.to_string())?; + + let js_device = JsDevice { inner: device }; + GPU_DEVICE.with(|refcell| refcell.borrow_mut().replace(js_device.clone())); + + Ok(js_device) +} + +pub fn gpu_sync() -> Result { + GPU_DEVICE + .with(|refcell| refcell.borrow().clone()) + .ok_or(JsError::new("GPU device not initialized")) +} + +pub fn cpu() -> JsDevice { + JsDevice { inner: Device::CPU } +} + +#[wasm_bindgen(js_name = gpu)] +pub async fn gpu_wasm() -> Result { + gpu().await +} + +#[wasm_bindgen(js_name = cpu)] +pub async fn cpu_wasm() -> Result { + Ok(cpu()) +} + +#[wasm_bindgen] +pub fn seed(seed: u64) -> Result<(), JsValue> { + gpu_sync()?.inner.set_seed(seed); + Ok(()) +} + +// Serialize and deserialize implementations for JsDevice tsify +impl Serialize for JsDevice { + fn serialize(&self, serializer: S) -> Result { + // Serialize by a short string identifier + match self.inner { + Device::CPU => serializer.serialize_str("cpu"), + Device::GPU(_) => serializer.serialize_str("webgpu"), + } + } +} + +impl<'de> Deserialize<'de> for JsDevice { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + let normalized = s.to_ascii_lowercase(); + match normalized.as_str() { + "cpu" => Ok(JsDevice { inner: Device::CPU }), + "gpu" | "webgpu" => { + // Use the already-initialized GPU if available + match gpu_sync() { + Ok(dev) => Ok(dev), + Err(_e) => Err(DeError::custom( + "GPU device not initialized. Call gpu() from JS to initialize first.", + )), + } + } + other => Err(DeError::custom(format!("Unsupported device name: {other}"))), + } + } +} + +impl TryFrom for JsDevice { + type Error = JsError; + fn try_from(value: JsValue) -> Result { + if value.is_string() { + let s = value.as_string().unwrap(); + match s.as_str() { + "cpu" => Ok(cpu()), + "gpu" | "webgpu" => gpu_sync(), + _ => Err(JsError::new(&format!("Unsupported device name: {s}"))), + } + } else { + downcast_from_ptr(&value, "__wbg_piston_device", false) + .ok_or_else(|| JsError::new("Failed to downcast Device from JS value")) + } + } +} diff --git a/crates/piston-web/src/dtype.rs b/crates/piston-web/src/dtype.rs new file mode 100644 index 00000000..3398e092 --- /dev/null +++ b/crates/piston-web/src/dtype.rs @@ -0,0 +1,70 @@ +use crate::js_util::downcast_from_ptr; +use piston::DType; +use wasm_bindgen::prelude::*; +use wasm_bindgen::{JsError, JsValue}; + +#[wasm_bindgen(js_name = DType)] +pub struct JsDType { + #[wasm_bindgen(skip)] + pub(crate) dtype: DType, +} + +#[wasm_bindgen(js_class = DType)] +impl JsDType { + // Marker function for downcasting from a JS object + #[wasm_bindgen] + pub fn __wbg_piston_dtype() {} + + #[wasm_bindgen] + pub fn _clone(&self) -> JsDType { + JsDType { dtype: self.dtype } + } + + #[wasm_bindgen(getter, js_name = isFloatingPoint)] + pub fn is_floating_point(&self) -> bool { + self.dtype.is_float() + } + + #[wasm_bindgen(getter, js_name = isSigned)] + pub fn is_signed(&self) -> bool { + self.dtype.is_signed() + } + + #[wasm_bindgen(getter)] + pub fn itemsize(&self) -> usize { + self.dtype.size_of() + } + + #[wasm_bindgen(getter)] + pub fn name(&self) -> String { + self.dtype.as_str().to_string() + } +} + +#[wasm_bindgen(getter)] +pub fn float32() -> JsDType { + JsDType { dtype: DType::F32 } +} + +#[wasm_bindgen(getter)] +pub fn float16() -> JsDType { + JsDType { dtype: DType::F16 } +} + +#[wasm_bindgen(getter)] +pub fn int32() -> JsDType { + JsDType { dtype: DType::I32 } +} + +#[wasm_bindgen(getter)] +pub fn uint32() -> JsDType { + JsDType { dtype: DType::U32 } +} + +impl TryFrom for JsDType { + type Error = JsError; + fn try_from(value: JsValue) -> Result { + downcast_from_ptr(&value, "__wbg_piston_dtype", false) + .ok_or_else(|| JsError::new("Failed to downcast DType from JS value")) + } +} diff --git a/crates/piston-web/src/error.rs b/crates/piston-web/src/error.rs new file mode 100644 index 00000000..3b870993 --- /dev/null +++ b/crates/piston-web/src/error.rs @@ -0,0 +1,44 @@ +use piston::TensorError; +use wasm_bindgen::{JsError, JsValue}; + +/// Helper trait to convert anyhow::Error to JsError while preserving error information +pub trait IntoJsError { + fn into_js_error(self) -> JsError; +} + +impl IntoJsError for anyhow::Error { + fn into_js_error(self) -> JsError { + // Create a JavaScript Error object with the full error chain + let mut message = self.to_string(); + + // Add the error chain if available + let mut current = self.source(); + while let Some(err) = current { + message.push_str(&format!("\nCaused by: {err}")); + current = err.source(); + } + + JsError::new(&message) + } +} + +impl IntoJsError for TensorError { + fn into_js_error(self) -> JsError { + self.into() + } +} + +impl IntoJsError for JsValue { + fn into_js_error(self) -> JsError { + let message = self + .as_string() + .unwrap_or_else(|| format!("JavaScript error: {self:?}")); + JsError::new(&message) + } +} + +impl IntoJsError for JsError { + fn into_js_error(self) -> JsError { + self + } +} diff --git a/crates/piston-web/src/function.rs b/crates/piston-web/src/function.rs new file mode 100644 index 00000000..18eff594 --- /dev/null +++ b/crates/piston-web/src/function.rs @@ -0,0 +1,527 @@ +use js_sys::Array; +use js_sys::Promise; +use js_sys::{Function, Reflect}; +use piston::{RVec, rvec}; +use std::cell::RefCell; +use wasm_bindgen::JsCast; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::{JsFuture, future_to_promise}; + +use crate::error::IntoJsError; +use crate::js_util::is_subclass; +use crate::with_piston_web_module; + +// Function-mode constructor set from JavaScript +thread_local! { + static FUNCTION_MODE_CONSTRUCTOR: RefCell> = const { RefCell::new(None) }; +} + +/// Register the base FunctionMode constructor from JS. +#[wasm_bindgen(js_name = _setFunctionModeConstructor)] +pub fn set_function_mode_constructor(constructor: &Function) { + FUNCTION_MODE_CONSTRUCTOR.with(|cell| { + cell.borrow_mut().replace(constructor.clone()); + }); +} + +// MarkStep-mode constructor set from JavaScript +thread_local! { + static MARK_STEP_MODE_CONSTRUCTOR: RefCell> = const { RefCell::new(None) }; +} + +/// Register the base MarkStepMode constructor from JS. +#[wasm_bindgen(js_name = _setMarkStepModeConstructor)] +pub fn set_mark_step_mode_constructor(constructor: &Function) { + MARK_STEP_MODE_CONSTRUCTOR.with(|cell| { + cell.borrow_mut().replace(constructor.clone()); + }); +} + +#[inline] +fn get_type_of_jsvalue(obj_or_type: &JsValue) -> JsValue { + if obj_or_type.is_instance_of::() { + // Already a constructor / class + obj_or_type.clone() + } else { + // For regular values look up their `constructor` property + Reflect::get(obj_or_type, &JsValue::from_str("constructor")).unwrap_or(JsValue::UNDEFINED) + } +} + +fn append_overloaded_arg<'a>( + overloaded_args: &mut RVec<&'a JsValue>, + obj: &'a JsValue, + obj_is_type: bool, +) { + let obj_type = if obj_is_type { + obj + } else { + &get_type_of_jsvalue(obj) + }; + + // Skip if we've already seen this constructor + if overloaded_args + .iter() + .any(|arg| JsValue::eq(obj_type, &get_type_of_jsvalue(arg))) + { + return; + } + + // Default insertion point is "after everyone else" + let mut insert_at = overloaded_args.len(); + + // But if this is a *subclass* of something we have seen, + // it has to come *before* its superclass. + for (idx, arg) in overloaded_args.iter().enumerate() { + if is_subclass(obj_type, &get_type_of_jsvalue(arg)) { + insert_at = idx; + break; + } + } + + overloaded_args.insert(insert_at, obj); +} + +fn check_has_piston_function(mode_obj: &JsValue) -> bool { + let pistion_function_func: Option = + Reflect::get(mode_obj, &JsValue::from_str("_pistonFunction")) + .and_then(|v: JsValue| v.dyn_into()) + .ok(); + pistion_function_func.is_some() +} + +pub fn get_overloaded_args<'a, const N: usize>(args: &[&'a JsValue; N]) -> RVec<&'a JsValue> { + // If we're currently in subclass dispatch, suppress further subclass-based overloading + // to avoid recursive redispatch when the subclass calls the original function. + if is_subclass_dispatch_active() { + return rvec![]; + } + let mut overloaded_args = rvec![]; + for &arg in args { + if check_has_piston_function(arg) { + append_overloaded_arg(&mut overloaded_args, arg, false); + } + } + overloaded_args +} + +// #[cfg(disable)] +fn dispatch_on_mode( + args: &RVec<&JsValue>, + named_args: &JsValue, + js_types: &RVec, + original_function: &Function, +) -> Result, JsError> { + let mut _mode_guard = StashFunctionModeGuard::new(); + let mode_obj = &_mode_guard + .current_mode + .as_ref() + .expect("No mode object") + .js_mode_obj; + + let piston_function_func: Function = + Reflect::get(mode_obj, &JsValue::from_str("_pistonFunction")) + .and_then(|v: JsValue| v.dyn_into()) + .map_err(|_| JsError::new("Mode object does not have a _pistonFunction method"))?; + + let ret = piston_function_func + .apply( + mode_obj, + &Array::of4( + original_function, + &Array::from_iter(js_types), + &Array::from_iter(args), + named_args, + ), + ) + .map_err(|e| e.into_js_error())?; + + // If the handler returned a Promise, keep function-mode disabled until it settles. + if let Some(promise) = ret.dyn_ref::() { + // Prevent Drop from restoring the mode now; it will be restored in the finally callback. + let saved_mode = std::rc::Rc::new(std::cell::RefCell::new(_mode_guard.current_mode.take())); + let saved_mode_for_cb = saved_mode.clone(); + + let finally_cb = Closure::once(move || { + if let Some(mode) = saved_mode_for_cb.borrow_mut().take() { + push_function_mode(mode); + } + }); + + let new_promise = promise.finally(&finally_cb); + // Leak the closure to keep it alive until JS calls it. + finally_cb.forget(); + return Ok(Some(new_promise.unchecked_into())); + } + + // Immediate value: let the guard restore on drop; just propagate value/None. + if ret.is_undefined() || ret.is_null() { + Ok(None) + } else { + Ok(Some(ret)) + } +} + +// #[cfg(disable)] +fn dispatch_on_subclass( + args: &RVec<&JsValue>, + named_args: &JsValue, + overloaded_args: &RVec<&JsValue>, + js_types: &RVec, + original_function: &Function, +) -> Result { + let js_types_array = Array::from_iter(js_types); + + let mut ret = None; + for (arg, js_type) in overloaded_args.iter().zip(js_types_array.iter()) { + // Get function using reflection, getting _pistonFunction or whatever we call it + let piston_function_func: Function = + Reflect::get(arg, &JsValue::from_str("_pistonFunction")) + .and_then(|v: JsValue| v.dyn_into()) + .map_err(|_| JsError::new("Argument does not have a _pistonFunction method"))?; + + // -- In PyTorch, we skip disabled torch dispatches for infra modes. Don't know how that'll + // pan out here + + // -- In PyTorch, if it's a plain method, not a classmethod, we throw an error here + + // -- !!! (this seems important) In PyTorch, we do something different if this isn't in + // torchfunction mode. I don't know what that means, and most of the useful places I see it + // do call it with torchfunction mode. + + // Suppress nested subclass overloading while the original function is called. + push_subclass_dispatch(); + let applied = piston_function_func + .apply( + // This is a static method, so we pass in the type + &js_type, + &Array::of4( + original_function, + &js_types_array, + &Array::from_iter(args), + named_args, + ), + ) + .map_err(|e| e.into_js_error()); + + let ret_ = match applied { + Ok(v) => v, + Err(e) => { + pop_subclass_dispatch(); + return Err(e); + } + }; + + if let Some(promise) = ret_.dyn_ref::() { + // Keep subclass-dispatch suppression active until the Promise settles. + let finally_cb = Closure::once(move || { + pop_subclass_dispatch(); + }); + let new_promise = promise.finally(&finally_cb); + finally_cb.forget(); + ret = Some(new_promise.unchecked_into()); + break; + } else { + // Immediate value: restore suppression now. + pop_subclass_dispatch(); + let is_undefined_or_null = ret_.is_undefined() || ret_.is_null(); + ret = Some(ret_); + if !is_undefined_or_null { + break; + } + } + } + Ok(ret.unwrap_or(JsValue::UNDEFINED)) +} + +// #[cfg(disable)] +pub fn handle_piston_function( + input: Option<&JsValue>, + function_name: &str, + overloaded_args: &RVec<&JsValue>, + args: &[&JsValue; N], + named_args: &JsValue, +) -> Result { + // Resolve the function only from the overall module (global exports like opMethodAdd) + let generic_function: Function = with_piston_web_module(|module| { + let val = Reflect::get(module, &JsValue::from_str(function_name)) + .map_err(|e| e.into_js_error())?; + val.dyn_into().map_err(|_| { + JsError::new(&format!( + "Target module does not have function {function_name}" + )) + }) + })?; + + // Combine self and args, if self exists + let args: RVec<&JsValue> = input.into_iter().chain(args.iter().copied()).collect(); + + // INTERESTINGLY: If self exists, we don't pull from overloaded_args, we just use self. + + // Create js_types from overloaded_args + let js_types: RVec = overloaded_args + .iter() + .map(|arg| get_type_of_jsvalue(arg)) + .collect::>(); + + let mode_active = is_function_mode_active(); + + let mut ret = None; + + if mode_active { + ret = dispatch_on_mode(&args, named_args, &js_types, &generic_function)?; + } + + if ret.is_none() + && let Ok(curr_ret) = dispatch_on_subclass( + &args, + named_args, + overloaded_args, + &js_types, + &generic_function, + ) + { + ret = Some(curr_ret); + } + + // In PyTorch, they .release() the return value—we probably want to make sure we're not + // refcounting it, whatever that most closely looks like here. + ret.ok_or_else(|| JsError::new("Piston function handling failed; function modes or subclasses were registered but none returned a value.")) +} + +/// Placeholder for a future function-mode object. +#[derive(Clone, Debug)] +pub struct FunctionMode { + js_mode_obj: JsValue, +} + +thread_local! { + static FUNCTION_MODE_STACK: RefCell> = const { RefCell::new(Vec::new()) }; +} + +/// Push a new function mode onto the per-thread stack. +pub fn push_function_mode(mode: FunctionMode) { + FUNCTION_MODE_STACK.with(|stack| stack.borrow_mut().push(mode)); +} + +/// Pop the current function mode from the stack. +pub fn pop_function_mode() -> Option { + FUNCTION_MODE_STACK.with(|stack| stack.borrow_mut().pop()) +} + +/// Push a new JS function mode object onto the stack. +#[wasm_bindgen(js_name = _pushFunctionMode)] +pub fn push_function_mode_js(mode_obj: &JsValue) -> Result<(), JsValue> { + if mode_obj.is_undefined() || mode_obj.is_null() { + return Ok(()); + } + push_function_mode(FunctionMode { + js_mode_obj: mode_obj.clone(), + }); + Ok(()) +} + +/// Pop the current function mode and return it back to JS. +#[wasm_bindgen(js_name = _popFunctionMode)] +pub fn pop_function_mode_js() -> JsValue { + pop_function_mode() + .map(|fm| fm.js_mode_obj) + .unwrap_or_else(JsValue::undefined) +} + +/// Is there at least one active function mode on this thread? +pub fn is_function_mode_active() -> bool { + FUNCTION_MODE_STACK.with(|stack| !stack.borrow().is_empty()) +} + +// Subclass-dispatch suppression: prevent re-entrant subclass redispatch when a subclass +// implementation calls the original function. We model this as a depth counter. +thread_local! { + static SUBCLASS_DISPATCH_DEPTH: RefCell = const { RefCell::new(0) }; +} + +fn push_subclass_dispatch() { + SUBCLASS_DISPATCH_DEPTH.with(|d| *d.borrow_mut() = d.borrow().saturating_add(1)); +} + +fn pop_subclass_dispatch() { + SUBCLASS_DISPATCH_DEPTH.with(|d| { + let mut b = d.borrow_mut(); + if *b > 0 { + *b -= 1; + } + }); +} + +fn is_subclass_dispatch_active() -> bool { + SUBCLASS_DISPATCH_DEPTH.with(|d| *d.borrow() > 0) +} + +/// RAII guard that temporarily removes the active function mode so that nested +/// calls don’t immediately recurse back into the same handler. The mode is +/// restored when the guard goes out of scope. +pub struct StashFunctionModeGuard { + current_mode: Option, +} + +impl StashFunctionModeGuard { + pub fn new() -> Self { + let saved_mode = pop_function_mode(); + Self { + current_mode: saved_mode, + } + } +} + +impl Drop for StashFunctionModeGuard { + fn drop(&mut self) { + if let Some(mode) = self.current_mode.take() { + push_function_mode(mode); + } + } +} + +/// MarkStep mode: simpler analog to FunctionMode for intercepting mark_step. +#[derive(Clone, Debug)] +pub struct MarkStepMode { + js_mode_obj: JsValue, +} + +thread_local! { + static MARK_STEP_MODE_STACK: RefCell> = const { RefCell::new(Vec::new()) }; +} + +pub fn push_mark_step_mode(mode: MarkStepMode) { + MARK_STEP_MODE_STACK.with(|stack| stack.borrow_mut().push(mode)); +} + +pub fn pop_mark_step_mode() -> Option { + MARK_STEP_MODE_STACK.with(|stack| stack.borrow_mut().pop()) +} + +#[wasm_bindgen(js_name = _pushMarkStepMode)] +pub fn push_mark_step_mode_js(mode_obj: &JsValue) -> Result<(), JsValue> { + if mode_obj.is_undefined() || mode_obj.is_null() { + return Ok(()); + } + push_mark_step_mode(MarkStepMode { + js_mode_obj: mode_obj.clone(), + }); + Ok(()) +} + +#[wasm_bindgen(js_name = _popMarkStepMode)] +pub fn pop_mark_step_mode_js() -> JsValue { + pop_mark_step_mode() + .map(|m| m.js_mode_obj) + .unwrap_or_else(JsValue::undefined) +} + +pub fn is_mark_step_mode_active() -> bool { + MARK_STEP_MODE_STACK.with(|stack| !stack.borrow().is_empty()) +} + +/// Handle mark_step dispatch via the active MarkStepMode if present. +/// If the mode handler returns undefined/null, fall back to the default implementation. +pub async fn handle_mark_step(device: &crate::device::JsDevice) -> Result<(), JsError> { + if !is_mark_step_mode_active() { + // No mode active: run default behavior + device + .inner + .try_gpu() + .unwrap() + .mark_step() + .await + .map_err(|e| JsError::new(&e.to_string()))?; + return Ok(()); + } + + let mut _mode_guard = StashMarkStepModeGuard::new(); + let mode_obj = &_mode_guard + .current_mode + .as_ref() + .expect("No MarkStep mode object") + .js_mode_obj; + + let piston_mark_step_func: Function = + Reflect::get(mode_obj, &JsValue::from_str("_pistonMarkStep")) + .and_then(|v: JsValue| v.dyn_into()) + .map_err(|_| JsError::new("Mode object does not have a _pistonMarkStep method"))?; + + // Create original function closure returning a Promise + let device_inner = std::rc::Rc::new(device.inner.clone()); + let device_inner_for_closure = device_inner.clone(); + let original_fn_closure = Closure::wrap(Box::new(move || -> JsValue { + let device_inner = device_inner_for_closure.clone(); + let fut = async move { + device_inner + .try_gpu() + .unwrap() + .mark_step() + .await + .map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(JsValue::UNDEFINED) + }; + future_to_promise(fut).unchecked_into::() + }) as Box JsValue>); + // Cast to Function for JS .apply. Keep the Closure alive in scope until we finish. + let original_fn: Function = original_fn_closure + .as_ref() + .unchecked_ref::() + .clone(); + + let ret = piston_mark_step_func + .apply(mode_obj, &Array::of1(&original_fn)) + .map_err(|e| e.into_js_error())?; + + if let Some(promise) = ret.dyn_ref::() { + // Keep mode disabled until promise settles, then restore it + let saved_mode = _mode_guard.current_mode.take(); + JsFuture::from(promise.clone()) + .await + .map_err(|e| e.into_js_error())?; + if let Some(mode) = saved_mode { + push_mark_step_mode(mode); + } + // original_fn_closure drops here + return Ok(()); + } + + // Immediate value: if undefined/null, fall back to default implementation + if ret.is_undefined() || ret.is_null() { + device + .inner + .try_gpu() + .unwrap() + .mark_step() + .await + .map_err(|e| JsError::new(&e.to_string()))?; + Ok(()) + } else { + // Non-null immediate indicates the handler completed the step + Ok(()) + } +} + +/// RAII guard for temporarily stashing the current MarkStepMode +pub struct StashMarkStepModeGuard { + current_mode: Option, +} + +impl StashMarkStepModeGuard { + pub fn new() -> Self { + let saved_mode = pop_mark_step_mode(); + Self { + current_mode: saved_mode, + } + } +} + +impl Drop for StashMarkStepModeGuard { + fn drop(&mut self) { + if let Some(mode) = self.current_mode.take() { + push_mark_step_mode(mode); + } + } +} diff --git a/crates/piston-web/src/js_util.rs b/crates/piston-web/src/js_util.rs new file mode 100644 index 00000000..41cb385c --- /dev/null +++ b/crates/piston-web/src/js_util.rs @@ -0,0 +1,177 @@ +use js_sys::{Function, Object, Reflect}; +use wasm_bindgen::{convert::FromWasmAbi, prelude::*}; + +/// Best-effort subclass check for two JS constructor functions. +/// +/// Returns false if either `.prototype` lookup fails. +#[inline] +pub(crate) fn is_subclass(sub: &JsValue, sup: &JsValue) -> bool { + // Must both be constructor functions + if !sub.is_instance_of::() || !sup.is_instance_of::() { + return false; + } + + // Try to get sub.prototype; if that fails, bail out + let sub_proto = match Reflect::get(sub, &JsValue::from_str("prototype")) { + Ok(val) if !val.is_null() && !val.is_undefined() => val, + _ => return false, + }; + + // Try to get sup.prototype; if that fails, bail out + let sup_proto = match Reflect::get(sup, &JsValue::from_str("prototype")) { + Ok(val) if !val.is_null() && !val.is_undefined() => val, + _ => return false, + }; + + // Now test Object.prototype chain + let sup_proto_obj: Object = sup_proto.unchecked_into(); + sup_proto_obj.is_prototype_of(&sub_proto) +} + +// Adapted with minimal changes from +// https://github.com/wasmerio/wasmer/blob/641030b8d9414ac0ddd31500631d3cd863c1608f/lib/api/src/backend/js/error.rs#L101-L138 +pub(crate) fn downcast_from_ptr>( + value: &JsValue, + marker: &str, + weak: bool, +) -> Option { + if !value.is_object() { + return None; + } + + let prototype = &Reflect::get_prototype_of(value).ok()?; + let class = prototype.constructor(); + let key = JsValue::from_str(marker); + + let marker_func: Option = Reflect::get(&class, &key) + .and_then(|v: JsValue| v.dyn_into()) + .ok(); + + marker_func.as_ref()?; + + // Safety: The marker function exists, therefore it's safe to convert back to T. + unsafe { + // Prefer cloning the JS wrapper if a `_clone()` method exists to avoid consuming + // the original object (which would null out its internal pointer). This prevents + // subsequent calls from seeing a null pointer when the same JS object is reused. + let maybe_clone = Reflect::get( + value, + &JsValue::from_str(if weak { "_cloneWeak" } else { "_clone" }), + ) + .ok() + .and_then(|v| v.dyn_into::().ok()) + .and_then(|clone_fn| clone_fn.call0(value).ok()); + + // let target_obj = maybe_clone.as_ref().unwrap_or(value); + let target_obj = maybe_clone.as_ref().expect("clone function returned null"); + + // Note: this assumes the wrapper class generated by #[wasm_bindgen] will always have a + // `__destroy_into_raw()` method which consumes the wrapper and returns a pointer. + // This is valid as of wasm-bindgen version 0.2.100 + let destroy_key = JsValue::from_str("__destroy_into_raw"); + let ptr = Reflect::get(target_obj, &destroy_key) + .ok() + .and_then(|v| v.dyn_into::().ok()) + .and_then(|destroy_into_raw| destroy_into_raw.call0(target_obj).ok()) + .and_then(|ret| ret.as_f64())?; + + Some(::from_abi( + ptr as u32, + )) + } +} + +// Adapter from +// https://github.com/RReverser/serde-wasm-bindgen/blob/eaee8643f4deaf3846fdafb1374708288e1def2f/src/lib.rs#L104C1-L187C2 +pub mod try_from_js_value_preserve { + use serde::{Deserialize, de::Error}; + use wasm_bindgen::{JsValue, convert::FromWasmAbi}; + + // Some arbitrary string that no one will collide with unless they try. + pub(crate) const PRESERVED_VALUE_MAGIC: &str = "1fc430ca-5b7f-4295-92de-33cf2b145d38"; + + struct Magic; + + impl<'de> serde::de::Deserialize<'de> for Magic { + fn deserialize>(de: D) -> Result { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Magic; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("serde-wasm-bindgen's magic string") + } + + fn visit_str(self, s: &str) -> Result { + if s == PRESERVED_VALUE_MAGIC { + Ok(Magic) + } else { + Err(E::invalid_value(serde::de::Unexpected::Str(s), &self)) + } + } + } + + de.deserialize_str(Visitor) + } + } + + // Intentionally asymmetrical wrapper to ensure that only serde-wasm-bindgen preserves roundtrip. + #[derive(Deserialize)] + #[serde(rename = "1fc430ca-5b7f-4295-92de-33cf2b145d38")] + struct PreservedValueDeWrapper(Magic, Option); + + /// Deserialize any `JsCast` value. + /// + /// When used with the `Derializer` in `serde_wasm_bindgen`, this serializes the value by + /// passing it through as a `JsValue` and casting it. + /// + /// This function is compatible with the `serde(deserialize_with)` derive annotation. + pub fn deserialize<'de, D: serde::Deserializer<'de>, T: TryFrom>( + de: D, + ) -> Result, D::Error> + where + T::Error: std::fmt::Debug, + { + let wrap = PreservedValueDeWrapper::deserialize(de)?; + // When used with our deserializer this unsafe is correct, because the + // deserializer just converted a JsValue into_abi. + // + // Other deserializers are unlikely to end up here, thanks + // to the asymmetry between PreservedValueSerWrapper and + // PreservedValueDeWrapper. Even if some other deserializer ends up + // here, this may be incorrect but it shouldn't be UB because JsValues + // are represented using indices into a JS-side (i.e. bounds-checked) + // array. + wrap.1 + .map(|val| { + let val: JsValue = unsafe { FromWasmAbi::from_abi(val) }; + val.try_into().map_err(|e| { + D::Error::custom(format_args!( + "incompatible JS value {e:?} for type {}", + std::any::type_name::() + )) + }) + }) + .transpose() + } + + // No-op serializer + pub fn serialize>( + _value: &Option, + serializer: S, + ) -> Result { + serializer.serialize_none() + } +} + +/// Treat a `JsValue` as an optional: returns `None` when the value is `undefined` or `null`, +/// otherwise returns `Some(value)`. +#[inline] +pub fn to_option(value: JsValue) -> Option { + if value.is_undefined() || value.is_null() { + None + } else { + Some(value) + } +} diff --git a/crates/piston-web/src/lib.rs b/crates/piston-web/src/lib.rs new file mode 100644 index 00000000..8f195492 --- /dev/null +++ b/crates/piston-web/src/lib.rs @@ -0,0 +1,76 @@ +#![cfg(target_arch = "wasm32")] +mod device; +mod dtype; +mod error; +mod function; +mod js_util; +mod serialization; +mod shape; +mod tensor; + +#[cfg(test)] +mod test_utils; + +use std::cell::RefCell; + +use wasm_bindgen::prelude::*; + +// Fix: https://github.com/rustwasm/wasm-bindgen/issues/4446#issuecomment-2729543167 +mod wasm_ctor_workaround { + unsafe extern "C" { + pub(super) fn __wasm_call_ctors(); + } +} + +thread_local! { + static PISTON_WEB_MOD: RefCell> = const { RefCell::new(None) }; +} + +#[wasm_bindgen(start)] +pub fn start() { + // This is important as long as we use inventory, which presumably uses ctors + unsafe { wasm_ctor_workaround::__wasm_call_ctors() }; + + console_error_panic_hook::set_once(); + let logger = fern::Dispatch::new() + .format(|out, _message, record| { + out.finish(format_args!( + "[WASM {file}:{line}] {text}", + file = record.file().unwrap_or_else(|| record.target()), + line = record + .line() + .map_or_else(|| "[Unknown]".to_string(), |line| line.to_string()), + text = record.args(), + )) + }) + .level_for("tokenizers", log::LevelFilter::Off) + .level(log::LevelFilter::Info) + .chain(fern::Output::call(console_log::log)) + .apply(); + + match logger { + Ok(_) => log::info!("Logging initialized."), + Err(error) => eprintln!("Error initializing logging: {error:?}"), + } +} + +/// Called once from JS right after you finish instantiating the wasm. +#[wasm_bindgen(js_name = "_setPistonWebModule")] +pub fn set_piston_web_module(module: &JsValue) { + PISTON_WEB_MOD.with(|cell| { + cell.borrow_mut().replace(module.clone()); + }); +} + +pub(crate) fn with_piston_web_module(f: F) -> Result +where + F: FnOnce(&JsValue) -> Result, +{ + PISTON_WEB_MOD.with(|cell| { + let module = cell.borrow(); + let Some(module) = module.as_ref() else { + return Err(JsError::new("PistonWeb module not loaded")); + }; + f(module) + }) +} diff --git a/crates/piston-web/src/serialization.rs b/crates/piston-web/src/serialization.rs new file mode 100644 index 00000000..022e7198 --- /dev/null +++ b/crates/piston-web/src/serialization.rs @@ -0,0 +1,180 @@ +use std::sync::Arc; + +use crate::device::JsDevice; +use crate::error::IntoJsError; +use crate::tensor::JsTensor; +use futures::lock::Mutex; +use js_sys::{Object, Reflect}; +use piston::{DType, Device, OpTensor, Tensor, TensorOptions}; +use std::collections::HashMap; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use wasm_bindgen_futures::future_to_promise; + +/// Saves tensors to a safetensors format and returns an ArrayBuffer +/// +/// The input is expected to be a JavaScript object mapping tensor ID strings to +/// either JsTensor or JsParameter objects. +#[wasm_bindgen] +pub async fn save( + data: JsValue, + #[wasm_bindgen(js_name = configSerialized)] config_serialized: Option, +) -> Result { + // Check if the data is a JavaScript object + if !data.is_object() { + return Err(JsError::new( + "Expected an object mapping tensor IDs to tensors or parameters", + )); + } + + // Get object keys + let obj = Object::from(data); + let keys = js_sys::Object::keys(&obj); + let keys_len = keys.length(); + + let mut tensors: Vec<(String, Tensor)> = Vec::with_capacity(keys_len as usize); + + for i in 0..keys_len { + let key = keys.get(i).as_string().unwrap(); + let value = Reflect::get(&obj, &JsValue::from_str(&key)).map_err(|e| e.into_js_error())?; + + // Try to extract tensor from the value + match JsTensor::try_from(value) { + Ok(tensor) => tensors.push((key, tensor.inner().clone())), + Err(e) => { + return Err(JsError::new(&format!( + "Error processing key '{key}': {e:?}" + ))); + } + } + } + + // Make sure we have at least one tensor + if tensors.is_empty() { + return Err(JsError::new( + "No valid tensors found in the provided object", + )); + } + + let data = Arc::new(Mutex::new(Vec::new())); + let mut futures = Vec::new(); + + // For each Var, move the Tensor to CPU asynchronously. + for (k, tensor) in tensors.iter() { + let k = k.clone(); + let t = tensor.clone(); + let data_ref = Arc::clone(&data); + futures.push(future_to_promise(async move { + let t_cpu = t.to(&Device::CPU).await.unwrap(); + data_ref.lock().await.push((k, t_cpu)); + Ok(JsValue::undefined()) + })); + } + + // Wait for all futures + let promise_array = js_sys::Array::from_iter(futures.iter()); + JsFuture::from(js_sys::Promise::all(&promise_array)) + .await + .map_err(|e| e.into_js_error())?; + + // Collect the results + let data = Arc::try_unwrap(data).unwrap().into_inner(); + + // Convert to the format expected by safetensors + let data_ref: Vec<(&String, OpTensor)> = data + .iter() + .map(|(k, v)| (k, v.inner().read().clone())) + .collect::>(); + + // Serialize to safetensors format + let serialized = safetensors::tensor::serialize( + data_ref.iter().map(|(k, v)| (k, v)), + &config_serialized.map(|s| HashMap::from([("piston_extra".to_string(), s)])), + ) + .map_err(|e| JsError::new(&format!("Failed to serialize tensors: {e}")))?; + + // Create an ArrayBuffer from the serialized data + Ok(js_sys::Uint8Array::from(&serialized[..])) +} + +/// Loads tensors from a safetensors byte buffer and returns a JavaScript object +/// mapping tensor names to Tensor objects. +#[wasm_bindgen(unchecked_return_type = "{ state: Record; extra?: unknown }")] +pub fn load( + bytes: js_sys::Uint8Array, + #[wasm_bindgen(js_name = mapDevice)] map_device: Option, +) -> Result { + let device = map_device.unwrap_or_else(|| JsDevice { inner: Device::CPU }); + + // Copy bytes from the Uint8Array into a Rust Vec + let mut buf = vec![0u8; bytes.length() as usize]; + bytes.copy_to(&mut buf[..]); + + // Deserialize safetensors from bytes + let st = safetensors::SafeTensors::deserialize(&buf) + .map_err(|e| JsError::new(&format!("Failed to deserialize safetensors: {e}")))?; + + let obj = js_sys::Object::new(); + + for name in st.names() { + let view = st + .tensor(name) + .map_err(|e| JsError::new(&format!("Failed to read tensor '{name}': {e}")))?; + + // Map safetensors dtype to piston dtype + let dtype = match view.dtype() { + safetensors::Dtype::F32 => DType::F32, + safetensors::Dtype::F16 => DType::F16, + safetensors::Dtype::BF16 => DType::BF16, + safetensors::Dtype::I32 => DType::I32, + safetensors::Dtype::U32 => DType::U32, + other => { + return Err(JsError::new(&format!( + "Unsupported dtype in safetensors: {:?}", + other + ))); + } + }; + + let shape = view.shape(); + let data = view.data(); + + let tensor = Tensor::from_bytes( + data, + shape, + TensorOptions::new() + .dtype(dtype) + .device(device.inner.clone()) + .requires_grad(false), + ) + .map_err(|e| JsError::new(&format!("Failed to construct tensor '{name}': {e}")))?; + + // Wrap as a JS-visible Tensor + let js_tensor = JsTensor::new(tensor); + js_sys::Reflect::set(&obj, &JsValue::from_str(name), &JsValue::from(js_tensor)) + .map_err(|e| JsError::new(&format!("Failed to set property '{name}': {e:?}")))?; + } + + // Build return object: { state, extra } + let out = js_sys::Object::new(); + + // state + js_sys::Reflect::set(&out, &JsValue::from_str("state"), &obj) + .map_err(|e| JsError::new(&format!("Failed to set state on return object: {e:?}")))?; + + // extra (parse JSON if present) + let extra_js = safetensors::SafeTensors::read_metadata(&buf) + .ok() + .and_then(|(_, meta)| { + meta.metadata().as_ref().and_then(|map| { + map.get("piston_extra") + .and_then(|s| js_sys::JSON::parse(s).ok()) + }) + }) + .unwrap_or(JsValue::UNDEFINED); + js_sys::Reflect::set(&out, &JsValue::from_str("extra"), &extra_js) + .map_err(|e| JsError::new(&format!("Failed to set extra on return object: {e:?}")))?; + + Ok(out.into()) +} diff --git a/crates/piston-web/src/shape.rs b/crates/piston-web/src/shape.rs new file mode 100644 index 00000000..8d45ad23 --- /dev/null +++ b/crates/piston-web/src/shape.rs @@ -0,0 +1,217 @@ +use piston::{D, Dim, Dims, RVec, Shape, ShapeWithOneHole}; +use wasm_bindgen::{JsCast, JsError, JsValue}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FromJsDim(pub(crate) isize); + +impl Dim for FromJsDim { + fn to_index(&self, shape: &Shape, op: &'static str) -> anyhow::Result { + let dim = self.0; + let dim: Box = match dim { + dim if dim < 0 => Box::new(D::Minus(dim.unsigned_abs())), + _ => Box::new(dim as usize), + }; + dim.to_index(shape, op) + } + + fn to_index_plus_one(&self, shape: &piston::Shape, op: &'static str) -> anyhow::Result { + let dim = self.0; + let dim: Box = match dim { + dim if dim < 0 => Box::new(D::Minus(dim.unsigned_abs())), + _ => Box::new(dim as usize), + }; + dim.to_index_plus_one(shape, op) + } +} + +pub struct FromJsVecISize(pub(crate) Vec); + +impl Dims for FromJsVecISize { + fn to_indexes_internal(self, shape: &Shape, op: &'static str) -> anyhow::Result> { + let dims = self + .0 + .iter() + .map(|d| FromJsDim(*d).to_index(shape, op)) + .collect::, _>>()?; + Ok(dims) + } +} + +impl FromJsVecISize { + // Create a function instead of implementing TryFrom for Option + pub fn from_js_value(value: JsValue) -> Result, JsError> { + if value.is_null() || value.is_undefined() { + return Ok(None); + } + + if let Some(num) = value.as_f64() { + // If it's a single number + Ok(Some(FromJsVecISize(vec![num as isize]))) + } else if value.is_array() { + // If it's a JavaScript array + let array = value.dyn_into::().unwrap(); + let dims = array + .iter() + .filter_map(|v| v.as_f64().map(|f| f as isize)) + .collect::>(); + Ok(Some(FromJsVecISize(dims))) + } else if js_sys::Int32Array::instanceof(&value) { + // If it's an Int32Array + let array = js_sys::Int32Array::from(value); + let dims = array + .to_vec() + .into_iter() + .map(|v| v as isize) + .collect::>(); + Ok(Some(FromJsVecISize(dims))) + } else { + Err(JsError::new("Expected a number, array, or Int32Array")) + } + } +} + +impl ShapeWithOneHole for FromJsVecISize { + fn into_shape(self, el_count: usize) -> anyhow::Result { + let mut hole_count = 0; + let mut hole_index = None; + let mut product = 1; + + for (i, &dim) in self.0.iter().enumerate() { + if dim == -1 { + hole_count += 1; + hole_index = Some(i); + if hole_count > 1 { + anyhow::bail!("at most one dimension can be -1, got shape {:?}", self.0); + } + } else if dim <= 0 { + anyhow::bail!( + "dimensions must be positive except for at most one -1, got shape {:?}", + self.0 + ); + } else { + product *= dim as usize; + } + } + + let mut shape_vec = RVec::with_capacity(self.0.len()); + + if product == 0 { + anyhow::bail!( + "cannot reshape tensor of {el_count} elements with a product of 0, got shape {:?}", + self.0 + ); + } + if !el_count.is_multiple_of(product) { + anyhow::bail!( + "ShapeWithOneHole.into_shape: cannot reshape tensor with {el_count} elements to shape with -1, got shape {:?}", + self.0 + ); + } + let inferred_dim = el_count / product; + + for (i, &dim) in self.0.iter().enumerate() { + if hole_index == Some(i) { + shape_vec.push(inferred_dim); + } else { + shape_vec.push(dim as usize); + } + } + + Ok(Shape::from(shape_vec)) + } +} + +pub struct FromJsVecUsize(pub(crate) Vec); + +impl FromJsVecUsize { + pub fn from_js_value(value: JsValue) -> Result, JsError> { + if value.is_null() || value.is_undefined() { + return Ok(None); + } + + if let Some(num) = value.as_f64() { + let f = num; + if !f.is_finite() { + return Err(JsError::new( + "Expected a finite non-negative integer for dimension", + )); + } + if f < 0.0 { + return Err(JsError::new(&format!( + "Expected non-negative dimension, got {f}" + ))); + } + if f.fract() != 0.0 { + return Err(JsError::new(&format!( + "Expected integer dimension, got {f}" + ))); + } + if f > (usize::MAX as f64) { + return Err(JsError::new(&format!( + "Dimension exceeds usize::MAX, got {f}" + ))); + } + Ok(Some(FromJsVecUsize(vec![f as usize]))) + } else if value.is_array() { + let array = value.dyn_into::().unwrap(); + let mut dims: Vec = Vec::with_capacity(array.length() as usize); + for v in array.iter() { + let Some(f) = v.as_f64() else { + return Err(JsError::new("All dimensions in the array must be numbers")); + }; + if !f.is_finite() { + return Err(JsError::new( + "All dimensions must be finite non-negative integers", + )); + } + if f < 0.0 { + return Err(JsError::new(&format!( + "Dimensions must be non-negative, got {f}" + ))); + } + if f.fract() != 0.0 { + return Err(JsError::new(&format!( + "Dimensions must be integers, got {f}" + ))); + } + if f > (usize::MAX as f64) { + return Err(JsError::new(&format!( + "Dimension exceeds usize::MAX, got {f}" + ))); + } + dims.push(f as usize); + } + Ok(Some(FromJsVecUsize(dims))) + } else if js_sys::Int32Array::instanceof(&value) { + let array = js_sys::Int32Array::from(value); + let mut dims: Vec = Vec::with_capacity(array.length() as usize); + for v in array.to_vec() { + if v < 0 { + return Err(JsError::new(&format!( + "Dimensions must be non-negative, got {v}" + ))); + } + dims.push(v as usize); + } + Ok(Some(FromJsVecUsize(dims))) + } else if js_sys::Uint32Array::instanceof(&value) { + let array = js_sys::Uint32Array::from(value); + let dims = array + .to_vec() + .into_iter() + .map(|v| v as usize) + .collect::>(); + Ok(Some(FromJsVecUsize(dims))) + } else { + Err(JsError::new( + "Expected a non-negative integer, an array of non-negative integers, Int32Array, or Uint32Array", + )) + } + } +} + +impl From for Shape { + fn from(value: FromJsVecUsize) -> Self { + Shape::from(value.0) + } +} diff --git a/crates/piston-web/src/tensor.rs b/crates/piston-web/src/tensor.rs new file mode 100644 index 00000000..8144ffa0 --- /dev/null +++ b/crates/piston-web/src/tensor.rs @@ -0,0 +1,1356 @@ +// TODO(vinhowe): This cursed approach to reference handling partly reflects the tension between +// Rust's convenient reference counting approach to memory management and JS's automatic garbage +// collection, but there is probably a more thoughtful approach to this. Hard to justify the energy +// right now, though. + +use crate::error::IntoJsError; +use crate::js_util::downcast_from_ptr; +use crate::shape::FromJsDim; +use half::f16; +use js_sys::{Array, Function, Object, Reflect}; +use parking_lot::RwLock; +use piston::{ + AllDims, DType, Dim, IrScalarValue, IrValue, KaimingFan, KaimingNonLinearity, LazyOp, NormOrd, + NormalOrUniform, Storage, Tensor, TensorId, TensorTypeOrScalar, TensorTypeOrScalarEnum, +}; +use piston_macros::js_tensor_web_op; +use std::cell::RefCell; +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::{Arc, Weak}; +use wasm_bindgen::prelude::*; +use wasm_bindgen::{JsError, JsValue}; + +use crate::{device::JsDevice, dtype::JsDType}; + +enum MaybeStrong { + Strong(Arc), + Weak(Weak), +} + +impl MaybeStrong { + pub fn downgrade(&self) -> Weak { + match self { + MaybeStrong::Strong(a) => Arc::downgrade(a), + MaybeStrong::Weak(w) => w.clone(), + } + } + + pub fn upgrade(&self) -> Option> { + match self { + MaybeStrong::Strong(a) => Some(a.clone()), + MaybeStrong::Weak(w) => w.upgrade(), + } + } +} + +#[wasm_bindgen(js_name = Tensor)] +pub struct JsTensor { + inner: MaybeStrong<(RwLock>, StrongJsTensorId)>, +} + +impl JsTensor { + fn new_impl(inner: MaybeStrong<(RwLock>, StrongJsTensorId)>) -> Self { + if let MaybeStrong::Strong(ref inner) = inner { + register_active_tensor(JsTensor::new_impl(MaybeStrong::Weak(Arc::downgrade(inner)))); + } + Self { inner } + } + + pub fn new(inner: Tensor) -> Self { + Self::new_impl(MaybeStrong::Strong(Arc::new(( + RwLock::new(Some(inner)), + StrongJsTensorId::new(), + )))) + } + + pub fn new_weak(inner: Weak<(RwLock>, StrongJsTensorId)>) -> Self { + Self::new_impl(MaybeStrong::Weak(inner)) + } + + pub(crate) fn inner(&self) -> Tensor { + match self.inner { + MaybeStrong::Strong(ref inner) => inner + .0 + .read() + .as_ref() + .expect("Tried to use a dropped Tensor; strong inner value taken") + .clone(), + MaybeStrong::Weak(ref inner) => inner + .upgrade() + .as_ref() + .expect("Tried to use a dropped Tensor; ref dropped") + .0 + .read() + .as_ref() + .expect("Tried to use a dropped Tensor; weak inner value taken") + .clone(), + } + } + + pub(crate) fn weak(&self) -> JsTensor { + JsTensor::new_weak(self.inner.downgrade()) + } + + fn js_value(&self) -> JsValue { + self.weak().into() + } +} + +#[wasm_bindgen(js_class = Tensor)] +impl JsTensor { + // Marker function for downcasting from a JS object + #[wasm_bindgen(unchecked_prelude = "__wbg_piston_tensor(): void;")] + pub fn __wbg_piston_tensor() {} + + #[wasm_bindgen(js_name = new, unchecked_return_type = "Tensor")] + pub async fn new_js( + #[wasm_bindgen( + unchecked_param_type = "Float32Array | Float64Array | Int32Array | Uint32Array | number[]" + )] + data: &JsValue, + #[wasm_bindgen(unchecked_param_type = "Uint32Array | number[]")] value: &JsValue, + #[wasm_bindgen(unchecked_param_type = "FromDataOptions")] options: &JsValue, + ) -> Result { + fromData(data, value, options) + } + + #[wasm_bindgen(js_name = __pistonDrop)] + pub fn __piston_drop(&self) { + if let Some(inner) = self.inner.upgrade() { + inner.0.write().take(); + } + } + + #[wasm_bindgen(getter = __pistonHasValue)] + pub fn __piston_has_value(&self) -> bool { + self.inner + .upgrade() + .map(|inner| inner.0.read().is_some()) + .unwrap_or(false) + } + + #[wasm_bindgen(getter = __pistonStrongId)] + pub fn __piston_strong_id(&self) -> usize { + self.inner.upgrade().map(|inner| inner.1.0).unwrap_or(0) + } + + #[wasm_bindgen(getter)] + pub fn id(&self) -> usize { + self.inner().id().0 + } + + #[wasm_bindgen(getter = __pistonStrongCount)] + pub fn __piston_strong_count(&self) -> usize { + self.inner().strong_count() + } + + // - Skipping storage_view + + pub fn dim(&self) -> usize { + self.inner().dim() + } + + #[wasm_bindgen(getter)] + pub fn ndim(&self) -> usize { + self.inner().dim() + } + + #[wasm_bindgen(getter)] + pub fn dtype(&self) -> JsDType { + JsDType { + dtype: self.inner().dtype(), + } + } + + #[wasm_bindgen( + unchecked_return_type = "number[] | number", + unchecked_prelude = "size(): number[];\nsize(dim: number): number;" + )] + pub fn size(&self, dim: Option) -> JsValue { + let inner = self.inner(); + if let Some(dim) = dim { + let dim = FromJsDim(dim); + let size = dim.to_index(&inner.shape(), "size").unwrap(); + JsValue::from_f64(inner.shape()[size] as f64) + } else { + let shape = inner.shape().to_vec(); + let array = js_sys::Array::new(); + for dim in shape { + array.push(&JsValue::from_f64(dim as f64)); + } + array.into() + } + } + + #[wasm_bindgen(getter, unchecked_return_type = "number[]")] + pub fn shape(&self) -> JsValue { + let shape = self.inner().shape().to_vec(); + let array = js_sys::Array::new(); + for dim in shape { + array.push(&JsValue::from_f64(dim as f64)); + } + array.into() + } + + #[wasm_bindgen( + unchecked_return_type = "number[] | number", + unchecked_prelude = "stride(): number[];\nstride(dim: number): number;" + )] + pub fn stride(&self, dim: Option) -> JsValue { + let inner = self.inner(); + if let Some(dim) = dim { + let dim = FromJsDim(dim); + let stride = dim.to_index(&inner.shape(), "stride").unwrap(); + JsValue::from_f64(inner.stride()[stride] as f64) + } else { + let stride = inner.stride().to_vec(); + let array = js_sys::Array::new(); + for dim in stride { + array.push(&JsValue::from_f64(dim as f64)); + } + array.into() + } + } + + #[wasm_bindgen(getter = nbytes)] + pub fn num_bytes(&self) -> usize { + self.inner().num_bytes() + } + + #[wasm_bindgen(getter)] + pub fn device(&self) -> JsDevice { + JsDevice { + inner: self.inner().device(), + } + } + + // - Skipping storage + + pub fn resolved(&self) -> bool { + self.inner().resolved() + } + + pub fn op(&self) -> JsValue { + let op = self.inner().op(); + let ir = op.ir(); + let name = ir.name().to_string(); + + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"name".into(), &JsValue::from_str(&name)).unwrap(); + + if let Some(ir_fields) = ir.fields() { + js_sys::Reflect::set( + &obj, + &"fields".into(), + &convert_ir_fields_to_js(ir_fields, &op), + ) + .unwrap(); + } + + obj.into() + } + + // Convenient for building graphs + #[wasm_bindgen(unchecked_return_type = "number[]", js_name = srcIds)] + pub fn src_ids(&self) -> JsValue { + let src_ids = self + .inner() + .op() + .srcs() + .iter() + .map(|s| s.id().0) + .collect::>(); + let array = js_sys::Array::new(); + for id in src_ids { + array.push(&JsValue::from_f64(id as f64)); + } + array.into() + } + + pub fn scope(&self) -> Option { + self.inner().scope().clone() + } + + #[wasm_bindgen(js_name = isScalar)] + pub fn is_scalar(&self) -> bool { + self.inner().is_scalar() + } + + #[wasm_bindgen(getter = requiresGrad)] + pub fn requires_grad(&self) -> bool { + self.inner().requires_grad() + } + + #[wasm_bindgen(getter = isLeaf)] + pub fn is_leaf(&self) -> bool { + self.inner().is_leaf() + } + + #[wasm_bindgen(getter = retainsGrad)] + pub fn retains_grad(&self) -> bool { + self.inner().retains_grad() + } + + #[wasm_bindgen(js_name = retainGrad)] + pub fn retain_grad(&self) -> Result<(), JsError> { + self.inner().retain_grad().map_err(|e| e.into_js_error()) + } + + pub fn is_contiguous(&self) -> bool { + self.inner().is_contiguous() + } +} + +macro_rules! impl_binary_op { + ($op:ident, $Name:ident) => { + #[js_tensor_web_op(name = $Name, variants = [method, method_inplace, function])] + pub fn $op(input: Tensor, other: TensorOrScalar) -> anyhow::Result {} + }; +} + +macro_rules! impl_binary_op_tensor_only { + ($op:ident, $Name:ident) => { + #[js_tensor_web_op(name = $Name, variants = [method, method_inplace, function])] + pub fn $op(input: Tensor, other: Tensor) -> anyhow::Result {} + }; +} + +macro_rules! impl_ternary_op { + ($op:ident, $Name:ident) => { + #[js_tensor_web_op(name = $Name, variants = [method, method_inplace, function])] + pub fn $op( + input: Tensor, + tensor1: Tensor, + tensor2: Tensor, + value: f32, + ) -> anyhow::Result { + } + }; +} + +macro_rules! impl_cmp_op { + ($op:ident, $Name:ident) => { + #[js_tensor_web_op(name = $Name, variants = [method, method_inplace, function])] + pub fn $op(input: Tensor, other: TensorOrScalar) -> anyhow::Result {} + }; +} + +macro_rules! impl_unary_op { + ($op:ident, $Name:ident) => { + #[js_tensor_web_op(name = $Name, variants = [method, method_inplace, function])] + pub fn $op(input: Tensor) -> anyhow::Result {} + }; +} + +impl_binary_op!(add, Add); +impl_binary_op!(sub, Sub); +impl_binary_op!(mul, Mul); +impl_binary_op!(div, Div); +impl_binary_op!(pow, Pow); +impl_binary_op_tensor_only!(minimum, Minimum); +impl_binary_op_tensor_only!(maximum, Maximum); + +impl_ternary_op!(addcdiv, Addcdiv); +impl_ternary_op!(addcmul, Addcmul); + +impl_cmp_op!(eq, Eq); +impl_cmp_op!(ne, Ne); +impl_cmp_op!(gt, Gt); +impl_cmp_op!(ge, Ge); +impl_cmp_op!(lt, Lt); +impl_cmp_op!(le, Le); +impl_cmp_op!(logical_and, LogicalAnd); +impl_cmp_op!(logical_or, LogicalOr); +impl_cmp_op!(logical_xor, LogicalXor); + +impl_unary_op!(gelu, Gelu); +impl_unary_op!(tanh, Tanh); +impl_unary_op!(exp, Exp); +impl_unary_op!(log, Log); +impl_unary_op!(sin, Sin); +impl_unary_op!(cos, Cos); +impl_unary_op!(abs, Abs); +impl_unary_op!(sqrt, Sqrt); +impl_unary_op!(relu, Relu); +impl_unary_op!(relu2, Relu2); +impl_unary_op!(floor, Floor); +impl_unary_op!(ceil, Ceil); +impl_unary_op!(neg, Neg); +impl_unary_op!(sigmoid, Sigmoid); +impl_unary_op!(swiglu, Swiglu); +impl_unary_op!(silu, Silu); +impl_unary_op!(square, Square); +impl_unary_op!(recip, Recip); +impl_unary_op!(logical_not, LogicalNot); +impl_unary_op!(isnan, IsNan); +impl_unary_op!(isinf, IsInf); + +#[js_tensor_web_op(name = Full, variants = [function])] +pub fn full(shape: Shape, value: f32, options: TensorOptions) -> JsTensorResult { + piston::full(shape, value, options) +} + +#[js_tensor_web_op(name = Cast, variants = [method])] +pub fn cast(input: Tensor, dtype: DType) -> JsTensorResult { + piston::cast(input, dtype) +} + +#[js_tensor_web_op(name = Float, variants = [method])] +pub fn float(input: Tensor) -> JsTensorResult { + piston::cast(input, DType::F32) +} + +#[js_tensor_web_op(name = Half, variants = [method])] +pub fn half(input: Tensor) -> JsTensorResult { + piston::cast(input, DType::F16) +} + +#[js_tensor_web_op(name = GroupNorm, variants = [method])] +pub fn group_norm( + input: Tensor, + num_groups: usize, + weight: Option, + bias: Option, + eps: f32, +) -> JsTensorResult { +} + +#[js_tensor_web_op(name = LayerNorm, variants = [method])] +pub fn layer_norm( + input: Tensor, + weight: Option, + bias: Option, + eps: f32, +) -> JsTensorResult { +} + +#[js_tensor_web_op(name = RmsNorm, variants = [method])] +pub fn rms_norm( + input: Tensor, + weight: Option, + #[op(default = 1e-5)] eps: f32, +) -> JsTensorResult { +} + +#[js_tensor_web_op(name = Conv1d, variants = [method])] +pub fn conv1d( + input: Tensor, + weight: Tensor, + bias: Option, + #[op(default = 1)] stride: usize, + #[op(default = 0)] padding: usize, +) -> JsTensorResult { +} + +#[js_tensor_web_op(name = Softmax, variants = [method])] +pub fn softmax(input: Tensor, dim: Dim) -> JsTensorResult {} + +#[js_tensor_web_op(name = Rope, variants = [method_inplace])] +pub fn rope(input: Tensor, dim: usize, base: f32, offset: usize) -> JsTensorResult {} + +#[js_tensor_web_op(name = Alibi, variants = [method])] +pub fn alibi(input: Tensor, #[op(default = 8.0)] max_bias: f32) -> JsTensorResult {} + +#[js_tensor_web_op(name = Matmul, variants = [method])] +pub fn matmul( + input: Tensor, + rhs: Tensor, + #[op(default = false)] trans_lhs: bool, + #[op(default = false)] trans_rhs: bool, +) -> JsTensorResult { +} + +#[js_tensor_web_op(name = Gemm, variants = [method])] +pub fn gemm( + input: Tensor, + rhs: Tensor, + bias: Option, + #[op(default = false)] trans_lhs: bool, + #[op(default = false)] trans_rhs: bool, + #[op(default = false)] trans_out: bool, +) -> JsTensorResult { +} + +#[js_tensor_web_op(name = Affine, variants = [method])] +pub fn affine( + input: Tensor, + #[op(default = 1.0)] mul: f32, + #[op(default = 0.0)] add: f32, +) -> JsTensorResult { +} + +#[js_tensor_web_op(name = Sum, variants = [method])] +pub fn sum( + input: Tensor, + dim: Option, + #[op(default = false)] keepdim: bool, +) -> JsTensorResult { + if let Some(sum_dims) = dim { + input.sum(sum_dims, keepdim) + } else { + input.sum(AllDims, keepdim) + } +} + +#[js_tensor_web_op(name = Mean, variants = [method])] +pub fn mean( + input: Tensor, + dim: Option, + #[op(default = false)] keepdim: bool, +) -> JsTensorResult { + if let Some(mean_dims) = dim { + input.mean(mean_dims, keepdim) + } else { + input.mean(AllDims, keepdim) + } +} + +#[js_tensor_web_op(name = Var, variants = [method])] +pub fn var( + input: Tensor, + dim: Option, + #[op(default = false)] keepdim: bool, +) -> JsTensorResult { + if let Some(var_dims) = dim { + input.var(var_dims, keepdim) + } else { + input.var(AllDims, keepdim) + } +} + +#[js_tensor_web_op(name = Max, variants = [method])] +pub fn max( + input: Tensor, + dim: Option, + #[op(default = false)] keepdim: bool, +) -> JsTensorResult { + if let Some(max_dims) = dim { + input.max(max_dims, keepdim) + } else { + input.max(AllDims, keepdim) + } +} + +#[js_tensor_web_op(name = Min, variants = [method])] +pub fn min( + input: Tensor, + dim: Option, + #[op(default = false)] keepdim: bool, +) -> JsTensorResult { + if let Some(min_dims) = dim { + input.min(min_dims, keepdim) + } else { + input.min(AllDims, keepdim) + } +} + +#[js_tensor_web_op(name = Argmax, variants = [method])] +pub fn argmax( + input: Tensor, + dim: Option, + #[op(default = false)] keepdim: bool, +) -> JsTensorResult { + if let Some(argmax_dims) = dim { + input.argmax(argmax_dims, keepdim) + } else { + input.argmax(AllDims, keepdim) + } +} + +#[js_tensor_web_op(name = Argmin, variants = [method])] +pub fn argmin( + input: Tensor, + dim: Option, + #[op(default = false)] keepdim: bool, +) -> JsTensorResult { + if let Some(argmin_dims) = dim { + input.argmin(argmin_dims, keepdim) + } else { + input.argmin(AllDims, keepdim) + } +} + +#[js_tensor_web_op(name = Norm, variants = [method])] +pub fn norm( + input: Tensor, + ord: Option, + dim: Option, + #[op(default = false)] keepdim: bool, +) -> JsTensorResult { + if let Some(norm_dims) = dim { + input.norm(ord, norm_dims, keepdim) + } else { + input.norm(ord, AllDims, keepdim) + } +} + +#[js_tensor_web_op(name = Flatten, variants = [method, function])] +pub fn flatten( + input: Tensor, + #[op(default = 0)] start_dim: Dim, + #[op(default = -1)] end_dim: Dim, +) -> JsTensorResult { +} + +#[js_tensor_web_op(name = Slice, variants = [method])] +pub fn slice( + input: Tensor, + #[op(unchecked_type = "[start: number, end: number][]")] ranges: JsValue, +) -> JsTensorResult { + let ranges = ranges + .dyn_into::() + .expect("Ranges must be an array") + .iter() + .map(|r| { + let range: js_sys::Array = r.clone().into(); + let start = range.get(0).as_f64().map(|v| v as usize); + let end = range.get(1).as_f64().map(|v| v as usize); + if let Some(end) = end { + start.expect("Invalid range")..end + } else { + 0usize..start.expect("Invalid range") + } + }) + .collect::>(); + input.slice(&ranges) +} + +#[js_tensor_web_op(name = Split, variants = [method, function])] +pub fn split( + input: Tensor, + #[op(unchecked_type = "number | number[]", name = "splitSizeOrSections")] + split_size_or_sections: JsValue, + #[op(default = 0)] dim: Dim, +) -> Result, JsError> { + let arg = if let Some(n) = split_size_or_sections.as_f64() { + piston::SplitArg::SplitSize(n as usize) + } else if split_size_or_sections.is_object() && split_size_or_sections.is_array() { + let arr = split_size_or_sections + .dyn_into::() + .map_err(|_| JsError::new("split sections must be an array"))?; + let sizes = arr + .iter() + .map(|v| { + v.as_f64() + .map(|f| f as usize) + .ok_or_else(|| JsError::new("sections must be numbers")) + }) + .collect::, JsError>>()?; + piston::SplitArg::Sizes(sizes.into()) + } else { + return Err(JsError::new( + "split requires a number or an array of numbers", + )); + }; + let parts = piston::split(input, arg, dim).map_err(|e| e.into_js_error())?; + Ok::<_, JsError>(parts.into_iter().collect::>()) +} + +#[js_tensor_web_op(name = Chunk, variants = [method, function])] +pub fn chunk( + input: Tensor, + chunks: usize, + #[op(default = 0)] dim: Dim, +) -> Result, JsError> { + let parts = piston::chunk(input, chunks, dim).map_err(|e| e.into_js_error())?; + Ok::<_, JsError>(parts.into_iter().collect::>()) +} + +#[js_tensor_web_op(name = View, variants = [method])] +pub fn view(input: Tensor, shape: ShapeWithOneHole) -> JsTensorResult {} + +#[js_tensor_web_op(name = Unsqueeze, variants = [method, function])] +pub fn unsqueeze(input: Tensor, dim: Dim) -> JsTensorResult {} + +#[js_tensor_web_op(name = Squeeze, variants = [method, function])] +pub fn squeeze(input: Tensor, dim: Option) -> JsTensorResult { + match dim { + Some(dims) => input.squeeze(dims), + None => input.squeeze(()), + } +} + +#[js_tensor_web_op(name = Permute, variants = [method, function])] +pub fn permute(input: Tensor, dims: Dims) -> JsTensorResult {} + +#[js_tensor_web_op(name = Flip, variants = [method, function])] +pub fn flip(input: Tensor, dims: Dims) -> JsTensorResult {} + +#[js_tensor_web_op(name = Transpose, variants = [method, function])] +pub fn transpose(input: Tensor, dim0: Dim, dim1: Dim) -> JsTensorResult {} + +#[js_tensor_web_op(name = t, variants = [method, function])] +pub fn t(input: Tensor) -> JsTensorResult {} + +#[js_tensor_web_op(getter, name = TUpper, variants = [method], target = T)] +#[allow(non_snake_case)] +pub fn T_upper(input: Tensor) -> JsTensorResult {} + +#[js_tensor_web_op(getter, name = mT, variants = [method])] +#[allow(non_snake_case)] +pub fn mT(input: Tensor) -> JsTensorResult {} + +#[js_tensor_web_op(name = Cache, variants = [method])] +pub fn cache(input: Tensor, source: Tensor, dim: Dim, offset: usize) -> JsTensorResult {} + +#[js_tensor_web_op(name = BroadcastLeft, variants = [method])] +pub fn broadcast_left(input: Tensor, left_shape: Shape) -> JsTensorResult {} + +#[js_tensor_web_op(name = BroadcastTo, variants = [method])] +pub fn broadcast_to(input: Tensor, shape: Shape) -> JsTensorResult {} + +#[js_tensor_web_op(name = IndexSelect, variants = [method])] +pub fn index_select(input: Tensor, indices: Tensor, dim: Dim) -> JsTensorResult {} + +#[js_tensor_web_op(name = IndexWrite, variants = [method])] +pub fn index_write(input: Tensor, src: Tensor, write_start: Dims) -> JsTensorResult {} + +#[js_tensor_web_op(name = Where, variants = [method, function], js_name = "where")] +pub fn where_cond(input: Tensor, condition: Tensor, on_false: TensorOrScalar) -> JsTensorResult {} + +#[js_tensor_web_op(name = Clamp, variants = [method, method_inplace, function])] +pub fn clamp( + input: Tensor, + min: Option, + max: Option, +) -> JsTensorResult { +} + +#[js_tensor_web_op(name = ScatterAdd, variants = [method, function])] +pub fn scatter_add(input: Tensor, indices: Tensor, source: Tensor, dim: Dim) -> JsTensorResult {} + +#[js_tensor_web_op(name = IndexAdd, variants = [method_inplace, function])] +pub fn index_add(input: Tensor, indices: Tensor, source: Tensor, dim: Dim) -> JsTensorResult {} + +#[js_tensor_web_op(name = Gather, variants = [method, function])] +pub fn gather(input: Tensor, dim: Dim, index: Tensor) -> JsTensorResult {} + +#[js_tensor_web_op(name = Triu, variants = [method, method_inplace, function])] +pub fn triu(input: Tensor, k: Option) -> JsTensorResult {} + +#[js_tensor_web_op(name = Tril, variants = [method, method_inplace, function])] +pub fn tril(input: Tensor, k: Option) -> JsTensorResult {} + +#[js_tensor_web_op(name = Lerp, variants = [method, method_inplace, function])] +pub fn lerp(input: Tensor, end: Tensor, weight: TensorOrScalar) -> JsTensorResult {} + +#[js_tensor_web_op(name = Bernoulli, variants = [function, method, method_inplace])] +pub fn bernoulli(input: Tensor) -> JsTensorResult {} + +#[js_tensor_web_op(name = Multinomial, variants = [method, function])] +pub fn multinomial( + input: Tensor, + #[op(name = "numSamples")] num_samples: usize, + #[op(default = false)] replacement: bool, +) -> JsTensorResult { +} + +#[js_tensor_web_op(name = Topk, variants = [method, function])] +pub fn topk( + input: Tensor, + k: usize, + #[op(default = -1)] dim: Dim, + #[op(default = true)] largest: bool, + #[op(default = false)] sorted: bool, +) -> Result, JsError> { + if sorted { + return Err(JsError::new("topk: sorted=true not implemented")); + } + let result = piston::topk(input, k, Some(dim), Some(largest), Some(sorted)) + .map_err(|e| e.into_js_error())?; + Ok::<_, JsError>(result.into_iter().collect::>()) +} + +#[js_tensor_web_op(name = Zero, variants = [method_inplace])] +pub fn zero(input: Tensor) -> JsTensorResult {} + +#[js_tensor_web_op(name = ZerosLike, variants = [function])] +pub fn zeros_like(input: Tensor, options: TensorOptions) -> JsTensorResult {} + +#[js_tensor_web_op(name = OnesLike, variants = [function])] +pub fn ones_like(input: Tensor, options: TensorOptions) -> JsTensorResult {} + +#[js_tensor_web_op(name = Contiguous, variants = [method])] +pub fn contiguous(input: Tensor) -> JsTensorResult {} + +#[js_tensor_web_op(name = RequiresGrad, variants = [method_inplace])] +pub fn requires_grad(input: Tensor, requires_grad: bool) -> JsTensorResult {} + +#[js_tensor_web_op(name = FromData, variants = [function])] +pub fn from_data( + #[op( + unchecked_type = "Float32Array | Float16Array | Float64Array | Int32Array | Uint32Array | Uint8Array | number[]" + )] + data: JsValue, + shape: Shape, + options: TensorOptions, +) -> JsTensorResult { + if data.is_array() { + let array = data + .dyn_into::() + .map_err(|_| JsError::new("Failed to convert data to array"))?; + + let data = array + .iter() + .map(|v| { + v.as_f64() + .map(|f| f as f32) + .ok_or_else(|| JsError::new("Array contains non-numeric values")) + }) + .collect::, JsError>>()?; + + Tensor::from_data(data, shape, options) + } else if js_sys::Float32Array::instanceof(&data) { + let array = js_sys::Float32Array::from(data); + let data = array.to_vec().into_iter().collect::>(); + Tensor::from_data(data, shape, options) + } else if js_sys::Float64Array::instanceof(&data) { + let array = js_sys::Float64Array::from(data); + let data = array + .to_vec() + .into_iter() + .map(|v| v as f32) + .collect::>(); + Tensor::from_data(data, shape, options) + } else if js_sys::Int32Array::instanceof(&data) { + let array = js_sys::Int32Array::from(data); + let data = array.to_vec().into_iter().collect::>(); + Tensor::from_data(data, shape, options) + } else if js_sys::Uint32Array::instanceof(&data) { + let array = js_sys::Uint32Array::from(data); + let data = array.to_vec().into_iter().collect::>(); + Tensor::from_data(data, shape, options) + } else { + return Err(JsError::new("Unsupported data type")); + } +} + +#[js_tensor_web_op(name = To, variants = [method])] +pub async fn to(input: Tensor, device: Device) -> Result { + Ok::<_, JsError>(input.to(&device).await?) +} + +#[js_tensor_web_op(name = Grad, variants = [method], getter)] +pub fn grad(input: Tensor) -> Result, JsError> { + Ok::<_, JsError>(input.grad().map(JsTensor::new)) +} + +#[cfg(not(feature = "debug"))] +#[js_tensor_web_op(name = DebugTensor, variants = [method], getter)] +pub fn debug_tensor(input: Tensor) -> Result { + input.get_or_create_debug_tensor() +} + +#[wasm_bindgen(js_name = promoteTypes)] +pub fn promote_types(dtype1: JsDType, dtype2: JsDType) -> Result { + piston::promote_types(dtype1.dtype, dtype2.dtype) + .map(|dtype| JsDType { dtype }) + .map_err(|e| e.into_js_error()) +} + +#[wasm_bindgen(js_class = Tensor)] +impl JsTensor { + // Skipping copy; the api is not great right now + + // Skipping into_bytes; probably won't work great with reference counting + + // Skipping from_quantized; we don't really have well-rounded quantized support right now + + // Skipping from_disk; not clear what it would represent in the JS API + + pub fn detach(&self) -> Result { + Ok(JsTensor::new( + self.inner().detach().map_err(|e| e.into_js_error())?, + )) + } + + pub fn detach_(&self) -> Result { + Ok(JsTensor::new( + self.inner().detach_().map_err(|e| e.into_js_error())?, + )) + } + + #[wasm_bindgen(js_name = gpuBuffer, unchecked_return_type = "GPUBuffer|null")] + pub fn gpu_buffer(&self) -> Result { + let inner = self.inner(); + let inner_source = inner.inner_or_source(); + match inner_source.storage().as_ref() { + Some(Storage::GPU(g)) => Ok(g.inner().as_webgpu_buffer().into()), + _ => Err(JsError::new("Tensor is not on GPU")), + } + } + + #[wasm_bindgen(js_name = toVec, unchecked_return_type = "Float32Array | Int32Array | Uint32Array")] + pub async fn to_vec(&self, dtype: Option) -> Result { + let dtype = dtype.map(|d| d.dtype).unwrap_or(self.inner().dtype()); + let inner = self.inner(); + match dtype { + DType::F32 => { + let result = inner.to_vec::().await.map_err(|e| e.into_js_error())?; + let array = js_sys::Float32Array::new_with_length(result.len() as u32); + array.copy_from(&result); + Ok(array.into()) + } + DType::F16 => { + let result = inner.to_vec::().await.map_err(|e| e.into_js_error())?; + let f32_vec: Vec = result.iter().map(|&x| f16::to_f32(x)).collect(); + let array = js_sys::Float32Array::new_with_length(f32_vec.len() as u32); + array.copy_from(&f32_vec); + Ok(array.into()) + } + DType::I32 => { + let result = inner.to_vec::().await.map_err(|e| e.into_js_error())?; + let array = js_sys::Int32Array::new_with_length(result.len() as u32); + array.copy_from(&result); + Ok(array.into()) + } + DType::U32 => { + let result = inner.to_vec::().await.map_err(|e| e.into_js_error())?; + let array = js_sys::Uint32Array::new_with_length(result.len() as u32); + array.copy_from(&result); + Ok(array.into()) + } + _ => { + panic!("Unsupported dtype"); + } + } + } + + #[wasm_bindgen(unchecked_return_type = "number")] + pub async fn item(&self, dtype: Option) -> Result { + let dtype = dtype.map(|d| d.dtype).unwrap_or(self.inner().dtype()); + let inner = self.inner(); + match dtype { + DType::F32 => Ok(JsValue::from_f64(inner.item::().await.into())), + DType::F16 => Ok(JsValue::from_f64( + f16::to_f32(inner.item::().await).into(), + )), + DType::I32 => Ok(JsValue::from_f64(inner.item::().await.into())), + DType::U32 => Ok(JsValue::from_f64(inner.item::().await.into())), + _ => panic!("Unsupported dtype"), + } + } + + #[wasm_bindgen(js_name = hasNaN)] + pub fn has_nan(&self, dtype: Option) -> bool { + let dtype = dtype.map(|d| d.dtype).unwrap_or(self.inner().dtype()); + match dtype { + DType::F32 => self.inner().has_nan::(), + DType::F16 => self.inner().has_nan::(), + _ => panic!("Unsupported dtype"), + } + } + + #[wasm_bindgen(setter = grad)] + pub fn set_grad(&self, grad: Option) { + self.inner().set_grad(grad.map(|g| g.inner().clone())); + } + + pub fn backward(&self) -> Result<(), JsError> { + self.inner().backward().map_err(|e| e.into_js_error()) + } +} + +#[js_tensor_web_op(name = "Cat", variants = [function])] +pub fn cat(tensors: Vec, #[op(default = 0)] dim: Dim) -> anyhow::Result { + piston::cat(tensors.into(), dim) +} + +#[js_tensor_web_op(name = "Stack", variants = [function])] +pub fn stack(tensors: Vec, #[op(default = 0)] dim: Dim) -> anyhow::Result { + piston::stack(tensors.into(), dim) +} + +#[js_tensor_web_op(name = "Arange", variants = [function])] +pub fn arange( + #[op(keyword)] end: f32, + #[op(default = 0.0)] start: f32, + #[op(default = 1.0)] step: f32, + options: TensorOptions, +) -> anyhow::Result { + piston::arange(Some(start), end, Some(step), options) +} + +#[js_tensor_web_op(name = "Randint", variants = [function])] +pub fn randint( + high: i32, + #[op(keyword)] shape: Shape, + #[op(default = 0)] low: i32, + options: TensorOptions, +) -> anyhow::Result { + piston::randint(low, high, shape, options) +} + +#[js_tensor_web_op(name = "Randn", variants = [function])] +pub fn randn( + shape: Shape, + mean: Option, + std: Option, + options: TensorOptions, +) -> anyhow::Result { + piston::randn(shape, mean, std, options) +} + +#[js_tensor_web_op(name = "Rand", variants = [function])] +pub fn rand( + shape: Shape, + lo: Option, + up: Option, + options: TensorOptions, +) -> anyhow::Result { + piston::rand(shape, lo, up, options) +} + +#[js_tensor_web_op(name = "Eye", variants = [function])] +pub fn eye(n: usize, m: Option, options: TensorOptions) -> anyhow::Result { + piston::eye(n, m, options) +} + +#[js_tensor_web_op(name = OneHot, variants = [function, method])] +pub fn one_hot(input: Tensor, #[op(name = "numClasses")] num_classes: usize) -> JsTensorResult {} + +#[js_tensor_web_op(name = "Zeros", variants = [function])] +pub fn zeros(shape: Shape, options: TensorOptions) -> anyhow::Result { + piston::zeros(shape, options) +} + +#[js_tensor_web_op(name = "Ones", variants = [function])] +pub fn ones(shape: Shape, options: TensorOptions) -> anyhow::Result { + piston::ones(shape, options) +} + +#[js_tensor_web_op(name = "InitUniform", variants = [function])] +pub fn init_uniform_(input: Tensor, low: Option, high: Option) -> anyhow::Result { + piston::init_uniform_(&input, low, high) +} + +#[js_tensor_web_op(name = "InitNormal", variants = [function])] +pub fn init_normal_(input: Tensor, mean: Option, std: Option) -> anyhow::Result { + piston::init_normal_(&input, mean, std) +} + +#[js_tensor_web_op(name = "InitConstant", variants = [function])] +pub fn init_constant_(input: Tensor, value: f32) -> anyhow::Result { + piston::init_constant_(&input, value) +} + +#[js_tensor_web_op(name = "InitOnes", variants = [function])] +pub fn init_ones_(input: Tensor) -> anyhow::Result { + piston::init_ones_(&input) +} + +#[js_tensor_web_op(name = "InitZeros", variants = [function])] +pub fn init_zeros_(input: Tensor) -> anyhow::Result { + piston::init_zeros_(&input) +} + +#[js_tensor_web_op(name = "InitEye", variants = [function])] +pub fn init_eye_(input: Tensor) -> anyhow::Result { + piston::init_eye_(&input) +} + +#[js_tensor_web_op(name = "InitXavierUniform", variants = [function])] +pub fn init_xavier_uniform_(input: Tensor, gain: Option) -> anyhow::Result { + piston::init_xavier_uniform_(&input, gain) +} + +#[js_tensor_web_op(name = "InitXavierNormal", variants = [function])] +pub fn init_xavier_normal_(input: Tensor, gain: Option) -> anyhow::Result { + piston::init_xavier_normal_(&input, gain) +} + +fn init_kaiming_impl( + input: Tensor, + dist: NormalOrUniform, + a: Option, + mode: String, + nonlinearity: String, +) -> anyhow::Result { + let mode = match mode.as_ref() { + "fan_in" => KaimingFan::FanIn, + "fan_out" => KaimingFan::FanOut, + _ => return Err(anyhow::anyhow!("Invalid mode: {:?}", mode)), + }; + let nonlinearity = match nonlinearity.as_ref() { + "relu" => KaimingNonLinearity::ReLU, + "linear" => KaimingNonLinearity::Linear, + "sigmoid" => KaimingNonLinearity::Sigmoid, + "tanh" => KaimingNonLinearity::Tanh, + "selu" => KaimingNonLinearity::SELU, + "leaky_relu" => KaimingNonLinearity::LeakyReLU, + _ => return Err(anyhow::anyhow!("Invalid nonlinearity: {:?}", nonlinearity)), + }; + match dist { + NormalOrUniform::Uniform => piston::init_kaiming_uniform_(&input, a, mode, nonlinearity), + NormalOrUniform::Normal => piston::init_kaiming_normal_(&input, a, mode, nonlinearity), + } +} + +#[js_tensor_web_op(name = "InitKaimingUniform", variants = [function])] +pub fn init_kaiming_uniform_( + input: Tensor, + a: Option, + #[op(default = "fan_in".to_string(), unchecked_type = "'fan_in' | 'fan_out'")] mode: String, + #[op( + default = "leaky_relu".to_string(), + unchecked_type = "'relu' | 'linear' | 'sigmoid' | 'tanh' | 'selu' | 'leaky_relu'" + )] + nonlinearity: String, +) -> anyhow::Result { + init_kaiming_impl(input, NormalOrUniform::Uniform, a, mode, nonlinearity) +} + +#[js_tensor_web_op(name = "InitKaimingNormal", variants = [function])] +pub fn init_kaiming_normal_( + input: Tensor, + a: Option, + #[op(default = "fan_in".to_string(), unchecked_type = "'fan_in' | 'fan_out'")] mode: String, + #[op( + default = "leaky_relu".to_string(), + unchecked_type = "'relu' | 'linear' | 'sigmoid' | 'tanh' | 'selu' | 'leaky_relu'" + )] + nonlinearity: String, +) -> anyhow::Result { + init_kaiming_impl(input, NormalOrUniform::Normal, a, mode, nonlinearity) +} + +#[js_tensor_web_op(name = "InitOrthogonal", variants = [function])] +pub fn init_orthogonal_(input: Tensor, gain: Option) -> anyhow::Result { + piston::init_orthogonal_(&input, gain) +} + +#[wasm_bindgen(js_class = Tensor)] +impl JsTensor { + #[wasm_bindgen(js_name = _clone)] + pub fn _clone_js(&self) -> JsTensor { + JsTensor::new(self.inner().clone()) + } + + #[wasm_bindgen(js_name = _cloneWeak)] + pub fn _clone_weak_js(&self) -> JsTensor { + JsTensor::new_weak(self.inner.downgrade()) + } +} + +#[derive(Clone)] +#[wasm_bindgen(js_name = TensorOrScalar)] +struct JsTensorOrScalar { + inner: JsValue, +} + +impl TensorTypeOrScalar for JsTensorOrScalar { + fn tensor_or_scalar(&self) -> anyhow::Result> { + if let Ok(other) = JsTensor::try_from(self.inner.clone()) { + Ok(TensorTypeOrScalarEnum::Tensor(other.inner())) + } else { + let other: f32 = self + .inner + .as_f64() + .map(|f| f as f32) + .ok_or_else(|| anyhow::anyhow!("Failed to convert JsValue to f32"))?; + Ok(TensorTypeOrScalarEnum::Scalar(other)) + } + } +} + +impl TryFrom for JsTensorOrScalar { + type Error = JsError; + fn try_from(value: JsValue) -> Result { + if let Some(scalar) = value.as_f64() { + Ok(JsTensorOrScalar { + inner: JsValue::from_f64(scalar), + }) + } else { + // We don't do any extra validation here; just hope it'll work + Ok(JsTensorOrScalar { + inner: value.clone(), + }) + } + } +} + +fn js_value_to_norm_ord(value: JsValue) -> Result, JsError> { + if value.is_undefined() || value.is_null() { + // Handle undefined or null values + Ok(None) + } else if let Some(num) = value.as_f64() { + // Handle special numeric cases + if num == 0.0 { + Ok(Some(NormOrd::Zero)) + } else if num == 1.0 { + Ok(Some(NormOrd::One)) + } else if num == -1.0 { + Ok(Some(NormOrd::NegOne)) + } else if num == f64::INFINITY { + Ok(Some(NormOrd::Inf)) + } else if num == f64::NEG_INFINITY { + Ok(Some(NormOrd::NegInf)) + } else { + // For other numbers, use P norm + Ok(Some(NormOrd::P(num as f32))) + } + } else if let Some(string) = value.as_string() { + // Handle string values using from_str + Ok(Some(NormOrd::from_str(&string).map_err(|e| { + JsError::new(&format!("Invalid norm order: {e}")) + })?)) + } else { + Err(JsError::new("Norm order must be a number or string")) + } +} + +fn convert_ir_fields_to_js( + fields: &HashMap, + op: &LazyOp, +) -> JsValue { + let obj = js_sys::Object::new(); + + for (key, value) in fields { + js_sys::Reflect::set(&obj, &key.into(), &convert_ir_value_to_js(value, op)).unwrap(); + } + + obj.into() +} + +// Helper function to convert individual IrValue to JsValue +fn convert_ir_value_to_js(value: &IrValue, op: &LazyOp) -> JsValue { + match value { + IrValue::Tensor(tensor_value) => { + // Use existing JS tensors from the active map to avoid creating strong allocations + if let Some(list) = active_tensors().get(&tensor_value.id) + && let Some(first) = list.first() + { + return first.js_value(); + } + // No suitable existing JS tensor found :( + JsValue::NULL + } + // Handle scalar values + IrValue::Scalar(scalar) => match scalar { + IrScalarValue::F32(val) => JsValue::from_f64(*val as f64), + IrScalarValue::I32(val) => JsValue::from_f64(*val as f64), + IrScalarValue::U32(val) => JsValue::from_f64(*val as f64), + IrScalarValue::Bool(val) => JsValue::from_bool(*val), + IrScalarValue::String(val) => JsValue::from_str(val), + IrScalarValue::Vec4U32(val) => { + let array = js_sys::Array::new(); + array.push(&JsValue::from_f64(val.x as f64)); + array.push(&JsValue::from_f64(val.y as f64)); + array.push(&JsValue::from_f64(val.z as f64)); + array.push(&JsValue::from_f64(val.w as f64)); + array.into() + } + }, + // Handle nested IR + IrValue::Ir(nested_ir) => { + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"name".into(), &JsValue::from_str(nested_ir.name())) + .unwrap(); + + if let Some(nested_fields) = nested_ir.fields() { + js_sys::Reflect::set( + &obj, + &"fields".into(), + &convert_ir_fields_to_js(nested_fields, op), + ) + .unwrap(); + } + + obj.into() + } + // Handle Fields (should not happen in finalized op) + IrValue::Fields(_) => JsValue::NULL, + // Handle Vectors of IrValues + IrValue::Vec(vec_values) => { + let array = js_sys::Array::new(); + for value in vec_values.iter() { + array.push(&convert_ir_value_to_js(value, op)); + } + array.into() + } + // Handle None + IrValue::None => JsValue::NULL, + } +} + +impl TryFrom for JsTensor { + type Error = JsError; + fn try_from(value: JsValue) -> Result { + downcast_from_ptr::(&value, "__wbg_piston_tensor", true) + .ok_or_else(|| JsError::new("Failed to downcast Tensor from JS value")) + } +} + +thread_local! { + pub(crate) static ACTIVE_TENSORS: RefCell> = const { RefCell::new(Vec::new()) }; +} + +fn register_active_tensor(tensor: JsTensor) { + ACTIVE_TENSORS + .try_with(|cell| { + cell.borrow_mut() + .push(JsTensor::new_weak(tensor.inner.downgrade())); + }) + .ok(); +} + +fn active_tensors() -> HashMap> { + ACTIVE_TENSORS.with(|cell| { + let mut list = cell.borrow_mut(); + // Clean out broken references + list.retain(|t| t.inner.upgrade().filter(|t| t.0.read().is_some()).is_some()); + // Group by TensorId + let mut map: HashMap> = HashMap::new(); + for t in list.iter() { + let id = t.inner().id(); + map.entry(id).or_default().push(t._clone_weak_js()); + } + map + }) +} + +#[wasm_bindgen(js_name = __pistonActiveTensors, unchecked_return_type = "Map")] +pub fn active_tensors_js() -> JsValue { + let map_js = js_sys::Map::new(); + for (id, list) in active_tensors() { + let arr = Array::new(); + for js_tensor in list { + arr.push(&JsValue::from(js_tensor)); + } + map_js.set(&JsValue::from_f64(id.0 as f64), &arr.into()); + } + map_js.into() +} + +/// Unique identifier for tensors. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[wasm_bindgen] +pub struct StrongJsTensorId(pub usize); + +impl std::fmt::Debug for StrongJsTensorId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "T{}", self.0) + } +} + +impl Ord for StrongJsTensorId { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } +} + +impl PartialOrd for StrongJsTensorId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl StrongJsTensorId { + pub(crate) fn new() -> Self { + // https://users.rust-lang.org/t/idiomatic-rust-way-to-generate-unique-id/33805 + use std::sync::atomic; + static COUNTER: atomic::AtomicUsize = atomic::AtomicUsize::new(1); + Self(COUNTER.fetch_add(1, atomic::Ordering::Relaxed)) + } +} diff --git a/crates/piston-web/src/test_utils.rs b/crates/piston-web/src/test_utils.rs new file mode 100644 index 00000000..ef300b4c --- /dev/null +++ b/crates/piston-web/src/test_utils.rs @@ -0,0 +1,21 @@ +pub(crate) fn log_init() { + console_error_panic_hook::set_once(); + let logger = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{}[{}][{}] {}", + chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), + record.target(), + record.level(), + message + )) + }) + .level_for("tokenizers", log::LevelFilter::Off) + .level(log::LevelFilter::Info) + .chain(fern::Output::call(console_log::log)) + .apply(); + match logger { + Ok(_) => log::info!("Logging initialized."), + Err(error) => eprintln!("Error initializing logging: {error:?}"), + } +} diff --git a/crates/ratchet-cli/Cargo.toml b/crates/ratchet-cli/Cargo.toml deleted file mode 100644 index 014bf678..00000000 --- a/crates/ratchet-cli/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "ratchet-cli" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "ratchet" -path = "src/bin/cli.rs" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ratchet = { path = "../ratchet-core" } -ratchet-loader = { path = "../ratchet-loader" } -ratchet-models = { path = "../ratchet-models" } -ratchet-hub = { path = "../ratchet-hub" } -ratchet-nn = { path = "../ratchet-nn" } -log.workspace = true -clap = { workspace = true, features = ["derive"] } -hf-hub = { workspace = true } -serde_json = { workspace = true } -env_logger = { workspace = true } -fern = { workspace = true } -chrono = { workspace = true } -tokenizers = { workspace = true } -ndarray = { workspace = true } -ndarray-stats = { workspace = true } -anyhow.workspace = true diff --git a/crates/ratchet-cli/src/bin/cli.rs b/crates/ratchet-cli/src/bin/cli.rs deleted file mode 100644 index 08eb3e3b..00000000 --- a/crates/ratchet-cli/src/bin/cli.rs +++ /dev/null @@ -1,253 +0,0 @@ -#[cfg(not(target_arch = "wasm32"))] -mod cli { - use clap::{value_parser, Arg, ArgMatches, Command}; - use hf_hub::api::sync::Api; - use ndarray::Axis; - use ndarray_stats::QuantileExt; - use ratchet::{shape, Device, DeviceRequest, Tensor}; - use ratchet_loader::gguf::gguf::{self, Header}; - use ratchet_models::registry::{ - AvailableModels, Quantization, WhisperVariants as RegistryWhisper, - }; - use ratchet_models::whisper::options::DecodingOptionsBuilder; - use ratchet_models::whisper::transcribe::transcribe; - use ratchet_models::{phi2::Phi2, whisper::Whisper}; - use ratchet_nn::Module; - use std::io::Write; - use std::path::Path; - use std::process::Command as TermCommand; - use tokenizers::Tokenizer; - - fn ffmpeg_preproc>(path: P) -> Vec { - let path = path.as_ref(); - let output = TermCommand::new("ffmpeg") - .args([ - "-nostdin", - "-threads", - "0", - "-i", - path.to_str().unwrap(), - "-f", - "s16le", - "-ac", - "1", - "-acodec", - "pcm_s16le", - "-loglevel", - "error", - "-ar", - "16000", - "-", - ]) - .output() - .expect("Failed to execute ffmpeg command"); - - if !output.status.success() { - panic!( - "ffmpeg command failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - let audio_data = output.stdout; - let mut samples = Vec::new(); - - for chunk in audio_data.chunks(2) { - let sample = i16::from_le_bytes([chunk[0], chunk[1]]) as f32 / 32768.0; - samples.push(sample); - } - - samples - } - - pub fn start_logger() { - let logger = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{}[{}][{}] {}", - chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), - record.target(), - record.level(), - message - )) - }) - .level_for("tokenizers", log::LevelFilter::Off) - .level(log::LevelFilter::Warn) - .apply(); - match logger { - Ok(_) => log::info!("Logging initialized."), - Err(error) => eprintln!("Error initializing logging: {:?}", error), - } - } - - fn handle_whisper(matches: &ArgMatches, api: Api) { - let quantization = matches - .get_one::("quantization") - .unwrap_or(&Quantization::Q8_0); - - let mut whisper = if let Some(variant) = matches.get_one::("variant") { - let model = AvailableModels::Whisper(variant.clone()); - let repo = api.model(model.repo_id()); - let model_path = repo.get(&model.model_id(quantization.clone())).unwrap(); - println!("MODEL PATH: {}", model_path.display()); - - let mut reader = std::io::BufReader::new(std::fs::File::open(model_path).unwrap()); - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - let header = gguf::Header::read(&mut reader).unwrap(); - Whisper::load(header, variant.clone(), &mut reader, device).unwrap() - } else { - panic!("Model not found"); - }; - - if let Some(input) = matches.get_one::("input") { - let options = DecodingOptionsBuilder::new().build(); - let samples = ffmpeg_preproc(input); - let transcript = - transcribe(&mut whisper, samples, options, Some(|s| println!("{}", s))).unwrap(); - log::info!("Processing time: {:?}", transcript.processing_time); - } else { - panic!("Input file not found"); - }; - } - - fn handle_phi2(matches: &ArgMatches, api: Api) -> anyhow::Result<()> { - let _ = env_logger::builder().is_test(true).try_init(); - let model_repo = api.model("FL33TW00D-HF/phi2".to_string()); - let model_path = model_repo.get("phi2-q8_0.gguf").unwrap(); - println!("MODEL PATH: {}", model_path.display()); - let mut reader = std::io::BufReader::new(std::fs::File::open(model_path)?); - let device = Device::request_device(DeviceRequest::GPU)?; - let content = Header::read(&mut reader)?; - let mut model = Phi2::load(content, &mut reader, &device)?; - - let tokenizer = - Tokenizer::from_file(concat!("../../", "/models/microsoft/phi-2/tokenizer.json")) - .unwrap(); - - let prompt = if let Some(prompt) = matches.get_one::("prompt") { - prompt - } else { - "def print_prime(n):" - }; - - let max_tokens = matches.get_one::("max-tokens").unwrap(); - - let encoding = tokenizer.encode(prompt, true).unwrap(); - let mut tokens = encoding - .get_ids() - .iter() - .map(|&x| x as i32) - .collect::>(); - - print!("{}", prompt); - std::io::stdout().flush().unwrap(); - let mut all_tokens = tokens.clone(); - let mut loop_cnt = 0; - let start_time = std::time::Instant::now(); - while tokens[tokens.len() - 1] != 50256 && loop_cnt < *max_tokens { - let input = Tensor::from_data(tokens.clone(), shape![1, tokens.len()], device.clone()); - let result = model.schedule(input)?.full()?.resolve()?; - let logits = result.to(&Device::CPU)?; - model.cache_mut().update(tokens.len()); - - tokens = logits - .to_ndarray_view::() - .map_axis(Axis(2), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - let u32_toks = tokens.iter().map(|&x| x as u32).collect::>(); - print!("{}", tokenizer.decode(&u32_toks, true).unwrap()); - std::io::stdout().flush().unwrap(); - all_tokens.extend(tokens.clone()); - loop_cnt += 1; - } - let elapsed = start_time.elapsed(); - println!("\nElapsed time: {:?}", elapsed); - println!( - "tok/sec: {}", - all_tokens.len() as f64 / elapsed.as_secs_f64() - ); - Ok(()) - } - - #[cfg(not(target_arch = "wasm32"))] - fn main() -> Result<(), Box> { - env_logger::init(); - let matches = Command::new("ratchet") - .about("LLM & VLM CLI") - .version("0.1.0") - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new("whisper") - .long_about( - "Cross-platform, GPU accelerated implementation of OpenAI's Whisper Model.", - ) - .arg( - Arg::new("variant") - .short('v') - .long("variant") - .default_value("small") - .help("Whisper model variant to use.") - .value_parser(value_parser!(RegistryWhisper)), - ) - .arg( - Arg::new("quantization") - .short('q') - .long("quantization") - .default_value("f32") - .help("Model quantization to use.") - .value_parser(value_parser!(Quantization)), - ) - .arg( - Arg::new("input") - .short('i') - .long("input") - .required(true) - .help("Path to the input file"), - ), - ) - .subcommand( - Command::new("phi2") - .long_about( - "Cross-platform, GPU accelerated implementation of Microsoft's Phi2 model.", - ) - .arg( - Arg::new("prompt") - .short('p') - .long("prompt") - .required(true) - .help("Input prompt."), - ) - .arg( - Arg::new("max-tokens") - .short('m') - .long("max-tokens") - .default_value("256") - .value_parser(value_parser!(usize)) - .help("Maximum number of tokens to generate."), - ), - ) - .get_matches(); - - let api = Api::new().unwrap(); - if let Some(matches) = matches.subcommand_matches("phi2") { - let _ = handle_phi2(matches, api); - } else if let Some(matches) = matches.subcommand_matches("whisper") { - handle_whisper(matches, api); - } - - Ok(()) - } -} - -#[cfg(not(target_arch = "wasm32"))] -fn main() { - cli::main(); -} - -#[cfg(target_arch = "wasm32")] -fn main() { - // Empty function to get the CLI to compile anyway -} diff --git a/crates/ratchet-cli/src/lib.rs b/crates/ratchet-cli/src/lib.rs deleted file mode 100644 index 8b137891..00000000 --- a/crates/ratchet-cli/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/ratchet-core/src/backprop.rs b/crates/ratchet-core/src/backprop.rs deleted file mode 100644 index 875977e7..00000000 --- a/crates/ratchet-core/src/backprop.rs +++ /dev/null @@ -1,804 +0,0 @@ -/// Adapted from candle: -/// https://github.com/huggingface/candle/blob/main/candle-core/src/backprop.rs -/// Methods for backpropagation of gradients. -use crate::ops::{BinaryOp, UnaryOp}; -use crate::{ - rvec, Affine, Alibi, Binary, Broadcast, Cmp, Concat, Conv, DType, Gather, GroupNorm, IndexAdd, - IndexSelect, LazyOp, Matmul, Norm, NormOp, Permute, Powf, Reduce, ReduceOp, Reindex, RoPE, - ScatterAdd, Shape, Slice, Softmax, Tensor, TensorId, Unary, View, WhereCond, -}; -use crate::{HashMap, Trilu}; -use anyhow::Result; -use std::collections::hash_map::Entry; - -// thiserror error for Tensor -#[derive(thiserror::Error, Debug)] -pub enum BackpropError { - #[error("Tensor is not resolved")] - BackwardNotSupported { op: &'static str }, -} - -// arg has been reduced to node via reduce_dims, expand it back to arg. -// This has to handle keepdims. -fn broadcast_back(arg: &Tensor, node: &Tensor, reduced_dims: &[usize]) -> Result { - if arg.rank() == node.rank() { - // keepdim = true - node.clone().broadcast_to(arg.shape().clone()) - } else { - // keepdim = false - node.clone() - .view(reduced_dims.into())? - .broadcast_to(arg.shape().clone()) - } -} - -thread_local! { - static RATCHET_GRAD_DO_NOT_DETACH: bool = { - match std::env::var("RATCHET_GRAD_DO_NOT_DETACH") { - Ok(s) => { - !s.is_empty() && s != "0" - }, - Err(_) => false, - } - } -} - -impl Tensor { - /// Return all the nodes that lead to this value in a topologically sorted vec, the first - /// elements having dependencies on the latter ones, e.g. the first element if any is the - /// argument. - /// This assumes that the op graph is a DAG. - // TODO(vinhowe): This could be consolidated with execution_order and whatever caching we - // do... - fn sorted_nodes(&self) -> Vec<&Tensor> { - // The vec of sorted nodes is passed as an owned value rather than a mutable reference - // to get around some lifetime limitations. - fn walk<'a>( - node: &'a Tensor, - nodes: Vec<&'a Tensor>, - already_seen: &mut HashMap, - ) -> (bool, Vec<&'a Tensor>) { - if let Some(&tg) = already_seen.get(&node.id()) { - return (tg, nodes); - } - let mut track_grad = false; - let mut nodes = if node.is_variable() { - // Do not call recursively on the "leaf" nodes. - track_grad = true; - nodes - } else if matches!(node.dt(), DType::I32 | DType::U32) { - nodes - } else { - match node.op() { - LazyOp::IndexAdd(IndexAdd { - dst: t1, - src: t2, - ids: t3, - .. - }) - | LazyOp::ScatterAdd(ScatterAdd { - dst: t1, - src: t2, - ids: t3, - .. - }) - | LazyOp::WhereCond(WhereCond { - input: t1, - on_true: t2, - on_false: t3, - }) => { - let (tg, nodes) = walk(t1, nodes, already_seen); - track_grad |= tg; - let (tg, nodes) = walk(t2, nodes, already_seen); - track_grad |= tg; - let (tg, nodes) = walk(t3, nodes, already_seen); - track_grad |= tg; - nodes - } - LazyOp::Conv(Conv { - input: lhs, - weight: rhs, - .. - }) - | LazyOp::Binary(Binary { lhs, rhs, .. }) - | LazyOp::Gather(Gather { - src: lhs, ids: rhs, .. - }) - | LazyOp::Select(IndexSelect { - src: lhs, - indices: rhs, - .. - }) - | LazyOp::Matmul(Matmul { lhs, rhs, .. }) => { - let (tg, nodes) = walk(lhs, nodes, already_seen); - track_grad |= tg; - let (tg, nodes) = walk(rhs, nodes, already_seen); - track_grad |= tg; - nodes - } - LazyOp::Concat(Concat { inputs, .. }) => { - inputs.iter().fold(nodes, |nodes, input| { - let (tg, nodes) = walk(input, nodes, already_seen); - track_grad |= tg; - nodes - }) - } - LazyOp::Affine(Affine { - src: input, mul, .. - }) => { - if *mul == 0. { - nodes - } else { - let (tg, nodes) = walk(input, nodes, already_seen); - track_grad |= tg; - nodes - } - } - LazyOp::Unary(Unary { - input: _node, - op: UnaryOp::Ceil, - }) - | LazyOp::Unary(Unary { - input: _node, - op: UnaryOp::Floor, - }) => nodes, - LazyOp::Cmp(Cmp { lhs: node, .. }) - | LazyOp::Unary(Unary { input: node, .. }) - | LazyOp::Reduce(Reduce { - input: node, - op: ReduceOp::Min | ReduceOp::Sum | ReduceOp::Max, - .. - }) - | LazyOp::Reindex(Reindex::Permute(Permute { src: node, .. })) - | LazyOp::Reindex(Reindex::Broadcast(Broadcast { src: node, .. })) - | LazyOp::Reindex(Reindex::Slice(Slice { src: node, .. })) - | LazyOp::Softmax(Softmax { input: node, .. }) - | LazyOp::RoPE(RoPE { input: node, .. }) - | LazyOp::Powf(Powf { src: node, .. }) => { - let (tg, nodes) = walk(node, nodes, already_seen); - track_grad |= tg; - nodes - } - LazyOp::View(View { src: node, .. }) => { - let (tg, nodes) = walk(node, nodes, already_seen); - track_grad |= tg; - nodes - } - LazyOp::Norm(NormOp::RMSNorm(Norm { input: node, .. })) - | LazyOp::Norm(NormOp::LayerNorm(Norm { input: node, .. })) - | LazyOp::Norm(NormOp::GroupNorm(GroupNorm { - norm: Norm { input: node, .. }, - .. - })) => { - let (tg, nodes) = walk(node, nodes, already_seen); - track_grad |= tg; - nodes - } - LazyOp::IndexWrite(_) => todo!(), - LazyOp::Cast(_) => todo!(), - LazyOp::Copy(_) => todo!(), - LazyOp::Detach(_) - | LazyOp::Const - | LazyOp::Alibi(_) - | LazyOp::Reduce(Reduce { - op: ReduceOp::ArgMax | ReduceOp::ArgMin, - .. - }) - | LazyOp::FillConstant(_) - | LazyOp::FillRandn(_) - | LazyOp::Arange(_) - | LazyOp::Cache(_) - | LazyOp::Trilu(_) => nodes, - } - }; - already_seen.insert(node.id(), track_grad); - if track_grad { - nodes.push(node); - } - (track_grad, nodes) - } - let (_tg, mut nodes) = walk(self, vec![], &mut HashMap::default()); - nodes.reverse(); - nodes - } - - pub fn backward(&self) -> Result { - let sorted_nodes = self.sorted_nodes(); - let mut grads = GradStore::new(); - grads.insert(self, self.ones_like::().contiguous()); - for node in sorted_nodes.iter() { - if node.is_variable() { - continue; - } - log::debug!("Backwarding: {:?}", node.op().name()); - let grad = grads - .remove(node) - .expect("ratchet internal error - grad not populated"); - // From candle: - // https://github.com/huggingface/candle/issues/1241 - // Ideally, we would make these operations in place where possible to ensure that we - // do not have to allocate too often. Here we just call `.detach` to avoid computing - // the backprop graph of the backprop itself. This would be an issue for second order - // derivatives but these are out of scope at the moment. - let do_not_detach = RATCHET_GRAD_DO_NOT_DETACH.with(|b| *b); - let grad = if do_not_detach { grad } else { grad.detach() }; - match node.op() { - LazyOp::Binary(Binary { - lhs, - rhs, - op: BinaryOp::Add, - }) => { - grads.accumulate_add(lhs, grad.clone())?; - grads.accumulate_add(rhs, grad)?; - } - LazyOp::Binary(Binary { - lhs, - rhs, - op: BinaryOp::Sub, - }) => { - grads.accumulate_add(lhs, grad.clone())?; - grads.accumulate_sub(rhs, grad)?; - } - LazyOp::Binary(Binary { - lhs, - rhs, - op: BinaryOp::Mul, - }) => { - let lhs_grad = grad.clone().mul(rhs.clone())?; - grads.accumulate_add(lhs, lhs_grad)?; - let rhs_grad = grad.mul(lhs.clone())?; - grads.accumulate_add(rhs, rhs_grad)?; - } - LazyOp::Binary(Binary { - lhs, - rhs, - op: BinaryOp::Div, - }) => { - let lhs_grad = grad.clone().div(rhs.clone())?; - grads.accumulate_add(lhs, lhs_grad)?; - let rhs_grad = grad.mul(lhs.clone())?.div(rhs.clone().square()?)?; - grads.accumulate_sub(rhs, rhs_grad)?; - } - LazyOp::WhereCond(WhereCond { - input, - on_true, - on_false, - }) => { - let zeros = grad.clone().zeros_like::(); - let t_grad = input.clone().where_cond(grad.clone(), zeros.clone())?; - grads.accumulate_add(on_true, t_grad)?; - let f_grad = input.clone().where_cond(zeros, grad)?; - grads.accumulate_add(on_false, f_grad)?; - } - LazyOp::Matmul(Matmul { - lhs, - rhs, - trans_lhs, - trans_rhs, - trans_dst, - bias, - }) => { - let lhs_grad = - grad.clone() - .gemm(rhs.clone(), None, *trans_dst, !trans_rhs, *trans_lhs)?; - grads.accumulate_add(lhs, lhs_grad)?; - - let rhs_grad = - lhs.clone() - .gemm(grad.clone(), None, !trans_lhs, *trans_dst, *trans_rhs)?; - grads.accumulate_add(rhs, rhs_grad)?; - - // Calculate the gradient with respect to the bias term - if let Some(bias) = bias { - let bias_grad = grad.sum_keepdim(&[0])?; // Assuming bias is summed over the appropriate axis - grads.accumulate_add(bias, bias_grad)?; - } - } - LazyOp::Reindex(Reindex::Broadcast(Broadcast { src, .. })) => { - let arg_dims = src.shape().to_vec(); - let node_dims = node.shape().to_vec(); - - let left_dims = node_dims.len() - arg_dims.len(); - let mut sum_dims: Vec = (0..left_dims).collect(); - for (dim, (node_dim, arg_dim)) in node_dims[left_dims..] - .iter() - .zip(arg_dims.iter()) - .enumerate() - { - if node_dim != arg_dim { - sum_dims.push(dim + left_dims); - } - } - - let mut arg_grad = grad.sum_keepdim(sum_dims.as_slice())?; - for _i in 0..left_dims { - arg_grad = arg_grad.squeeze()?; - } - grads.accumulate_add(src, arg_grad.broadcast_to(src.shape().clone())?)?; - } - LazyOp::Reindex(Reindex::Slice(Slice { src: arg, indices })) => { - let arg_dims = arg.shape().to_vec(); - let index_lens = indices.iter().map(|range| range.end - range.start); - - // Get index of first dimension with length that doesn't match as a heuristic to make cat work - let first_different_index = arg_dims - .iter() - .zip(index_lens) - .position(|(arg_dim, slice_dim)| *arg_dim != slice_dim) - .unwrap(); - - let left_pad = if indices[first_different_index].start == 0 { - None - } else { - let mut dims = arg_dims.to_vec(); - dims[first_different_index] = indices[first_different_index].start; - Some(Tensor::zeros::(&Shape::from(dims), arg.device())) - }; - - let right_pad = - if arg_dims[first_different_index] == indices[first_different_index].end { - None - } else { - let mut dims = arg_dims.to_vec(); - dims[first_different_index] = arg_dims[first_different_index] - - indices[first_different_index].end; - Some(Tensor::zeros::(&Shape::from(dims), arg.device())) - }; - - let arg_grad = match (left_pad, right_pad) { - (None, None) => grad.clone(), - (Some(left_pad), None) => { - Tensor::cat(rvec![left_pad, grad], first_different_index)? - } - (None, Some(right_pad)) => { - Tensor::cat(rvec![grad, right_pad], first_different_index)? - } - (Some(left_pad), Some(right_pad)) => { - Tensor::cat(rvec![left_pad, grad, right_pad], first_different_index)? - } - }; - - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Reindex(Reindex::Permute(Permute { src: arg, dims })) => { - let mut inv_dims = vec![0; dims.len()]; - for (i, &dim) in dims.iter().enumerate() { - inv_dims[dim] = i; - } - let arg_grad = grad.permute(&inv_dims)?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Reduce(Reduce { - input: arg, - reduced_shape, - op: ReduceOp::Sum, - .. - }) => { - let grad = broadcast_back(arg, &grad, reduced_shape.inner())?; - grads.accumulate_add(arg, grad)?; - } - LazyOp::Reduce(Reduce { - input: arg, - reduced_shape, - op: ReduceOp::Max, - .. - }) => { - let node = broadcast_back(arg, node, reduced_shape.inner())?; - let grad = broadcast_back(arg, &grad, reduced_shape.inner())?; - let grad = node.eq(arg.clone())?.cast(grad.dt())?.mul(grad)?; - grads.accumulate_add(arg, grad.broadcast_to(arg.shape().clone())?)?; - } - LazyOp::Reduce(Reduce { - input: arg, - reduced_shape, - op: ReduceOp::Min, - .. - }) => { - let node = broadcast_back(arg, node, reduced_shape.inner())?; - let grad = broadcast_back(arg, &grad, reduced_shape.inner())?; - let grad = node.eq(arg.clone())?.cast(grad.dt())?.mul(grad)?; - grads.accumulate_add(arg, grad.broadcast_to(arg.shape().clone())?)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Log, - }) => { - let arg_grad = (grad / arg.clone())?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Sin, - }) => { - let arg_grad = (grad * arg.clone().cos())?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Cos, - }) => { - let arg_grad = (grad * arg.clone().sin())?; - grads.accumulate_sub(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Tanh, - }) => { - let minus_dtanh = ((*node).clone().square()? - 1.)?; - let arg_grad = (grad.clone() * minus_dtanh)?; - grads.accumulate_sub(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Abs, - }) => { - let ones = arg.ones_like::(); - let abs_grad = arg - .clone() - .ge(arg.clone().zeros_like::())? - .where_cond(ones.clone(), ones.neg()?)?; - let arg_grad = (grad * abs_grad)?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Exp, - }) => { - let arg_grad = (grad * (*node).clone())?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Neg, - }) => { - grads.accumulate_sub(arg, grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Reciprocal, - }) => { - let arg_grad = (grad / arg.clone().square()?)?; - grads.accumulate_sub(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: _, - op: UnaryOp::Ceil, - }) => Err(BackpropError::BackwardNotSupported { op: "ceil" })?, - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Gelu, - }) => { - let cube = arg.clone().powf(3.)?; - let tanh = (0.0356774 * cube.clone() + (0.797885 * arg.clone())?)?.tanh()?; - let gelu_grad = (((0.5 * tanh.clone())? - + (0.0535161 * cube + (0.398942 * arg.clone())?)? - * (1. - tanh.clone().powf(2.)?))? - + 0.5)?; - let arg_grad = (grad * gelu_grad)?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Relu, - }) => { - let relu_grad = arg.clone().affine(2.0, 0.0)?.mul( - arg.clone() - .ge(arg.clone().zeros_like::())? - .cast(arg.dt())?, - )?; - let arg_grad = grad.mul(relu_grad)?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Relu2, - }) => { - let relu_grad = arg - .clone() - .ge(arg.clone().zeros_like::())? - .cast(arg.dt())?; - let arg_grad = grad.mul(relu_grad)?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Silu, - }) => { - let sigmoid_arg = (arg.clone().neg()?.exp()? + 1.)?.recip()?; - let silu_grad = - (sigmoid_arg.clone() * (1. + (arg.clone() * (1. - sigmoid_arg)?)?)?)?; - let arg_grad = grad.mul(silu_grad)?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Swiglu, - }) => { - // swiglu(x) = x^2 * sigma(x) - // - // By product rule: - // d/dx [x^2 * sigma(x)] = 2x * sigma(x) + x^2 * sigma(x)*(1 - sigma(x)). - - // 1) Compute sigma(x) = 1 / (1 + e^-x). - let sigmoid_arg = (arg.clone().neg()?.exp()? + 1.)?.recip()?; - - // By product rule: - // 2) term1 = 2x * sigma(x). - let product_term_1 = (arg.clone() * 2.)?.mul(sigmoid_arg.clone())?; - - // 3) term2 = x^2 * sigma(x)*(1 - sigma(x)). - let product_term_2 = arg - .clone() - .square()? - .mul(sigmoid_arg.clone())? - .mul((1. - sigmoid_arg.clone())?)?; - - // 4) Final derivative wrt x is term1 + term2; multiply by the chain-rule grad. - let swiglu_grad = product_term_1.add(product_term_2)?; - let arg_grad = grad.mul(swiglu_grad)?; - - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Square, - }) => { - let arg_grad = arg.clone().mul(grad)?.affine(2., 0.)?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Unary(Unary { - input: arg, - op: UnaryOp::Sqrt, - }) => { - let arg_grad = grad.div((*node).clone())?.affine(0.5, 0.)?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Detach(_) => todo!(), - LazyOp::Unary(Unary { - input: _, - op: UnaryOp::Sigmoid, - }) => todo!(), - LazyOp::Unary(Unary { - input: _, - op: UnaryOp::Floor, - }) - | LazyOp::Reduce(Reduce { - input: _, - op: ReduceOp::ArgMax, - .. - }) - | LazyOp::Reduce(Reduce { - input: _, - op: ReduceOp::ArgMin, - .. - }) - | LazyOp::FillConstant(_) - | LazyOp::FillRandn(_) - | LazyOp::Arange(_) => {} - LazyOp::View(View { src: arg, .. }) => { - let arg_grad = grad.clone().view(arg.shape().clone())?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Select(IndexSelect { - src: arg, - indices, - dim, - }) => { - let sum_grad = grads.or_insert(arg.clone())?; - *sum_grad = sum_grad - .clone() - .index_add(indices.clone(), grad.clone(), *dim)?; - } - LazyOp::Softmax(Softmax { input: arg, dim }) => { - // Get the softmax output (s) - let softmax_output = (*node).clone(); - - // Compute the sum of the gradients - let sum_grad = grad.clone().sum_keepdim(&[*dim])?; - - // Compute the gradient with respect to the softmax input - let input_grad = softmax_output - .clone() - .mul(grad.clone())? - .sub(softmax_output.clone().mul(sum_grad)?)?; - - grads.accumulate_add(arg, input_grad)?; - } - LazyOp::Norm(NormOp::LayerNorm(Norm { - input: arg, - scale, - bias, - eps, - })) => { - let d = arg.shape()[1] as f32; - - // Retrieve the necessary intermediate values from the forward pass - let mean = (arg.clone().sum(&[0])? / d)?; // Compute mean of the input - let mean_broadcast = mean.clone().broadcast_to(arg.shape().clone())?; - - let var = (arg - .clone() - .sub(mean_broadcast.clone())? - .square()? - .sum(&[0])? - / d)?; - - let x_normed = arg - .clone() - .sub(mean_broadcast)? - .div((var.clone() + *eps)?.sqrt()?)?; - - // Compute the gradients with respect to beta and gamma - let grad_beta = grad.clone().sum_keepdim(&[0])?; - let grad_gamma = (x_normed.clone().mul(grad.clone()))?.sum_keepdim(&[0])?; - - // Compute the gradient with respect to the normalized input - let grad_x_normed = grad.clone().mul(scale.clone())?; - - // Compute the gradients with respect to mean and variance - let std = (var.clone() + *eps)?.sqrt()?; - let grad_mean = - (grad_x_normed.clone().sum_keepdim(&[1])?.neg())?.div(std.clone())?; - let grad_var = ((grad_x_normed.clone().mul(x_normed.clone()))? - .sum_keepdim(&[1])? - .neg()? - .div((var.clone() + *eps)?)? - / 2.0)?; - - let grad_x = grad_x_normed - .clone() - .div(std.clone())? - .add((grad_mean.clone() / d)?)? - .add( - (x_normed - .clone() - .mul(std.clone())? - .mul((grad_var.clone() * 2.0)?)? - / d)?, - )?; - - grads.accumulate_add(arg, grad_x)?; - grads.accumulate_add(scale, grad_gamma)?; - grads.accumulate_add(&bias.clone().unwrap(), grad_beta)?; - } - LazyOp::Affine(Affine { src: arg, mul, .. }) => { - let arg_grad = grad.affine(*mul, 0.)?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Gather(Gather { src, ids, dim, .. }) => { - let sum_grad = grads.or_insert(src.clone())?; - *sum_grad = sum_grad - .clone() - .scatter_add(ids.clone(), grad.clone(), *dim)?; - } - LazyOp::ScatterAdd(ScatterAdd { dst, src, ids, dim }) => { - grads.accumulate_add(dst, grad.clone())?; - let src_grad = grad.gather(ids.clone(), *dim)?; - grads.accumulate_add(src, src_grad)?; - } - LazyOp::Trilu(Trilu { src: arg, upper, k }) => { - let masked_grad = if *upper { - grad.triu(*k)? - } else { - grad.tril(*k)? - }; - grads.accumulate_add(arg, masked_grad)?; - } - LazyOp::Alibi(Alibi { input, .. }) => { - grads.accumulate_add(input, grad)?; - } - LazyOp::Norm(_) => todo!(), - LazyOp::Const => panic!("ratchet internal error - const node in backprop"), - LazyOp::Concat(_) => todo!(), - LazyOp::Cmp(_) => todo!(), - LazyOp::Powf(_) => todo!(), - LazyOp::Cast(_) => todo!(), - LazyOp::RoPE(RoPE { - input: arg, - dim, - base, - offset, - .. - }) => { - let arg_grad = grad.rope_backward(*dim, *base, *offset)?; - grads.accumulate_add(arg, arg_grad)?; - } - LazyOp::Conv(_) => todo!(), - LazyOp::IndexWrite(_) => todo!(), - LazyOp::IndexAdd(_) => todo!(), - LazyOp::Cache(_) => todo!(), - LazyOp::Copy(_) => todo!(), - }; - } - #[cfg(feature = "plotting")] - { - crate::plot::render_backward_to_file(&grads, "backward.svg").unwrap(); - } - Ok(grads) - } -} - -/// A store for gradients, associating a tensor id to the corresponding gradient tensor, used for back propagation. -#[derive(Debug)] -pub struct GradStore(HashMap); - -impl GradStore { - /// Create a new gradient store - fn new() -> Self { - GradStore(HashMap::default()) - } - - /// Get the gradient tensor corresponding to the given tensor id - pub fn get_id(&self, id: TensorId) -> Option<&Tensor> { - self.0.get(&id) - } - - /// Get the gradient tensor associated with the given tensor - pub fn get(&self, tensor: &Tensor) -> Option<&Tensor> { - self.0.get(&tensor.id()) - } - - /// Remove the gradient tensor associated with the given tensor, returning it if it exists - pub fn remove(&mut self, tensor: &Tensor) -> Option { - self.0.remove(&tensor.id()) - } - - /// Insert a gradient tensor associated with the given tensor, returning the previous gradient tensor if it existed - pub fn insert(&mut self, tensor: &Tensor, grad: Tensor) -> Option { - self.0.insert(tensor.id(), grad) - } - - pub fn iter(&self) -> impl Iterator { - self.0.iter() - } - - pub fn iter_mut(&mut self) -> impl Iterator { - self.0.iter_mut() - } - - /// Get the gradient tensor associated with the given tensor, or, if it does not exist, - /// insert a tensor of zeroes, with the same shape and type as the given tensors and return it - fn or_insert(&mut self, tensor: Tensor) -> Result<&mut Tensor> { - let grad = match self.0.entry(tensor.id()) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => { - let grad = tensor.clone().zeros_like::(); - entry.insert(grad) - } - }; - Ok(grad) - } - - /// If there's an existing gradient for `tensor`, add `grad` to it. - /// Otherwise, just store `grad` as-is (no need to create zeros and then add). - fn accumulate_add(&mut self, tensor: &Tensor, grad: Tensor) -> Result<()> { - use std::collections::hash_map::Entry; - match self.0.entry(tensor.id()) { - Entry::Occupied(mut entry) => { - let existing = entry.get_mut(); - *existing = existing.clone().add(grad)?; - } - Entry::Vacant(entry) => { - // TODO(vinhowe): This is a hack to avoid creating zeros and then adding; it does - // increase perf. - // It's not great; we should do a tensor copy or something. - entry.insert(grad.affine(1., 0.)?); - } - } - Ok(()) - } - - fn accumulate_sub(&mut self, tensor: &Tensor, grad: Tensor) -> Result<()> { - use std::collections::hash_map::Entry; - match self.0.entry(tensor.id()) { - Entry::Occupied(mut entry) => { - let existing = entry.get_mut(); - *existing = existing.clone().sub(grad)?; - } - Entry::Vacant(entry) => { - entry.insert(grad.neg()?); - } - } - Ok(()) - } -} diff --git a/crates/ratchet-core/src/cpu/binary.rs b/crates/ratchet-core/src/cpu/binary.rs deleted file mode 100644 index 146d80f4..00000000 --- a/crates/ratchet-core/src/cpu/binary.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::cpu::cpu_store_result; -use crate::{Binary, BinaryOp, CPUOperation, DType, OperationError, Tensor, TensorDType}; -use core::marker::PhantomData; -use half::{bf16, f16}; -use num_traits::NumOps; - -#[inline] -pub(crate) fn binary_map( - lhs: &[T], - rhs: &[T], - dst: &mut [U], - f: fn(T, T) -> U, -) { - assert_eq!(lhs.len(), dst.len()); - assert_eq!(rhs.len(), dst.len()); - for ((l, r), d) in lhs - .iter() - .copied() - .zip(rhs.iter().copied()) - .zip(dst.iter_mut()) - { - *d = f(l, r); - } -} - -#[inline] -pub(crate) fn binary_map_inplace(lhs: &mut [T], rhs: &[T], f: fn(T, T) -> T) { - assert_eq!(lhs.len(), rhs.len()); - lhs.iter_mut().zip(rhs.iter()).for_each(|(l, r)| { - *l = f(*l, *r); - }); -} - -#[inline] -pub(crate) fn binary_apply( - lhs: &Tensor, - rhs: &Tensor, - dst: &Tensor, - f: fn(T, T) -> U, -) -> Result<(), OperationError> { - let lhs = lhs.to_vec::()?; - let rhs = rhs.to_vec::()?; - let mut result = vec![U::zero(); dst.shape().numel()]; - binary_map(&lhs, &rhs, &mut result, f); - cpu_store_result(dst, &result); - Ok(()) -} - -#[inline] -pub(crate) fn binary_apply_inplace( - lhs: &Tensor, - rhs: &Tensor, - dst: &Tensor, - f: fn(T, T) -> T, -) -> Result<(), OperationError> { - let mut lhs = lhs.to_vec::()?; - let rhs = rhs.to_vec::()?; - binary_map_inplace(&mut lhs, &rhs, f); - cpu_store_result(dst, &lhs); - Ok(()) -} - -pub struct BinaryOps { - dtype: PhantomData, -} - -macro_rules! impl_cpu_binary_op { - ($method_name:ident, $dtype:ident, $op:expr) => { - fn $method_name(lhs: &Tensor, rhs: &Tensor, dst: Tensor) -> Result { - binary_apply_inplace::<$dtype>(lhs, rhs, &dst, $op)?; - Ok(dst) - } - }; -} - -macro_rules! cpu_binary_op_fn { - ($method_name:ident, $op:expr) => { - #[inline] - pub(crate) fn $method_name(lhs: &mut [T], rhs: &[T]) { - binary_map_inplace::(lhs, rhs, $op); - } - }; -} - -cpu_binary_op_fn!(add, |lhs, rhs| lhs + rhs); -cpu_binary_op_fn!(sub, |lhs, rhs| lhs - rhs); -cpu_binary_op_fn!(mul, |lhs, rhs| lhs * rhs); -cpu_binary_op_fn!(div, |lhs, rhs| lhs / rhs); - -macro_rules! impl_cpu_binary { - ($dtype:ident) => { - impl BinaryOps<$dtype> { - impl_cpu_binary_op!(add, $dtype, |lhs, rhs| lhs + rhs); - impl_cpu_binary_op!(sub, $dtype, |lhs, rhs| lhs - rhs); - impl_cpu_binary_op!(mul, $dtype, |lhs, rhs| lhs * rhs); - impl_cpu_binary_op!(div, $dtype, |lhs, rhs| lhs / rhs); - - pub fn apply(op: &Binary, dst: Tensor) -> Result { - match op.op() { - BinaryOp::Add => Self::add(op.lhs(), op.rhs(), dst), - BinaryOp::Sub => Self::sub(op.lhs(), op.rhs(), dst), - BinaryOp::Mul => Self::mul(op.lhs(), op.rhs(), dst), - BinaryOp::Div => Self::div(op.lhs(), op.rhs(), dst), - } - } - } - }; -} - -impl CPUOperation for Binary { - fn apply_cpu(&self, dst: Tensor) -> Result { - match dst.dt() { - DType::F32 => BinaryOps::::apply(self, dst), - DType::F16 => BinaryOps::::apply(self, dst), - DType::BF16 => BinaryOps::::apply(self, dst), - _ => todo!(), - } - } -} - -impl_cpu_binary!(f32); -impl_cpu_binary!(f16); -impl_cpu_binary!(bf16); diff --git a/crates/ratchet-core/src/cpu/mod.rs b/crates/ratchet-core/src/cpu/mod.rs deleted file mode 100644 index 67c6f370..00000000 --- a/crates/ratchet-core/src/cpu/mod.rs +++ /dev/null @@ -1,226 +0,0 @@ -mod binary; -pub mod gemm; -mod norm; -pub mod reindex; -pub mod rope; -mod softmax; -mod unary; -mod utils; - -use crate::{ - dequantize, Cast, Concat, DType, IndexSelect, InvariantError, LazyOp, Operation, - OperationError, RVec, Shape, Tensor, TensorDType, -}; -use anyhow::anyhow; -use half::{bf16, f16}; -use rope::cpu_rope; -use unary::unary_apply_fn; -use utils::cpu_store_result; - -pub fn apply_operation(op: LazyOp, dst: Tensor) -> Result { - match op { - LazyOp::Binary(b) => b.apply_cpu(dst), - LazyOp::Cast(c) => cpu_cast(c, dst), - LazyOp::Matmul(m) => m.apply_cpu(dst), - LazyOp::Softmax(s) => s.apply_cpu(dst), - LazyOp::RoPE(r) => cpu_rope(r, dst), - LazyOp::Alibi(a) => todo!(), - LazyOp::Unary(u) => u.apply_cpu(dst), - LazyOp::Reindex(r) => r.apply_cpu(dst), - LazyOp::Concat(c) => cpu_concat(c, dst), - LazyOp::Norm(n) => n.apply_cpu(dst), - LazyOp::Affine(_a) => todo!(), - LazyOp::Cmp(_c) => todo!(), - LazyOp::Powf(_p) => todo!(), - LazyOp::Conv(_c) => todo!(), - LazyOp::Select(i) => cpu_index_select(i, dst), - LazyOp::IndexWrite(_i) => todo!(), - LazyOp::Cache(_c) => todo!(), - LazyOp::Trilu(_t) => todo!(), - LazyOp::Const => todo!(), - LazyOp::View(_) => todo!(), - LazyOp::WhereCond(_w) => todo!(), - LazyOp::Reduce(_r) => todo!(), - LazyOp::Gather(_g) => todo!(), - LazyOp::FillConstant(_f) => todo!(), - LazyOp::FillRandn(_f) => todo!(), - LazyOp::Arange(_a) => todo!(), - LazyOp::IndexAdd(_i) => todo!(), - LazyOp::ScatterAdd(_s) => todo!(), - LazyOp::Detach(_d) => todo!(), - LazyOp::Copy(_c) => todo!(), - } -} - -pub trait CPUOperation: Operation { - fn apply_cpu(&self, dst: Tensor) -> Result; -} - -fn index_select( - index_select: IndexSelect, - dst: Tensor, -) -> Result { - let src = index_select.src(); - let indices = index_select.indices(); - let dim = index_select.dim(); - - // TODO: Add support for other indexing types - if !matches!(indices.dt(), DType::I32) { - return Err(InvariantError::DTypeMismatch { - expected: DType::I32, - actual: indices.dt(), - } - .into()); - } - - let mut dst_dims = src.shape().to_vec(); - let indices_dims = indices.shape().to_vec(); - - let src_dim = dst_dims[dim]; - let n_ids = indices_dims[0]; - dst_dims[dim] = n_ids; - - let dst_len: usize = dst_dims.iter().product(); - let left_len: usize = dst_dims[..dim].iter().product(); - let right_len: usize = dst_dims[dim + 1..].iter().product(); - - let src = src.to_vec::()?; - let indices = indices.to_vec::()?; - let mut result = vec![T::zero(); dst_len]; - - for left_i in 0..left_len { - let start_src_idx = left_i * right_len * src_dim; - let start_dst_idx = left_i * right_len * n_ids; - for (i, idx) in indices.iter().enumerate().take(n_ids) { - let src_idx = start_src_idx + *idx as usize * right_len; - let dst_idx = start_dst_idx + i * right_len; - result[dst_idx..dst_idx + right_len] - .copy_from_slice(&src[src_idx..src_idx + right_len]); - } - } - cpu_store_result(&dst, &result); - Ok(dst) -} - -fn qindex_select(op: IndexSelect, dst: Tensor) -> Result { - // NOTE: qindex_select is functional but not optimized at all. - // Currently we simply dequantize the entire input tensor to f32 and then call index_select. - // Because of borrowing rules dequantizing also requires a deep clone of the input tensor, which is less than ideal. - // In the future we would rather directly index the raw buffer of the quantized tensor and dequantize only what is required. - // TODO: Add support for direct indexing + partial dequantization - let src = op.src().deep_clone(); - - // NOTE: Support for other quantization types is dependent on the corresponding dequantization functions. - let src = dequantize(src); - let indices = op.indices().clone(); - let dim = op.dim(); - - index_select::(IndexSelect::new(src, indices, dim), dst) -} - -pub fn cpu_index_select(i: IndexSelect, dst: Tensor) -> Result { - match i.src().dt() { - DType::F32 => index_select::(i, dst), - DType::F16 => index_select::(i, dst), - DType::BF16 => index_select::(i, dst), - DType::Q8_0F(_) => qindex_select(i, dst), - dtype => Err(InvariantError::UnsupportedDType(dtype).into()), - } -} - -fn direct_cast( - input: &Tensor, - dst: &Tensor, -) -> Result<(), OperationError> { - let input = input.to_vec::()?; - let result = - bytemuck::try_cast_slice::(&input).map_err(|_| anyhow!("Failed direct cast"))?; - cpu_store_result(dst, result); - Ok(()) -} - -pub fn cpu_cast(cast: Cast, dst: Tensor) -> Result { - if cast.input().dt() == cast.dst_dt() { - return Ok(cast.input().clone()); - } - match (cast.input().dt(), cast.dst_dt()) { - // F32 -> - (DType::F32, DType::F16) => unary_apply_fn::(cast.input(), &dst, f16::from_f32)?, - (DType::F32, DType::BF16) => { - unary_apply_fn::(cast.input(), &dst, bf16::from_f32)? - } - (DType::F32, DType::I32) => direct_cast::(cast.input(), &dst)?, - (DType::F32, DType::U32) => direct_cast::(cast.input(), &dst)?, - - // F16 -> - (DType::F16, DType::F32) => unary_apply_fn::(cast.input(), &dst, f32::from)?, - - // BF16 -> - (DType::BF16, DType::F32) => unary_apply_fn::(cast.input(), &dst, f32::from)?, - - // I32 -> - (DType::I32, DType::F32) => direct_cast::(cast.input(), &dst)?, - - // U32 -> - (DType::U32, DType::F32) => direct_cast::(cast.input(), &dst)?, - - _ => unimplemented!("Cannot cast {:?} -> {:?}", cast.input().dt(), cast.dst_dt()), - }; - - Ok(dst) -} - -pub(crate) fn concat( - inputs: &[(Shape, Vec)], - dim: usize, - dst_shape: &Shape, - dst: &mut [T], -) -> Result<(), OperationError> { - let dst_dim_len = dst_shape[dim]; - let block: usize = dst_shape.iter().skip(1 + dim).product(); - let dst_s = block * dst_dim_len; - let src_o = 0; - let mut dst_o = 0; - for (src_s, src) in inputs { - let a_dim: usize = src_s.iter().take(dim).product(); - let b_dim = block * src_s[dim]; - for idx in 0..a_dim { - let dst_idx = idx * dst_s + dst_o; - let src_idx = idx * b_dim + src_o; - let dst_t = &mut dst[dst_idx..dst_idx + b_dim]; - let src = &src[src_idx..src_idx + b_dim]; - dst_t.copy_from_slice(src) - } - dst_o += b_dim; - } - Ok(()) -} -pub(crate) fn apply_concat( - inputs: RVec, - dim: usize, - dst: Tensor, -) -> Result { - let dst_size = dst.shape().numel(); - let mut result = vec![T::zero(); dst_size]; - - let inputs = inputs - .iter() - .map(|t| match t.to_vec::() { - Ok(v) => Ok((t.shape().clone(), v)), - Err(e) => Err(e.into()), - }) - .collect::, OperationError>>(); - - concat(&inputs?, dim, dst.shape(), &mut result)?; - cpu_store_result(&dst, &result); - Ok(dst) -} - -pub fn cpu_concat(Concat { inputs, dim }: Concat, dst: Tensor) -> Result { - match dst.dt() { - DType::F32 => apply_concat::(inputs, dim, dst), - DType::F16 => apply_concat::(inputs, dim, dst), - DType::BF16 => apply_concat::(inputs, dim, dst), - dtype => Err(InvariantError::UnsupportedDType(dtype).into()), - } -} diff --git a/crates/ratchet-core/src/cpu/reindex.rs b/crates/ratchet-core/src/cpu/reindex.rs deleted file mode 100644 index 3f33425e..00000000 --- a/crates/ratchet-core/src/cpu/reindex.rs +++ /dev/null @@ -1,250 +0,0 @@ -use super::utils::cpu_store_result; -use crate::{ - Broadcast, CPUOperation, DType, OperationError, Permute, Reindex, Shape, Slice, Strides, - Tensor, TensorDType, -}; -use half::{bf16, f16}; - -impl CPUOperation for Reindex { - fn apply_cpu(&self, dst: Tensor) -> Result { - match self { - Reindex::Permute(p) => p.apply_cpu(dst), - Reindex::Slice(s) => s.apply_cpu(dst), - Reindex::Broadcast(b) => b.apply_cpu(dst), - } - } -} - -impl CPUOperation for Permute { - fn apply_cpu(&self, dst: Tensor) -> Result { - match dst.dt() { - DType::F32 => apply_permute::(self, dst), - DType::BF16 => apply_permute::(self, dst), - DType::F16 => apply_permute::(self, dst), - DType::I32 => apply_permute::(self, dst), - DType::U32 => apply_permute::(self, dst), - _ => todo!(), - } - } -} - -fn apply_permute(p: &Permute, dst: Tensor) -> Result { - let perm: [usize; 4] = p.promote().as_slice().try_into().unwrap(); - let Permute { src, dims: _ } = p; - let result = permute(&src.to_vec::()?, src.shape(), dst.shape(), perm); - cpu_store_result(&dst, &result); - Ok(dst) -} - -// TODO: Optimize. -// This generic implementation is almost a direct copy from the gpu impl, -// and can definitely be way more performant. -fn permute( - src: &[T], - src_shape: &Shape, - dst_shape: &Shape, - perm: [usize; 4], -) -> Vec { - let mut result = vec![T::zero(); src_shape.numel()]; - - // We now know that these will always be len 4, same as gpu impl. - let src_shape = &Shape::promote(src_shape.clone(), 4); - let dst_shape = &Shape::promote(dst_shape.clone(), 4); - - let src_strides = &Strides::from(src_shape); - let dst_strides = &Strides::from(dst_shape); - - let src_strides: [usize; 4] = src_strides.into(); - let dst_strides: [usize; 4] = dst_strides.into(); - - (0..result.len()).for_each(|i| { - let dst_index = offset_to_ndindex(i, dst_strides); - let mut src_index = [0; 4]; - src_index[perm[0]] = dst_index[0]; - src_index[perm[1]] = dst_index[1]; - src_index[perm[2]] = dst_index[2]; - src_index[perm[3]] = dst_index[3]; - let src_offset = nd_index_to_offset(src_index, src_strides); - result[i] = src[src_offset] - }); - result -} - -impl CPUOperation for Slice { - fn apply_cpu(&self, dst: Tensor) -> Result { - match dst.dt() { - DType::F32 => apply_slice::(self, dst), - DType::BF16 => apply_slice::(self, dst), - DType::F16 => apply_slice::(self, dst), - DType::I32 => apply_slice::(self, dst), - DType::U32 => apply_slice::(self, dst), - _ => todo!(), - } - } -} - -fn apply_slice(s: &Slice, dst: Tensor) -> Result { - let (start, stop): (Vec<_>, Vec<_>) = s.indices().iter().map(|r| (r.start, r.end)).unzip(); - let result = slice(&s.src.to_vec::()?, s.src.strides(), &start, &stop); - - cpu_store_result(&dst, &result); - Ok(dst) -} - -pub(crate) fn slice( - src: &[T], - src_strides: &Strides, - start: &[usize], - stop: &[usize], -) -> Vec { - assert!(start.len() == stop.len()); - assert!(start.len() == src_strides.rank()); - start.iter().zip(stop.iter()).for_each(|(s, t)| { - assert!(s < t); - }); - - let dst_shape: Vec = stop.iter().zip(start.iter()).map(|(s, t)| s - t).collect(); - let dst_numel: usize = dst_shape.iter().product(); - - let mut dst = vec![T::zero(); dst_numel]; - - let mut dst_dots = vec![]; - for d in 0..dst_shape.len() { - dst_dots.push(dst_shape[d + 1..].iter().product::().max(1)); - } - - (0..dst.len()).for_each(|i| { - let mut src_index = 0; - let mut tmp = i; - for d in 0..dst_shape.len() { - let coord = tmp / dst_dots[d]; - tmp %= dst_dots[d]; - src_index += (coord + start[d]) * src_strides[d] as usize; - } - dst[i] = src[src_index]; - }); - - dst -} - -impl CPUOperation for Broadcast { - fn apply_cpu(&self, dst: Tensor) -> Result { - match dst.dt() { - DType::F32 => apply_broadcast::(self, dst), - DType::BF16 => apply_broadcast::(self, dst), - DType::F16 => apply_broadcast::(self, dst), - DType::I32 => apply_broadcast::(self, dst), - DType::U32 => apply_broadcast::(self, dst), - _ => todo!(), - } - } -} - -fn apply_broadcast(b: &Broadcast, dst: Tensor) -> Result { - let result = broadcast(&b.src.to_vec::()?, b.src.shape(), b.to()); - cpu_store_result(&dst, &result); - Ok(dst) -} - -pub(crate) fn broadcast_vector(src: &[T], dst: &mut [T]) { - let chunk_size = dst.len() / src.len(); - - (0..dst.len()) - .step_by(chunk_size) - .enumerate() - .for_each(|(i, chunk)| { - dst[chunk..chunk + chunk_size].fill(src[i]); - }); -} - -pub(crate) fn broadcast(src: &[T], src_shape: &Shape, dst_shape: &Shape) -> Vec { - let mut result = vec![T::zero(); dst_shape.numel()]; - - if src_shape.is_scalar() { - // Life is simple - result.fill(src[0]); - } else if src_shape.is_vector() { - // If from is a vector and the first dimension is the broadcasting dimension - if src_shape[0] > 1 && src_shape[0] == dst_shape[0] { - broadcast_vector(src, &mut result) - } else { - generic_broadcast(src, &mut result, src_shape, dst_shape) - } - } else { - generic_broadcast(src, &mut result, src_shape, dst_shape) - } - - result -} - -// TODO: Optimize. -// This generic implementation is almost a direct copy from the gpu impl, -// and can definitely be way more performant. -fn generic_broadcast( - src: &[T], - result: &mut [T], - src_shape: &Shape, - dst_shape: &Shape, -) { - // We now know that these will always be len 4, same as gpu impl. - let src_shape = &Shape::promote(src_shape.clone(), 4); - let dst_shape = &Shape::promote(dst_shape.clone(), 4); - - let src_strides = &Strides::from(src_shape); - let dst_strides = &Strides::from(dst_shape); - - let src_shape: [usize; 4] = src_shape.try_into().unwrap(); - let src_strides: [usize; 4] = src_strides.into(); - let dst_strides: [usize; 4] = dst_strides.into(); - - fn select(a: [usize; 4], b: [usize; 4], t: [bool; 4]) -> [usize; 4] { - let mut result = [0; 4]; - result[0] = if t[0] { a[0] } else { b[0] }; - result[1] = if t[1] { a[1] } else { b[1] }; - result[2] = if t[2] { a[2] } else { b[2] }; - result[3] = if t[3] { a[3] } else { b[3] }; - result - } - - let shape_onedim_lookup: [bool; 4] = [ - src_shape[0] != 1, - src_shape[1] != 1, - src_shape[2] != 1, - src_shape[3] != 1, - ]; - (0..result.len()).for_each(|i| { - let dst_index = offset_to_ndindex(i, dst_strides); - let src_index = select(dst_index, [0; 4], shape_onedim_lookup); - let src_offset = nd_index_to_offset(src_index, src_strides); - result[i] = src[src_offset] - }); -} - -#[inline] -fn offset_to_ndindex(offset: usize, strides: [usize; 4]) -> [usize; 4] { - let mut indices = [0; 4]; - let mut remaining = offset; - - let idx = remaining / strides[0]; - indices[0] = idx; - remaining -= idx * strides[0]; - - let idx = remaining / strides[1]; - indices[1] = idx; - remaining -= idx * strides[1]; - - let idx = remaining / strides[2]; - indices[2] = idx; - remaining -= idx * strides[2]; - - indices[3] = remaining; - indices -} - -#[inline] -fn nd_index_to_offset(ndindex: [usize; 4], strides: [usize; 4]) -> usize { - ndindex[0] * strides[0] - + ndindex[1] * strides[1] - + ndindex[2] * strides[2] - + ndindex[3] * strides[3] -} diff --git a/crates/ratchet-core/src/cpu/softmax.rs b/crates/ratchet-core/src/cpu/softmax.rs deleted file mode 100644 index 1d6a3df0..00000000 --- a/crates/ratchet-core/src/cpu/softmax.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::cpu::utils::cpu_store_result; -use crate::{CPUOperation, DType, OperationError, Softmax, Tensor, TensorDType}; -use half::{bf16, f16}; -use num::Float; -use num_traits::NumAssignOps; - -impl CPUOperation for Softmax { - fn apply_cpu(&self, dst: Tensor) -> Result { - let Softmax { input, dim } = self; - match input.dt() { - DType::F32 => softmax::(input, *dim, &dst)?, - DType::F16 => softmax::(input, *dim, &dst)?, - DType::BF16 => softmax::(input, *dim, &dst)?, - _ => todo!(), - } - - Ok(dst) - } -} - -fn softmax(input: &Tensor, dim: usize, dst: &Tensor) -> Result<(), OperationError> -where - T: TensorDType + Float + NumAssignOps, -{ - let src_shape = input.shape(); - let mut input = input.to_vec::()?; - let N = src_shape[dim]; - input.chunks_mut(N).for_each(|chunk| { - let mut sum = T::zero(); - for j in 0..N { - chunk[j] = chunk[j].exp(); - sum += chunk[j]; - } - for j in 0..N { - chunk[j] /= sum; - } - }); - - cpu_store_result(dst, &input); - - Ok(()) -} diff --git a/crates/ratchet-core/src/gpu/buffer_allocator/lazy_graph_executor.rs b/crates/ratchet-core/src/gpu/buffer_allocator/lazy_graph_executor.rs deleted file mode 100644 index b5660a13..00000000 --- a/crates/ratchet-core/src/gpu/buffer_allocator/lazy_graph_executor.rs +++ /dev/null @@ -1,646 +0,0 @@ -use crate::{ - CpuUniform, Executable, ExecutionError, GPUBuffer, HashMap, HashSet, Hasher as HasherType, - Inner, LazyOp, Storage, TensorError, WgpuDevice, -}; -#[cfg(feature = "debug")] -use crate::{DebugTensor, Device, DeviceStorage}; -use crate::{Tensor, TensorId}; -use parking_lot::RwLock; -use std::collections::BTreeMap; -use std::hash::{BuildHasherDefault, Hasher}; -use std::sync::{Arc, Weak}; - -enum EmitStatus { - Emitting, - Emitted, -} - -type EmissionMap = HashMap; -type PostOrderData<'a> = Vec<&'a Tensor>; - -struct CachedExecutable { - executable: Arc, - shared_realloc: bool, -} - -pub struct LazyGraphExecutor { - tensors: Arc>>>, - cache: HashMap, - pass_index: u64, - inplace_support: bool, -} - -fn panic_cycle(id: TensorId) { - panic!( - "Cycle detected whilst computing topological order: {:?}. Try plotting with feature `plotting`.", - id - ); -} - -#[cfg(feature = "debug")] -macro_rules! mut_in_debug { - ($ident:ident) => { mut $ident }; -} - -#[cfg(not(feature = "debug"))] -macro_rules! mut_in_debug { - ($ident:ident) => { - $ident - }; -} - -fn compute_post_order(tensor: &Tensor) -> Vec<&Tensor> { - let mut post_order = Vec::new(); - let mut node_stack = vec![tensor]; - let mut emission_map = EmissionMap::default(); - while let Some(node) = node_stack.last().cloned() { - match emission_map.get(&node.id()) { - None => { - emission_map.insert(node.id(), EmitStatus::Emitting); - for src in node.op().srcs() { - if let Some(EmitStatus::Emitting) = emission_map.get(&src.id()) { - panic_cycle(src.id()); - } - - node_stack.push(src); - } - } - Some(EmitStatus::Emitting) => { - for src in node.op().srcs() { - if let Some(EmitStatus::Emitting) = emission_map.get(&src.id()) { - panic_cycle(src.id()); - } - } - emission_map.insert(node.id(), EmitStatus::Emitted); - post_order.push(node); - node_stack.pop(); - } - Some(EmitStatus::Emitted) => { - node_stack.pop(); - } - } - } - post_order -} - -fn compute_post_order_from_nodes(roots: Vec<&Tensor>) -> PostOrderData { - let mut post_order = Vec::new(); - for root in roots { - post_order.extend(compute_post_order(root)); - } - post_order -} - -impl LazyGraphExecutor { - pub fn new(inplace_support: bool) -> Self { - Self { - tensors: Arc::new(RwLock::new(BTreeMap::default())), - cache: HashMap::default(), - pass_index: Default::default(), - inplace_support, - } - } - - pub fn register_tensor(&self, tensor: &Tensor) { - log::trace!("Registering tensor {:?}", tensor.id()); - self.tensors - .write() - .insert(tensor.id(), Arc::downgrade(&tensor.inner)); - } - - /// Unregisters a tensor by its `TensorId`. - pub fn unregister_tensor(&self, id: TensorId) { - log::trace!("Unregistering tensor {:?}", id); - self.tensors.write().remove(&id); - } - - fn get_live_tensors(&self) -> BTreeMap { - self.tensors - .read() - .iter() - // Attempt to upgrade from Weak → Arc. - // If it succeeds, wrap Arc in Tensor. - .filter_map(|(id, weak_inner)| { - weak_inner - .upgrade() - .map(|arc_inner| (*id, Tensor { inner: arc_inner })) - .filter(|(_, t)| !t.resolved()) - }) - .collect() - } - - fn run_post_order<'a>(&self, tensors: Vec<&'a Tensor>) -> PostOrderData<'a> { - compute_post_order_from_nodes(tensors) - } - - pub fn sync_live_tensors_graph( - &mut self, - gpu_device: &WgpuDevice, - ) -> anyhow::Result<(), TensorError> { - log::trace!("Syncing live tensors graph"); - let tensors = self.get_live_tensors(); - log::debug!("All registered IDs: {:?}", self.tensors.read().keys()); - let owned_tensors = tensors.keys().cloned().collect(); - self.sync_tensors_graph_impl(tensors, Some(owned_tensors), gpu_device, true) - } - - pub fn sync_tensors_graph( - &mut self, - tensors: Vec<&Tensor>, - gpu_device: &WgpuDevice, - ) -> anyhow::Result<(), TensorError> { - self.sync_tensors_graph_impl( - tensors.into_iter().map(|t| (t.id(), t.clone())).collect(), - None, - gpu_device, - false, - ) - } - - fn run_executable( - &mut self, - executable: &Executable, - gpu_device: &WgpuDevice, - immediate: bool, - ) -> anyhow::Result<(), ExecutionError> { - log::debug!("Running executable"); - #[cfg(feature = "debug")] - let index = executable.dispatch_debugging(gpu_device)?; - - #[cfg(not(feature = "debug"))] - let index = executable.dispatch(gpu_device)?; - - if immediate { - gpu_device.poll(wgpu::MaintainBase::WaitForSubmissionIndex(index)); - } - Ok(()) - } - - fn sync_tensors_graph_impl( - &mut self, - tensors: BTreeMap, - owned_tensors: Option>, - gpu_device: &WgpuDevice, - use_cache: bool, - ) -> Result<(), TensorError> { - // First check if the tensors are already resolved - log::debug!("Syncing tensors graph"); - if tensors.values().all(|t| t.resolved()) { - return Ok(()); - } - - // Notably, we compute post order first because we want to hash the tensors in post order, - // since each hash depends on the hashes of its sources. It's not clear to me that this - // violates some important unspoken assumption on the part of the LazyTensor authors. - // We also flip the hash order—post order first, then insertion order—because it's more - // convenient to treat it as one big hash pass. - // let tensors = tensors.clone(); - let post_order = self.run_post_order(tensors.values().collect()); - - let mut indices = Vec::with_capacity(tensors.len()); - let mut tensor_ids = HashSet::with_capacity_and_hasher( - tensors.len(), - BuildHasherDefault::::default(), - ); - - let mut hasher = HasherType::default(); - let mut tensor_hashes = BTreeMap::default(); - - let mut consumed_tensors = HashSet::with_capacity_and_hasher( - tensors.len(), - BuildHasherDefault::::default(), - ); - - let mut uniform = CpuUniform::new(); - let mut compile_keys = HashMap::default(); - #[cfg(feature = "plotting")] - let mut strong_counts_inplace = HashMap::default(); - - // Keep track of the real source of each tensor; important to help resolve handle those - // annoying views correctly. - let mut tensor_sources = HashMap::default(); - - // First we loop over the post order to hash the tensors in the right order - for tensor in &post_order { - // Scope to drop tensor_hashes before inserting - let srcs = tensor.op().srcs(); - log::trace!( - "{:?}: Srcs: {:?}", - tensor.id(), - srcs.iter().map(|s| s.id()).collect::>() - ); - let first_src = srcs.first().cloned(); - - let mut to_modify = None; - if !matches!(tensor.op(), LazyOp::View(_)) { - tensor_sources.insert(tensor.id(), tensor); - to_modify = first_src.map(|src| { - tensor_sources - .get(&src.id()) - .cloned() - .expect("Source missing entry in tensor_sources") - }); - } else if let Some(src) = tensor_sources - .get(&first_src.expect("All views should have one src").id()) - .cloned() - { - tensor_sources.insert(tensor.id(), src); - to_modify = Some(src); - } - - let can_inplace = match to_modify { - Some(to_modify_src) => { - log::trace!( - "{:?}: Supports inplace: {:?}, is variable: {:?}", - tensor.id(), - tensor.op().supports_inplace(), - to_modify_src.is_variable() - ); - - if !self.inplace_support { - match tensor.op() { - LazyOp::Softmax(_) | LazyOp::ScatterAdd(_) | LazyOp::IndexAdd(_) => { - true - } - LazyOp::Detach(d) => { - matches!( - d.as_ref(), - LazyOp::Softmax(_) - | LazyOp::ScatterAdd(_) - | LazyOp::IndexAdd(_) - ) - } - _ => false, - } - } else if !tensor.op().supports_inplace() - // vinhowe: we need to check if the src is a variable, because we can't - // inplace variables unless we've disabled gradient tracking. - || to_modify_src.is_variable() - { - false - } else { - // Typical references: - // 1. Its original consumer. Whatever scope it was created in. - // 2. `tensors`, as passed into this method, if it wasn't resolved and we - // upgraded its weak reference. This happens when we do a sync of live - // tensors, say, in an optimizer step, but a one-off sync won't do this. - // This is why we have the optional `owned_tensors`. - // If these two are the only references, then we can inplace. Usually, - // additional references include, not in any particular order: - // 3. The optimizer, if it is a variable. We'll also check if the src is a - // variable. - // 4+ Any other Tensor consumers in the post-order. If it's not a variable, - // these are the references we're concerned about messing with. - // - // If we own a copy, 2, otherwise 1. - let expected_strong = owned_tensors - .as_ref() - .and_then(|ot| ot.contains(&to_modify_src.id()).then_some(2)) - .unwrap_or(1); - - to_modify_src.strong_count() == expected_strong - } - } - None => false, - }; - - #[cfg(feature = "plotting")] - strong_counts_inplace.insert(tensor.id(), (tensor.strong_count(), can_inplace)); - log::trace!( - "Can inplace: {:?}, op: {:?} ({:?}), strong: {:?}", - can_inplace, - tensor.op().name(), - tensor.id(), - to_modify.as_ref().map(|t| t.strong_count()) - ); - let compile_key = tensor.gpu_compile_key(can_inplace, &mut uniform); - let ir = tensor.op().ir(); - ir.shape_hash(&mut hasher, &tensor_hashes, &compile_key); - if let Some(compile_key) = compile_key { - compile_keys.insert(tensor.id(), compile_key); - } - let hash = hasher.finish(); - tensor_hashes.insert(tensor.id(), hash); - log::debug!("IR: {:?}", ir); - log::debug!("Tensor hash: {:#x} (op: {:?})", hash, tensor.op().name()); - for src in tensor.op().srcs() { - consumed_tensors.insert(src.id()); - } - } - - log::debug!("Post-order hash: {:?}", hasher.finish()); - - let output_tensors = tensors - .iter() - .filter(|(id, _)| !consumed_tensors.contains(id)) - .map(|(id, tensor)| (*id, tensor)) - .collect::>(); - - #[cfg(feature = "plotting")] - crate::plot::render_to_file( - &post_order, - &output_tensors, - &strong_counts_inplace, - None, - construct_plot_filename("post_order", self.pass_index, self.inplace_support), - ) - .unwrap(); - - for (i, (id, tensor)) in tensors.iter().enumerate() { - if !tensor_ids.insert(id) || tensor.resolved() { - continue; - } - - #[cfg(feature = "debug")] - if !tensor_hashes.contains_key(id) { - log::warn!("Missing shape hash for tensor {:?}", id); - continue; - } - hasher.write_u64( - *tensor_hashes - .get(id) - .expect("Missing shape hash for tensor"), - ); - indices.push(i); - } - let hash = hasher.finish(); - - log::debug!("Shape hash: {:?}", hash); - - #[cfg(feature = "debug")] - let mut cpu_bufs = HashMap::default(); - - #[cfg(feature = "debug")] - // Get CPU buffers from existing allocations - for tensor in &post_order { - let storage_guard = tensor.storage(); - match storage_guard.as_ref() { - Some(Storage::GPU(gpu_buf)) => { - log::trace!("Getting CPU buffer for {:?}", tensor.id()); - cpu_bufs.insert( - tensor.id(), - gpu_buf.to_cpu(&Device::GPU(gpu_device.clone()))?, - ); - } - Some(Storage::CPU(cpu_buf)) => { - log::trace!("Using existing CPU buffer for {:?}", tensor.id()); - cpu_bufs.insert(tensor.id(), cpu_buf.clone()); - } - None => {} - } - } - - let (mut cached_exec, do_shared_realloc) = if use_cache { - self.cache - .remove(&hash) - .map(|cached_exec| { - if cached_exec.shared_realloc { - (Arc::try_unwrap(cached_exec.executable).ok(), false) - } else { - (None, true) - } - }) - .unwrap_or((None, false)) - } else { - (None, false) - }; - - let mut compiled_ops = Vec::with_capacity(post_order.len()); - - gpu_device.begin_pass(self.pass_index); - - let mut allocations = if cached_exec.is_none() || do_shared_realloc { - Some(gpu_device.allocate_cfg( - &post_order, - &output_tensors, - &compile_keys, - do_shared_realloc, - gpu_device, - )?) - } else { - None - }; - - #[cfg(debug_assertions)] - log::debug!( - "Resolved tensors in post order: {:?}", - post_order - .iter() - .filter(|t| t.resolved()) - .map(|t| t.id()) - .collect::>() - ); - - #[cfg(feature = "debug")] - let mut compute_dsts = Vec::new(); - - #[cfg(feature = "plotting")] - crate::plot::render_to_file( - &post_order, - &output_tensors, - &strong_counts_inplace, - None, - construct_plot_filename("prealloc", self.pass_index, self.inplace_support), - ) - .unwrap(); - - for t in &post_order { - if t.op().is_const() || t.resolved() { - continue; - } - - if let Some(allocations) = &mut allocations { - let id = t.id(); - let inner = allocations.remove(&id).ok_or(TensorError::NoStorage(id))?; - t.update_storage(Storage::GPU(GPUBuffer { - inner, - alignment: t.dt().size_of(), - cpu_size: Some(t.num_bytes()), - })); - } - - if let Some(compile_key) = compile_keys.get(&t.id()) { - if cached_exec.is_some() { - // TODO: Update debug things if needed here, otherwise, delete this branch - } else if let Some(compiled_op) = - t.compile_gpu(compile_key, gpu_device, cfg!(feature = "debug")) - { - compiled_ops.push(Some(compiled_op)); - } else { - log::warn!("Compilation failed for operation: {:?}", t.op().name()); - compiled_ops.push(None); - } - - #[cfg(feature = "debug")] - compute_dsts.push((*t).clone()); - } - } - - // At this point we have a cached executable that we want to ignore. - cached_exec = cached_exec.map(|exec| exec.with_tensors(&post_order).unwrap()); - - #[cfg(feature = "debug")] - let debug_list = compute_dsts - .into_iter() - .map(|t| { - DebugTensor::new( - t.storage().clone(), - t.dt(), - t.op() - .srcs() - .iter() - .map(|s| { - DebugTensor::new(s.storage().clone(), s.dt(), vec![], s.num_bytes()) - }) - .collect(), - t.num_bytes(), - ) - }) - .collect::>(); - - if use_cache { - if let Some(mut_in_debug!(cached_exec)) = cached_exec { - log::debug!("Using cached executable"); - - #[cfg(feature = "debug")] - let mut cpu_bufs = HashMap::default(); - - #[cfg(feature = "debug")] - // Get CPU buffers from existing allocations - for tensor in &post_order { - let storage_guard = tensor.storage(); - match storage_guard.as_ref() { - Some(Storage::GPU(gpu_buf)) => { - log::trace!("Getting CPU buffer for {:?}", tensor.id()); - cpu_bufs.insert( - tensor.id(), - gpu_buf.to_cpu(&Device::GPU(gpu_device.clone()))?, - ); - } - Some(Storage::CPU(cpu_buf)) => { - log::trace!("Using existing CPU buffer for {:?}", tensor.id()); - cpu_bufs.insert(tensor.id(), cpu_buf.clone()); - } - None => {} - } - } - - #[cfg(feature = "debug")] - { - cached_exec.debug_list = Some(debug_list); - cached_exec.cpu_bufs = Some(Arc::new(RwLock::new(cpu_bufs))); - } - - self.run_executable(&cached_exec, gpu_device, false) - .unwrap(); - - #[cfg(all(feature = "debug", feature = "plotting"))] - { - let cpu_bufs_guard = cached_exec.cpu_bufs.as_ref().map(|arc| arc.read()); - - crate::plot::render_to_file( - &post_order, - &output_tensors, - &strong_counts_inplace, - cpu_bufs_guard.as_deref(), - construct_plot_filename("post_exec", self.pass_index, self.inplace_support), - ) - .unwrap(); - } - - self.cache.insert( - hash, - CachedExecutable { - executable: Arc::new(cached_exec), - shared_realloc: true, - }, - ); - self.pass_index += 1; - return Ok(()); - } - - // On a cache miss: Clear cache because currently I don't know how to make sure - // allocations are compatible between runs. - self.cache.clear(); - } - - #[cfg(feature = "plotting")] - crate::plot::render_to_file( - &post_order, - &output_tensors, - &strong_counts_inplace, - None, - construct_plot_filename("alloc", self.pass_index, self.inplace_support), - ) - .unwrap(); - - // Only keep the ops that successfully compiled. - let filtered_compiled_ops: Vec<_> = compiled_ops.into_iter().flatten().collect(); - - let mut executable = Executable::new( - None, - filtered_compiled_ops, - uniform.into_gpu(gpu_device)?, - #[cfg(feature = "debug")] - Some(debug_list), - #[cfg(feature = "debug")] - Some(Arc::new(RwLock::new(cpu_bufs))), - ); - - self.run_executable(&executable, gpu_device, false).unwrap(); - - #[cfg(all(feature = "debug", feature = "plotting"))] - { - let cpu_bufs_guard = executable.cpu_bufs.as_ref().map(|arc| arc.read()); - - crate::plot::render_to_file( - &post_order, - &output_tensors, - &strong_counts_inplace, - cpu_bufs_guard.as_deref(), - construct_plot_filename("post_exec", self.pass_index, self.inplace_support), - ) - .unwrap(); - } - - executable.set_storage(post_order.iter().map(|t| t.storage().clone()).collect()); - - if use_cache { - // After creating/running the executable, we cache it - self.cache.insert( - hash, - CachedExecutable { - executable: Arc::new(executable), - shared_realloc: do_shared_realloc, - }, - ); - } - - self.pass_index += 1; - Ok(()) - } -} - -impl Default for LazyGraphExecutor { - fn default() -> Self { - Self::new(false) - } -} - -/// Constructs the plot filename with an optional "_inplace" segment. -/// -/// The resulting filename is in the format: -/// "[_inplace]_.svg" -/// -/// # Arguments -/// * `name` - The base part of the file name (e.g., "post_order"). -/// * `pass_index` - The pass index used in the file name. -/// * `inplace_support` - Flag indicating whether to add "_inplace" before the pass number. -#[cfg(feature = "plotting")] -fn construct_plot_filename(name: &str, pass_index: u64, inplace_support: bool) -> String { - if inplace_support { - format!("{}_inplace_{}", name, pass_index) - } else { - format!("{}_{}", name, pass_index) - } -} diff --git a/crates/ratchet-core/src/ops/binary.rs b/crates/ratchet-core/src/ops/binary.rs deleted file mode 100644 index c47073c1..00000000 --- a/crates/ratchet-core/src/ops/binary.rs +++ /dev/null @@ -1,334 +0,0 @@ -use derive_new::new; -use encase::ShaderType; -use half::f16; -use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; - -use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, InvariantError, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, Shape, - StorageView, Strides, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, - Workload, -}; -#[cfg(test)] -use test_strategy::Arbitrary; - -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Debug, Clone, Hash, IrFields)] -pub enum BinaryOp { - Add, - Sub, - Mul, - Div, -} - -impl BinaryOp { - pub fn kernel_name(&self) -> &'static str { - match self { - BinaryOp::Add => "add", - BinaryOp::Sub => "sub", - BinaryOp::Mul => "mul", - BinaryOp::Div => "div", - } - } - - pub fn kernel_operator(&self) -> &'static str { - match self { - BinaryOp::Add => "+", - BinaryOp::Sub => "-", - BinaryOp::Mul => "*", - BinaryOp::Div => "/", - } - } -} - -#[derive(new, Debug, Clone, IrFields)] -pub struct Binary { - pub lhs: Tensor, - pub rhs: Tensor, - pub op: BinaryOp, -} - -impl KernelRenderable for BinaryKernels { - fn register_bindings( - &self, - builder: &mut WgslKernelBuilder, - inplace: bool, - ) -> Result<(), OperationError> { - if inplace { - builder.register_storage("A", BindingMode::ReadWrite, Array::

::default()); - builder.register_storage("B", BindingMode::ReadOnly, Array::

::default()); - } else { - builder.register_storage("A", BindingMode::ReadOnly, Array::

::default()); - builder.register_storage("B", BindingMode::ReadOnly, Array::

::default()); - builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); - } - builder.register_uniform(); - Ok(()) - } - - fn render( - &self, - inplace: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let device = dst.device().try_gpu()?; - let mut kernel_builder = WgslKernelBuilder::new( - workgroup_size.clone(), - rvec![ - BuiltIn::WorkgroupId, - BuiltIn::LocalInvocationIndex, - BuiltIn::NumWorkgroups - ], - device.compute_features().clone(), - ); - - self.register_bindings::

(&mut kernel_builder, inplace)?; - kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - - let N = (P::W as u32).render(); - - kernel_builder.write_main(wgsl! { - let x_offset = workgroup_id.x * 64u; - let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; - if (index >= metadata.numel / 'N) { - return; - } - }); - - let BinaryKernels::Standard(inner) = self; - let op = inner.op.kernel_operator(); - let apply = if inplace { - wgsl! { - let val = A[index]; - A[index] = val 'op B[index]; - } - } else { - wgsl! { Y[index] = A[index] 'op B[index]; } - }; - kernel_builder.write_main(apply); - Ok(kernel_builder.build()?) - } -} - -impl Binary { - pub fn op(&self) -> &BinaryOp { - &self.op - } - - pub fn lhs(&self) -> &Tensor { - &self.lhs - } - - pub fn rhs(&self) -> &Tensor { - &self.rhs - } -} - -#[derive(Debug, ShaderType, WgslMetadata)] -pub struct BinaryMeta { - numel: u32, -} - -impl OpGuards for Binary { - fn check_shapes(&self) { - let shapes = [self.lhs.shape(), self.rhs.shape()]; - let broadcasted = Shape::multi_broadcast(&shapes); - assert!(broadcasted.is_some()); - } - - fn check_dtypes(&self) { - assert_eq!(self.lhs.dt(), self.rhs.dt()); - } -} - -impl Operation for Binary { - fn name(&self) -> &'static str { - match self.op { - BinaryOp::Add => "Add", - BinaryOp::Sub => "Sub", - BinaryOp::Mul => "Mul", - BinaryOp::Div => "Div", - } - } - - fn compute_view(&self) -> Result { - let lhs = &self.lhs; - let rhs = &self.rhs; - let shapes = &[lhs.shape(), rhs.shape()]; - if lhs.is_scalar() || rhs.is_scalar() { - let other = if lhs.is_scalar() { rhs } else { lhs }; - return Ok(other.storage_view().clone()); - } - let broadcasted = Shape::multi_broadcast(shapes); - if broadcasted.is_none() { - let failed = shapes.iter().map(|s| (*s).clone()).collect::>(); - return Err(InvariantError::BroadcastingFailed(failed).into()); - } - let broadcasted = broadcasted.unwrap(); - let ostrides = Strides::from(&broadcasted); - Ok(StorageView::new(broadcasted, lhs.dt(), ostrides)) - } - - #[inline] - fn srcs(&self) -> RVec<&Tensor> { - rvec![&self.lhs, &self.rhs] - } - - fn supports_inplace(&self) -> bool { - true - } -} - -impl GPUOperation for Binary { - type KernelEnum = BinaryKernels; - - fn select_kernel(&self) -> Self::KernelEnum { - BinaryKernels::Standard(self.clone()) - } -} - -pub enum BinaryKernels { - Standard(Binary), -} - -impl Kernel for BinaryKernels { - type Metadata = BinaryMeta; - - fn storage_bind_group_layout( - &self, - inplace: bool, - ) -> Result { - if inplace { - Ok(BindGroupLayoutDescriptor::binary_inplace()) - } else { - Ok(BindGroupLayoutDescriptor::binary()) - } - } - - fn kernel_name(&self) -> String { - match self { - BinaryKernels::Standard(k) => k.op.kernel_name().to_string(), - } - } - - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { - let numel = dst.shape().numel() as _; - let meta = BinaryMeta { numel }; - - Ok(meta) - } - - fn kernel_element(&self, dst: &Tensor) -> KernelElement { - let numel = dst.shape().numel(); - - if numel % 4 == 0 { - KernelElement::Vec4 - } else if numel % 2 == 0 { - KernelElement::Vec2 - } else { - KernelElement::Scalar - } - } - - fn calculate_dispatch(&self, dst: &Tensor) -> Result { - Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) - } - - fn build_kernel( - &self, - inplace: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let BinaryKernels::Standard(inner) = self; - let kernel_element = self.kernel_element(dst); - match (inner.lhs.dt(), &kernel_element) { - (DType::F32, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - _ => Err(OperationError::CompileError(format!( - "Unsupported dtype {:?} or kernel element {:?}", - inner.lhs.dt(), - kernel_element - ))), - } - } -} - -#[cfg(all(test, feature = "pyo3"))] -mod tests { - use crate::{test_util::run_py_prg, BinaryOp, Device, DeviceRequest, Shape, Tensor}; - use test_strategy::{proptest, Arbitrary}; - - #[derive(Arbitrary, Debug)] - struct BinaryProblem { - op: BinaryOp, - #[any(vec![1..=4, 1..=4, 1..=1, 1..=256])] - shape: Shape, - } - - fn ground_truth(a: &Tensor, b: &Tensor, op: &BinaryOp) -> anyhow::Result { - let kn = op.kernel_name(); - let prg = format!( - r#" -import torch -def {}(a, b): - return torch.{}(torch.from_numpy(a), torch.from_numpy(b)).numpy() -"#, - kn, kn - ); - run_py_prg(prg.to_string(), &[a, b], &[], a.dt()) - } - - fn run_binary_trial(prob: BinaryProblem, device: Device) -> anyhow::Result<()> { - let cpu_device = Device::request_device(DeviceRequest::CPU)?; - let BinaryProblem { op, shape } = prob; - let a = Tensor::randn::(0., 1., shape.clone(), cpu_device.clone()); - let b = Tensor::randn::(0., 1., shape, cpu_device.clone()); - let ground = ground_truth(&a, &b, &op)?; - - let a = a.to(&device)?; - let b = b.to(&device)?; - - let c = match op { - BinaryOp::Add => a.add(b)?, - BinaryOp::Sub => a.sub(b)?, - BinaryOp::Mul => a.mul(b)?, - BinaryOp::Div => a.div(b)?, - }; - - let d = c.to(&Device::CPU)?; - ground.all_close(&d, 1e-4, 1e-4)?; - Ok(()) - } - - #[proptest(cases = 8)] - fn test_binary_gpu(prob: BinaryProblem) { - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - run_binary_trial(prob, device).unwrap(); - } - - #[proptest(cases = 8)] - fn test_binary_cpu(prob: BinaryProblem) { - let device = Device::request_device(DeviceRequest::CPU).unwrap(); - run_binary_trial(prob, device).unwrap(); - } -} diff --git a/crates/ratchet-core/src/ops/cmp.rs b/crates/ratchet-core/src/ops/cmp.rs deleted file mode 100644 index f0369d09..00000000 --- a/crates/ratchet-core/src/ops/cmp.rs +++ /dev/null @@ -1,334 +0,0 @@ -use derive_new::new; -use encase::ShaderType; -use half::f16; -use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; - -use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, InvariantError, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, Shape, - StorageView, Strides, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, - Workload, -}; -#[cfg(test)] -use test_strategy::Arbitrary; - -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Debug, Clone, Hash, IrFields)] -pub enum CmpOp { - Eq, - Ne, - Le, - Ge, - Lt, - Gt, -} - -impl CmpOp { - pub fn kernel_name(&self) -> &'static str { - match self { - CmpOp::Eq => "eq", - CmpOp::Ne => "ne", - CmpOp::Le => "le", - CmpOp::Ge => "ge", - CmpOp::Lt => "lt", - CmpOp::Gt => "gt", - } - } - - pub fn op_str(&self) -> &'static str { - match self { - CmpOp::Eq => "==", - CmpOp::Ne => "!=", - CmpOp::Le => "<=", - CmpOp::Ge => ">=", - CmpOp::Lt => "<", - CmpOp::Gt => ">", - } - } -} - -#[derive(new, Debug, Clone, IrFields)] -pub struct Cmp { - pub lhs: Tensor, - pub rhs: Tensor, - pub op: CmpOp, -} - -impl KernelRenderable for CmpKernels { - fn register_bindings( - &self, - builder: &mut WgslKernelBuilder, - _: bool, - ) -> Result<(), OperationError> { - builder.register_storage("A", BindingMode::ReadOnly, Array::

::default()); - builder.register_storage("B", BindingMode::ReadOnly, Array::

::default()); - let CmpKernels::Standard(inner) = self; - match self.kernel_element(&inner.lhs) { - KernelElement::Scalar => { - builder.register_storage( - "Y", - BindingMode::ReadWrite, - Array::>::default(), - ); - } - KernelElement::Vec2 => { - builder.register_storage( - "Y", - BindingMode::ReadWrite, - Array::>::default(), - ); - } - KernelElement::Vec4 => { - builder.register_storage( - "Y", - BindingMode::ReadWrite, - Array::>::default(), - ); - } - } - - builder.register_uniform(); - Ok(()) - } - - fn render( - &self, - inplace: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let device = dst.device().try_gpu()?; - let mut kernel_builder = WgslKernelBuilder::new( - workgroup_size.clone(), - rvec![ - BuiltIn::WorkgroupId, - BuiltIn::LocalInvocationIndex, - BuiltIn::NumWorkgroups - ], - device.compute_features().clone(), - ); - - self.register_bindings::

(&mut kernel_builder, inplace)?; - kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - - let CmpKernels::Standard(inner) = self; - - let N = (P::W as u32).render(); - let dt = match self.kernel_element(dst) { - KernelElement::Scalar => "i32", - KernelElement::Vec2 => "vec2", - KernelElement::Vec4 => "vec4", - }; - let op = inner.op.op_str(); - - kernel_builder.write_main(wgsl! { - let x_offset = workgroup_id.x * 64u; - let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; - if (index >= metadata.numel / 'N) { - return; - } - - Y[index] = 'dt(A[index] 'op B[index]); - }); - - Ok(kernel_builder.build()?) - } -} - -impl Cmp { - pub fn op(&self) -> &CmpOp { - &self.op - } -} - -#[derive(Debug, ShaderType, WgslMetadata)] -pub struct CmpMeta { - numel: u32, -} - -impl OpGuards for Cmp { - fn check_shapes(&self) { - let shapes = [self.lhs.shape(), self.rhs.shape()]; - let broadcasted = Shape::multi_broadcast(&shapes); - assert!(broadcasted.is_some()); - } - - fn check_dtypes(&self) { - assert_eq!(self.lhs.dt(), self.rhs.dt()); - } -} - -impl Operation for Cmp { - fn name(&self) -> &'static str { - "Cmp" - } - - fn compute_view(&self) -> Result { - let lhs = &self.lhs; - let rhs = &self.rhs; - let shapes = &[lhs.shape(), rhs.shape()]; - if lhs.is_scalar() || rhs.is_scalar() { - let other = if lhs.is_scalar() { rhs } else { lhs }; - return Ok(other.storage_view().clone()); - } - let broadcasted = Shape::multi_broadcast(shapes); - if broadcasted.is_none() { - let failed = shapes.iter().map(|s| (*s).clone()).collect::>(); - return Err(InvariantError::BroadcastingFailed(failed).into()); - } - let broadcasted = broadcasted.unwrap(); - let ostrides = Strides::from(&broadcasted); - Ok(StorageView::new(broadcasted, crate::DType::I32, ostrides)) - } - - #[inline] - fn srcs(&self) -> RVec<&Tensor> { - rvec![&self.lhs, &self.rhs] - } -} - -pub enum CmpKernels { - Standard(Cmp), -} - -impl GPUOperation for Cmp { - type KernelEnum = CmpKernels; - - fn select_kernel(&self) -> Self::KernelEnum { - CmpKernels::Standard(self.clone()) - } -} - -impl Kernel for CmpKernels { - type Metadata = CmpMeta; - - fn kernel_name(&self) -> String { - match self { - CmpKernels::Standard(_) => "cmp".to_string(), - } - } - - fn kernel_element(&self, dst: &Tensor) -> KernelElement { - let numel = dst.shape().numel(); - - if numel % 4 == 0 { - KernelElement::Vec4 - } else if numel % 2 == 0 { - KernelElement::Vec2 - } else { - KernelElement::Scalar - } - } - - fn calculate_dispatch(&self, dst: &Tensor) -> Result { - Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) - } - - fn storage_bind_group_layout( - &self, - inplace: bool, - ) -> Result { - if inplace { - panic!("Cmp cannot be done in place"); - } - Ok(BindGroupLayoutDescriptor::binary()) - } - - fn metadata(&self, dst: &Tensor, _: &KernelElement) -> Result { - let numel = dst.shape().numel() as _; - Ok(CmpMeta { numel }) - } - - fn build_kernel( - &self, - inplace: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let CmpKernels::Standard(inner) = self; - let kernel_element = self.kernel_element(dst); - match (inner.lhs.dt(), &kernel_element) { - (DType::F32, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - _ => Err(OperationError::CompileError(format!( - "Unsupported dtype {:?} or kernel element {:?}", - inner.lhs.dt(), - kernel_element - ))), - } - } -} - -#[cfg(all(test, feature = "pyo3"))] -mod tests { - use crate::{test_util::run_py_prg, CmpOp, DType, Device, DeviceRequest, Shape, Tensor}; - use test_strategy::{proptest, Arbitrary}; - - #[derive(Arbitrary, Debug)] - struct BinaryProblem { - op: CmpOp, - #[any(vec![1..=4, 1..=4, 1..=1, 1..=256])] - shape: Shape, - } - - fn ground_truth(a: &Tensor, b: &Tensor, op: &CmpOp) -> anyhow::Result { - let kn = op.kernel_name(); - let prg = format!( - r#" -import torch -import numpy as np -def {}(a, b): - return torch.{}(torch.from_numpy(a), torch.from_numpy(b)).numpy().astype(np.int32) -"#, - kn, kn - ); - run_py_prg(prg.to_string(), &[a, b], &[], DType::I32) - } - - fn run_cmp_trial(prob: BinaryProblem, device: Device) -> anyhow::Result<()> { - let cpu_device = Device::request_device(DeviceRequest::CPU)?; - let BinaryProblem { op, shape } = prob; - let a = Tensor::randn::(0., 1., shape.clone(), cpu_device.clone()); - let b = Tensor::randn::(0., 1., shape, cpu_device.clone()); - let ground = ground_truth(&a, &b, &op)?.cast(DType::F32)?; - - let a_gpu = a.to(&device)?; - let b_gpu = b.to(&device)?; - let c_gpu = match op { - CmpOp::Eq => a_gpu.eq(b_gpu)?, - CmpOp::Ne => a_gpu.ne(b_gpu)?, - CmpOp::Le => a_gpu.le(b_gpu)?, - CmpOp::Ge => a_gpu.ge(b_gpu)?, - CmpOp::Lt => a_gpu.le(b_gpu)?, - CmpOp::Gt => a_gpu.gt(b_gpu)?, - }; - - let d_gpu = c_gpu.to(&Device::CPU)?.cast(DType::F32)?; - ground.all_close(&d_gpu, 1e-4, 1e-4)?; - Ok(()) - } - - #[proptest(cases = 8)] - fn test_binary(prob: BinaryProblem) { - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - run_cmp_trial(prob, device).unwrap(); - } -} diff --git a/crates/ratchet-core/src/ops/fill_constant.rs b/crates/ratchet-core/src/ops/fill_constant.rs deleted file mode 100644 index 815a0a89..00000000 --- a/crates/ratchet-core/src/ops/fill_constant.rs +++ /dev/null @@ -1,251 +0,0 @@ -use derive_new::new; -use encase::ShaderType; -use half::f16; -use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; - -use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, Shape, - StorageView, Strides, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, - Workload, -}; - -#[derive(new, Debug, Clone, IrFields)] -pub struct FillConstant { - pub shape: Shape, - pub value: f32, -} - -#[derive(Debug, derive_new::new, ShaderType, WgslMetadata)] -pub struct FillConstantMeta { - numel: u32, - value: f32, -} - -impl Operation for FillConstant { - fn name(&self) -> &'static str { - "FillConstant" - } - fn compute_view(&self) -> Result { - let shape: Shape = self.shape.clone(); - let strides = Strides::from(&shape); - Ok(StorageView::new(shape, DType::F32, strides)) - } - - fn srcs(&self) -> RVec<&Tensor> { - rvec![] - } - - fn supports_inplace(&self) -> bool { - false - } -} - -impl OpGuards for FillConstant { - fn check_shapes(&self) { - // No input shapes to check - } - - fn check_dtypes(&self) { - // No input dtypes to check - } -} - -pub enum FillConstantKernels { - Standard(FillConstant), -} - -impl GPUOperation for FillConstant { - type KernelEnum = FillConstantKernels; - - fn select_kernel(&self) -> Self::KernelEnum { - FillConstantKernels::Standard(self.clone()) - } -} - -impl KernelRenderable for FillConstantKernels { - fn register_bindings( - &self, - builder: &mut WgslKernelBuilder, - _: bool, - ) -> Result<(), OperationError> { - builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); - builder.register_uniform(); - Ok(()) - } - - fn render( - &self, - _: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let device = dst.device().try_gpu()?; - let mut kernel_builder = WgslKernelBuilder::new( - workgroup_size.clone(), - rvec![ - BuiltIn::WorkgroupId, - BuiltIn::LocalInvocationIndex, - BuiltIn::NumWorkgroups - ], - device.compute_features().clone(), - ); - - self.register_bindings::

(&mut kernel_builder, false)?; - kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - - let N = (P::W as u32).render(); - let dt = P::render_type(); - - kernel_builder.write_main(wgsl! { - let x_offset = workgroup_id.x * 64u; - let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; - if (index >= metadata.numel / 'N) { - return; - } - Y[index] = 'dt(metadata.value); - }); - - Ok(kernel_builder.build()?) - } -} - -impl Kernel for FillConstantKernels { - type Metadata = FillConstantMeta; - - fn kernel_name(&self) -> String { - match self { - FillConstantKernels::Standard(_) => "fill_constant".to_string(), - } - } - - fn kernel_element(&self, dst: &Tensor) -> KernelElement { - let rank = dst.shape().rank(); - let N = if rank > 0 { dst.shape()[rank - 1] } else { 1 }; - - if N % 4 == 0 { - KernelElement::Vec4 - } else if N % 2 == 0 { - KernelElement::Vec2 - } else { - KernelElement::Scalar - } - } - - fn calculate_dispatch(&self, dst: &Tensor) -> Result { - Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) - } - - fn storage_bind_group_layout( - &self, - _inplace: bool, - ) -> Result { - Ok(BindGroupLayoutDescriptor::unary_inplace()) - } - - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { - let FillConstantKernels::Standard(inner) = self; - Ok(FillConstantMeta { - numel: inner.shape.clone().numel() as u32, - value: inner.value, - }) - } - - fn build_kernel( - &self, - inplace: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let kernel_element = self.kernel_element(dst); - match (dst.dt(), &kernel_element) { - (DType::F32, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::I32, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::I32, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::I32, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - _ => Err(OperationError::CompileError(format!( - "Unsupported dtype {:?} or kernel element {:?}", - dst.dt(), - kernel_element - ))), - } - } -} - -#[cfg(all(test, feature = "pyo3"))] -mod tests { - use test_strategy::{proptest, Arbitrary}; - - use crate::{shape, test_util::run_py_prg, DType, Device, DeviceRequest, Tensor}; - - fn ground_truth(shape: &[usize], value: f32) -> anyhow::Result { - let prg = r#" -import torch -def fill_constant(shape, value): - return torch.full(shape, value, dtype=torch.float32).cpu().numpy() -"#; - - run_py_prg(prg.to_string(), &[], &[&shape, &value], DType::F32) - } - - fn run_fill_constant_trial(problem: FillConstantProblem, device: Device) { - let FillConstantProblem { B, M, N, value } = problem; - - let a = Tensor::full(&shape![B, M, N], value, &device); - let ground = ground_truth(&[B, M, N], value).unwrap(); - - let a_gpu = a.to(&device).unwrap(); - - let ours = a_gpu.to(&Device::CPU).unwrap(); - - println!("ours = {:?}", ours); - println!("ground = {:?}", ground); - - // Compare our result with ground truth - ground.all_close(&ours, 1e-6, 1e-6).unwrap(); - } - - #[derive(Arbitrary, Debug)] - struct FillConstantProblem { - #[strategy(1..=128usize)] - B: usize, - #[strategy(1..=128usize)] - M: usize, - #[strategy(1..=128usize)] - N: usize, - value: f32, - } - - #[proptest(cases = 8)] - fn test_fill_constant(prob: FillConstantProblem) { - let FillConstantProblem { B, M, N, value } = prob; - println!("B = {}, M = {}, N = {}, value = {}", B, M, N, value); - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - run_fill_constant_trial(prob, device); - } -} diff --git a/crates/ratchet-core/src/ops/fill_randn.rs b/crates/ratchet-core/src/ops/fill_randn.rs deleted file mode 100644 index 5f6a5610..00000000 --- a/crates/ratchet-core/src/ops/fill_randn.rs +++ /dev/null @@ -1,289 +0,0 @@ -use derive_new::new; -use encase::ShaderType; -use half::f16; -use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; - -use crate::{ - gpu::BindGroupLayoutDescriptor, rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, - KernelElement, KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, - Scalar, Shape, StorageView, Strides, Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, - WorkgroupSize, Workload, -}; - -#[derive(new, Debug, Clone, IrFields)] -pub struct FillRandn { - pub shape: Shape, - pub mean: f32, - pub std: f32, - pub seed: Option, -} - -#[derive(Debug, derive_new::new, ShaderType, WgslMetadata)] -pub struct FillRandnMeta { - numel: u32, - mean: f32, - stddev: f32, - seed: u32, -} - -impl Operation for FillRandn { - fn name(&self) -> &'static str { - "FillRandn" - } - - fn compute_view(&self) -> Result { - let shape: Shape = self.shape.clone(); - let strides = Strides::from(&shape); - Ok(StorageView::new(shape, crate::DType::F32, strides)) - } - - fn srcs(&self) -> RVec<&Tensor> { - rvec![] - } - - fn supports_inplace(&self) -> bool { - false - } -} - -impl OpGuards for FillRandn { - fn check_shapes(&self) { - // No input shapes to check - } - - fn check_dtypes(&self) { - // No input dtypes to check - } -} - -pub enum FillRandnKernels { - Standard(FillRandn), -} - -impl GPUOperation for FillRandn { - type KernelEnum = FillRandnKernels; - - fn select_kernel(&self) -> Self::KernelEnum { - FillRandnKernels::Standard(self.clone()) - } -} - -impl KernelRenderable for FillRandnKernels { - fn register_bindings( - &self, - builder: &mut WgslKernelBuilder, - _: bool, - ) -> Result<(), OperationError> { - builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); - builder.register_uniform(); - Ok(()) - } - - fn render( - &self, - _: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let device = dst.device().try_gpu()?; - let mut kernel_builder = WgslKernelBuilder::new( - workgroup_size.clone(), - rvec![ - BuiltIn::WorkgroupId, - BuiltIn::LocalInvocationIndex, - BuiltIn::NumWorkgroups - ], - device.compute_features().clone(), - ); - - self.register_bindings::

(&mut kernel_builder, false)?; - kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - - kernel_builder.write_global(wgsl! { - fn pcg_hash(input: u32) -> u32 { - let state = input * 747796405u + 2891336453u; - let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; - return (word >> 22u) ^ word; - } - - fn rand(seed: u32) -> f32 { - return f32(pcg_hash(seed)) / 4294967295.0; - } - - fn box_muller_1d(u1: f32, u2: f32) -> f32 { - let r = sqrt(-2.0 * log(u1)); - let theta = 2.0 * 3.14159265359 * u2; - return r * cos(theta); - } - }); - - kernel_builder.write_main(wgsl! { - let x_offset = workgroup_id.x * 64u; - let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; - if (index >= metadata.numel) { - return; - } - - let seed1 = index; - let seed2 = index ^ 2747636419u; // XOR with a prime for a different seed - - let u1 = rand(seed1 + metadata.seed); - let u2 = rand(seed2 + metadata.seed); - - let normal = box_muller_1d(u1, u2); - - Y[index] = f32(normal) * metadata.stddev + metadata.mean; - }); - - Ok(kernel_builder.build()?) - } -} - -impl Kernel for FillRandnKernels { - type Metadata = FillRandnMeta; - - fn kernel_name(&self) -> String { - match self { - FillRandnKernels::Standard(_) => "fill_randn".to_string(), - } - } - - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { - KernelElement::Scalar - } - - fn calculate_dispatch(&self, dst: &Tensor) -> Result { - Ok(Workload::std(dst.shape().numel(), self.kernel_element(dst))) - } - - fn storage_bind_group_layout( - &self, - _inplace: bool, - ) -> Result { - Ok(BindGroupLayoutDescriptor::unary_inplace()) - } - - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { - let FillRandnKernels::Standard(inner) = self; - Ok(FillRandnMeta { - numel: inner.shape.clone().numel() as u32, - mean: inner.mean, - stddev: inner.std, - seed: inner.seed.unwrap_or(0), - }) - } - - fn build_kernel( - &self, - inplace: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let kernel_element = self.kernel_element(dst); - match (dst.dt(), &kernel_element) { - (DType::F32, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - _ => Err(OperationError::CompileError(format!( - "Unsupported dtype {:?} or kernel element {:?}", - dst.dt(), - kernel_element - ))), - } - } -} - -#[cfg(all(test, feature = "pyo3"))] -mod tests { - use test_strategy::{proptest, Arbitrary}; - - use crate::{shape, test_util::run_py_prg, DType, Device, DeviceRequest, Tensor}; - - fn normal_parameters(output: &Tensor) -> anyhow::Result { - let prg = r#" -import numpy as np - -def check_normal(output): - output_np = np.array(output) - mean = np.mean(output_np) - std = np.std(output_np) - return np.array([mean, std], dtype=np.float32) -"#; - - run_py_prg(prg.to_string(), &[output], &[], DType::F32) - } - - fn run_fill_randn_trial(problem: FillRandnProblem, device: Device) { - let FillRandnProblem { B, M, N } = problem; - - let a = Tensor::randn::(0f32, 1f32, shape![B, M, N], device.clone()) - .to(&Device::CPU) - .unwrap(); - - let params = normal_parameters(&a) - .unwrap() - .to(&device) - .unwrap() - .to(&Device::CPU) - .unwrap() - .to_vec::() - .unwrap(); - - let mean = params[0]; - let std = params[1]; - - // Check if the distribution is approximately normal - // We use a tolerance of 0.1 for both mean and standard deviation - if (mean - 0.0).abs() < 0.1 && (std - 1.0).abs() < 0.1 { - println!( - "\x1b[1;32mDistribution approximately normal\x1b[0m - mean={} std={}", - mean, std - ); - } else { - (|| -> anyhow::Result<()> { - { - anyhow::bail!( - "\x1b[1;31mDistribution not normal\x1b[0m - mean={} std={}", - mean, - std - ) - } - })() - .unwrap(); - } - } - - #[derive(Arbitrary, Debug)] - struct FillRandnProblem { - #[strategy(1..=128usize)] - B: usize, - #[strategy(1..=128usize)] - M: usize, - #[strategy(1..=128usize)] - N: usize, - } - - #[proptest(cases = 8)] - fn test_fill_randn(prob: FillRandnProblem) { - let FillRandnProblem { B, M, N } = prob; - println!("B = {}, M = {}, N = {}", B, M, N); - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - run_fill_randn_trial(prob, device); - } -} diff --git a/crates/ratchet-core/src/ops/powf.rs b/crates/ratchet-core/src/ops/powf.rs deleted file mode 100644 index 68e36ff5..00000000 --- a/crates/ratchet-core/src/ops/powf.rs +++ /dev/null @@ -1,277 +0,0 @@ -use derive_new::new; -use encase::ShaderType; -use half::f16; -use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; - -use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, StorageView, - Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, -}; - -#[derive(new, Debug, Clone, IrFields)] -pub struct Powf { - pub src: Tensor, - pub e: f32, -} - -#[derive(Debug, ShaderType, WgslMetadata)] -pub struct PowfMeta { - numel: u32, - e: f32, -} - -impl OpGuards for Powf { - fn check_shapes(&self) {} - - fn check_dtypes(&self) { - let a = &self.src; - assert!(matches!(a.dt(), crate::DType::F32)); - } -} - -impl Operation for Powf { - fn name(&self) -> &'static str { - "Powf" - } - - fn compute_view(&self) -> Result { - Ok(self.src.storage_view().clone()) - } - - #[inline] - fn srcs(&self) -> RVec<&Tensor> { - rvec![&self.src] - } - - fn supports_inplace(&self) -> bool { - true - } -} - -pub enum PowfKernels { - Standard(Powf), -} - -impl GPUOperation for Powf { - type KernelEnum = PowfKernels; - - fn select_kernel(&self) -> Self::KernelEnum { - PowfKernels::Standard(self.clone()) - } -} - -impl KernelRenderable for PowfKernels { - fn register_bindings( - &self, - builder: &mut WgslKernelBuilder, - inplace: bool, - ) -> Result<(), OperationError> { - let arr = Array::

::default(); - if inplace { - builder.register_storage("X", BindingMode::ReadWrite, arr); - } else { - builder.register_storage("X", BindingMode::ReadOnly, arr); - builder.register_storage("Y", BindingMode::ReadWrite, arr); - } - builder.register_uniform(); - Ok(()) - } - - fn render( - &self, - inplace: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let device = dst.device().try_gpu()?; - let mut kernel_builder = WgslKernelBuilder::new( - workgroup_size.clone(), - rvec![ - BuiltIn::WorkgroupId, - BuiltIn::LocalInvocationIndex, - BuiltIn::NumWorkgroups - ], - device.compute_features().clone(), - ); - - self.register_bindings::

(&mut kernel_builder, inplace)?; - kernel_builder.render_metadata(&self.metadata(dst, &self.kernel_element(dst))?); - - let N = (P::W as u32).render(); - let dt = P::render_type(); - - kernel_builder.write_main(wgsl! { - let x_offset = workgroup_id.x * 64u; - let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; - if (index >= metadata.numel / 'N) { - return; - } - - let val = X[index]; - }); - - // pow(x, e) is undefined for x < 0 in Dawn, but apparently not in wgpu, - // but only when the compiler doesn't have enough information to coerce - // e into an integer. We supply e through the metadata, so, at compile-time, - // its type is unknown. - // - // Multiplying by the sign is a fix to make this shader work correctly in Chrome. - let apply = if inplace { - wgsl! { - X[index] = sign(val) * pow(abs(val), 'dt(metadata.e)); - } - } else { - wgsl! { Y[index] = sign(val) * pow(abs(val), 'dt(metadata.e)); } - }; - - kernel_builder.write_main(apply); - Ok(kernel_builder.build()?) - } -} - -impl Kernel for PowfKernels { - type Metadata = PowfMeta; - - fn kernel_name(&self) -> String { - match self { - PowfKernels::Standard(_) => "powf".to_string(), - } - } - - fn calculate_dispatch(&self, dst: &Tensor) -> Result { - let PowfKernels::Standard(inner) = self; - Ok(Workload::std( - inner.src.shape().numel(), - self.kernel_element(dst), - )) - } - - fn storage_bind_group_layout( - &self, - inplace: bool, - ) -> Result { - if inplace { - Ok(BindGroupLayoutDescriptor::unary_inplace()) - } else { - Ok(BindGroupLayoutDescriptor::unary()) - } - } - - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { - let PowfKernels::Standard(inner) = self; - let numel = inner.src.shape().numel() as u32; - Ok(PowfMeta { numel, e: inner.e }) - } - - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { - let PowfKernels::Standard(inner) = self; - let a_rank = inner.src.shape().rank(); - let N = if a_rank > 0 { - inner.src.shape()[a_rank - 1] - } else { - 1 - }; - - if N % 4 == 0 { - KernelElement::Vec4 - } else if N % 2 == 0 { - KernelElement::Vec2 - } else { - KernelElement::Scalar - } - } - - fn build_kernel( - &self, - inplace: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let kernel_element = self.kernel_element(dst); - let PowfKernels::Standard(inner) = self; - match (inner.src.dt(), &kernel_element) { - (DType::F32, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - _ => Err(OperationError::CompileError(format!( - "Unsupported dtype {:?} or kernel element {:?}", - inner.src.dt(), - kernel_element - ))), - } - } -} - -#[cfg(all(test, feature = "pyo3"))] -mod tests { - use test_strategy::{proptest, Arbitrary}; - - use crate::test_util::run_py_prg; - use crate::{shape, Device, DeviceRequest, Tensor}; - - fn ground_truth(a: &Tensor, e: f32) -> anyhow::Result { - let func_prg = r#" -import torch -def powf(a, e): - a_tensor = torch.from_numpy(a) - sign = torch.sign(a_tensor) - return (torch.pow(torch.abs(a_tensor), e) * sign).numpy() -"# - .to_string(); - - let prg = func_prg; - - run_py_prg(prg.to_string(), &[a], &[&e], a.dt()) - } - - fn run_powf_trial(problem: PowfProblem, device: Device) { - let PowfProblem { B, M, N, e } = problem; - let a = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); - let ground = ground_truth(&a, e).unwrap(); - - let a_gpu = a.to(&device).unwrap(); - let b = a_gpu.powf(e).unwrap(); - - let ours = b.to(&Device::CPU).unwrap(); - - ground.all_close(&ours, 1e-5, 1e-5).unwrap(); - } - - #[derive(Arbitrary, Debug)] - struct PowfProblem { - #[strategy(1..=128usize)] - B: usize, - #[strategy(1..=128usize)] - M: usize, - #[strategy(1..=128usize)] - N: usize, - #[strategy(-10.0f32..=10.0f32)] - e: f32, - } - - #[proptest(cases = 16)] - fn test_powf(prob: PowfProblem) { - let PowfProblem { B, M, N, e } = prob; - println!("B = {}, M = {}, N = {}, e = {}", B, M, N, e); - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - run_powf_trial(prob, device); - } -} diff --git a/crates/ratchet-core/src/ops/where_cond.rs b/crates/ratchet-core/src/ops/where_cond.rs deleted file mode 100644 index b9cc26ce..00000000 --- a/crates/ratchet-core/src/ops/where_cond.rs +++ /dev/null @@ -1,305 +0,0 @@ -use derive_new::new; -use encase::ShaderType; -use half::f16; -use inline_wgsl::wgsl; -use ratchet_macros::{IrFields, WgslMetadata}; - -use crate::{ - gpu::{dtype::WgslDType, BindGroupLayoutDescriptor}, - rvec, Array, BindingMode, BuiltIn, DType, GPUOperation, Kernel, KernelElement, - KernelRenderable, KernelSource, OpGuards, Operation, OperationError, RVec, Scalar, StorageView, - Tensor, Vec2, Vec4, WgslKernelBuilder, WgslPrimitive, WorkgroupSize, Workload, -}; - -#[derive(new, Debug, Clone, IrFields)] -pub struct WhereCond { - pub input: Tensor, - pub on_true: Tensor, - pub on_false: Tensor, -} - -#[derive(Debug, ShaderType, WgslMetadata)] -pub struct WhereCondMeta { - numel: u32, -} - -impl OpGuards for WhereCond { - fn check_shapes(&self) { - let (a, b, c) = (&self.input, &self.on_true, &self.on_false); - assert_eq!(a.shape(), b.shape()); - assert_eq!(a.shape(), c.shape()); - } - - fn check_dtypes(&self) { - let (a, b, c) = (&self.input, &self.on_true, &self.on_false); - assert!(matches!(a.dt(), crate::DType::F32 | crate::DType::I32)); - assert!(matches!(b.dt(), crate::DType::F32 | crate::DType::I32)); - assert!(b.dt() == c.dt()) - } -} - -impl Operation for WhereCond { - fn name(&self) -> &'static str { - "WhereCond" - } - - fn compute_view(&self) -> Result { - Ok(self.on_true.storage_view().clone()) - } - - #[inline] - fn srcs(&self) -> RVec<&Tensor> { - rvec![&self.input, &self.on_true, &self.on_false] - } - - fn supports_inplace(&self) -> bool { - // For inplace, the on_{true,false} tensors must be the same dtype as the input tensor - self.on_true.dt() == self.input.dt() && self.on_false.dt() == self.input.dt() - } -} - -pub enum WhereCondKernels { - Standard(WhereCond), -} - -impl GPUOperation for WhereCond { - type KernelEnum = WhereCondKernels; - - fn select_kernel(&self) -> Self::KernelEnum { - WhereCondKernels::Standard(self.clone()) - } -} - -impl Kernel for WhereCondKernels { - type Metadata = WhereCondMeta; - - fn kernel_name(&self) -> String { - match self { - WhereCondKernels::Standard(_) => "where_cond".to_string(), - } - } - - fn kernel_element(&self, _dst: &Tensor) -> KernelElement { - let WhereCondKernels::Standard(inner) = self; - let a_rank = inner.input.shape().rank(); - let N = if a_rank > 0 { - inner.input.shape()[a_rank - 1] - } else { - 1 - }; - - if N % 4 == 0 { - KernelElement::Vec4 - } else if N % 2 == 0 { - KernelElement::Vec2 - } else { - KernelElement::Scalar - } - } - - fn calculate_dispatch(&self, dst: &Tensor) -> Result { - let WhereCondKernels::Standard(inner) = self; - Ok(Workload::std( - inner.input.shape().numel(), - self.kernel_element(dst), - )) - } - - fn storage_bind_group_layout( - &self, - inplace: bool, - ) -> Result { - if inplace { - Ok(BindGroupLayoutDescriptor::ternary_inplace()) - } else { - Ok(BindGroupLayoutDescriptor::ternary()) - } - } - - fn metadata(&self, _: &Tensor, _: &KernelElement) -> Result { - let WhereCondKernels::Standard(inner) = self; - let numel = inner.input.shape().numel() as u32; - Ok(WhereCondMeta { numel }) - } - - fn build_kernel( - &self, - inplace: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let kernel_element = self.kernel_element(dst); - let WhereCondKernels::Standard(inner) = self; - match (inner.input.dt(), &kernel_element) { - (DType::F32, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F32, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::F16, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::I32, KernelElement::Scalar) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::I32, KernelElement::Vec2) => { - self.render::>(inplace, dst, workgroup_size) - } - (DType::I32, KernelElement::Vec4) => { - self.render::>(inplace, dst, workgroup_size) - } - _ => Err(OperationError::CompileError(format!( - "Unsupported dtype {:?} or kernel element {:?}", - inner.input.dt(), - kernel_element - ))), - } - } -} - -impl KernelRenderable for WhereCondKernels { - fn register_bindings( - &self, - builder: &mut WgslKernelBuilder, - inplace: bool, - ) -> Result<(), OperationError> { - let arr = Array::

::default(); - builder.register_storage( - "A", - if inplace { - BindingMode::ReadWrite - } else { - BindingMode::ReadOnly - }, - arr, - ); - - builder.register_storage("B", BindingMode::ReadOnly, Array::

::default()); - builder.register_storage("C", BindingMode::ReadOnly, Array::

::default()); - - if !inplace { - builder.register_storage("Y", BindingMode::ReadWrite, Array::

::default()); - } - builder.register_uniform(); - Ok(()) - } - - fn render( - &self, - inplace: bool, - dst: &Tensor, - workgroup_size: &WorkgroupSize, - ) -> Result { - let device = dst.device().try_gpu()?; - let mut kernel_builder = WgslKernelBuilder::new( - workgroup_size.clone(), - rvec![ - BuiltIn::WorkgroupId, - BuiltIn::NumWorkgroups, - BuiltIn::LocalInvocationIndex - ], - device.compute_features().clone(), - ); - - let kernel_element = self.kernel_element(dst); - - self.register_bindings::

(&mut kernel_builder, inplace)?; - kernel_builder.render_metadata(&self.metadata(dst, &kernel_element)?); - - let N = (P::W as u32).render(); - - kernel_builder.write_main(wgsl! { - let x_offset = workgroup_id.x * 64u; - let index = (workgroup_id.y * num_workgroups.x * 64u) + x_offset + local_invocation_index; - if (index >= metadata.numel / 'N) { - return; - } - }); - - let dt = P::T::DT; - - let kernel_element_str = match kernel_element { - KernelElement::Scalar => dt.to_string(), - KernelElement::Vec2 => format!("{}<{}>", kernel_element.as_str(), dt), - KernelElement::Vec4 => format!("{}<{}>", kernel_element.as_str(), dt), - }; - - let apply = if inplace { - wgsl! { - let val = A[index]; - A[index] = select(B[index], C[index], val != 'kernel_element_str(0)); - } - } else { - wgsl! { Y[index] = select(B[index], C[index], A[index] != 'kernel_element_str(0)); } - }; - kernel_builder.write_main(apply); - Ok(kernel_builder.build()?) - } -} - -#[cfg(all(test, feature = "pyo3"))] -mod tests { - use test_strategy::{proptest, Arbitrary}; - - use crate::test_util::run_py_prg; - use crate::{shape, Device, DeviceRequest, Tensor}; - - fn ground_truth(a: &Tensor, b: &Tensor, c: &Tensor) -> anyhow::Result { - let prg = r#" -import torch -def where_cond(a, b, c): - return torch.where(torch.from_numpy(a) == 0, torch.from_numpy(b), torch.from_numpy(c)).numpy() -"#; - run_py_prg(prg.to_string(), &[a, b, c], &[], b.dt()) - } - - fn run_where_cond_trial(problem: WhereCondProblem, device: Device) { - let WhereCondProblem { B, M, N } = problem; - // Put through a ReLU so some of its entries are 0 - let a = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU) - .relu() - .unwrap(); - let b = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); - let c = Tensor::randn::(0., 1., shape![B, M, N], Device::CPU); - let ground = ground_truth(&a, &b, &c).unwrap(); - - let a_gpu = a.to(&device).unwrap(); - let b_gpu = b.to(&device).unwrap(); - let c_gpu = c.to(&device).unwrap(); - let b = a_gpu.where_cond(b_gpu, c_gpu).unwrap(); - - let ours = b.to(&Device::CPU).unwrap(); - - log::debug!("ours = {:?}", ours); - log::debug!("ground = {:?}", ground); - - ground.all_close(&ours, 1e-6, 1e-6).unwrap(); - } - - #[derive(Arbitrary, Debug)] - struct WhereCondProblem { - #[strategy(1..=3usize)] - B: usize, - #[strategy(1..=256usize)] - M: usize, - #[strategy(1..=256usize)] - N: usize, - } - - #[proptest(cases = 8)] - fn test_where_cond(prob: WhereCondProblem) { - let _ = env_logger::builder().try_init(); - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - run_where_cond_trial(prob, device); - } -} diff --git a/crates/ratchet-core/src/shape.rs b/crates/ratchet-core/src/shape.rs deleted file mode 100644 index ca0c6641..00000000 --- a/crates/ratchet-core/src/shape.rs +++ /dev/null @@ -1,343 +0,0 @@ -use crate::{shape, RVec, Strides}; -use encase::impl_wrapper; -use std::{ - ops::{RangeFrom, RangeTo}, - slice::Iter, -}; - -#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, PartialEq, Eq, Hash, Default)] -pub struct Shape(RVec); - -impl_wrapper!(Shape; using); - -impl Shape { - pub fn new(shape: RVec) -> Self { - Self(shape) - } - - pub fn inner(&self) -> &RVec { - &self.0 - } - - pub fn get(&self, index: usize) -> Option<&usize> { - self.0.get(index) - } - - pub fn insert(&mut self, index: usize, dim: usize) { - self.0.insert(index, dim); - } - - pub fn numel(&self) -> usize { - self.0.iter().product() - } - - pub fn to_vec(&self) -> Vec { - self.0.to_vec() - } - - pub fn iter(&self) -> Iter<'_, usize> { - self.0.iter() - } - - pub fn reverse(&mut self) { - self.0.reverse(); - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn rank(&self) -> usize { - self.len() - } - - pub fn push(&mut self, dim: usize) { - self.0.push(dim); - } - - pub fn remove(&mut self, index: usize) -> usize { - self.0.remove(index) - } - - pub fn is_scalar(&self) -> bool { - self.0.iter().all(|&x| x == 1) - } - - pub fn is_vector(&self) -> bool { - let mut shape = self.clone(); - shape.squeeze(); - shape.rank() <= 1 - } - - #[inline] - pub fn left_pad_to(&mut self, scalar: usize, rank: usize) { - while self.0.len() < rank { - self.0.insert(0, scalar); - } - } - - #[inline] - pub fn right_pad_to(&mut self, scalar: usize, rank: usize) { - while self.0.len() < rank { - self.0.push(scalar); - } - } - - #[inline] - pub fn promote(shape: Shape, rank: usize) -> Shape { - let mut shape = shape; - shape.left_pad_to(1, rank); - shape - } - - #[inline] - pub fn squeeze(&mut self) { - self.0.retain(|x| *x != 1); - } - - #[inline] - pub fn unsqueeze(&mut self, dim: usize) { - self.0.insert(dim, 1); - } - - pub fn drain(&mut self, range: R) -> smallvec::Drain<'_, [usize; 4]> - where - R: std::ops::RangeBounds, - { - self.0.drain(range) - } - - pub fn slice(&self, range: std::ops::Range) -> Self { - Shape(self.0[range].to_vec().into()) - } - - pub fn as_slice(&self) -> &[usize] { - &self.0 - } - - pub fn multi_broadcast(shapes: &[&Shape]) -> Option { - let max_rank = shapes.iter().map(|shape| shape.rank()).max()?; - let mut shape: Shape = shape![]; - for i in 0..max_rank { - let mut current_dim_size = 1; - for shape in shapes { - let len = shape.rank(); - let dim = if i < len { &shape[len - i - 1] } else { &1 }; - if dim != &1 { - if current_dim_size != 1 && dim != ¤t_dim_size { - return None; - } - current_dim_size = *dim; - } - } - shape.0.insert(0, current_dim_size) - } - Some(shape) - } - - /// Returns true if the strides are C contiguous (aka row major). - pub fn is_contiguous(&self, strides: &Strides) -> bool { - let strides_vec = strides.to_vec(); - if self.0.len() != strides_vec.len() { - return false; - } - let mut acc = 1; - for (&stride, &dim) in strides_vec.iter().zip(self.0.iter()).rev() { - if dim > 1 && stride != acc { - return false; - } - acc *= dim as isize; - } - true - } - - pub fn transpose(&mut self) { - let rank = self.rank(); - if rank < 2 { - return; - } - self.0.swap(rank - 2, rank - 1); - } -} - -impl core::ops::Deref for Shape { - type Target = [usize]; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::fmt::Debug for Shape { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut shape = format!("[{}", self.0.first().unwrap_or(&0)); - for dim in self.0.iter().skip(1) { - shape.push_str(&format!("x{}", dim)); - } - write!(f, "{}]", shape) - } -} - -impl std::fmt::Display for Shape { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.0) - } -} - -impl std::ops::Index for Shape { - type Output = usize; - - fn index(&self, index: usize) -> &Self::Output { - &self.0[index] - } -} - -impl std::ops::IndexMut for Shape { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.0[index] - } -} - -impl std::ops::Index> for Shape { - type Output = [usize]; - - fn index(&self, index: RangeFrom) -> &Self::Output { - &self.0[index] - } -} - -impl std::ops::Index> for Shape { - type Output = [usize]; - - fn index(&self, index: RangeTo) -> &Self::Output { - &self.0[index] - } -} - -impl From> for Shape { - fn from(shape: Vec) -> Self { - Self(shape.into()) - } -} - -impl From> for Shape { - fn from(shape: Vec) -> Self { - Self(shape.into_iter().map(|x| x as usize).collect()) - } -} - -impl From<&[usize]> for Shape { - fn from(slice: &[usize]) -> Self { - Shape(slice.into()) - } -} - -impl From> for Shape { - fn from(shape: RVec) -> Self { - Self(shape) - } -} - -impl std::iter::Iterator for Shape { - type Item = usize; - - fn next(&mut self) -> Option { - self.0.pop() - } -} - -impl std::iter::DoubleEndedIterator for Shape { - fn next_back(&mut self) -> Option { - self.0.pop() - } -} - -impl From<&Shape> for glam::UVec4 { - fn from(shape: &Shape) -> Self { - glam::UVec4::new( - shape[0] as u32, - shape[1] as u32, - shape[2] as u32, - shape[3] as u32, - ) - } -} - -impl From for glam::UVec4 { - fn from(shape: Shape) -> Self { - (&shape).into() - } -} - -impl From<&Shape> for glam::IVec3 { - fn from(shape: &Shape) -> Self { - glam::IVec3::new(shape[0] as i32, shape[1] as i32, shape[2] as i32) - } -} - -impl From for glam::IVec3 { - fn from(shape: Shape) -> Self { - (&shape).into() - } -} - -impl From for RVec { - fn from(shape: Shape) -> Self { - shape.0 - } -} - -macro_rules! impl_try_into_arr_for_shape { - ($($N:expr),*) => { - $( - impl TryInto<[usize; $N]> for &Shape { - type Error = anyhow::Error; - - fn try_into(self) -> Result<[usize; $N], Self::Error> { - if self.0.len() == $N { - let mut arr = [0; $N]; - for (i, &item) in self.0.iter().enumerate().take($N) { - arr[i] = item; - } - Ok(arr) - } else { - Err(anyhow::anyhow!("Shape has length {} but expected {}", self.0.len(), $N)) - } - } - } - )* - }; -} - -impl_try_into_arr_for_shape!(1, 2, 3, 4); - -#[cfg(test)] -mod tests { - use crate::Shape; - use proptest::prelude::*; - use std::ops::RangeInclusive; - - impl Arbitrary for Shape { - type Parameters = Vec>; - type Strategy = BoxedStrategy; - - fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { - args.prop_map(Into::::into).boxed() - } - } - - impl Shape { - pub fn as_torch(&self) -> String { - let mut shape = format!("({}", self[0]); - for dim in self.iter().skip(1) { - shape.push_str(&format!(", {}", dim)); - } - shape.push(')'); - shape - } - } -} diff --git a/crates/ratchet-core/src/tensor.rs b/crates/ratchet-core/src/tensor.rs deleted file mode 100644 index 655f98db..00000000 --- a/crates/ratchet-core/src/tensor.rs +++ /dev/null @@ -1,2145 +0,0 @@ -use crate::gpu::{BindGroupEntry, CpuUniform, WgpuDevice}; -use crate::{ - cpu, ops::*, rvec, shape, BufferSegment, CPUBuffer, Compiled, CompiledOp, ComputeCompileKey, - DType, Device, DeviceStorage, GPUOperation, GpuCompileKey, InvariantError, LazyOp, Operation, - OperationError, RVec, RawCPUBuffer, Shape, Storage, Strides, TensorDType, TensorId, -}; -use bitvec::prelude::*; -use derive_new::new; -use maybe_async::maybe_async; -use npyz::WriterBuilder; -use num_traits::AsPrimitive; -use parking_lot::{RwLock, RwLockReadGuard}; -use std::borrow::Cow; -use std::io::{BufRead, Seek}; -use std::mem::ManuallyDrop; -use std::ops::Bound; -use std::path::Path; -use std::sync::Arc; - -#[cfg(feature = "rand")] -use { - rand::prelude::*, - rand_distr::{Normal, Uniform}, -}; - -#[cfg(feature = "testing")] -use ndarray::{ArrayD, ArrayViewD, Dimension}; - -#[cfg(all(not(target_arch = "wasm32"), feature = "pyo3"))] -use numpy::PyArrayDyn; - -// thiserror error for Tensor -#[derive(thiserror::Error, Debug)] -pub enum TensorError { - #[error("Tensor is not resolved")] - NotResolved, - #[error("Tensor {0:?} is missing storage")] - NoStorage(TensorId), - #[error(transparent)] - DeviceError(#[from] crate::DeviceError), - #[error("Failed to transfer data to host")] - TransferError, - #[error(transparent)] - OperationError(#[from] OperationError), -} - -/// A multi-dimensional array of data. -/// -/// A tensor is a lazy representation of an operation. The nodes required to compute it's -/// value and it's own value will not be computed until `resolve` is called. -#[derive(Clone)] -pub struct Tensor { - pub(crate) inner: Arc, -} - -unsafe impl Send for Tensor {} - -macro_rules! ensure_resolved { - ($self:ident) => { - if !$self.resolved() { - $self - .apply_pending_graph() - .expect("Failed to apply pending graph"); - } - }; -} - -impl Tensor { - fn register_with_device(&self) { - if let Device::GPU(inner) = self.device() { - log::trace!("Attempting to register tensor {:?}", self.id()); - inner.register_tensor(self); - } - } - - pub(crate) fn new_impl( - op: LazyOp, - meta: StorageView, - storage: Option, - device: Device, - is_variable: bool, - ) -> Self { - let value = Self { - inner: Arc::new(Inner::new(op, meta, storage, device.clone(), is_variable)), - }; - value.register_with_device(); - value - } - - pub fn new(op: LazyOp, meta: StorageView, storage: Option, device: Device) -> Self { - Self::new_impl(op, meta, storage, device, false) - } - - pub(crate) fn full_impl>( - shape: &Shape, - value: T, - device: &Device, - is_variable: bool, - ) -> Self { - let meta = StorageView { - shape: shape.clone(), - dt: T::dt(), - strides: Strides::from(&shape.clone()), - }; - Self::new_impl( - LazyOp::FillConstant(FillConstant { - shape: shape.clone(), - value: value.as_(), - }), - meta, - None, - device.clone(), - is_variable, - ) - } - - pub fn full>( - shape: &Shape, - value: T, - device: &Device, - ) -> Self { - Self::full_impl::(shape, value, device, false) - } - - #[track_caller] - fn lazy(op: LazyOp, meta: StorageView, device: Device, is_variable: bool) -> Self { - op.check_invariants(); - Self::new_impl(op, meta, None, device, is_variable) - } - - pub fn shallow( - op: LazyOp, - meta: StorageView, - storage: Arc>>, - device: Device, - is_variable: bool, - ) -> Self { - let value = Self { - inner: Arc::new(Inner::from_shallow(op, meta, storage, device, is_variable)), - }; - value.register_with_device(); - value - } - - pub fn strong_count(&self) -> usize { - Arc::strong_count(&self.inner) - } - - pub(crate) fn update_storage(&self, storage: Storage) { - *self.inner.storage.write() = Some(storage); - } -} - -impl std::fmt::Debug for Tensor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.device() { - Device::CPU => match self.dt() { - DType::F32 => self.to_ndarray_view::().fmt(f), - _ => { - let storage_fmt = self.storage().as_ref().map(|s| s.dump(self.dt(), false)); - let (id, op) = (self.id(), self.op()); - f.debug_struct("Tensor") - .field("id", &id) - .field("shape", &self.shape()) - .field("dt", &self.dt()) - .field("op", &op) - .field("storage", &storage_fmt) - .finish() - } - }, - Device::GPU(_) => { - let storage_fmt = self.storage().as_ref().map(|s| s.dump(self.dt(), false)); - let (id, op) = (self.id(), self.op()); - f.debug_struct("Tensor") - .field("id", &id) - .field("shape", &self.shape()) - .field("dt", &self.dt()) - .field("op", &op) - .field("storage", &storage_fmt) - .finish() - } - } - } -} - -impl PartialEq for Tensor { - fn eq(&self, other: &Self) -> bool { - self.inner.id == other.inner.id - } -} - -impl std::ops::Deref for Tensor { - type Target = Inner; - - fn deref(&self) -> &Self::Target { - self.inner.as_ref() - } -} - -/// Tensors are just an view into their underlying byte storage. -#[derive(new, Debug, Clone)] -pub struct StorageView { - shape: Shape, - dt: DType, - strides: Strides, -} - -impl StorageView { - pub fn is_contiguous(&self) -> bool { - self.shape.is_contiguous(&self.strides) - } -} - -impl Drop for Inner { - fn drop(&mut self) { - if let Device::GPU(inner) = &self.device { - log::trace!("Attempting to unregister tensor {:?}", self.id); - inner.unregister_tensor(self.id); - } - - unsafe { - ManuallyDrop::drop(&mut self.storage); - } - } -} - -#[derive(Debug)] -pub struct Inner { - id: TensorId, - op: LazyOp, - device: Device, - view: StorageView, - is_variable: bool, - storage: ManuallyDrop>>>, -} - -impl AsRef for Inner { - fn as_ref(&self) -> &Inner { - self - } -} - -impl Inner { - fn new( - op: LazyOp, - meta: StorageView, - storage: Option, - device: Device, - is_variable: bool, - ) -> Self { - Self { - id: TensorId::new(), - view: meta, - op, - device, - storage: ManuallyDrop::new(Arc::new(RwLock::new(storage))), - is_variable, - } - } - - fn from_shallow( - op: LazyOp, - meta: StorageView, - storage: Arc>>, - device: Device, - is_variable: bool, - ) -> Self { - Self { - id: TensorId::new(), - view: meta, - op, - device, - storage: ManuallyDrop::new(storage), - is_variable, - } - } -} - -impl Tensor { - pub fn id(&self) -> TensorId { - self.inner.id - } - - pub fn storage_view(&self) -> &StorageView { - &self.view - } - - pub fn rank(&self) -> usize { - self.view.shape.len() - } - - pub fn dt(&self) -> DType { - self.view.dt - } - - pub fn shape(&self) -> &Shape { - &self.view.shape - } - - pub fn strides(&self) -> &Strides { - &self.view.strides - } - - //WARNING: very wrong for quantized types! - pub fn num_bytes(&self) -> usize { - self.view.shape.numel() * self.view.dt.size_of() - } - - pub fn device(&self) -> &Device { - &self.device - } - - pub fn storage(&self) -> RwLockReadGuard> { - self.inner.storage.read() - } - - pub fn resolved(&self) -> bool { - self.storage().is_some() - } - - pub fn op(&self) -> &LazyOp { - &self.inner.op - } - - pub fn is_scalar(&self) -> bool { - self.shape().is_scalar() - } - - pub fn is_variable(&self) -> bool { - self.inner.is_variable - } - - #[cfg(feature = "plotting")] - pub fn plot_fmt(&self) -> String { - let shape = self.shape(); - let dt = self.dt(); - let storage = self.storage(); - let storage_fmt = storage - .as_ref() - .map(|s| s.plot_fmt()) - .unwrap_or_else(|| "Unresolved".to_string()); - let references = self.strong_count(); - format!( - "#{:?}-{:?}-{:?}{}\n{:#?}\n{}\n{:?} references", - self.id(), - dt, - shape, - if self.is_variable() { " (var)" } else { "" }, - self.op().ir().fields(), - storage_fmt, - references - ) - } -} - -macro_rules! impl_binary_op { - ($method_name:ident, $op:expr) => { - #[allow(clippy::should_implement_trait)] - pub fn $method_name(self, other: Tensor) -> anyhow::Result { - let device = self.device.clone(); - //TODO: avoid broadcasting if either operand is scalar - let (mut lhs, mut rhs) = (self, other); - let shapes = &[lhs.shape(), rhs.shape()]; - let broadcasted = Shape::multi_broadcast(shapes); - if broadcasted.is_none() { - let failed = shapes.iter().map(|s| (*s).clone()).collect::>(); - return Err(InvariantError::BroadcastingFailed(failed).into()); - } - let broadcasted = broadcasted.unwrap(); - let left_required = shapes[0] != &broadcasted; - let right_required = shapes[1] != &broadcasted; - - (lhs, rhs) = if left_required { - (lhs.broadcast_to(broadcasted.clone())?, rhs.clone()) - } else if right_required { - (lhs, rhs.broadcast_to(broadcasted.clone())?) - } else { - (lhs, rhs) - }; - - let binary = Binary::new(lhs, rhs, $op); - let new_view = binary.compute_view()?; - - Ok(Tensor::lazy( - LazyOp::Binary(binary), - new_view, - device, - false, - )) - } - }; -} - -macro_rules! impl_cmp_op { - ($method_name:ident, $op:expr) => { - #[allow(clippy::should_implement_trait)] - pub fn $method_name(self, other: Tensor) -> anyhow::Result { - let device = self.device.clone(); - //TODO: avoid broadcasting if either operand is scalar - let (mut lhs, mut rhs) = (self, other); - let shapes = &[lhs.shape(), rhs.shape()]; - let broadcasted = Shape::multi_broadcast(shapes); - if broadcasted.is_none() { - let failed = shapes.iter().map(|s| (*s).clone()).collect::>(); - return Err(InvariantError::BroadcastingFailed(failed).into()); - } - let broadcasted = broadcasted.unwrap(); - let left_required = shapes[0] != &broadcasted; - let right_required = shapes[1] != &broadcasted; - - (lhs, rhs) = if left_required { - (lhs.broadcast_to(broadcasted.clone())?, rhs.clone()) - } else if right_required { - (lhs, rhs.broadcast_to(broadcasted.clone())?) - } else { - (lhs, rhs) - }; - - let cmp = Cmp::new(lhs, rhs, $op); - let new_view = cmp.compute_view()?; - - Ok(Tensor::lazy(LazyOp::Cmp(cmp), new_view, device, false)) - } - }; -} - -macro_rules! impl_unary_op { - ($method_name:ident, $op:expr) => { - #[allow(clippy::should_implement_trait)] - pub fn $method_name(self) -> anyhow::Result { - let device = self.device.clone(); - let unary = Unary::new(self.clone(), $op); - let new_view = unary.compute_view()?; - Ok(Tensor::lazy(LazyOp::Unary(unary), new_view, device, false)) - } - }; -} - -impl Tensor { - impl_binary_op!(add, BinaryOp::Add); - impl_binary_op!(sub, BinaryOp::Sub); - impl_binary_op!(mul, BinaryOp::Mul); - impl_binary_op!(div, BinaryOp::Div); - - impl_cmp_op!(eq, CmpOp::Eq); - impl_cmp_op!(ne, CmpOp::Ne); - impl_cmp_op!(le, CmpOp::Le); - impl_cmp_op!(ge, CmpOp::Ge); - impl_cmp_op!(lt, CmpOp::Lt); - impl_cmp_op!(gt, CmpOp::Gt); - - impl_unary_op!(gelu, UnaryOp::Gelu); - impl_unary_op!(tanh, UnaryOp::Tanh); - impl_unary_op!(exp, UnaryOp::Exp); - impl_unary_op!(log, UnaryOp::Log); - impl_unary_op!(sin, UnaryOp::Sin); - impl_unary_op!(cos, UnaryOp::Cos); - impl_unary_op!(abs, UnaryOp::Abs); - impl_unary_op!(sqrt, UnaryOp::Sqrt); - impl_unary_op!(relu, UnaryOp::Relu); - impl_unary_op!(relu2, UnaryOp::Relu2); - impl_unary_op!(floor, UnaryOp::Floor); - impl_unary_op!(ceil, UnaryOp::Ceil); - impl_unary_op!(neg, UnaryOp::Neg); - impl_unary_op!(sigmoid, UnaryOp::Sigmoid); - impl_unary_op!(swiglu, UnaryOp::Swiglu); - impl_unary_op!(silu, UnaryOp::Silu); - impl_unary_op!(square, UnaryOp::Square); - impl_unary_op!(recip, UnaryOp::Reciprocal); - - pub fn cast(self, dst_dt: DType) -> anyhow::Result { - if self.dt() == dst_dt { - return Ok(self); - } - - let device = self.device.clone(); - let cast = Cast::new(self, dst_dt); - let new_view = cast.compute_view()?; - Ok(Tensor::lazy(LazyOp::Cast(cast), new_view, device, false)) - } - - /// Cast a tensor to full precision (IEEE 754 32-bit floating point). - pub fn float(self) -> anyhow::Result { - self.cast(DType::F32) - } - - /// Cast a tensor to half precision (IEEE 754 16-bit floating point). - pub fn half(self) -> anyhow::Result { - self.cast(DType::F16) - } - - pub fn group_norm( - self, - num_groups: usize, - weight: Tensor, - bias: Option, - eps: f32, - ) -> anyhow::Result { - let device = self.device.clone(); - let group_norm = GroupNorm::new(Norm::new(self, weight, bias, eps), num_groups); - let norm_op = NormOp::GroupNorm(group_norm); - let new_view = norm_op.compute_view()?; - Ok(Tensor::lazy(LazyOp::Norm(norm_op), new_view, device, false)) - } - - pub fn layer_norm( - self, - weight: Tensor, - bias: Option, - eps: f32, - ) -> anyhow::Result { - let device = self.device.clone(); - let layer_norm = Norm::new(self, weight, bias, eps); - let op = NormOp::LayerNorm(layer_norm); - let new_view = op.compute_view()?; - Ok(Tensor::lazy(LazyOp::Norm(op), new_view, device, false)) - } - - pub fn rms_norm(self, weight: Tensor, eps: f32) -> anyhow::Result { - let device = self.device.clone(); - let rms = Norm::new(self, weight, None, eps); - let op = NormOp::RMSNorm(rms); - let new_view = op.compute_view()?; - Ok(Tensor::lazy(LazyOp::Norm(op), new_view, device, false)) - } - - pub fn conv1d( - self, - weight: Tensor, - bias: Option, - stride: usize, - padding: usize, - ) -> anyhow::Result { - let device = self.device.clone(); - let conv = Conv::new(self, weight, bias, stride, padding); - let new_view = conv.compute_view()?; - Ok(Tensor::lazy(LazyOp::Conv(conv), new_view, device, false)) - } - - //TODO: switch dim to isize and allow negative indexing - pub fn softmax(self, dim: usize) -> anyhow::Result { - let device = self.device.clone(); - let softmax = Softmax::new(self, dim); - let new_view = softmax.compute_view()?; - Ok(Tensor::lazy( - LazyOp::Softmax(softmax), - new_view, - device, - false, - )) - } - - fn rope_impl( - self, - dim: usize, - base: f32, - offset: usize, - is_backward: bool, - ) -> anyhow::Result { - let device = self.device.clone(); - let rope = RoPE::new(self, dim, base, offset, is_backward); - let new_view = rope.compute_view()?; - Ok(Tensor::lazy(LazyOp::RoPE(rope), new_view, device, false)) - } - - pub fn rope(self, dim: usize, base: f32, offset: usize) -> anyhow::Result { - self.rope_impl(dim, base, offset, false) - } - - pub(crate) fn rope_backward( - self, - dim: usize, - base: f32, - offset: usize, - ) -> anyhow::Result { - self.rope_impl(dim, base, offset, true) - } - - pub fn alibi(self, max_bias: f32) -> anyhow::Result { - let device = self.device.clone(); - let alibi = Alibi::new(self, max_bias); - let new_view = alibi.compute_view()?; - Ok(Tensor::lazy(LazyOp::Alibi(alibi), new_view, device, false)) - } - - //TODO: horrific interface - pub fn matmul(self, rhs: Tensor, trans_lhs: bool, trans_rhs: bool) -> anyhow::Result { - let device = self.device.clone(); - let matmul = Matmul::new(self, rhs, None, trans_lhs, trans_rhs, false); - let new_view = matmul.compute_view()?; - Ok(Tensor::lazy( - LazyOp::Matmul(matmul), - new_view, - device, - false, - )) - } - - pub fn gemm( - self, - rhs: Tensor, - bias: Option, - trans_lhs: bool, - trans_rhs: bool, - trans_out: bool, - ) -> anyhow::Result { - let device = self.device.clone(); - let gemm = Matmul::new(self, rhs, bias, trans_lhs, trans_rhs, trans_out); - let new_view = gemm.compute_view()?; - Ok(Tensor::lazy(LazyOp::Matmul(gemm), new_view, device, false)) - } - - pub fn affine(self, mul: f32, add: f32) -> anyhow::Result { - let device = self.device.clone(); - let affine = Affine::new(self, mul, add); - let new_view = affine.compute_view()?; - Ok(Tensor::lazy( - LazyOp::Affine(affine), - new_view, - device, - false, - )) - } - - pub fn powf(self, e: f32) -> anyhow::Result { - let device = self.device.clone(); - let powf = Powf::new(self, e); - let new_view = powf.compute_view()?; - Ok(Tensor::lazy(LazyOp::Powf(powf), new_view, device, false)) - } - - fn reduce_impl(self, dim: usize, keepdim: bool, op: ReduceOp) -> anyhow::Result { - let device = self.device.clone(); - let reduce = Reduce::new(self, op, rvec![dim], keepdim); - let new_view = reduce.compute_view()?; - Ok(Tensor::lazy( - LazyOp::Reduce(reduce), - new_view, - device, - false, - )) - } - - fn sum_impl(self, sum_dims: &[usize], keepdim: bool) -> anyhow::Result { - let device = self.device.clone(); - let sum = Reduce::new(self, ReduceOp::Sum, sum_dims.into(), keepdim); - let new_view = sum.compute_view()?; - Ok(Tensor::lazy(LazyOp::Reduce(sum), new_view, device, false)) - } - - pub fn sum_keepdim(self, sum_dims: &[usize]) -> anyhow::Result { - self.sum_impl(sum_dims, true) - } - - pub fn sum(self, sum_dims: &[usize]) -> anyhow::Result { - self.sum_impl(sum_dims, false) - } - - pub fn sum_all(self) -> anyhow::Result { - let dims: Vec<_> = (0..self.rank()).collect(); - self.sum(&dims) - } - - pub fn max_keepdim(self, dim: usize) -> anyhow::Result { - self.reduce_impl(dim, true, ReduceOp::Max) - } - - pub fn max(self, dim: usize) -> anyhow::Result { - self.reduce_impl(dim, false, ReduceOp::Max) - } - - pub fn min_keepdim(self, dim: usize) -> anyhow::Result { - self.reduce_impl(dim, true, ReduceOp::Min) - } - - pub fn min(self, dim: usize) -> anyhow::Result { - self.reduce_impl(dim, false, ReduceOp::Min) - } - - pub fn argmax_keepdim(self, dim: usize) -> anyhow::Result { - self.reduce_impl(dim, true, ReduceOp::ArgMax) - } - - pub fn argmax(self, dim: usize) -> anyhow::Result { - self.reduce_impl(dim, false, ReduceOp::ArgMax) - } - - pub fn argmin_keepdim(self, dim: usize) -> anyhow::Result { - self.reduce_impl(dim, true, ReduceOp::ArgMin) - } - - /// Similar to `argmin_keepdim` but the target dimension is squeezed. - pub fn argmin(self, dim: usize) -> anyhow::Result { - self.reduce_impl(dim, false, ReduceOp::ArgMin) - } - - pub fn norm(self) -> anyhow::Result { - self.square()?.sum_all()?.sqrt() - } - - fn flatten_impl( - self, - start_dim: Option, - end_dim: Option, - ) -> anyhow::Result { - if self.rank() == 0 { - self.view(shape![1]) - } else { - let start_dim = start_dim.unwrap_or(0); - let end_dim = end_dim.unwrap_or(self.rank() - 1); - if start_dim < end_dim { - let dims = self.shape(); - let mut dst_dims = dims[..start_dim].to_vec(); - dst_dims.push( - dims.to_vec()[start_dim..end_dim + 1] - .iter() - .product::(), - ); - if end_dim + 1 < dims.len() { - dst_dims.extend(&dims[end_dim + 1..]); - } - self.view(Shape::from(dst_dims)) - } else { - Ok(self.clone()) - } - } - } - - pub fn flatten(self, start_dim: usize, end_dim: usize) -> anyhow::Result { - self.flatten_impl(Some(start_dim), Some(end_dim)) - } - - pub fn flatten_to(self, end_dim: usize) -> anyhow::Result { - self.flatten_impl(None::, Some(end_dim)) - } - - pub fn flatten_from(self, start_dim: usize) -> anyhow::Result { - self.flatten_impl(Some(start_dim), None::) - } - - pub fn flatten_all(self) -> anyhow::Result { - self.flatten_impl(None::, None::) - } - - /// # Slice - /// - /// Current slice implementation requires specification of all dimensions. - /// Currently very user hostile, but will be improved. - /// TODO: should allow mixed range types - pub fn slice>(self, ranges: &[D]) -> anyhow::Result { - let device = self.device.clone(); - let mut resolved_ranges = rvec![]; - - for (ridx, r) in ranges.iter().enumerate() { - let start = match r.start_bound() { - Bound::Included(&s) => s, - Bound::Excluded(&s) => s + 1, - Bound::Unbounded => 0, - }; - let end = match r.end_bound() { - Bound::Included(&e) => e + 1, - Bound::Excluded(&e) => e, - Bound::Unbounded => self.shape()[ridx], - }; - resolved_ranges.push(start..end); - } - - let slice = Slice::new(self, resolved_ranges); - let out_view = slice.compute_view()?; - let op = LazyOp::Reindex(Reindex::Slice(slice)); - Ok(Tensor::lazy(op, out_view, device, false)) - } - - /// # View - /// - /// Creates a new tensor with the same data, but a different shape. - /// The new shape must have the same number of elements as the original shape. - pub fn view(self, shape: Shape) -> anyhow::Result { - if self.shape().numel() != shape.numel() { - anyhow::bail!( - "Cannot reshape tensor with {} elements to shape {:?} ({} elements)", - self.shape().numel(), - shape, - shape.numel() - ); - } - let device = self.device.clone(); - let storage = Arc::clone(&self.storage); - let op = View::new(self, shape); - let out_view = op.compute_view()?; - - Ok(Tensor::shallow( - LazyOp::View(op), - out_view, - storage, - device, - false, - )) - } - - // Use view to add a singleton dimension - pub fn unsqueeze(self, dim: usize) -> anyhow::Result { - let mut new_shape = self.shape().clone(); - new_shape.unsqueeze(dim); - self.view(new_shape) - } - - pub fn squeeze(self) -> anyhow::Result { - let mut new_shape = self.shape().clone(); - new_shape.squeeze(); - self.view(new_shape) - } - - pub fn cat(tensors: RVec, dim: usize) -> anyhow::Result { - let device = tensors[0].device.clone(); - assert!(tensors.iter().all(|t| t.device == device), "Mixed devices"); - - let cat = Concat::new(tensors, dim); - let new_view = cat.compute_view()?; - Ok(Tensor::lazy(LazyOp::Concat(cat), new_view, device, false)) - } - - fn stack_impl(tensors: RVec, dim: usize, root: bool) -> anyhow::Result { - match tensors.len() { - 0 => anyhow::bail!("Cannot stack empty list of tensors"), - 1 => { - if root { - Ok(tensors[0].clone().unsqueeze(dim)?) - } else { - Ok(tensors[0].clone()) - } - } - len => { - let tensors = if root { - tensors - .iter() - .map(|t| t.clone().unsqueeze(dim)) - .collect::>>()? - } else { - tensors - }; - - let device = tensors[0].device.clone(); - assert!(tensors.iter().all(|t| t.device == device), "Mixed devices"); - - if len <= 4 { - return Self::cat(tensors, dim); - } - - // Process tensors in chunks of 4 recursively - let mut current_level = tensors; - - while current_level.len() > 1 { - let mut next_level = RVec::with_capacity((current_level.len() + 3) / 4); - - for chunk in current_level.chunks(4) { - let chunk_vec = chunk.iter().cloned().collect(); - let reduced = Self::stack_impl(chunk_vec, dim, false)?; - next_level.push(reduced); - } - - current_level = next_level; - } - - Ok(current_level.into_iter().next().unwrap()) - } - } - } - - pub fn stack(tensors: RVec, dim: usize) -> anyhow::Result { - Self::stack_impl(tensors, dim, true) - } - - pub fn permute(self, dims: &[usize]) -> anyhow::Result { - let device = self.device.clone(); - let permute = Permute::new(self, dims.into()); - let out_view = permute.compute_view()?; - - let op = LazyOp::Reindex(Reindex::Permute(permute)); - Ok(Tensor::lazy(op, out_view, device, false)) - } - - pub fn cache(self, source: Tensor, dim: usize, offset: usize) -> anyhow::Result { - let device = self.device.clone(); - let cache = Cache::new(self, source, dim, offset); - let new_view = cache.compute_view()?; - Ok(Tensor::lazy(LazyOp::Cache(cache), new_view, device, false)) - } - - /// Returns a new tensor duplicating data from the original tensor. New dimensions are inserted - /// on the left. - pub fn broadcast_left(self, left_shape: Shape) -> anyhow::Result { - let mut dims = left_shape.to_vec(); - dims.extend(self.shape().to_vec()); - self.broadcast_to(Shape::from(dims)) - } - - pub fn broadcast_to(self, shape: Shape) -> anyhow::Result { - let device = self.device.clone(); - let broadcast = Broadcast::new(self, shape); - let new_view = broadcast.compute_view()?; - - let op = LazyOp::Reindex(Reindex::Broadcast(broadcast)); - Ok(Tensor::lazy(op, new_view, device, false)) - } - - pub fn index_select(self, indices: Tensor, dim: usize) -> anyhow::Result { - let device = self.device.clone(); - let index_select = IndexSelect::new(self, indices, dim); - let new_view = index_select.compute_view()?; - Ok(Tensor::lazy( - LazyOp::Select(index_select), - new_view, - device, - false, - )) - } - - pub fn index_write(self, src: Tensor, write_start: RVec) -> anyhow::Result { - let device = self.device.clone(); - let index_write = IndexWrite::new(self, src, write_start); - let new_view = index_write.compute_view()?; - let op = LazyOp::IndexWrite(index_write); - Ok(Tensor::lazy(op, new_view, device, false)) - } - - pub fn where_cond(self, on_true: Tensor, on_false: Tensor) -> anyhow::Result { - let device = self.device.clone(); - let where_cond = WhereCond::new(self, on_true, on_false); - let new_view = where_cond.compute_view()?; - Ok(Tensor::lazy( - LazyOp::WhereCond(where_cond), - new_view, - device, - false, - )) - } - - pub fn scatter_add( - self, - indices: Tensor, - source: Tensor, - dim: usize, - ) -> anyhow::Result { - let source_dims = source.shape().to_vec(); - let self_dims = self.shape().to_vec(); - let mismatch = if source_dims.len() != self_dims.len() { - true - } else { - let mut mismatch = false; - for (i, (&d1, &d2)) in self_dims.iter().zip(source_dims.iter()).enumerate() { - if i != dim && d1 != d2 { - mismatch = true; - break; - } - } - mismatch - }; - if mismatch { - Err(InvariantError::ShapeMismatchBinaryOp { - op: "scatter-add (self, src)", - lhs: self.shape().clone(), - rhs: source.shape().clone(), - })? - } - if indices.shape() != source.shape() { - Err(InvariantError::ShapeMismatchBinaryOp { - op: "scatter-add (indexes, src)", - lhs: indices.shape().clone(), - rhs: source.shape().clone(), - })? - } - let device = self.device.clone(); - let scatter_add = ScatterAdd::new(self, source, indices, dim); - let new_view = scatter_add.compute_view()?; - Ok(Tensor::lazy( - LazyOp::ScatterAdd(scatter_add), - new_view, - device, - false, - )) - } - - pub fn index_add(self, indices: Tensor, source: Tensor, dim: usize) -> anyhow::Result { - let source_dims = source.shape().to_vec(); - let self_dims = self.shape().to_vec(); - let mismatch = if source_dims.len() != self_dims.len() { - true - } else { - let mut mismatch = false; - for (i, (&d1, &d2)) in self_dims.iter().zip(source_dims.iter()).enumerate() { - if i != dim && d1 != d2 { - mismatch = true; - break; - } - } - mismatch - }; - if mismatch { - Err(InvariantError::ShapeMismatchBinaryOp { - op: "index_add", - lhs: self.shape().clone(), - rhs: source.shape().clone(), - })? - } - if indices.rank() != 1 { - Err(InvariantError::RankMismatch { - accepted: 1..=1, - actual: indices.rank(), - })? - } - let indices_len = indices.shape()[0]; - if source_dims[dim] != indices_len { - Err(InvariantError::ShapeMismatchBinaryOp { - op: "index_add", - lhs: indices.shape().clone(), - rhs: source.shape().clone(), - })? - } - let device = self.device.clone(); - let index_add = IndexAdd::new(self, source, indices, dim); - let new_view = index_add.compute_view()?; - Ok(Tensor::lazy( - LazyOp::IndexAdd(index_add), - new_view, - device, - false, - )) - } - - pub fn gather(self, indices: Tensor, dim: usize) -> anyhow::Result { - let self_dims = self.shape().to_vec(); - let indices_dims = indices.shape().to_vec(); - let mismatch = if indices_dims.len() != self_dims.len() { - true - } else { - let mut mismatch = false; - for (i, (&d1, &d2)) in self_dims.iter().zip(indices_dims.iter()).enumerate() { - if i != dim && d1 != d2 { - mismatch = true; - break; - } - } - mismatch - }; - if mismatch { - Err(InvariantError::ShapeMismatchBinaryOp { - op: "gather", - lhs: self.shape().clone(), - rhs: indices.shape().clone(), - })? - } - let device = self.device.clone(); - let gather = Gather::new(self, indices, dim); - let new_view = gather.compute_view()?; - Ok(Tensor::lazy( - LazyOp::Gather(gather), - new_view, - device, - false, - )) - } - - pub fn arange>( - start: T, - end: T, - device: &Device, - ) -> anyhow::Result { - Self::arange_step::(start, end, T::one(), device) - } - - /// Creates a new 1D tensor with values from the interval `[start, end)` taken with a common - /// difference `step` from `start`. - pub fn arange_step>( - start: T, - end: T, - step: T, - device: &Device, - ) -> anyhow::Result { - if step == T::zero() { - anyhow::bail!("step cannot be zero") - } - - if device.is_cpu() { - let mut data = vec![]; - let mut current = start; - if step >= T::zero() { - while current < end { - data.push(current); - current = current + step; - } - } else { - while current > end { - data.push(current); - current = current + step; - } - } - let len = data.len(); - Ok(Tensor::from_data(data, shape![len], device.clone())) - } else { - let arange = Arange::new(start.as_(), end.as_(), step.as_()); - let numel = arange.numel(); - let op = LazyOp::Arange(arange); - - let meta = StorageView { - shape: shape![numel], - dt: T::dt(), - strides: Strides::from(&shape![numel]), - }; - - Ok(Tensor::lazy(op, meta, device.clone(), false)) - } - } - - #[cfg(feature = "rand")] - pub(crate) fn randint_impl( - low: T, - high: T, - shape: Shape, - device: Device, - is_variable: bool, - ) -> Tensor { - let rng = device.get_rng(); - let data = (0..shape.numel()) - .map(|_| { - let sample: T = rng.write().gen_range(low..high); - sample - }) - .collect::>(); - Tensor::from_data_impl(data, shape, device, is_variable) - } - - #[cfg(feature = "rand")] - pub fn randint( - low: T, - high: T, - shape: Shape, - device: Device, - ) -> Tensor { - Self::randint_impl(low, high, shape, device, false) - } - - #[cfg(feature = "rand")] - pub(crate) fn randn_impl( - mean: f32, - std: f32, - shape: Shape, - device: Device, - is_variable: bool, - ) -> Self { - let rng = device.get_rng(); - if device.is_cpu() { - let distr = Normal::new(mean as f64, std as f64).unwrap(); - let data = (0..shape.numel()) - .map(|_| { - let sample: f64 = distr.sample(&mut *rng.write()); - T::from(sample as f32).expect("Failed to convert sample") - }) - .collect::>(); - let storage = Storage::from_slice(&data, &shape, &device); - let strides = Strides::from(&shape); - let meta = StorageView::new(shape, T::dt(), strides); - Self::new_impl(LazyOp::Const, meta, Some(storage), device, is_variable) - } else { - let meta = StorageView { - shape: shape.clone(), - dt: DType::F32, - strides: Strides::from(&shape.clone()), - }; - Self::new_impl( - LazyOp::FillRandn(FillRandn { - shape, - mean, - std, - seed: Some(rng.write().next_u32()), - }), - meta, - None, - device, - is_variable, - ) - } - } - - #[cfg(feature = "rand")] - pub fn randn( - mean: f32, - std: f32, - shape: Shape, - device: Device, - ) -> Tensor { - Self::randn_impl::(mean, std, shape, device, false) - } - - #[cfg(feature = "rand")] - pub(crate) fn rand_impl( - lo: f32, - up: f32, - shape: Shape, - device: Device, - is_variable: bool, - ) -> Self { - let rng = device.get_rng(); - let distr = Uniform::new(lo, up); - let data = (0..shape.numel()) - .map(|_| { - let sample: f32 = distr.sample(&mut *rng.write()); - T::from(sample).expect("Failed to convert sample") - }) - .collect::>(); - - Self::from_data_impl(data, shape, device, is_variable) - } - - #[cfg(feature = "rand")] - pub fn rand( - lo: f32, - up: f32, - shape: Shape, - device: Device, - ) -> Tensor { - Self::rand_impl::(lo, up, shape, device, false) - } - - pub(crate) fn zeros_impl>( - shape: &Shape, - device: &Device, - is_variable: bool, - ) -> Tensor { - if device.is_cpu() { - let storage = Storage::zeros::(shape, device); - let strides = Strides::from(shape); - let meta = StorageView::new(shape.clone(), T::dt(), strides); - Tensor::new_impl( - LazyOp::Const, - meta, - Some(storage), - device.clone(), - is_variable, - ) - } else { - Self::full_impl(shape, T::zero(), device, is_variable) - } - } - - pub fn range(shape: &Shape, device: Device) -> Tensor { - let data: Vec = (0..shape.numel()) - .map(|i| T::from(i).expect("Failed to convert index to T")) - .collect(); - Tensor::from_data(data, shape.clone(), device) - } - - pub fn zeros>( - shape: &Shape, - device: &Device, - ) -> Tensor { - Self::zeros_impl::(shape, device, false) - } - - pub fn zeros_like>(self) -> Tensor { - Self::zeros::(self.shape(), self.device()) - } - - pub(crate) fn ones_impl>( - shape: &Shape, - device: &Device, - is_variable: bool, - ) -> Tensor { - if device.is_cpu() { - let storage = Storage::ones::(shape, device); - let strides = Strides::from(shape); - let meta = StorageView::new(shape.clone(), T::dt(), strides); - Tensor::new_impl( - LazyOp::Const, - meta, - Some(storage), - device.clone(), - is_variable, - ) - } else { - Self::full_impl(shape, T::one(), device, is_variable) - } - } - - pub fn ones>( - shape: &Shape, - device: &Device, - ) -> Tensor { - Self::ones_impl::(shape, device, false) - } - - pub fn ones_like>(&self) -> Tensor { - Self::ones::(self.shape(), self.device()) - } - - fn trilu(self, upper: bool, k: Option) -> anyhow::Result { - let device = self.device.clone(); - let trilu = Trilu::new(self, upper, k); - let new_view = trilu.compute_view()?; - Ok(Tensor::lazy(LazyOp::Trilu(trilu), new_view, device, false)) - } - - pub fn triu(self, k: Option) -> anyhow::Result { - self.trilu(true, k) - } - - pub fn tril(self, k: Option) -> anyhow::Result { - self.trilu(false, k) - } - - /// Returns true if the data is stored in a C contiguous (aka row major) way. - pub fn is_contiguous(&self) -> bool { - self.view.is_contiguous() - } - - /// Returns a tensor that is in row major order. This is the same as the original tensor if it - /// was already contiguous, otherwise a copy is triggered. - pub fn contiguous(self) -> Tensor { - if self.is_contiguous() { - self.clone() - } else { - let storage_guard = self.storage(); - let storage = storage_guard.as_ref().unwrap(); - let cloned_storage = storage.deep_clone(self.device()).unwrap(); - Tensor::new_impl( - LazyOp::Const, - self.view.clone(), - Some(cloned_storage), - self.device.clone(), - false, - ) - } - } - - pub fn has_nan(&self) -> bool { - assert!(self.device().is_cpu()); - let self_nd = self.to_ndarray_view::(); - self_nd.iter().any(|&x| !x.is_finite()) - } - - /// Creates a new tensor from a chunk of data. - /// - /// The Tensor is instantly resolved. - /// If a non-CPU device is specified, the data will be copied to the device. - pub(crate) fn from_data_impl>( - data: U, - shape: Shape, - device: Device, - is_variable: bool, - ) -> Tensor { - let storage = Storage::from_slice(data.as_ref(), &shape, &device); - let strides = Strides::from(&shape); - let meta = StorageView::new(shape, T::dt(), strides); - Tensor::new_impl(LazyOp::Const, meta, Some(storage), device, is_variable) - } - - pub fn from_data>( - data: U, - shape: Shape, - device: Device, - ) -> Tensor { - Self::from_data_impl(data, shape, device, false) - } - - pub fn from_bytes( - data: &[u8], - dt: DType, - shape: Shape, - device: Device, - ) -> anyhow::Result { - let storage = Storage::from_bytes(data, dt.size_of(), &device); - let strides = Strides::from(&shape); - let meta = StorageView::new(shape, dt, strides); - Ok(Tensor::new_impl( - LazyOp::Const, - meta, - Some(storage), - device, - false, - )) - } - - /// Create a variable based on the values currently stored in a tensor. The storage is always - /// copied. - pub(crate) fn make_var(&self) -> anyhow::Result { - let storage_guard = self.storage(); - let storage = storage_guard.as_ref().unwrap(); - let cloned_storage = storage.deep_clone(self.device()).unwrap(); - Ok(Tensor::new_impl( - LazyOp::Const, - self.view.clone(), - Some(cloned_storage), - self.device.clone(), - true, - )) - } - - /// Returns a new tensor detached from the current graph, gradient are not propagated through - /// this new node. The storage of this tensor is shared with the initial tensor. - /// - /// If the tensor is already detached from the computation graph, the same tensor is returned. - pub fn detach(&self) -> Tensor { - match self.op { - LazyOp::Const if !self.is_variable => self.clone(), - _ => { - let storage_guard = self.storage(); - let storage = storage_guard.as_ref().map(|s| s.clone()); - Self::new( - LazyOp::Detach(Box::new(self.op().clone())), - self.view.clone(), - storage, - self.device.clone(), - ) - } - } - } - - pub fn copy(&self, dst: &Self) -> Tensor { - Tensor::new_impl( - LazyOp::Copy(TensorCopy { - src: self.clone(), - dst: dst.clone(), - }), - self.view.clone(), - None, - self.device.clone(), - false, - ) - } - - pub(crate) fn same_storage(&self, rhs: &Self) -> bool { - match (self.storage().as_ref(), rhs.storage().as_ref()) { - (Some(lhs), Some(rhs)) => std::ptr::eq(lhs, rhs), - _ => false, - } - } - - /// # Safety - /// - /// If the tensor has more than 1 reference, you die. - /// If the tensor has no storage, you die. - pub fn into_bytes(self) -> anyhow::Result> { - let mut inner = Arc::try_unwrap(self.inner).map_err(|_| { - anyhow::anyhow!("Cannot convert tensor into bytes with multiple references.") - })?; - let storage = unsafe { ManuallyDrop::take(&mut inner.storage) }; - let storage = Arc::try_unwrap(storage).unwrap().into_inner().unwrap(); - Ok(storage.into_bytes()) - } - - pub fn from_quantized>( - data: U, - dt: DType, - shape: Shape, - device: Device, - ) -> Tensor { - let storage = unsafe { Storage::from_quantized(data.as_ref(), &device) }; - let strides = Strides::from(&shape); - let meta = StorageView::new(shape, dt, strides); - Tensor::new_impl(LazyOp::Const, meta, Some(storage), device, false) - } - - pub fn from_disk( - reader: &mut R, - shape: Shape, - device: Device, - ) -> anyhow::Result { - let storage = Storage::from_disk::(reader, &shape, &device)?; - let strides = Strides::from(&shape); - let meta = StorageView::new(shape, T::dt(), strides); - Ok(Tensor::new_impl( - LazyOp::Const, - meta, - Some(storage), - device, - false, - )) - } - - pub fn item(&self) -> T { - assert!(self.is_scalar()); - ensure_resolved!(self); - let storage_guard = self.storage(); - let buffer = storage_guard.as_ref().unwrap().try_cpu().unwrap(); - buffer.to_slice::(self.shape())[0] - } - - /// # Bindings - /// - /// Only applicable to GPU tensors. - /// Generates the bind group entries required to bind the tensor to a kernel. - /// Quantized tensors may use multiple bind groups. - /// Unquantized tensors should only use a single bind group. - pub(crate) fn bind_group_entries(&self) -> RVec { - assert!(self.device().is_gpu()); - let storage_guard = self.storage(); - let storage = storage_guard - .as_ref() - .unwrap_or_else(|| panic!("Storage missing for {:?}", self.id())); - let gpu_buf = storage.try_gpu().unwrap(); - let handle = gpu_buf.inner().handle; - self.segments() - .iter() - .fold(rvec![], |mut entries, segment| { - let (offset, size) = (segment.offset, segment.size); - entries.push(BindGroupEntry { - handle, - offset, - size: Some(size), - }); - entries - }) - } - - /// # Segments - /// - /// In Ratchet, a tensor may be split into multiple segments. - /// This is due to our quantization scheme allowing multiple quantized components to be packed - /// and stored in a single tensor. - pub(crate) fn segments(&self) -> RVec { - self.dt().segments(self.shape().numel()) - } - - /// Converts the tensor into a 1D vector. - /// - /// The 1D vector contains the data from the tensor, as it was laid out in memory. - pub fn to_vec(&self) -> anyhow::Result> { - ensure_resolved!(self); - let storage_guard = self.storage(); - let buffer = storage_guard.as_ref().unwrap().try_cpu()?; - let slice = buffer.to_slice::(self.shape()); - Ok(slice.to_vec()) - } - - pub(crate) fn execution_order(&self) -> Vec<&Tensor> { - let mut done = BitVec::::repeat(false, self.id().0 + 1); - let mut pending = BitVec::::repeat(false, self.id().0 + 1); - let mut order = Vec::new(); - - let mut stack: Vec<(&Tensor, usize)> = vec![(self, 0)]; - while let Some((cur_t, cur_src)) = stack.pop() { - let all_deps_done = cur_src == cur_t.op().srcs().len(); - - if all_deps_done { - done.set(cur_t.id().0, true); - pending.set(cur_t.id().0, false); - order.push(cur_t); - continue; - } - - let (srcs_with_deps, srcs_without_deps): (Vec<_>, Vec<_>) = cur_t - .op() - .srcs() - .iter() - .partition(|s| s.op().srcs().is_empty()); - - let all_srcs = srcs_with_deps - .into_iter() - .chain(srcs_without_deps) - .collect::>(); - - let precursor: &Tensor = all_srcs[cur_src]; - let precursor_id = precursor.id().0; - - if done[precursor_id] { - stack.push((cur_t, cur_src + 1)); - } else if pending[precursor_id] { - panic!( - "Cycle detected whilst computing topological order: {:?}. Try plotting with feature `plotting`.", - precursor_id - ); - } else { - pending.set(precursor_id, true); - stack.push((cur_t, cur_src)); - stack.push((precursor, 0)); - } - } - - order - } - - pub fn cpu_apply(self, dst: Tensor) -> Option { - cpu::apply_operation(self.op().clone(), dst).ok() - } - - fn gpu_compile_key_for_op<'a>( - &'a self, - op: &'a LazyOp, - can_inplace: bool, - uniform: &mut CpuUniform, - ) -> Option> { - match op { - LazyOp::Binary(b) => b.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Cast(c) => c.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Matmul(m) => m.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Softmax(s) => s.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::RoPE(r) => r.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Alibi(a) => a.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Unary(u) => u.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Reindex(r) => r.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Concat(c) => c.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Norm(n) => n.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Affine(a) => a.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Cmp(c) => c.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Powf(p) => p.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::WhereCond(w) => w.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Conv(c) => c.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Select(i) => i.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::IndexWrite(i) => i.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::IndexAdd(i) => i.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::ScatterAdd(s) => s.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Trilu(t) => t.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Cache(c) => c.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Reduce(s) => s.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Detach(d) => self.gpu_compile_key_for_op(d, can_inplace, uniform), - LazyOp::Gather(g) => g.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::FillConstant(f) => f.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::FillRandn(f) => f.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Arange(a) => a.create_gpu_compile_key(self, can_inplace, uniform).ok(), - LazyOp::Copy(_) | LazyOp::View(_) | LazyOp::Const => None, - } - } - - pub fn gpu_compile_key( - &self, - can_inplace: bool, - uniform: &mut CpuUniform, - ) -> Option { - match self.op() { - LazyOp::Copy(c) => Some(GpuCompileKey::Copy(c.create_gpu_compile_key())), - _ => self - .gpu_compile_key_for_op(self.op(), can_inplace, uniform) - .map(GpuCompileKey::Compute), - } - } - - pub fn compile_gpu<'a>( - &'a self, - gpu_compile_key: &GpuCompileKey<'a>, - gpu_device: &'a WgpuDevice, - debug: bool, - ) -> Option { - match gpu_compile_key { - GpuCompileKey::Copy(_) => { - if let LazyOp::Copy(c) = self.op() { - c.compile_gpu().ok() - } else { - None - } - } - GpuCompileKey::Compute(compute_key) => { - compile_gpu_for_op(self.op(), compute_key, gpu_device, debug).map(Compiled::Compute) - } - } - } - - fn resolve_cpu(self) -> Result { - let mut tensor = self.clone(); - let execution_order = self.execution_order(); - - for t in execution_order.into_iter() { - log::debug!("Running: {:?}", t.op().name()); - assert!(t.device().is_cpu()); - if t.resolved() { - continue; - } - tensor = tensor.cpu_apply(t.clone()).unwrap(); - } - - Ok(tensor.clone()) - } - - /// Applies the pending graph to the tensor. - fn apply_pending_graph(&self) -> Result { - if self.resolved() { - return Ok(self.clone()); - } - - match self.device() { - Device::GPU(gpu_device) => { - gpu_device.sync_tensors_graph(vec![&self])?; - Ok(self.clone()) - } - Device::CPU => self.clone().resolve_cpu(), - } - } - - fn to_gpu(&self, dst_device: &Device) -> Result { - ensure_resolved!(self); - let storage_guard = self.storage(); - let cpu_buf = storage_guard - .as_ref() - .ok_or(TensorError::TransferError)? - .try_cpu()?; - let gpu_buf = cpu_buf.to_device(dst_device)?; - - let wgpu_device = dst_device.try_gpu()?; - Ok(Tensor::new_impl( - LazyOp::Const, - self.view.clone(), - Some(Storage::GPU(gpu_buf)), - Device::GPU(wgpu_device.clone()), - false, - )) - } - - pub fn deep_clone(&self) -> Tensor { - ensure_resolved!(self); - let storage_guard = self.storage(); - let storage = storage_guard.as_ref().unwrap(); - let cloned_storage = storage.deep_clone(self.device()).unwrap(); - Tensor::new_impl( - LazyOp::Const, - self.view.clone(), - Some(cloned_storage), - self.device.clone(), - false, - ) - } - - #[maybe_async] - async fn to_cpu(&self) -> Result { - ensure_resolved!(self); - - if self.device().is_cpu() { - return Ok(self.clone()); - } - let storage_guard = self.storage(); - let gpu_buf = storage_guard - .as_ref() - .ok_or(TensorError::TransferError)? - .try_gpu()?; - let cpu_buf = gpu_buf.to_cpu(&self.device).await?; - - Ok(Tensor::new_impl( - LazyOp::Const, - self.view.clone(), - Some(Storage::CPU(cpu_buf)), - Device::CPU, - false, - )) - } - - /// Transfers the tensor to the specified device. - /// - /// If the tensor is already on the specified device, it will be returned as-is, - /// and the underlying storage will not be copied. - /// If the tensor is on a different device, it will be copied to the specified device. - #[maybe_async] - pub async fn to(&self, device: &Device) -> Result { - match (self.device(), device) { - (Device::GPU(_), Device::CPU) => self.to_cpu().await, - (Device::CPU, Device::GPU(_)) => self.to_gpu(device), - _ => Ok(self.clone()), - } - } - - #[cfg(feature = "pyo3")] - pub fn to_py<'s, 'p: 's, T: TensorDType + numpy::Element>( - &'s self, - py: &'p pyo3::Python<'p>, - ) -> &'s PyArrayDyn { - use numpy::PyArray; - assert!( - self.device().is_cpu(), - "Cannot convert non-CPU tensor to numpy array" - ); - PyArray::from_owned_array(*py, self.deep_clone().into_ndarray::()) - } -} - -pub fn compile_gpu_for_op( - op: &LazyOp, - gpu_compile_key: &ComputeCompileKey, - gpu_device: &WgpuDevice, - debug: bool, -) -> Option { - match op { - LazyOp::Binary(b) => b.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Cast(c) => c.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Matmul(m) => m.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Softmax(s) => s.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::RoPE(r) => r.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Alibi(a) => a.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Unary(u) => u.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Reindex(r) => r.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Concat(c) => c.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Norm(n) => n.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Affine(a) => a.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Cmp(c) => c.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Powf(p) => p.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::WhereCond(w) => w.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Conv(c) => c.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Select(i) => i.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::IndexWrite(i) => i.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::IndexAdd(i) => i.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::ScatterAdd(s) => s.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Trilu(t) => t.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Cache(c) => c.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Reduce(s) => s.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Detach(d) => compile_gpu_for_op(d, gpu_compile_key, gpu_device, debug), - LazyOp::Gather(g) => g.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::FillConstant(f) => f.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::FillRandn(f) => f.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::Arange(a) => a.compile_gpu(gpu_compile_key, gpu_device, debug).ok(), - LazyOp::View(_) | LazyOp::Const => None, - LazyOp::Copy(_) => panic!("Copy should not have a gpu_compile_key"), - } -} - -#[cfg(feature = "pyo3")] -impl From<&PyArrayDyn> for Tensor { - fn from(array: &PyArrayDyn) -> Self { - Self::from(array.to_owned_array()) - } -} - -#[cfg(feature = "testing")] -#[derive(Default)] -struct CloseStats { - total_error: T, - max_abs_error: T, - max_abs_error_idxs: Option>, - element_count: usize, - fail_count: usize, - atol: T, - rtol: T, -} - -#[cfg(feature = "testing")] -impl CloseStats { - fn new(atol: T, rtol: T) -> Self { - Self { - atol, - rtol, - ..Default::default() - } - } - - fn update(&mut self, a: &T, b: &T, index: ndarray::IxDyn) { - let abs_diff = (*a - *b).abs(); - self.total_error = self.total_error + abs_diff; - self.element_count += 1; - - if abs_diff > self.max_abs_error { - self.max_abs_error = abs_diff; - self.max_abs_error_idxs = Some(index.slice().into()); - } - - if !self.is_close(a, b, abs_diff) { - self.fail_count += 1; - } - } - - fn avg_error(&self) -> T { - self.total_error / T::from(self.element_count).expect("Failed to convert") - } - - fn is_close(&self, a: &T, b: &T, abs_diff: T) -> bool { - (a.is_nan() && b.is_nan()) - || (a.is_infinite() && b.is_infinite() && a.signum() == b.signum()) - || abs_diff <= self.atol + self.rtol * b.abs() - } -} - -#[cfg(feature = "testing")] -impl Tensor { - pub fn read_npy(path: P, device: &Device, is_variable: bool) -> anyhow::Result - where - T: TensorDType + npyz::Deserialize, - P: AsRef, - { - Self::from_npy_bytes::(&std::fs::read(path)?, device, is_variable) - } - - pub fn write_npy(&self, path: P) -> anyhow::Result<()> - where - T: TensorDType + npyz::Serialize, - P: AsRef, - { - let mut out_buf = vec![]; - let shape = self - .shape() - .to_vec() - .iter() - .map(|x| *x as u64) - .collect::>(); - let mut writer = { - npyz::WriteOptions::new() - .dtype(self.dt().into()) - .shape(&shape) - .writer(&mut out_buf) - .begin_nd()? - }; - let ndarray = self.to_ndarray_view::(); - ndarray.iter().for_each(|x| { - writer.push(x).unwrap(); - }); - writer.finish()?; - std::fs::write(path, out_buf)?; - Ok(()) - } - - pub fn from_npy_bytes( - bytes: &[u8], - device: &Device, - is_variable: bool, - ) -> anyhow::Result { - let reader = npyz::NpyFile::new(bytes)?; - let shape = reader - .shape() - .iter() - .map(|&x| x as usize) - .collect::>() - .into(); - let data = reader.into_vec::()?; - Ok(Tensor::from_data_impl( - data, - shape, - device.clone(), - is_variable, - )) - } - - pub fn into_ndarray(self) -> ArrayD { - self.to_ndarray_view().into_owned() - } - - pub fn to_ndarray_view(&self) -> ArrayViewD { - ensure_resolved!(self); - assert!(self.device().is_cpu()); - assert!(self.dt() == T::dt()); - let shape = self.shape().to_vec(); - if self.num_bytes() != 0 { - let storage_guard = self.storage(); - let buffer = storage_guard.as_ref().unwrap().try_cpu().unwrap(); - let (ptr, _) = buffer.inner().into_raw_parts(); - unsafe { ArrayViewD::from_shape_ptr(shape, ptr as *const T) } - } else { - ArrayViewD::from_shape(shape, &[]).unwrap() - } - } - - pub fn all_close(&self, other: &Self, atol: T, rtol: T) -> anyhow::Result<()> - where - T: TensorDType + std::fmt::Display + num_traits::Float + Default, - { - if self.shape() != other.shape() { - anyhow::bail!("Shape mismatch {:?} != {:?}", self.shape(), other.shape()) - } - assert!( - self.dt() == other.dt(), - "DType mismatch {:?} != {:?}", - self.dt(), - other.dt() - ); - assert!( - self.dt() == T::dt(), - "DType mismatch {:?} != {:?}", - self.dt(), - T::dt() - ); - - let self_nd = self.to_ndarray_view::(); - let other_nd = other.to_ndarray_view::(); - - let mut stats = CloseStats::new(atol, rtol); - ndarray::indices_of(&self_nd).into_iter().for_each(|idx| { - let (a, b) = (self_nd[&idx], other_nd[&idx]); - stats.update(&a, &b, idx); - }); - - let idx_fmt = stats.max_abs_error_idxs.as_ref(); - if stats.fail_count > 0 { - anyhow::bail!( - "\x1b[1;31m{} samples not close \x1b[0m - AVGE={} MAE={} at {:?}", - stats.fail_count, - stats.avg_error(), - stats.max_abs_error, - idx_fmt - ); - } else { - println!( - "\x1b[1;32mAll close \x1b[0m - AVGE={} MAE={} at {:?}", - stats.avg_error(), - stats.max_abs_error, - idx_fmt - ); - Ok(()) - } - } -} - -impl From> for Tensor { - fn from(it: ArrayD) -> Self { - if it.as_slice().is_some() { - let layout = std::alloc::Layout::from_size_align( - it.len() * std::mem::size_of::(), - std::mem::align_of::(), - ) - .unwrap(); - let shape = it.shape().to_vec().into(); - let strides = Strides::from(&shape); - let vec = it.into_raw_vec().into_boxed_slice(); - let ptr = Box::into_raw(vec) as *mut u8; - - let raw_buf = RawCPUBuffer::new(ptr, layout); - let meta = StorageView::new(shape, T::dt(), strides); - Tensor::new_impl( - LazyOp::Const, - meta, - Some(Storage::CPU(CPUBuffer::new(raw_buf))), - Device::CPU, - false, - ) - } else { - panic!("Cannot convert numpy array with non-contiguous memory layout to tensor"); - } - } -} - -macro_rules! bin_trait { - ($trait:ident, $fn1:ident, $mul:expr, $add:expr) => { - impl std::ops::$trait for Tensor { - type Output = anyhow::Result; - - fn $fn1(self, rhs: Tensor) -> Self::Output { - Tensor::$fn1(self, rhs) - } - } - - impl std::ops::$trait for anyhow::Result { - type Output = anyhow::Result; - - fn $fn1(self, rhs: Tensor) -> Self::Output { - Tensor::$fn1(self?, rhs) - } - } - - impl std::ops::$trait> for Tensor { - type Output = anyhow::Result; - - fn $fn1(self, rhs: anyhow::Result) -> Self::Output { - Tensor::$fn1(self, rhs?) - } - } - - impl std::ops::$trait for Tensor { - type Output = anyhow::Result; - - fn $fn1(self, rhs: f32) -> Self::Output { - self.affine($mul(rhs), $add(rhs)) - } - } - }; -} - -bin_trait!(Add, add, |_| 1., |v| v); -bin_trait!(Sub, sub, |_| 1., |v: f32| -v); -bin_trait!(Mul, mul, |v| v, |_| 0.); -bin_trait!(Div, div, |v| 1. / v, |_| 0.); - -impl std::ops::Add for f32 { - type Output = anyhow::Result; - - fn add(self, rhs: Tensor) -> Self::Output { - rhs + self - } -} - -impl std::ops::Mul for f32 { - type Output = anyhow::Result; - - fn mul(self, rhs: Tensor) -> Self::Output { - rhs * self - } -} - -impl std::ops::Sub for f32 { - type Output = anyhow::Result; - - fn sub(self, rhs: Tensor) -> Self::Output { - rhs.affine(-1., self) - } -} - -impl std::ops::Div for f32 { - type Output = anyhow::Result; - - #[allow(clippy::suspicious_arithmetic_impl)] - fn div(self, rhs: Tensor) -> Self::Output { - rhs.recip()? * self - } -} - -impl safetensors::View for &Tensor { - fn dtype(&self) -> safetensors::Dtype { - match self.dt() { - DType::F32 => safetensors::Dtype::F32, - DType::U32 => safetensors::Dtype::U32, - DType::I32 => safetensors::Dtype::I32, - DType::F16 => safetensors::Dtype::F16, - DType::Q8_0F(_) | DType::Q8_0H(_) => safetensors::Dtype::U8, - DType::BF16 => safetensors::Dtype::BF16, - DType::Q4_KF(_) | DType::Q4_KH(_) => todo!(), - } - } - - fn shape(&self) -> &[usize] { - Tensor::shape(self).inner() - } - - fn data(&self) -> Cow<'_, [u8]> { - assert!( - self.device().is_cpu(), - "Cannot convert non-CPU tensor to safetensors" - ); - let storage_guard = self.storage(); - let buffer = storage_guard.as_ref().unwrap().try_cpu().unwrap(); - let (ptr, _) = buffer.inner().into_raw_parts(); - Cow::from(unsafe { std::slice::from_raw_parts(ptr, self.num_bytes()) }) - } - - fn data_len(&self) -> usize { - self.num_bytes() - } -} - -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use crate::{rvec, shape, Device, Tensor}; - - #[test] - fn has_nan_works() { - let device = Device::request_device(crate::DeviceRequest::GPU).unwrap(); - let rand = Tensor::randn::(0., 1., shape![1, 1500, 384], device.clone()); - let nans = Tensor::from_data(vec![f32::NAN; 1500 * 384], shape![1, 1500, 384], device); - - let bingo = Tensor::cat(rvec![rand, nans], 2).unwrap(); - - let result = bingo.to(&Device::CPU).unwrap(); - println!("RESULT: {:?}", result); - assert!(result.has_nan::()); - } -} diff --git a/crates/ratchet-core/src/variable.rs b/crates/ratchet-core/src/variable.rs deleted file mode 100644 index 856b4de3..00000000 --- a/crates/ratchet-core/src/variable.rs +++ /dev/null @@ -1,141 +0,0 @@ -// Taken from candle - -use core::panic; - -// Variables are wrappers around tensors that can be modified, they are typically used for holding -// weights and being modified by gradient descent. -// We do not expose a public way to create variables as this would break the invariant that the -// tensor within a variable is actually with `is_variable` set to `true`. -use crate::{Device, GPUBuffer, Inner, LazyOp, Shape, Storage, StorageView, Tensor, TensorDType}; -use anyhow::Result; -use num_traits::AsPrimitive; - -/// A variable is a wrapper around a tensor, however variables can have their content modified -/// whereas tensors are immutable. -#[derive(Clone, Debug)] -pub struct Var(Tensor); - -impl std::ops::Deref for Var { - type Target = Inner; - - fn deref(&self) -> &Self::Target { - self.0.as_ref() - } -} - -impl Var { - pub fn zeros>(shape: &Shape, device: &Device) -> Self { - let inner = Tensor::zeros_impl::(shape, device, true); - Self(inner) - } - - pub fn ones>(shape: &Shape, device: &Device) -> Self { - let inner = Tensor::ones_impl::(shape, device, true); - Self(inner) - } - - pub fn full>( - shape: &Shape, - value: T, - device: &Device, - ) -> Self { - let inner = Tensor::full_impl::(shape, value, device, true); - Self(inner) - } - - // Convert a tensor to a variable, if the tensor is already a variable then it is returned as is. - pub fn from_tensor(t: &Tensor) -> Result { - if t.is_variable() { - Ok(Self(t.clone())) - } else { - let inner = t.make_var()?; - Ok(Self(inner)) - } - } - - #[cfg(feature = "rand")] - pub fn randint( - low: T, - high: T, - shape: Shape, - device: Device, - ) -> Self { - let inner = Tensor::randint_impl(low, high, shape, device, true); - Self(inner) - } - - #[cfg(feature = "rand")] - pub fn rand( - lo: f32, - up: f32, - shape: Shape, - device: Device, - ) -> Self { - let inner = Tensor::rand_impl::(lo, up, shape, device, true); - Self(inner) - } - - #[cfg(feature = "rand")] - pub fn randn( - mean: f32, - std: f32, - shape: Shape, - device: Device, - ) -> Self { - let inner = Tensor::randn_impl::(mean, std, shape, device, true); - Self(inner) - } - - /// Creates a new tensor on the specified device using the content and shape of the input. - /// This is similar to `new` but the resulting tensor is a variable. - pub fn new(op: LazyOp, meta: StorageView, storage: Option, device: Device) -> Self { - let inner = Tensor::new_impl(op, meta, storage, device, true); - Self(inner) - } - - pub fn from_data>(data: U, shape: Shape, device: Device) -> Self { - let inner = Tensor::from_data_impl::(data, shape, device, true); - Self(inner) - } - - pub fn as_detached_tensor(&self) -> Tensor { - self.0.detach() - } - - pub fn as_tensor(&self) -> &Tensor { - &self.0 - } - - /// Consumes this `Var` and return the underlying tensor. - pub fn into_inner(self) -> Tensor { - self.0 - } - - /// Sets the content of the inner tensor, this does not require a mutable reference as inner - /// mutability is used. - pub fn set_sync(&self, src: Tensor) -> Result<()> { - if self.as_tensor().same_storage(&src) { - panic!("cannot set a variable to a tensor that is derived from its value"); - } - if self.as_tensor().shape() != src.shape() { - panic!( - "shape mismatch: {:?} != {:?} (target id: {:?}, source id: {:?})", - self.as_tensor().shape(), - src.shape(), - self.as_tensor().id(), - src.id() - ); - } - let dst = self.as_tensor(); - dst.update_storage(Storage::GPU(GPUBuffer { - inner: src.storage().as_ref().unwrap().try_gpu()?.inner.clone(), - alignment: dst.dt().size_of(), - cpu_size: Some(dst.num_bytes()), - })); - Ok(()) - } - - pub fn set(&self, src: Tensor) -> Tensor { - src.copy(self.as_tensor()) - } -} diff --git a/crates/ratchet-datasets/src/nlp/toy.rs b/crates/ratchet-datasets/src/nlp/toy.rs deleted file mode 100644 index caf2711c..00000000 --- a/crates/ratchet-datasets/src/nlp/toy.rs +++ /dev/null @@ -1,526 +0,0 @@ -use rand::rngs::StdRng; -use rand::Rng; -use rand::SeedableRng; -use ratchet::shape; -use ratchet::Device; -use ratchet::Tensor; - -pub trait ToyTask { - /// Generate a single example of the toy task. - fn generate_example(&mut self) -> String; -} - -pub struct ToyTaskIter { - task: T, - device: Device, -} - -impl ToyTaskIter { - pub fn new(task: T, device: Device) -> Self { - Self { task, device } - } -} - -impl Iterator for ToyTaskIter { - type Item = anyhow::Result<(Tensor, Tensor)>; - - fn next(&mut self) -> Option { - // Generate the next raw string from the toy task - let example_str = self.task.generate_example(); - let bytes: Vec<_> = example_str.to_string().bytes().map(|b| b as i32).collect(); - - // For an autoregressive input/target, we need at least 2 tokens - if bytes.len() < 2 { - return None; - } - - // If we have N tokens, input is [0..N-1], target is [1..N] - let seq_len = bytes.len() - 1; - let inputs = Tensor::from_data(&bytes[..seq_len], shape![seq_len], self.device.clone()); - let targets = Tensor::from_data(&bytes[1..], shape![seq_len], self.device.clone()); - - Some(Ok((inputs, targets))) - } -} - -/// "2-sum sequences" with optional seeding. -pub struct TwoSumTask { - max_num: u8, - seq_len: usize, - rng: StdRng, -} - -impl TwoSumTask { - pub fn new(max_num: u8, seq_len: usize, seed: Option) -> Self { - let rng = match seed { - Some(s) => StdRng::seed_from_u64(s), - None => StdRng::from_entropy(), - }; - Self { - max_num, - seq_len, - rng, - } - } -} - -impl ToyTask for TwoSumTask { - fn generate_example(&mut self) -> String { - let mut nums = Vec::with_capacity(self.seq_len); - for _ in 0..self.seq_len { - nums.push(self.rng.gen_range(0..self.max_num)); - } - - let i = self.rng.gen_range(0..self.seq_len); - let j = self.rng.gen_range(0..self.seq_len); - let target = nums[i] as u16 + nums[j] as u16; - - let nums_str: Vec = nums.iter().map(|&n| format!("{:02}", n)).collect(); - format!( - "{}:{:03}={:02},{:02}", - nums_str.join(","), - target, - nums[i], - nums[j] - ) - } -} - -/// For debugging more than anything, a sequence of zeros. -pub struct ZerosTask { - seq_len: usize, -} - -impl ZerosTask { - pub fn new(seq_len: usize) -> Self { - Self { seq_len } - } -} - -impl ToyTask for ZerosTask { - fn generate_example(&mut self) -> String { - "\x00".repeat(self.seq_len) - } -} - -/// A task where the model needs to sort numbers after a "sort:" token -pub struct SortTask { - seq_len: usize, - rng: StdRng, -} - -impl SortTask { - pub fn new(seq_len: usize, seed: Option) -> Self { - let rng = match seed { - Some(s) => StdRng::seed_from_u64(s), - None => StdRng::from_entropy(), - }; - Self { seq_len, rng } - } -} - -impl ToyTask for SortTask { - fn generate_example(&mut self) -> String { - let mut nums = Vec::with_capacity(self.seq_len); - for _ in 0..self.seq_len { - nums.push(self.rng.gen_range(0..10)); - } - - // Create a sorted copy for the target - let mut sorted_nums = nums.clone(); - sorted_nums.sort(); - - let nums_str: Vec = nums.iter().map(|&n| format!("{}", n)).collect(); - let sorted_str: Vec = sorted_nums.iter().map(|&n| format!("{}", n)).collect(); - - // format!("{}:{}", nums_str.join(","), sorted_str.join(",")) - format!("{}:{}", nums_str.concat(), sorted_str.concat()) - } -} - -/// A task where the model needs to add two numbers -pub struct AddTask { - max_num: usize, - rng: StdRng, -} - -impl AddTask { - pub fn new(max_num: usize, seed: Option) -> Self { - let rng = match seed { - Some(s) => StdRng::seed_from_u64(s), - None => StdRng::from_entropy(), - }; - Self { max_num, rng } - } -} - -impl ToyTask for AddTask { - fn generate_example(&mut self) -> String { - let a = self.rng.gen_range(0..self.max_num); - let b = self.rng.gen_range(0..self.max_num); - let sum = a + b; - - // We want fixed-width examples - let width = (self.max_num as f64).log10().floor() as usize + 1; - - format!("{:0width$}+{:0width$}={:0width$}", a, b, sum, width = width) - } -} - -/// A task where the model needs to count occurrences of a character in a string -pub struct CountTask { - len: usize, - max_char: char, - rng: StdRng, -} - -impl CountTask { - pub fn new(len: usize, max_char: char, seed: Option) -> Self { - let rng = match seed { - Some(s) => StdRng::seed_from_u64(s), - None => StdRng::from_entropy(), - }; - Self { len, max_char, rng } - } -} - -impl ToyTask for CountTask { - fn generate_example(&mut self) -> String { - // Generate a random string length between 2 and max_len - // Calculate range of valid characters (from 'a' to max_char inclusive) - let char_range = self.max_char as u8 - b'a' + 1; - - // Generate random letters up to max_char - let chars: Vec = (0..self.len) - .map(|_| (b'a' + self.rng.gen_range(0..char_range)) as char) - .collect(); - - // Pick a random character from the generated string to count - let target_char = chars[self.rng.gen_range(0..self.len)]; - - // Count occurrences - let count = chars.iter().filter(|&&c| c == target_char).count(); - - format!( - "{}:{}={}", - chars.iter().collect::(), - target_char, - count - ) - } -} - -/// A task where the model needs to identify when to slap in a card game -/// Rules: -/// - Slap on Jack (J) -/// - Slap on doubles (same card twice in a row) -/// - Slap on sandwiches (same card with one card between) -pub struct SlapjackTask { - seq_len: usize, - rng: StdRng, -} - -impl SlapjackTask { - pub fn new(seq_len: usize, seed: Option) -> Self { - let rng = match seed { - Some(s) => StdRng::seed_from_u64(s), - None => StdRng::from_entropy(), - }; - Self { seq_len, rng } - } - - fn generate_card(&mut self) -> char { - const CARDS: &[char] = &['A', '2', '3', '4', '5', '6', '7', '8', '9', 'J', 'Q', 'K']; - CARDS[self.rng.gen_range(0..CARDS.len())] - } -} - -impl ToyTask for SlapjackTask { - fn generate_example(&mut self) -> String { - let mut sequence = Vec::new(); - let mut n_cards = 0; - let mut n_slaps = 0; - - while sequence.len() - n_slaps < ((self.seq_len / 2) - 1) { - let card = self.generate_card(); - sequence.push(card); - - if !sequence.is_empty() { - let i = sequence.len() - 1; - - // Check slap conditions - let card_is_jack = card == 'J'; - let card_is_double = n_cards > 0 && card == sequence[i - 1]; - let card_is_sandwich = n_cards > 1 && card == sequence[i - 2]; - - let should_slap = card_is_jack || card_is_double || card_is_sandwich; - - if should_slap && sequence.len() < self.seq_len { - sequence.push('*'); - n_cards = 0; - n_slaps += 1; - continue; - } - } - - n_cards += 1; - } - - // Format as input=output where input is sequence without slaps - // and output is sequence with slaps - let input: String = sequence.iter().filter(|&&c| c != '*').collect(); - let output: String = sequence.iter().collect(); - - let mut result = format!("{}={}", input, output); - result.truncate(self.seq_len); - result - } -} - -/// A task where the model needs to perform modular addition modulo a prime number -pub struct ModAddTask { - prime: usize, - rng: StdRng, -} - -impl ModAddTask { - pub fn new(prime: usize, seed: Option) -> Self { - let rng = match seed { - Some(s) => StdRng::seed_from_u64(s), - None => StdRng::from_entropy(), - }; - Self { prime, rng } - } -} - -impl ToyTask for ModAddTask { - fn generate_example(&mut self) -> String { - let a = self.rng.gen_range(0..self.prime); - let b = self.rng.gen_range(0..self.prime); - let sum = (a as u16 + b as u16) % (self.prime as u16); - - // Get number of digits needed by finding floor(log10(prime)) + 1 - // We want fixed-width examples - let width = (self.prime as f64).log10().floor() as usize + 1; - - format!("{:0width$}+{:0width$}={:0width$}", a, b, sum, width = width) - } -} - -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use super::*; - - #[test] - fn test_twosum() { - // Create the seeded TwoSumTask iterator - let mut two_sum_seeded = TwoSumTask::new(100, 5, Some(42)); - - // Get exactly one example - let ex = two_sum_seeded.generate_example(); - - // e.g. "12,35,07,99,03:134=35,99" - - // 1) Split at the colon to separate "12,35,07,99,03" from "134=35,99" - let (nums_part, rest) = ex - .split_once(':') - .expect("Expected a colon ':' in the 2-sum format."); - - // 2) Split `rest` at '=' to separate "134" from "35,99" - let (target_str, final_vals) = rest - .split_once('=') - .expect("Expected an '=' in the 2-sum format."); - - // 3) Parse the list of initial numbers - let nums: Vec = nums_part - .split(',') - .map(|s| { - s.parse::() - .expect("Failed parsing initial number as u16") - }) - .collect(); - - // 4) Parse the target - let target: u16 = target_str - .parse() - .expect("Failed parsing the target sum as u16."); - - // 5) Parse the two "final" numbers - let final_nums: Vec = final_vals - .split(',') - .map(|s| { - s.parse::() - .expect("Failed parsing final numbers as u16") - }) - .collect(); - - // We expect exactly two final numbers - assert_eq!(final_nums.len(), 2, "Expected exactly 2 final numbers."); - let (val1, val2) = (final_nums[0], final_nums[1]); - - // Check sum - assert_eq!( - val1 + val2, - target, - "The two final numbers do not sum up to the target." - ); - - // Check that val1 and val2 are in the original list of numbers - assert!( - nums.contains(&val1) && nums.contains(&val2), - "Expected both final numbers to appear in the initial list." - ); - } - - #[test] - fn test_zeros() { - let mut zeros = ZerosTask::new(5); - let example = zeros.generate_example(); - assert_eq!(example, "\x00\x00\x00\x00\x00"); - assert_eq!(example.len(), 5); - } - - #[test] - fn test_sort() { - let mut sort_task = SortTask::new(100, Some(42)); - let example = sort_task.generate_example(); - - // Split at the colon to get the command and data - let (cmd, rest) = example - .split_once(':') - .expect("Expected a colon in sort format"); - assert_eq!(cmd, "sort", "Command should be 'sort'"); - - // Split at equals to get input and output - let (input_str, output_str) = rest.split_once('=').expect("Expected an equals sign"); - - // Parse input and output numbers - let input: Vec = input_str - .split(',') - .map(|s| s.parse::().expect("Failed parsing input number")) - .collect(); - let output: Vec = output_str - .split(',') - .map(|s| s.parse::().expect("Failed parsing output number")) - .collect(); - - // Verify output is sorted - let mut expected = input.clone(); - expected.sort(); - assert_eq!(output, expected, "Output should be sorted input"); - } - - #[test] - fn test_add() { - let mut add_task = AddTask::new(100, Some(42)); - let example = add_task.generate_example(); - - // Split at the plus to get first number - let (a_str, rest) = example.split_once('+').expect("Expected a plus sign"); - // Split at equals to get second number and result - let (b_str, result_str) = rest.split_once('=').expect("Expected an equals sign"); - - // Parse the numbers - let a: u8 = a_str.parse().expect("Failed parsing first number"); - let b: u8 = b_str.parse().expect("Failed parsing second number"); - let result: u8 = result_str.parse().expect("Failed parsing result"); - - // Verify the addition - assert_eq!(a + b, result, "Addition result should be correct"); - } - - #[test] - fn test_count() { - let mut count_task = CountTask::new(10, 'f', Some(42)); - let example = count_task.generate_example(); - - // Split at the colon to get the input string - let (input_str, rest) = example.split_once(':').expect("Expected a colon"); - // Split at equals to get target char and count - let (target_char_str, count_str) = rest.split_once('=').expect("Expected an equals sign"); - - // Parse the target character and count - let target_char = target_char_str - .chars() - .next() - .expect("Expected a target character"); - let count: usize = count_str.parse().expect("Failed parsing count"); - - // Verify the count - let actual_count = input_str.chars().filter(|&c| c == target_char).count(); - assert_eq!(count, actual_count, "Count should match actual occurrences"); - } - - #[test] - fn test_slapjack() { - let mut slapjack_task = SlapjackTask::new(18, Some(42)); - let example = slapjack_task.generate_example(); - - // Split at equals to get input and output sequences - let (input, output) = example.split_once('=').expect("Expected an equals sign"); - - // Verify that input sequence has no slaps - assert!( - !input.contains('*'), - "Input sequence should not contain slaps" - ); - - // Verify that output sequence contains slaps - let mut n_cards = 0; - let output_chars: Vec = output.chars().collect(); - - for i in 0..output_chars.len() { - let card = output_chars[i]; - if card == '*' { - // Verify that the slap was valid - let prev_card = output_chars[i - 1]; - let is_jack = prev_card == 'J'; - let is_double = n_cards > 0 && i > 0 && prev_card == output_chars[i - 1]; - let is_sandwich = - n_cards > 1 && i > 1 && prev_card != '*' && prev_card == output_chars[i - 2]; - - assert!( - is_jack || is_double || is_sandwich, - "Invalid slap at position {}", - i - ); - - n_cards = 0; - } else { - n_cards += 1; - } - } - } - - #[test] - fn test_mod_add() { - let mut mod_add_task = ModAddTask::new(7, Some(42)); - let example = mod_add_task.generate_example(); - - // Split at the plus to get first number - let (a_str, rest) = example.split_once('+').expect("Expected a plus sign"); - // Split at equals to get second number and result with modulus - let (b_str, result_with_mod) = rest.split_once('=').expect("Expected an equals sign"); - // Split at space to get result and modulus - let (result_str, modulus_part) = result_with_mod.split_once(' ').expect("Expected a space"); - // Extract modulus number - let modulus: u8 = modulus_part[5..modulus_part.len() - 1] - .parse() - .expect("Failed parsing modulus"); - - // Parse the numbers - let a: u8 = a_str.parse().expect("Failed parsing first number"); - let b: u8 = b_str.parse().expect("Failed parsing second number"); - let result: u8 = result_str.parse().expect("Failed parsing result"); - - // Verify the modular addition - assert_eq!( - (a as u16 + b as u16) % (modulus as u16), - result as u16, - "Modular addition result should be correct" - ); - assert!(a < modulus, "First number should be less than modulus"); - assert!(b < modulus, "Second number should be less than modulus"); - assert!(result < modulus, "Result should be less than modulus"); - } -} diff --git a/crates/ratchet-hub/Cargo.toml b/crates/ratchet-hub/Cargo.toml deleted file mode 100644 index 93cfad95..00000000 --- a/crates/ratchet-hub/Cargo.toml +++ /dev/null @@ -1,68 +0,0 @@ -[package] -name = "ratchet-hub" -version = "0.1.0" -edition = "2021" -license = "MIT" -description = "A web-first, cross-platform ML framework." -keywords = ["llm","wasm","transformers","webgpu","ml","machine-learning","deep-learning"] -repository = "https://github.com/FL33TW00D/ratchet" - -[lib] -crate-type = ["cdylib", "rlib"] - -[package.metadata.wasm-pack.profile.dev.wasm-bindgen] -debug-js-glue = true -demangle-name-section = true -dwarf-debug-info = true - -[package.metadata.wasm-pack.profile.release] -wasm-opt = ['-O3', '--enable-simd'] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -ratchet = { path = "../ratchet-core" } -ratchet-loader = { path = "../ratchet-loader" } -js-sys.workspace = true -thiserror.workspace = true -anyhow.workspace = true -log.workspace = true -wasm-bindgen.workspace = true -serde.workspace = true - -wasm-bindgen-futures = { workspace = true } -indexed_db_futures = { workspace = true } -serde-wasm-bindgen = { workspace = true } -serde_bytes = { workspace = true } -console_error_panic_hook = { workspace = true } -console_log = { workspace = true } -fern = { workspace = true } -chrono = { workspace = true } -gloo-net = { workspace = true, features = ["http"] } - -[dependencies.web-sys] -features = [ - 'console', - 'Headers', - 'Request', - 'RequestInit', - 'RequestMode', - 'Response', - 'ReadableStream', - 'ReadableStreamGetReaderOptions', - 'ReadableStreamReaderMode', - 'ReadableStreamDefaultReader', - 'Window', - 'Navigator', - 'StorageManager', - 'Cache', - 'CacheStorage', - 'IdbKeyRange', -] -workspace = true - -[target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = { version = "0.2.6", features = ["js"] } - -[dev-dependencies] -wasm-bindgen-test.workspace = true - diff --git a/crates/ratchet-hub/src/lib.rs b/crates/ratchet-hub/src/lib.rs deleted file mode 100644 index b71c3609..00000000 --- a/crates/ratchet-hub/src/lib.rs +++ /dev/null @@ -1,299 +0,0 @@ -#![cfg(target_arch = "wasm32")] -use gloo_net::http::Request; -use js_sys::{Object, Reflect, Uint8Array}; -use ratchet_loader::gguf::gguf::{self}; -use util::{js_error, js_to_js_error}; -use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::JsFuture; -use web_sys::RequestMode; - -mod util; - -#[wasm_bindgen(start)] -pub fn start() { - console_error_panic_hook::set_once(); - let logger = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{}[{}][{}] {}", - chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), - record.target(), - record.level(), - message - )) - }) - .level_for("tokenizers", log::LevelFilter::Off) - .level(log::LevelFilter::Debug) - .chain(fern::Output::call(console_log::log)) - .apply(); - match logger { - Ok(_) => log::info!("Logging initialized."), - Err(error) => eprintln!("Error initializing logging: {:?}", error), - } -} - -pub type ProgressBar = dyn Fn(u32); - -#[wasm_bindgen] -#[derive(Debug, Clone, Copy)] -pub enum RepoType { - /// This is a model, usually it consists of weight files and some configuration - Model, - /// This is a dataset, usually contains data within parquet files - Dataset, - /// This is a space, usually a demo showcashing a given model or dataset - Space, -} - -#[wasm_bindgen] -pub struct ApiBuilder { - endpoint: String, -} - -#[wasm_bindgen] -impl ApiBuilder { - /// Build an Api from a HF hub repository. - #[wasm_bindgen] - pub fn from_hf(repo_id: &str, ty: RepoType) -> Self { - Self { - endpoint: Self::endpoint(repo_id, ty), - } - } - - pub fn endpoint(repo_id: &str, ty: RepoType) -> String { - match ty { - RepoType::Model => { - format!("https://huggingface.co/{repo_id}/resolve/main") - } - RepoType::Dataset => { - format!("https://huggingface.co/datasets/{repo_id}/resolve/main") - } - RepoType::Space => { - format!("https://huggingface.co/spaces/{repo_id}/resolve/main") - } - } - } - - /// Build an Api from a HF hub repository at a specific revision. - #[wasm_bindgen] - pub fn from_hf_with_revision(repo_id: String, revision: String) -> Self { - Self { - endpoint: format!("https://huggingface.co/{repo_id}/resolve/{revision}"), - } - } - - /// Build an Api from a custom URL. - #[wasm_bindgen] - pub fn from_custom(endpoint: String) -> Self { - Self { endpoint } - } - - /// Build the Api. - #[wasm_bindgen] - pub fn build(&self) -> Api { - Api { - endpoint: self.endpoint.clone(), - } - } -} - -#[wasm_bindgen] -pub struct Api { - endpoint: String, -} - -#[wasm_bindgen] -impl Api { - #[wasm_bindgen] - pub async fn get_with_progress( - &self, - file_name: &str, - callback: &js_sys::Function, - ) -> Result { - self.get_internal(file_name, Some(callback)) - .await - .map_err(js_to_js_error) - } - - /// Get a file from the repository - #[wasm_bindgen] - pub async fn get(&self, file_name: &str) -> Result { - self.get_internal(file_name, None) - .await - .map_err(js_to_js_error) - } - - async fn get_internal( - &self, - file_name: &str, - progress_cb: Option<&js_sys::Function>, - ) -> Result { - let file_url = format!("{}/{}", self.endpoint, file_name); - log::debug!("Fetching file: {}", file_url); - - let response = Request::get(&file_url) - .mode(RequestMode::Cors) - .send() - .await - .unwrap(); - - if !response.ok() { - return Err( - js_error(format!("Failed to fetch file: {}", response.status()).as_str()).into(), - ); - } - - let content_len = response - .headers() - .get("Content-Length") - .ok_or(js_error("No content length"))? - .parse::() - .map_err(|p| js_error(format!("Failed to parse content length: {}", p).as_str()))?; - - let reader = response - .body() - .ok_or(js_error("No body"))? - .get_reader() - .dyn_into::()?; - - let mut recv_len = 0; - - let buf = Uint8Array::new_with_length(content_len); - while let Ok(result) = JsFuture::from(reader.read()).await?.dyn_into::() { - let done = Reflect::get(&result, &"done".into())? - .as_bool() - .unwrap_or(true); - if done { - break; - } - - if let Ok(chunk) = Reflect::get(&result, &"value".into()) { - let chunk_array: Uint8Array = chunk.dyn_into()?; - buf.set(&chunk_array, recv_len); - recv_len += chunk_array.length(); - let req_progress = (recv_len as f64 / content_len as f64) * 100.0; - if let Some(progress) = progress_cb.as_ref() { - progress.call1(&JsValue::NULL, &req_progress.into())?; - } - } - } - - Ok(buf) - } - - pub async fn fetch_gguf_header(&self, file_name: &str) -> Result { - //TODO: we should fetch bytes when needed for header - const MAX_HEADER_SIZE: u32 = 8_000_000; //We assume header is 8MB maximum - - let file_url = format!("{}/{}", self.endpoint, file_name); - log::debug!("Fetching file: {}", file_url); - let buf = Self::fetch_range(&self, file_name, 0, MAX_HEADER_SIZE as u64).await?; - let header = gguf::Header::read(&mut std::io::BufReader::new(std::io::Cursor::new( - buf.to_vec(), - ))) - .map_err(|e| js_error(format!("Failed to read header: {:?}", e).as_str()))?; - - Ok(serde_wasm_bindgen::to_value(&header).unwrap()) - } - - pub async fn fetch_range( - &self, - file_name: &str, - start: u64, - end: u64, - ) -> Result { - let file_url = format!("{}/{}", self.endpoint, file_name); - log::debug!("Fetching file: {}", file_url); - - let response = Request::get(&file_url) - .mode(RequestMode::Cors) - .header("Range", format!("bytes={}-{}", start, end).as_str()) - .send() - .await - .unwrap(); - - //206 is the status code for partial content - if response.status() != 206 { - return Err( - js_error(format!("Failed to fetch file: {}", response.status()).as_str()).into(), - ); - } - - let content_len = response - .headers() - .get("Content-Length") - .ok_or(js_error("No content length"))? - .parse::() - .map_err(|p| js_error(format!("Failed to parse content length: {}", p).as_str()))?; - - let reader = response - .body() - .ok_or(js_error("No body"))? - .get_reader() - .dyn_into::()?; - - let mut recv_len = 0; - let buf = Uint8Array::new_with_length(content_len); - while let Ok(result) = JsFuture::from(reader.read()).await?.dyn_into::() { - let done = Reflect::get(&result, &"done".into())? - .as_bool() - .unwrap_or(true); - if done { - break; - } - - if let Ok(chunk) = Reflect::get(&result, &"value".into()) { - let chunk_array: Uint8Array = chunk.dyn_into()?; - buf.set(&chunk_array, recv_len); - recv_len += chunk_array.length(); - } - } - log::info!("Successfully fetched range: {}-{}", start, end); - Ok(buf) - } -} - -#[cfg(all(test, target_arch = "wasm32"))] -mod tests { - use super::*; - use wasm_bindgen_test::*; - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - fn log_init() { - console_error_panic_hook::set_once(); - let logger = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{}[{}][{}] {}", - chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), - record.target(), - record.level(), - message - )) - }) - .level_for("tokenizers", log::LevelFilter::Off) - .level(log::LevelFilter::Info) - .chain(fern::Output::call(console_log::log)) - .apply(); - match logger { - Ok(_) => log::info!("Logging initialized."), - Err(error) => eprintln!("Error initializing logging: {:?}", error), - } - } - - #[wasm_bindgen_test] - async fn pull_from_hf() -> Result<(), JsValue> { - log_init(); - let model_repo = ApiBuilder::from_hf("jantxu/ratchet-test", RepoType::Model).build(); - let cb: Closure = Closure::new(|p| { - log::info!("Provided closure got progress: {}", p); - }); - let js_cb: &js_sys::Function = cb.as_ref().unchecked_ref(); - let model_bytes = model_repo - .get_with_progress("model.safetensors", js_cb) - .await?; - let length = model_bytes.length(); - assert!(length == 8388776, "Length was {length}"); - Ok(()) - } -} diff --git a/crates/ratchet-hub/src/util.rs b/crates/ratchet-hub/src/util.rs deleted file mode 100644 index 8a26c37d..00000000 --- a/crates/ratchet-hub/src/util.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![cfg(target_arch = "wasm32")] -use js_sys::JSON; -use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::JsFuture; -use web_sys::{Request, RequestInit, RequestMode, Response}; - -pub(crate) fn js_to_js_error(value: JsValue) -> JsError { - JsError::new( - JSON::stringify(&value) - .map(|js_string| { - js_string - .as_string() - .unwrap_or(String::from("An unknown error occurred.")) - }) - .unwrap_or(String::from("An unknown error occurred.")) - .as_str(), - ) -} - -pub(crate) fn js_error(message: &str) -> JsError { - JsError::new(message) -} - -pub(crate) async fn to_future(promise: js_sys::Promise) -> Result -where - T: JsCast, -{ - let result = JsFuture::from(promise).await?; - result.dyn_into::() -} - -pub(crate) async fn fetch(url: &str) -> Result { - let mut opts = RequestInit::new(); - opts.method("GET"); - opts.mode(RequestMode::Cors); - - let request = Request::new_with_str_and_init(url, &opts)?; - - let window = web_sys::window().unwrap(); - let promise = window.fetch_with_request(&request); - to_future(promise).await -} diff --git a/crates/ratchet-loader/Cargo.toml b/crates/ratchet-loader/Cargo.toml deleted file mode 100644 index 24d46bb5..00000000 --- a/crates/ratchet-loader/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "ratchet-loader" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -ratchet = { path = "../ratchet-core" } -half.workspace = true -byteorder.workspace = true -anyhow.workspace = true -bytemuck.workspace = true -thiserror.workspace = true -log.workspace = true -itertools = { workspace = true } -env_logger.workspace = true - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2.84" -serde = { workspace = true, features = ["derive"] } - -[dev-dependencies] -wasm-bindgen-test.workspace = true -tokio = { workspace = true, features = ["sync", "macros", "io-util", "rt", "time"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -hf-hub.workspace = true diff --git a/crates/ratchet-loader/src/error.rs b/crates/ratchet-loader/src/error.rs deleted file mode 100644 index 3a6b1330..00000000 --- a/crates/ratchet-loader/src/error.rs +++ /dev/null @@ -1,40 +0,0 @@ -/// Main library error type. -#[derive(thiserror::Error, Debug)] -pub enum Error { - /// I/O error. - #[error(transparent)] - Io(#[from] std::io::Error), - - /// Arbitrary errors wrapping. - #[error(transparent)] - Wrapped(Box), - - /// User generated error message, typically created via `bail!`. - #[error("{0}")] - Msg(String), -} - -impl Error { - pub fn wrap(err: impl std::error::Error + Send + Sync + 'static) -> Self { - Self::Wrapped(Box::new(err)) - } - - pub fn msg(err: impl std::error::Error + Send + Sync + 'static) -> Self { - Self::Msg(err.to_string()) - } -} - -#[macro_export] -macro_rules! bail { - ($msg:literal $(,)?) => { - return Err($crate::error::Error::Msg(format!($msg).into())) - }; - ($err:expr $(,)?) => { - return Err($crate::error::Error::Msg(format!($err).into())) - }; - ($fmt:expr, $($arg:tt)*) => { - return Err($crate::error::Error::Msg(format!($fmt, $($arg)*).into())) - }; -} - -pub type Result = std::result::Result; diff --git a/crates/ratchet-loader/src/gguf/dtype.rs b/crates/ratchet-loader/src/gguf/dtype.rs deleted file mode 100644 index ea867c76..00000000 --- a/crates/ratchet-loader/src/gguf/dtype.rs +++ /dev/null @@ -1,240 +0,0 @@ -#![allow(non_camel_case_types)] -use half::f16; -use ratchet::{DType, Device, Padding, Shape, Tensor}; -use ratchet::{Q4_KF, Q4_KH, Q8_0F, Q8_0H}; - -use crate::k_quants::*; - -pub const QK_K: usize = 256; -pub const K_SCALE_SIZE: usize = 12; - -pub const QK4_0: usize = 32; -pub const QK4_1: usize = 32; -pub const QK5_0: usize = 32; -pub const QK5_1: usize = 32; -pub const QK8_0: usize = 32; -pub const QK8_1: usize = 32; - -/// # GGUF Interoperability -/// -/// Supported GGUF types with the ability to be loaded into Ratchet. -pub trait GGUFInterop { - //Associated block type - type GGUF_TYPE: GGType; - //Number of elements in a block - const BLCK_NUMEL: usize; - //Size of the block in bytes - const TYPE_SIZE: usize = std::mem::size_of::(); - - //Given differences between GGUF and Ratchet, we need to "transcode" tensors from the raw GGUF - //data into a format consumable by Ratchet. - fn transcode( - data: &[Self::GGUF_TYPE], - n_blocks: usize, - shape: Shape, - device: &Device, - ) -> anyhow::Result; -} - -impl GGUFInterop for Q4_KF { - type GGUF_TYPE = BlockQ4K; - const BLCK_NUMEL: usize = QK_K; - - fn transcode( - data: &[Self::GGUF_TYPE], - n_blocks: usize, - shape: Shape, - device: &Device, - ) -> anyhow::Result { - //TODO: these should be uninit - let mut qs_bytes = Vec::with_capacity(n_blocks * QK_K); - let mut scales_bytes = Vec::with_capacity(n_blocks * K_SCALE_SIZE); - let mut dmin_bytes = Vec::with_capacity(n_blocks * 4); - let mut d_bytes = Vec::with_capacity(n_blocks * 4); - - for block in data { - dmin_bytes.extend_from_slice(&block.dmin.to_f32().to_le_bytes()); - d_bytes.extend_from_slice(&block.d.to_f32().to_le_bytes()); - let block_qs = block.qs; - qs_bytes.extend_from_slice(bytemuck::cast_slice(&block_qs)); - scales_bytes.extend_from_slice(bytemuck::cast_slice(&block.scales)); - } - - let _ = qs_bytes.pad_to_offset(); - let _ = scales_bytes.pad_to_offset(); - let _ = dmin_bytes.pad_to_offset(); - let _ = d_bytes.pad_to_offset(); - - qs_bytes.append(&mut scales_bytes); - qs_bytes.append(&mut dmin_bytes); - qs_bytes.append(&mut d_bytes); - let casted = bytemuck::cast_slice::(&qs_bytes); - unsafe { - Ok(Tensor::from_quantized::( - casted, - DType::Q4_KF(Q4_KF::default()), - shape, - device.clone(), - )) - } - } -} - -impl GGUFInterop for Q4_KH { - type GGUF_TYPE = BlockQ4K; - const BLCK_NUMEL: usize = QK_K; - - fn transcode( - data: &[Self::GGUF_TYPE], - n_blocks: usize, - shape: Shape, - device: &Device, - ) -> anyhow::Result { - //TODO: these should be uninit - let mut qs_bytes = Vec::with_capacity(n_blocks * QK_K); - let mut scales_bytes = Vec::with_capacity(n_blocks * K_SCALE_SIZE); - let mut dmin_bytes = Vec::with_capacity(n_blocks * 2); - let mut d_bytes = Vec::with_capacity(n_blocks * 2); - - for block in data { - dmin_bytes.extend_from_slice(&block.dmin.to_le_bytes()); - d_bytes.extend_from_slice(&block.d.to_le_bytes()); - let block_qs = block.qs; - qs_bytes.extend_from_slice(bytemuck::cast_slice(&block_qs)); - scales_bytes.extend_from_slice(bytemuck::cast_slice(&block.scales)); - } - - let _ = qs_bytes.pad_to_offset(); - let _ = scales_bytes.pad_to_offset(); - let _ = dmin_bytes.pad_to_offset(); - let _ = d_bytes.pad_to_offset(); - - qs_bytes.append(&mut scales_bytes); - qs_bytes.append(&mut dmin_bytes); - qs_bytes.append(&mut d_bytes); - let casted = bytemuck::cast_slice::(&qs_bytes); - unsafe { - Ok(Tensor::from_quantized::( - casted, - DType::Q4_KH(Q4_KH::default()), - shape, - device.clone(), - )) - } - } -} - -//TODO: code reuse -impl GGUFInterop for Q8_0F { - type GGUF_TYPE = BlockQ8_0; - const BLCK_NUMEL: usize = QK8_0; - - fn transcode( - data: &[Self::GGUF_TYPE], - n_blocks: usize, - shape: Shape, - device: &Device, - ) -> anyhow::Result { - //TODO: these should be uninit - let mut qs_bytes = Vec::with_capacity(n_blocks * QK8_0); - let mut ds_bytes = Vec::with_capacity(n_blocks * 4); - - for block in data { - ds_bytes.extend_from_slice(&block.d.to_f32().to_le_bytes()); - let block_qs = block.qs; - qs_bytes.extend_from_slice(bytemuck::cast_slice(&block_qs)); - } - - let _ = ds_bytes.pad_to_offset(); - let _ = qs_bytes.pad_to_offset(); - - qs_bytes.append(&mut ds_bytes); - let casted = bytemuck::cast_slice::(&qs_bytes); - unsafe { - Ok(Tensor::from_quantized::( - casted, - DType::Q8_0F(Q8_0F::default()), - shape, - device.clone(), - )) - } - } -} - -impl GGUFInterop for Q8_0H { - type GGUF_TYPE = BlockQ8_0; - const BLCK_NUMEL: usize = QK8_0; - - fn transcode( - data: &[Self::GGUF_TYPE], - n_blocks: usize, - shape: Shape, - device: &Device, - ) -> anyhow::Result { - //TODO: these should be uninit - let mut qs_bytes = Vec::with_capacity(n_blocks * QK8_0); - let mut ds_bytes = Vec::with_capacity(n_blocks * 2); - - for block in data { - ds_bytes.extend_from_slice(&block.d.to_le_bytes()); - let block_qs = block.qs; - qs_bytes.extend_from_slice(bytemuck::cast_slice(&block_qs)); - } - - let _ = ds_bytes.pad_to_offset(); - let _ = qs_bytes.pad_to_offset(); - - qs_bytes.append(&mut ds_bytes); - let casted = bytemuck::cast_slice::(&qs_bytes); - unsafe { - Ok(Tensor::from_quantized::( - casted, - DType::Q8_0H(Q8_0H::default()), - shape, - device.clone(), - )) - } - } -} - -impl GGUFInterop for f32 { - type GGUF_TYPE = f32; - const BLCK_NUMEL: usize = 1; - - fn transcode( - data: &[Self::GGUF_TYPE], - _n_blocks: usize, - shape: Shape, - device: &Device, - ) -> anyhow::Result { - Ok(Tensor::from_data(data, shape, device.clone())) - } -} - -impl GGUFInterop for f16 { - type GGUF_TYPE = f16; - const BLCK_NUMEL: usize = 1; - - fn transcode( - data: &[Self::GGUF_TYPE], - _n_blocks: usize, - shape: Shape, - device: &Device, - ) -> anyhow::Result { - match device { - Device::CPU => { - log::warn!("Creating CPU tensor from F16 data, may be unsupported"); - Ok(Tensor::from_data(data, shape, device.clone())) - } - Device::GPU(gpu) => { - if gpu.compute_features().SHADER_F16 { - Ok(Tensor::from_data(data, shape, device.clone())) - } else { - log::warn!("Transcoding F16 -> F32 for GPU tensor, as this device does not support SHADER_F16"); - let f32_data = data.iter().map(|f| f.to_f32()).collect::>(); - Ok(Tensor::from_data(f32_data, shape, device.clone())) - } - } - } - } -} diff --git a/crates/ratchet-loader/src/gguf/gguf.rs b/crates/ratchet-loader/src/gguf/gguf.rs deleted file mode 100644 index e8e29888..00000000 --- a/crates/ratchet-loader/src/gguf/gguf.rs +++ /dev/null @@ -1,510 +0,0 @@ -//! Support for the GGUF file format. -//! Spec: https://github.com/philpax/ggml/blob/gguf-spec/docs/gguf.md -//! Adapted from https://github.com/huggingface/candle/blob/5ebcfeaf0f5af69bb2f74385e8d6b020d4a3b8df/candle-core/src/quantized/gguf_file.rs - -use super::dtype::GGUFInterop; -use crate::{error::Result, GgmlDType}; - -use byteorder::{LittleEndian, ReadBytesExt}; -use ratchet::{Device, Shape, Tensor, Q4_KF, Q4_KH, Q8_0F, Q8_0H}; -use std::collections::HashMap; -use std::ops::Range; - -pub const DEFAULT_ALIGNMENT: u64 = 32; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Magic { - Gguf, -} - -impl TryFrom for Magic { - type Error = crate::error::Error; - fn try_from(value: u32) -> Result { - let magic = match value { - 0x46554747 | 0x47475546 => Self::Gguf, - _ => crate::bail!("unknown magic 0x{value:08x}"), - }; - Ok(magic) - } -} - -#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VersionedMagic { - GgufV1, - GgufV2, - GgufV3, -} - -impl VersionedMagic { - fn read(reader: &mut R) -> Result { - let magic = reader.read_u32::()?; - let magic = Magic::try_from(magic)?; - let version = reader.read_u32::()?; - let versioned_magic = match (magic, version) { - (Magic::Gguf, 1) => Self::GgufV1, - (Magic::Gguf, 2) => Self::GgufV2, - (Magic::Gguf, 3) => Self::GgufV3, - _ => crate::bail!("gguf: unsupported magic/version {magic:?}/{version}"), - }; - Ok(versioned_magic) - } -} - -#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] -pub struct TensorInfo { - pub ggml_dtype: GgmlDType, - pub shape: Shape, - pub offset: u64, -} - -impl TensorInfo { - pub fn read( - &self, - reader: &mut R, - tensor_data_offset: u64, - device: &Device, - ) -> anyhow::Result { - let tensor_elems = self.shape.numel(); - let block_numel = self.ggml_dtype.block_numel(); - if tensor_elems % block_numel != 0 { - anyhow::bail!( - "the number of elements {tensor_elems} is not divisible by the block size {block_numel}" - ) - } - - let tensor_blocks = tensor_elems / block_numel; - let size_in_bytes = tensor_blocks * self.ggml_dtype.type_size(); - - let mut raw_data = vec![0u8; size_in_bytes]; //TODO: MaybeUninit - reader.seek(std::io::SeekFrom::Start(tensor_data_offset + self.offset))?; - reader.read_exact(&mut raw_data)?; - ratchet_from_gguf(self.ggml_dtype, &raw_data, self.shape.clone(), device) - } - - pub fn byte_range(&self, tensor_data_offset: u64) -> Range { - let size_in_bytes = self.size_in_bytes(); - let start = tensor_data_offset + self.offset; - let end = start + size_in_bytes as u64; - start..end - } - - pub fn size_in_bytes(&self) -> usize { - let tensor_elems = self.shape.numel(); - let block_numel = self.ggml_dtype.block_numel(); - let tensor_blocks = tensor_elems / block_numel; - tensor_blocks * self.ggml_dtype.type_size() - } -} - -fn from_raw_data( - raw_data: &[u8], - size_in_bytes: usize, - shape: Shape, - device: &Device, -) -> anyhow::Result { - let raw_data_ptr = raw_data.as_ptr(); - let n_blocks = size_in_bytes / std::mem::size_of::(); - let data = unsafe { std::slice::from_raw_parts(raw_data_ptr as *const I::GGUF_TYPE, n_blocks) }; - I::transcode(data, n_blocks, shape, device) -} - -pub fn ratchet_from_gguf( - ggml_dtype: GgmlDType, - raw_data: &[u8], - shape: Shape, - device: &Device, -) -> anyhow::Result { - let tensor_elems = shape.numel(); - let block_size = ggml_dtype.block_numel(); - let size_in_bytes = tensor_elems / block_size * ggml_dtype.type_size(); - if tensor_elems % block_size != 0 { - anyhow::bail!( - "the number of elements {tensor_elems} is not divisible by the block size {block_size}" - ) - } - match ggml_dtype { - GgmlDType::F32 => from_raw_data::(raw_data, size_in_bytes, shape, device), - GgmlDType::F16 => from_raw_data::(raw_data, size_in_bytes, shape, device), - GgmlDType::Q8_0 => match device { - Device::GPU(gpu) => { - if gpu.compute_features().SHADER_F16 { - log::info!("Device supports F16, loading Q8_0 with F16"); - from_raw_data::(raw_data, size_in_bytes, shape, device) - } else { - log::info!("Device does not support F16, loading Q8_0 with F32"); - from_raw_data::(raw_data, size_in_bytes, shape, device) - } - } - _ => panic!("Loading from GGUF -> Ratchet using CPU device, no way of knowing if F16 is supported"), - }, - GgmlDType::Q4K => match device { - Device::GPU(gpu) => { - if gpu.compute_features().SHADER_F16 { - log::info!("Device supports F16, loading Q4K with F16"); - from_raw_data::(raw_data, size_in_bytes, shape, device) - } else { - log::info!("Device does not support F16, loading Q4K with F32"); - from_raw_data::(raw_data, size_in_bytes, shape, device) - } - } - _ => panic!("Loading from GGUF -> Ratchet using CPU device, no way of knowing if F16 is supported"), - }, - _ => anyhow::bail!("unsupported ggml dtype {ggml_dtype:?}"), - } -} - -#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug)] -pub struct Metadata(HashMap); - -impl Metadata { - pub fn get(&self, key: &str) -> anyhow::Result<&Value> { - self.0 - .get(key) - .ok_or_else(|| anyhow::anyhow!("missing key {key}")) - } -} - -#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug)] -pub struct Header { - pub magic: VersionedMagic, - pub metadata: Metadata, - pub tensor_infos: HashMap, - pub tensor_data_offset: u64, -} - -fn read_string(reader: &mut R, magic: &VersionedMagic) -> Result { - let len = match magic { - VersionedMagic::GgufV1 => reader.read_u32::()? as usize, - VersionedMagic::GgufV2 | VersionedMagic::GgufV3 => { - reader.read_u64::()? as usize - } - }; - let mut v = vec![0u8; len]; - reader.read_exact(&mut v)?; - // GGUF strings are supposed to be non-null terminated but in practice this happens. - while let Some(0) = v.last() { - v.pop(); - } - // GGUF strings are utf8 encoded but there are cases that don't seem to be valid. - Ok(String::from_utf8_lossy(&v).into_owned()) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ValueType { - // The value is a 8-bit unsigned integer. - U8, - // The value is a 8-bit signed integer. - I8, - // The value is a 16-bit unsigned little-endian integer. - U16, - // The value is a 16-bit signed little-endian integer. - I16, - // The value is a 32-bit unsigned little-endian integer. - U32, - // The value is a 32-bit signed little-endian integer. - I32, - // The value is a 64-bit unsigned little-endian integer. - U64, - // The value is a 64-bit signed little-endian integer. - I64, - // The value is a 32-bit IEEE754 floating point number. - F32, - // The value is a 64-bit IEEE754 floating point number. - F64, - // The value is a boolean. - // 1-byte value where 0 is false and 1 is true. - // Anything else is invalid, and should be treated as either the model being invalid or the reader being buggy. - Bool, - // The value is a UTF-8 non-null-terminated string, with length prepended. - String, - // The value is an array of other values, with the length and type prepended. - // Arrays can be nested, and the length of the array is the number of elements in the array, not the number of bytes. - Array, -} - -#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] -pub enum Value { - U8(u8), - I8(i8), - U16(u16), - I16(i16), - U32(u32), - I32(i32), - U64(u64), - I64(i64), - F32(f32), - F64(f64), - Bool(bool), - String(String), - Array(Vec), -} - -impl Value { - pub fn value_type(&self) -> ValueType { - match self { - Self::U8(_) => ValueType::U8, - Self::I8(_) => ValueType::I8, - Self::U16(_) => ValueType::U16, - Self::I16(_) => ValueType::I16, - Self::U32(_) => ValueType::U32, - Self::I32(_) => ValueType::I32, - Self::U64(_) => ValueType::U64, - Self::I64(_) => ValueType::I64, - Self::F32(_) => ValueType::F32, - Self::F64(_) => ValueType::F64, - Self::Bool(_) => ValueType::Bool, - Self::String(_) => ValueType::String, - Self::Array(_) => ValueType::Array, - } - } - - pub fn to_u8(&self) -> Result { - match self { - Self::U8(v) => Ok(*v), - v => crate::bail!("not a u8 {v:?}"), - } - } - - pub fn to_i8(&self) -> Result { - match self { - Self::I8(v) => Ok(*v), - v => crate::bail!("not a i8 {v:?}"), - } - } - - pub fn to_u16(&self) -> Result { - match self { - Self::U16(v) => Ok(*v), - v => crate::bail!("not a u16 {v:?}"), - } - } - - pub fn to_i16(&self) -> Result { - match self { - Self::I16(v) => Ok(*v), - v => crate::bail!("not a i16 {v:?}"), - } - } - - pub fn to_u32(&self) -> Result { - match self { - Self::U32(v) => Ok(*v), - v => crate::bail!("not a u32 {v:?}"), - } - } - - pub fn to_i32(&self) -> Result { - match self { - Self::I32(v) => Ok(*v), - v => crate::bail!("not a i32 {v:?}"), - } - } - - pub fn to_u64(&self) -> Result { - match self { - Self::U64(v) => Ok(*v), - v => crate::bail!("not a u64 {v:?}"), - } - } - - pub fn to_i64(&self) -> Result { - match self { - Self::I64(v) => Ok(*v), - v => crate::bail!("not a i64 {v:?}"), - } - } - - pub fn to_f32(&self) -> Result { - match self { - Self::F32(v) => Ok(*v), - v => crate::bail!("not a f32 {v:?}"), - } - } - - pub fn to_f64(&self) -> Result { - match self { - Self::F64(v) => Ok(*v), - v => crate::bail!("not a f64 {v:?}"), - } - } - - pub fn to_bool(&self) -> Result { - match self { - Self::Bool(v) => Ok(*v), - v => crate::bail!("not a bool {v:?}"), - } - } - - pub fn to_vec(&self) -> Result<&Vec> { - match self { - Self::Array(v) => Ok(v), - v => crate::bail!("not a vec {v:?}"), - } - } - - pub fn to_string(&self) -> Result<&String> { - match self { - Self::String(v) => Ok(v), - v => crate::bail!("not a string {v:?}"), - } - } - - fn read( - reader: &mut R, - value_type: ValueType, - magic: &VersionedMagic, - ) -> Result { - let v = match value_type { - ValueType::U8 => Self::U8(reader.read_u8()?), - ValueType::I8 => Self::I8(reader.read_i8()?), - ValueType::U16 => Self::U16(reader.read_u16::()?), - ValueType::I16 => Self::I16(reader.read_i16::()?), - ValueType::U32 => Self::U32(reader.read_u32::()?), - ValueType::I32 => Self::I32(reader.read_i32::()?), - ValueType::U64 => Self::U64(reader.read_u64::()?), - ValueType::I64 => Self::I64(reader.read_i64::()?), - ValueType::F32 => Self::F32(reader.read_f32::()?), - ValueType::F64 => Self::F64(reader.read_f64::()?), - ValueType::Bool => match reader.read_u8()? { - 0 => Self::Bool(false), - 1 => Self::Bool(true), - b => crate::bail!("unexpected bool value {b}"), - }, - ValueType::String => Self::String(read_string(reader, magic)?), - ValueType::Array => { - let value_type = reader.read_u32::()?; - let value_type = ValueType::from_u32(value_type)?; - let len = match magic { - VersionedMagic::GgufV1 => reader.read_u32::()? as usize, - VersionedMagic::GgufV2 | VersionedMagic::GgufV3 => { - reader.read_u64::()? as usize - } - }; - let mut vs = Vec::with_capacity(len); - for _ in 0..len { - vs.push(Value::read(reader, value_type, magic)?) - } - Self::Array(vs) - } - }; - Ok(v) - } -} - -impl ValueType { - fn from_u32(v: u32) -> Result { - let v = match v { - 0 => Self::U8, - 1 => Self::I8, - 2 => Self::U16, - 3 => Self::I16, - 4 => Self::U32, - 5 => Self::I32, - 6 => Self::F32, - 7 => Self::Bool, - 8 => Self::String, - 9 => Self::Array, - 10 => Self::U64, - 11 => Self::I64, - 12 => Self::F64, - v => crate::bail!("unrecognized value-type {v:#08x}"), - }; - Ok(v) - } -} - -impl Header { - pub fn read(reader: &mut R) -> Result { - let magic = VersionedMagic::read(reader)?; - - let tensor_count = match magic { - VersionedMagic::GgufV1 => reader.read_u32::()? as usize, - VersionedMagic::GgufV2 | VersionedMagic::GgufV3 => { - reader.read_u64::()? as usize - } - }; - let metadata_kv_count = match magic { - VersionedMagic::GgufV1 => reader.read_u32::()? as usize, - VersionedMagic::GgufV2 | VersionedMagic::GgufV3 => { - reader.read_u64::()? as usize - } - }; - - let mut metadata = HashMap::new(); - for _idx in 0..metadata_kv_count { - let key = read_string(reader, &magic)?; - let value_type = reader.read_u32::()?; - let value_type = ValueType::from_u32(value_type)?; - let value = Value::read(reader, value_type, &magic)?; - metadata.insert(key, value); - } - let mut tensor_infos = HashMap::new(); - for _idx in 0..tensor_count { - let tensor_name = read_string(reader, &magic)?; - let n_dimensions = reader.read_u32::()?; - - let mut dimensions: Vec = match magic { - VersionedMagic::GgufV1 => { - let mut dimensions = vec![0; n_dimensions as usize]; - reader.read_u32_into::(&mut dimensions)?; - dimensions.into_iter().map(|c| c as usize).collect() - } - VersionedMagic::GgufV2 | VersionedMagic::GgufV3 => { - let mut dimensions = vec![0; n_dimensions as usize]; - reader.read_u64_into::(&mut dimensions)?; - dimensions.into_iter().map(|c| c as usize).collect() - } - }; - - dimensions.reverse(); - let ggml_dtype = GgmlDType::try_from(reader.read_u32::()?).unwrap(); - let offset = reader.read_u64::()?; - tensor_infos.insert( - tensor_name, - TensorInfo { - shape: Shape::from(dimensions), - offset, - ggml_dtype, - }, - ); - } - let position = reader.stream_position()?; - let alignment = match metadata.get("general.alignment") { - Some(Value::U8(v)) => *v as u64, - Some(Value::U16(v)) => *v as u64, - Some(Value::U32(v)) => *v as u64, - Some(Value::I8(v)) if *v >= 0 => *v as u64, - Some(Value::I16(v)) if *v >= 0 => *v as u64, - Some(Value::I32(v)) if *v >= 0 => *v as u64, - _ => DEFAULT_ALIGNMENT, - }; - let tensor_data_offset = (position + alignment - 1) / alignment * alignment; - Ok(Self { - magic, - metadata: Metadata(metadata), - tensor_infos, - tensor_data_offset, - }) - } - - /// # Tensor - /// Load the GGUF tensor from the reader into memory. - pub fn tensor( - &self, - reader: &mut R, - name: &str, - device: &Device, - ) -> anyhow::Result { - let tensor_info = match self.tensor_infos.get(name) { - Some(tensor_info) => tensor_info, - None => anyhow::bail!("cannot find tensor info for {name}"), - }; - log::info!("Loading tensor {tensor_info:#?}"); - tensor_info.read(reader, self.tensor_data_offset, device) - } -} diff --git a/crates/ratchet-loader/src/gguf/mod.rs b/crates/ratchet-loader/src/gguf/mod.rs deleted file mode 100644 index 9760e1ce..00000000 --- a/crates/ratchet-loader/src/gguf/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod dtype; -pub mod gguf; -pub mod utils; diff --git a/crates/ratchet-loader/src/gguf/utils.rs b/crates/ratchet-loader/src/gguf/utils.rs deleted file mode 100644 index bdd2fd70..00000000 --- a/crates/ratchet-loader/src/gguf/utils.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Adapted from https://github.com/huggingface/candle/blob/fc67d878bb4a25cbeba361d0a31290f14beb9344/candle-core/src/quantized/utils.rs - -use half::f16; - -use crate::error::Result; - -pub trait ReadHalf { - fn read_f16(&mut self) -> Result; -} - -impl ReadHalf for R { - fn read_f16(&mut self) -> Result { - let mut d = [0u8; 2]; - self.read_exact(&mut d)?; - let f16_value = half::f16::from_le_bytes(d); - Ok(f16_value) - } -} - -pub trait WriteHalf { - fn write_f16(&mut self, input: f16) -> Result; -} - -impl WriteHalf for W { - fn write_f16(&mut self, input: f16) -> Result { - let bytes = input.to_le_bytes(); - let num_written = self.write(&bytes)?; - Ok(num_written) - } -} - -pub trait ReadInto { - fn read_u8s_into(&mut self, other: &mut Other, length: usize) -> Result<()>; -} - -impl ReadInto for R { - fn read_u8s_into(&mut self, other: &mut Other, length: usize) -> Result<()> { - let mut temp = vec![0u8; length]; - self.read_exact(&mut temp)?; - other.write_all(&temp)?; - Ok(()) - } -} - -pub trait ReadLen { - fn read_len_bytes(&mut self, length: usize) -> Result>; -} - -impl ReadLen for R { - fn read_len_bytes(&mut self, length: usize) -> Result> { - let mut temp = vec![0u8; length]; - self.read_exact(&mut temp)?; - Ok(temp) - } -} diff --git a/crates/ratchet-loader/src/k_quants.rs b/crates/ratchet-loader/src/k_quants.rs deleted file mode 100644 index 7485e83e..00000000 --- a/crates/ratchet-loader/src/k_quants.rs +++ /dev/null @@ -1,164 +0,0 @@ -// Credit: https://github.com/huggingface/candle/blob/main/candle-core/src/quantized/k_quants.rs -use half::f16; - -use crate::GgmlDType; -// Default to QK_K 256 rather than 64. -pub const QK_K: usize = 256; -pub const K_SCALE_SIZE: usize = 12; - -pub const QK4_0: usize = 32; -pub const QK4_1: usize = 32; -pub const QK5_0: usize = 32; -pub const QK5_1: usize = 32; -pub const QK8_0: usize = 32; -pub const QK8_1: usize = 32; - -pub trait GGType: Sized + Clone + Send + Sync { - const DTYPE: GgmlDType; - const BLCK_NUMEL: usize; -} - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ4_0 { - pub(crate) d: f16, - pub(crate) qs: [u8; QK4_0 / 2], -} -const _: () = assert!(std::mem::size_of::() == 18); - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ4_1 { - pub(crate) d: f16, - pub(crate) m: f16, - pub(crate) qs: [u8; QK4_1 / 2], -} -const _: () = assert!(std::mem::size_of::() == 20); - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ5_0 { - pub(crate) d: f16, - pub(crate) qh: [u8; 4], - pub(crate) qs: [u8; QK5_0 / 2], -} -const _: () = assert!(std::mem::size_of::() == 22); - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ5_1 { - pub(crate) d: f16, - pub(crate) m: f16, - pub(crate) qh: [u8; 4], - pub(crate) qs: [u8; QK5_1 / 2], -} -const _: () = assert!(std::mem::size_of::() == 24); - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ8_0 { - pub(crate) d: f16, - pub(crate) qs: [i8; QK8_0], -} -const _: () = assert!(std::mem::size_of::() == 34); - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ8_1 { - pub(crate) d: f16, - pub(crate) s: f16, - pub(crate) qs: [i8; QK8_1], -} -const _: () = assert!(std::mem::size_of::() == 36); - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ2K { - pub(crate) scales: [u8; QK_K / 16], - pub(crate) qs: [u8; QK_K / 4], - pub(crate) d: f16, - pub(crate) dmin: f16, -} -const _: () = assert!(QK_K / 16 + QK_K / 4 + 2 * 2 == std::mem::size_of::()); - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ3K { - pub(crate) hmask: [u8; QK_K / 8], - pub(crate) qs: [u8; QK_K / 4], - pub(crate) scales: [u8; 12], - pub(crate) d: f16, -} -const _: () = assert!(QK_K / 8 + QK_K / 4 + 12 + 2 == std::mem::size_of::()); - -#[derive(Debug, Clone, PartialEq)] -// https://github.com/ggerganov/llama.cpp/blob/468ea24fb4633a0d681f7ac84089566c1c6190cb/k_quants.h#L82 -#[repr(C)] -pub struct BlockQ4K { - pub(crate) d: f16, - pub(crate) dmin: f16, - pub(crate) scales: [u8; K_SCALE_SIZE], - pub(crate) qs: [u8; QK_K / 2], -} -const _: () = assert!(QK_K / 2 + K_SCALE_SIZE + 2 * 2 == std::mem::size_of::()); - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ5K { - pub(crate) d: f16, - pub(crate) dmin: f16, - pub(crate) scales: [u8; K_SCALE_SIZE], - pub(crate) qh: [u8; QK_K / 8], - pub(crate) qs: [u8; QK_K / 2], -} -const _: () = - assert!(QK_K / 8 + QK_K / 2 + 2 * 2 + K_SCALE_SIZE == std::mem::size_of::()); - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ6K { - pub(crate) ql: [u8; QK_K / 2], - pub(crate) qh: [u8; QK_K / 4], - pub(crate) scales: [i8; QK_K / 16], - pub(crate) d: f16, -} -const _: () = assert!(3 * QK_K / 4 + QK_K / 16 + 2 == std::mem::size_of::()); - -#[derive(Debug, Clone, PartialEq)] -#[repr(C)] -pub struct BlockQ8K { - pub(crate) d: f32, - pub(crate) qs: [i8; QK_K], - pub(crate) bsums: [i16; QK_K / 16], -} -const _: () = assert!(4 + QK_K + QK_K / 16 * 2 == std::mem::size_of::()); - -impl GGType for BlockQ4K { - const DTYPE: GgmlDType = GgmlDType::Q4K; - const BLCK_NUMEL: usize = QK_K; -} - -impl GGType for BlockQ6K { - const DTYPE: GgmlDType = GgmlDType::Q6K; - const BLCK_NUMEL: usize = QK_K; -} - -impl GGType for BlockQ4_0 { - const DTYPE: GgmlDType = GgmlDType::Q4_0; - const BLCK_NUMEL: usize = QK4_0; -} - -impl GGType for BlockQ8_0 { - const DTYPE: GgmlDType = GgmlDType::Q8_0; - const BLCK_NUMEL: usize = QK8_0; -} - -impl GGType for f32 { - const DTYPE: GgmlDType = GgmlDType::F32; - const BLCK_NUMEL: usize = 1; -} - -impl GGType for f16 { - const DTYPE: GgmlDType = GgmlDType::F16; - const BLCK_NUMEL: usize = 1; -} diff --git a/crates/ratchet-loader/src/lib.rs b/crates/ratchet-loader/src/lib.rs deleted file mode 100644 index e23ae1e3..00000000 --- a/crates/ratchet-loader/src/lib.rs +++ /dev/null @@ -1,142 +0,0 @@ -mod error; -pub mod gguf; -mod k_quants; - -pub const STORAGE_BUFFER_ALIGN: usize = 256; - -#[derive(Debug, thiserror::Error)] -pub enum LoadError { - #[error("Invalid GGML Format: {0:#x}")] - InvalidFormat(u32), - #[error("non-specific I/O error")] - Io(#[from] std::io::Error), - #[error("could not convert bytes to a UTF-8 string")] - InvalidUtf8(#[from] std::string::FromUtf8Error), - #[error("invalid integer conversion")] - InvalidIntegerConversion(#[from] std::num::TryFromIntError), - #[error("Unsupported tensor type {dtype} for tensor {name}")] - UnsupportedDType { name: String, dtype: u32 }, - #[error("invariant broken: {0}")] - InvariantBroken(String), - #[error("invalid data type {0}")] - InvalidDType(u32), - #[error("Missing tensor {name}")] - MissingTensor { name: String }, -} - -#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum GgmlDType { - F32, - F16, - Q4_0, - Q4_1, - Q5_0, - Q5_1, - Q8_0, - Q8_1, - Q2K, - Q3K, - Q4K, - Q5K, - Q6K, - Q8K, -} - -impl From for ratchet::DType { - fn from(val: GgmlDType) -> Self { - match val { - GgmlDType::F32 => ratchet::DType::F32, - GgmlDType::F16 => ratchet::DType::F16, - //TODO: disambiguate F and H variants - _ => unimplemented!(), - } - } -} - -impl TryFrom for GgmlDType { - type Error = LoadError; - - fn try_from(u: u32) -> Result { - let dtype = match u { - 0 => Self::F32, - 1 => Self::F16, - 2 => Self::Q4_0, - 3 => Self::Q4_1, - 6 => Self::Q5_0, - 7 => Self::Q5_1, - 8 => Self::Q8_0, - 9 => Self::Q8_1, - 10 => Self::Q2K, - 11 => Self::Q3K, - 12 => Self::Q4K, - 13 => Self::Q5K, - 14 => Self::Q6K, - 15 => Self::Q8K, - _ => return Err(LoadError::InvalidDType(u)), - }; - Ok(dtype) - } -} - -impl GgmlDType { - pub fn to_u32(self) -> u32 { - match self { - Self::F32 => 0, - Self::F16 => 1, - Self::Q4_0 => 2, - Self::Q4_1 => 3, - Self::Q5_0 => 6, - Self::Q5_1 => 7, - Self::Q8_0 => 8, - Self::Q8_1 => 9, - Self::Q2K => 10, - Self::Q3K => 11, - Self::Q4K => 12, - Self::Q5K => 13, - Self::Q6K => 14, - Self::Q8K => 15, - } - } - - /// The type size for blocks in bytes. - pub fn type_size(&self) -> usize { - use k_quants::*; - match self { - Self::F32 => 4, - Self::F16 => 2, - Self::Q4_0 => std::mem::size_of::(), - Self::Q4_1 => std::mem::size_of::(), - Self::Q5_0 => std::mem::size_of::(), - Self::Q5_1 => std::mem::size_of::(), - // https://github.com/ggerganov/llama.cpp/blob/468ea24fb4633a0d681f7ac84089566c1c6190cb/ggml.c#L932 - Self::Q8_0 => std::mem::size_of::(), - Self::Q8_1 => std::mem::size_of::(), - Self::Q2K => std::mem::size_of::(), - Self::Q3K => std::mem::size_of::(), - Self::Q4K => std::mem::size_of::(), - Self::Q5K => std::mem::size_of::(), - Self::Q6K => std::mem::size_of::(), - Self::Q8K => std::mem::size_of::(), - } - } - - /// The block size, i.e. the number of elements stored in each block. - pub fn block_numel(&self) -> usize { - match self { - Self::F32 => 1, - Self::F16 => 1, - Self::Q4_0 => k_quants::QK4_0, - Self::Q4_1 => k_quants::QK4_1, - Self::Q5_0 => k_quants::QK5_0, - Self::Q5_1 => k_quants::QK5_1, - Self::Q8_0 => k_quants::QK8_0, - Self::Q8_1 => k_quants::QK8_1, - Self::Q2K | Self::Q3K | Self::Q4K | Self::Q5K | Self::Q6K | Self::Q8K => k_quants::QK_K, - } - } - - pub fn tensor_size(&self, numel: usize) -> usize { - numel * self.type_size() / self.block_numel() - } -} diff --git a/crates/ratchet-loader/test-data/nano-llama-q4k.gguf b/crates/ratchet-loader/test-data/nano-llama-q4k.gguf deleted file mode 100644 index edeedb83..00000000 Binary files a/crates/ratchet-loader/test-data/nano-llama-q4k.gguf and /dev/null differ diff --git a/crates/ratchet-macros/src/lib.rs b/crates/ratchet-macros/src/lib.rs deleted file mode 100644 index 09fc8c80..00000000 --- a/crates/ratchet-macros/src/lib.rs +++ /dev/null @@ -1,23 +0,0 @@ -mod ir_fields; -mod wgsl_metadata; - -use proc_macro::TokenStream; -use syn::parse_macro_input; - -/// Derives the `OpMetadata` trait implementation for a struct. -/// -/// Generates a `.render()` method that converts a Rust struct into a WGSL struct. -#[proc_macro_derive(WgslMetadata, attributes(builder))] -pub fn derive_wgsl_metadata(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input); - wgsl_metadata::derive(input).into() -} - -/// Derives the `IrFields` trait implementation for a struct. -/// -/// Generates a `.ir_fields()` method we use for hashing compute graphs. -#[proc_macro_derive(IrFields, attributes(builder))] -pub fn derive_ir_fields(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input); - ir_fields::derive(input).into() -} diff --git a/crates/ratchet-models/src/gpt2/generate.rs b/crates/ratchet-models/src/gpt2/generate.rs deleted file mode 100644 index 984c242b..00000000 --- a/crates/ratchet-models/src/gpt2/generate.rs +++ /dev/null @@ -1,57 +0,0 @@ -#![cfg(target_arch = "wasm32")] -use crate::gpt2::GPT2; -use crate::TokenOutputStream; -use ndarray::Axis; -use ndarray_stats::QuantileExt; -use ratchet::{shape, Device, Tensor}; -use ratchet_nn::Module; -use tokenizers::Tokenizer; - -pub async fn generate( - model: &mut GPT2, - tokenizer: Tokenizer, - prompt: String, - callback: impl Fn(String), -) -> anyhow::Result<()> { - use web_time::Instant; - log::warn!("Prompt: {}", prompt); - - let mut tos = TokenOutputStream::new(tokenizer); - let encoding = tos.tokenizer().encode(prompt, true).unwrap(); - let mut tokens = encoding - .get_ids() - .iter() - .map(|&x| x as i32) - .collect::>(); - let mut all_tokens = tokens.clone(); - let mut loop_cnt = 0; - let start = Instant::now(); - while tokens[tokens.len() - 1] != 50256 && loop_cnt < 256 { - let input = Tensor::from_data( - tokens.clone(), - shape![1, tokens.len()], - model.device.clone(), - ); - let result = model.schedule(input)?; - let logits = result.to(&Device::CPU).await?; - model.cache_mut().update(tokens.len()); - - tokens = logits - .to_ndarray_view::() - .map_axis(Axis(2), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - - if let Some(t) = tos.next_token(tokens[0] as u32)? { - callback(t); - } - all_tokens.extend(tokens.clone()); - loop_cnt += 1; - } - let elapsed = start.elapsed(); - log::warn!("Elapsed: {:?}", elapsed); - log::warn!("Tok/s {}", all_tokens.len() as f64 / elapsed.as_secs_f64()); - model.reset(); - Ok(()) -} diff --git a/crates/ratchet-models/src/lib.rs b/crates/ratchet-models/src/lib.rs deleted file mode 100644 index 7cb943d3..00000000 --- a/crates/ratchet-models/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -#![allow(clippy::upper_case_acronyms)] -pub mod gpt2; -// pub mod moondream; -// pub mod phi2; -// pub mod phi3; -pub mod registry; -mod token_stream; -// pub mod whisper; -pub use token_stream::TokenOutputStream; - -#[cfg(target_arch = "wasm32")] -#[derive(Debug, derive_new::new)] -pub struct WebTensor { - ggml_dtype: ratchet_loader::GgmlDType, - data: js_sys::Uint8Array, - shape: ratchet::Shape, -} - -#[cfg(target_arch = "wasm32")] -pub type TensorMap = std::collections::HashMap; - -#[cfg(target_arch = "wasm32")] -pub fn ratchet_from_gguf_web( - wt: WebTensor, - device: &ratchet::Device, -) -> anyhow::Result { - use ratchet_loader::gguf::gguf::ratchet_from_gguf; - let shape = wt.shape.clone(); - let data = wt.data.to_vec(); - ratchet_from_gguf(wt.ggml_dtype, &data, shape, device) -} diff --git a/crates/ratchet-models/src/moondream/generate.rs b/crates/ratchet-models/src/moondream/generate.rs deleted file mode 100644 index 2a97b599..00000000 --- a/crates/ratchet-models/src/moondream/generate.rs +++ /dev/null @@ -1,207 +0,0 @@ -use super::model::Moondream; -use crate::TokenOutputStream; -use ndarray::Axis; -use ndarray_stats::QuantileExt; -use ratchet::shape; -use ratchet::Device; -use ratchet::Tensor; -use ratchet_nn::Module; -use tokenizers::Tokenizer; - -#[cfg(not(target_arch = "wasm32"))] -pub fn generate( - model: &mut Moondream, - image_bytes: &[u8], - question: String, - tokenizer: Tokenizer, - callback: impl Fn(String), -) -> anyhow::Result<()> { - use ratchet::rvec; - use web_time::Instant; - let device = model.text_model.device.clone(); - - let prompt = format!("\n\nQuestion: {}\n\nAnswer:", question); - log::warn!("Prompt: {}", prompt); - - let mut tos = TokenOutputStream::new(tokenizer); - - let img = image::ImageReader::new(std::io::Cursor::new(image_bytes)) - .with_guessed_format()? - .decode() - .unwrap() - .resize_to_fill(378, 378, image::imageops::FilterType::Triangle); // Adjusted to 378x378 - - let pixels: Vec<_> = img - .to_rgb8() - .to_vec() - .iter() - .map(|&x| (x as f32 / 255.0)) - .collect(); - - let img_tensor = Tensor::from_data(pixels, shape![378, 378, 3], device.clone()) - .permute(&[2, 0, 1])? - .view(shape![1, 3, 378, 378])? - .cast(device.compute_precision())?; - - let img_embed = model.vision_encoder.schedule(img_tensor)?.resolve()?; - - let bos_token = model - .text_model - .embedding - .schedule(Tensor::from_data([50256], shape![1], device.clone()))? - .view(shape![1, 1, 2048])?; - - let encoding = tos.tokenizer().encode(prompt, false).unwrap(); - - let mut tokens = encoding - .get_ids() - .iter() - .map(|&x| x as i32) - .collect::>(); - - let mut all_tokens = tokens.clone(); - - let start = Instant::now(); - let mut generated_tokens = vec![]; - while *tokens.last().unwrap() != 50256 { - let input = Tensor::from_data(tokens.clone(), shape![1, tokens.len()], device.clone()); - let mut embeds: Tensor; - if generated_tokens.is_empty() { - embeds = model.text_model.embedding.schedule(input)?; - embeds = Tensor::cat( - rvec![bos_token.clone(), img_embed.clone(), embeds.clone()], - 1, - )?; - } else { - embeds = model.text_model.embedding.schedule(input).unwrap(); - } - - let result = model - .text_model - .schedule(embeds.clone())? - .full()? - .resolve()?; - - model.text_model.cache_mut().update(embeds.shape()[1]); - - let logits = result.to(&Device::CPU).unwrap(); - let next_tokens = logits - .to_ndarray_view::() - .map_axis(Axis(2), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - tokens = next_tokens.clone(); - generated_tokens.extend(next_tokens.clone()); - all_tokens.extend(next_tokens.clone()); - - if let Some(t) = tos.next_token(next_tokens[0] as u32)? { - callback(t); - } - } - - let elapsed = start.elapsed(); - log::warn!("Elapsed: {:?}", elapsed); - log::warn!("Tok/s {}", all_tokens.len() as f64 / elapsed.as_secs_f64()); - model.text_model.reset(); - Ok(()) -} - -#[cfg(target_arch = "wasm32")] -pub async fn generate( - model: &mut Moondream, - image_bytes: Vec, - question: String, - tokenizer: Tokenizer, - callback: impl Fn(String), -) -> anyhow::Result<()> { - use web_time::Instant; - let device = model.text_model.device.clone(); - - let img = image::io::Reader::new(std::io::Cursor::new(image_bytes)) - .with_guessed_format()? - .decode() - .unwrap() - .resize_to_fill(378, 378, image::imageops::FilterType::Triangle); // Adjusted to 378x378 - - let prompt = format!("\n\nQuestion: {}\n\nAnswer:", question); - log::warn!("Prompt: {}", prompt); - - let mut tos = TokenOutputStream::new(tokenizer); - - let pixels: Vec<_> = img - .to_rgb8() - .to_vec() - .iter() - .map(|&x| (x as f32 / 255.0)) - .collect(); - - let img_tensor = Tensor::from_data(&pixels, shape![378, 378, 3], device.clone()) - .permute(&[2, 0, 1])? - .view(shape![1, 3, 378, 378])?; - - let img_embed = model.vision_encoder.schedule(img_tensor)?.resolve()?; - - let bos_token = model - .text_model - .embedding - .schedule(Tensor::from_data([50256], shape![1], device.clone()))? - .view(shape![1, 1, 2048])?; - - let encoding = tos.tokenizer().encode(prompt, false).unwrap(); - - let mut tokens = encoding - .get_ids() - .iter() - .map(|&x| x as i32) - .collect::>(); - - let mut all_tokens = tokens.clone(); - - let start = Instant::now(); - let mut generated_tokens = vec![]; - while *tokens.last().unwrap() != 50256 { - let input = Tensor::from_data(tokens.clone(), shape![1, tokens.len()], device.clone()); - let mut embeds: Tensor; - if generated_tokens.len() == 0 { - embeds = model.text_model.embedding.schedule(input).unwrap(); - embeds = Tensor::cat( - vec![bos_token.clone(), img_embed.clone(), embeds.clone()].into(), - 1, - ) - .unwrap(); - } else { - embeds = model.text_model.embedding.schedule(input).unwrap(); - } - - let result = model - .text_model - .schedule(embeds.clone())? - .full()? - .resolve()?; - - model.text_model.cache_mut().update(embeds.shape()[1]); - - let logits = result.to(&Device::CPU).await.unwrap(); - let next_tokens = logits - .to_ndarray_view::() - .map_axis(Axis(2), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - tokens = next_tokens.clone(); - generated_tokens.extend(next_tokens.clone()); - all_tokens.extend(next_tokens.clone()); - - if let Some(t) = tos.next_token(next_tokens[0] as u32)? { - callback(t); - } - } - - let elapsed = start.elapsed(); - log::warn!("Elapsed: {:?}", elapsed); - log::warn!("Tok/s {}", all_tokens.len() as f64 / elapsed.as_secs_f64()); - println!("Tok/s {}", all_tokens.len() as f64 / elapsed.as_secs_f64()); - model.text_model.reset(); - Ok(()) -} diff --git a/crates/ratchet-models/src/moondream/mlp.rs b/crates/ratchet-models/src/moondream/mlp.rs deleted file mode 100644 index e94c02ec..00000000 --- a/crates/ratchet-models/src/moondream/mlp.rs +++ /dev/null @@ -1,18 +0,0 @@ -use ratchet::Tensor; -use ratchet_nn::{Linear, Module}; - -#[derive(Debug, derive_new::new)] -pub struct MLP { - pub fc1: Linear, - pub fc2: Linear, -} - -impl Module for MLP { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let input_dt = input.dt(); - self.fc2 - .schedule(self.fc1.schedule(input)?.full()?.gelu()?.cast(input_dt)?) - } -} diff --git a/crates/ratchet-models/src/moondream/mod.rs b/crates/ratchet-models/src/moondream/mod.rs deleted file mode 100644 index 02d58c48..00000000 --- a/crates/ratchet-models/src/moondream/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod generate; -mod mlp; -pub mod model; -mod text_model; -mod vision_encoder; - -pub use generate::generate; -pub use model::Moondream; diff --git a/crates/ratchet-models/src/moondream/model.rs b/crates/ratchet-models/src/moondream/model.rs deleted file mode 100644 index 37ccfa26..00000000 --- a/crates/ratchet-models/src/moondream/model.rs +++ /dev/null @@ -1,284 +0,0 @@ -use std::io::{BufRead, Seek}; - -use anyhow::Ok; -use half::f16; -use ratchet::{shape, DType, Device, Tensor}; -use ratchet_loader::gguf::gguf::Header; -use ratchet_nn::{Embedding, KVCache, LayerNorm, Linear, RotaryEmbedding}; - -#[cfg(target_arch = "wasm32")] -use {crate::ratchet_from_gguf_web, crate::TensorMap}; - -use super::{ - mlp::MLP, - text_model::{DecoderLayer, SelfAttention, TextModel}, - vision_encoder::{ - Attention, LinearPatchEmbedding, VisionEncoder, VisionProjection, VisionTransformer, - VitBlock, - }, -}; - -#[derive(Debug)] -pub struct Moondream { - pub vision_encoder: VisionEncoder, - pub text_model: TextModel, -} - -impl Moondream { - pub fn load( - header: Header, - reader: &mut R, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| header.tensor(reader, name, device).unwrap(); - Self::load_inner(&header, lt, device) - } - - #[cfg(target_arch = "wasm32")] - pub async fn from_web(header: Header, mut tensors: TensorMap) -> anyhow::Result { - let device = Device::request_device(ratchet::DeviceRequest::GPU).await?; - let mut lt = |name: &str| { - let tensor = tensors - .remove(name) - .ok_or_else(|| anyhow::anyhow!("missing tensor")); - ratchet_from_gguf_web(tensor.unwrap(), &device).unwrap() - }; - Self::load_inner(&header, lt, &device) - } - - fn load_inner(_header: &Header, mut lt: F, device: &Device) -> anyhow::Result - where - F: FnMut(&str) -> Tensor, - { - let n_layers = 24_i32; - let dim = 2048_f32; - let n_heads = 32_u32; - let n_kv_heads = 32_u32; - let rope_base = 10000.0f32; - let rope_dim = 32_u32; - let ln_eps = 1e-05; - let hdim = dim / n_heads as f32; - let softmax_scale = Tensor::from_data([1.0 / hdim.sqrt()], shape![1], device.clone()); - let cache_shape = shape![1, 32, 4096, 64]; - - let kv_cache = match device.compute_precision() { - DType::F16 => KVCache::new::(n_layers as _, cache_shape, device), - DType::F32 => KVCache::new::(n_layers as _, cache_shape, device), - _ => unimplemented!(), - }; - - let text_model = TextModel::new( - Embedding::new(lt("text_model.transformer.embd.wte.weight")), - (0..n_layers) - .map(|i| { - DecoderLayer::new( - LayerNorm::new( - lt(&format!("text_model.transformer.h.{}.ln.weight", i)), - Some(lt(&format!("text_model.transformer.h.{}.ln.bias", i))), - ln_eps, - ), - SelfAttention::new( - Linear::new( - lt(&format!("text_model.transformer.h.{}.mixer.Wqkv.weight", i)), - Some(lt(&format!( - "text_model.transformer.h.{}.mixer.Wqkv.bias", - i - ))), - ), - Linear::new( - lt(&format!( - "text_model.transformer.h.{}.mixer.out_proj.weight", - i - )), - Some(lt(&format!( - "text_model.transformer.h.{}.mixer.out_proj.bias", - i - ))), - ), - RotaryEmbedding::new(rope_dim as usize, false, rope_base, 1.0), - n_heads, - softmax_scale.clone(), - n_kv_heads, - ), - MLP::new( - Linear::new( - lt(&format!("text_model.transformer.h.{}.mlp.fc1.weight", i)), - Some(lt(&format!("text_model.transformer.h.{}.mlp.fc1.bias", i))), - ), - Linear::new( - lt(&format!("text_model.transformer.h.{}.mlp.fc2.weight", i)), - Some(lt(&format!("text_model.transformer.h.{}.mlp.fc2.bias", i))), - ), - ), - ) - }) - .collect(), - LayerNorm::new( - lt("text_model.lm_head.ln.weight"), - Some(lt("text_model.lm_head.ln.bias")), - ln_eps, - ), - Linear::new( - lt("text_model.lm_head.linear.weight"), - Some(lt("text_model.lm_head.linear.bias")), - ), - kv_cache, - device.clone(), - ); - - let projection = VisionProjection::new(MLP::new( - Linear::new( - lt("vision_encoder.projection.mlp.fc1.weight"), - Some(lt("vision_encoder.projection.mlp.fc1.bias")), - ), - Linear::new( - lt("vision_encoder.projection.mlp.fc2.weight"), - Some(lt("vision_encoder.projection.mlp.fc2.bias")), - ), - )); - - let transformer = VisionTransformer::new( - LinearPatchEmbedding::new( - Linear::new(lt("vision_encoder.encoder.model.visual.patch_embed.linear.weight"), Some(lt("vision_encoder.encoder.model.visual.patch_embed.linear.bias"))), - ), - lt("vision_encoder.encoder.model.visual.pos_embed"), - (0..27) - .map(|layer| { - let qkvw = lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.attn.qkv.weight", layer)); - let qkvb = lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.attn.qkv.bias", layer)); - - let n_heads = 16; - let dim = 1152; - let h_dim = dim / n_heads; - let scale_factor = - Tensor::from_data([1.0 / (h_dim as f32).sqrt()], shape![1], device.clone()); - - VitBlock::new( - 1152, - Attention::new( - n_heads, - dim, - Linear::new(qkvw, Some(qkvb)), - Linear::new( - lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.attn.proj.weight", layer)), - Some(lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.attn.proj.bias", layer))), - ), - scale_factor, - ), - MLP::new( - Linear::new( - lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.mlp.fc1.weight", layer)), - Some(lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.mlp.fc1.bias", layer))), - ), - Linear::new( - lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.mlp.fc2.weight", layer)), - Some(lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.mlp.fc2.bias", layer))), - ), - ), - LayerNorm::new( - lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.norm1.weight", layer)), - Some(lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.norm1.bias", layer))), - ln_eps, - ), - LayerNorm::new( - lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.norm2.weight", layer)), - Some(lt(&format!("vision_encoder.encoder.model.visual.blocks.{}.norm2.bias", layer))), - ln_eps, - ), - ) - }).collect::>(), - LayerNorm::new(lt("vision_encoder.encoder.model.visual.norm.weight"), Some(lt("vision_encoder.encoder.model.visual.norm.bias")), ln_eps), - ); - - let vision_encoder = VisionEncoder::new(projection, transformer); - Ok(Self { - vision_encoder, - text_model, - }) - } -} - -#[cfg(all(test, not(target_arch = "wasm32"), feature = "pyo3"))] -mod tests { - use std::fs; - - use anyhow::Ok; - use hf_hub::api::sync::Api; - use ratchet::{shape, test_util::run_py_prg, Device, DeviceRequest, Tensor}; - use ratchet_loader::gguf; - use ratchet_nn::Module; - use tokenizers::Tokenizer; - - use crate::moondream::{ - generate::generate, text_model::TextModel, vision_encoder::VisionEncoder, - }; - - use super::Moondream; - - fn vision_ground_truth(tensor: Tensor) -> anyhow::Result { - let prg = r#" -from transformers import AutoModelForCausalLM, AutoTokenizer -import torch - -def ground(*args): - tensor = torch.from_numpy(args[0]) - model_id = "vikhyatk/moondream2" - revision = "2024-05-20" - model = AutoModelForCausalLM.from_pretrained( - model_id, trust_remote_code=True, revision=revision - ) - return model.encode_image(tensor).numpy() -"#; - - run_py_prg(prg.to_string(), &[&tensor], &[], ratchet::DType::F32) - } - - #[test] - #[cfg_attr(feature = "ci", ignore)] - fn moondream_encoder() -> anyhow::Result<()> { - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - let api = Api::new().unwrap(); - let model_repo = api.model("ratchet-community/ratchet-moondream-2".to_string()); - let model_path = model_repo.get("moondream_f32.gguf").unwrap(); - let mut reader = std::io::BufReader::new(std::fs::File::open(model_path).unwrap()); - let content = gguf::gguf::Header::read(&mut reader).unwrap(); - let model = Moondream::load(content, &mut reader, &device).unwrap(); - - let input = Tensor::randn::(shape![1, 3, 378, 378], device); - let ours = model - .vision_encoder - .schedule(input.clone())? - .resolve()? - .to(&Device::CPU)?; - let theirs = vision_ground_truth(input.to(&Device::CPU).unwrap()).unwrap(); - ours.all_close(&theirs, 1e-1, 1e-1).unwrap(); - Ok(()) - } - - #[test] - #[cfg_attr(feature = "ci", ignore)] - fn moondream_end_to_end() { - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - let api = Api::new().unwrap(); - let model_repo = api.model("ratchet-community/ratchet-moondream-2".to_string()); - let model_path = model_repo.get("moondream_q8_0.gguf").unwrap(); - let mut reader = std::io::BufReader::new(std::fs::File::open(model_path).unwrap()); - let content = gguf::gguf::Header::read(&mut reader).unwrap(); - let mut model = Moondream::load(content, &mut reader, &device).unwrap(); - - let tokenizer_path = model_repo.get("tokenizer.json").unwrap(); - let tokenizer = Tokenizer::from_file(tokenizer_path).unwrap(); - - let img_path = model_repo.get("demo.jpg").unwrap(); - let img = fs::read(img_path).unwrap(); - - generate( - &mut model, - &img, - "What is happening here?".to_owned(), - tokenizer, - |token| print!("{}", token), - ) - .unwrap(); - } -} diff --git a/crates/ratchet-models/src/moondream/text_model.rs b/crates/ratchet-models/src/moondream/text_model.rs deleted file mode 100644 index 57bb4a91..00000000 --- a/crates/ratchet-models/src/moondream/text_model.rs +++ /dev/null @@ -1,192 +0,0 @@ -use ratchet::{rvec, shape, Device, Tensor}; -use ratchet_nn::{ - Embedding, KVCache, KVEntry, LayerNorm, Linear, Module, RotaryEmbedding, RotaryInput, -}; - -use super::mlp::MLP; - -#[derive(Debug, derive_new::new)] -pub struct SelfAttention { - qkv: Linear, - o: Linear, - rope: RotaryEmbedding, - n_heads: u32, - softmax_scale: Tensor, - n_kv_heads: u32, -} - -pub struct AttnInput { - pub input: Tensor, - pub mask: Option, - pub kv_cache: Option, -} - -impl Module for SelfAttention { - type Input = AttnInput; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let AttnInput { - input, - mask, - kv_cache, - } = input; - let [batch_size, q_len, n_state]: [usize; 3] = input.shape().try_into()?; - - let hdim = n_state / self.n_heads as usize; - let kv_x_hdim = self.n_kv_heads as usize * hdim; - - let qkv = self.qkv.schedule(input)?; - let query_pos = self.n_heads as usize * hdim; - let key_pos = query_pos + kv_x_hdim; - let value_pos = key_pos + kv_x_hdim; - - let query_states = qkv - .clone() - .slice(&[0..batch_size, 0..q_len, 0..query_pos])?; - let key_states = qkv - .clone() - .slice(&[0..batch_size, 0..q_len, query_pos..key_pos])?; - let value_states = qkv - .clone() - .slice(&[0..batch_size, 0..q_len, key_pos..value_pos])?; - - let q_shape = shape![batch_size as _, q_len, self.n_heads as _, hdim]; - let kv_shape = shape![batch_size as _, q_len, self.n_kv_heads as _, hdim]; - - let query_states = query_states.view(q_shape)?.permute(&[0, 2, 1, 3])?; - let key_states = key_states.view(kv_shape.clone())?.permute(&[0, 2, 1, 3])?; - let value_states = value_states.view(kv_shape)?.permute(&[0, 2, 1, 3])?; - - let offset = kv_cache.as_ref().map(|kv| kv.entries).unwrap_or(0); - let q_dt = query_states.dt(); - let query_states = self - .rope - .schedule(RotaryInput { - input: query_states.full()?, - offset, - })? - .cast(q_dt)?; - let key_states = self - .rope - .schedule(RotaryInput { - input: key_states.full()?, - offset, - })? - .cast(q_dt)?; - - let (key_states, value_states) = if let Some(kv) = kv_cache { - let k_cache = kv.k_cache.cache(key_states, 2, offset)?; - let v_cache = kv.v_cache.cache(value_states, 2, offset)?; - (k_cache, v_cache) - } else { - (key_states, value_states) - }; - - let mut attn_weights = query_states - .full()? - .matmul(key_states.full()?, false, true)? - .mul(self.softmax_scale.clone())? - .cast(q_dt)?; - - if let Some(m) = mask { - let attn_dt = attn_weights.dt(); - attn_weights = attn_weights.add(m.cast(attn_dt)?)?; - } - - let w = attn_weights.full()?.softmax(3)?.cast(value_states.dt())?; - let wv = w - .matmul(value_states, false, false)? - .permute(&[0, 2, 1, 3])?; - let wv = wv.view(shape![batch_size as _, q_len, n_state])?; - self.o.schedule(wv) - } -} - -#[derive(Debug, derive_new::new)] -pub struct DecoderLayer { - pub ln: LayerNorm, - pub self_attn: SelfAttention, - pub mlp: MLP, -} - -#[derive(Debug)] -pub struct DecoderLayerInput { - pub x: Tensor, - pub mask: Option, - pub kv_cache: Option, -} - -impl Module for DecoderLayer { - type Input = DecoderLayerInput; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let DecoderLayerInput { x, mask, kv_cache } = input; - let residual = x.clone(); - let xs = self.ln.schedule(x)?; - let attn_output = self.self_attn.schedule(AttnInput { - input: xs.clone(), - mask, - kv_cache, - })?; - let ff_hs = self.mlp.schedule(xs)?; - attn_output.add(ff_hs)?.add(residual) - } -} - -#[derive(Debug, derive_new::new)] -pub struct TextModel { - pub embedding: Embedding, - pub layers: Vec, - pub ln_post: LayerNorm, - pub lm_head: Linear, - pub kv_cache: KVCache, - pub device: Device, -} - -impl Module for TextModel { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let mut x = input.clone(); - let [_, seq_len, n_state]: [usize; 3] = x.shape().try_into()?; - let mask = if seq_len <= 1 { - None - } else { - Some(Self::generate_mask(seq_len, x.device())?) - }; - - for (i, layer) in self.layers.iter().enumerate() { - let input = DecoderLayerInput { - x, - mask: mask.clone(), - kv_cache: Some(self.kv_cache[i].clone()), - }; - x = layer.schedule(input)?; - } - x = self.ln_post.schedule(x)?; - x = x.slice(&[0..1, seq_len - 1..seq_len, 0..n_state])?; - let logits = self.lm_head.schedule(x)?; - Ok(logits) - } -} - -impl TextModel { - pub fn generate_mask(seq_len: usize, device: &Device) -> anyhow::Result { - let mask: Vec<_> = (0..seq_len) - .flat_map(|i| (0..seq_len).map(move |j| if j > i { f32::NEG_INFINITY } else { 0f32 })) - .collect(); - - Ok(Tensor::from_data( - mask, - shape![seq_len, seq_len], - device.clone(), - )) - } - - pub fn cache_mut(&mut self) -> &mut KVCache { - &mut self.kv_cache - } - pub fn reset(&mut self) { - self.kv_cache.reset(); - } -} diff --git a/crates/ratchet-models/src/moondream/vision_encoder.rs b/crates/ratchet-models/src/moondream/vision_encoder.rs deleted file mode 100644 index 26eb9c3a..00000000 --- a/crates/ratchet-models/src/moondream/vision_encoder.rs +++ /dev/null @@ -1,173 +0,0 @@ -use ratchet::{prelude::shape, rvec, Tensor}; -use ratchet_nn::{LayerNorm, Linear, Module}; - -use super::mlp::MLP; - -#[derive(Debug, derive_new::new)] -pub struct Attention { - n_heads: usize, - dim: usize, - qkv: Linear, - proj: Linear, - scale_factor: Tensor, -} - -impl Module for Attention { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let h_dim = self.dim / self.n_heads; - let [b, n, c]: [usize; 3] = input.shape().try_into()?; - // step 1 - 0, 1, 2, 3, 4 - // step 2 - 0, 2, 1, 3, 4 - // step 3 - 2, 0, 1, 3, 4 - // step 4 - 2, 0, 3, 1, 4 - - // b, n, 3, nh, hd - let mut qkv = self.qkv.schedule(input.clone())?; - // b, 3, n, nh, hd - qkv = qkv - .view(shape![b, n, 3, self.n_heads * h_dim])? - .permute(&[0, 2, 1, 3])?; - // 3, b, n, nh, hd - qkv = qkv - .view(shape![b, 3, n * self.n_heads * h_dim])? - .permute(&[1, 0, 2])?; - // 3, b, nh, n, hd - qkv = qkv - .view(shape![3 * b, n, self.n_heads, h_dim])? - .permute(&[0, 2, 1, 3])? - .view(shape![3, b * self.n_heads * n * h_dim])?; - - let q = qkv - .clone() - .slice(&[0..1, 0..(b * self.n_heads * n * h_dim)])? - .view(shape![b, self.n_heads, n, h_dim])?; - let k = qkv - .clone() - .slice(&[1..2, 0..(b * self.n_heads * n * h_dim)])? - .view(shape![b, self.n_heads, n, h_dim])?; - let v = qkv - .clone() - .slice(&[2..3, 0..(b * self.n_heads * n * h_dim)])? - .view(shape![b, self.n_heads, n, h_dim])?; - - // scaled dot-product attention - let mut attn_weights = q - .full()? - .matmul(k.permute(&[0, 1, 3, 2])?.full()?, false, false)? - .mul(self.scale_factor.clone())?; - attn_weights = attn_weights.softmax(3)?.cast(v.dt())?; - let mut x = attn_weights.matmul(v, false, false)?; - x = x.permute(&[0, 2, 1, 3])?.view(shape![b, n, c])?; - self.proj.schedule(x) - } -} - -#[derive(Debug, derive_new::new)] -pub struct VitBlock { - embed_dim: usize, - attn: Attention, - mlp: MLP, - norm1: LayerNorm, - norm2: LayerNorm, -} - -impl Module for VitBlock { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let x = input - .clone() - .add(self.attn.schedule(self.norm1.schedule(input)?)?)?; - x.clone().add(self.mlp.schedule(self.norm2.schedule(x)?)?) - } -} - -#[derive(Debug, derive_new::new)] -pub struct LinearPatchEmbedding { - linear: Linear, -} - -impl Module for LinearPatchEmbedding { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let [b, c, hp1, wp2]: [usize; 4] = input.shape().try_into()?; - let (p1, p2) = (14, 14); - let (h, w) = (hp1 / p1, wp2 / p2); - // step 1 - 0, 1, 2, 3, 4, 5 - // step 2 - 0, 2, 1, 3, 4, 5 - // step 3 - 0, 2, 1, 4, 3, 5 - // step 4 - 0, 2, 4, 1, 3, 5 - - // b, c, h, p1, w, p2 - let mut x = input - .view(shape![b, c, h, p1 * w * p2])? - .permute(&[0, 2, 1, 3])?; - // b, h, c, p1, w, p2 - x = x - .view(shape![b * h * c, p1, w, p2])? - .permute(&[0, 2, 1, 3])?; - // b, h, c, w, p1, p2 - x = x - .view(shape![b * h, c, w, p1 * p2])? - .permute(&[0, 2, 1, 3])?; - // b, h, w, c, p1, p2 - x = x.view(shape![b, h * w, c * p1 * p2])?; - self.linear.schedule(x) - } -} - -#[derive(Debug, derive_new::new)] -pub struct VisionTransformer { - patch_embed: LinearPatchEmbedding, - pos_embed: Tensor, - blocks: Vec, - norm: LayerNorm, -} - -impl Module for VisionTransformer { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let mut x = self.patch_embed.schedule(input)?; - x = x.clone().add(self.pos_embed.clone())?; - x = self - .blocks - .iter() - .fold(x.clone(), |acc, blk| blk.schedule(acc).unwrap()); - self.norm.schedule(x) - } -} - -#[derive(Debug, derive_new::new)] -pub struct VisionProjection { - mlp: MLP, -} - -impl Module for VisionProjection { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - self.mlp.schedule(input) - } -} - -#[derive(Debug, derive_new::new)] -pub struct VisionEncoder { - projection: VisionProjection, - transformer: VisionTransformer, -} - -impl Module for VisionEncoder { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let transformed = self.transformer.schedule(input)?; - self.projection.schedule(Tensor::cat( - rvec![transformed.clone(), transformed.clone()], - 2, - )?) - } -} diff --git a/crates/ratchet-models/src/phi2/attn.rs b/crates/ratchet-models/src/phi2/attn.rs deleted file mode 100644 index 976516f8..00000000 --- a/crates/ratchet-models/src/phi2/attn.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::io::{BufRead, Seek}; - -use ratchet::{prelude::shape, rvec, Device, Tensor}; -use ratchet_loader::gguf::gguf::Header; -use ratchet_nn::{KVEntry, Linear, Module, RotaryEmbedding, RotaryInput}; - -#[cfg(target_arch = "wasm32")] -use crate::{ratchet_from_gguf_web, TensorMap}; - -#[derive(Debug)] -pub struct PhiSelfAttention { - q: Linear, - k: Linear, - v: Linear, - o: Linear, - rope: RotaryEmbedding, - n_heads: u32, - softmax_scale: Tensor, - n_kv_heads: u32, -} - -impl PhiSelfAttention { - pub fn load( - disk_model: &Header, - reader: &mut R, - layer_index: usize, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| { - let key = format!("blk.{}.{}", layer_index, name); - disk_model.tensor(reader, &key, device) - }; - Self::load_inner(disk_model, lt, device) - } - - #[cfg(target_arch = "wasm32")] - pub fn from_web( - header: &Header, - tensors: &mut TensorMap, - layer_index: usize, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| { - let key = format!("blk.{}.{}", layer_index, name); - let tensor = tensors - .remove(&key) - .ok_or_else(|| anyhow::anyhow!("missing tensor"))?; - ratchet_from_gguf_web(tensor, device) - }; - Self::load_inner(header, lt, device) - } - - fn load_inner(header: &Header, mut lt: F, device: &Device) -> anyhow::Result - where - F: FnMut(&str) -> anyhow::Result, - { - let q = Linear::new(lt("attn_q.weight")?, Some(lt("attn_q.bias")?)); - let k = Linear::new(lt("attn_k.weight")?, Some(lt("attn_k.bias")?)); - let v = Linear::new(lt("attn_v.weight")?, Some(lt("attn_v.bias")?)); - let o = Linear::new(lt("attn_output.weight")?, Some(lt("attn_output.bias")?)); - - let n_heads = header - .metadata - .get("phi2.attention.head_count") - .unwrap() - .to_u32()?; - let n_kv_heads = header - .metadata - .get("phi2.attention.head_count_kv") - .unwrap() - .to_u32()?; - - let scale_val = 1.0 / 80_f32.sqrt(); - let softmax_scale = Tensor::from_data([scale_val], shape![1], device.clone()); - //TODO: hardcoded for Phi2, should read from meta - let base = 10000.0; - let dim = (0.4 * (2560f64 / 32f64)) as usize; - let rope = RotaryEmbedding::new(dim, false, base, 1.0); - Ok(Self { - q, - k, - v, - o, - rope, - n_heads, - softmax_scale, - n_kv_heads, - }) - } -} - -pub struct PhiAttnInput { - pub input: Tensor, - pub mask: Option, - pub cache: Option, -} - -impl Module for PhiSelfAttention { - type Input = PhiAttnInput; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let PhiAttnInput { input, mask, cache } = input; - let [batch_size, seq_len, n_state]: [usize; 3] = input.shape().try_into()?; - let q = self.q.schedule(input.clone())?; - let k = self.k.schedule(input.clone())?; - let v = self.v.schedule(input)?; - - let h_dim = n_state / self.n_heads as usize; - - //TODO: - //if self.qk_layer_norm { ... } - - let q_shape = shape![batch_size as _, seq_len, self.n_heads as _, h_dim]; - let kv_shape = shape![batch_size as _, seq_len, self.n_kv_heads as _, h_dim]; - let query_states = q.view(q_shape)?.permute(&[0, 2, 1, 3])?; - let key_states = k.view(kv_shape.clone())?.permute(&[0, 2, 1, 3])?; - let value_states = v.view(kv_shape)?.permute(&[0, 2, 1, 3])?; - - let offset = cache.as_ref().map(|kv| kv.entries).unwrap_or(0); - let query_states = self.rope.schedule(RotaryInput { - input: query_states, - offset, - })?; - let key_states = self.rope.schedule(RotaryInput { - input: key_states, - offset, - })?; - - let (key_states, value_states) = if let Some(kv) = cache { - let k_cache = kv.k_cache.cache(key_states, 2, offset)?; - let v_cache = kv.v_cache.cache(value_states, 2, offset)?; - (k_cache, v_cache) - } else { - (key_states, value_states) - }; - - //TODO: can we just use the built in transposed matmul? - let mut attn_weights = query_states - .full()? - .matmul(key_states.permute(&[0, 1, 3, 2])?.full()?, false, false)? - .mul(self.softmax_scale.clone())?; - - if let Some(m) = mask { - attn_weights = attn_weights.add(m)?; - } - - let w = attn_weights.softmax(3)?.cast(value_states.dt())?; - let wv = w - .matmul(value_states, false, false)? - .permute(&[0, 2, 1, 3])?; - let wv = wv.view(shape![batch_size as _, seq_len, n_state])?; - self.o.schedule(wv) - } -} diff --git a/crates/ratchet-models/src/phi2/generate.rs b/crates/ratchet-models/src/phi2/generate.rs deleted file mode 100644 index 7955b471..00000000 --- a/crates/ratchet-models/src/phi2/generate.rs +++ /dev/null @@ -1,57 +0,0 @@ -#![cfg(target_arch = "wasm32")] -use crate::phi2::Phi2; -use crate::TokenOutputStream; -use ndarray::Axis; -use ndarray_stats::QuantileExt; -use ratchet::{shape, Device, Tensor}; -use ratchet_nn::Module; -use tokenizers::Tokenizer; - -pub async fn generate( - model: &mut Phi2, - tokenizer: Tokenizer, - prompt: String, - callback: impl Fn(String), -) -> anyhow::Result<()> { - use web_time::Instant; - log::warn!("Prompt: {}", prompt); - - let mut tos = TokenOutputStream::new(tokenizer); - let encoding = tos.tokenizer().encode(prompt, true).unwrap(); - let mut tokens = encoding - .get_ids() - .iter() - .map(|&x| x as i32) - .collect::>(); - let mut all_tokens = tokens.clone(); - let mut loop_cnt = 0; - let start = Instant::now(); - while tokens[tokens.len() - 1] != 50256 && loop_cnt < 256 { - let input = Tensor::from_data( - tokens.clone(), - shape![1, tokens.len()], - model.device.clone(), - ); - let result = model.schedule(input)?.resolve()?; - let logits = result.to(&Device::CPU).await?; - model.cache_mut().update(tokens.len()); - - tokens = logits - .to_ndarray_view::() - .map_axis(Axis(2), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - - if let Some(t) = tos.next_token(tokens[0] as u32)? { - callback(t); - } - all_tokens.extend(tokens.clone()); - loop_cnt += 1; - } - let elapsed = start.elapsed(); - log::warn!("Elapsed: {:?}", elapsed); - log::warn!("Tok/s {}", all_tokens.len() as f64 / elapsed.as_secs_f64()); - model.reset(); - Ok(()) -} diff --git a/crates/ratchet-models/src/phi2/mlp.rs b/crates/ratchet-models/src/phi2/mlp.rs deleted file mode 100644 index 992bd397..00000000 --- a/crates/ratchet-models/src/phi2/mlp.rs +++ /dev/null @@ -1,16 +0,0 @@ -use ratchet::Tensor; -use ratchet_nn::{Linear, Module}; - -#[derive(Debug, derive_new::new)] -pub struct MLP { - l1: Linear, - l2: Linear, -} - -impl Module for MLP { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - self.l2.schedule(self.l1.schedule(input)?.gelu()?) - } -} diff --git a/crates/ratchet-models/src/phi2/mod.rs b/crates/ratchet-models/src/phi2/mod.rs deleted file mode 100644 index 27b46b4c..00000000 --- a/crates/ratchet-models/src/phi2/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod attn; -mod generate; -mod mlp; -mod model; - -pub use model::Phi2; - -#[cfg(target_arch = "wasm32")] -pub use generate::generate; diff --git a/crates/ratchet-models/src/phi2/model.rs b/crates/ratchet-models/src/phi2/model.rs deleted file mode 100644 index 110c0677..00000000 --- a/crates/ratchet-models/src/phi2/model.rs +++ /dev/null @@ -1,345 +0,0 @@ -use super::{ - attn::{PhiAttnInput, PhiSelfAttention}, - mlp::MLP, -}; -use half::f16; -use ratchet::{shape, DType, Device, Tensor}; -use ratchet_loader::gguf::gguf::Header; -use ratchet_nn::{Embedding, KVCache, KVEntry, LayerNorm, Linear, Module}; -use std::io::{BufRead, Seek}; - -#[cfg(target_arch = "wasm32")] -use {crate::ratchet_from_gguf_web, crate::TensorMap}; - -#[derive(Debug)] -pub struct DecoderLayer { - ln: LayerNorm, - self_attn: PhiSelfAttention, - mlp: MLP, -} - -impl DecoderLayer { - pub fn load( - disk_model: &Header, - reader: &mut R, - layer_index: usize, - device: &Device, - ) -> anyhow::Result { - let self_attn = PhiSelfAttention::load(disk_model, reader, layer_index, device)?; - let mut lt = |name: &str| { - let key = format!("blk.{}.{}", layer_index, name); - disk_model.tensor(reader, &key, device) - }; - - let ln = LayerNorm::new(lt("attn_norm.weight")?, Some(lt("attn_norm.bias")?), 1e-5); - - let mlp = MLP::new( - Linear::new(lt("ffn_up.weight")?, Some(lt("ffn_up.bias")?)), - Linear::new(lt("ffn_down.weight")?, Some(lt("ffn_down.bias")?)), - ); - Ok(Self { ln, self_attn, mlp }) - } - - #[cfg(target_arch = "wasm32")] - pub fn from_web( - header: &Header, - tensors: &mut TensorMap, - layer_index: usize, - device: &Device, - ) -> anyhow::Result { - let self_attn = PhiSelfAttention::from_web(header, tensors, layer_index, device)?; - let mut lt = |name: &str| { - let key = format!("blk.{}.{}", layer_index, name); - let tensor = tensors - .remove(&key) - .ok_or_else(|| anyhow::anyhow!("missing tensor"))?; - ratchet_from_gguf_web(tensor, device) - }; - - let ln = LayerNorm::new(lt("attn_norm.weight")?, Some(lt("attn_norm.bias")?), 1e-5); - - let mlp = MLP::new( - Linear::new(lt("ffn_up.weight")?, Some(lt("ffn_up.bias")?)), - Linear::new(lt("ffn_down.weight")?, Some(lt("ffn_down.bias")?)), - ); - Ok(Self { ln, self_attn, mlp }) - } -} - -pub struct DecoderLayerInput { - pub x: Tensor, - pub mask: Option, - pub cache: Option, -} - -impl Module for DecoderLayer { - type Input = DecoderLayerInput; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let DecoderLayerInput { x, mask, cache } = input; - let residual = x.clone(); - let xs = self.ln.schedule(x)?; - let attn_output = self.self_attn.schedule(PhiAttnInput { - input: xs.clone(), - mask, - cache, - })?; - let ff_hs = self.mlp.schedule(xs)?; - attn_output.add(ff_hs)?.add(residual) - } -} - -#[derive(Debug)] -pub struct Phi2 { - pub embedding: Embedding, - pub layers: Vec, - pub ln_post: LayerNorm, - pub lm_head: Linear, - pub kv_cache: KVCache, - pub device: Device, -} - -impl Module for Phi2 { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let mut x = self.embedding.schedule(input)?; - - let [_, seq_len, n_state]: [usize; 3] = x.shape().try_into()?; - let mask = if seq_len <= 1 { - None - } else { - Some(Self::generate_mask(seq_len, x.device())?) - }; - - for (layer_idx, layer) in self.layers.iter().enumerate() { - let input = DecoderLayerInput { - x, - mask: mask.clone(), - cache: Some(self.kv_cache[layer_idx].clone()), - }; - x = layer.schedule(input)?; - } - x = self.ln_post.schedule(x)?; - x = x.slice(&[0..1, seq_len - 1..seq_len, 0..n_state])?; - let logits = self.lm_head.schedule(x)?; - Ok(logits) - } -} - -impl Phi2 { - const MAX_CACHE: usize = 1024; //TODO: configurable - - pub fn load( - header: Header, - reader: &mut R, - device: &Device, - ) -> anyhow::Result { - let token_embedding = header.tensor(reader, "token_embd.weight", device)?; - let embedding = Embedding::new(token_embedding); - - let n_layers = header.metadata.get("phi2.block_count").unwrap().to_u32()? as i32; - - let layers = (0..n_layers) - .fold(Vec::with_capacity(n_layers as _), |mut blocks, i| { - blocks.push(DecoderLayer::load(&header, reader, i as _, device)); - blocks - }) - .into_iter() - .collect::>>()?; - - let mut lt = |name: &str| { - let key = format!("output{}", name); - header.tensor(reader, &key, device) - }; - - let ln_post = LayerNorm::new(lt("_norm.weight")?, Some(lt("_norm.bias")?), 1e-5); - let lm_head = Linear::new(lt(".weight")?, Some(lt(".bias")?)); - - let cache_shape = shape![1, 32, Self::MAX_CACHE, 80]; - let kv_cache = match device.compute_precision() { - DType::F16 => KVCache::new::(n_layers, cache_shape, device), - DType::F32 => KVCache::new::(n_layers, cache_shape, device), - _ => unimplemented!(), - }; - - Ok(Self { - embedding, - layers, - ln_post, - lm_head, - kv_cache, - device: device.clone(), - }) - } - - //TODO: dedup - #[cfg(target_arch = "wasm32")] - pub async fn from_web(header: Header, mut tensors: TensorMap) -> anyhow::Result { - let device = Device::request_device(ratchet::DeviceRequest::GPU).await?; - let embedding = Embedding::new(ratchet_from_gguf_web( - tensors - .remove("token_embd.weight") - .ok_or_else(|| anyhow::anyhow!("missing tensor"))?, - &device, - )?); - - let n_layers = header.metadata.get("phi2.block_count").unwrap().to_u32()? as i32; - - let layers = (0..n_layers) - .fold(Vec::with_capacity(n_layers as _), |mut blocks, i| { - blocks.push(DecoderLayer::from_web( - &header, - &mut tensors, - i as _, - &device, - )); - blocks - }) - .into_iter() - .collect::>>()?; - - let mut lt = |name: &str| { - let key = format!("output{}", name); - let tensor = tensors - .remove(&key) - .ok_or_else(|| anyhow::anyhow!("missing tensor"))?; - ratchet_from_gguf_web(tensor, &device) - }; - - let ln_post = LayerNorm::new(lt("_norm.weight")?, Some(lt("_norm.bias")?), 1e-5); - let lm_head = Linear::new(lt(".weight")?, Some(lt(".bias")?)); - - let cache_shape = shape![1, 32, Self::MAX_CACHE, 80]; - let kv_cache = match device.compute_precision() { - DType::F16 => KVCache::new::(n_layers, cache_shape, &device), - DType::F32 => KVCache::new::(n_layers, cache_shape, &device), - _ => unimplemented!(), - }; - - Ok(Self { - embedding, - layers, - ln_post, - lm_head, - kv_cache, - device, - }) - } - - pub fn generate_mask(seq_len: usize, device: &Device) -> anyhow::Result { - let mask: Vec<_> = (0..seq_len) - .flat_map(|i| (0..seq_len).map(move |j| if j > i { f32::NEG_INFINITY } else { 0f32 })) - .collect(); - - Ok(Tensor::from_data( - mask, - shape![seq_len, seq_len], - device.clone(), - )) - } - - pub fn reset(&mut self) { - self.kv_cache.reset(); - } - - pub fn cache_mut(&mut self) -> &mut KVCache { - &mut self.kv_cache - } -} - -#[cfg(all(test, not(target_arch = "wasm32"), feature = "pyo3"))] -mod tests { - use hf_hub::api::sync::Api; - use ndarray::Axis; - use ndarray_stats::QuantileExt; - use numpy::PyArrayDyn; - use pyo3::{types::PyModule, Python}; - use ratchet::{prelude::shape, Device, DeviceRequest, Tensor}; - use ratchet_loader::gguf; - use ratchet_nn::Module; - use tokenizers::Tokenizer; - - use super::Phi2; - - fn ground_truth() -> anyhow::Result> { - let prg = r#" -import torch -from transformers import AutoModelForCausalLM, AutoTokenizer -import collections - -def ground(): - model = AutoModelForCausalLM.from_pretrained("microsoft/phi-2", torch_dtype=torch.float32, device_map="cpu", trust_remote_code=True) - tokenizer = AutoTokenizer.from_pretrained("microsoft/phi-2", trust_remote_code=True) - inputs = tokenizer("def print_prime(n):", return_tensors="pt", return_attention_mask=False) - outputs = model.generate(**inputs, max_length=20, return_dict_in_generate=True, output_logits=True) - generated_logits = outputs.logits - - result = [torch.unsqueeze(l, 0).numpy() for l in generated_logits] - return result -"#; - Python::with_gil(|py| { - let prg = PyModule::from_code(py, prg, "x.py", "x")?; - let py_result: Vec<&PyArrayDyn> = prg.getattr("ground")?.call0()?.extract()?; - Ok(py_result.into_iter().map(Tensor::from).collect::<_>()) - }) - } - - #[test] - #[cfg_attr(feature = "ci", ignore)] - fn load_phi2() -> anyhow::Result<()> { - let _ = env_logger::builder().is_test(true).try_init(); - let api = Api::new().unwrap(); - let model_repo = api.model("FL33TW00D-HF/phi2".to_string()); - let model_path = model_repo.get("phi2-f16.gguf").unwrap(); - println!("MODEL PATH: {}", model_path.display()); - - let mut reader = std::io::BufReader::new(std::fs::File::open(model_path)?); - let device = Device::request_device(DeviceRequest::GPU)?; - let content = gguf::gguf::Header::read(&mut reader)?; - let mut model = Phi2::load(content, &mut reader, &device)?; - - let tokenizer_repo = api.model("microsoft/phi-2".to_string()); - let tokenizer_path = tokenizer_repo.get("tokenizer.json").unwrap(); - let tokenizer = Tokenizer::from_file(tokenizer_path).unwrap(); - - let prompt = "def print_prime(n):"; - print!("{}", prompt); - let encoding = tokenizer.encode(prompt, true).unwrap(); - let mut tokens = encoding - .get_ids() - .iter() - .map(|&x| x as i32) - .collect::>(); - let mut all_logits = vec![]; - let mut all_tokens = tokens.clone(); - let mut loop_cnt = 0; - while tokens[tokens.len() - 1] != 50256 && loop_cnt < 13 { - let input = Tensor::from_data(tokens.clone(), shape![1, tokens.len()], device.clone()); - let result = model.schedule(input)?.resolve()?; - let logits = result.to(&Device::CPU)?; - all_logits.push(logits.clone()); - model.cache_mut().update(tokens.len()); - - tokens = logits - .to_ndarray_view::() - .map_axis(Axis(2), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - let u32_toks = tokens.iter().map(|&x| x as u32).collect::>(); - print!("{}", tokenizer.decode(&u32_toks, true).unwrap()); - all_tokens.extend(tokens.clone()); - loop_cnt += 1; - } - - let ground_logits = ground_truth()?; - let all_equal = all_logits - .iter() - .zip(ground_logits.iter()) - .all(|(our, their)| their.all_close(our, 1e-3, 1e-3).is_ok()); - println!("All logits equal: {}", all_equal); - assert!(all_equal); - Ok(()) - } -} diff --git a/crates/ratchet-models/src/phi3/attn.rs b/crates/ratchet-models/src/phi3/attn.rs deleted file mode 100644 index 96fea397..00000000 --- a/crates/ratchet-models/src/phi3/attn.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::io::{BufRead, Seek}; - -use ratchet::{prelude::shape, rvec, Device, Tensor}; -use ratchet_loader::gguf::gguf::Header; -use ratchet_nn::{KVEntry, Linear, Module, RotaryEmbedding, RotaryInput}; - -#[cfg(target_arch = "wasm32")] -use crate::{ratchet_from_gguf_web, TensorMap}; - -#[derive(Debug)] -pub struct PhiSelfAttention { - qkv: Linear, - o: Linear, - rope: RotaryEmbedding, - n_heads: u32, - softmax_scale: Tensor, - n_kv_heads: u32, -} - -impl PhiSelfAttention { - pub fn load( - disk_model: &Header, - reader: &mut R, - layer_index: usize, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| { - let key = format!("blk.{}.{}", layer_index, name); - disk_model.tensor(reader, &key, device) - }; - Self::load_inner(disk_model, lt, device) - } - - #[cfg(target_arch = "wasm32")] - pub fn from_web( - header: &Header, - tensors: &mut TensorMap, - layer_index: usize, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| { - let key = format!("blk.{}.{}", layer_index, name); - let tensor = tensors - .remove(&key) - .ok_or_else(|| anyhow::anyhow!("missing tensor"))?; - ratchet_from_gguf_web(tensor, device) - }; - Self::load_inner(header, lt, device) - } - - fn load_inner(header: &Header, mut lt: F, device: &Device) -> anyhow::Result - where - F: FnMut(&str) -> anyhow::Result, - { - let qkv = Linear::new(lt("attn_qkv.weight")?, None); - let o = Linear::new(lt("attn_output.weight")?, None); - - let metadata = &header.metadata; - let n_heads = metadata.get("phi3.attention.head_count")?.to_u32()?; - let n_kv_heads = metadata.get("phi3.attention.head_count_kv")?.to_u32()?; - let d_model = metadata.get("phi3.embedding_length")?.to_u32()?; - let rope_base = 10000.0f32; - let rope_dim = metadata.get("phi3.rope.dimension_count")?.to_u32()?; - - let hdim = d_model as f32 / n_heads as f32; - let softmax_scale = Tensor::from_data([1.0 / hdim.sqrt()], shape![1], device.clone()); - let rope = RotaryEmbedding::new(rope_dim as _, false, rope_base, 1.0); - Ok(Self { - qkv, - o, - rope, - n_heads, - softmax_scale, - n_kv_heads, - }) - } -} - -pub struct PhiAttnInput { - pub input: Tensor, - pub mask: Option, - pub cache: Option, -} - -impl Module for PhiSelfAttention { - type Input = PhiAttnInput; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let PhiAttnInput { input, mask, cache } = input; - let [batch_size, q_len, n_state]: [usize; 3] = input.shape().try_into()?; - - let hdim = n_state / self.n_heads as usize; - let kv_x_hdim = self.n_kv_heads as usize * hdim; - - let qkv = self.qkv.schedule(input)?; - let query_pos = self.n_heads as usize * hdim; - let key_pos = query_pos + kv_x_hdim; - let value_pos = key_pos + kv_x_hdim; - - let query_states = qkv - .clone() - .slice(&[0..batch_size, 0..q_len, 0..query_pos])?; - let key_states = qkv - .clone() - .slice(&[0..batch_size, 0..q_len, query_pos..key_pos])?; - let value_states = qkv - .clone() - .slice(&[0..batch_size, 0..q_len, key_pos..value_pos])?; - - let q_shape = shape![batch_size as _, q_len, self.n_heads as _, hdim]; - let kv_shape = shape![batch_size as _, q_len, self.n_kv_heads as _, hdim]; - - let query_states = query_states.view(q_shape)?.permute(&[0, 2, 1, 3])?; - let key_states = key_states.view(kv_shape.clone())?.permute(&[0, 2, 1, 3])?; - let value_states = value_states.view(kv_shape)?.permute(&[0, 2, 1, 3])?; - - let offset = cache.as_ref().map(|kv| kv.entries).unwrap_or(0); - let q_dt = query_states.dt(); - let query_states = self - .rope - .schedule(RotaryInput { - input: query_states.full()?, - offset, - })? - .cast(q_dt)?; - let key_states = self - .rope - .schedule(RotaryInput { - input: key_states.full()?, - offset, - })? - .cast(q_dt)?; - - let (key_states, value_states) = if let Some(kv) = cache { - let k_cache = kv.k_cache.cache(key_states, 2, offset)?; - let v_cache = kv.v_cache.cache(value_states, 2, offset)?; - (k_cache, v_cache) - } else { - (key_states, value_states) - }; - - let mut attn_weights = query_states - .full()? - .matmul(key_states.full()?, false, true)? - .mul(self.softmax_scale.clone())? - .cast(q_dt)?; - - if let Some(m) = mask { - let attn_dt = attn_weights.dt(); - attn_weights = attn_weights.add(m.cast(attn_dt)?)?; - } - - let w = attn_weights.full()?.softmax(3)?.cast(value_states.dt())?; - let wv = w - .matmul(value_states, false, false)? - .permute(&[0, 2, 1, 3])?; - let wv = wv.view(shape![batch_size as _, q_len, n_state])?; - self.o.schedule(wv) - } -} diff --git a/crates/ratchet-models/src/phi3/generate.rs b/crates/ratchet-models/src/phi3/generate.rs deleted file mode 100644 index 7f1ea8a6..00000000 --- a/crates/ratchet-models/src/phi3/generate.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::phi3::Phi3; -use crate::TokenOutputStream; -use ndarray::Axis; -use ndarray_stats::QuantileExt; -use ratchet::{shape, Device, Tensor}; -use ratchet_nn::Module; -use tokenizers::Tokenizer; - -#[cfg(target_arch = "wasm32")] -pub async fn generate( - model: &mut Phi3, - tokenizer: Tokenizer, - prompt: String, - callback: impl Fn(String), -) -> anyhow::Result<()> { - use web_time::Instant; - log::warn!("Prompt: {}", prompt); - - let prompt = format!( - r#"<|user|> -{}<|end|> -<|assistant|>"#, - prompt - ); - - let mut tos = TokenOutputStream::new(tokenizer); - - let encoding = tos.tokenizer().encode(prompt, true).unwrap(); - let mut tokens = encoding - .get_ids() - .iter() - .map(|&x| x as i32) - .collect::>(); - tokens.insert(0, 1); - let mut all_tokens = tokens.clone(); - let start = Instant::now(); - while tokens[tokens.len() - 1] != 32007 && all_tokens.len() < 2048 { - let input = Tensor::from_data( - tokens.clone(), - shape![1, tokens.len()], - model.device.clone(), - ); - let result = model.schedule(input)?.resolve()?; - let logits = result.to(&Device::CPU).await?; - model.cache_mut().update(tokens.len()); - - tokens = logits - .to_ndarray_view::() - .map_axis(Axis(2), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - all_tokens.extend(tokens.clone()); - if let Some(t) = tos.next_token(tokens[0] as u32)? { - callback(t); - } - } - let elapsed = start.elapsed(); - log::warn!("Elapsed: {:?}", elapsed); - log::warn!("Tok/s {}", all_tokens.len() as f64 / elapsed.as_secs_f64()); - model.reset(); - Ok(()) -} - -#[cfg(not(target_arch = "wasm32"))] -pub fn generate( - model: &mut Phi3, - tokenizer: Tokenizer, - prompt: String, - callback: impl Fn(String), -) -> anyhow::Result<()> { - use web_time::Instant; - log::warn!("Prompt: {}", prompt); - - let prompt = format!( - r#"<|user|> -{}<|end|> -<|assistant|>"#, - prompt - ); - - let mut tos = TokenOutputStream::new(tokenizer); - - let encoding = tos.tokenizer().encode(prompt, true).unwrap(); - let mut tokens = encoding - .get_ids() - .iter() - .map(|&x| x as i32) - .collect::>(); - tokens.insert(0, 1); - let mut all_tokens = tokens.clone(); - let start = Instant::now(); - while tokens[tokens.len() - 1] != 32007 && all_tokens.len() < 2048 { - let input = Tensor::from_data( - tokens.clone(), - shape![1, tokens.len()], - model.device.clone(), - ); - let result = model.schedule(input)?.resolve()?; - let logits = result.to(&Device::CPU)?; - model.cache_mut().update(tokens.len()); - - tokens = logits - .to_ndarray_view::() - .map_axis(Axis(2), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - all_tokens.extend(tokens.clone()); - if let Some(t) = tos.next_token(tokens[0] as u32)? { - callback(t); - } - } - let elapsed = start.elapsed(); - log::warn!("Elapsed: {:?}", elapsed); - log::warn!("Tok/s {}", all_tokens.len() as f64 / elapsed.as_secs_f64()); - model.reset(); - Ok(()) -} diff --git a/crates/ratchet-models/src/phi3/mlp.rs b/crates/ratchet-models/src/phi3/mlp.rs deleted file mode 100644 index bea9fc03..00000000 --- a/crates/ratchet-models/src/phi3/mlp.rs +++ /dev/null @@ -1,40 +0,0 @@ -use ratchet::Tensor; -use ratchet_nn::{Linear, Module}; - -#[derive(Debug, derive_new::new)] -pub struct MLP { - up_proj: Linear, - down_proj: Linear, -} - -//class Phi3MLP(nn.Module): -// def __init__(self, config): -// super().__init__() -// -// self.config = config -// self.gate_up_proj = nn.Linear(config.hidden_size, 2 * config.intermediate_size, bias=False) -// self.down_proj = nn.Linear(config.intermediate_size, config.hidden_size, bias=False) -// -// self.activation_fn = ACT2FN[config.hidden_act] -// -// def forward(self, hidden_states: torch.FloatTensor) -> torch.FloatTensor: -// up_states = self.gate_up_proj(hidden_states) -// -// gate, up_states = up_states.chunk(2, dim=-1) -// up_states = up_states * self.activation_fn(gate) -// -// return self.down_proj(up_states) - -impl Module for MLP { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let input_dt = input.dt(); - let up_states = self.up_proj.schedule(input)?; - let [x, y, z]: [usize; 3] = up_states.shape().try_into()?; - let gate = up_states.clone().slice(&[0..x, 0..y, 0..z / 2])?; - let up_states = up_states.clone().slice(&[0..x, 0..y, z / 2..z])?; - let up_states = up_states.mul(gate.full()?.silu()?.cast(input_dt)?)?; - self.down_proj.schedule(up_states) - } -} diff --git a/crates/ratchet-models/src/phi3/mod.rs b/crates/ratchet-models/src/phi3/mod.rs deleted file mode 100644 index 1717e7d7..00000000 --- a/crates/ratchet-models/src/phi3/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod attn; -mod generate; -mod mlp; -mod model; - -pub use generate::generate; -pub use model::Phi3; diff --git a/crates/ratchet-models/src/phi3/model.rs b/crates/ratchet-models/src/phi3/model.rs deleted file mode 100644 index 2cfb28ef..00000000 --- a/crates/ratchet-models/src/phi3/model.rs +++ /dev/null @@ -1,409 +0,0 @@ -use std::io::{BufRead, Seek}; - -use half::f16; -use ratchet::{shape, DType, Device, Tensor}; -use ratchet_loader::gguf::gguf::Header; -use ratchet_nn::{Embedding, KVCache, KVEntry, Linear, Module, RMSNorm}; - -use super::{ - attn::{PhiAttnInput, PhiSelfAttention}, - mlp::MLP, -}; - -#[cfg(target_arch = "wasm32")] -use {crate::ratchet_from_gguf_web, crate::TensorMap}; - -#[derive(Debug)] -pub struct DecoderLayer { - input_norm: RMSNorm, - self_attn: PhiSelfAttention, - ffn_norm: RMSNorm, - mlp: MLP, -} - -impl DecoderLayer { - pub fn load( - header: &Header, - reader: &mut R, - layer_index: usize, - device: &Device, - ) -> anyhow::Result { - let self_attn = PhiSelfAttention::load(header, reader, layer_index, device)?; - let mut lt = |name: &str| { - let key = format!("blk.{}.{}", layer_index, name); - header.tensor(reader, &key, device) - }; - - let norm_eps = header - .metadata - .get("phi3.attention.layer_norm_rms_epsilon")? - .to_f32()?; - - let input_norm = RMSNorm::new(lt("attn_norm.weight")?, norm_eps); - let ffn_norm = RMSNorm::new(lt("ffn_norm.weight")?, norm_eps); - - let mlp = MLP::new( - Linear::new(lt("ffn_up.weight")?, None), - Linear::new(lt("ffn_down.weight")?, None), - ); - Ok(Self { - input_norm, - self_attn, - ffn_norm, - mlp, - }) - } - - #[cfg(target_arch = "wasm32")] - pub fn from_web( - header: &Header, - tensors: &mut TensorMap, - layer_index: usize, - device: &Device, - ) -> anyhow::Result { - let self_attn = PhiSelfAttention::from_web(header, tensors, layer_index, device)?; - let mut lt = |name: &str| { - let key = format!("blk.{}.{}", layer_index, name); - let tensor = tensors - .remove(&key) - .ok_or_else(|| anyhow::anyhow!("missing tensor"))?; - ratchet_from_gguf_web(tensor, device) - }; - - let norm_eps = header - .metadata - .get("phi3.attention.layer_norm_rms_epsilon")? - .to_f32()?; - - let input_norm = RMSNorm::new(lt("attn_norm.weight")?, norm_eps); - let ffn_norm = RMSNorm::new(lt("ffn_norm.weight")?, norm_eps); - - let mlp = MLP::new( - Linear::new(lt("ffn_up.weight")?, None), - Linear::new(lt("ffn_down.weight")?, None), - ); - Ok(Self { - input_norm, - self_attn, - ffn_norm, - mlp, - }) - } -} - -pub struct DecoderLayerInput { - pub x: Tensor, - pub mask: Option, - pub cache: Option, -} - -impl Module for DecoderLayer { - type Input = DecoderLayerInput; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let DecoderLayerInput { x, mask, cache } = input; - let residual = x.clone(); - let xs = self.input_norm.schedule(x)?; - let attn_output = self.self_attn.schedule(PhiAttnInput { - input: xs.clone(), - mask, - cache, - })?; - let xs = residual.add(attn_output)?; - let residual = xs.clone(); - let xs = self.ffn_norm.schedule(xs)?; - let xs = self.mlp.schedule(xs)?; - let xs = residual.add(xs)?; - Ok(xs) - } -} - -#[derive(Debug)] -pub struct Phi3 { - pub embedding: Embedding, - pub layers: Vec, - pub ln_post: RMSNorm, - pub lm_head: Linear, - pub kv_cache: KVCache, - pub device: Device, -} - -impl Module for Phi3 { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let mut x = self.embedding.schedule(input)?; - - let [_, seq_len, n_state]: [usize; 3] = x.shape().try_into()?; - let mask = if seq_len <= 1 { - None - } else { - Some(Self::generate_mask(seq_len, x.device())?) - }; - - for (layer_idx, layer) in self.layers.iter().enumerate() { - let input = DecoderLayerInput { - x, - mask: mask.clone(), - cache: Some(self.kv_cache[layer_idx].clone()), - }; - x = layer.schedule(input)?; - } - x = self.ln_post.schedule(x)?; - x = x.slice(&[0..1, seq_len - 1..seq_len, 0..n_state])?; - let logits = self.lm_head.schedule(x)?; - Ok(logits) - } -} - -impl Phi3 { - const MAX_CACHE: usize = 4096; //TODO: configurable - - pub fn load( - header: Header, - reader: &mut R, - device: &Device, - ) -> anyhow::Result { - let embedding = Embedding::new(header.tensor(reader, "token_embd.weight", device)?); - - let n_layers = header.metadata.get("phi3.block_count")?.to_u32()? as i32; - - let layers = (0..n_layers) - .fold(Vec::with_capacity(n_layers as _), |mut blocks, i| { - blocks.push(DecoderLayer::load(&header, reader, i as _, device)); - blocks - }) - .into_iter() - .collect::>>()?; - - let mut lt = |name: &str| { - let key = format!("output{}", name); - header.tensor(reader, &key, device) - }; - - let metadata = &header.metadata; - - let norm_eps = metadata - .get("phi3.attention.layer_norm_rms_epsilon")? - .to_f32()?; - let ln_post = RMSNorm::new(lt("_norm.weight")?, norm_eps); - let lm_head = Linear::new(lt(".weight")?, None); - - let n_layers = metadata.get("phi3.block_count")?.to_u32()?; - let d_model = metadata.get("phi3.embedding_length")?.to_u32()?; - let n_heads = metadata.get("phi3.attention.head_count")?.to_u32()?; - let hdim = d_model as f32 / n_heads as f32; - - let cache_shape = shape![1, n_layers as _, Self::MAX_CACHE, hdim as _]; - let kv_cache = match device.compute_precision() { - DType::F16 => KVCache::new::(n_layers as _, cache_shape, device), - DType::F32 => KVCache::new::(n_layers as _, cache_shape, device), - _ => unimplemented!(), - }; - - Ok(Self { - embedding, - layers, - ln_post, - lm_head, - kv_cache, - device: device.clone(), - }) - } - - //TODO: dedup - #[cfg(target_arch = "wasm32")] - pub async fn from_web(header: Header, mut tensors: TensorMap) -> anyhow::Result { - let device = Device::request_device(ratchet::DeviceRequest::GPU).await?; - let embedding = Embedding::new(ratchet_from_gguf_web( - tensors - .remove("token_embd.weight") - .ok_or_else(|| anyhow::anyhow!("missing tensor"))?, - &device, - )?); - - let n_layers = header.metadata.get("phi3.block_count")?.to_u32()? as i32; - - let layers = (0..n_layers) - .fold(Vec::with_capacity(n_layers as _), |mut blocks, i| { - blocks.push(DecoderLayer::from_web( - &header, - &mut tensors, - i as _, - &device, - )); - blocks - }) - .into_iter() - .collect::>>()?; - - let mut lt = |name: &str| { - let key = format!("output{}", name); - let tensor = tensors - .remove(&key) - .ok_or_else(|| anyhow::anyhow!("missing tensor"))?; - ratchet_from_gguf_web(tensor, &device) - }; - - let metadata = &header.metadata; - - let norm_eps = metadata - .get("phi3.attention.layer_norm_rms_epsilon")? - .to_f32()?; - let ln_post = RMSNorm::new(lt("_norm.weight")?, norm_eps); - let lm_head = Linear::new(lt(".weight")?, None); - - let n_layers = metadata.get("phi3.block_count")?.to_u32()?; - let d_model = metadata.get("phi3.embedding_length")?.to_u32()?; - let n_heads = metadata.get("phi3.attention.head_count")?.to_u32()?; - let hdim = d_model as f32 / n_heads as f32; - - let cache_shape = shape![1, n_layers as _, Self::MAX_CACHE, hdim as _]; - Ok(Self { - embedding, - layers, - ln_post, - lm_head, - kv_cache: KVCache::new::(n_layers as _, cache_shape, &device), - device: device.clone(), - }) - } - - pub fn generate_mask(seq_len: usize, device: &Device) -> anyhow::Result { - let mask: Vec<_> = (0..seq_len) - .flat_map(|i| (0..seq_len).map(move |j| if j > i { f32::NEG_INFINITY } else { 0f32 })) - .collect(); - - Ok(Tensor::from_data( - mask, - shape![seq_len, seq_len], - device.clone(), - )) - } - - pub fn reset(&mut self) { - self.kv_cache.reset(); - } - - pub fn cache_mut(&mut self) -> &mut KVCache { - &mut self.kv_cache - } -} - -#[cfg(all(test, not(target_arch = "wasm32"), feature = "pyo3"))] -mod tests { - use hf_hub::api::sync::Api; - use ndarray::Axis; - use ndarray_stats::QuantileExt; - use numpy::PyArrayDyn; - use pyo3::{types::PyModule, Python}; - use ratchet::{prelude::shape, Device, DeviceRequest, Tensor}; - use ratchet_loader::gguf; - use ratchet_nn::Module; - use tokenizers::Tokenizer; - - use super::Phi3; - - fn ground_truth(prompt: &str, max_tokens: usize) -> anyhow::Result> { - let prg = format!( - r#" -import torch -from transformers import Phi3ForCausalLM, AutoTokenizer - -def ground(): - model = Phi3ForCausalLM.from_pretrained("microsoft/Phi-3-mini-4k-instruct", torch_dtype=torch.float32, device_map="cpu", trust_remote_code=True) - model.eval() - - tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct", trust_remote_code=True) - inputs = tokenizer("""{}""", return_tensors="pt", return_attention_mask=False) - outputs = model.generate(**inputs, max_length={}, return_dict_in_generate=True, output_logits=True) - generated_logits = outputs.logits - print("Generated: ", tokenizer.decode(outputs[0][0], skip_special_tokens=True)) - - result = [torch.unsqueeze(l, 0).numpy() for l in generated_logits] - return result -"#, - prompt, max_tokens - ); - - println!("GROUND TRUTH PROGRAM: {}", prg); - Python::with_gil(|py| { - let prg = PyModule::from_code(py, &prg, "x.py", "x")?; - let py_result: Vec<&PyArrayDyn> = prg.getattr("ground")?.call0()?.extract()?; - Ok(py_result.into_iter().map(Tensor::from).collect::<_>()) - }) - } - - #[test] - #[cfg_attr(feature = "ci", ignore)] - fn load_phi3() -> anyhow::Result<()> { - let _ = env_logger::builder().is_test(true).try_init(); - let api = Api::new().unwrap(); - let model_repo = api.model("FL33TW00D-HF/phi3".to_string()); - let model_path = model_repo.get("phi3-mini-4k-f16.gguf").unwrap(); - println!("MODEL PATH: {}", model_path.display()); - - let mut reader = std::io::BufReader::new(std::fs::File::open(model_path)?); - let device = Device::request_device(DeviceRequest::GPU)?; - let content = gguf::gguf::Header::read(&mut reader)?; - let mut model = Phi3::load(content, &mut reader, &device)?; - - let tokenizer_repo = api.model("microsoft/Phi-3-mini-4k-instruct".to_string()); - let tokenizer_path = tokenizer_repo.get("tokenizer.json").unwrap(); - let tokenizer = Tokenizer::from_file(tokenizer_path).unwrap(); - - let MAX_TOKENS = 100; - let prompt = r#"<|user|> -How to explain Internet for a medieval knight?<|end|> -<|assistant|>"#; - let encoding = tokenizer.encode(prompt, true).unwrap(); - let mut tokens = encoding - .get_ids() - .iter() - .map(|&x| x as i32) - .collect::>(); - tokens.insert(0, 1); //TODO: what is going on here with tokenizers? - let mut all_logits = vec![]; - let mut all_tokens = tokens.clone(); - let mut generated_cnt = tokens.len(); - let start = std::time::Instant::now(); - - while tokens[tokens.len() - 1] != 32007 && generated_cnt < MAX_TOKENS { - let input = Tensor::from_data(tokens.clone(), shape![1, tokens.len()], device.clone()); - let result = model.schedule(input)?.full()?.resolve()?; - let logits = result.to(&Device::CPU)?; - all_logits.push(logits.clone()); - model.cache_mut().update(tokens.len()); - - tokens = logits - .to_ndarray_view::() - .map_axis(Axis(2), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - all_tokens.extend(tokens.clone()); - generated_cnt += 1; - } - let elapsed = start.elapsed(); - let u32_toks = all_tokens.iter().map(|&x| x as u32).collect::>(); - - let generated = tokenizer.decode(&u32_toks, true).unwrap(); - println!("We generated: \n{}\n", generated); - - let ground_logits = ground_truth(prompt, MAX_TOKENS)?; - assert_eq!(all_logits.len(), ground_logits.len()); - let all_equal = - ground_logits - .iter() - .zip(all_logits.iter()) - .enumerate() - .all(|(i, (their, our))| { - print!("Checking: {}", i); - our.all_close(their, 1e-1, 1e-1).is_ok() - }); - - println!("All logits equal: {}", all_equal); - assert!(all_equal); - Ok(()) - } -} diff --git a/crates/ratchet-models/src/registry.rs b/crates/ratchet-models/src/registry.rs deleted file mode 100644 index f89181a7..00000000 --- a/crates/ratchet-models/src/registry.rs +++ /dev/null @@ -1,116 +0,0 @@ -#![allow(non_local_definitions)] -//! # Registry -//! -//! The registry is responsible for surfacing available models to the user in both the CLI & WASM interfaces. - -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::wasm_bindgen; - -#[derive(Debug, Clone)] -#[cfg_attr( - target_arch = "wasm32", - derive(tsify::Tsify, serde::Serialize, serde::Deserialize), - tsify(from_wasm_abi), - serde(rename_all = "snake_case") -)] -#[cfg_attr(not(target_arch = "wasm32"), derive(clap::ValueEnum))] -pub enum WhisperVariants { - Tiny, - Base, - Small, - Medium, - LargeV2, - LargeV3, - DistilLargeV3, -} - -impl WhisperVariants { - pub fn repo_id(&self) -> &str { - match self { - WhisperVariants::Tiny => "FL33TW00D-HF/whisper-tiny", - WhisperVariants::Base => "FL33TW00D-HF/whisper-base", - WhisperVariants::Small => "FL33TW00D-HF/whisper-small", - WhisperVariants::Medium => "FL33TW00D-HF/whisper-medium", - WhisperVariants::LargeV2 => "FL33TW00D-HF/whisper-large-v2", - WhisperVariants::LargeV3 => "FL33TW00D-HF/whisper-large-v3", - WhisperVariants::DistilLargeV3 => "FL33TW00D-HF/distil-whisper-large-v3", - } - } -} - -#[derive(Debug, Clone)] -#[cfg_attr( - target_arch = "wasm32", - derive(tsify::Tsify, serde::Serialize, serde::Deserialize), - tsify(from_wasm_abi), - serde(rename_all = "snake_case") -)] -#[cfg_attr(not(target_arch = "wasm32"), derive(clap::ValueEnum))] -pub enum PhiVariants { - Phi2, - Phi3, -} - -/// # Available Models -/// -/// This is a type safe way to surface models to users, -/// providing autocomplete **within** model families. -#[derive(Debug, Clone)] -#[non_exhaustive] -#[cfg_attr( - target_arch = "wasm32", - derive(tsify::Tsify, serde::Serialize, serde::Deserialize) -)] -#[cfg_attr(target_arch = "wasm32", tsify(from_wasm_abi))] -pub enum AvailableModels { - Whisper(WhisperVariants), - Phi(PhiVariants), - Moondream, -} - -impl AvailableModels { - pub fn repo_id(&self) -> String { - let id = match self { - AvailableModels::Whisper(w) => w.repo_id(), - AvailableModels::Phi(p) => match p { - PhiVariants::Phi2 => "FL33TW00D-HF/phi2", - PhiVariants::Phi3 => "FL33TW00D-HF/phi3", - }, - AvailableModels::Moondream => "ratchet-community/ratchet-moondream-2", - }; - id.to_string() - } - - pub fn model_id(&self, quantization: Quantization) -> String { - let model_stem = match self { - AvailableModels::Whisper(w) => match w { - WhisperVariants::Tiny => "tiny", - WhisperVariants::Base => "base", - WhisperVariants::Small => "small", - WhisperVariants::Medium => "medium", - WhisperVariants::LargeV2 => "large-v2", - WhisperVariants::LargeV3 => "large-v3", - WhisperVariants::DistilLargeV3 => "distil-large-v3", - }, - AvailableModels::Phi(p) => match p { - PhiVariants::Phi2 => "phi2", - PhiVariants::Phi3 => "phi3-mini-4k", - }, - AvailableModels::Moondream => "moondream", - }; - match quantization { - Quantization::Q8_0 => format!("{}_q8_0.gguf", model_stem), - Quantization::F16 => format!("{}_f16.gguf", model_stem), - Quantization::F32 => format!("{}_f32.gguf", model_stem), - } - } -} - -#[derive(Debug, Clone)] -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] -#[cfg_attr(not(target_arch = "wasm32"), derive(clap::ValueEnum))] -pub enum Quantization { - Q8_0, - F16, - F32, -} diff --git a/crates/ratchet-models/src/whisper/config.rs b/crates/ratchet-models/src/whisper/config.rs deleted file mode 100644 index e1a8d5fd..00000000 --- a/crates/ratchet-models/src/whisper/config.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[derive(Debug, Clone, PartialEq, serde::Deserialize)] -pub struct Config { - #[serde(alias = "num_mel_bins")] - pub n_mels: usize, - #[serde(alias = "max_source_positions")] - pub n_audio_ctx: usize, - #[serde(alias = "d_model")] - pub n_audio_state: usize, - #[serde(alias = "encoder_attention_heads")] - pub n_audio_head: usize, - #[serde(alias = "encoder_layers")] - pub n_audio_layer: usize, - #[serde(alias = "vocab_size")] - pub n_vocab: usize, - #[serde(alias = "max_target_positions")] - pub n_text_ctx: usize, - #[serde(alias = "decoder_attention_heads")] - pub n_text_head: usize, - #[serde(alias = "decoder_layers")] - pub n_text_layer: usize, - #[serde(alias = "torch_dtype")] - pub dtype: String, - #[serde(default)] - pub suppress_tokens: Vec, -} diff --git a/crates/ratchet-models/src/whisper/decoder.rs b/crates/ratchet-models/src/whisper/decoder.rs deleted file mode 100644 index d912b46d..00000000 --- a/crates/ratchet-models/src/whisper/decoder.rs +++ /dev/null @@ -1,385 +0,0 @@ -use super::config::Config; -use crate::whisper::residual_block::*; -use half::f16; -use ratchet::{prelude::*, DType, TensorDType}; -use ratchet_loader::gguf::gguf::Header; -use ratchet_nn::{Embedding, KVCache, LayerNorm, Module}; -use std::io::{BufRead, Seek}; - -#[cfg(target_arch = "wasm32")] -use {crate::ratchet_from_gguf_web, crate::TensorMap}; - -#[derive(Debug)] -pub(crate) struct DecoderStem { - pub token_embed: Embedding, - pub pos_embed: Tensor, -} - -impl DecoderStem { - pub fn load( - header: &Header, - reader: &mut R, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| { - let key = format!("model.decoder.{}", name); - header.tensor(reader, &key, device) - }; - Self::load_inner(lt) - } - - #[cfg(target_arch = "wasm32")] - pub fn from_web( - header: &Header, - tensor_map: &mut TensorMap, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| { - let key = format!("model.decoder.{}", name); - let wt = tensor_map.remove(&key).unwrap(); - ratchet_from_gguf_web(wt, device) - }; - Self::load_inner(lt) - } - - fn load_inner(mut lt: F) -> anyhow::Result - where - F: FnMut(&str) -> anyhow::Result, - { - Ok(Self { - token_embed: Embedding::new(lt("embed_tokens.weight")?), - pos_embed: lt("embed_positions.weight")?, - }) - } -} - -#[derive(Debug)] -pub struct StemInput { - pub tokens: Tensor, - pub offset: usize, -} - -impl Module for DecoderStem { - type Input = StemInput; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let StemInput { tokens, offset } = input; - let num_tokens = tokens.shape()[tokens.rank() - 1]; - let start = offset; - let end = offset + num_tokens; - let mut sliced = self - .pos_embed - .clone() - .slice(&[start..end, 0..self.pos_embed.shape()[1]])?; - - sliced = sliced.cast(self.token_embed.weight.dt().activation_dt())?; - - self.token_embed.schedule(tokens)?.add(sliced) - } -} - -#[derive(Debug)] -pub struct WhisperDecoder { - stem: DecoderStem, - blocks: Vec, - mask: Tensor, - ln_post: LayerNorm, - cache: KVCache, - #[allow(dead_code)] //Should maintain a handle to the device - device: Device, -} - -impl Module for WhisperDecoder { - type Input = [Tensor; 2]; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let [audio_ctx, tokens] = input; - let mut x = self.stem.schedule(StemInput { - tokens, - offset: self.cache.entries(0), - })?; - - for (block_idx, block) in self.blocks.iter().enumerate() { - let block_input = ResidualAttentionBlockInputs { - x, - xa: Some(audio_ctx.clone()), - mask: Some(self.mask.clone()), - cache: Some(self.cache[block_idx].clone()), - }; - x = block.schedule(block_input)?; - } - x = self.ln_post.schedule(x)?; - let logits = self - .stem - .token_embed - .weight - .clone() - .gemm(x, None, false, true, true)? - .full()?; - Ok(logits) - } -} - -impl WhisperDecoder { - pub const MAX_CACHE: usize = 512; - - pub fn cache_mut(&mut self) -> &mut KVCache { - &mut self.cache - } - - pub fn reset(&mut self) { - self.cache.reset(); - } - - fn load_mask(n_ctx: usize, device: &Device) -> Tensor { - let mask: Vec<_> = (0..n_ctx) - .flat_map(|i| { - (0..n_ctx).map(move |j| if j > i { T::neg_infinity() } else { T::zero() }) - }) - .collect(); - Tensor::from_data(mask, shape![n_ctx, n_ctx], device.clone()) - } - - #[cfg(target_arch = "wasm32")] - pub fn from_web( - header: &Header, - config: &Config, - tensor_map: &mut TensorMap, - device: &Device, - ) -> anyhow::Result { - let (n_layers, n_heads) = (config.n_text_layer, config.n_text_head); - let stem = DecoderStem::from_web(header, tensor_map, device)?; - - let blocks = (0..n_layers) - .fold(Vec::with_capacity(n_layers as _), |mut blocks, i| { - blocks.push(ResidualAttentionBlock::from_web( - header, - tensor_map, - i as _, - n_heads as _, - "decoder", - device, - )); - blocks - }) - .into_iter() - .collect::, _>>()?; - - let mut lt = |name: &str| { - let key = format!("model.decoder.layer_norm.{}", name); - let wt = tensor_map.remove(&key).unwrap(); - ratchet_from_gguf_web(wt, device) - }; - - let n_state = config.n_audio_state as _; - - let dt = blocks[0].mlp.activation_dt(); - let mask = match dt { - DType::F16 => Self::load_mask::(config.n_text_ctx as _, device), - DType::F32 => Self::load_mask::(config.n_text_ctx as _, device), - _ => unimplemented!(), - }; - - let cache_shape = shape![1, Self::MAX_CACHE, n_state]; - let cache = match dt { - DType::F16 => KVCache::new::(n_layers as _, cache_shape, device), - DType::F32 => KVCache::new::(n_layers as _, cache_shape, device), - _ => unimplemented!(), - }; - - Ok(Self { - stem, - blocks, - mask, - ln_post: LayerNorm::new(lt("weight")?, Some(lt("bias")?), 1e-5), - cache, - device: device.clone(), - }) - } - - pub fn load( - header: &Header, - config: &Config, - reader: &mut R, - device: &Device, - ) -> anyhow::Result { - let stem = DecoderStem::load(header, reader, device)?; - let (n_layers, n_heads) = (config.n_text_layer, config.n_text_head); - - let blocks = (0..n_layers) - .fold(Vec::with_capacity(n_layers as _), |mut blocks, i| { - blocks.push(ResidualAttentionBlock::load( - header, - reader, - i as _, - n_heads as _, - "decoder", - device, - )); - blocks - }) - .into_iter() - .collect::, _>>()?; - - let mut lt = |name: &str| { - let key = format!("model.decoder.layer_norm.{}", name); - header.tensor(reader, &key, device) - }; - - let n_state = config.n_audio_state as _; - - let dt = blocks[0].mlp.activation_dt(); - let mask = match dt { - DType::F16 => Self::load_mask::(config.n_text_ctx as _, device), - DType::F32 => Self::load_mask::(config.n_text_ctx as _, device), - _ => unimplemented!(), - }; - - let cache_shape = shape![1, Self::MAX_CACHE, n_state]; - let cache = match dt { - DType::F16 => KVCache::new::(n_layers as _, cache_shape, device), - DType::F32 => KVCache::new::(n_layers as _, cache_shape, device), - _ => unimplemented!(), - }; - - Ok(Self { - stem, - blocks, - mask, - ln_post: LayerNorm::new(lt("weight")?, Some(lt("bias")?), 1e-5), - cache, - device: device.clone(), - }) - } -} - -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use hf_hub::api::sync::Api; - use ndarray::{s, Axis}; - use ndarray_stats::QuantileExt; - use numpy::PyArrayDyn; - use pyo3::{ - prelude::*, - types::{IntoPyDict, PyTuple}, - }; - use ratchet::{shape, Device, DeviceRequest, Tensor}; - use ratchet_loader::gguf::gguf; - use ratchet_nn::Module; - use tokenizers::Tokenizer; - - use crate::whisper::decoder::Config; - use crate::whisper::{ - decoder::WhisperDecoder, - options::{DecodingOptions, DecodingOptionsBuilder}, - }; - - fn log_init() { - let _ = env_logger::builder().is_test(true).try_init(); - } - - fn ground_truth(audio_path: &str, options: DecodingOptions) -> anyhow::Result> { - let prg = format!( - r#" -import warnings -warnings.simplefilter("ignore") -import whisper -import numpy as np -def ground(options): - model = whisper.load_model("tiny") - result = model.transcribe(audio="{}", **options) - output_logits = [l.numpy()[np.newaxis] for logits in result["all_logits"] for l in logits] - return output_logits -"#, - audio_path - ); - Python::with_gil(|py| { - let prg = PyModule::from_code(py, &prg, "x.py", "x")?; - let py_args = PyTuple::new(py, [options.into_py_dict(py)]); - let py_result: Vec<&PyArrayDyn> = - prg.getattr("ground")?.call1(py_args)?.extract()?; - Ok(py_result.into_iter().map(Tensor::from).collect::<_>()) - }) - } - - #[test] - fn decoder_matches() -> anyhow::Result<()> { - log_init(); - let api = Api::new().unwrap(); - let model = api.model("FL33TW00D-HF/whisper-tiny".to_string()); - let path = model.get("tiny_f32.gguf").unwrap(); - let config_path = model.get("config.json").unwrap(); - let config: Config = serde_json::from_slice(&std::fs::read(config_path).unwrap()).unwrap(); - println!("MODEL LOADED FROM: {}", path.display()); - - let dataset = api.dataset("FL33TW00D-HF/ratchet-util".to_string()); - let options = DecodingOptionsBuilder::new().build(); - let hs_npy = dataset.get("jfk_tiny_encoder_hs.npy").unwrap(); - let audio_path = dataset.get("jfk.wav").unwrap(); - - let tokenizer_repo = api.model("openai/whisper-tiny".to_string()); - let tokenizer_path = tokenizer_repo.get("tokenizer.json").unwrap(); - let tokenizer = Tokenizer::from_file(tokenizer_path).unwrap(); - - let mut reader = std::io::BufReader::new(std::fs::File::open(path).unwrap()); - let header = gguf::Header::read(&mut reader).unwrap(); - - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - - let audio_ctx = Tensor::read_npy::(hs_npy, &device)? - .cast(device.compute_precision())? - .resolve()?; - let mut decoder = WhisperDecoder::load(&header, &config, &mut reader, &device)?; - - let mut tokens = vec![50258, 50259, 50359]; - let mut all_tokens = tokens.clone(); - let mut all_logits = vec![]; - let start = std::time::Instant::now(); - while tokens[tokens.len() - 1] != 50257 { - let token_t = - Tensor::from_data(tokens.clone(), shape![1, tokens.len()], device.clone()); - let result = decoder - .schedule([audio_ctx.clone(), token_t])? - .resolve_debug()?; - - let our_logits = result.to(&Device::CPU)?; - let nd_logits = our_logits.to_ndarray_view::(); - println!("ND LOGITS: {:?}", nd_logits); - all_logits.push(Tensor::from( - nd_logits - .slice(s![.., .., ..tokenizer.get_vocab_size(true)]) - .to_owned() - .into_dyn(), - )); - - let sliced = nd_logits - .slice(s![.., -1.., ..tokenizer.get_vocab_size(true)]) - .remove_axis(Axis(1)); - decoder.cache_mut().update(tokens.len()); - - tokens = sliced - .map_axis(Axis(1), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - println!("Token: {:?}", tokens); - all_tokens.extend(tokens.clone()); - } - println!("Took: {:?}", start.elapsed()); - - let u32_tokens: Vec<_> = all_tokens.iter().map(|&x| x as u32).collect(); - let decoded = tokenizer.decode(&u32_tokens, true).unwrap(); - println!("All tokens: {:?}", all_tokens); - println!("Decoded: {}", decoded); - - let ground_logits = ground_truth(&audio_path.to_string_lossy(), options)?; - let all_equal = all_logits - .iter() - .zip(ground_logits.iter()) - .all(|(our, their)| their.all_close(our, 1e-4, 1e-4).is_ok()); - - assert!(all_equal); - - Ok(()) - } -} diff --git a/crates/ratchet-models/src/whisper/encoder.rs b/crates/ratchet-models/src/whisper/encoder.rs deleted file mode 100644 index 482ffe3f..00000000 --- a/crates/ratchet-models/src/whisper/encoder.rs +++ /dev/null @@ -1,250 +0,0 @@ -use std::io::{BufRead, Seek}; - -use ratchet::{DType, Device, Tensor}; -use ratchet_loader::gguf::gguf::Header; -use ratchet_nn::{LayerNorm, Module}; - -use super::{ - config::Config, - residual_block::{ResidualAttentionBlock, ResidualAttentionBlockInputs}, -}; - -#[cfg(target_arch = "wasm32")] -use {crate::ratchet_from_gguf_web, crate::TensorMap}; - -#[derive(Debug, derive_new::new)] -struct ConvBlock { - w: Tensor, - b: Tensor, - stride: usize, - padding: usize, -} - -impl Module for ConvBlock { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let input_dt = input.dt(); - input - .conv1d( - self.w.clone().cast(input_dt)?, - Some(self.b.clone().cast(input_dt)?), - self.stride, - self.padding, - )? - .gelu() - } -} - -#[derive(Debug)] -pub(crate) struct EncoderStem { - conv1: ConvBlock, - conv2: ConvBlock, - pos_embed: Tensor, -} - -impl Module for EncoderStem { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - //Currently do CONV in FP32 due to precision issues in kernel - let convolved = self.conv2.schedule(self.conv1.schedule(input.full()?)?)?; - convolved - .permute(&[0, 2, 1])? - .add(self.pos_embed.clone().full()?) - } -} - -impl EncoderStem { - pub fn load( - header: &Header, - reader: &mut R, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| { - let key = format!("model.encoder.{}", name); - header.tensor(reader, &key, device) - }; - Self::load_inner(lt) - } - - #[cfg(target_arch = "wasm32")] - pub fn from_web( - header: &Header, - tensor_map: &mut TensorMap, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| { - let key = format!("model.encoder.{}", name); - let wt = tensor_map.remove(&key).unwrap(); - ratchet_from_gguf_web(wt, device) - }; - Self::load_inner(lt) - } - - fn load_inner(mut lt: F) -> anyhow::Result - where - F: FnMut(&str) -> anyhow::Result, - { - Ok(Self { - conv1: ConvBlock::new(lt("conv1.weight")?, lt("conv1.bias")?, 1, 1), - conv2: ConvBlock::new(lt("conv2.weight")?, lt("conv2.bias")?, 2, 1), - pos_embed: lt("embed_positions.weight")?, - }) - } -} - -#[derive(Debug)] -pub struct WhisperEncoder { - stem: EncoderStem, - blocks: Vec, - ln_post: LayerNorm, - activation_dt: DType, -} - -impl Module for WhisperEncoder { - type Input = Tensor; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let mut x = self.stem.schedule(input)?.cast(self.activation_dt)?; - - for block in &self.blocks { - let input = ResidualAttentionBlockInputs { - x: x.clone(), - xa: None, - mask: None, - cache: None, - }; - x = block.schedule(input)?; - } - - self.ln_post.schedule(x) - } -} - -impl WhisperEncoder { - #[cfg(target_arch = "wasm32")] - pub fn from_web( - header: &Header, - config: &Config, - tensor_map: &mut TensorMap, - device: &Device, - ) -> anyhow::Result { - let stem = EncoderStem::from_web(header, tensor_map, device)?; - let (n_layers, n_heads) = (config.n_audio_layer, config.n_audio_head); - - let blocks = (0..n_layers) - .fold(Vec::with_capacity(n_layers as _), |mut blocks, i| { - blocks.push(ResidualAttentionBlock::from_web( - header, - tensor_map, - i as _, - n_heads as _, - "encoder", - device, - )); - blocks - }) - .into_iter() - .collect::, _>>()?; - - let mut lt = |name: &str| { - let key = format!("model.encoder.layer_norm.{}", name); - let wt = tensor_map.remove(&key).unwrap(); - ratchet_from_gguf_web(wt, device) - }; - - let activation_dt = blocks[0].mlp.activation_dt(); - - Ok(Self { - stem, - blocks, - ln_post: LayerNorm::new(lt("weight")?, Some(lt("bias")?), 1e-5), - activation_dt, - }) - } - - pub fn load( - header: &Header, - config: &Config, - reader: &mut R, - device: &Device, - ) -> anyhow::Result { - let stem = EncoderStem::load(header, reader, device)?; - let (n_layers, n_heads) = (config.n_audio_layer, config.n_audio_head); - - let blocks = (0..n_layers) - .fold(Vec::with_capacity(n_layers as _), |mut blocks, i| { - blocks.push(ResidualAttentionBlock::load( - header, - reader, - i as _, - n_heads as _, - "encoder", - device, - )); - blocks - }) - .into_iter() - .collect::, _>>()?; - - let activation_dt = blocks[0].mlp.activation_dt(); - - let mut lt = |name: &str| { - let key = format!("model.encoder.layer_norm.{}", name); - header.tensor(reader, &key, device) - }; - - Ok(Self { - stem, - blocks, - ln_post: LayerNorm::new(lt("weight")?, Some(lt("bias")?), 1e-5), - activation_dt, - }) - } -} - -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use hf_hub::api::sync::Api; - use ratchet::{Device, DeviceRequest, Tensor}; - use ratchet_loader::gguf::gguf; - use ratchet_nn::Module; - - use crate::whisper::{config::Config, encoder::WhisperEncoder}; - - fn log_init() { - let _ = env_logger::builder().is_test(true).try_init(); - } - - #[test] - fn encoder_matches() -> anyhow::Result<()> { - log_init(); - let api = Api::new().unwrap(); - let model = api.model("FL33TW00D-HF/whisper-tiny".to_string()); - let model_path = model.get("tiny_f32.gguf").unwrap(); - println!("Path: {}", model_path.display()); - let config_path = model.get("config.json").unwrap(); - - let dataset = api.dataset("FL33TW00D-HF/ratchet-util".to_string()); - let input_npy = dataset.get("jfk_tiny_encoder_input.npy").unwrap(); - let ground_npy = dataset.get("jfk_tiny_encoder_hs.npy").unwrap(); - - let mut reader = std::io::BufReader::new(std::fs::File::open(model_path).unwrap()); - let header = gguf::Header::read(&mut reader).unwrap(); - let config: Config = serde_json::from_slice(&std::fs::read(config_path).unwrap()).unwrap(); - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - - let encoder = WhisperEncoder::load(&header, &config, &mut reader, &device)?; - let input = Tensor::read_npy::(input_npy, &device)?; - - let result = encoder.schedule(input)?.full()?.resolve()?; - let ours = result.to(&Device::CPU)?; - let ground = Tensor::read_npy::(ground_npy, &Device::CPU)?; - println!("OURS: {:#?}", ours); - println!("Ground: {:#?}", ground); - ground.all_close(&ours, 1e-3, 1e-3)?; - - Ok(()) - } -} diff --git a/crates/ratchet-models/src/whisper/logit_mutators/mod.rs b/crates/ratchet-models/src/whisper/logit_mutators/mod.rs deleted file mode 100644 index bf6589ab..00000000 --- a/crates/ratchet-models/src/whisper/logit_mutators/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod timestamp_rules; -pub use timestamp_rules::*; - -use crate::whisper::tokenizer::WhisperTokenizer; -use ratchet::Tensor; - -pub trait LogitMutator { - fn apply( - &self, - logits: Tensor, - tokenizer: &WhisperTokenizer, - tokens: Option<&Tensor>, - ) -> anyhow::Result; -} diff --git a/crates/ratchet-models/src/whisper/logit_mutators/timestamp_rules.rs b/crates/ratchet-models/src/whisper/logit_mutators/timestamp_rules.rs deleted file mode 100644 index bb50ab46..00000000 --- a/crates/ratchet-models/src/whisper/logit_mutators/timestamp_rules.rs +++ /dev/null @@ -1,97 +0,0 @@ -use ndarray::s; -use ndarray_stats::QuantileExt; -use ratchet::{NDArrayExt, Tensor}; - -use super::LogitMutator; -use crate::whisper::tokenizer::WhisperTokenizer; - -#[derive(Debug, derive_new::new)] -pub struct ApplyTimestampRules { - pub sample_begin: usize, - pub max_initial_timestamp_index: Option, -} - -impl LogitMutator for ApplyTimestampRules { - fn apply( - &self, - logits: Tensor, - tokenizer: &WhisperTokenizer, - tokens: Option<&Tensor>, - ) -> anyhow::Result { - let nd_tokens = tokens.unwrap().clone().into_ndarray::(); - let mut nd_logits = logits.into_ndarray::(); - - nd_logits - .slice_mut(s![.., tokenizer.notimestamps() as usize]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - - for k in 0..nd_tokens.shape()[0] { - let sampled_tokens = nd_tokens.slice(s![k, self.sample_begin..]); - let sample_len = sampled_tokens.len(); - - let last_was_timestamp = !sampled_tokens.is_empty() - && sampled_tokens[sample_len - 1] >= tokenizer.timestamp_begin(); - let penultimate_was_timestamp = sampled_tokens.len() < 2 - || sampled_tokens[sample_len - 2] >= tokenizer.timestamp_begin(); - - if last_was_timestamp { - if penultimate_was_timestamp { - nd_logits - .slice_mut(s![k, tokenizer.timestamp_begin()..]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - } else { - nd_logits - .slice_mut(s![k, ..WhisperTokenizer::EOT]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - } - } - - let timestamps = sampled_tokens - .iter() - .filter(|x| **x >= tokenizer.timestamp_begin()) - .collect::>(); - - if !timestamps.is_empty() { - // timestamps shouldn't decrease; forbid timestamp tokens smaller than the last - // also force each segment to have a nonzero length, to prevent infinite looping - let timestamp_last = if last_was_timestamp && !penultimate_was_timestamp { - *timestamps[timestamps.len() - 1] - } else { - timestamps[timestamps.len() - 1] + 1 - }; - nd_logits - .slice_mut(s![k, tokenizer.timestamp_begin()..timestamp_last]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - } - } - if nd_tokens.shape()[1] == self.sample_begin { - // suppress generating non-timestamp tokens at the beginning - nd_logits - .slice_mut(s![.., ..tokenizer.timestamp_begin()]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - - if self.max_initial_timestamp_index.is_some() { - let last_allowed = (tokenizer.timestamp_begin() as usize) - + self.max_initial_timestamp_index.unwrap(); - nd_logits - .slice_mut(s![.., last_allowed + 1..]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - } - } - - let logprobs = nd_logits.log_softmax(1); - for _k in 0..nd_tokens.shape()[0] { - let timestamp_logprob = logprobs - .slice(s![.., tokenizer.timestamp_begin()..]) - .logsumexp(1); - let text_logprobs = logprobs.slice(s![.., ..tokenizer.timestamp_begin()]); - let max_text_token_logprob = text_logprobs.max()?; - if timestamp_logprob > *max_text_token_logprob { - nd_logits - .slice_mut(s![.., ..tokenizer.timestamp_begin()]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - } - } - Ok(Tensor::from(nd_logits)) - } -} diff --git a/crates/ratchet-models/src/whisper/mha.rs b/crates/ratchet-models/src/whisper/mha.rs deleted file mode 100644 index faabdeae..00000000 --- a/crates/ratchet-models/src/whisper/mha.rs +++ /dev/null @@ -1,128 +0,0 @@ -use half::f16; -use num::traits::real::Real; -use ratchet::{rvec, shape, Tensor}; -use ratchet_nn::{KVEntry, Linear, Module}; - -#[derive(Debug)] -pub struct MultiHeadAttention { - q: Linear, - k: Linear, - v: Linear, - o: Linear, - n_heads: usize, - dk: Tensor, -} - -impl MultiHeadAttention { - pub fn new(q: Linear, k: Linear, v: Linear, o: Linear, n_heads: usize) -> MultiHeadAttention { - let n_state = q.w.shape()[1]; - let dk = match q.w.dt().activation_dt() { - ratchet::DType::F16 => { - let dk = f16::from_f32((n_state / n_heads) as f32); - Tensor::from_data( - [dk.powf(f16::from_f32(-0.25))], - shape![1], - q.w.device().clone(), - ) - } - ratchet::DType::F32 => { - let dk = (n_state / n_heads) as f32; - Tensor::from_data([dk.powf(-0.25)], shape![1], q.w.device().clone()) - } - _ => unimplemented!(), - }; - MultiHeadAttention { - q, - k, - v, - o, - n_heads, - dk, - } - } -} - -#[derive(Debug, derive_new::new)] -pub struct MHAInputs { - x: Tensor, - xa: Option, - mask: Option, - cache: Option, - is_causal: bool, -} - -impl Module for MultiHeadAttention { - type Input = MHAInputs; - - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let MHAInputs { - x, - xa, - mask, - cache, - is_causal, - } = input; - - let q = self.q.schedule(x.clone())?; - - let to_project = xa.unwrap_or(x); - let k = self.k.schedule(to_project.clone())?; - let v = self.v.schedule(to_project)?; - - let (k, v) = if let Some(kv) = cache { - let prev_entries = kv.entries; - let k_cache = kv.k_cache.cache(k, 1, prev_entries)?; - let v_cache = kv.v_cache.cache(v, 1, prev_entries)?; - (k_cache, v_cache) - } else { - (k, v) - }; - - self.qkv_attention(q, k, v, mask, is_causal) - } -} - -impl MultiHeadAttention { - fn qkv_attention( - &self, - q: Tensor, - k: Tensor, - v: Tensor, - mask: Option, - is_causal: bool, - ) -> anyhow::Result { - let [bs, n_ctx, n_state]: [usize; 3] = q.shape().try_into()?; - let [k0, k1, _]: [usize; 3] = k.shape().try_into()?; - let [v0, v1, _]: [usize; 3] = v.shape().try_into()?; - let q_dt = q.dt(); - - let hdim = n_state / self.n_heads; - - let qs = shape![bs, n_ctx, self.n_heads, hdim]; - let ks = shape![k0, k1, self.n_heads, hdim]; - let vs = shape![v0, v1, self.n_heads, hdim]; - - let q = q.view(qs)?.permute(&[0, 2, 1, 3])?.mul(self.dk.clone())?; - let k = k.view(ks)?.permute(&[0, 2, 3, 1])?.mul(self.dk.clone())?; - let v = v.view(vs)?.permute(&[0, 2, 1, 3])?; - - let mut qk = q.matmul(k, false, false)?; - - if let Some(m) = mask { - let prepared_mask = if is_causal { - m.slice(&[0..n_ctx, 0..n_ctx])? - } else { - m.clone() - }; - qk = qk.add(prepared_mask)?; - } - qk = qk.full()?; - - let w = qk.softmax(3)?.cast(q_dt)?; - - let s = shape![bs, n_ctx, n_state]; - let wv = w.matmul(v, false, false)?.permute(&[0, 2, 1, 3])?.view(s)?; - - self.o.schedule(wv) - } -} diff --git a/crates/ratchet-models/src/whisper/mlp.rs b/crates/ratchet-models/src/whisper/mlp.rs deleted file mode 100644 index 66af4949..00000000 --- a/crates/ratchet-models/src/whisper/mlp.rs +++ /dev/null @@ -1,23 +0,0 @@ -use ratchet::Tensor; -use ratchet_nn::{Linear, Module}; - -#[derive(Debug, derive_new::new)] -pub struct MLP { - l1: Linear, - l2: Linear, -} - -impl MLP { - pub fn activation_dt(&self) -> ratchet::DType { - self.l1.w.dt().activation_dt() - } -} - -impl Module for MLP { - type Input = Tensor; - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let input_dt = input.dt(); - self.l2 - .schedule(self.l1.schedule(input)?.full()?.gelu()?.cast(input_dt)?) - } -} diff --git a/crates/ratchet-models/src/whisper/mod.rs b/crates/ratchet-models/src/whisper/mod.rs deleted file mode 100644 index bba35c1e..00000000 --- a/crates/ratchet-models/src/whisper/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -mod config; -mod decoder; -mod encoder; -mod logit_mutators; -mod mha; -mod mlp; -mod model; -mod residual_block; -mod samplers; -mod spectrogram; -mod task; - -pub mod options; -pub mod tokenizer; -pub mod transcribe; -pub mod transcript; - -pub use config::Config; -pub use decoder::WhisperDecoder; -pub use encoder::WhisperEncoder; -pub use model::Whisper; diff --git a/crates/ratchet-models/src/whisper/model.rs b/crates/ratchet-models/src/whisper/model.rs deleted file mode 100644 index 90541936..00000000 --- a/crates/ratchet-models/src/whisper/model.rs +++ /dev/null @@ -1,293 +0,0 @@ -use ratchet::{shape, Device, Tensor}; -use ratchet_loader::gguf::gguf::Header; -use ratchet_nn::Module; - -use ndarray::{s, Dimension}; -use ndarray_stats::QuantileExt; -use ratchet::NDArrayExt; - -#[cfg(not(target_arch = "wasm32"))] -use { - hf_hub::api::sync::Api, - std::io::{BufRead, Seek}, -}; - -#[cfg(target_arch = "wasm32")] -use {crate::TensorMap, ratchet_hub::ApiBuilder, ratchet_hub::RepoType, wasm_bindgen::prelude::*}; - -use crate::registry::WhisperVariants; -use crate::whisper::{options::Language, task::DecodingTask, tokenizer::WhisperTokenizer}; - -use super::encoder::WhisperEncoder; -use super::spectrogram::SpectrogramGenerator; -use super::{config::Config, decoder::WhisperDecoder}; - -#[derive(Debug)] -pub struct Whisper { - pub specgen: SpectrogramGenerator, - pub encoder: WhisperEncoder, - pub decoder: WhisperDecoder, - pub config: Config, - pub device: Device, -} - -impl Whisper { - #[cfg(not(target_arch = "wasm32"))] - pub fn load( - header: Header, - variant: WhisperVariants, - reader: &mut R, - device: Device, - ) -> anyhow::Result { - let mel_fname = match variant { - WhisperVariants::DistilLargeV3 | WhisperVariants::LargeV3 => "melfilters128.bytes", - _ => "melfilters.bytes", - }; - let mel_bytes = Self::fetch_resource(&variant, mel_fname)?; - let mut mel_filters = vec![0f32; mel_bytes.len() / 4]; - ::read_f32_into( - &mel_bytes, - &mut mel_filters, - ); - let specgen = SpectrogramGenerator::new(mel_filters); - - let config: Config = - serde_json::from_slice(&Self::fetch_resource(&variant, "config.json")?)?; - let encoder = WhisperEncoder::load(&header, &config, reader, &device)?; - let decoder = WhisperDecoder::load(&header, &config, reader, &device)?; - - Ok(Self { - specgen, - encoder, - decoder, - config, - device, - }) - } - - #[cfg(target_arch = "wasm32")] - pub async fn from_web( - header: Header, - mut tensors: TensorMap, - variant: WhisperVariants, - ) -> anyhow::Result { - let device = Device::request_device(ratchet::DeviceRequest::GPU).await?; - let mel_fname = match variant { - WhisperVariants::DistilLargeV3 | WhisperVariants::LargeV3 => "melfilters128.bytes", - _ => "melfilters.bytes", - }; - let mel_bytes = Self::fetch_resource(&variant, mel_fname).await.unwrap(); - let mut mel_filters = vec![0f32; mel_bytes.len() / 4]; - ::read_f32_into( - &mel_bytes, - &mut mel_filters, - ); - let specgen = SpectrogramGenerator::new(mel_filters); - - let config: Config = - serde_json::from_slice(&Self::fetch_resource(&variant, "config.json").await.unwrap())?; - - let encoder = WhisperEncoder::from_web(&header, &config, &mut tensors, &device)?; - let decoder = WhisperDecoder::from_web(&header, &config, &mut tensors, &device)?; - - Ok(Self { - specgen, - encoder, - decoder, - config, - device, - }) - } - - #[cfg(target_arch = "wasm32")] - pub async fn fetch_resource( - variant: &WhisperVariants, - resource: &str, - ) -> Result, JsValue> { - let repo_id = variant.repo_id(); - let model_repo = ApiBuilder::from_hf(repo_id, RepoType::Model).build(); - let resp = model_repo.get(resource).await; - match resp { - Ok(data) => Ok(data.to_vec()), - Err(_) => Err(JsError::new(format!("Failed to fetch resource").as_str()).into()), - } - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn fetch_resource(variant: &WhisperVariants, resource: &str) -> anyhow::Result> { - let api = Api::new().unwrap(); - let repo_id = variant.repo_id(); - - let repo = api.model(repo_id.to_string()); - Ok(std::fs::read(repo.get(resource).unwrap())?) - } -} - -impl Whisper { - pub fn is_multilingual(&self) -> bool { - self.config.n_vocab >= 51865 - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn detect_language(&mut self, _mel: Tensor) -> anyhow::Result { - panic!("DETECT LANGUAGE NOT IMPLEMENTED"); - let audio_ctx = self.encoder.schedule(_mel)?.resolve()?; - let sot = Tensor::from_data([WhisperTokenizer::SOT], shape![1, 1], self.device.clone()); - - let logits = self.decoder.schedule([audio_ctx, sot])?.full()?.resolve()?; - self.decoder.reset(); - - let cpu_logits = logits.to(&Device::CPU)?; - println!("LOGITS: {:?}", cpu_logits); - let logits = DecodingTask::slice_logits(cpu_logits, self.config.n_vocab); - - let device = logits.device().clone(); - let mut nd_logits = logits.into_ndarray::(); - - let languages_end = if self.config.n_vocab == 51865 { - 50358 - } else if self.config.n_vocab == 51866 { - 50359 - } else { - panic!("Unsupported number of tokens") - }; - - nd_logits - .slice_mut(s![.., ..WhisperTokenizer::LANGUAGES_BEGIN]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - - nd_logits - .slice_mut(s![.., languages_end..]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - - let language_tokens_probs = nd_logits.softmax(nd_logits.ndim() - 1); - - let argmax_dims = language_tokens_probs.argmax_skipnan().unwrap(); - let argmax: u32 = argmax_dims[argmax_dims.ndim() - 1] as _; - let lang_t = Tensor::from_data([argmax], shape![1], device); - - Ok(Language::Token(lang_t.item())) - } - - #[cfg(target_arch = "wasm32")] - pub async fn detect_language(&mut self, mel: Tensor) -> anyhow::Result { - panic!("DETECT LANGUAGE NOT IMPLEMENTED"); - let audio_ctx = self.encoder.schedule(mel)?.resolve()?; - let sot = Tensor::from_data([WhisperTokenizer::SOT], shape![1, 1], self.device.clone()); - - let logits = self.decoder.schedule([audio_ctx, sot])?.resolve()?; - self.decoder.reset(); - - let cpu_logits = logits.to(&Device::CPU).await?; - let logits = DecodingTask::slice_logits(cpu_logits, self.config.n_vocab as usize); - - let device = logits.device().clone(); - let mut nd_logits = logits.into_ndarray::(); - - let languages_end = if self.config.n_vocab == 51865 { - 50358 - } else if self.config.n_vocab == 51866 { - 50359 - } else { - panic!("Unsupported number of tokens") - }; - - nd_logits - .slice_mut(s![.., ..WhisperTokenizer::LANGUAGES_BEGIN]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - - nd_logits - .slice_mut(s![.., languages_end..]) - .map_inplace(move |el| *el = f32::NEG_INFINITY); - - let language_tokens_probs = nd_logits.softmax(nd_logits.ndim() - 1); - - let argmax_dims = language_tokens_probs.argmax_skipnan().unwrap(); - let argmax: u32 = argmax_dims[argmax_dims.ndim() - 1] as _; - let lang_t = Tensor::from_data([argmax], shape![1], device); - - Ok(Language::Token(lang_t.item())) - } -} - -#[cfg(all(test, not(target_arch = "wasm32")))] -mod tests { - use std::path::PathBuf; - - use hf_hub::api::sync::Api; - use ratchet::{Device, DeviceRequest}; - use ratchet_loader::gguf::gguf; - - use crate::{ - registry::WhisperVariants, - whisper::{ - model::Whisper, options::DecodingOptionsBuilder, transcribe::transcribe, - transcript::StreamedSegment, - }, - }; - - fn log_init() { - let _ = env_logger::builder().is_test(true).try_init(); - } - - fn load_sample(path: PathBuf) -> Vec { - let mut reader = hound::WavReader::open(path).unwrap(); - reader - .samples::() - .map(|x| x.unwrap() as f32 / 32768.0) - .collect::>() - } - - const MM0_Q8_GROUND: [u32; 196] = [ - 50364, 639, 307, 264, 4532, 3479, 13460, 264, 881, 34674, 5932, 30340, 295, 5116, 2065, - 5729, 13, 50524, 50524, 1981, 472, 575, 12023, 4365, 337, 257, 1702, 6034, 3028, 1523, - 1804, 4651, 4532, 3479, 50668, 50668, 8963, 6742, 300, 1619, 257, 3804, 5214, 2610, 5214, - 6383, 2643, 5214, 293, 544, 2176, 50816, 50816, 8963, 21800, 281, 747, 604, 1081, 293, 456, - 366, 867, 34674, 3190, 281, 862, 365, 309, 1184, 50948, 50948, 472, 1487, 365, 1080, 1065, - 2121, 11377, 4532, 3479, 5864, 293, 1019, 5456, 4122, 300, 51084, 51084, 544, 20095, 1286, - 13, 51134, 51134, 30062, 264, 13436, 574, 412, 264, 10155, 35310, 587, 264, 3874, 14701, - 1068, 281, 264, 7267, 3096, 2541, 428, 1032, 51264, 51264, 281, 818, 5675, 5300, 264, 281, - 3623, 293, 613, 1081, 300, 311, 3318, 1214, 281, 1254, 257, 4532, 3479, 51404, 51404, 1002, - 13, 51454, 51454, 1222, 3479, 8963, 6742, 300, 311, 1270, 257, 7195, 5870, 370, 6239, - 13600, 370, 309, 1177, 380, 51552, 51552, 321, 2607, 1488, 68, 322, 257, 8963, 264, 16026, - 4532, 8379, 293, 4532, 3479, 8963, 6742, 300, 311, 51696, 50364, 3718, 14759, 490, 3114, - 996, 264, 4356, 436, 366, 264, 1101, 436, 366, 13, 50500, - ]; - - #[test] - pub fn whisper_end_to_end() { - log_init(); - let api = Api::new().unwrap(); - let model = api.model("FL33TW00D-HF/whisper-tiny".to_string()); - let model_path = model.get("tiny_q8_0.gguf").unwrap(); - let variant = WhisperVariants::Tiny; - println!("PATH: {:?}", model_path.display()); - - let dataset = api.dataset("FL33TW00D-HF/ratchet-util".to_string()); - let audio_path = dataset.get("mm0.wav").unwrap(); - let samples = load_sample(audio_path); - - let options = DecodingOptionsBuilder::new() - .language("en".to_string()) - .build(); - let mut reader = std::io::BufReader::new(std::fs::File::open(model_path).unwrap()); - let header = gguf::Header::read(&mut reader).unwrap(); - - let device = Device::request_device(DeviceRequest::GPU).unwrap(); - - let mut whisper = Whisper::load(header, variant, &mut reader, device).unwrap(); - - let empty_cb: Option = None; - let transcript = transcribe(&mut whisper, samples, options, empty_cb).unwrap(); - - let all_tokens = transcript - .segments - .iter() - .flat_map(|s| s.tokens.clone().into_iter()) - .collect::>(); - - println!("{}", transcript.formatted.unwrap()); - println!("Processing time: {:?}", transcript.processing_time); - //assert_eq!(all_tokens, MM0_Q8_GROUND); - } -} diff --git a/crates/ratchet-models/src/whisper/options.rs b/crates/ratchet-models/src/whisper/options.rs deleted file mode 100644 index 0f5ade77..00000000 --- a/crates/ratchet-models/src/whisper/options.rs +++ /dev/null @@ -1,310 +0,0 @@ -use crate::whisper::tokenizer::WhisperTokenizer; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - -#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] -pub enum Language { - String(String), - Token(i32), -} - -#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] -pub enum Prompt { - Text(String), - Tokens(Vec), -} - -#[cfg_attr( - target_arch = "wasm32", - wasm_bindgen, - derive(serde::Serialize, serde::Deserialize) -)] -#[derive(Debug, Clone, Copy)] -pub enum Task { - Transcribe, - Translate, -} - -impl Task { - pub fn as_token(&self, tokenizer: &WhisperTokenizer) -> i32 { - match self { - Task::Transcribe => tokenizer.transcribe(), - Task::Translate => tokenizer.translate(), - } - } -} - -#[allow(dead_code)] -#[cfg_attr( - target_arch = "wasm32", - wasm_bindgen, - derive(serde::Serialize, serde::Deserialize) -)] -#[derive(Debug, Clone)] -pub struct DecodingOptions { - pub(crate) task: Task, // default: "transcribe" - pub(crate) language: Option, // default: None - pub(crate) temperature: f32, // default: 0.0 - pub(crate) sample_len: Option, // default: None - pub(crate) best_of: Option, // default: None - pub(crate) beam_size: Option, // default: None - pub(crate) patience: Option, // default: None - pub(crate) length_penalty: Option, // default: None - pub(crate) prompt: Option, // default: None - pub(crate) prefix: Option, // default: None - pub(crate) suppress_tokens: Option>, // default: Some("-1".to_string()) - pub(crate) suppress_blank: bool, // default: true - pub(crate) without_timestamps: bool, // default: false - pub(crate) max_initial_timestamp: Option, // default: Some(1.0) - pub(crate) time_offset: Option, // default: None -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] -pub struct DecodingOptionsBuilder { - task: Option, - language: Option, - temperature: Option, - sample_len: Option, - best_of: Option, - beam_size: Option, - patience: Option, - length_penalty: Option, - prompt: Option, - prefix: Option, - suppress_tokens: Option>, - suppress_blank: Option, - without_timestamps: Option, - max_initial_timestamp: Option, - time_offset: Option, -} - -impl Default for DecodingOptionsBuilder { - fn default() -> Self { - Self::new() - } -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] -impl DecodingOptionsBuilder { - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))] - pub fn new() -> DecodingOptionsBuilder { - DecodingOptionsBuilder { - task: Some(Task::Transcribe), - language: Some(String::from("en")), - temperature: Some(0.0), - sample_len: None, - best_of: None, - beam_size: None, - patience: None, - length_penalty: None, - prompt: None, - prefix: None, - suppress_tokens: Some(vec![-1]), - suppress_blank: Some(true), - max_initial_timestamp: Some(1.0), - without_timestamps: Some(false), - time_offset: None, - } - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setTask"))] - pub fn task(mut self, task: Task) -> Self { - self.task = Some(task); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setLanguage"))] - pub fn language(mut self, language: String) -> Self { - self.language = Some(language); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setTemperature"))] - pub fn temperature(mut self, temperature: f32) -> Self { - self.temperature = Some(temperature); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setSampleLen"))] - pub fn sample_len(mut self, sample_len: u32) -> Self { - self.sample_len = Some(sample_len); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setBestOf"))] - pub fn best_of(mut self, best_of: u32) -> Self { - self.best_of = Some(best_of); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setBeamSize"))] - pub fn beam_size(mut self, beam_size: u32) -> Self { - self.beam_size = Some(beam_size); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setPatience"))] - pub fn patience(mut self, patience: f32) -> Self { - self.patience = Some(patience); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setLengthPenalty"))] - pub fn length_penalty(mut self, length_penalty: f32) -> Self { - self.length_penalty = Some(length_penalty); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setPrompt"))] - pub fn prompt(mut self, prompt: String) -> Self { - self.prompt = Some(prompt); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setPrefix"))] - pub fn prefix(mut self, prefix: String) -> Self { - self.prefix = Some(prefix); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setSuppressTokens"))] - pub fn suppress_tokens(mut self, suppress_tokens: Vec) -> Self { - self.suppress_tokens = Some(suppress_tokens); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setSuppressBlank"))] - pub fn suppress_blank(mut self, suppress_blank: bool) -> Self { - self.suppress_blank = Some(suppress_blank); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setWithoutTimestamps"))] - pub fn without_timestamps(mut self, without_timestamps: bool) -> Self { - self.without_timestamps = Some(without_timestamps); - self - } - - #[cfg_attr( - target_arch = "wasm32", - wasm_bindgen(js_name = "setMaxInitialTimestamp") - )] - pub fn max_initial_timestamp(mut self, max_initial_timestamp: f32) -> Self { - self.max_initial_timestamp = Some(max_initial_timestamp); - self - } - - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = "setTimeOffset"))] - pub fn time_offset(mut self, time_offset: f64) -> Self { - self.time_offset = Some(time_offset); - self - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn build(&self) -> DecodingOptions { - DecodingOptions { - task: self.task.unwrap_or(Task::Transcribe), - language: self.language.clone().map(Language::String), - temperature: self.temperature.unwrap_or(0.0), - sample_len: self.sample_len, - best_of: self.best_of, - beam_size: self.beam_size, - patience: self.patience, - length_penalty: self.length_penalty, - prompt: self.prompt.clone().map(Prompt::Text), - prefix: self.prefix.clone(), - suppress_tokens: self.suppress_tokens.clone(), - suppress_blank: self.suppress_blank.unwrap_or(true), - without_timestamps: self.without_timestamps.unwrap_or(false), - max_initial_timestamp: self.max_initial_timestamp, - time_offset: self.time_offset, - } - } - - #[cfg(target_arch = "wasm32")] - pub fn build(&self) -> JsValue { - let options = DecodingOptions { - task: self.task.unwrap_or(Task::Transcribe), - language: self.language.clone().map(Language::String), - temperature: self.temperature.unwrap_or(0.0), - sample_len: self.sample_len, - best_of: self.best_of, - beam_size: self.beam_size, - patience: self.patience, - length_penalty: self.length_penalty, - prompt: self.prompt.clone().map(Prompt::Text), - prefix: self.prefix.clone(), - suppress_tokens: self.suppress_tokens.clone(), - suppress_blank: self.suppress_blank.unwrap_or(true), - without_timestamps: self.without_timestamps.unwrap_or(false), - max_initial_timestamp: self.max_initial_timestamp, - time_offset: self.time_offset, - }; - serde_wasm_bindgen::to_value(&options).unwrap() - } -} - -cfg_if::cfg_if! { - if #[cfg(all(not(target_arch = "wasm32"), test))] { - use pyo3::types::{IntoPyDict, PyDict}; - use pyo3::Python; - use pyo3::types::PyString; - use pyo3::IntoPy; - use pyo3::PyObject; - - impl IntoPy for Task { - fn into_py(self, py: Python) -> PyObject { - let task = match self { - Task::Transcribe => "transcribe", - Task::Translate => "translate", - }; - PyString::new(py, task).into() - } - } - - impl IntoPy for Language { - fn into_py(self, py: Python) -> PyObject { - let language = match self { - Language::String(s) => s, - Language::Token(t) => t.to_string(), - }; - PyString::new(py, &language).into() - } - } - - impl IntoPy for Prompt { - fn into_py(self, py: Python) -> PyObject { - match self { - Prompt::Text(s) => PyString::new(py, &s).into(), - Prompt::Tokens(t) => t.into_py(py), - } - } - } - - impl IntoPyDict for DecodingOptions { - fn into_py_dict(self, py: Python) -> &pyo3::types::PyDict { - let dict = PyDict::new(py); - let supress_tokens_string = self.suppress_tokens.map(|v| v.iter().map(|t| t.to_string()).collect::>().join(",")); - - let _ = dict.set_item("task", self.task.into_py(py)); - let _ = dict.set_item("language", self.language.map_or_else(|| py.None(), |v| v.into_py(py))); - let _ = dict.set_item("temperature", self.temperature.into_py(py)); - let _ = dict.set_item("sample_len", self.sample_len.map_or_else(|| py.None(), |v| v.into_py(py))); - let _ = dict.set_item("best_of", self.best_of.map_or_else(|| py.None(), |v| v.into_py(py))); - let _ = dict.set_item("beam_size", self.beam_size.map_or_else(|| py.None(), |v| v.into_py(py))); - let _ = dict.set_item("patience", self.patience.map_or_else(|| py.None(), |v| v.into_py(py))); - let _ = dict.set_item("length_penalty", self.length_penalty.map_or_else(|| py.None(), |v| v.into_py(py))); - let _ = dict.set_item("prompt", self.prompt.map_or_else(|| py.None(), |v| v.into_py(py))); - let _ = dict.set_item("prefix", self.prefix.map_or_else(|| py.None(), |v| v.into_py(py))); - let _ = dict.set_item("suppress_tokens", supress_tokens_string.map_or_else(|| py.None(), |v| v.into_py(py))); - let _ = dict.set_item("suppress_blank", self.suppress_blank.into_py(py)); - let _ = dict.set_item("without_timestamps", self.without_timestamps.into_py(py)); - let _ = dict.set_item("max_initial_timestamp", self.max_initial_timestamp.map_or_else(|| py.None(), |v| v.into_py(py))); - - dict - } - } - } -} diff --git a/crates/ratchet-models/src/whisper/residual_block.rs b/crates/ratchet-models/src/whisper/residual_block.rs deleted file mode 100644 index 97d0fde3..00000000 --- a/crates/ratchet-models/src/whisper/residual_block.rs +++ /dev/null @@ -1,161 +0,0 @@ -use super::{mha::*, mlp::MLP}; -use ratchet::{Device, Tensor}; -use ratchet_loader::gguf::gguf::Header; -use ratchet_nn::{KVEntry, LayerNorm, Linear, Module}; -use std::io::{BufRead, Seek}; - -#[cfg(target_arch = "wasm32")] -use {crate::ratchet_from_gguf_web, crate::TensorMap}; - -#[derive(Debug)] -pub struct ResidualAttentionBlock { - attn_ln: LayerNorm, - attn: MultiHeadAttention, - x_attn_ln: Option, - x_attn: Option, - mlp_ln: LayerNorm, - pub mlp: MLP, -} - -#[derive(Debug, derive_new::new)] -pub struct ResidualAttentionBlockInputs { - pub x: Tensor, - pub xa: Option, - pub mask: Option, - pub cache: Option, -} - -impl Module for ResidualAttentionBlock { - type Input = ResidualAttentionBlockInputs; - fn schedule(&self, input: Self::Input) -> anyhow::Result { - let ResidualAttentionBlockInputs { x, xa, mask, cache } = input; - - let attn_ln = self.attn_ln.schedule(x.clone())?; - let self_attn = - self.attn - .schedule(MHAInputs::new(attn_ln, None, mask.clone(), cache, true))?; - - let mut attn = x.add(self_attn)?; - - if let Some(ref xa_blck) = self.x_attn { - if let Some(xa_ln) = &self.x_attn_ln { - let x_attn_ln = xa_ln.schedule(attn.clone())?; - let x_attn = - xa_blck.schedule(MHAInputs::new(x_attn_ln, xa.clone(), None, None, false))?; - attn = x_attn.add(attn.clone())?; - } - } - let mlp_ln = self.mlp_ln.schedule(attn.clone())?; - let mlp = self.mlp.schedule(mlp_ln)?; - mlp.add(attn) - } -} - -impl ResidualAttentionBlock { - pub fn load( - header: &Header, - reader: &mut R, - layer_index: usize, - n_heads: usize, - prefix: &str, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| { - let key = format!("model.{}.layers.{}.{}", prefix, layer_index, name); - header.tensor(reader, &key, device) - }; - Self::load_inner(lt, prefix, n_heads) - } - - #[cfg(target_arch = "wasm32")] - pub fn from_web( - header: &Header, - tensor_map: &mut TensorMap, - layer_index: usize, - n_heads: usize, - prefix: &str, - device: &Device, - ) -> anyhow::Result { - let lt = |name: &str| { - let key = format!("model.{}.layers.{}.{}", prefix, layer_index, name); - let tensor = tensor_map - .remove(&key) - .ok_or_else(|| anyhow::anyhow!("missing tensor"))?; - ratchet_from_gguf_web(tensor, device) - }; - Self::load_inner(lt, prefix, n_heads) - } - - fn load_inner(mut lt: F, prefix: &str, n_heads: usize) -> anyhow::Result - where - F: FnMut(&str) -> anyhow::Result, - { - let attn_ln = LayerNorm::new( - lt("self_attn_layer_norm.weight")?, - Some(lt("self_attn_layer_norm.bias")?), - 1e-5, - ); - //model.encoder.layers.0.self_attn.v_proj.weight - let attn = MultiHeadAttention::new( - Linear::new( - lt("self_attn.q_proj.weight")?, - Some(lt("self_attn.q_proj.bias")?), - ), - Linear::new(lt("self_attn.k_proj.weight")?, None), - Linear::new( - lt("self_attn.v_proj.weight")?, - Some(lt("self_attn.v_proj.bias")?), - ), - Linear::new( - lt("self_attn.out_proj.weight")?, - Some(lt("self_attn.out_proj.bias")?), - ), - n_heads, - ); - - let (x_attn_ln, x_attn) = if prefix == "decoder" { - let x_attn_ln = LayerNorm::new( - lt("encoder_attn_layer_norm.weight")?, - Some(lt("encoder_attn_layer_norm.bias")?), - 1e-5, - ); - let x_attn = MultiHeadAttention::new( - Linear::new( - lt("encoder_attn.q_proj.weight")?, - Some(lt("encoder_attn.q_proj.bias")?), - ), - Linear::new(lt("encoder_attn.k_proj.weight")?, None), - Linear::new( - lt("encoder_attn.v_proj.weight")?, - Some(lt("encoder_attn.v_proj.bias")?), - ), - Linear::new( - lt("encoder_attn.out_proj.weight")?, - Some(lt("encoder_attn.out_proj.bias")?), - ), - n_heads, - ); - (Some(x_attn_ln), Some(x_attn)) - } else { - (None, None) - }; - - let mlp_ln = LayerNorm::new( - lt("final_layer_norm.weight")?, - Some(lt("final_layer_norm.bias")?), - 1e-5, - ); - let mlp = MLP::new( - Linear::new(lt("fc1.weight")?, Some(lt("fc1.bias")?)), - Linear::new(lt("fc2.weight")?, Some(lt("fc2.bias")?)), - ); - Ok(Self { - attn_ln, - attn, - x_attn_ln, - x_attn, - mlp_ln, - mlp, - }) - } -} diff --git a/crates/ratchet-models/src/whisper/samplers/greedy.rs b/crates/ratchet-models/src/whisper/samplers/greedy.rs deleted file mode 100644 index 7635a409..00000000 --- a/crates/ratchet-models/src/whisper/samplers/greedy.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::whisper::task::DecodeError; -use crate::whisper::tokenizer::WhisperTokenizer; - -use ndarray::Axis; -use ndarray_stats::QuantileExt; -use ratchet::Tensor; - -pub struct GreedySampler; - -impl GreedySampler { - pub fn sample( - mut tokens: Vec, - logits: Tensor, - ) -> Result<(Tensor, Vec, bool), DecodeError> { - let nd_logits = logits.to_ndarray_view::(); - let next_tokens = nd_logits - .map_axis(Axis(1), |row| row.argmax_skipnan()) - .iter() - .map(|r| { - r.as_ref() - .map_err(|_| DecodeError::InvalidLogits) - .map(|v| *v as i32) - }) - .collect::, DecodeError>>()?; - - tokens.extend_from_slice(&next_tokens); - let completed = tokens[tokens.len() - 1] == WhisperTokenizer::EOT; - Ok((logits, tokens, completed)) - } -} diff --git a/crates/ratchet-models/src/whisper/samplers/mod.rs b/crates/ratchet-models/src/whisper/samplers/mod.rs deleted file mode 100644 index c6c9bc48..00000000 --- a/crates/ratchet-models/src/whisper/samplers/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod greedy; - -pub use greedy::*; diff --git a/crates/ratchet-models/src/whisper/spectrogram.rs b/crates/ratchet-models/src/whisper/spectrogram.rs deleted file mode 100644 index ad440f74..00000000 --- a/crates/ratchet-models/src/whisper/spectrogram.rs +++ /dev/null @@ -1,166 +0,0 @@ -//Adapted from: https://github.com/tanmayb123/OpenAI-Whisper-CoreML -use ndarray::{Array1, Array2}; -use ndarray_stats::QuantileExt; -use num::complex::Complex; -use ratchet::Tensor; -use realfft::{RealFftPlanner, RealToComplex}; -use std::f32::consts::PI; -use std::sync::Arc; - -pub static SAMPLE_RATE: usize = 16000; -pub static N_FFT: usize = 400; -pub static HOP_LENGTH: usize = 160; -pub static CHUNK_LENGTH: usize = 30; -pub static N_AUDIO_CTX: usize = 1500; //same for all -pub static N_SAMPLES: usize = SAMPLE_RATE * CHUNK_LENGTH; // 480000 -pub static N_FRAMES: usize = N_SAMPLES / HOP_LENGTH; // 3000 -pub static FFT_PAD: usize = N_FFT / 2; - -#[derive(Debug, thiserror::Error)] -pub enum AudioError { - #[error("Audio must be 30 seconds long (with stft padding): {0} != {1}")] - InvalidLength(usize, usize), - #[error("Invalid audio provided: {0}")] - InvalidAudio(#[from] anyhow::Error), -} - -pub struct SpectrogramGenerator { - fft_plan: Arc>, - hann_window: Array1, - mels: Array2, -} - -impl std::fmt::Debug for SpectrogramGenerator { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SpectrogramGenerator").finish() - } -} - -impl SpectrogramGenerator { - pub fn new(mels: Vec) -> Self { - let mut planner = RealFftPlanner::new(); - let n_mels = mels.len() / (N_FFT / 2 + 1); - Self { - fft_plan: planner.plan_fft_forward(N_FFT), - hann_window: Self::hann_window(), - mels: Array2::from_shape_vec((n_mels, N_FFT / 2 + 1), mels).unwrap(), - } - } - - fn hann_window() -> Array1 { - let window = (0..N_FFT) - .map(|i| (i as f32 * 2.0 * PI) / N_FFT as f32) - .map(|i| (1.0 - i.cos()) / 2.0) - .collect::>(); - Array1::from(window) - } - - fn fft(&self, audio: &[f32]) -> Vec> { - let mut input = Array1::from_vec(audio.to_vec()); - input *= &self.hann_window; - let mut spectrum = self.fft_plan.make_output_vec(); - self.fft_plan - .process(input.as_slice_mut().unwrap(), &mut spectrum) - .unwrap(); - spectrum - } - - fn mel_spectrogram(&self, audio: &[f32]) -> Tensor { - let n_frames = (audio.len() - N_FFT) / HOP_LENGTH; - let right_padding = N_SAMPLES + FFT_PAD; //padding is all 0s, so we can ignore it - - let mut spectrogram = Array2::::zeros((201, n_frames)); - for i in (0..audio.len() - right_padding).step_by(HOP_LENGTH) { - if i / HOP_LENGTH >= n_frames { - break; - } - let fft = self.fft(&audio[i..i + N_FFT]); - let spectrogram_col = fft.iter().map(|c| c.norm_sqr()).collect::>(); - spectrogram - .column_mut(i / HOP_LENGTH) - .assign(&spectrogram_col); - } - - let mut mel_spec = self.mels.dot(&spectrogram); - mel_spec.mapv_inplace(|x| x.max(1e-10).log10()); - let max = *mel_spec.max().unwrap(); - mel_spec.mapv_inplace(|x| (x.max(max - 8.0) + 4.0) / 4.0); - let expanded = mel_spec.insert_axis(ndarray::Axis(0)); - Tensor::from(expanded.into_dyn()) - } - - pub fn generate(&self, audio: Vec) -> Result { - if audio.is_empty() { - return Err(AudioError::InvalidAudio(anyhow::anyhow!( - "Audio must be non-empty" - ))); - } - let padded = Self::pad_audio(audio, N_SAMPLES); - Ok(self.mel_spectrogram(&padded)) - } - - //The padding done by OAI is as follows: - //1. First explicitly pad with (CHUNK_LENGTH * SAMPLE_RATE) (480,000) zeros - //2. Then perform a reflection padding of FFT_PAD (200) samples on each side - // This must be done with care, because we have already performed the explicit padding - // the pre-padding will contain non-zero values, but the post-padding must be zero - pub fn pad_audio(audio: Vec, padding: usize) -> Vec { - let padded_len = FFT_PAD + audio.len() + padding + FFT_PAD; - let mut padded_samples = vec![0.0; padded_len]; - - let mut reflect_padding = vec![0.0; FFT_PAD]; - for i in 0..FFT_PAD { - reflect_padding[i] = audio[FFT_PAD - i]; - } - - padded_samples[0..FFT_PAD].copy_from_slice(&reflect_padding); - padded_samples[FFT_PAD..(FFT_PAD + audio.len())].copy_from_slice(&audio); - padded_samples - } -} - -#[cfg(all(test, feature = "pyo3", not(target_arch = "wasm32")))] -mod tests { - use super::SpectrogramGenerator; - use hf_hub::api::sync::Api; - use ratchet::test_util::run_py_prg; - use ratchet::DType; - use std::path::PathBuf; - - const MAX_DIFF: f32 = 5e-5; - - pub fn load_npy(path: PathBuf) -> Vec { - let bytes = std::fs::read(path).unwrap(); - npyz::NpyFile::new(&bytes[..]).unwrap().into_vec().unwrap() - } - - fn load_sample(path: PathBuf) -> Vec { - let mut reader = hound::WavReader::open(path).unwrap(); - reader - .samples::() - .map(|x| x.unwrap() as f32 / 32768.0) - .collect::>() - } - - #[test] - fn spectrogram_matches() { - let api = Api::new().unwrap(); - let repo = api.dataset("FL33TW00D-HF/ratchet-util".to_string()); - let gb0 = repo.get("erwin_jp.wav").unwrap(); - let mels = repo.get("mel_filters_128.npy").unwrap(); - let prg = format!( - r#" -import whisper -import numpy as np -def ground_truth(): - audio = whisper.load_audio("{}") - return whisper.log_mel_spectrogram(audio, n_mels=128, padding=480000).numpy()[np.newaxis] -"#, - gb0.to_str().unwrap() - ); - let ground_truth = run_py_prg(prg.to_string(), &[], &[], DType::F32).unwrap(); - let generator = SpectrogramGenerator::new(load_npy(mels)); - let result = generator.generate(load_sample(gb0)).unwrap(); - ground_truth.all_close(&result, MAX_DIFF, MAX_DIFF).unwrap(); - } -} diff --git a/crates/ratchet-models/src/whisper/task.rs b/crates/ratchet-models/src/whisper/task.rs deleted file mode 100644 index c3d698fe..00000000 --- a/crates/ratchet-models/src/whisper/task.rs +++ /dev/null @@ -1,330 +0,0 @@ -use super::{ - decoder::WhisperDecoder, logit_mutators::*, samplers::*, spectrogram::*, - tokenizer::WhisperTokenizer, transcript::*, -}; -use crate::whisper::options::{DecodingOptions, Prompt}; -use ndarray::{s, Axis}; -use ratchet::{shape, Device, Tensor}; -use ratchet_nn::Module; - -#[derive(Debug, thiserror::Error)] -pub enum DecodeError { - #[error("No valid logits found")] - InvalidLogits, - #[error("Tokenizer error: {0}")] - TokenizerError(#[from] tokenizers::Error), - #[error("Unknown error: {0}")] - UnknownError(#[from] anyhow::Error), - #[error("Failed to resolve tensor: {0}")] - TensorResolveError(#[from] ratchet::TensorError), -} - -pub struct DecodingTask { - tokenizer: WhisperTokenizer, - options: DecodingOptions, - sample_len: u32, - logit_mutators: Vec>, - initial_tokens: Option>, - initial_tokens_len: Option, -} - -impl DecodingTask { - fn get_initial_tokens(&self) -> Vec { - let mut init_tokens = self.tokenizer.sot_sequence(); - if let Some(prompt) = &self.options.prompt { - let prompt_tokens = match prompt { - Prompt::Tokens(tokens) => tokens.clone(), - Prompt::Text(text) => self - .tokenizer - .encode(format!(" {}", text).as_str(), false) - .unwrap(), - }; - let max_prompt_length = 448 / 2 - 1; // equivalent to self.n_ctx // 2 - 1 in python - let prompt_length = prompt_tokens.len().min(max_prompt_length); - let mut tokens = vec![self.tokenizer.sot_prev()]; - tokens.extend_from_slice(&prompt_tokens[prompt_tokens.len() - prompt_length..]); - tokens.extend(init_tokens); - init_tokens = tokens; - } - init_tokens - } - - pub fn new(options: DecodingOptions, tokenizer: WhisperTokenizer) -> Self { - let sample_len = options.sample_len.unwrap_or(256); - let _selected_lang = options.language.as_ref().unwrap(); - let max_initial_timestamp = options.max_initial_timestamp; - let mut task = DecodingTask { - tokenizer, - options, - logit_mutators: vec![], - sample_len, - initial_tokens: None, - initial_tokens_len: None, - }; - task.initial_tokens = Some(task.get_initial_tokens()); - task.initial_tokens_len = Some(task.initial_tokens.as_ref().unwrap().len()); - - let mut max_initial_timestamp_index = None; - if let Some(max_initial_timestamp) = max_initial_timestamp { - let precision = CHUNK_LENGTH as f32 / N_AUDIO_CTX as f32; - max_initial_timestamp_index = - Some((max_initial_timestamp / precision).round() as usize); - } - task.logit_mutators.push(Box::new(ApplyTimestampRules { - sample_begin: task.initial_tokens_len.unwrap(), - max_initial_timestamp_index, - })); - - task - } - - #[cfg(not(target_arch = "wasm32"))] - fn main_loop( - &self, - decoder: &mut WhisperDecoder, - audio_ctx: Tensor, - callback: &Option, - ) -> Result, DecodeError> { - use ratchet::DType; - - let mut tokens = self.get_initial_tokens(); - let sliced_vocab_size = self.tokenizer.vocab_size(); - let device = audio_ctx.device().clone(); - let mut timestamps_seen = 0; - - for _ in 0..self.sample_len { - let input = if tokens.len() > self.initial_tokens_len.unwrap() { - &tokens[tokens.len() - 1..] - } else { - &tokens - }; - let input_t = Tensor::from_data(input, shape![1, input.len()], device.clone()); - - let logits = decoder - .schedule([audio_ctx.clone(), input_t])? - .cast(DType::F32)? - .resolve()?; - decoder.cache_mut().update(input.len()); - - let cpu_logits = logits.to(&Device::CPU)?; - let mut logits = Self::slice_logits(cpu_logits, sliced_vocab_size); - let token_t = Tensor::from_data(tokens.clone(), shape![1, tokens.len()], Device::CPU); - for m in &self.logit_mutators { - logits = m.apply(logits, &self.tokenizer, Some(&token_t))?; - } - - let (_, new_tokens, completed) = GreedySampler::sample(tokens, logits)?; - - if let Some(ref cb) = callback { - self.handle_callback(&self.tokenizer, &new_tokens, &mut timestamps_seen, cb); - } - - tokens = new_tokens; - if completed { - break; - } - } - Ok(tokens) - } - - #[cfg(target_arch = "wasm32")] - async fn main_loop( - &self, - decoder: &mut WhisperDecoder, - audio_ctx: Tensor, - callback: &Option, - ) -> Result, DecodeError> { - let mut tokens = self.get_initial_tokens(); - let device = audio_ctx.device().clone(); - let sliced_vocab_size = self.tokenizer.vocab_size(); - let mut timestamps_seen = 0; - - for _ in 0..self.sample_len { - let input = if tokens.len() > self.initial_tokens_len.unwrap() { - &tokens[tokens.len() - 1..] - } else { - &tokens - }; - let input_t = Tensor::from_data(input, shape![1, input.len()], device.clone()); - - let logits = decoder.schedule([audio_ctx.clone(), input_t])?.resolve()?; - decoder.cache_mut().update(input.len()); - - let cpu_logits = logits.to(&Device::CPU).await?; - let mut logits = Self::slice_logits(cpu_logits, sliced_vocab_size); - let token_t = Tensor::from_data(tokens.clone(), shape![1, tokens.len()], Device::CPU); - for m in &self.logit_mutators { - logits = m.apply(logits, &self.tokenizer, Some(&token_t))?; - } - - let (_, new_tokens, completed) = GreedySampler::sample(tokens, logits)?; - - if let Some(ref cb) = callback { - self.handle_callback(&self.tokenizer, &new_tokens, &mut timestamps_seen, cb); - } - - tokens = new_tokens; - if completed { - break; - } - } - Ok(tokens) - } - - fn handle_callback( - &self, - tokenizer: &WhisperTokenizer, - new_tokens: &[i32], - timestamps_seen: &mut i32, - callback: impl Fn(StreamedSegment), - ) { - if tokenizer.is_timestamp(new_tokens[new_tokens.len() - 1]) { - *timestamps_seen += 1; - if *timestamps_seen % 2 == 0 { - let previous_timestamp = new_tokens[..new_tokens.len() - 2] - .iter() - .rposition(|x| tokenizer.is_timestamp(*x)); - if let Some(previous_timestamp) = previous_timestamp { - callback(StreamedSegment::from_tokens( - tokenizer, - &new_tokens[previous_timestamp..], - self.options.time_offset.unwrap_or(0.0), - false, - )); - } - } - } - } - - /// Slice logits from [1xnum_tokensx51872] -> [1x1x51865] - pub(crate) fn slice_logits(logits: Tensor, vocab_size: usize) -> Tensor { - let nd_logits = logits.into_ndarray::(); - let sliced = nd_logits - .slice(s![.., -1.., ..vocab_size]) - .remove_axis(Axis(1)); - Tensor::from(sliced.to_owned().into_dyn()) - } - - pub fn build_segments( - tokenizer: &WhisperTokenizer, - tokens: Vec, - offset: f64, - segment_size: usize, - segment_duration: usize, - input_stride: usize, - ) -> (Vec, usize) { - let content_tokens = tokens; - let content_length = content_tokens.len(); - if content_length < 2 { - log::error!("Failed to build segments."); - return (Vec::new(), 0); - } - let (penultimate, last) = ( - content_tokens[content_length - 2], - content_tokens[content_length - 1], - ); - - let single_timestamp_ending = - !tokenizer.is_timestamp(penultimate) && tokenizer.is_timestamp(last); - - let mut consecutive = content_tokens - .windows(2) - .enumerate() - .filter_map(|(i, x)| { - if tokenizer.is_timestamp(x[0]) && tokenizer.is_timestamp(x[1]) { - Some(i + 1) - } else { - None - } - }) - .collect::>(); - - let advance; - let segments; - if !consecutive.is_empty() { - // if the output contains two consecutive timestamp tokens - if single_timestamp_ending { - consecutive.push(content_length); - } - - #[allow(unused_assignments)] - let mut last_slice = 0; - (segments, last_slice) = - consecutive - .iter() - .fold((Vec::new(), 0), |(mut acc, last_slice), &slice| { - let segment_tokens = &content_tokens[last_slice..slice]; - acc.push(Segment::from_tokens( - tokenizer, - segment_tokens, - offset, - false, - )); - (acc, slice) - }); - - advance = if single_timestamp_ending { - segment_size - } else { - let last_timestamp_pos = - content_tokens[last_slice - 1] - tokenizer.timestamp_begin(); - last_timestamp_pos as usize * input_stride - } - } else { - let duration = content_tokens - .iter() - .filter(|&x| tokenizer.is_timestamp(*x)) - .last() - .map_or(segment_duration as f64, |&last_ts| { - let last_timestamp_pos = last_ts - tokenizer.timestamp_begin(); - last_timestamp_pos as f64 * input_stride as f64 * (HOP_LENGTH as f64) - / (SAMPLE_RATE as f64) - }); - - let segment_tokens = content_tokens.iter().map(|x| *x as u32).collect::>(); - segments = vec![Segment::new( - offset, - offset + duration, - segment_tokens, - false, - )]; - advance = segment_size; - } - - (segments, advance) - } - - #[cfg(target_arch = "wasm32")] - pub async fn run( - &self, - decoder: &mut WhisperDecoder, - audio_ctx: Tensor, - callback: &Option, - ) -> Result, DecodeError> { - let mut tokens = self.main_loop(decoder, audio_ctx, &callback).await?; - - tokens = tokens.drain(self.initial_tokens_len.unwrap()..).collect(); - let eot_index = tokens.iter().position(|x| *x == WhisperTokenizer::EOT); - if let Some(eot_index) = eot_index { - tokens.truncate(eot_index); - } - Ok(tokens) - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn run( - &self, - decoder: &mut WhisperDecoder, - audio_ctx: Tensor, - callback: &Option, - ) -> Result, DecodeError> { - let mut tokens = self.main_loop(decoder, audio_ctx, callback)?; - - tokens = tokens.drain(self.initial_tokens_len.unwrap()..).collect(); - let eot_index = tokens.iter().position(|x| *x == WhisperTokenizer::EOT); - if let Some(eot_index) = eot_index { - tokens.truncate(eot_index); - } - Ok(tokens) - } -} diff --git a/crates/ratchet-models/src/whisper/tokenizer.rs b/crates/ratchet-models/src/whisper/tokenizer.rs deleted file mode 100644 index af008bdc..00000000 --- a/crates/ratchet-models/src/whisper/tokenizer.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::whisper::options::{Language, Task}; -use tokenizers::Tokenizer; - -#[cfg(not(target_arch = "wasm32"))] -use hf_hub::api::sync::Api; -#[cfg(target_arch = "wasm32")] -use {ratchet_hub::ApiBuilder, ratchet_hub::RepoType, wasm_bindgen::JsError}; - -lazy_static::lazy_static! { - pub static ref LANGUAGES: [&'static str; 100] = { - [ - "en", "zh", "de", "es", "ru", "ko", "fr", "ja", "pt", "tr", "pl", "ca", "nl", "ar", - "sv", "it", "id", "hi", "fi", "vi", "he", "uk", "el", "ms", "cs", "ro", "da", "hu", - "ta", "no", "th", "ur", "hr", "bg", "lt", "la", "mi", "ml", "cy", "sk", "te", "fa", - "lv", "bn", "sr", "az", "sl", "kn", "et", "mk", "br", "eu", "is", "hy", "ne", "mn", - "bs", "kk", "sq", "sw", "gl", "mr", "pa", "si", "km", "sn", "yo", "so", "af", "oc", - "ka", "be", "tg", "sd", "gu", "am", "yi", "lo", "uz", "fo", "ht", "ps", "tk", "nn", - "mt", "sa", "lb", "my", "bo", "tl", "mg", "as", "tt", "haw", "ln", "ha", "ba", "jw", - "su", "yue", - ] - }; -} - -//Wrapper around tokenizers::Tokenizer with helpers -#[derive(Clone)] -pub struct WhisperTokenizer { - inner: Tokenizer, - language: i32, - task: Task, -} - -impl WhisperTokenizer { - pub const SOT: i32 = 50258; - pub const EOT: i32 = 50257; - pub const LANGUAGES_BEGIN: usize = 50259; - pub const BLANK: i32 = 220; - - //https://github.com/openai/whisper/blob/1cea4357687b676b293cb5473e1ade25f5b1cef7/whisper/tokenizer.py#L242 - pub const NON_SPEECH: [i32; 82] = [ - 1, 2, 7, 8, 9, 10, 14, 25, 26, 27, 28, 29, 31, 58, 59, 60, 61, 62, 63, 90, 91, 92, 93, 359, - 503, 522, 542, 873, 893, 902, 918, 922, 931, 1350, 1853, 1982, 2460, 2627, 3246, 3253, - 3268, 3536, 3846, 3961, 4183, 4667, 6585, 6647, 7273, 9061, 9383, 10428, 10929, 11938, - 12033, 12331, 12562, 13793, 14157, 14635, 15265, 15618, 16553, 16604, 18362, 18956, 20075, - 21675, 22520, 26130, 26161, 26435, 28279, 29464, 31650, 32302, 32470, 36865, 42863, 47425, - 49870, 50254, - ]; - - #[cfg(not(target_arch = "wasm32"))] - pub fn load_inner(bytes: Option>, v3: bool) -> Tokenizer { - if let Some(bytes) = bytes { - Tokenizer::from_bytes(bytes).unwrap() - } else { - Self::fetch(v3) - } - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn load(bytes: Option>, v3: bool, language: Language, task: Task) -> Self { - let inner = Self::load_inner(bytes, v3); - let mut tokenizer = Self { - inner, - language: -1, - task, - }; - tokenizer.set_language(language); - tokenizer - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn fetch(v3: bool) -> Tokenizer { - let api = Api::new().unwrap(); - - //TODO: dumb hack - let repo_name = match v3 { - true => "openai/whisper-large-v3", - false => "openai/whisper-tiny", - }; - - let tokenizer_repo = api.model(repo_name.to_string()); - let tokenizer_path = tokenizer_repo.get("tokenizer.json").unwrap(); - Tokenizer::from_file(tokenizer_path).unwrap() - } - - #[cfg(target_arch = "wasm32")] - pub async fn load_inner(bytes: Option>, v3: bool) -> Tokenizer { - use wasm_bindgen::JsValue; - - if let Some(bytes) = bytes { - Tokenizer::from_bytes(bytes).unwrap() - } else { - Self::fetch(v3).await.map_err(JsValue::from).unwrap() - } - } - - #[cfg(target_arch = "wasm32")] - pub async fn load(bytes: Option>, v3: bool, language: Language, task: Task) -> Self { - let inner = Self::load_inner(bytes, v3).await; - let mut tokenizer = Self { - inner, - language: -1, - task, - }; - tokenizer.set_language(language); - tokenizer - } - - #[cfg(target_arch = "wasm32")] - pub async fn fetch(v3: bool) -> Result { - let repo_name = match v3 { - true => "openai/whisper-large-v3", - false => "openai/whisper-tiny", - }; - - let model_repo = ApiBuilder::from_hf(repo_name, RepoType::Model).build(); - let model_bytes = model_repo.get("tokenizer.json").await?; - Ok(Tokenizer::from_bytes(model_bytes.to_vec()).unwrap()) - } - - pub fn set_language(&mut self, language: Language) { - let token = match language { - Language::String(s) => { - let lang_position = LANGUAGES.iter().position(|x| *x == s); - if lang_position.is_none() { - panic!("Language {} not found", s); - } - - WhisperTokenizer::SOT + 1 + lang_position.unwrap() as i32 - } - Language::Token(t) => t, - }; - self.language = token; - } - - #[inline] - pub fn sot_prev(&self) -> i32 { - self.inner - .encode("<|startofprev|>", false) - .unwrap() - .get_ids()[0] as i32 - } - - #[inline] - pub fn sot_sequence(&self) -> Vec { - vec![Self::SOT, self.language, self.task.as_token(self)] - } - - #[inline] - pub fn transcribe(&self) -> i32 { - self.inner - .encode("<|transcribe|>", false) - .unwrap() - .get_ids()[0] as i32 - } - - #[inline] - pub fn translate(&self) -> i32 { - self.inner.encode("<|translate|>", false).unwrap().get_ids()[0] as i32 - } - - #[inline] - pub fn notimestamps(&self) -> i32 { - self.inner - .encode("<|notimestamps|>", false) - .unwrap() - .get_ids()[0] as i32 - } - - #[inline] - pub fn timestamp_begin(&self) -> i32 { - self.inner.encode("<|0.00|>", false).unwrap().get_ids()[0] as i32 - } - - #[inline] - pub fn is_timestamp(&self, token: i32) -> bool { - (self.timestamp_begin()..=self.vocab_size() as i32).contains(&token) - } - - #[inline] - pub fn is_multilingual(&self) -> bool { - self.inner.get_vocab_size(true) >= 51865 - } - - #[inline] - pub fn vocab_size(&self) -> usize { - self.inner.get_vocab_size(true) - } - - pub fn encode(&self, text: &str, skip_special: bool) -> Result, tokenizers::Error> { - Ok(self - .inner - .encode(text, skip_special)? - .get_ids() - .iter() - .map(|x| *x as _) - .collect()) - } - - pub fn decode(&self, tokens: &[u32], skip_special: bool) -> Result { - self.inner.decode(tokens, skip_special) - } -} diff --git a/crates/ratchet-models/src/whisper/transcribe.rs b/crates/ratchet-models/src/whisper/transcribe.rs deleted file mode 100644 index 7e3ecf86..00000000 --- a/crates/ratchet-models/src/whisper/transcribe.rs +++ /dev/null @@ -1,174 +0,0 @@ -use crate::whisper::model::Whisper; -use crate::whisper::options::*; -use crate::whisper::{spectrogram::*, task::*, tokenizer::*, transcript::*}; -use ratchet_nn::Module; -use std::cmp::min; -use web_time::Instant; - -#[cfg(not(target_arch = "wasm32"))] -pub fn transcribe( - model: &mut Whisper, - audio: Vec, - mut decode_options: DecodingOptions, - callback: Option, -) -> anyhow::Result { - let n_mels = model.config.n_mels; - let runtime = Instant::now(); - let mel = model.specgen.generate(audio)?.to(&model.device)?; - let content_frames = mel.shape()[mel.rank() - 1] - N_FRAMES; - - if decode_options.language.is_none() { - if !model.is_multilingual() { - log::warn!("No language specified, using English"); - decode_options.language = Some(Language::String("en".to_string())); - } else { - log::warn!("No language specified, using language detection"); - let mel = mel.clone().slice(&[0..1, 0..n_mels, 0..3000])?; - decode_options.language = Some(model.detect_language(mel)?); - } - } - - let language = decode_options.language.as_ref().unwrap(); - let task = decode_options.task; - let tokenizer = WhisperTokenizer::load(None, n_mels == 128, language.clone(), task); - - let mut seek = 0; - let input_stride = N_FRAMES / N_AUDIO_CTX; - let mut all_tokens = Vec::with_capacity(512); - let mut all_segments = Vec::with_capacity(512); - let prompt_since_reset = 0; - - while seek < content_frames { - let mut decode_options = decode_options.clone(); - let time_offset = (seek * HOP_LENGTH) as f64 / SAMPLE_RATE as f64; - decode_options.time_offset = Some(time_offset); - let mel_segment = mel - .clone() - .slice(&[0..1, 0..n_mels, seek..(seek + N_FRAMES)])?; - log::info!("Processing segment: {} -> {}", seek, seek + N_FRAMES); - - let segment_size = min(N_FRAMES, content_frames - seek); - let segment_duration = segment_size * HOP_LENGTH / SAMPLE_RATE; - - if !all_tokens.is_empty() { - decode_options.prompt = Some(Prompt::Tokens(all_tokens[prompt_since_reset..].to_vec())); - } - - let hs = model.encoder.schedule(mel_segment)?.resolve()?; - - let task = DecodingTask::new(decode_options, tokenizer.clone()); - let decoded = task.run(&mut model.decoder, hs, &callback)?; - let (segments, advance) = DecodingTask::build_segments( - &tokenizer, - decoded, - time_offset, - segment_size, - segment_duration, - input_stride, - ); - let all_segment_tokens = segments - .iter() - .flat_map(|s| s.tokens.iter().copied()) - .map(|x| x as i32) - .collect::>(); - all_tokens.extend(all_segment_tokens); - all_segments.extend(segments); - model.decoder.reset(); - seek += advance; - } - - let mut t = TranscriptionResult::new(runtime.elapsed(), all_segments, None); - t.generate_formatted(&tokenizer); - Ok(t) -} - -#[cfg(target_arch = "wasm32")] -pub async fn transcribe( - model: &mut Whisper, - audio: Vec, - mut decode_options: DecodingOptions, - callback: Option, -) -> anyhow::Result { - let n_mels = model.config.n_mels as usize; - let runtime = Instant::now(); - let mel = model.specgen.generate(audio)?.to(&model.device).await?; - let content_frames = mel.shape()[mel.rank() - 1] - N_FRAMES; - - if decode_options.language.is_none() { - if !model.is_multilingual() { - log::warn!("No language specified, using English"); - decode_options.language = Some(Language::String("en".to_string())); - } else { - log::warn!("No language specified, using language detection"); - let mel = mel.clone().slice(&[0..1, 0..n_mels, 0..3000])?; - decode_options.language = Some(model.detect_language(mel).await?); - } - } - - let language = decode_options.language.as_ref().unwrap(); - let task = decode_options.task; - let tokenizer = - WhisperTokenizer::load(None, n_mels == 128, language.clone(), task.clone()).await; - - let mut seek = 0; - let input_stride = N_FRAMES / N_AUDIO_CTX; - let mut all_tokens = Vec::with_capacity(512); - let mut all_segments = Vec::with_capacity(512); - let prompt_since_reset = 0; - - while seek < content_frames { - let mut decode_options = decode_options.clone(); - let time_offset = (seek * HOP_LENGTH) as f64 / SAMPLE_RATE as f64; - decode_options.time_offset = Some(time_offset); - let mel_segment = mel - .clone() - .slice(&[0..1, 0..n_mels, seek..(seek + N_FRAMES)])?; - log::info!("Processing segment: {} -> {}", seek, seek + N_FRAMES); - - let segment_size = min(N_FRAMES, content_frames - seek); - let segment_duration = segment_size * HOP_LENGTH / SAMPLE_RATE; - - if !all_tokens.is_empty() { - decode_options.prompt = Some(Prompt::Tokens(all_tokens[prompt_since_reset..].to_vec())); - } - - let hs = model.encoder.schedule(mel_segment)?.resolve()?; - - let dbg = hs.clone().to(&ratchet::Device::CPU).await; - log::warn!("HS: {:?}", dbg); - - let task = DecodingTask::new(decode_options, tokenizer.clone()); - let decoded = task.run(&mut model.decoder, hs, &callback).await?; - - let (segments, advance) = DecodingTask::build_segments( - &tokenizer, - decoded, - time_offset, - segment_size, - segment_duration, - input_stride, - ); - let all_segment_tokens = segments - .iter() - .flat_map(|s| s.tokens.iter().copied()) - .map(|x| x as i32) - .collect::>(); - all_tokens.extend(all_segment_tokens); - all_segments.extend(segments); - model.decoder.reset(); - seek += advance; - } - - if let Some(cb) = callback { - cb(StreamedSegment::from_tokens( - &tokenizer, - &[WhisperTokenizer::EOT], - content_frames as f64 / 100., - true, - )); - } - - let mut t = TranscriptionResult::new(runtime.elapsed(), all_segments, None); - t.generate_formatted(&tokenizer); - Ok(t) -} diff --git a/crates/ratchet-models/src/whisper/transcript.rs b/crates/ratchet-models/src/whisper/transcript.rs deleted file mode 100644 index 0fd1df84..00000000 --- a/crates/ratchet-models/src/whisper/transcript.rs +++ /dev/null @@ -1,159 +0,0 @@ -use super::spectrogram::*; -use super::tokenizer::WhisperTokenizer; -use num::integer::div_floor; -use serde::{Deserialize, Serialize}; -use std::time::Duration; - -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - -#[cfg_attr(target_arch = "wasm32", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, derive_new::new)] -pub struct TranscriptionResult { - pub processing_time: Duration, - pub segments: Vec, - pub formatted: Option, -} - -impl TranscriptionResult { - pub fn generate_formatted(&mut self, tokenizer: &WhisperTokenizer) { - let formatted = self.as_oai(tokenizer); - self.formatted = Some(formatted); - } - - fn format_single(&self, segment: &Segment, tokenizer: &WhisperTokenizer) -> String { - let fragment_tokens = segment - .tokens - .iter() - .copied() - .filter(|x| *x < WhisperTokenizer::EOT as _) - .collect::>(); - let fragment_text = tokenizer.decode(fragment_tokens.as_slice(), true).unwrap(); - format!( - "[{} --> {}] {}\n", - Self::format_timestamp(segment.start, false, "."), - Self::format_timestamp(segment.stop, false, "."), - fragment_text.trim().replace("-->", "->") - ) - } - - pub fn as_oai(&self, tokenizer: &WhisperTokenizer) -> String { - self.segments - .iter() - .map(|segment| self.format_single(segment, tokenizer)) - .collect::() - } - - fn format_timestamp(num: f64, always_include_hours: bool, decimal_marker: &str) -> String { - assert!(num >= 0.0, "non-negative timestamp expected"); - let milliseconds: i64 = (num * 1000.0) as i64; - - let hours = div_floor(milliseconds, 3_600_000); - let minutes = div_floor(milliseconds % 3_600_000, 60_000); - let seconds = div_floor(milliseconds % 60_000, 1000); - let milliseconds = milliseconds % 1000; - - let hours_marker = if always_include_hours || hours != 0 { - format!("{:02}:", hours) - } else { - String::new() - }; - - format!( - "{}{:02}:{:02}{}{:03}", - hours_marker, minutes, seconds, decimal_marker, milliseconds - ) - } -} - -#[derive(Debug, Serialize, Deserialize, derive_new::new)] -pub struct Segment { - pub start: f64, - pub stop: f64, - pub tokens: Vec, - pub last: bool, -} - -impl Segment { - pub fn from_tokens( - tokenizer: &WhisperTokenizer, - sliced_tokens: &[i32], - offset: f64, - last: bool, - ) -> Self { - let input_stride = N_FRAMES / N_AUDIO_CTX; // mel frames per output token: 2 - let time_precision: f64 = input_stride as f64 * (HOP_LENGTH as f64) / (SAMPLE_RATE as f64); // time per output token: 0.02 (seconds) - - let start_timestamp_pos = sliced_tokens[0] - tokenizer.timestamp_begin(); - let end_timestamp_pos = - sliced_tokens[sliced_tokens.len() - 1] - tokenizer.timestamp_begin(); - - let segment_tokens = sliced_tokens.iter().map(|x| *x as u32).collect::>(); - - let st = offset + (start_timestamp_pos as f64 * time_precision); - let et = offset + (end_timestamp_pos as f64 * time_precision); - let st = (st * 100.).round() / 100.; - let et = (et * 100.).round() / 100.; - Segment::new(st, et, segment_tokens, last) - } -} - -#[cfg_attr( - target_arch = "wasm32", - wasm_bindgen(getter_with_clone, js_name = Segment), - derive(serde::Serialize, serde::Deserialize) -)] -#[derive(Debug, derive_new::new)] -pub struct StreamedSegment { - pub start: f64, - pub stop: f64, - pub text: String, - pub last: bool, -} - -impl std::fmt::Display for StreamedSegment { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "[{} --> {}] {}", - TranscriptionResult::format_timestamp(self.start, false, "."), - TranscriptionResult::format_timestamp(self.stop, false, "."), - self.text.trim().replace("-->", "->") - ) - } -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] -impl StreamedSegment { - pub fn start(&self) -> f64 { - self.start - } - - pub fn stop(&self) -> f64 { - self.stop - } - - pub fn text(&self) -> String { - self.text.clone() - } - - pub fn last(&self) -> bool { - self.last - } - - pub(crate) fn from_tokens( - tokenizer: &WhisperTokenizer, - sliced_tokens: &[i32], - offset: f64, - last: bool, - ) -> Self { - let segment = Segment::from_tokens(tokenizer, sliced_tokens, offset, last); - let segment_tokens = segment - .tokens - .into_iter() - .filter(|t| *t < tokenizer.timestamp_begin() as _) - .collect::>(); - let segment_text = tokenizer.decode(segment_tokens.as_slice(), true).unwrap(); - StreamedSegment::new(segment.start, segment.stop, segment_text, last) - } -} diff --git a/crates/ratchet-models/tests/whisper.rs b/crates/ratchet-models/tests/whisper.rs deleted file mode 100644 index f34ced21..00000000 --- a/crates/ratchet-models/tests/whisper.rs +++ /dev/null @@ -1,218 +0,0 @@ -#![cfg(target_arch = "wasm32")] -wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - -use ndarray::{s, Axis}; -use ndarray_stats::QuantileExt; -use ratchet::{shape, Device, DeviceRequest, Tensor}; -use ratchet_hub::{ApiBuilder, RepoType}; -use ratchet_loader::gguf::gguf; -use ratchet_models::whisper::{Config, Whisper, WhisperDecoder, WhisperEncoder}; -use ratchet_nn::Module; -use std::path::PathBuf; -use wasm_bindgen::prelude::*; -use wasm_bindgen_test::*; - -fn log_init() { - console_error_panic_hook::set_once(); - log::set_max_level(log::LevelFilter::Off); - console_log::init_with_level(log::Level::Debug).unwrap(); -} - -/* -#[wasm_bindgen_test] -async fn distil_large_v3_encoder() -> Result<(), JsValue> { - log_init(); - let model_repo = - ApiBuilder::from_hf("FL33TW00D-HF/distil-whisper-large-v3", RepoType::Model).build(); - let model_data = model_repo.get("distil-large-v3_q8_0.gguf").await?; - let config_data = model_repo.get("config.json").await?; - - let ground_repo = ApiBuilder::from_hf("FL33TW00D-HF/ratchet-util", RepoType::Dataset).build(); - let input_npy = ground_repo.get("distil_large_v3_q80_mm0_input.npy").await?; - let ground_npy = ground_repo.get("distil_large_v3_q80_mm0_hs.npy").await?; - - let mut reader = std::io::BufReader::new(std::io::Cursor::new(model_data.to_vec())); - let header = gguf::Header::read(&mut reader).unwrap(); - let config: Config = serde_json::from_slice(&config_data.to_vec()).unwrap(); - - let device = Device::request_device(DeviceRequest::GPU).await.unwrap(); - - let input_data = &input_npy.to_vec(); - let input = Tensor::from_npy_bytes::(input_data, &device).unwrap(); - let ground = Tensor::from_npy_bytes::(&ground_npy.to_vec(), &Device::CPU).unwrap(); - - let encoder = WhisperEncoder::load(&header, &config, &mut reader, &device).unwrap(); - let result = encoder - .schedule(input) - .unwrap() - .full() - .unwrap() - .resolve() - .unwrap(); - let ours = result.to(&Device::CPU).await.unwrap(); - ground.all_close(&ours, 1e-3, 1e-3).unwrap(); - Ok(()) -}*/ - -/* -#[wasm_bindgen_test] -async fn distil_large_v3_decoder() -> Result<(), JsValue> { - log_init(); - let model_repo = - ApiBuilder::from_hf("FL33TW00D-HF/distil-whisper-large-v3", RepoType::Model).build(); - let model_data = model_repo.get("distil-large-v3_q8_0.gguf").await?; - let config_data = model_repo.get("config.json").await?; - - let ground_repo = ApiBuilder::from_hf("FL33TW00D-HF/ratchet-util", RepoType::Dataset).build(); - let hs_data = ground_repo.get("distil_large_v3_q80_mm0_hs.npy").await?; - - let mut reader = std::io::BufReader::new(std::io::Cursor::new(model_data.to_vec())); - let header = gguf::Header::read(&mut reader).unwrap(); - let config: Config = serde_json::from_slice(&config_data.to_vec()).unwrap(); - - let device = Device::request_device(DeviceRequest::GPU).await.unwrap(); - let audio_ctx = Tensor::from_npy_bytes::(&hs_data.to_vec(), &device) - .unwrap() - .half() - .unwrap() - .resolve() - .unwrap(); - log::debug!("Audio Context: {:?}", audio_ctx); - let mut decoder = WhisperDecoder::load(&header, &config, &mut reader, &device).unwrap(); - - let mut tokens = vec![50258, 50259, 50360]; - let mut all_tokens = tokens.clone(); - let mut all_logits = vec![]; - while tokens[tokens.len() - 1] != 50257 { - let token_t = Tensor::from_data(tokens.clone(), shape![1, tokens.len()], device.clone()); - let result = decoder - .schedule([audio_ctx.clone(), token_t]) - .unwrap() - .resolve_debug() - .unwrap(); - - let our_logits = result.to(&Device::CPU).await.unwrap(); - all_logits.push(our_logits.clone()); - let nd_logits = our_logits.to_ndarray_view::(); - log::info!("Logits: {:?}", nd_logits); - - let sliced = nd_logits.slice(s![.., -1.., ..51866]).remove_axis(Axis(1)); - decoder.cache_mut().update(tokens.len()); - - tokens = sliced - .map_axis(Axis(1), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - println!("Token: {:?}", tokens); - panic!(); - all_tokens.extend(tokens.clone()); - } - - let ground_tokens = vec![ - 50258, 50259, 50360, 50365, 639, 307, 264, 4532, 3479, 587, 11, 15578, 264, 881, 2062, 847, - 11, 34674, 5932, 30340, 295, 3123, 4397, 608, 1652, 13, 50517, 50530, 6947, 472, 575, - 12023, 4365, 11, 20899, 11, 10445, 11, 18356, 11, 4225, 4782, 11, 50624, 50626, 1804, 4651, - 3123, 4397, 34922, 8963, 862, 6352, 13, 50695, 50701, 821, 311, 257, 3804, 5214, 11, 2610, - 5214, 11, 6383, 11, 2643, 5214, 11, 293, 544, 13, 50797, 50807, 10246, 8963, 2436, 2965, - 281, 747, 604, 1081, 13, 50875, 50881, 400, 456, 366, 867, 34674, 862, 365, 11, 293, 1184, - 472, 1487, 365, 1080, 1065, 2121, 11377, 11, 51002, 51007, 4532, 3479, 5864, 293, 1019, 11, - 5456, 4122, 300, 30686, 25038, 1286, 13, 51120, 51135, 30062, 264, 6582, 29814, 412, 264, - 10155, 11, 1849, 1426, 11, 587, 11, 264, 3874, 34544, 412, 264, 7267, 3096, 13, 51243, - 51246, 18463, 428, 1032, 412, 264, 1032, 5675, 13, 51287, 51290, 30062, 264, 16629, 7283, - 13, 51320, 51328, 400, 613, 862, 6352, 3318, 1214, 281, 1254, 257, 3123, 4397, 34922, 8963, - 1081, 6352, 11, 370, 27985, 11, 51504, 51504, 370, 6239, 11, 370, 44078, 1688, 356, 9942, - 11, 291, 603, 528, 281, 8963, 552, 439, 13, 51623, 51635, 25642, 12089, 1652, 366, 1027, - 14759, 490, 7336, 836, 65, 13, 51743, 51743, 440, 4356, 436, 366, 11, 264, 1101, 436, 366, - 13, 51834, - ]; - assert_eq!(all_tokens, ground_tokens); - Ok(()) -}*/ - -/* -#[wasm_bindgen_test] -async fn tiny_encoder() -> Result<(), JsValue> { - log_init(); - let model_repo = ApiBuilder::from_hf("FL33TW00D-HF/whisper-tiny", RepoType::Model).build(); - let model_data = model_repo.get("tiny_f32.gguf").await?; - let config_data = model_repo.get("config.json").await?; - - let ground_repo = ApiBuilder::from_hf("FL33TW00D-HF/ratchet-util", RepoType::Dataset).build(); - let input_npy = ground_repo.get("jfk_tiny_encoder_input.npy").await?; - let ground_npy = ground_repo.get("jfk_tiny_encoder_hs.npy").await?; - - let mut reader = std::io::BufReader::new(std::io::Cursor::new(model_data.to_vec())); - let header = gguf::Header::read(&mut reader).unwrap(); - let config: Config = serde_json::from_slice(&config_data.to_vec()).unwrap(); - - let device = Device::request_device(DeviceRequest::GPU).await.unwrap(); - - let input_data = &input_npy.to_vec(); - let input = Tensor::from_npy_bytes::(input_data, &device).unwrap(); - let ground = Tensor::from_npy_bytes::(&ground_npy.to_vec(), &Device::CPU).unwrap(); - - let encoder = WhisperEncoder::load(&header, &config, &mut reader, &device).unwrap(); - let result = encoder.schedule(input).unwrap().resolve().unwrap(); - let ours = result.to(&Device::CPU).await.unwrap(); - ground.all_close(&ours, 1e-3, 1e-3).unwrap(); - Ok(()) -} -*/ - -#[wasm_bindgen_test] -async fn tiny_decoder() -> Result<(), JsValue> { - log_init(); - let model_repo = ApiBuilder::from_hf("FL33TW00D-HF/whisper-tiny", RepoType::Model).build(); - let model_data = model_repo.get("tiny_f32.gguf").await?; - let config_data = model_repo.get("config.json").await?; - - let ground_repo = ApiBuilder::from_hf("FL33TW00D-HF/ratchet-util", RepoType::Dataset).build(); - let hs_data = ground_repo.get("jfk_tiny_encoder_hs.npy").await?; - - let mut reader = std::io::BufReader::new(std::io::Cursor::new(model_data.to_vec())); - let header = gguf::Header::read(&mut reader).unwrap(); - let config: Config = serde_json::from_slice(&config_data.to_vec()).unwrap(); - - let device = Device::request_device(DeviceRequest::GPU).await.unwrap(); - - let audio_ctx = Tensor::from_npy_bytes::(&hs_data.to_vec(), &device).unwrap(); - let mut decoder = WhisperDecoder::load(&header, &config, &mut reader, &device).unwrap(); - - let mut tokens = vec![50258, 50259, 50359]; - let mut all_tokens = tokens.clone(); - let mut all_logits = vec![]; - let vocab_size = 51865; - while tokens[tokens.len() - 1] != 50257 { - let token_t = Tensor::from_data(tokens.clone(), shape![1, tokens.len()], device.clone()); - let result = decoder - .schedule([audio_ctx.clone(), token_t]) - .unwrap() - .resolve() - .unwrap(); - - let our_logits = result.to(&Device::CPU).await.unwrap(); - all_logits.push(our_logits.clone()); - let nd_logits = our_logits.to_ndarray_view::(); - log::debug!("ND LOGITS: {:?}", nd_logits); - let sliced = nd_logits - .slice(s![.., -1.., ..vocab_size]) - .remove_axis(Axis(1)); - decoder.cache_mut().update(tokens.len()); - - tokens = sliced - .map_axis(Axis(1), |row| row.argmax_skipnan().unwrap()) - .iter() - .map(|&x| x as i32) - .collect::>(); - println!("Token: {:?}", tokens); - all_tokens.extend(tokens.clone()); - } - - let ground_tokens = vec![ - 50258, 50259, 50359, 50363, 400, 370, 452, 7177, 6280, 1029, 406, 437, 428, 1941, 393, 360, - 337, 291, 1029, 437, 291, 393, 360, 337, 428, 1941, 13, 50257, - ]; - assert_eq!(all_tokens, ground_tokens); - Ok(()) -} diff --git a/crates/ratchet-models/webdriver.json b/crates/ratchet-models/webdriver.json deleted file mode 100644 index cb1190c2..00000000 --- a/crates/ratchet-models/webdriver.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "goog:chromeOptions": { - "args": [ - "--no-sandbox", - "--headless=new", - "--use-angle=vulkan", - "--enable-features=Vulkan", - "--disable-vulkan-surface", - "--enable-unsafe-webgpu" - ] - } -} diff --git a/crates/ratchet-nn/src/kv_cache.rs b/crates/ratchet-nn/src/kv_cache.rs deleted file mode 100644 index cb5a16eb..00000000 --- a/crates/ratchet-nn/src/kv_cache.rs +++ /dev/null @@ -1,85 +0,0 @@ -use num_traits::AsPrimitive; -use ratchet::{shape, Device, HashMap, Shape, Tensor, TensorDType}; - -#[derive(Clone, Debug)] -pub struct KVEntry { - pub k_cache: Tensor, - pub v_cache: Tensor, - pub entries: usize, -} - -impl KVEntry { - pub fn allocate>(shape: &Shape, device: &Device) -> Self { - KVEntry { - k_cache: Tensor::zeros::(shape, device), - v_cache: Tensor::zeros::(shape, device), - entries: 0, - } - } -} - -#[derive(Clone, Debug)] -pub struct KVCache { - entries: Vec, - use_kv_cache: bool, - masks: HashMap, - device: Device, -} - -impl std::ops::Index for KVCache { - type Output = KVEntry; - - fn index(&self, index: usize) -> &Self::Output { - &self.entries[index] - } -} - -impl KVCache { - pub fn new>( - n_layers: i32, - use_kv_cache: bool, - shape: Shape, - device: &Device, - ) -> Self { - let mut entries = Vec::with_capacity(n_layers as _); - for _ in 0..n_layers { - entries.push(KVEntry::allocate::(&shape, device)); - } - KVCache { - entries, - masks: HashMap::default(), - device: device.clone(), - use_kv_cache, - } - } - - pub fn update(&mut self, offset: usize) { - for entry in &mut self.entries { - entry.entries += offset; - } - } - - pub fn entries(&self, layer: usize) -> usize { - self.entries[layer].entries - } - - pub fn reset(&mut self) { - for entry in &mut self.entries { - entry.entries = 0; - } - } - - pub fn mask(&mut self, t: usize) -> anyhow::Result { - if let Some(mask) = self.masks.get(&t) { - log::debug!("Using existing mask for {:?}", t); - Ok(mask.clone()) - } else { - log::debug!("Creating mask for {:?}", t); - log::debug!("masks: {:?}", self.masks); - let ones = Tensor::ones::(&shape![t, t], &self.device); - let mask = ones.triu(Some(1))?; - self.masks.insert(t, mask.clone()); - Ok(mask) - } - } -} diff --git a/crates/ratchet-nn/src/loss.rs b/crates/ratchet-nn/src/loss.rs deleted file mode 100644 index ea6e66d9..00000000 --- a/crates/ratchet-nn/src/loss.rs +++ /dev/null @@ -1,152 +0,0 @@ -use ratchet::Tensor; - -pub fn nll(inp: Tensor, target: Tensor) -> anyhow::Result { - let b_sz = match &target.shape().to_vec()[..] { - &[b_sz] => b_sz, - dims => anyhow::bail!("the target tensor should have a single dimension ({dims:?})"), - }; - match &inp.shape().to_vec()[..] { - &[inp_b_sz, _] => { - if inp_b_sz != b_sz { - anyhow::bail!("batch size mismatch between inp ({inp_b_sz}) and target ({b_sz})") - } - } - dims => anyhow::bail!("the target tensor should have two dimensions ({dims:?})"), - } - inp.gather(target.clone().unsqueeze(1)?, 1)? - .sum_all()? - .affine(-1f32 / b_sz as f32, 0.) -} - -pub fn log_softmax(xs: Tensor, d: usize) -> anyhow::Result { - let max = xs.clone().max_keepdim(d)?; - let diff = xs.clone().sub(max)?; - let sum_exp = diff.clone().exp()?.sum_keepdim(&[d])?; - let log_sm = diff.sub(sum_exp.log()?)?; - Ok(log_sm) -} - -pub fn cross_entropy(inp: Tensor, target: Tensor) -> anyhow::Result { - if inp.rank() != 2 { - anyhow::bail!("cross_entropy expects an input tensor of rank 2") - } - let inp = log_softmax(inp, 1)?; - nll(inp, target) -} - -#[cfg(all(test, feature = "pyo3"))] -mod tests { - use super::*; - use anyhow::Result; - use ratchet::{prelude::shape, DType, Device, DeviceRequest, Var}; - use test_strategy::{proptest, Arbitrary}; - - thread_local! { - static GPU_DEVICE: Device = Device::request_device(DeviceRequest::GPU).unwrap(); - } - - fn ground_truth_cross_entropy(input: &Tensor, target: &Tensor) -> Result<(Tensor, Tensor)> { - let grad_prg = r#" -import torch -import torch.nn.functional as F - -def cross_entropy_with_grad(input, target): - input_tensor = torch.tensor(torch.from_numpy(input), requires_grad=True) - target_tensor = torch.tensor(torch.from_numpy(target), dtype=torch.long) - loss = F.cross_entropy(input_tensor, target_tensor, reduction='mean') - loss.backward() - return input_tensor.grad.numpy() -"#; - let grad = ratchet::test_util::run_py_prg( - grad_prg.to_string(), - &[input, target], - &[], - DType::F32, - )?; - - let loss_prg = r#" -import torch -import torch.nn.functional as F - -def cross_entropy(input, target): - input_tensor = torch.tensor(torch.from_numpy(input)) - target_tensor = torch.tensor(torch.from_numpy(target), dtype=torch.long) - return F.cross_entropy(input_tensor, target_tensor, reduction='mean').numpy() -"#; - let loss = ratchet::test_util::run_py_prg( - loss_prg.to_string(), - &[input, target], - &[], - DType::F32, - )?; - - Ok((loss, grad)) - } - - #[derive(Arbitrary, Debug)] - struct CrossEntropyProblem { - #[strategy(1..=32usize)] - batch_size: usize, - #[strategy(2..=10usize)] - num_classes: usize, - } - - fn run_cross_entropy_trial(problem: CrossEntropyProblem) -> Result<()> { - let device = GPU_DEVICE.with(|d| d.clone()); - let CrossEntropyProblem { - batch_size, - num_classes, - } = problem; - - // Generate random input and target tensors - let input = Tensor::randn::(0., 1., shape![batch_size, num_classes], Device::CPU); - let target = Tensor::randint(0, num_classes as i32, shape![batch_size], Device::CPU); - - // Compute ground truth - let (ground_loss, ground_grad) = ground_truth_cross_entropy(&input, &target)?; - - // Compute our implementation - let input_gpu = input.to(&device)?; - let target_gpu = target.to(&device)?; - let input_var = Var::from_tensor(&input_gpu)?; - let our_loss = cross_entropy(input_var.as_tensor().clone(), target_gpu)?; - - // Compute gradients - let grads = our_loss.backward()?; - device.try_gpu()?.mark_step()?; - let our_grad = grads.get(input_var.as_tensor()).unwrap().clone(); - - // Compare results - let our_loss_cpu = our_loss.to(&Device::CPU)?; - let our_grad_cpu = our_grad.to(&Device::CPU)?; - let ground_grad = ground_grad.to(&Device::CPU)?; - let ground_loss = ground_loss.to(&Device::CPU)?; - - println!("Input shape: {:?}", input.shape()); - println!("Target shape: {:?}", target.shape()); - println!("Our loss: {:?}", our_loss_cpu.to_vec::()); - println!("Ground truth loss: {:?}", ground_loss.to_vec::()); - println!("Our grad: {:?}", our_grad_cpu.to_vec::()); - println!("Ground truth grad: {:?}", ground_grad.to_vec::()); - println!("Our grad shape: {:?}", our_grad_cpu.shape()); - println!("Ground truth grad shape: {:?}", ground_grad.shape()); - - ground_loss.all_close(&our_loss_cpu, 1e-5, 1e-5)?; - ground_grad.all_close(&our_grad_cpu, 1e-5, 1e-5)?; - - Ok(()) - } - - #[proptest(cases = 10)] - fn test_cross_entropy(prob: CrossEntropyProblem) { - let CrossEntropyProblem { - batch_size, - num_classes, - } = prob; - println!( - "Testing with batch_size = {}, num_classes = {}", - batch_size, num_classes - ); - run_cross_entropy_trial(prob).unwrap(); - } -} diff --git a/crates/ratchet-nn/src/optim.rs b/crates/ratchet-nn/src/optim.rs deleted file mode 100644 index 4ed86443..00000000 --- a/crates/ratchet-nn/src/optim.rs +++ /dev/null @@ -1,236 +0,0 @@ -#[cfg(target_arch = "wasm32")] -use async_trait::async_trait; -use half::{bf16, f16}; -use maybe_async::maybe_async; -use ratchet::{DType, Device, GradStore, Var}; - -#[maybe_async(AFIT)] -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] -pub trait Optimizer: Sized { - type Config: Sized; - - fn new(vars: Vec<(Option, Var)>, config: Self::Config) -> anyhow::Result; - - async fn step(&mut self, grads: &ratchet::GradStore, device: &Device) -> anyhow::Result<()>; - - fn learning_rate(&self) -> f64; - - fn set_learning_rate(&mut self, lr: f64); - - fn empty(config: Self::Config) -> anyhow::Result { - Self::new(vec![], config) - } - - async fn backward_step( - &mut self, - grads: &mut GradStore, - device: &Device, - ) -> anyhow::Result<()> { - self.step(grads, device).await - } - - fn from_slice(vars: &[&Var], config: Self::Config) -> anyhow::Result { - let vars: Vec<_> = vars.iter().map(|&v| (None, v.clone())).collect(); - Self::new(vars, config) - } -} - -#[derive(Debug)] -pub struct SGD { - vars: Vec<(Option, Var)>, - learning_rate: f64, -} - -#[maybe_async(AFIT)] -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] -impl Optimizer for SGD { - type Config = f64; - - fn new(vars: Vec<(Option, Var)>, learning_rate: f64) -> anyhow::Result { - let vars = vars - .into_iter() - .filter(|(_, v)| v.as_tensor().dt().is_float()) - .collect(); - Ok(Self { - vars, - learning_rate, - }) - } - - fn learning_rate(&self) -> f64 { - self.learning_rate - } - - fn set_learning_rate(&mut self, lr: f64) { - self.learning_rate = lr; - } - - async fn step(&mut self, grads: &ratchet::GradStore, device: &Device) -> anyhow::Result<()> { - let mut updates = Vec::new(); - for (_, var) in &self.vars { - if let Some(grad) = grads.get(var.as_tensor()) { - let update = - (var.as_tensor().clone() - (grad.clone() * self.learning_rate as f32)?)?; - updates.push(var.set(update)); - } - } - - if let Ok(gpu_device) = device.try_gpu() { - gpu_device.mark_step()?; - } - - Ok(()) - } -} - -#[derive(Clone, Debug)] -pub struct ParamsAdamW { - pub lr: f64, - pub beta1: f64, - pub beta2: f64, - pub eps: f64, - pub weight_decay: f64, -} - -impl Default for ParamsAdamW { - fn default() -> Self { - Self { - lr: 0.001, - beta1: 0.9, - beta2: 0.999, - eps: 1e-8, - weight_decay: 0.01, - } - } -} - -#[derive(Debug)] -struct VarAdamW { - var: Var, - first_moment: Var, - second_moment: Var, - label: Option, -} - -#[derive(Debug)] -pub struct AdamW { - vars: Vec, - step_t: usize, - params: ParamsAdamW, -} - -#[maybe_async(AFIT)] -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait)] -impl Optimizer for AdamW { - type Config = ParamsAdamW; - - fn new(vars: Vec<(Option, Var)>, params: ParamsAdamW) -> anyhow::Result { - let vars = vars - .into_iter() - .filter(|(_, var)| var.as_tensor().dt().is_float()) - .map(|(label, var)| { - let var_t = var.as_tensor(); - let dtype = var_t.dt(); - let shape = var_t.shape(); - let device = var_t.device(); - let (first_moment, second_moment) = match dtype { - DType::F32 => ( - Var::zeros::(shape, device), - Var::zeros::(shape, device), - ), - DType::F16 => ( - Var::zeros::(shape, device), - Var::zeros::(shape, device), - ), - DType::BF16 => ( - Var::zeros::(shape, device), - Var::zeros::(shape, device), - ), - _ => return Err(anyhow::anyhow!("Unsupported dtype for AdamW: {:?}", dtype)), - }; - Ok(VarAdamW { - var, - first_moment, - second_moment, - label, - }) - }) - .collect::>>()?; - Ok(Self { - vars, - params, - step_t: 0, - }) - } - fn learning_rate(&self) -> f64 { - self.params.lr - } - - fn set_learning_rate(&mut self, lr: f64) { - self.params.lr = lr - } - - async fn step(&mut self, grads: &ratchet::GradStore, device: &Device) -> anyhow::Result<()> { - self.step_t += 1; - let lr = self.params.lr; - let lambda = self.params.weight_decay; - let lr_lambda = lr * lambda; - let beta1 = self.params.beta1; - let beta2 = self.params.beta2; - let scale_m = 1f64 / (1f64 - beta1.powi(self.step_t as i32)); - let scale_v = 1f64 / (1f64 - beta2.powi(self.step_t as i32)); - - // This makes sure we keep references to the copy tensors. - let mut updates = Vec::new(); - - for var in self.vars.iter_mut() { - let theta = &var.var; - let m = &var.first_moment; - let v = &var.second_moment; - - // println!("Optimizer stepping: {:?}", var.label); - // println!("Theta op: {:?}", theta.as_tensor().op()); - // println!("Theta id: {:?}", theta.as_tensor().id()); - - if let Some(g) = grads.get(theta.as_tensor()) { - let next_m = ((m.as_tensor().clone() * beta1 as f32)? - + (g.clone() * (1.0 - beta1 as f32))?)?; - let next_v = ((v.as_tensor().clone() * beta2 as f32)? - + (g.clone().square()? * (1.0 - beta2 as f32))?)?; - let m_hat = (next_m.clone() * scale_m as f32)?; - let v_hat = (next_v.clone() * scale_v as f32)?; - let next_theta = (theta.as_tensor().clone() * (1f32 - lr_lambda as f32))?; - let adjusted_grad = (m_hat / (v_hat.sqrt()? + self.params.eps as f32)?)?; - let next_theta = (next_theta - (adjusted_grad.clone() * lr as f32)?)?; - - // This ensures we keep references to the copy tensors. - updates.push((theta.set(next_theta), m.set(next_m), v.set(next_v))); - } - } - - // Finalize all the tensors we just built above. - if let Ok(gpu) = device.try_gpu() { - gpu.mark_step()?; - } - - Ok(()) - } -} - -impl AdamW { - pub fn new_lr(vars: Vec<(Option, Var)>, learning_rate: f64) -> anyhow::Result { - let params = ParamsAdamW { - lr: learning_rate, - ..ParamsAdamW::default() - }; - Self::new(vars, params) - } - - pub fn params(&self) -> &ParamsAdamW { - &self.params - } - - pub fn set_params(&mut self, params: ParamsAdamW) { - self.params = params; - } -} diff --git a/crates/ratchet-nn/src/util.rs b/crates/ratchet-nn/src/util.rs deleted file mode 100644 index 480776c4..00000000 --- a/crates/ratchet-nn/src/util.rs +++ /dev/null @@ -1,33 +0,0 @@ -use ratchet::{prelude::shape, Device, GradStore, Tensor}; - -// This seems to work, but it is very slow. Norming and adding all the gradients is probaby quite slow; -// there are probably some easy wins here, like maybe a fused norm op? -pub fn clip_grad_norm( - grads: &mut GradStore, - max_norm: f32, - device: &Device, -) -> anyhow::Result { - let mut total_norm = Tensor::full(&shape![1], 0., device); - let mut any_grads = false; - - for (_, grad) in grads.iter() { - total_norm = (total_norm + grad.clone().norm()?)?; - any_grads = true; - } - - if !any_grads { - return Ok(total_norm); - } - - let clip_coef = (max_norm / (total_norm.clone() + 1e-6)?)?; - let ones_max = Tensor::ones::(&shape![1], device); - let clip_coef = (clip_coef.clone().lt(ones_max.clone()))? - .where_cond(clip_coef.clone(), ones_max)? - .detach(); - - for (_, grad) in grads.iter_mut() { - *grad = grad.clone().mul(clip_coef.clone())?.detach(); - } - - Ok(total_norm) -} diff --git a/crates/ratchet-web/.gitignore b/crates/ratchet-web/.gitignore deleted file mode 100644 index d53c04e7..00000000 --- a/crates/ratchet-web/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/test-data diff --git a/crates/ratchet-web/Cargo.toml b/crates/ratchet-web/Cargo.toml deleted file mode 100644 index c0b5ebc3..00000000 --- a/crates/ratchet-web/Cargo.toml +++ /dev/null @@ -1,73 +0,0 @@ -[package] -name = "ratchet-web" -version = "0.3.0" -edition = "2021" -license = "MIT" -description = "A web-first, cross-platform ML framework." -keywords = ["llm","wasm","transformers","webgpu","ml","machine-learning","deep-learning"] -repository = "https://github.com/FL33TW00D/ratchet" - -[lib] -crate-type = ["cdylib", "rlib"] - -[package.metadata.docs.rs] -default-target = "wasm32-unknown-unknown" - -[package.metadata.wasm-pack.profile.dev.wasm-bindgen] -debug-js-glue = true -demangle-name-section = true -dwarf-debug-info = true - -[package.metadata.wasm-pack.profile.release] -wasm-opt = ['-O3', '--enable-simd'] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -ratchet-models = { path = "../ratchet-models" } -ratchet-hub = { path = "../ratchet-hub" } -ratchet-loader = { path = "../ratchet-loader" } -wasm-bindgen = { workspace = true } -wasm-bindgen-futures = { workspace = true } -js-sys = { workspace = true } -indexed_db_futures = { workspace = true } -thiserror.workspace = true -anyhow.workspace = true -serde = { workspace = true } -serde-wasm-bindgen = { workspace = true } -console_error_panic_hook = { workspace = true } -console_log = { workspace = true, features = ["color"] } -log.workspace = true -hound = { workspace = true } -fern = { workspace = true } -chrono = { workspace = true } -uuid = { workspace = true, features = ["v4", "serde"] } -tokenizers = { version = "0.19.1", default-features = false, features=["unstable_wasm"] } -futures = "0.3.30" -[dependencies.web-sys] -features = [ - 'console', - 'Headers', - 'Request', - 'RequestInit', - 'RequestMode', - 'Response', - 'ReadableStream', - 'ReadableStreamGetReaderOptions', - 'ReadableStreamReaderMode', - 'Window', - 'Navigator', - 'StorageManager', - 'Cache', - 'CacheStorage', - 'IdbKeyRange', -] -workspace = true - - -[target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = { version = "0.2.6", features = ["js"] } - -[dev-dependencies] -wasm-bindgen-test.workspace = true -ratchet-hub = { path = "../ratchet-hub" } - diff --git a/crates/ratchet-web/README.md b/crates/ratchet-web/README.md deleted file mode 100644 index a1247315..00000000 --- a/crates/ratchet-web/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# ratchet-web - diff --git a/crates/ratchet-web/src/db.rs b/crates/ratchet-web/src/db.rs deleted file mode 100644 index b7464268..00000000 --- a/crates/ratchet-web/src/db.rs +++ /dev/null @@ -1,286 +0,0 @@ -use indexed_db_futures::prelude::*; -use js_sys::Uint8Array; -use ratchet_loader::gguf::gguf::Header; -use ratchet_models::{ - registry::{AvailableModels, Quantization}, - TensorMap, WebTensor, -}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use wasm_bindgen::prelude::*; - -#[derive(Debug, thiserror::Error)] -pub enum RatchetDBError { - #[error("DomException {name} ({code}): {message}")] - DomException { - name: String, - message: String, - code: u16, - }, - #[error(transparent)] - SerdeError(#[from] serde_wasm_bindgen::Error), -} - -impl From for RatchetDBError { - fn from(e: indexed_db_futures::web_sys::DomException) -> Self { - Self::DomException { - name: e.name(), - message: e.message(), - code: e.code(), - } - } -} - -pub struct RatchetDB { - pub(crate) inner: IdbDatabase, -} - -type Result = std::result::Result; - -impl RatchetDB { - pub const DB_VERSION: u32 = 1; - pub const DB_NAME: &'static str = "ratchet"; - pub const MODEL_STORE: &'static str = "models"; - pub const TOKENIZER_STORE: &'static str = "tokenizers"; - pub const TENSOR_STORE: &'static str = "tensors"; - pub const TENSOR_INDEX: &'static str = "model_key"; - - fn serialize(o: &impl Serialize) -> Result { - serde_wasm_bindgen::to_value(o).map_err(|e| e.into()) - } - - fn deserialize Deserialize<'de>>(o: Option) -> Result> { - o.map(serde_wasm_bindgen::from_value) - .transpose() - .map_err(|e| e.into()) - } - - pub async fn open() -> Result { - let mut db_req: OpenDbRequest = IdbDatabase::open_u32(Self::DB_NAME, Self::DB_VERSION)?; - - db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { - let create_store_if_needed = - |evt: &IdbVersionChangeEvent, store_key: &'static str| -> Result<(), JsValue> { - if let None = evt.db().object_store_names().find(|n| n == store_key) { - evt.db().create_object_store(store_key)?; - } - Ok(()) - }; - - let create_store_with_index_if_needed = |evt: &IdbVersionChangeEvent, - store_key: &'static str, - index_key: &'static str| - -> Result<(), JsValue> { - if let None = evt.db().object_store_names().find(|n| n == store_key) { - let store = evt.db().create_object_store(store_key)?; - store.create_index(index_key, &IdbKeyPath::str(index_key))?; - } - Ok(()) - }; - - create_store_if_needed(evt, Self::MODEL_STORE)?; - create_store_if_needed(evt, Self::TOKENIZER_STORE)?; - create_store_with_index_if_needed(evt, Self::TENSOR_STORE, Self::TENSOR_INDEX)?; - - Ok(()) - })); - - Ok(Self { - inner: db_req.await?, - }) - } - - pub async fn get_model(&self, key: &ModelKey) -> Result> { - let tx = self - .inner - .transaction_on_one_with_mode(Self::MODEL_STORE, IdbTransactionMode::Readonly)?; - let store = tx.object_store(Self::MODEL_STORE)?; - let serial_key = Self::serialize(key)?; - let req = store.get(&serial_key)?.await?; - Self::deserialize(req) - } - - pub async fn put_model(&self, key: &ModelKey, model: ModelRecord) -> Result<()> { - let tx = self - .inner - .transaction_on_one_with_mode(Self::MODEL_STORE, IdbTransactionMode::Readwrite)?; - let store = tx.object_store(Self::MODEL_STORE)?; - store - .put_key_val(&Self::serialize(key)?, &Self::serialize(&model)?)? - .await?; - Ok(()) - } - - pub async fn get_tensors(&self, key: &ModelKey) -> Result { - let tx = self - .inner - .transaction_on_one_with_mode(Self::MODEL_STORE, IdbTransactionMode::Readonly)?; - let store = tx.object_store(Self::MODEL_STORE)?; - let req = store.get(&Self::serialize(key)?)?.await?; - let model: ModelRecord = Self::deserialize(req)?.unwrap(); - - log::warn!("Model: {:?}", model); - let header = serde_wasm_bindgen::from_value::
(model.header).unwrap(); - - log::warn!("GOT HEADER: {:?}", header); - - let tx = self - .inner - .transaction_on_one_with_mode(Self::TENSOR_STORE, IdbTransactionMode::Readonly)?; - let store = tx.object_store(Self::TENSOR_STORE)?; - let index = store.index(Self::TENSOR_INDEX)?; - let matches = index.get_all_with_key(&Self::serialize(key)?)?.await?; - - let mut map = TensorMap::new(); - for record in matches { - let record: TensorRecord = Self::deserialize(Some(record))?.unwrap(); - let ti = header.tensor_infos.get(&record.name).unwrap(); - let tensor = WebTensor::new(ti.ggml_dtype, record.bytes, ti.shape.clone()); - map.insert(record.name, tensor); - } - Ok(map) - } - - pub async fn put_tensor(&self, tensor: TensorRecord) -> Result<()> { - let tx = self - .inner - .transaction_on_one_with_mode(Self::TENSOR_STORE, IdbTransactionMode::Readwrite)?; - let store = tx.object_store(Self::TENSOR_STORE)?; - store - .put_key_val( - &Self::serialize(&tensor.tensor_id)?, - &Self::serialize(&tensor)?, - )? - .await?; - Ok(()) - } - - pub async fn get_tokenizer>( - &self, - repo_id: S, - ) -> Result> { - let tx = self - .inner - .transaction_on_one_with_mode(Self::TOKENIZER_STORE, IdbTransactionMode::Readonly)?; - let store = tx.object_store(Self::TOKENIZER_STORE)?; - let req = store.get(&Self::serialize(&repo_id.as_ref())?)?.await?; - Self::deserialize(req) - } - - pub async fn put_tokenizer>( - &self, - repo_id: S, - tokenizer: TokenizerRecord, - ) -> Result<()> { - let tx = self - .inner - .transaction_on_one_with_mode(Self::TOKENIZER_STORE, IdbTransactionMode::Readwrite)?; - let store = tx.object_store(Self::TOKENIZER_STORE)?; - store - .put_key_val( - &Self::serialize(&repo_id.as_ref())?, - &Self::serialize(&tokenizer)?, - )? - .await?; - Ok(()) - } -} - -#[wasm_bindgen] -#[derive(Debug, Clone)] -pub struct ModelKey { - repo_id: String, - model_id: String, -} - -impl serde::Serialize for ModelKey { - fn serialize( - &self, - serializer: S, - ) -> std::result::Result { - let s = format!("{}:{}", self.repo_id, self.model_id); - serializer.serialize_str(&s) - } -} - -impl<'de> serde::Deserialize<'de> for ModelKey { - fn deserialize>( - deserializer: D, - ) -> std::result::Result { - let s = String::deserialize(deserializer)?; - let mut parts = s.split(':'); - let repo_id = parts.next().unwrap().to_string(); - let model_id = parts.next().unwrap().to_string(); - Ok(Self { repo_id, model_id }) - } -} - -impl ModelKey { - pub fn from_available(av: &AvailableModels, quant: Quantization) -> Self { - ModelKey { - repo_id: av.repo_id(), - model_id: av.model_id(quant), - } - } -} - -#[wasm_bindgen] -impl ModelKey { - #[wasm_bindgen(constructor)] - pub fn new(repo_id: String, model_id: String) -> Self { - Self { repo_id, model_id } - } - - #[wasm_bindgen(getter)] - pub fn repo_id(&self) -> String { - self.repo_id.clone() - } - - #[wasm_bindgen(getter)] - pub fn model_id(&self) -> String { - self.model_id.clone() - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ModelRecord { - pub key: ModelKey, - pub model: AvailableModels, - #[serde(with = "serde_wasm_bindgen::preserve")] - pub header: JsValue, -} - -impl ModelRecord { - pub fn new(key: ModelKey, model: AvailableModels, header: Header) -> Self { - let header = serde_wasm_bindgen::to_value(&header).unwrap(); - Self { key, model, header } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct TensorRecord { - pub tensor_id: Uuid, - pub name: String, - pub model_key: ModelKey, - #[serde(with = "serde_wasm_bindgen::preserve")] - pub bytes: Uint8Array, -} - -impl TensorRecord { - pub fn new(name: String, model_key: ModelKey, bytes: Uint8Array) -> Self { - Self { - tensor_id: Uuid::new_v4(), - name, - model_key, - bytes, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct TokenizerRecord { - pub repo_id: String, - pub tokenizer_id: String, - #[serde(with = "serde_wasm_bindgen::preserve")] - pub bytes: Uint8Array, -} diff --git a/crates/ratchet-web/src/lib.rs b/crates/ratchet-web/src/lib.rs deleted file mode 100644 index 66c6e4d9..00000000 --- a/crates/ratchet-web/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -#![cfg(target_arch = "wasm32")] -mod db; -mod model; diff --git a/crates/ratchet-web/src/model.rs b/crates/ratchet-web/src/model.rs deleted file mode 100644 index e08101f3..00000000 --- a/crates/ratchet-web/src/model.rs +++ /dev/null @@ -1,408 +0,0 @@ -use crate::db::*; -use futures::stream::TryStreamExt; -use futures::StreamExt; -use ratchet_hub::{Api, ApiBuilder, RepoType}; -use ratchet_loader::gguf::gguf::{self, Header, TensorInfo}; -use ratchet_models::moondream::{self, Moondream}; -use ratchet_models::phi2; -use ratchet_models::phi2::Phi2; -use ratchet_models::phi3::{self, Phi3}; -use ratchet_models::registry::{AvailableModels, PhiVariants, Quantization}; -use ratchet_models::whisper::{transcribe::transcribe, transcript::StreamedSegment, Whisper}; -use ratchet_models::TensorMap; -use tokenizers::Tokenizer; -use wasm_bindgen::prelude::*; - -#[derive(Debug)] -pub enum WebModel { - Whisper(Whisper), - Phi2(Phi2), - Phi3(Phi3), - Moondream(Moondream), -} - -impl WebModel { - pub async fn run(&mut self, input: JsValue) -> Result { - match self { - WebModel::Whisper(model) => { - let input: WhisperInputs = serde_wasm_bindgen::from_value(input)?; - let options = serde_wasm_bindgen::from_value(input.decode_options)?; - - let callback = if !input.callback.is_null() { - let rs_callback = |decoded: StreamedSegment| { - let js_decoded = serde_wasm_bindgen::to_value(&decoded).unwrap(); - let _ = input.callback.call1(&JsValue::NULL, &js_decoded); - }; - Some(rs_callback) - } else { - None - }; - - let result = transcribe(model, input.audio, options, callback) - .await - .unwrap(); - serde_wasm_bindgen::to_value(&result).map_err(|e| e.into()) - } - WebModel::Phi2(model) => { - let input: PhiInputs = serde_wasm_bindgen::from_value(input)?; - let rs_callback = |output: String| { - let _ = input.callback.call1(&JsValue::NULL, &output.into()); - }; - let prompt = input.prompt; - - let model_repo = ApiBuilder::from_hf("microsoft/phi-2", RepoType::Model).build(); - let model_bytes = model_repo.get("tokenizer.json").await?; - let tokenizer = Tokenizer::from_bytes(model_bytes.to_vec()).unwrap(); - phi2::generate(model, tokenizer, prompt, rs_callback) - .await - .unwrap(); - Ok(JsValue::NULL) - } - WebModel::Phi3(model) => { - let input: PhiInputs = serde_wasm_bindgen::from_value(input)?; - let rs_callback = |output: String| { - let _ = input.callback.call1(&JsValue::NULL, &output.into()); - }; - let prompt = input.prompt; - - let model_repo = - ApiBuilder::from_hf("microsoft/Phi-3-mini-4k-instruct", RepoType::Model) - .build(); - let model_bytes = model_repo.get("tokenizer.json").await?; - let tokenizer = Tokenizer::from_bytes(model_bytes.to_vec()).unwrap(); - phi3::generate(model, tokenizer, prompt, rs_callback) - .await - .unwrap(); - Ok(JsValue::NULL) - } - WebModel::Moondream(model) => { - let input: MoondreamInputs = serde_wasm_bindgen::from_value(input)?; - let rs_callback = |output: String| { - let _ = input.callback.call1(&JsValue::NULL, &output.into()); - }; - let model_repo = - ApiBuilder::from_hf("tgestson/ratchet-moondream2", RepoType::Model).build(); - let model_bytes = model_repo.get("tokenizer.json").await?; - let tokenizer = Tokenizer::from_bytes(model_bytes.to_vec()).unwrap(); - moondream::generate( - model, - input.image_bytes, - input.question, - tokenizer, - rs_callback, - ) - .await - .unwrap(); - Ok(JsValue::NULL) - } - } - } - - pub async fn from_stored( - model_record: ModelRecord, - tensor_map: TensorMap, - ) -> Result { - let header = serde_wasm_bindgen::from_value::
(model_record.header).unwrap(); - match model_record.model { - AvailableModels::Whisper(variant) => { - let model = Whisper::from_web(header, tensor_map, variant).await?; - Ok(WebModel::Whisper(model)) - } - AvailableModels::Phi(variant) => match variant { - PhiVariants::Phi2 => { - let model = Phi2::from_web(header, tensor_map).await?; - Ok(WebModel::Phi2(model)) - } - PhiVariants::Phi3 => { - let model = Phi3::from_web(header, tensor_map).await?; - Ok(WebModel::Phi3(model)) - } - }, - AvailableModels::Moondream => { - let model = Moondream::from_web(header, tensor_map).await?; - Ok(WebModel::Moondream(model)) - } - _ => Err(anyhow::anyhow!("Unknown model type")), - } - } -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct WhisperInputs { - pub audio: Vec, - #[serde(with = "serde_wasm_bindgen::preserve")] - pub decode_options: JsValue, - #[serde(with = "serde_wasm_bindgen::preserve")] - pub callback: js_sys::Function, -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct PhiInputs { - pub prompt: String, - #[serde(with = "serde_wasm_bindgen::preserve")] - pub callback: js_sys::Function, -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct MoondreamInputs { - pub question: String, - pub image_bytes: Vec, - #[serde(with = "serde_wasm_bindgen::preserve")] - pub callback: js_sys::Function, -} - -#[wasm_bindgen] -#[derive(Debug)] -pub struct Model { - inner: WebModel, -} - -#[wasm_bindgen] -impl Model { - /// The main JS entrypoint into the library. - /// - /// Loads a model with the provided ID. - /// This key should be an enum of supported models. - #[wasm_bindgen] - pub async fn load( - model: AvailableModels, - quantization: Quantization, - progress: &js_sys::Function, - ) -> Result { - let model_key = ModelKey::from_available(&model, quantization); - let model_repo = ApiBuilder::from_hf(&model_key.repo_id(), RepoType::Model).build(); - - let webModel = Self::load_inner(model, &model_repo, model_key, progress).await?; - - Ok(Model { inner: webModel }) - } - - #[wasm_bindgen] - pub async fn load_custom( - endpoint: String, - model: AvailableModels, - quantization: Quantization, - progress: &js_sys::Function, - ) -> Result { - let model_key = ModelKey::from_available(&model, quantization); - let model_repo = ApiBuilder::from_custom(endpoint).build(); - - let webModel = Self::load_inner(model, &model_repo, model_key, progress).await?; - - Ok(Model { inner: webModel }) - } - - async fn load_inner( - model: AvailableModels, - model_repo: &Api, - model_key: ModelKey, - progress: &js_sys::Function, - ) -> Result { - let db = RatchetDB::open().await.map_err(|e| { - let e: JsError = e.into(); - Into::::into(e) - })?; - - log::warn!("Loading model: {:?}", model_key); - if let None = db.get_model(&model_key).await.map_err(|e| { - let e: JsError = e.into(); - Into::::into(e) - })? { - let header: gguf::Header = serde_wasm_bindgen::from_value( - model_repo.fetch_gguf_header(&model_key.model_id()).await?, - )?; - Self::fetch_tensors(&db, &model_repo, &header, model_key.clone(), progress).await?; - let model_record = ModelRecord::new(model_key.clone(), model.clone(), header); - db.put_model(&model_key, model_record).await.map_err(|e| { - let e: JsError = e.into(); - Into::::into(e) - })?; - }; - - let model_record = db.get_model(&model_key).await.unwrap().unwrap(); - let tensors = db.get_tensors(&model_key).await.unwrap(); - - Ok(WebModel::from_stored(model_record, tensors).await.unwrap()) - } - - /// User-facing method to run the model. - /// - /// Untyped input is required unfortunately. - pub async fn run(&mut self, input: JsValue) -> Result { - self.inner.run(input).await - } - - async fn fetch_tensors( - db: &RatchetDB, - model_repo: &Api, - header: &Header, - model_key: ModelKey, - progress: &js_sys::Function, - ) -> Result<(), JsValue> { - let model_id = model_key.model_id(); - let data_offset = header.tensor_data_offset; - let content_len = header - .tensor_infos - .values() - .fold(0, |acc, ti| acc + ti.size_in_bytes()); - - let mut tensor_infos: Vec<(String, TensorInfo)> = - header.tensor_infos.clone().into_iter().collect(); - tensor_infos.sort_by(|(_, a), (_, b)| b.size_in_bytes().cmp(&a.size_in_bytes())); - - let tensor_stream = futures::stream::iter(tensor_infos); - - let mut total_progress = 0.0; - - tensor_stream - .map(|(name, info): (String, TensorInfo)| { - let model_id = model_id.clone(); - let model_key = model_key.clone(); - async move { - let range = info.byte_range(data_offset); - let bytes = model_repo - .fetch_range(&model_id, range.start, range.end) - .await - .unwrap(); - let length = bytes.length(); - let record = - TensorRecord::new(name.clone().to_string(), model_key.clone(), bytes); - db.put_tensor(record).await.map_err(|e| { - let e: JsError = e.into(); - Into::::into(e) - }); - length - } - }) - .buffer_unordered(6) - .map(|num_bytes| { - let req_progress = (num_bytes as f64) / (content_len as f64) * 100.0; - total_progress += req_progress; - let _ = progress.call1(&JsValue::NULL, &total_progress.into()); - }) - .collect::<()>() - .await; - - Ok(()) - } -} - -#[cfg(all(test, target_arch = "wasm32"))] -mod tests { - use super::*; - use ratchet_hub::{ApiBuilder, RepoType}; - use ratchet_models::registry::PhiVariants; - use ratchet_models::registry::WhisperVariants; - use ratchet_models::whisper::options::DecodingOptionsBuilder; - use tokenizers::Tokenizer; - use wasm_bindgen_test::*; - - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - fn log_init() { - console_error_panic_hook::set_once(); - let logger = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{}[{}][{}] {}", - chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), - record.target(), - record.level(), - message - )) - }) - .level_for("tokenizers", log::LevelFilter::Off) - .level(log::LevelFilter::Warn) - .chain(fern::Output::call(console_log::log)) - .apply(); - match logger { - Ok(_) => log::info!("Logging initialized."), - Err(error) => eprintln!("Error initializing logging: {:?}", error), - } - } - - fn load_sample(bytes: &[u8]) -> Vec { - let mut reader = hound::WavReader::new(std::io::Cursor::new(bytes)).unwrap(); - reader - .samples::() - .map(|x| x.unwrap() as f32 / 32768.0) - .collect::>() - } - - #[wasm_bindgen_test] - async fn whisper_browser() -> Result<(), JsValue> { - log_init(); - let download_cb: Closure = Closure::new(|p| { - log::info!("Provided closure got progress: {}", p); - }); - let js_cb: &js_sys::Function = download_cb.as_ref().unchecked_ref(); - - let mut model = Model::load( - AvailableModels::Whisper(WhisperVariants::Base), - Quantization::F16, - js_cb, - ) - .await - .unwrap(); - - let data_repo = ApiBuilder::from_hf("FL33TW00D-HF/ratchet-util", RepoType::Dataset).build(); - let audio_bytes = data_repo.get("mm0.wav").await?; - let sample = load_sample(&audio_bytes.to_vec()); - - let decode_options = DecodingOptionsBuilder::default().build(); - - let cb: Closure = Closure::new(|s| { - log::info!("GENERATED SEGMENT: {:?}", s); - }); - let js_cb: &js_sys::Function = cb.as_ref().unchecked_ref(); - - let input = WhisperInputs { - audio: sample, - decode_options, - callback: js_cb.clone(), - }; - let input = serde_wasm_bindgen::to_value(&input).unwrap(); - let result = model.run(input).await.unwrap(); - log::warn!("Result: {:?}", result); - Ok(()) - } - - #[wasm_bindgen_test] - async fn whisper_browser_custom() -> Result<(), JsValue> { - log_init(); - let download_cb: Closure = Closure::new(|p| { - log::info!("Provided closure got progress: {}", p); - }); - let js_cb: &js_sys::Function = download_cb.as_ref().unchecked_ref(); - - let mut model = Model::load_custom( - "https://huggingface.co/FL33TW00D-HF/whisper-base/resolve/main".to_string(), - AvailableModels::Whisper(WhisperVariants::Base), - Quantization::F16, - js_cb, - ) - .await - .unwrap(); - - let data_repo = ApiBuilder::from_hf("FL33TW00D-HF/ratchet-util", RepoType::Dataset).build(); - let audio_bytes = data_repo.get("mm0.wav").await?; - let sample = load_sample(&audio_bytes.to_vec()); - - let decode_options = DecodingOptionsBuilder::default().build(); - - let cb: Closure = Closure::new(|s| { - log::info!("GENERATED SEGMENT: {:?}", s); - }); - let js_cb: &js_sys::Function = cb.as_ref().unchecked_ref(); - - let input = WhisperInputs { - audio: sample, - decode_options, - callback: js_cb.clone(), - }; - let input = serde_wasm_bindgen::to_value(&input).unwrap(); - let result = model.run(input).await.unwrap(); - log::warn!("Result: {:?}", result); - Ok(()) - } -} diff --git a/examples/piston-train-toy/.editorconfig b/examples/piston-train-toy/.editorconfig new file mode 100644 index 00000000..5696c4e8 --- /dev/null +++ b/examples/piston-train-toy/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = tab +tab_width = 2 +charset = utf-8 +trim_trailing_whitespace = true + +[*.py] +indent_style = space +indent_size = 4 +tab_width = 4 + +[*.ipynb] +indent_style = space +indent_size = 4 +tab_width = 4 + +[package.json] +indent_style = space diff --git a/examples/piston-train-toy/.gitignore b/examples/piston-train-toy/.gitignore new file mode 100644 index 00000000..a1bfde7c --- /dev/null +++ b/examples/piston-train-toy/.gitignore @@ -0,0 +1,26 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +static/tokenizer +static/tokenized diff --git a/examples/piston-train-toy/.npmrc b/examples/piston-train-toy/.npmrc new file mode 100644 index 00000000..fbb338d9 --- /dev/null +++ b/examples/piston-train-toy/.npmrc @@ -0,0 +1,2 @@ +engine-strict=true +use-node-version=22.17.1 diff --git a/examples/piston-train-toy/.prettierignore b/examples/piston-train-toy/.prettierignore new file mode 100644 index 00000000..ab78a95d --- /dev/null +++ b/examples/piston-train-toy/.prettierignore @@ -0,0 +1,4 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/examples/piston-train-toy/.prettierrc b/examples/piston-train-toy/.prettierrc new file mode 100644 index 00000000..bd92dfb0 --- /dev/null +++ b/examples/piston-train-toy/.prettierrc @@ -0,0 +1,18 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "tabWidth": 2, + "plugins": [ + "prettier-plugin-svelte" + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/examples/piston-train-toy/README.md b/examples/piston-train-toy/README.md new file mode 100644 index 00000000..28e15fc5 --- /dev/null +++ b/examples/piston-train-toy/README.md @@ -0,0 +1 @@ +This is the source code for the web demo at [sequence.toys](https://sequence.toys). It is a static Svelte app. diff --git a/examples/piston-train-toy/eslint.config.js b/examples/piston-train-toy/eslint.config.js new file mode 100644 index 00000000..2318336b --- /dev/null +++ b/examples/piston-train-toy/eslint.config.js @@ -0,0 +1,77 @@ +import { includeIgnoreFile } from '@eslint/compat'; +import js from '@eslint/js'; +import prettier from 'eslint-config-prettier'; +import perfectionist from 'eslint-plugin-perfectionist'; +import svelte from 'eslint-plugin-svelte'; +import { defineConfig } from 'eslint/config'; +import globals from 'globals'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript-eslint'; +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default defineConfig( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs['flat/recommended'], + ...svelte.configs['flat/prettier'], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + } + } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.ts'], + + languageOptions: { + parserOptions: { + parser: ts.parser + } + } + }, + { + plugins: { + prettier, + perfectionist + }, + rules: { + 'no-unused-vars': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } + ], + '@typescript-eslint/no-var-requires': 'off', + 'perfectionist/sort-imports': [ + 'error', + { + type: 'natural', + order: 'asc', + groups: [ + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'side-effect', + 'style', + 'object', + 'unknown' + ] + } + ], + 'perfectionist/sort-exports': ['error', { type: 'natural' }], + 'perfectionist/sort-named-exports': ['error', { type: 'natural' }], + 'perfectionist/sort-named-imports': ['error', { type: 'natural' }] + } + } +); diff --git a/examples/piston-train-toy/package.json b/examples/piston-train-toy/package.json new file mode 100644 index 00000000..72b1fef3 --- /dev/null +++ b/examples/piston-train-toy/package.json @@ -0,0 +1,60 @@ +{ + "name": "piston-train-toy", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "eslint . && prettier --check .", + "format": "prettier --write ." + }, + "dependencies": { + "@codemirror/autocomplete": "^6.19.1", + "@codemirror/language": "^6.11.3", + "@codemirror/lint": "^6.9.1", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.6", + "@huggingface/jinja": "^0.5.1", + "@piston-ml/piston-web": "workspace:^", + "@types/katex": "^0.16.7", + "codemirror": "^6.0.2", + "echarts": "^6.0.0", + "katex": "^0.16.23", + "lucide-svelte": "^0.548.0", + "maxrects-packer": "^2.7.3", + "random-js": "^2.1.0", + "svelte-portal": "^2.2.1", + "unique-names-generator": "^4.7.1" + }, + "devDependencies": { + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.37.0", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.46.4", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/vite": "^4.1.14", + "@types/glob": "^9.0.0", + "@webgpu/types": "^0.1.65", + "eslint": "^9.37.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-perfectionist": "^4.15.1", + "eslint-plugin-svelte": "^3.12.4", + "glob": "^11.0.3", + "globals": "^16.4.0", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.4.0", + "rollup": "^4.52.4", + "svelte": "^5.39.11", + "svelte-check": "^4.3.3", + "tailwindcss": "^4.1.14", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.0", + "vite": "^7.1.9", + "vite-plugin-wasm": "^3.5.0" + } +} diff --git a/examples/piston-train-toy/src/app.css b/examples/piston-train-toy/src/app.css new file mode 100644 index 00000000..fda981af --- /dev/null +++ b/examples/piston-train-toy/src/app.css @@ -0,0 +1,96 @@ +@import url('https://use.typekit.net/ixr7lrv.css'); +@import 'tailwindcss' source('./'); + +@font-face { + font-family: 'Berkeley Mono Variable'; + src: url('/Berkeley Mono Variable.woff2') format('woff2-variations'); + font-weight: 400 700; + font-style: normal; + font-display: swap; +} + +@theme { + --font-mono: 'Berkeley Mono Variable', monospace; + --font-sans: 'neue-haas-unica', sans-serif; + + --spacing: 0.2rem; + + /* 10px */ + --text-2xs: 0.625rem; + --text-2xs--line-height: calc(1 / 0.625); + /* 11px */ + --text-xs: 0.6875rem; + --text-xs--line-height: calc(1 / 0.6875); + /* 12px */ + --text-sm: 0.75rem; + --text-sm--line-height: calc(1 / 0.75); + /* 13px */ + --text-base: 0.8125rem; + --text-base--line-height: calc(1.125 / 0.8125); + /* 14px */ + --text-lg: 0.875rem; + --text-lg--line-height: calc(1.25 / 0.875); + /* 16px */ + --text-xl: 1rem; + --text-xl--line-height: calc(1.5 / 1); + /* 18px */ + --text-2xl: 1.125rem; + --text-2xl--line-height: calc(1.75 / 1.125); + /* 20px */ + --text-3xl: 1.25rem; + --text-3xl--line-height: calc(1.75 / 1.25); + /* 24px */ + --text-4xl: 1.5rem; + --text-4xl--line-height: calc(2 / 1.5); + /* 30px */ + --text-5xl: 1.875rem; + --text-5xl--line-height: calc(2.25 / 1.875); + /* 36px */ + --text-6xl: 2.25rem; + --text-6xl--line-height: calc(2.5 / 2.25); + /* 48px */ + --text-7xl: 3rem; + --text-7xl--line-height: 1; + /* 60px */ + --text-8xl: 3.75rem; + --text-8xl--line-height: 1; + /* 72px */ + --text-9xl: 4.5rem; + + --text-controls-numeric: 0.78125rem; + --text-controls-numeric--line-height: calc(1 / 0.78125); + + --color-panel-border-base: var(--color-neutral-300); + --color-panel: oklch(0.96 0 0); +} + +:root { + interpolate-size: allow-keywords; +} + +html { + /* Base font size for fine pointers (desktop/mouse) */ + font-size: 16px; + overscroll-behavior: none; + @apply h-full; + @apply w-full; +} + +@media (pointer: coarse) { + html { + /* Larger base size for touch devices */ + font-size: 18px; + } + + :root { + /* Extra spacing on mobile */ + --spacing: 0.25rem; + } +} + +body { + @apply h-full; + @apply w-full; + @apply text-base; + overscroll-behavior: none; +} diff --git a/examples/piston-train-toy/src/app.d.ts b/examples/piston-train-toy/src/app.d.ts new file mode 100644 index 00000000..da2a8798 --- /dev/null +++ b/examples/piston-train-toy/src/app.d.ts @@ -0,0 +1,14 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + const __COMMIT_HASH__: string; + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/examples/piston-train-toy/src/app.html b/examples/piston-train-toy/src/app.html new file mode 100644 index 00000000..f273cc58 --- /dev/null +++ b/examples/piston-train-toy/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/examples/piston-train-toy/src/lib/attachments/echarts.svelte.ts b/examples/piston-train-toy/src/lib/attachments/echarts.svelte.ts new file mode 100644 index 00000000..2ba10bd5 --- /dev/null +++ b/examples/piston-train-toy/src/lib/attachments/echarts.svelte.ts @@ -0,0 +1,236 @@ +import type { Attachment } from 'svelte/attachments'; + +import * as echarts from 'echarts/core'; + +type EChartsAttachmentParams = { + opts: () => echarts.EChartsCoreOption | null; + // Optional setup hook to programmatically configure the instance (e.g., event bridging) + // May return a cleanup function that will be called on detach. + setup?: (chart: echarts.ECharts, getPeers: undefined) => void | (() => void); +}; + +export function setupAxisSync(chart: echarts.ECharts, getPeers: () => echarts.ECharts[]) { + let isRelaying = false; + chart.on('updateAxisPointer', (evt) => { + if (isRelaying) return; + const e = evt as { axesInfo?: Array<{ axisDim: string; value: number }> }; + const axesInfo = e?.axesInfo; + if (!axesInfo || axesInfo.length === 0) return; + const xInfo = axesInfo.find((a) => a.axisDim === 'x'); + if (!xInfo) return; + + const catIndex = Math.max(0, Math.floor(xInfo.value ?? 0)); + const opt = chart.getOption?.() as + | { xAxis?: Array<{ data?: (number | string)[] }> } + | undefined; + const cats = opt?.xAxis?.[0]?.data ?? []; + const step = Number(cats[catIndex] ?? catIndex); + + isRelaying = true; + for (const peer of getPeers()) { + const hit = findPeerDataIndexByStep(peer, step); + peer.dispatchAction({ + type: 'updateAxisPointer', + seriesIndex: hit?.seriesIndex ?? 0, + dataIndex: hit?.dataIndex ?? catIndex + }); + } + Promise.resolve().then(() => { + isRelaying = false; + }); + }); +} + +// Binary-search utilities for sorted numeric step arrays +export function exactIndex(steps: number[], target: number): number { + let lo = 0, + hi = steps.length - 1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + const v = steps[mid]; + if (v === target) return mid; + if (v < target) lo = mid + 1; + else hi = mid - 1; + } + return -1; +} + +export function nearestIndex(steps: number[], target: number): number { + if (steps.length === 0) return -1; + const first = steps[0], + last = steps[steps.length - 1]; + if (target < first || target > last) return -1; + let lo = 0, + hi = steps.length; + while (lo < hi) { + const mid = (lo + hi) >> 1; + if (steps[mid] < target) lo = mid + 1; + else hi = mid; + } + if (lo === 0) return 0; + if (lo === steps.length) return steps.length - 1; + return target - steps[lo - 1] <= steps[lo] - target ? lo - 1 : lo; +} + +// Extract numeric x/step from an ECharts updateAxisPointer event +export function extractStepFromAxisPointerEvent(evt: unknown): number | null { + const e = evt as { axesInfo?: Array<{ axisDim: string; value: number }> } | undefined; + const xInfo = e?.axesInfo?.find((a) => a.axisDim === 'x'); + const step = Number(xInfo?.value); + return Number.isFinite(step) ? step : null; +} + +function extractXValue(datum: unknown): number | null { + if (Array.isArray(datum)) { + const x = Number(datum[0]); + return Number.isFinite(x) ? x : null; + } + if (datum && typeof datum === 'object') { + const obj = datum as { value?: unknown; x?: unknown; step?: unknown }; + if (Array.isArray(obj.value)) { + const x = Number(obj.value[0]); + return Number.isFinite(x) ? x : null; + } + const xCandidate = obj.x ?? obj.step; + if (typeof xCandidate === 'number') return xCandidate; + if (typeof xCandidate === 'string') { + const parsed = Number(xCandidate); + return Number.isFinite(parsed) ? parsed : null; + } + } + return null; +} + +function linearSearchByX(data: unknown[], targetStep: number): number { + for (let i = 0; i < data.length; i++) { + const x = extractXValue(data[i]); + if (x === targetStep) return i; + } + return -1; +} + +function binarySearchByX(data: unknown[], targetStep: number): number { + if (data.length === 0) return -1; + const first = extractXValue(data[0]); + const last = extractXValue(data[data.length - 1]); + if (first === null || last === null) return linearSearchByX(data, targetStep); + + const ascending = last >= first; + let lo = 0; + let hi = data.length - 1; + while (lo <= hi) { + const mid = lo + ((hi - lo) >> 1); + const x = extractXValue(data[mid]); + if (x === null) return linearSearchByX(data, targetStep); + if (x === targetStep) return mid; + if (ascending ? x < targetStep : x > targetStep) lo = mid + 1; + else hi = mid - 1; + } + return -1; +} + +function getSeriesArrayFromOption(opt: unknown): unknown[] { + const o = opt as { series?: unknown[] } | undefined; + return Array.isArray(o?.series) ? (o!.series as unknown[]) : []; +} + +function getDataArrayFromSeries(seriesItem: unknown): unknown[] { + const s = seriesItem as { data?: unknown[] } | undefined; + return Array.isArray(s?.data) ? (s!.data as unknown[]) : []; +} + +export function findPeerDataIndexByStep( + peer: echarts.ECharts, + step: number +): { seriesIndex: number; dataIndex: number } | null { + const opt = peer.getOption() as unknown; + const seriesArr = getSeriesArrayFromOption(opt); + if (seriesArr.length === 0) return null; + const seriesIndex = seriesArr.length - 1; // highest series index + const data = getDataArrayFromSeries(seriesArr[seriesIndex]); + if (data.length === 0) return null; + const dataIndex = binarySearchByX(data, step); + if (dataIndex < 0) return null; + return { seriesIndex, dataIndex }; +} + +export default function createEChartsAttachment(params: EChartsAttachmentParams): Attachment { + return (node: Element) => { + if (!(node instanceof HTMLDivElement)) { + throw new Error('ECharts attachment requires a div element'); + } + + const chart = echarts.init(node); + const getPeers = undefined; + + // Allow caller to set up custom behavior (e.g., axis-pointer mapping) + let setupCleanup: (() => void) | undefined; + const maybeCleanup = params.setup?.(chart, getPeers); + if (typeof maybeCleanup === 'function') setupCleanup = maybeCleanup; + + const resizeObserver = new ResizeObserver(() => { + chart.resize(); + }); + resizeObserver.observe(node); + + $effect(() => { + const options = params.opts?.(); + if (options) { + chart.setOption(options, { + notMerge: false, + replaceMerge: ['series'], + lazyUpdate: false + }); + } + }); + + return () => { + resizeObserver.unobserve(node); + setupCleanup?.(); + chart.dispose(); + }; + }; +} + +type MoveDetail = { + sourceId: string; + runId: string; + step: number; +}; + +type ClearDetail = { + sourceId: string; +}; + +const MOVE_EVENT = 'run-pointer:move'; +const CLEAR_EVENT = 'run-pointer:clear'; + +const bus: EventTarget = new EventTarget(); + +export function publishMove(detail: MoveDetail): void { + bus.dispatchEvent(new CustomEvent(MOVE_EVENT, { detail })); +} + +export function publishClear(detail: ClearDetail): void { + bus.dispatchEvent(new CustomEvent(CLEAR_EVENT, { detail })); +} + +export function subscribe( + onMove?: (detail: MoveDetail) => void, + onClear?: (detail: ClearDetail) => void +): () => void { + const moveListener = (e: Event) => { + const ce = e as CustomEvent; + if (onMove) onMove(ce.detail); + }; + const clearListener = (e: Event) => { + const ce = e as CustomEvent; + if (onClear) onClear(ce.detail); + }; + if (onMove) bus.addEventListener(MOVE_EVENT, moveListener as EventListener); + if (onClear) bus.addEventListener(CLEAR_EVENT, clearListener as EventListener); + return () => { + if (onMove) bus.removeEventListener(MOVE_EVENT, moveListener as EventListener); + if (onClear) bus.removeEventListener(CLEAR_EVENT, clearListener as EventListener); + }; +} diff --git a/examples/piston-train-toy/src/lib/components/ActionButton.svelte b/examples/piston-train-toy/src/lib/components/ActionButton.svelte new file mode 100644 index 00000000..e93cda97 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/ActionButton.svelte @@ -0,0 +1,63 @@ + + + diff --git a/examples/piston-train-toy/src/lib/components/ChevronIcon.svelte b/examples/piston-train-toy/src/lib/components/ChevronIcon.svelte new file mode 100644 index 00000000..5111db89 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/ChevronIcon.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/examples/piston-train-toy/src/lib/components/CuteLogo.svelte b/examples/piston-train-toy/src/lib/components/CuteLogo.svelte new file mode 100644 index 00000000..ec2a4261 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/CuteLogo.svelte @@ -0,0 +1,58 @@ + + + + + + + + + + + diff --git a/examples/piston-train-toy/src/lib/components/KatexBlock.svelte b/examples/piston-train-toy/src/lib/components/KatexBlock.svelte new file mode 100644 index 00000000..efdc1c51 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/KatexBlock.svelte @@ -0,0 +1,43 @@ + + +{#if processed.isHtml} + + {@html processed.output} +{:else} + {processed.output} +{/if} diff --git a/examples/piston-train-toy/src/lib/components/ToggleChips.svelte b/examples/piston-train-toy/src/lib/components/ToggleChips.svelte new file mode 100644 index 00000000..1d52745b --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/ToggleChips.svelte @@ -0,0 +1,41 @@ + + +
+ {#each items as name (name)} + + {/each} +
diff --git a/examples/piston-train-toy/src/lib/components/UserGuideTooltip.svelte b/examples/piston-train-toy/src/lib/components/UserGuideTooltip.svelte new file mode 100644 index 00000000..ae619b3a --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/UserGuideTooltip.svelte @@ -0,0 +1,36 @@ + + +
+ + +
diff --git a/examples/piston-train-toy/src/lib/components/Visualize.svelte b/examples/piston-train-toy/src/lib/components/Visualize.svelte new file mode 100644 index 00000000..46fe4a96 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/Visualize.svelte @@ -0,0 +1,1025 @@ + + +{#snippet boxLabel( + box: MatchBox & { fullyQualifiedPath: string }, + type: 'activation' | 'parameter' | 'gradient', + isHighlighted: boolean, + isSelected: boolean +)} +
+
+
+ + + +
+ + + ({#if type === 'gradient'} + gradient + {:else if type === 'activation'} + activation + {:else} + parameter + {/if}): + + + +
+
+{/snippet} + +
+
+
+
+ {#if trainingState.current !== 'stopped'} + + {/if} + + {#if !isMinimized} + + {/if} + {#if isMinimized} +
expandToFull()} + tabindex="0" + role="button" + aria-label="Edit script" + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + expandToFull(); + } + }} + > + + Click script to edit +
+ {/if} +
+
+ {#each exampleOptions as opt (opt.value)} + {@const selected = opt.value === config.visualization.example} + + {/each} +
+
+
{ + if (isMinimized) expandToFull(); + }} + role="button" + tabindex="0" + aria-label="Expand editor" + onkeydown={(e) => { + if (!isMinimized) return; + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + expandToFull(); + } + }} + >
+
+
+
updateOverflowShadow()} + style:height={`${canvasContainerHeight}px`} + onclick={() => { + selectedQueryIndex = null; + selectedBoxIndex = null; + }} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + selectedQueryIndex = null; + selectedBoxIndex = null; + } + }} + role="button" + tabindex="0" + aria-label="Clear selection" + > + {#key workerVersion} + + {#if boxes} + {#each boxes as box, i (i)} + {@const isHighlighted = + !scriptChangedSinceApply && + (hoveredQueryIndex === (box.match.queryIndex ?? -1) || + selectedQueryIndex === (box.match.queryIndex ?? -1))} + {@const isSelected = selectedBoxIndex === i} +
{ + if (!scriptChangedSinceApply) + setHoveredQueryIndexDebounced(box.match.queryIndex ?? null); + }} + onmouseleave={() => { + if (!scriptChangedSinceApply) setHoveredQueryIndexDebounced(null); + }} + onclick={(e) => { + e.stopPropagation(); + moveEditorToQueryStart(box.match.queryIndex ?? null); + selectedBoxIndex = i; + }} + role="button" + tabindex="0" + onkeydown={(e) => { + e.stopPropagation(); + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + moveEditorToQueryStart(box.match.queryIndex ?? null); + selectedBoxIndex = i; + } + }} + > +
+ {@render boxLabel( + box, + box.match.source.gradient + ? 'gradient' + : box.match.type === 'parameter' + ? 'parameter' + : 'activation', + isHighlighted, + isSelected + )} +
+ +
+
+
+ {/each} + {/if} + {/key} +
+
+
+
+ +
+ +
+
+
+
+ + diff --git a/examples/piston-train-toy/src/lib/components/controls/ActivationPicker.svelte b/examples/piston-train-toy/src/lib/components/controls/ActivationPicker.svelte new file mode 100644 index 00000000..3fdf518b --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/ActivationPicker.svelte @@ -0,0 +1,176 @@ + + +
+ +
+ +
+
+ + + + + + + + +
+ {#if onReset} + + {/if} +
+
+
diff --git a/examples/piston-train-toy/src/lib/components/controls/BorderedGroup.svelte b/examples/piston-train-toy/src/lib/components/controls/BorderedGroup.svelte new file mode 100644 index 00000000..78ff9098 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/BorderedGroup.svelte @@ -0,0 +1,61 @@ + + +
+
+ {#if title} +
+

+ +

+ {#if citations} + + {/if} +
+ {:else} + {@render header?.()} + {/if} + {#if action} + {@render action?.()} + {/if} +
+ {#if contentClass} +
+ {@render children?.()} +
+ {:else} + {@render children?.()} + {/if} +
diff --git a/examples/piston-train-toy/src/lib/components/controls/Citations.svelte b/examples/piston-train-toy/src/lib/components/controls/Citations.svelte new file mode 100644 index 00000000..3787ed24 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/Citations.svelte @@ -0,0 +1,28 @@ + + + + {#each citations?.entries ?? [] as e, i (e.name)} + {#if i > 0}, + {/if} + {#if e.url} + + {e.name} + {:else} + {e.name} + {/if} + {/each}{#if citations?.extra}{citations.extra}{/if} + diff --git a/examples/piston-train-toy/src/lib/components/controls/CollapsibleSection.svelte b/examples/piston-train-toy/src/lib/components/controls/CollapsibleSection.svelte new file mode 100644 index 00000000..b97dff66 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/CollapsibleSection.svelte @@ -0,0 +1,59 @@ + + +
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleClick(); + } + }} + > +

{title}

+ +
+ + {#if isOpen} +
+ {@render children?.()} +
+ {/if} +
diff --git a/examples/piston-train-toy/src/lib/components/controls/Controls.svelte b/examples/piston-train-toy/src/lib/components/controls/Controls.svelte new file mode 100644 index 00000000..6c21310f --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/Controls.svelte @@ -0,0 +1,1790 @@ + + +{#snippet parameterComparisonFunFact()} + {parameterComparison} +{/snippet} + +{#snippet projectionBlock(configName: 'attention' | 'mlp' | 'lmHead', displayName: string)} + + resetConfigToDefaults(`model.transformer.initialization.projections.${configName}.present`)} + > + + resetConfigToDefaults( + `model.transformer.initialization.projections.${configName}.strategy` + )} + /> + +{/snippet} + +{#snippet layerNormalization()} + {#if config.model.family === 'rnn'} + resetConfigToDefaults('model.layerNormalization.rnn.withinCell')} + /> + resetConfigToDefaults('model.layerNormalization.rnn.betweenLayers')} + /> + {/if} + {#if config.model.family === 'transformer' || config.model.layerNormalization.rnn.withinCell || config.model.layerNormalization.rnn.betweenLayers} + resetConfigToDefaults('model.layerNormalization.type')} + /> + {#if config.model.family === 'transformer'} + resetConfigToDefaults('model.layerNormalization.transformer.position')} + /> + {/if} + resetConfigToDefaults('model.layerNormalization.eps')} + /> + {/if} +{/snippet} + +
+
+ +
+ + {#if runsMap.size >= 1} + toggleControlSection('runs')} + contentClass="w-full" + > + + + {/if} + + toggleControlSection('task')} + contentClass={collapsibleSectionClass} + > + + + + toggleControlSection('model')} + contentClass={collapsibleSectionClass} + > + resetConfigToDefaults('model.family')} + /> + + +
+ + {#if getParameterCount() !== null} + {getParameterCount()?.toLocaleString()} + {:else} + ...,... + {/if} + + + {getHiddenSize()} + + + {currentDataset.vocabSize} + + {#if config.model.topology === 'encoder-decoder'} + {@const blockSize = currentDataset.blockSize as { source: number; target: number }} + + {blockSize.source} + + + {blockSize.target} + + {:else} + + {currentDataset.blockSize} + + {/if} +
+ + resetConfigToDefaults('model.topology')} + /> + + {#if config.model.family === 'rnn'} + resetConfigToDefaults('model.rnn.cellType')} + /> + {/if} + + {#if config.model.topology !== 'encoder-decoder'} + resetConfigToDefaults('model.layers')} + /> + {:else} + resetConfigToDefaults('model.encoderDecoder.encoderLayers')} + /> + resetConfigToDefaults('model.encoderDecoder.decoderLayers')} + /> + {/if} + + {#if config.model.family === 'transformer'} + resetConfigToDefaults('model.transformer.headDim')} + /> + {/if} + + {#if config.model.family !== 'rnn' || config.model.rnn.embedding.type !== 'one-hot'} + resetConfigToDefaults('model.tieEmbeddingsAndLmHead')} + /> + {/if} + + {#if config.model.family === 'rnn'} + {#if (config.model.family === 'rnn' && config.model.topology === 'encoder') || config.model.topology === 'encoder-decoder'} + resetConfigToDefaults('model.rnn.encoder.bidirectional')} + /> + {/if} + + resetConfigToDefaults('model.rnn.embedding.type')} + /> + {#if config.model.rnn.embedding.type === 'learned'} + resetConfigToDefaults('model.rnn.embedding.learned.size')} + /> + {/if} + + resetConfigToDefaults('model.rnn.separateHiddenSize.present')} + > + resetConfigToDefaults('model.rnn.separateHiddenSize.value')} + /> + + resetConfigToDefaults('model.rnn.hiddenStateProjection.present')} + > + resetConfigToDefaults('model.rnn.hiddenStateProjection.size')} + /> + + {/if} + + resetConfigToDefaults('model.roundVocabSizeToNearestMultiple.present')} + > + resetConfigToDefaults('model.roundVocabSizeToNearestMultiple.value')} + /> + + + {#if config.model.family === 'transformer'} + resetConfigToDefaults('model.transformer.attention.present')} + > + resetConfigToDefaults('model.transformer.attention.nKeyValueHeads')} + /> + resetConfigToDefaults('model.transformer.attention.sinks.present')} + /> + + resetConfigToDefaults('model.transformer.attention.groupedQueryAttention.present')} + > + + resetConfigToDefaults( + 'model.transformer.attention.groupedQueryAttention.queryHeadsPerKeyValueHead' + )} + /> + + + resetConfigToDefaults('model.transformer.attention.gating.present')} + > + resetConfigToDefaults('model.transformer.attention.gating.activation')} + /> + + resetConfigToDefaults('model.transformer.attention.gating.sites.afterSdpaOutput')} + /> + + resetConfigToDefaults( + 'model.transformer.attention.gating.sites.afterValueProjection' + )} + /> + + resetConfigToDefaults('model.transformer.attention.gating.sites.afterKeyProjection')} + /> + + resetConfigToDefaults( + 'model.transformer.attention.gating.sites.afterQueryProjection' + )} + /> + + resetConfigToDefaults( + 'model.transformer.attention.gating.sites.afterFinalOutputProjection' + )} + /> + + + + resetConfigToDefaults('model.transformer.mlp.present')} + > + resetConfigToDefaults('model.transformer.mlp.variant')} + /> + + {getMlpIntermediateSize()} + + resetConfigToDefaults('model.transformer.mlp.activation')} + /> + resetConfigToDefaults('model.transformer.mlp.hiddenExpansionFactor')} + /> + + + resetConfigToDefaults('model.transformer.positionalEncoding.present')} + > + resetConfigToDefaults('model.transformer.positionalEncoding.type')} + /> + {#if config.model.transformer.positionalEncoding.type === 'rope'} + resetConfigToDefaults('model.transformer.positionalEncoding.rope.base')} + /> + {/if} + {#if config.model.transformer.positionalEncoding.type === 'alibi'} + + resetConfigToDefaults('model.transformer.positionalEncoding.alibi.maxBias')} + /> + {/if} + + {/if} + + {#if config.model.family === 'rnn'} + {#if config.model.topology === 'encoder-decoder'} + resetConfigToDefaults('model.rnn.encoderDecoderAttention.present')} + > + resetConfigToDefaults('model.rnn.encoderDecoderAttention.type')} + /> + {#if config.model.rnn.encoderDecoderAttention.type === 'multiplicative'} + + resetConfigToDefaults( + 'model.rnn.encoderDecoderAttention.multiplicative.scaleByInverseSqrtHiddenSize' + )} + /> + {/if} + + resetConfigToDefaults('model.rnn.encoderDecoderAttention.inputFeedingProjection')} + /> + + {/if} + {/if} + + {#if config.model.family === 'transformer'} + + resetConfigToDefaults('model.layerNormalization.transformer.present')} + > + {@render layerNormalization()} + + resetConfigToDefaults('model.transformer.normalization.qkNorm.present')} + > + resetConfigToDefaults('model.transformer.normalization.qkNorm.type')} + /> + resetConfigToDefaults('model.transformer.normalization.qkNorm.eps')} + /> + + + + + resetConfigToDefaults('model.transformer.normalization.softcap.attention.present')} + > + + resetConfigToDefaults('model.transformer.normalization.softcap.attention.value')} + /> + + + resetConfigToDefaults('model.transformer.normalization.softcap.logits.present')} + > + + resetConfigToDefaults('model.transformer.normalization.softcap.logits.value')} + /> + + + + {:else} + + {@render layerNormalization()} + + {/if} + + resetConfigToDefaults(`model.${config.model.family}.initialization.present`)} + > + {#if config.model.family === 'transformer'} + resetConfigToDefaults('model.transformer.initialization.std')} + /> + + {@render projectionBlock('attention', 'Attention')} + {@render projectionBlock('mlp', 'MLP')} + {@render projectionBlock('lmHead', 'LM Head')} + + {:else if config.model.family === 'rnn'} + + resetConfigToDefaults(`model.rnn.initialization.orthogonalRecurrentColumns`)} + /> + resetConfigToDefaults(`model.rnn.initialization.perGateOrthogonalBlocks`)} + /> + resetConfigToDefaults(`model.rnn.initialization.zeroBiases`)} + /> + + resetConfigToDefaults(`model.rnn.initialization.xavierInputColumns.present`)} + > + + resetConfigToDefaults(`model.rnn.initialization.xavierInputColumns.distribution`)} + /> + + {#if config.model.layerNormalization.type === 'layernorm'} + {#if config.model.rnn.cellType === 'gru'} + + resetConfigToDefaults(`model.rnn.initialization.gru.updateGateBias.present`)} + > + + resetConfigToDefaults(`model.rnn.initialization.gru.updateGateBias.value`)} + /> + + {:else if config.model.rnn.cellType === 'lstm'} + + resetConfigToDefaults(`model.rnn.initialization.lstm.forgetGateBias.present`)} + > + + resetConfigToDefaults(`model.rnn.initialization.lstm.forgetGateBias.value`)} + /> + + {/if} + {/if} + {/if} + +
+ + toggleControlSection('training')} + > + resetConfigToDefaults('training.logSteps')} + /> + + resetConfigToDefaults('training.batchSize')} + /> + + resetConfigToDefaults('training.enableVisualization')} + /> + + resetConfigToDefaults('training.gradNorm.track')} + > + resetConfigToDefaults('training.gradNorm.errorIfNonfinite')} + /> + + resetConfigToDefaults('training.clipGradNorm.present')} + > + resetConfigToDefaults('training.clipGradNorm.value')} + /> + + + + resetConfigToDefaults('training.validation.present')} + > + resetConfigToDefaults('training.validation.valSteps')} + /> + resetConfigToDefaults('training.validation.batchSize')} + /> + resetConfigToDefaults('training.validation.completions.present')} + > + resetConfigToDefaults('training.validation.completions.amount')} + /> + {#if config.training.validation.completions.amount === 'subset'} + resetConfigToDefaults('training.validation.completions.subsetSize')} + /> + {/if} + + resetConfigToDefaults('training.validation.temperature')} + /> + + + + + resetConfigToDefaults('training.limitTraining.present')} + > + resetConfigToDefaults('training.limitTraining.steps')} + /> + + + + resetConfigToDefaults('training.labelSmoothing.present')} + > + resetConfigToDefaults('training.labelSmoothing.value')} + /> + + + {#if config.model.family === 'transformer'} + resetConfigToDefaults('training.dropout.present')} + > + resetConfigToDefaults('training.dropout.embedding')} + /> + resetConfigToDefaults('training.dropout.transformer.attention')} + /> + resetConfigToDefaults('training.dropout.transformer.residual')} + /> + + {/if} + + + resetConfigToDefaults('training.randomSeed.present')} + > + resetConfigToDefaults('training.randomSeed.value')} + /> + + + resetConfigToDefaults('training.vramLimitMb.present')} + > + { + if (value >= 1024) { + const gb = value / 1024; + const gbStr = gb % 1 === 0 ? `${gb}GB` : `${gb.toFixed(1)}GB`; + return gbStr; + } + return `${value}MB`; + }} + hasDefaultValue={equalsConfigDefault('training.vramLimitMb.value')} + onReset={() => resetConfigToDefaults('training.vramLimitMb.value')} + /> + + + + toggleControlSection('optimizer')} + contentClass={collapsibleSectionClass} + > + resetConfigToDefaults('optimizer.type')} + /> + + resetConfigToDefaults('optimizer.lr')} + /> + + resetConfigToDefaults('optimizer.warmupSteps.present')} + > + resetConfigToDefaults('optimizer.warmupSteps.value')} + /> + + + resetConfigToDefaults('optimizer.lrScheduler.present')} + > + + + + resetConfigToDefaults('optimizer.weightDecay.present')} + > + resetConfigToDefaults('optimizer.weightDecay.value')} + /> + + resetConfigToDefaults('optimizer.weightDecay.useWeightDecayGroups')} + /> + + +
+ {#if config.optimizer.type === 'Muon'} + +
+ resetConfigToDefaults('optimizer.muon.nsSteps')} + /> + resetConfigToDefaults('optimizer.muon.momentum')} + /> + resetConfigToDefaults('optimizer.muon.nesterov')} + /> +
+
+ {/if} + {#if config.optimizer.type === 'AdamW' || config.optimizer.type === 'Adam' || config.optimizer.type === 'Muon'} + {@const settingsName = config.optimizer.type === 'Adam' ? 'Adam' : 'AdamW'} + +
+ resetConfigToDefaults('optimizer.adam.beta1')} + /> + resetConfigToDefaults('optimizer.adam.beta2')} + /> + resetConfigToDefaults('optimizer.adam.eps')} + /> + resetConfigToDefaults('optimizer.adam.amsgrad')} + /> +
+
+ {/if} + + {#if config.optimizer.type === 'SGD'} +
+ resetConfigToDefaults('optimizer.sgd.momentum')} + /> + resetConfigToDefaults('optimizer.sgd.dampening')} + /> + resetConfigToDefaults('optimizer.sgd.nesterov')} + /> +
+ {/if} +
+
+ + toggleControlSection('advanced')} + contentClass={collapsibleSectionClass} + > + resetConfigToDefaults('training.useWeakTensorReferences')} + /> + resetConfigToDefaults('training.sharedObjectAllocation')} + /> + resetConfigToDefaults('training.inplaceSupport')} + /> + +
+ + diff --git a/examples/piston-train-toy/src/lib/components/controls/ControlsNote.svelte b/examples/piston-train-toy/src/lib/components/controls/ControlsNote.svelte new file mode 100644 index 00000000..f2d23fb0 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/ControlsNote.svelte @@ -0,0 +1,49 @@ + + +
+ + {#if type === 'info'} + + {:else if type === 'warning'} + + {:else if type === 'error'} + + {/if} + + + + {@render children?.()} + +
diff --git a/examples/piston-train-toy/src/lib/components/controls/ControlsStatistic.svelte b/examples/piston-train-toy/src/lib/components/controls/ControlsStatistic.svelte new file mode 100644 index 00000000..e5c6ad44 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/ControlsStatistic.svelte @@ -0,0 +1,32 @@ + + + +
+ + + + {@render children?.()} + + {#if funFact} + + {@render funFact?.()} + + {/if} + +
diff --git a/examples/piston-train-toy/src/lib/components/controls/DatasetControls.svelte b/examples/piston-train-toy/src/lib/components/controls/DatasetControls.svelte new file mode 100644 index 00000000..70def347 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/DatasetControls.svelte @@ -0,0 +1,163 @@ + + + resetConfigToDefaults('data.dataset')} +/> + +

{datasetConfigMetadata?.description}

+ + + +{#if getShowLowDiversityDatasetError()} +
+ +

+ Not enough example diversity in the training dataset for a held-out validation set of size {config + .training.validation.batchSize}. Consider changing dataset parameters or reducing the + validation batch size. +

+
+
+{/if} + +
+ {#key datasetName} + {#if parameters && datasetConfig} +
+ {#each Object.entries(parameters) as [paramKey, paramMeta] (paramKey)} + {@const value = datasetConfig[paramKey as keyof typeof datasetConfig]} + {#if isSliderParameter(paramMeta)} + {@const sliderProps = getSliderProps(paramMeta)} + {#if sliderProps} + { + datasetConfig[paramKey as keyof typeof datasetConfig] = + paramMeta.default as never; + }} + /> + {/if} + {:else if isCheckboxParameter(paramMeta)} + { + datasetConfig[paramKey as keyof typeof datasetConfig] = paramMeta.default as never; + }} + /> + {/if} + {/each} +
+ {/if} + {/key} + {#if isNatural} + resetConfigToDefaults('data.natural.contextSize')} + /> + String(config.data.natural.vocabSize), + (v) => + (config.data.natural.vocabSize = + v === 'char' ? 'char' : (parseInt(v) as typeof config.data.natural.vocabSize)) + } + options={[ + { value: 'char', label: 'Character-level' }, + { value: '512', label: '512' }, + { value: '1024', label: '1024' }, + { value: '2048', label: '2048' }, + { value: '4096', label: '4096' }, + { value: '8192', label: '8192' }, + { value: '16384', label: '16384' }, + { value: '32768', label: '32768' }, + { value: '65536', label: '65536' } + ]} + hasDefaultValue={equalsConfigDefault('data.natural.vocabSize')} + onReset={() => resetConfigToDefaults('data.natural.vocabSize')} + /> + {/if} + {#if showDivider} +
+ {/if} + {#if showMaskRatio} + resetConfigToDefaults('data.maskRatio')} + /> + {/if} +
diff --git a/examples/piston-train-toy/src/lib/components/controls/DatasetSample.svelte b/examples/piston-train-toy/src/lib/components/controls/DatasetSample.svelte new file mode 100644 index 00000000..2a2e3c2c --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/DatasetSample.svelte @@ -0,0 +1,156 @@ + + +{#snippet token(value: number, dashed: boolean = false)} + + {#if tokenizer instanceof Promise} + {#await tokenizer then tokenizer} + {decodeSingle(value, tokenizer)} + {/await} + {:else} + {decodeSingle(value, tokenizer)} + {/if} + +{/snippet} + +{#snippet tokenSequence(values: number[], ignored?: boolean[])} +
+ {#each values as value, i (i)} + {@render token(value, ignored ? ignored[i] : false)} + {/each} +
+{/snippet} + +
0 ? `${lastStableHeight}px` : 'auto'} + style:overflow-y={anyPending ? 'hidden' : 'visible'} +> + {#await sampleData then { collated, hasPrompt }} + {#if collated.length > 0} +
+ + + + {#if hasPrompt} + + {/if} + + + + + {#each collated as { prompt, target, fullSequence } (Array.prototype.concat.call(fullSequence, prompt ?? [], target ?? []))} + {@const pLen = prompt?.length || 0} + {@const targetFlags = maskedFlagsForRange(fullSequence, pLen, target?.length ?? 0)} + + {#if hasPrompt} + {@const promptFlags = maskedFlagsForRange(fullSequence, 0, pLen)} + + {/if} + + + {/each} + + {#if hasPrompt} + + {/if} + + + +
PromptTarget
+ {@render tokenSequence(prompt!, promptFlags)} + + {@render tokenSequence(target ?? fullSequence, targetFlags)} +
......
+
+ {/if} + {/await} +
diff --git a/examples/piston-train-toy/src/lib/components/controls/FormLabel.svelte b/examples/piston-train-toy/src/lib/components/controls/FormLabel.svelte new file mode 100644 index 00000000..baf166ac --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/FormLabel.svelte @@ -0,0 +1,17 @@ + + + diff --git a/examples/piston-train-toy/src/lib/components/controls/LRSchedulePicker.svelte b/examples/piston-train-toy/src/lib/components/controls/LRSchedulePicker.svelte new file mode 100644 index 00000000..191ee3b3 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/LRSchedulePicker.svelte @@ -0,0 +1,503 @@ + + +
+ + + + + resetConfigToDefaults('optimizer.lrScheduler.type')} + /> + + +
+ + + + + + + {#if points.length > 0} + {@const lrs = points.map(([, y]) => y)} + {@const maxLr = Math.max(...lrs)} + {@const minLr = Math.min(...lrs)} + {#if maxLr === minLr} + + {maxLrLabel} + {:else} + + {maxLrLabel} + {minLrLabel} + {/if} + {/if} + + + steps + {#if points.length > 0} + {Math.max(...points.map(([x]) => x))} + {/if} + + + {#if svgPoints} + + {/if} + + + +
+ +
+
+ + +
+ + (stepsToShow = 1000)} + /> + + + {#if config.optimizer.lrScheduler.type === 'linear'} + resetConfigToDefaults('optimizer.lrScheduler.linearSchedule.startFactor')} + /> + resetConfigToDefaults('optimizer.lrScheduler.linearSchedule.endFactor')} + /> + resetConfigToDefaults('optimizer.lrScheduler.linearSchedule.totalIters')} + /> + {/if} + + + {#if config.optimizer.lrScheduler.type === 'constant'} + resetConfigToDefaults('optimizer.lrScheduler.constantSchedule.factor')} + /> + resetConfigToDefaults('optimizer.lrScheduler.constantSchedule.totalIters')} + /> + {/if} + + + {#if config.optimizer.lrScheduler.type === 'cosine'} + resetConfigToDefaults('optimizer.lrScheduler.cosineAnnealingSchedule.tMax')} + /> + + resetConfigToDefaults('optimizer.lrScheduler.cosineAnnealingSchedule.etaMin')} + /> + {/if} + + + {#if config.optimizer.lrScheduler.type === 'step'} + resetConfigToDefaults('optimizer.lrScheduler.stepSchedule.stepSize')} + /> + resetConfigToDefaults('optimizer.lrScheduler.stepSchedule.gamma')} + /> + {/if} + + + {#if config.optimizer.lrScheduler.type === 'exponential'} + resetConfigToDefaults('optimizer.lrScheduler.exponentialSchedule.gamma')} + /> + {/if} +
+
diff --git a/examples/piston-train-toy/src/lib/components/controls/NumberInput.svelte b/examples/piston-train-toy/src/lib/components/controls/NumberInput.svelte new file mode 100644 index 00000000..cf902162 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/NumberInput.svelte @@ -0,0 +1,71 @@ + + +
+ {#if label} + + {/if} +
+ {#if unit} +
+ + + {unit} + +
+ {:else} + + {/if} + {#if onReset} +
+ +
+ {/if} +
+
diff --git a/examples/piston-train-toy/src/lib/components/controls/ResetValueButton.svelte b/examples/piston-train-toy/src/lib/components/controls/ResetValueButton.svelte new file mode 100644 index 00000000..19e33fd4 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/ResetValueButton.svelte @@ -0,0 +1,22 @@ + + + diff --git a/examples/piston-train-toy/src/lib/components/controls/RunsTable.svelte b/examples/piston-train-toy/src/lib/components/controls/RunsTable.svelte new file mode 100644 index 00000000..2f6c23b2 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/RunsTable.svelte @@ -0,0 +1,55 @@ + + +
+ {#if runs.length > 1} + + {/if} + +
+
+
+ + + + + + + + + + + + {#each runs as run (run.runId)} + + + + + {/each} + +
{frozenColumn.label}Changes
{run.runId}{run.diffSummary ?? 'initial experiment'}
+
+
+
+
diff --git a/examples/piston-train-toy/src/lib/components/controls/SelectDataset.svelte b/examples/piston-train-toy/src/lib/components/controls/SelectDataset.svelte new file mode 100644 index 00000000..85dd1679 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/SelectDataset.svelte @@ -0,0 +1,146 @@ + + +{#snippet nameAndBadges(_opt: DatasetOption)} + {@const opt = _opt as DatasetOption} + {@const meta = getMeta(opt.value as DatasetName)} + {@const name = opt.text ?? meta.name} + {@const citations = + 'citations' in meta + ? ((meta as Record).citations as CitationsType) + : undefined} + {@const supports = ( + 'supportsModelTypes' in meta ? meta.supportsModelTypes : MODEL_TYPES + ).toSorted()} + {@const currentModel = config.model.topology as ModelType} + {@const isSupported = supports.includes(currentModel)} + {@const autoModel = isSupported ? null : (supports[0] as ModelType)} +
+ + {name} + {#if autoModel} + + (will switch to + {MODEL_DISPLAY_NAMES[autoModel as keyof typeof MODEL_DISPLAY_NAMES]}) + + {/if} + + {#if citations} + + {/if} +
+ {#each supports as n (n)} + {MODEL_DISPLAY_NAMES[n as keyof typeof MODEL_DISPLAY_NAMES]} + {/each} +
+
+{/snippet} + +
+ + {#snippet option(_opt, _selected)} + {@const opt = _opt as DatasetOption} + {@render nameAndBadges(opt)} + {/snippet} + {#snippet trigger(_selected)} + {#if _selected} + {@const opt = _selected as DatasetOption} + {opt.text ?? opt.value} + {:else} + Select... + {/if} + {/snippet} + + {#if 'citations' in selectedMeta} +
+ +
+ {/if} +
diff --git a/examples/piston-train-toy/src/lib/components/controls/Slider.svelte b/examples/piston-train-toy/src/lib/components/controls/Slider.svelte new file mode 100644 index 00000000..500670b8 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/Slider.svelte @@ -0,0 +1,745 @@ + + +
+ {#if label} + + {/if} +
+
+
+
+ +
+ + +
+ + + {#if showTicks && ticks.length > 0} +
+ + {#each ticks as tick (tick.key)} +
+ {/each} +
+ {/if} + + +
{ + let stepAmount = step > 0 ? step : useLog ? displayValue * 0.05 : (max - min) / 100; + if (stepAmount === 0 && max > min) stepAmount = (max - min) / 100; + if (stepAmount === 0) stepAmount = EPSILON; + + if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { + scheduleCommit(clampToRange(displayValue - stepAmount)); + } else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { + scheduleCommit(clampToRange(displayValue + stepAmount)); + } else if (e.key === 'Home') { + scheduleCommit(min); + } else if (e.key === 'End') { + scheduleCommit(max); + } + }} + >
+
+
+
+ {tickFormatter + ? tickFormatter(min) + : useLog + ? formatExponential(min, EXPONENTIAL_PRECISION) + : min.toLocaleString()} +
+
+ {tickFormatter + ? tickFormatter(max) + : useLog + ? formatExponential(max, EXPONENTIAL_PRECISION) + : max.toLocaleString()} +
+
+
+
+ + +
+
+ (isInputFocused = true)} + onblur={handleInputBlur} + onkeydown={handleInputKeyDown} + style:width={inputWidth} + class="bg-transparent text-controls-numeric text-right font-mono border-panel-border-base focus:border-neutral-500 border focus:outline-none h-full px-0.5" + {min} + {max} + /> + + {#if unit} + + {unit} + + {/if} +
+ + {#if onReset} + + {/if} +
+
+
+ + diff --git a/examples/piston-train-toy/src/lib/components/controls/TextInput.svelte b/examples/piston-train-toy/src/lib/components/controls/TextInput.svelte new file mode 100644 index 00000000..97d42747 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/TextInput.svelte @@ -0,0 +1,45 @@ + + +
+ {#if label} + + {/if} +
+ + {#if onReset} + + {/if} +
+
diff --git a/examples/piston-train-toy/src/lib/components/controls/ToggleGroup.svelte b/examples/piston-train-toy/src/lib/components/controls/ToggleGroup.svelte new file mode 100644 index 00000000..80e2a46a --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/ToggleGroup.svelte @@ -0,0 +1,56 @@ + + + + {#snippet action()} + {#if showEnableToggle} + + {/if} + {/snippet} + {#if !showEnableToggle || (showEnableToggle && enabled)} +
+ {@render children?.()} +
+ {/if} +
diff --git a/examples/piston-train-toy/src/lib/components/controls/checkbox/CheckboxIcon.svelte b/examples/piston-train-toy/src/lib/components/controls/checkbox/CheckboxIcon.svelte new file mode 100644 index 00000000..1a3a5293 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/checkbox/CheckboxIcon.svelte @@ -0,0 +1,21 @@ + + + + + + + diff --git a/examples/piston-train-toy/src/lib/components/controls/checkbox/CheckboxInput.svelte b/examples/piston-train-toy/src/lib/components/controls/checkbox/CheckboxInput.svelte new file mode 100644 index 00000000..f85205b8 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/checkbox/CheckboxInput.svelte @@ -0,0 +1,55 @@ + + + diff --git a/examples/piston-train-toy/src/lib/components/controls/radio/RadioGroupInput.svelte b/examples/piston-train-toy/src/lib/components/controls/radio/RadioGroupInput.svelte new file mode 100644 index 00000000..e06c1810 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/radio/RadioGroupInput.svelte @@ -0,0 +1,64 @@ + + +
+ {#if label} + + {/if} +
+
+ {#each options as opt (String(opt.value))} +
+ +
+ {/each} +
+ {#if onReset} +
+ +
+ {/if} +
+
diff --git a/examples/piston-train-toy/src/lib/components/controls/radio/RadioIcon.svelte b/examples/piston-train-toy/src/lib/components/controls/radio/RadioIcon.svelte new file mode 100644 index 00000000..2447c975 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/radio/RadioIcon.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/examples/piston-train-toy/src/lib/components/controls/radio/RadioInput.svelte b/examples/piston-train-toy/src/lib/components/controls/radio/RadioInput.svelte new file mode 100644 index 00000000..1dca2d1d --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/radio/RadioInput.svelte @@ -0,0 +1,52 @@ + + + diff --git a/examples/piston-train-toy/src/lib/components/controls/select/SelectInput.svelte b/examples/piston-train-toy/src/lib/components/controls/select/SelectInput.svelte new file mode 100644 index 00000000..0ed160a7 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/select/SelectInput.svelte @@ -0,0 +1,367 @@ + + +
+ {#if label} + + {/if} +
+
+ +
+ +
+ + {#if isOpen} + +
    = 0 && + menuItems[highlightedIndex]?.kind === 'option' + ? `${id}-opt-${highlightedIndex}` + : undefined} + tabindex="-1" + onkeydown={onMenuKeydown} + class="fixed z-[9999] border border-neutral-300 bg-white max-h-120 overflow-auto text-base rounded-none shadow-lg" + style={menuStyle} + > + {#each menuItems as item, i (item.id)} + {#if item.kind === 'group'} +
  • 0} + class:border-b={i < menuItems.length - 1} + > + {item.label} +
  • + {:else} + {@const opt = item.option} + {@const isDisabled = isOptionDisabled(opt)} +
  • (!isDisabled ? (highlightedIndex = i) : undefined)} + > + +
  • + {/if} + {/each} +
+
+ {/if} +
+ {#if onReset} + + {/if} +
+
diff --git a/examples/piston-train-toy/src/lib/components/controls/select/SelectModelTopology.svelte b/examples/piston-train-toy/src/lib/components/controls/select/SelectModelTopology.svelte new file mode 100644 index 00000000..a6034e1b --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/select/SelectModelTopology.svelte @@ -0,0 +1,171 @@ + + +{#snippet optionView(_opt: unknown, _selected: boolean, _index: number)} + {@const opt = _opt as ModelOption} + {@const label = opt.title} + {@const citations = opt.citations} +
+
{label}
+ {#if citations} + + {/if} + {#if opt.disabled} + {@const datasetName = DATASET_CONFIG_METADATA[config.data.dataset].name} +
Not supported by {datasetName}
+ {/if} +
+{/snippet} + +
+ + {#snippet option(_opt, _selected, _i)} + {@render optionView(_opt, _selected, _i)} + {/snippet} + {#snippet trigger(_selected)} + {#if _selected} + {@const opt = _selected as ModelOption} + {opt.title} + {:else} + Select... + {/if} + {/snippet} + + {#if selectedOption?.citations} +
+ {/if} +
diff --git a/examples/piston-train-toy/src/lib/components/controls/select/SelectWithCitations.svelte b/examples/piston-train-toy/src/lib/components/controls/select/SelectWithCitations.svelte new file mode 100644 index 00000000..f246a442 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/controls/select/SelectWithCitations.svelte @@ -0,0 +1,66 @@ + + +{#snippet citationView(_opt: CitationOption)} + {@const label = _opt.title} + {@const citations = _opt.citations} +
+
{label}
+ {#if citations} + + {/if} +
+{/snippet} + +
+ + {#snippet option(_opt, _selected)} + {@const opt = _opt as CitationOption} + {@render citationView(opt)} + {/snippet} + {#snippet trigger(_selected)} + {#if _selected} + {@const opt = _selected as CitationOption} + {opt.title} + + + {:else} + Select... + {/if} + {/snippet} + + {#if selectedOption?.citations} +
+ {/if} +
diff --git a/examples/piston-train-toy/src/lib/components/footnotes/Footnote.svelte b/examples/piston-train-toy/src/lib/components/footnotes/Footnote.svelte new file mode 100644 index 00000000..1fb49c46 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/footnotes/Footnote.svelte @@ -0,0 +1,46 @@ + + + + + {computedLabel ?? (label != null ? String(label) : '?')} + diff --git a/examples/piston-train-toy/src/lib/components/footnotes/FootnotesProvider.svelte b/examples/piston-train-toy/src/lib/components/footnotes/FootnotesProvider.svelte new file mode 100644 index 00000000..fbb0eba6 --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/footnotes/FootnotesProvider.svelte @@ -0,0 +1,42 @@ + + +{@render children()} + +
+

Footnotes

+
    + {#each entries as e (e.key)} +
  1. + {e.label + '.'} +
    + {@render e.content()} + ↩︎ +
    +
  2. + {/each} +
+
diff --git a/examples/piston-train-toy/src/lib/components/metrics/MetricsSection.svelte b/examples/piston-train-toy/src/lib/components/metrics/MetricsSection.svelte new file mode 100644 index 00000000..ab15269b --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/metrics/MetricsSection.svelte @@ -0,0 +1,65 @@ + + +
+
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleClick(); + } + }} + > + +

{title}

+
+ {@render chips?.()} +
+ + {#if isOpen} +
+ {@render children?.()} +
+ {/if} +
diff --git a/examples/piston-train-toy/src/lib/components/metrics/RunChart.svelte b/examples/piston-train-toy/src/lib/components/metrics/RunChart.svelte new file mode 100644 index 00000000..8ef57a5e --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/metrics/RunChart.svelte @@ -0,0 +1,323 @@ + + +
+
chartOptions, + setup: (chart) => { + let isRelaying = false; + + const onUpdateAxisPointer = (evt: unknown) => { + if (isRelaying) return; + const s = extractStepFromAxisPointerEvent(evt); + if (s == null) return; + // Choose topmost series that contains this exact step + for (let idx = seriesIndexToRunId.length - 1; idx >= 0; idx--) { + const runId = seriesIndexToRunId[idx]; + if (!runId) continue; + const seriesInfo = runIdToSeries.get(runId); + if (!seriesInfo) continue; + if (exactIndex(seriesInfo.steps, s) !== -1) { + publishMove({ sourceId: chartId, runId, step: s }); + break; + } + } + }; + + const onGlobalOut = () => { + publishClear({ sourceId: chartId }); + }; + + chart.on('updateAxisPointer', onUpdateAxisPointer); + chart.on('globalout', onGlobalOut); + + const unsubscribe = subscribe( + ({ sourceId, runId, step }) => { + if (sourceId === chartId) return; + const info = runIdToSeries.get(runId); + if (!info) return; + const { seriesIndex, steps, first, last } = info; + if (step < first || step > last) return; + const dataIndex = nearestIndex(steps, step); + if (dataIndex < 0) return; + isRelaying = true; + try { + // ECharts accepts a second options argument with { silent: true } + chart.dispatchAction( + { type: 'updateAxisPointer', seriesIndex, dataIndex }, + { silent: true } + ); + } finally { + isRelaying = false; + } + }, + ({ sourceId }) => { + if (sourceId === chartId) return; + chart.dispatchAction({ type: 'hideTip' }, { silent: true }); + } + ); + + return () => { + unsubscribe(); + chart.off('updateAxisPointer', onUpdateAxisPointer); + chart.off('globalout', onGlobalOut); + }; + } + })} + >
+
diff --git a/examples/piston-train-toy/src/lib/components/metrics/validationCompletions/CompletionsToken.svelte b/examples/piston-train-toy/src/lib/components/metrics/validationCompletions/CompletionsToken.svelte new file mode 100644 index 00000000..8613d92a --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/metrics/validationCompletions/CompletionsToken.svelte @@ -0,0 +1,124 @@ + + + { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleClick(e); + } + }} +> + {#if targetText != null} + + + {#if actualText} + + {visualizeToken(actualText)} + + {/if} + + {visualizeToken(targetText)} + + + {:else} + + {/if} + diff --git a/examples/piston-train-toy/src/lib/components/metrics/validationCompletions/ValidationCompletionsViewer.svelte b/examples/piston-train-toy/src/lib/components/metrics/validationCompletions/ValidationCompletionsViewer.svelte new file mode 100644 index 00000000..785a021d --- /dev/null +++ b/examples/piston-train-toy/src/lib/components/metrics/validationCompletions/ValidationCompletionsViewer.svelte @@ -0,0 +1,901 @@ + + +
+
+ + validation/completions + {#if completionsData} + + of {completionsData.targetStep.completions.length} + {/if} + + {#if completionsData} +

+ Step {completionsData.stepNumber} • Temp: {completionsData.targetStep.samplingParams + .temperature} +

+ {/if} + {#if config.visualization.target === 'validation'} + Visualizing Selected Example + {/if} +
+ + {#if !completionsData} +
+

No validation data available. Validation will run during training.

+
+ {:else} + {@const visibleCompletionsNumberWidth = visibleCompletions.length.toString().length} + {@const focus = hoveredFocus ?? config.visualization.selectedValidation} +
+
+ {#await tokenizer then tokenizer} + {#each visibleCompletions as completion, index (index)} + {@const genIds = completion.tokenIds} + {@const hasTargets = Boolean(completionsData.targets)} + {@const tgtIds = hasTargets ? completionsData.targets?.[index] || [] : []} + {@const tokenComparisons = hasTargets ? compareTokenIds(genIds, tgtIds, tokenizer) : []} + {@const matchesRow = completionsData.targetStep.matches?.[index] || []} + {@const modelType = runConfig?.model.topology ?? 'decoder'} + {@const prefixLen = + completionsData.decoderPromptLengths?.[index] ?? + (modelType === 'encoder' ? 0 : modelType === 'encoder-decoder' ? 1 : 1)} + {@const showEncoderGrid = + runConfig?.model.topology === 'encoder-decoder' && completionsData.encoderInputs} + +
+ (hoveredFocus = { + exampleIndex: index, + tokenIndex: hoveredFocus?.tokenIndex ?? 0 + })} + onmouseleave={() => (hoveredFocus = null)} + onclick={() => + updateVisualizerSelectedValidation({ + exampleIndex: index, + tokenIndex: 0 + })} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + updateVisualizerSelectedValidation({ + exampleIndex: index, + tokenIndex: 0 + }); + } + }} + > + {#if showEncoderGrid} +
+
+ Encoder +
+
+ → +
+
+ Decoder +
+ {/if} +
+ {(index + 1).toString().padStart(visibleCompletionsNumberWidth, '\u00A0')} +
+ {#if showEncoderGrid} +
+ {#each completionsData.encoderInputs?.[index] || [] as encId, encIdx (encIdx)} + + {visualizeToken(decodeSingle(encId, tokenizer))} + + {/each} +
+
+ {/if} + + +
+ {#if hasTargets} + {#each tokenComparisons as item, tIndex (tIndex)} + {@const isPromptItem = item.kind === 'prompt'} + {@const completedBefore = tokenComparisons + .slice(0, tIndex) + .filter((it) => it.kind !== 'prompt').length} + {@const sequenceTokenIndex = isPromptItem + ? prefixLen + : prefixLen + Math.max(0, completedBefore)} + {@const isHighlighted = + !isPromptItem && + focus?.exampleIndex === index && + focus?.tokenIndex === sequenceTokenIndex} + {#if item.kind === 'prompt'} + {}} + onLeave={() => {}} + onSelect={() => {}} + /> + {:else if item.isCorrect} + (hoveredFocus = { exampleIndex: ei, tokenIndex: ti })} + onLeave={() => (hoveredFocus = null)} + onSelect={(ei, ti) => + updateVisualizerSelectedValidation({ + exampleIndex: ei, + tokenIndex: ti + })} + /> + {:else} + (hoveredFocus = { exampleIndex: ei, tokenIndex: ti })} + onLeave={() => (hoveredFocus = null)} + onSelect={(ei, ti) => + updateVisualizerSelectedValidation({ + exampleIndex: ei, + tokenIndex: ti + })} + /> + {/if} + {/each} + {:else if genIds.length === 0} + [empty] + {:else} + {#each genIds as id, tIndex (tIndex)} + {@const text = decodeSingle(id, tokenizer)} + {@const isPrompt = tIndex < prefixLen} + {@const match = matchesRow[tIndex - prefixLen]} + {@const variant = isPrompt + ? 'prompt' + : !hasMatchData || matchesRow.length === 0 + ? 'generated' + : match === true + ? 'correct' + : match === false + ? 'incorrect' + : 'neutral'} + {@const isHighlighted = + !isPrompt && focus?.exampleIndex === index && focus?.tokenIndex === tIndex} + (hoveredFocus = { exampleIndex: ei, tokenIndex: ti })} + onLeave={() => (hoveredFocus = null)} + onSelect={(ei, ti) => + updateVisualizerSelectedValidation({ + exampleIndex: ei, + tokenIndex: ti + })} + /> + {/each} + {/if} +
+
+ {/each} + {/await} +
+ + + {#if chartOptions} + +
+
+ {#if selectedProbsStep} +
+ (selectedProbsStep = null)} + /> +
+ {/if} +
{ + if (!chartOptions) return null; + const baseBar = chartOptions.bar; + const tbc = chartOptions.tokensByCol ?? []; + const last = Math.max(0, tbc.length - 1); + const col = + activeStepCol == null + ? last + : Math.max(0, Math.min(last, Math.floor(activeStepCol))); + const defaultCategories = chartOptions.tokenCategoriesByCol[col]; + const defaultData = tbc[col]; + const categories = selectedProbsStep + ? selectedProbsStep.categories + : defaultCategories; + const data = selectedProbsStep ? selectedProbsStep.data : defaultData; + return { + ...baseBar, + title: selectedProbsStep + ? { ...baseBar.title, text: `probs (step ${selectedProbsStep.step})` } + : baseBar?.title, + xAxis: [ + { + type: 'category', + data: categories, + axisPointer: { show: true, type: 'shadow' }, + triggerEvent: true, + axisTick: { show: false }, + axisLabel: { show: false }, + axisLine: { show: false } + } + ], + series: [{ id: 'token-bars', type: 'bar', data }] + }; + } + })} + >
+
+
+
+ {/if} +
+ {/if} +
diff --git a/examples/piston-train-toy/src/lib/train/capture.ts b/examples/piston-train-toy/src/lib/train/capture.ts new file mode 100644 index 00000000..d691a25f --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/capture.ts @@ -0,0 +1,211 @@ +import type { BaseStepData } from '$lib/workspace/runs.svelte'; + +import { + CapturePlan, + type CaptureResult, + CaptureSession, + Module, + pin, + Tensor, + type TensorQuery +} from '@piston-ml/piston-web'; + +import type { + BidirectionalBatchType, + EncoderDecoderBatchType, + GeneratableModel, + NaturalCollateFnType, + PistonCollateFnType, + ToyCollateFnType +} from './types'; +import type { ValidationExamples } from './validation'; + +import { RNNDecoder, RNNEncoder, RNNEncoderDecoder } from './model/rnn'; +import { + DecoderTransformer, + EncoderDecoderTransformer, + EncoderTransformer +} from './model/transformer'; + +export interface CaptureMatch { + matchId: number; + buffer?: GPUBuffer | null; + type: 'module' | 'parameter' | 'op'; + op?: string; + parameter?: string; + moduleSite?: 'input' | 'output'; + batchIndex?: number; + source: TensorQuery; + queryIndex?: number; + path: string; + shape: number[]; + tensor: Tensor; + mean?: number; + variance?: number; +} + +export type RemoteCaptureMatch = Omit; + +export type CaptureStep = BaseStepData & { + type: 'capture'; + matches: CaptureMatch[]; +}; + +export type RemoteCaptureStep = BaseStepData & { + type: 'capture'; + matches: RemoteCaptureMatch[]; +}; + +export function createCapturePlan(query: string, module: Module): CapturePlan | null { + const capturePlan = new CapturePlan(); + try { + return capturePlan.parseScript(query).hookModule(module); + } catch (e) { + console.warn('Failed to create CapturePlan', e); + console.warn(capturePlan.getDiagnostics()); + return null; + } +} + +export function processCaptureResults( + results: CaptureResult[], + batchIndex: number = 0 +): CaptureMatch[] { + const processedMatches: (CaptureMatch | null)[] = []; + + for (const result of results) { + for (const [path, matches] of Object.entries(result.matches)) { + for (const match of matches) { + // For now we just ignore array-tensors until we know what to do with them + if (match.bufferTensor !== undefined && !Array.isArray(match.bufferTensor)) { + const t = match.bufferTensor; + // Alas, debugTensor doesn't seem to work in all scenarios, including when doing + // validation. Seems like we don't copy the buffer to the GPU at the right time. + // const effectiveTensor = match.type === 'parameter' ? t._clone() : t._clone().debugTensor; + const effectiveTensor = t._clone(); + processedMatches.push({ + matchId: t.id, + type: match.type, + ...(match.type !== 'parameter' ? { batchIndex } : {}), + source: result.source, + queryIndex: match.queryIndex ?? undefined, + op: (match as { op?: string }).op, + parameter: (match as { parameter?: string }).parameter, + moduleSite: (match as { site?: 'input' | 'output' }).site, + path: path, + shape: t.shape, + tensor: pin(effectiveTensor) + }); + } + } + } + } + + return processedMatches.filter((m): m is CaptureMatch => m !== null); +} + +export function makeCaptureMatchRemote( + match: RemoteCaptureMatch & { tensor?: unknown; buffer?: unknown } +): RemoteCaptureMatch { + const { buffer: _1, tensor: _2, ...rest } = match; + return rest; +} + +export async function runValidationExampleForCapture( + model: GeneratableModel, + sequences: ValidationExamples, + collateFn: PistonCollateFnType, + batchIndex: number +): Promise { + model.eval(); + + let valLoss: number | null = null; + try { + let collated; + if ('toySequences' in sequences) { + collated = (collateFn as ToyCollateFnType)([sequences.toySequences[batchIndex]]); + } else { + collated = (collateFn as NaturalCollateFnType)([ + sequences.naturalSequences[batchIndex] + ]); + } + + let loss: Tensor | null = null; + let modelName = ''; + if (model instanceof DecoderTransformer || model instanceof RNNDecoder) { + const [inputs, targets] = collated.tensors; + [, loss] = model.forward(await inputs.to('gpu'), { + targets: await targets.to('gpu') + }); + modelName = 'decoder-only'; + } else if (model instanceof EncoderDecoderTransformer || model instanceof RNNEncoderDecoder) { + const [encoderInputs, decoderInputs, decoderTargets] = ( + collated as EncoderDecoderBatchType + ).tensors; + [, loss] = model.forward(await encoderInputs.to('gpu'), await decoderInputs.to('gpu'), { + targets: await decoderTargets.to('gpu') + }); + modelName = 'encoder-decoder'; + } else if (model instanceof EncoderTransformer || model instanceof RNNEncoder) { + // Encoder-only: compute MLM loss over masked tokens + const [inputs, labels, attentionMask] = (collated as BidirectionalBatchType).tensors; + modelName = 'encoder-only'; + if (model instanceof EncoderTransformer) { + [, , , loss] = model.forward(await inputs.to('gpu'), { + attentionMask: await attentionMask.to('gpu'), + targets: await labels.to('gpu') + }); + } else { + // No attention mask here + [, , loss] = model.forward(await inputs.to('gpu'), { targets: await labels.to('gpu') }); + } + } else { + throw new Error('Unsupported model for validation'); + } + + if (!loss) { + throw new Error(`No loss tensor returned from ${modelName} model during validation`); + } + valLoss = await (await loss.to('cpu')).item(); + if (valLoss === null) { + throw new Error(`Validation loss item is null for ${modelName} model`); + } + // We do a backward pass to make sure we can capture the gradients + loss.backward(); + } finally { + model.train(); + } +} + +export type CapturePlanConfig = { + enabled: boolean; + script?: string | null; +}; + +export class CaptureManager { + private _plan: CapturePlan | null = null; + + build(model: Module, config: CapturePlanConfig): void { + this._plan = null; + if (!config.enabled) return; + if (!config.script) return; + this._plan = createCapturePlan(config.script, model); + } + + createSession(): CaptureSession | null { + return this._plan ? this._plan.createSession() : null; + } + + finalize(session: CaptureSession, batchIndex: number = 0): CaptureMatch[] { + const results = session.finalize(); + return processCaptureResults(results, batchIndex); + } + + get queries(): unknown[] { + return this._plan?.queries ?? []; + } + + get plan(): CapturePlan | null { + return this._plan; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/collate.ts b/examples/piston-train-toy/src/lib/train/data/collate.ts new file mode 100644 index 00000000..799d64c9 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/collate.ts @@ -0,0 +1,52 @@ +import type { + NaturalCollateFnType, + PistonCollateFnType, + PistonDatasetType, + ToyCollateFnType +} from '$lib/train/types'; + +import { gpu, int32, tensor, Tensor } from '@piston-ml/piston-web'; + +import type { CollatedRawSequence, ToySequence } from './toy/dataset'; + +import { NaturalLanguageDataset } from './natural/dataset'; +import ToyDataset from './toy/dataset'; + +export type CollateWrapFunction = (sequences: number[][]) => T; + +export function tensorWrap(sequences: number[][]): Tensor { + return tensor(sequences, { dtype: int32, device: gpu }); +} + +// Utility functions for accessing raw format data +export async function getCollatedSampleData( + dataset: PistonDatasetType, + collateFn: PistonCollateFnType, + sampleCount: number = 4 +): Promise<{ + samples: ToySequence[] | number[][]; + collated: CollatedRawSequence[]; +}> { + let collated: CollatedRawSequence[]; + let samples: ToySequence[] | number[][]; + + if (dataset instanceof ToyDataset) { + const iterator = dataset[Symbol.iterator](); + samples = Array.from({ length: sampleCount }, () => iterator.next().value); + collated = (collateFn as ToyCollateFnType)(samples as ToySequence[]).raw; + } else if (dataset instanceof NaturalLanguageDataset) { + const iterator = dataset[Symbol.asyncIterator](); + samples = []; + for (let i = 0; i < sampleCount; i++) { + const sample = await iterator.next(); + samples.push(sample.value); + } + collated = (collateFn as NaturalCollateFnType)(samples as number[][]).samples.map((s) => ({ + fullSequence: s + })); + } else { + throw new Error('Unsupported dataset type'); + } + + return { samples, collated }; +} diff --git a/examples/piston-train-toy/src/lib/train/data/filter.ts b/examples/piston-train-toy/src/lib/train/data/filter.ts new file mode 100644 index 00000000..1b7d2b9f --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/filter.ts @@ -0,0 +1,145 @@ +import { AsyncIterableDataset, IterableDataset } from '@piston-ml/piston-web'; + +import type { PistonDatasetType } from '../types'; +import type { ToyDatasetLike, ToySequence } from './toy/dataset'; + +import { NaturalLanguageDataset } from './natural/dataset'; + +// Custom error class for low-diversity dataset so we can catch it and display a custom message +export class LowDiversityDatasetError extends Error { + constructor(consecutiveSkips: number) { + const message = + `FilteredPistonDataset: skipped ${consecutiveSkips} consecutive examples because they ` + + 'collide with validation set; not enough diversity in the training dataset. Consider ' + + 'changing seed or dataset parameters.'; + super(message); + this.name = 'LowDiversityDatasetError'; + } +} + +export function buildToySequenceKeyer(datasetName: string): (seq: ToySequence) => string { + return (seq) => { + const prompt = seq.prompt ?? []; + const mask = seq.mask ?? []; + return [ + `dataset=${datasetName}`, + `promptLength=${prompt.length}`, + `targetLength=${seq.target.length}`, + `maskLength=${mask.length}`, + `prompt=${prompt.join(',')}`, + `target=${seq.target.join(',')}`, + `mask=${mask.join(',')}` + ].join('|'); + }; +} + +export function buildNaturalSequenceKeyer(datasetName: string): (seq: number[]) => string { + return (seq) => [`dataset=${datasetName}`, `len=${seq.length}`, `seq=${seq.join(',')}`].join('|'); +} + +export interface FilterOptions { + sequenceKeyer: (sample: T) => string; + maxConsecutiveSkips?: number; +} + +export function makeFilteredIterableDataset>( + base: B, + disallowed: Set, + options: FilterOptions +): B { + const maxSkips = options.maxConsecutiveSkips ?? 10; + const sequenceKeyer = options.sequenceKeyer; + + const handler: ProxyHandler = { + get(target, prop, receiver) { + if (prop === Symbol.iterator) { + return function (this: unknown) { + const iter = target[Symbol.iterator](); + let consecutiveSkips = 0; + return { + next(): IteratorResult { + for (;;) { + const n = iter.next(); + if (n.done) return n; + const sample = n.value; + const key = sequenceKeyer(sample); + if (!disallowed.has(key)) { + consecutiveSkips = 0; + return { value: sample, done: false }; + } + consecutiveSkips++; + if (consecutiveSkips >= maxSkips) { + throw new LowDiversityDatasetError(consecutiveSkips); + } + } + } + }; + }; + } + return Reflect.get(target, prop, receiver); + } + }; + + return new Proxy(base, handler); +} + +export function makeFilteredAsyncIterableDataset>( + base: B, + disallowed: Set, + options: FilterOptions +): B { + const maxSkips = options.maxConsecutiveSkips ?? 10; + const sequenceKeyer = options.sequenceKeyer; + + const handler: ProxyHandler = { + get(target, prop, receiver) { + if (prop === Symbol.asyncIterator) { + return function (this: unknown) { + const iter = target[Symbol.asyncIterator](); + let consecutiveSkips = 0; + return { + async next(): Promise> { + for (;;) { + const n = await iter.next(); + if (n.done) return n; + const sample = n.value; + const key = sequenceKeyer(sample); + if (!disallowed.has(key)) { + consecutiveSkips = 0; + return { value: sample, done: false }; + } + consecutiveSkips++; + if (consecutiveSkips >= maxSkips) { + throw new LowDiversityDatasetError(consecutiveSkips); + } + } + } + }; + }; + } + return Reflect.get(target, prop, receiver); + } + }; + + return new Proxy(base, handler); +} + +export function filterDatasetByHeldoutSamples( + dataset: PistonDatasetType, + datasetName: string, + samples: ReadonlyArray | ReadonlyArray +): PistonDatasetType { + if (dataset instanceof NaturalLanguageDataset) { + const sequenceKeyer = buildNaturalSequenceKeyer(datasetName); + const disallowed = new Set((samples as ReadonlyArray).map(sequenceKeyer)); + + return makeFilteredAsyncIterableDataset(dataset, disallowed, { sequenceKeyer }); + } else { + const sequenceKeyer = buildToySequenceKeyer(datasetName); + const disallowed = new Set((samples as ReadonlyArray).map(sequenceKeyer)); + + return makeFilteredIterableDataset(dataset, disallowed, { + sequenceKeyer + }); + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/index.ts b/examples/piston-train-toy/src/lib/train/data/index.ts new file mode 100644 index 00000000..ff1679df --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/index.ts @@ -0,0 +1,51 @@ +import type { Config } from '$lib/workspace/config'; +import type { Random } from 'random-js'; + +import type { PistonDatasetType } from '../types'; + +import { + buildNaturalLanguageDataset, + NATURAL_DATASET_META, + type NaturalDatasetName +} from './natural'; +import { buildToyDataset } from './toy'; +import { TOY_DATASET_CONFIG_DEFAULTS, TOY_DATASET_CONFIG_METADATA } from './toy/config'; + +export const DATASET_CONFIG_METADATA = { + ...Object.fromEntries( + Object.entries(NATURAL_DATASET_META).map(([name, meta]) => [ + name, + { + ...meta, + supportsModelTypes: ['encoder', 'decoder'], + type: 'natural' + } + ]) + ), + ...Object.fromEntries( + Object.entries(TOY_DATASET_CONFIG_METADATA).map(([name, meta]) => [ + name, + { ...meta, type: 'toy' } + ]) + ) +} as const; + +export const DATASET_CONFIG_DEFAULTS = { + ...TOY_DATASET_CONFIG_DEFAULTS, + ...(Object.fromEntries( + Object.entries(NATURAL_DATASET_META).map(([name]) => [name, {}]) + ) as Record) +} as const; + +export function buildDataset( + config: Config, + generator: Random, + split: 'train' | 'val' +): PistonDatasetType { + if (Object.keys(NATURAL_DATASET_META).includes(config.data.dataset)) { + return buildNaturalLanguageDataset(split, config); + } + return buildToyDataset(config, generator); +} + +export * from './collate'; diff --git a/examples/piston-train-toy/src/lib/train/data/natural/collate.ts b/examples/piston-train-toy/src/lib/train/data/natural/collate.ts new file mode 100644 index 00000000..7b5a9a8f --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/natural/collate.ts @@ -0,0 +1,78 @@ +import type { Tensor } from '@piston-ml/piston-web'; +import type { Random } from 'random-js'; + +import { type CollateWrapFunction, tensorWrap } from '../collate'; + +export type NaturalLanguageAutoregressiveBatch = { + tensors: [T, T]; // [input, target] + samples: number[][]; +}; + +export type NaturalLanguageBidirectionalBatch = { + tensors: [T, T, T]; // [input, labels, attentionMask] + samples: number[][]; +}; + +export function naturalLanguageAutoregressiveCollate( + batch: number[][], + options: { wrapFunction?: CollateWrapFunction | null } = {} +): NaturalLanguageAutoregressiveBatch { + const wrap = options.wrapFunction === undefined ? tensorWrap : options.wrapFunction; + + const inputsArr: number[][] = []; + const targetsArr: number[][] = []; + for (const sequence of batch) { + const L = sequence.length; + if (L < 2) { + inputsArr.push([]); + targetsArr.push([]); + continue; + } + inputsArr.push(sequence.slice(0, L - 1)); + targetsArr.push(sequence.slice(1)); + } + + const inputs = wrap ? wrap(inputsArr) : inputsArr; + const targets = wrap ? wrap(targetsArr) : targetsArr; + + return { tensors: [inputs as T, targets as T], samples: batch }; +} + +export function naturalLanguageBidirectionalCollate( + batch: number[][], + options: { + maskRatio: number; + generator: Random; + maskTokenId: number; + wrapFunction?: CollateWrapFunction | null; + } +): NaturalLanguageBidirectionalBatch { + const { maskRatio, generator, maskTokenId, wrapFunction = tensorWrap } = options; + + const inputIdsArr: number[][] = []; + const labelsArr: number[][] = []; + const attentionMaskArr: number[][] = []; + + for (const sequence of batch) { + const labels: number[] = new Array(sequence.length).fill(-100); + const inputs: number[] = [...sequence]; + const attn: number[] = new Array(sequence.length).fill(1); + + for (let i = 0; i < sequence.length; i++) { + if (generator.real(0, 1) < maskRatio) { + labels[i] = sequence[i]; + inputs[i] = maskTokenId; + } + } + + inputIdsArr.push(inputs); + labelsArr.push(labels); + attentionMaskArr.push(attn); + } + + const inputIds = wrapFunction ? wrapFunction(inputIdsArr) : inputIdsArr; + const labels = wrapFunction ? wrapFunction(labelsArr) : labelsArr; + const attentionMask = wrapFunction ? wrapFunction(attentionMaskArr) : attentionMaskArr; + + return { tensors: [inputIds as T, labels as T, attentionMask as T], samples: batch }; +} diff --git a/examples/piston-train-toy/src/lib/train/data/natural/dataset.ts b/examples/piston-train-toy/src/lib/train/data/natural/dataset.ts new file mode 100644 index 00000000..3eb450ac --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/natural/dataset.ts @@ -0,0 +1,308 @@ +import type { CitationEntries } from '$lib/components/controls/Citations.svelte'; +import type { Config } from '$lib/workspace/config'; + +import { PUBLIC_DATA_URL } from '$env/static/public'; +import { PreTrainedTokenizer } from '$lib/train/tokenizer'; +import { AsyncIterableDataset } from '@piston-ml/piston-web'; + +import type { ToyTokenizer } from '../toy/types'; + +import { globalShardCache } from './shardCache'; + +export type NaturalDatasetSplit = 'train' | 'val'; + +export type NaturalDatasetName = 'tinychat' | 'tinyshakespeare' | 'tinystories' | 'fineweb'; +export type NaturalDatasetMeta = { + name: string; + description: string; + citations: CitationEntries; +}; +export const NATURAL_DATASET_META: Record = { + tinystories: { + name: 'TinyStories (v2)', + description: 'Short stories generated by GPT-4 using a limited vocabulary.', + citations: { + entries: [ + { + name: 'Eldan, 2023', + url: 'https://arxiv.org/abs/2305.07759' + } + ] + } + }, + tinyshakespeare: { + name: 'TinyShakespeare', + description: "40,000 lines of Shakespeare from a variety of Shakespeare's plays.", + citations: { + entries: [ + { + name: 'Karpathy, 2015', + url: 'https://github.com/karpathy/char-rnn' + } + ] + } + }, + tinychat: { + name: 'TinyChat', + description: 'Short conversations generated by GPT-4o.', + citations: { + entries: [ + { + name: 'Raghavendra, 2024', + url: 'https://web.archive.org/https://nikhilr.io/posts/TinyChat15M/' + } + ] + } + }, + fineweb: { + name: 'FineWeb', + description: 'Cleaned and deduplicated English web data from CommonCrawl.', + citations: { + entries: [ + { + name: 'Penedo et al., 2024', + url: 'https://huggingface.co/spaces/HuggingFaceFW/blogpost-fineweb-v1' + } + ] + } + } +}; + +export interface NaturalDatasetConfig { + name: NaturalDatasetName; + split: NaturalDatasetSplit; + contextSize: number; + masked: boolean; + vocabSize: 'char' | 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768 | 65536; +} + +function joinUrl(base: string, path: string): string { + const trimmedBase = base.endsWith('/') ? base.slice(0, -1) : base; + const trimmedPath = path.startsWith('/') ? path.slice(1) : path; + return `${trimmedBase}/${trimmedPath}`; +} + +function pad4(n: number): string { + return n.toString().padStart(4, '0'); +} + +// Format is from Karpathy's llm.c: https://github.com/karpathy/llm.c +function parseShard(buffer: ArrayBuffer): Uint16Array { + if (buffer.byteLength < 256 * 4) { + throw new Error('Shard too small to contain header'); + } + const header = new Int32Array(buffer, 0, 256); + const magic = header[0]; + const version = header[1]; + const ntok = header[2]; + if (magic !== 20251003) { + throw new Error(`magic number mismatch in the data .bin file (got ${magic})`); + } + if (version !== 1) { + throw new Error(`unsupported version ${version}`); + } + const tokens = new Uint16Array(buffer, 256 * 4); + if (tokens.length !== ntok) { + throw new Error(`number of tokens read (${tokens.length}) does not match header (${ntok})`); + } + return tokens; +} + +async function fetchShard(url: string): Promise { + // Try cache first + const cached = await globalShardCache.get(url).catch(() => undefined); + if (cached) { + return parseShard(cached); + } + const res = await fetch(url); + if (res.status === 404) return null; + if (!res.ok) { + throw new Error(`Failed to fetch shard ${url}: ${res.status} ${res.statusText}`); + } + const buf = await res.arrayBuffer(); + // Store in cache (best-effort) + void globalShardCache.set(url, buf); + return parseShard(buf); +} + +// const charTokenizer: ToyTokenizer = { +// vocab: Object.fromEntries(Array.from({ length: 256 }, (_, i) => [String.fromCharCode(i), i])), +// ids: Object.fromEntries(Array.from({ length: 256 }, (_, i) => [i, String.fromCharCode(i)])), +// lastToken: 0, +// decode: (tokens: number[]) => tokens.map((token) => String.fromCharCode(token)).join('') +// }; + +function buildCharTokenizer(masked: boolean): ToyTokenizer { + const vocabList = Array.from({ length: 256 }, (_, i) => String.fromCharCode(i)); + vocabList.push(''); + vocabList[32] = '␣'; + if (masked) { + vocabList.push(''); + } + const vocab = Object.fromEntries(vocabList.map((char, i) => [char, i])); + const ids = Object.fromEntries(vocabList.map((char, i) => [i, char])); + return { + vocab, + ids, + lastToken: vocabList.length - 1, + decode: (tokens: number[]) => tokens.map((token) => ids[token] ?? '').join('') + }; +} + +type NaturalLanguageShard = { + data: Uint16Array; + cursor: number; +}; + +export class NaturalLanguageDataset extends AsyncIterableDataset { + readonly name: NaturalDatasetName; + readonly split: NaturalDatasetSplit; + readonly contextSize: number; + readonly charLevel: boolean; + vocabSize: number | Promise; + maskId: number | Promise | null; + eosId: number | Promise; + bosId: number | Promise; + tokenizer: PreTrainedTokenizer | Promise | ToyTokenizer | null; + + private shard: NaturalLanguageShard | null = null; + private shardIndex: number = 0; + private nextShardPromise: Promise | null = null; + + constructor(config: NaturalDatasetConfig) { + super(); + this.name = config.name; + this.split = config.split; + this.contextSize = config.contextSize; + this.charLevel = Boolean(config.vocabSize === 'char'); + const baseVocabSize = config.vocabSize === 'char' ? 257 : config.vocabSize; + + this.tokenizer = null; + + if (config.vocabSize === 'char') { + this.tokenizer = buildCharTokenizer(config.masked); + if (config.masked) { + this.vocabSize = baseVocabSize + 1; + this.maskId = this.vocabSize - 1; + this.eosId = this.vocabSize - 2; + this.bosId = this.vocabSize - 2; + } else { + this.vocabSize = baseVocabSize; + this.maskId = null; + this.eosId = baseVocabSize - 1; + this.bosId = baseVocabSize - 1; + } + } else { + this.vocabSize = config.vocabSize; + this.tokenizer = PreTrainedTokenizer.fromPretrained(`${this.name}/${this.vocabSize}`); + this.maskId = this.tokenizer.then((tokenizer) => tokenizer.maskTokenId!); + this.eosId = this.tokenizer.then((tokenizer) => tokenizer.eosTokenId!); + this.bosId = this.tokenizer.then((tokenizer) => tokenizer.bosTokenId!); + + this.tokenizer.then((tokenizer) => { + this.tokenizer = tokenizer; + this.maskId = tokenizer.maskTokenId!; + this.eosId = tokenizer.eosTokenId!; + this.bosId = tokenizer.bosTokenId!; + }); + } + } + + private buildShardUrl(shardIndex: number): string { + const file = `${this.split}-${pad4(shardIndex)}.bin`; + return joinUrl( + PUBLIC_DATA_URL, + `/tokenized/${this.name}/${this.charLevel ? 'char' : this.vocabSize}/${file}?v=1` + ); + } + + private async ensureCurrentLoaded(): Promise { + if (this.shard) return; + const first = await fetchShard(this.buildShardUrl(0)); + if (!first) { + // No data available; mark as exhausted + this.shard = null; + return; + } + this.shard = { data: first, cursor: 0 }; + if (this.split === 'train') { + this.nextShardPromise = fetchShard(this.buildShardUrl(1)); + } + } + + private async advanceShard(): Promise { + if (this.split === 'val') { + // Validation: single shard only; end iteration + this.shard = null; + return; + } + // Train: move to next shard, keeping one-shard lookahead + const next = this.nextShardPromise ? await this.nextShardPromise : null; + if (!next) { + this.shard = null; + return; + } + this.shardIndex += 1; + this.shard = { data: next, cursor: 0 }; + // Prefetch following shard + this.nextShardPromise = fetchShard(this.buildShardUrl(this.shardIndex + 1)); + } + + public async *[Symbol.asyncIterator](): AsyncIterator { + await this.ensureCurrentLoaded(); + while (true) { + if (!this.shard) { + return; + } + const tokens = this.shard.data; + const start = this.shard.cursor; + const end = start + this.contextSize; + if (end <= tokens.length) { + const slice = tokens.slice(start, end); + this.shard.cursor = end; + yield Array.from(slice); + continue; + } + // Advance if we need more tokens than are available in the current shard + await this.advanceShard(); + } + } + + // Export/import state for resumption + public exportState(): { shardIndex: number; cursor: number } | null { + if (!this.shard) return { shardIndex: this.shardIndex, cursor: 0 }; + return { shardIndex: this.shardIndex, cursor: this.shard.cursor }; + } + + public async importState(state: { shardIndex: number; cursor: number } | null): Promise { + if (!state) return; + this.shardIndex = Math.max(0, state.shardIndex | 0); + const current = await fetchShard(this.buildShardUrl(this.shardIndex)); + if (!current) { + this.shard = null; + this.nextShardPromise = null; + return; + } + this.shard = { data: current, cursor: Math.min(state.cursor | 0, current.length) }; + if (this.split === 'train') { + this.nextShardPromise = fetchShard(this.buildShardUrl(this.shardIndex + 1)); + } else { + this.nextShardPromise = null; + } + } +} + +export function buildNaturalLanguageDataset( + split: NaturalDatasetSplit, + config: Config +): NaturalLanguageDataset { + return new NaturalLanguageDataset({ + name: config.data.dataset as NaturalDatasetName, + split, + contextSize: config.data.natural.contextSize, + masked: config.model.topology === 'encoder', + vocabSize: config.data.natural.vocabSize + }); +} + +export default NaturalLanguageDataset; diff --git a/examples/piston-train-toy/src/lib/train/data/natural/index.ts b/examples/piston-train-toy/src/lib/train/data/natural/index.ts new file mode 100644 index 00000000..300a42ca --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/natural/index.ts @@ -0,0 +1,2 @@ +export * from './collate'; +export * from './dataset'; diff --git a/examples/piston-train-toy/src/lib/train/data/natural/shardCache.ts b/examples/piston-train-toy/src/lib/train/data/natural/shardCache.ts new file mode 100644 index 00000000..e77430a4 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/natural/shardCache.ts @@ -0,0 +1,144 @@ +// A tiny IndexedDB-backed LRU cache specialized for dataset shard ArrayBuffers. +// Uses one object store for blobs and a metadata store for LRU bookkeeping. + +const DB_NAME = 'natural-shard-cache'; +const DB_VERSION = 1; +const STORE_BLOBS = 'blobs'; +const META_KEY = 'lru'; +const STORE_META = 'meta'; + +export interface ShardCacheOptions { + // Maximum total size in bytes + maxBytes?: number; +} + +type LruEntry = { last: number; size: number }; +type LruState = { entries: Record; totalSize: number }; + +const DEFAULTS: Required = { + maxBytes: 256 * 1024 * 1024 // 256MB +}; + +function openDb(): Promise { + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onupgradeneeded = () => { + const db = req.result; + if (!db.objectStoreNames.contains(STORE_BLOBS)) { + db.createObjectStore(STORE_BLOBS); + } + if (!db.objectStoreNames.contains(STORE_META)) { + db.createObjectStore(STORE_META); + } + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + req.onblocked = () => reject(new Error('IndexedDB upgrade blocked')); + }); +} + +function promisify(req: IDBRequest): Promise { + return new Promise((resolve, reject) => { + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); +} + +function txRequest( + db: IDBDatabase, + storeName: string, + mode: IDBTransactionMode, + op: (store: IDBObjectStore) => IDBRequest +): Promise { + const tx = db.transaction(storeName, mode); + const store = tx.objectStore(storeName); + return promisify(op(store)); +} + +async function readMeta(db: IDBDatabase): Promise { + const res = await txRequest(db, STORE_META, 'readonly', (s) => + s.get(META_KEY) + ); + return res ?? { entries: {}, totalSize: 0 }; +} + +async function writeMeta(db: IDBDatabase, meta: LruState): Promise { + await txRequest(db, STORE_META, 'readwrite', (s) => s.put(meta, META_KEY)); +} + +async function getBlob(db: IDBDatabase, url: string): Promise { + return txRequest(db, STORE_BLOBS, 'readonly', (s) => s.get(url)); +} + +async function putBlob(db: IDBDatabase, url: string, buf: ArrayBuffer): Promise { + await txRequest(db, STORE_BLOBS, 'readwrite', (s) => s.put(buf, url)); +} + +async function deleteBlob(db: IDBDatabase, url: string): Promise { + await txRequest(db, STORE_BLOBS, 'readwrite', (s) => s.delete(url)); +} + +export class ShardCache { + private dbPromise: Promise | null = null; + private options: Required; + + constructor(options: ShardCacheOptions = {}) { + this.options = { ...DEFAULTS, ...options }; + } + + private get db(): Promise { + if (!this.dbPromise) this.dbPromise = openDb(); + return this.dbPromise; + } + + async get(url: string): Promise { + const db = await this.db; + const [meta, buf] = await Promise.all([readMeta(db), getBlob(db, url)]); + if (!buf) return undefined; + // Bump LRU timestamp + meta.entries[url] = { last: performance.now(), size: buf.byteLength }; + await writeMeta(db, meta); + console.debug('dataset shard cache hit', url); + return buf; + } + + async set(url: string, buf: ArrayBuffer): Promise { + const db = await this.db; + const meta = await readMeta(db); + const size = buf.byteLength; + const prevSize = meta.entries[url]?.size ?? 0; + meta.entries[url] = { last: performance.now(), size }; + meta.totalSize += size - prevSize; + await putBlob(db, url, buf); + await this.evictIfNeeded(db, meta); + } + + private async evictIfNeeded(db: IDBDatabase, meta: LruState): Promise { + try { + // Keep at most maxBytes + const { maxBytes } = this.options; + const keys = Object.keys(meta.entries); + if (keys.length === 0) return; + + const overBytes = Math.max(0, meta.totalSize - maxBytes); + if (overBytes === 0) return; + + // Sort by last access ascending + keys.sort((a, b) => meta.entries[a].last - meta.entries[b].last); + + let bytesToFree = overBytes; + for (const key of keys) { + if (bytesToFree <= 0) break; + const size = meta.entries[key]?.size ?? 0; + await deleteBlob(db, key); + delete meta.entries[key]; + meta.totalSize = Math.max(0, meta.totalSize - size); + if (bytesToFree > 0) bytesToFree = Math.max(0, bytesToFree - size); + } + } finally { + await writeMeta(db, meta); + } + } +} + +export const globalShardCache = new ShardCache(); diff --git a/examples/piston-train-toy/src/lib/train/data/pipeline.ts b/examples/piston-train-toy/src/lib/train/data/pipeline.ts new file mode 100644 index 00000000..1377d30d --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/pipeline.ts @@ -0,0 +1,49 @@ +import type { Config } from '$lib/workspace/config'; +import type { Tensor } from '@piston-ml/piston-web'; +import type { Random } from 'random-js'; + +import type { PistonCollateFnType, PistonDatasetType } from '../types'; + +import { buildDataset, tensorWrap } from '.'; +import { createCollateFn, createDataloader } from '../utils/model'; +import { forkRandom } from '../utils/random'; + +export type BuiltData = { + train: { + dataset: PistonDatasetType; + iterator: AsyncIterator<{ + readonly tensors: Tensor[]; + readonly samples?: unknown[]; + }>; + }; + validation?: { + dataset: PistonDatasetType; + collateFn: PistonCollateFnType; + }; +}; + +export async function buildDataPipeline( + config: Config, + generator: Random, + maskGenerator: Random | null, + trainDatasetOverride?: PistonDatasetType +): Promise { + const trainDataset = trainDatasetOverride ?? buildDataset(config, generator, 'train'); + const [trainDataloader] = createDataloader(config, trainDataset, generator, tensorWrap); + + let validation: BuiltData['validation'] | undefined; + if (config.training.validation.present) { + const validationGenerator = forkRandom(generator); + const validationDataset = buildDataset(config, validationGenerator, 'val'); + const collateFn = createCollateFn(config, validationDataset, maskGenerator, tensorWrap); + validation = { dataset: validationDataset, collateFn }; + } + + return { + train: { + dataset: trainDataset, + iterator: trainDataloader[Symbol.asyncIterator]() + }, + validation + }; +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/addition.ts b/examples/piston-train-toy/src/lib/train/data/toy/addition.ts new file mode 100644 index 00000000..dbb94f49 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/addition.ts @@ -0,0 +1,99 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface AdditionConfig { + maxNumber: number; + includeExpressionTokens: boolean; +} + +export const ADDITION_CONFIG_METADATA = { + name: 'Addition', + description: 'Add two numbers.', + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + maxNumber: { + name: 'Max Number', + description: 'Maximum value for each addend', + type: 'number' as const, + min: 5, + max: 100, + default: 20 + }, + includeExpressionTokens: { + name: 'Include Expression Tokens (+, =)', + type: 'boolean' as const, + default: true + } + } +} as const; + +export const ADDITION_CONFIG_DEFAULTS: AdditionConfig = { + maxNumber: ADDITION_CONFIG_METADATA.parameters.maxNumber.default, + includeExpressionTokens: ADDITION_CONFIG_METADATA.parameters.includeExpressionTokens.default +}; + +export const ADDITION_SHORT_DESCRIPTIONS = { + maxNumber: 'max num', + includeExpressionTokens: 'expr tokens' +}; + +export class AdditionDataset extends ToyDataset { + /** + * Tokenizer for addition. + * - tokens 0 .. maxNum represent the numbers 0 … maxNum + * (When converting to a string, token id i <= maxNum becomes `<${i}>` with zero-padding.) + * - token maxNum+1 -> "+" + * - token maxNum+2 -> "=" + */ + protected buildVocab(): string[] { + const { maxNumber, includeExpressionTokens } = this.config; + const vocab: string[] = []; + const padWidth = maxNumber.toString().length; + // 1. Add tokens for numbers + vocab.push( + ...Array.from({ length: maxNumber + 1 }, (_, i) => i.toString().padStart(padWidth, '0')) + ); + // 2. Add tokens for + and = + if (includeExpressionTokens) { + vocab.push('+', '='); + } + return vocab; + } + + /** + * Addition: generate two addends (in [0, maxNum]) chosen so that their sum <= maxNum. + * Example: <15>+<03>=<18> + */ + public generateSequence(): ToySequence { + const { maxNumber, includeExpressionTokens } = this.config; + + // 1. Pick numbers and compute sum. Include maxNumber in the range. + const sum = this.generator.integer(0, maxNumber); + const num1 = sum > 0 ? this.generator.integer(0, sum - 1) : 0; + const num2 = sum - num1; + + // 2. Convert to token IDs: + // - 0..maxNum are numbers + // - maxNum+1 is '+' + // - maxNum+2 is '=' + let prompt; + if (includeExpressionTokens) { + prompt = [num1, maxNumber + 1, num2, maxNumber + 2]; + } else { + prompt = [num1, num2]; + } + + // 3. Target is a single token corresponding to the sum + const target = [sum]; + + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/collate.ts b/examples/piston-train-toy/src/lib/train/data/toy/collate.ts new file mode 100644 index 00000000..95679aff --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/collate.ts @@ -0,0 +1,335 @@ +import type { Tensor } from '@piston-ml/piston-web'; + +import { MersenneTwister19937, Random } from 'random-js'; + +import { type CollateWrapFunction, tensorWrap } from '../collate'; +import { + deriveToySampleSeed, + type ToyAutoregressiveBatch, + type ToyBidirectionalBatch, + type ToyDatasetLike, + type ToyEncoderDecoderBatch, + type ToySequence +} from './dataset'; + +// Helper function to add special tokens to sequences for rawData +function withSpecials( + body: number[], + { bosId, eosId }: { bosId: number | null; eosId: number | null } +): number[] { + const seq = [...body]; + if (eosId !== null) seq.push(eosId); + return bosId !== null ? [bosId, ...seq] : seq; +} + +interface AutoregressiveCollateOptions { + ignorePrompt?: boolean; + wrapFunction?: CollateWrapFunction | null; +} + +interface BidirectionalCollateOptions { + maskPrompt?: boolean; + maskRatio?: number; + generator?: Random; + wrapFunction?: CollateWrapFunction | null; +} + +export interface EncoderDecoderCollateOptions { + wrapFunction?: CollateWrapFunction | null; +} + +export function toyDatasetAutoregressiveCollate( + batch: ToySequence[], + dataset: ToyDatasetLike, + options: AutoregressiveCollateOptions = {} +): ToyAutoregressiveBatch { + const { ignorePrompt = false, wrapFunction = tensorWrap } = options; + + const sequencesData = batch.map(({ prompt, target: completion, mask }) => { + // Build full sequence, potentially adding BOS at start and EOS at end + const fullSequence: number[] = []; + + // Add BOS if available + if (dataset.bosId !== null) { + fullSequence.push(dataset.bosId); + } + + // Add prompt and completion + fullSequence.push(...(prompt ?? []), ...completion); + + // Add EOS if available + if (dataset.eosId !== null) { + fullSequence.push(dataset.eosId); + } + + const input = fullSequence.slice(0, -1); + const target = fullSequence.slice(1); + + // Mask prompt tokens in target if ignorePrompt is true + // Note: target has already been shifted by slice(1), so no bosOffset needed + const promptLength = prompt?.length ?? 0; + if (ignorePrompt && promptLength > 0) { + target.fill(-100, 0, promptLength); + } + + // Apply dataset-provided mask to target positions that correspond to completion tokens + if (mask && mask.length > 0) { + for (let i = 0; i < completion.length; i++) { + if (mask[i] === false) { + const targetIndex = promptLength + i; + if (targetIndex >= 0 && targetIndex < target.length) { + target[targetIndex] = -100; + } + } + } + } + + // Create visible completion with mask tokens for rawData + const visibleCompletion = [...completion]; + if (mask && dataset.maskId !== null) { + for (let i = 0; i < visibleCompletion.length; i++) { + if (mask[i] === false) { + visibleCompletion[i] = dataset.maskId; + } + } + } + + return { + input, + target, + rawData: { + fullSequence, + prompt: withSpecials(prompt ?? [], { bosId: dataset.bosId, eosId: null }), + target: withSpecials(visibleCompletion, { bosId: null, eosId: dataset.eosId }), + ignored: target.map((t) => t === -100) + } + }; + }); + + const inputSequences = sequencesData.map(({ input }) => input); + const targetSequences = sequencesData.map(({ target }) => target); + + const input = wrapFunction ? wrapFunction(inputSequences) : inputSequences; + const target = wrapFunction ? wrapFunction(targetSequences) : targetSequences; + + return { + tensors: [input as T, target as T], + raw: sequencesData.map(({ rawData }) => rawData), + samples: batch + }; +} + +export function toyDatasetBidirectionalCollate( + batch: ToySequence[], + dataset: ToyDatasetLike, + options: BidirectionalCollateOptions = {} +): ToyBidirectionalBatch { + const { maskPrompt = false, maskRatio = 0.15, generator, wrapFunction = tensorWrap } = options; + + if (dataset.maskId === null) { + throw new Error( + 'Encoder-only collation requires a mask token, but none was configured in the dataset' + ); + } + + const maskToken = dataset.maskId; + + const sequencesData = batch.map((seq) => { + const { prompt, target, mask, absoluteIndex } = seq; + const fullSequence: number[] = [...(prompt ?? []), ...target]; + // If a global generator is not provided, derive a per-sample RNG from baseSeed and absoluteIndex + const rng = generator + ? generator + : new Random( + MersenneTwister19937.seed( + deriveToySampleSeed( + dataset.baseSeed, + dataset.datasetName, + absoluteIndex ?? 0, + 'toy-mask' + ) + ) + ); + + const maskedSequence = [...fullSequence]; + const labels = new Array(fullSequence.length).fill(-100); + + // Determine which tokens are eligible for masking + const promptLength = prompt?.length ?? 0; + + const eligibleForMasking = fullSequence.map((_, index) => { + if (maskPrompt) { + // Allow masking across both prompt and target (no specials in bidirectional) + return true; + } else { + // Only target tokens eligible (after prompt) + const targetStart = promptLength; + const targetEnd = fullSequence.length; + if (index < targetStart || index >= targetEnd) return false; + // If dataset provided a mask array, restrict eligible positions to those marked true + if (mask && mask[index - targetStart] === false) return false; + return true; + } + }); + + // Apply random masking and track which positions got masked + const maskedPositions: boolean[] = new Array(fullSequence.length).fill(false); + eligibleForMasking.forEach((eligible, index) => { + if (eligible && rng.real(0, 1) < maskRatio) { + maskedSequence[index] = maskToken; + maskedPositions[index] = true; + labels[index] = fullSequence[index]; // compute loss against original token + } + }); + + // Ensure at least one token is masked per example + if (!maskedPositions.some((v) => v)) { + // Prefer among eligible positions + const candidates: number[] = eligibleForMasking + .map((eligible, idx) => (eligible ? idx : -1)) + .filter((idx) => idx >= 0); + if (candidates.length > 0) { + const forcedIdx = candidates[rng.integer(0, candidates.length - 1)]; + maskedSequence[forcedIdx] = maskToken; + maskedPositions[forcedIdx] = true; + labels[forcedIdx] = fullSequence[forcedIdx]; + } else { + throw new Error('No tokens eligible for masking'); + } + } + + return { + maskedSequence, + labels, + rawData: { + fullSequence: maskedSequence, + prompt: [...(prompt ?? [])], + target: [...target], + ignored: labels.map((l) => l === -100) + } + }; + }); + + const inputSequences = sequencesData.map(({ maskedSequence }) => maskedSequence); + const labelSequences = sequencesData.map(({ labels }) => labels); + const attentionMaskSequences = sequencesData.map(({ maskedSequence }) => + maskedSequence.map(() => 1) + ); + + const input = wrapFunction ? wrapFunction(inputSequences) : inputSequences; + const labels = wrapFunction ? wrapFunction(labelSequences) : labelSequences; + const attentionMask = wrapFunction + ? wrapFunction(attentionMaskSequences) + : attentionMaskSequences; + + return { + tensors: [input as T, labels as T, attentionMask as T], + raw: sequencesData.map(({ rawData }) => rawData), + samples: batch + }; +} + +export function toyDatasetEncoderDecoderCollate( + batch: ToySequence[], + dataset: ToyDatasetLike, + options: EncoderDecoderCollateOptions = {} +): ToyEncoderDecoderBatch { + if (dataset.bosId === null) { + throw new Error( + 'Encoder-decoder collation requires a BOS token, but none was configured in the dataset' + ); + } + + const encoderData = batch.map(({ prompt }) => { + // Build encoder input WITHOUT BOS/EOS + const encoderSequence: number[] = [...(prompt ?? [])]; + + return { + encoderSequence, + rawData: { + fullSequence: encoderSequence, + prompt: encoderSequence, // encoder sequence is the prompt without specials + target: [], // No target for encoder + ignored: [] + } + }; + }); + + const decoderData = batch.map(({ target, mask }) => { + // Apply mask to target array first + let maskedTarget = [...target]; + if (mask) { + maskedTarget = maskedTarget.map((token, i) => (mask[i] === false ? -100 : token)); + } + + // Build decoder target with EOS if available + const decoderTarget: number[] = [...maskedTarget]; + if (dataset.eosId !== null) { + decoderTarget.push(dataset.eosId); + } + + // Decoder input: BOS + target (for teacher forcing) + // Note: dataset.bosId is guaranteed to be non-null because we checked earlier + // If EOS is enabled, we feed BOS + full target so model predicts target then EOS. + // If EOS is disabled, feed BOS + target[:-1] so input/target lengths match and + // the loss is computed only over generated tokens. + const decoderInput: number[] = + dataset.eosId !== null + ? [dataset.bosId!, ...target] + : [dataset.bosId!, ...target.slice(0, -1)]; + + // Create visible target with mask tokens for rawData + const visibleTarget = [...target]; + if (mask && dataset.maskId !== null) { + for (let i = 0; i < visibleTarget.length; i++) { + if (mask[i] === false) { + visibleTarget[i] = dataset.maskId; + } + } + } + + return { + decoderInput, + decoderTarget, + rawData: { + fullSequence: decoderInput, + prompt: [], // decoder has no prompt concept + target: withSpecials(visibleTarget, { bosId: null, eosId: dataset.eosId }), + ignored: decoderTarget.map((t) => t === -100) + } + }; + }); + + const encoderSequences = encoderData.map(({ encoderSequence }) => encoderSequence); + const decoderInputSequences = decoderData.map(({ decoderInput }) => decoderInput); + const decoderTargetSequences = decoderData.map(({ decoderTarget }) => decoderTarget); + + const encoderInput = options.wrapFunction + ? options.wrapFunction(encoderSequences) + : encoderSequences; + const decoderInput = options.wrapFunction + ? options.wrapFunction(decoderInputSequences) + : decoderInputSequences; + const decoderTarget = options.wrapFunction + ? options.wrapFunction(decoderTargetSequences) + : decoderTargetSequences; + + // Combine encoder and decoder raw data into single sequences + const combinedRaw = batch.map((_, index) => { + const encoderRaw = encoderData[index].rawData; + const decoderRaw = decoderData[index].rawData; + + return { + fullSequence: [...encoderRaw.fullSequence, ...decoderRaw.fullSequence], + prompt: encoderRaw.prompt, // encoder provides the prompt + target: decoderRaw.target, // decoder provides the target + ignored: [...encoderRaw.ignored, ...decoderRaw.ignored] + }; + }); + + return { + tensors: [encoderInput as T, decoderInput as T, decoderTarget as T], + raw: combinedRaw, + samples: batch + }; +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/config.ts b/examples/piston-train-toy/src/lib/train/data/toy/config.ts new file mode 100644 index 00000000..6f455682 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/config.ts @@ -0,0 +1,116 @@ +// Shared configuration parameter interface +export interface ConfigParameter { + name: string; + description?: string; + type: 'number' | 'boolean'; + min?: number; + max?: number; + step?: number; + default: number | boolean; +} + +// Import all dataset configurations +import { ADDITION_CONFIG_DEFAULTS, ADDITION_CONFIG_METADATA } from './addition'; +import { COPY_MEMORY_CONFIG_DEFAULTS, COPY_MEMORY_CONFIG_METADATA } from './copyMemory'; +import { DYCK_CONFIG_DEFAULTS, DYCK_CONFIG_METADATA } from './dyck'; +import { ELMAN_CONFIG_DEFAULTS, ELMAN_CONFIG_METADATA } from './elman'; +import { MARKED_ADDITION_CONFIG_DEFAULTS, MARKED_ADDITION_CONFIG_METADATA } from './markedAddition'; +import { + MODULAR_ADDITION_CONFIG_DEFAULTS, + MODULAR_ADDITION_CONFIG_METADATA +} from './modularAddition'; +import { PARITY_CONFIG_DEFAULTS, PARITY_CONFIG_METADATA } from './parity'; +import { RANDOM_CONFIG_DEFAULTS, RANDOM_CONFIG_METADATA } from './random'; +import { REPEAT_CONFIG_DEFAULTS, REPEAT_CONFIG_METADATA } from './repeat'; +import { REVERSE_CONFIG_DEFAULTS, REVERSE_CONFIG_METADATA } from './reverse'; +import { SLAPJACK_CONFIG_DEFAULTS, SLAPJACK_CONFIG_METADATA } from './slapjack'; +import { SORT_CONFIG_DEFAULTS, SORT_CONFIG_METADATA } from './sort'; +import { TEMPORAL_ORDER_CONFIG_DEFAULTS, TEMPORAL_ORDER_CONFIG_METADATA } from './temporalOrder'; +import { TWO_SUM_CONFIG_DEFAULTS, TWO_SUM_CONFIG_METADATA } from './twoSum'; +import { ZEROS_CONFIG_DEFAULTS, ZEROS_CONFIG_METADATA } from './zeros'; + +// Re-export types and constants +export type { AdditionConfig } from './addition'; +export { ADDITION_CONFIG_DEFAULTS, ADDITION_CONFIG_METADATA } from './addition'; + +export type { CopyMemoryConfig } from './copyMemory'; +export { COPY_MEMORY_CONFIG_DEFAULTS, COPY_MEMORY_CONFIG_METADATA } from './copyMemory'; + +export type { DyckConfig } from './dyck'; +export { DYCK_CONFIG_DEFAULTS, DYCK_CONFIG_METADATA } from './dyck'; + +export { ELMAN_CONFIG_DEFAULTS, ELMAN_CONFIG_METADATA } from './elman'; + +export type { MarkedAdditionConfig } from './markedAddition'; +export { MARKED_ADDITION_CONFIG_DEFAULTS, MARKED_ADDITION_CONFIG_METADATA } from './markedAddition'; + +export type { ModularAdditionConfig } from './modularAddition'; +export { + MODULAR_ADDITION_CONFIG_DEFAULTS, + MODULAR_ADDITION_CONFIG_METADATA +} from './modularAddition'; + +export type { ParityConfig } from './parity'; +export { PARITY_CONFIG_DEFAULTS, PARITY_CONFIG_METADATA } from './parity'; + +export type { RandomConfig } from './random'; +export { RANDOM_CONFIG_DEFAULTS, RANDOM_CONFIG_METADATA } from './random'; + +export type { RepeatConfig } from './repeat'; +export { REPEAT_CONFIG_DEFAULTS, REPEAT_CONFIG_METADATA } from './repeat'; + +export type { ReverseConfig } from './reverse'; +export { REVERSE_CONFIG_DEFAULTS, REVERSE_CONFIG_METADATA } from './reverse'; + +export type { SlapjackConfig } from './slapjack'; +export { SLAPJACK_CONFIG_DEFAULTS, SLAPJACK_CONFIG_METADATA } from './slapjack'; + +export type { SortConfig } from './sort'; +export { SORT_CONFIG_DEFAULTS, SORT_CONFIG_METADATA } from './sort'; + +export type { TemporalOrderConfig } from './temporalOrder'; +export { TEMPORAL_ORDER_CONFIG_DEFAULTS, TEMPORAL_ORDER_CONFIG_METADATA } from './temporalOrder'; + +export type { TwoSumConfig } from './twoSum'; +export { TWO_SUM_CONFIG_DEFAULTS, TWO_SUM_CONFIG_METADATA } from './twoSum'; + +export type { ZerosConfig } from './zeros'; +export { ZEROS_CONFIG_DEFAULTS, ZEROS_CONFIG_METADATA } from './zeros'; + +// Aggregate metadata for all datasets +export const TOY_DATASET_CONFIG_METADATA = { + addition: ADDITION_CONFIG_METADATA, + 'copy-memory': COPY_MEMORY_CONFIG_METADATA, + dyck: DYCK_CONFIG_METADATA, + elman: ELMAN_CONFIG_METADATA, + 'marked-addition': MARKED_ADDITION_CONFIG_METADATA, + 'modular-addition': MODULAR_ADDITION_CONFIG_METADATA, + parity: PARITY_CONFIG_METADATA, + random: RANDOM_CONFIG_METADATA, + repeat: REPEAT_CONFIG_METADATA, + reverse: REVERSE_CONFIG_METADATA, + slapjack: SLAPJACK_CONFIG_METADATA, + sort: SORT_CONFIG_METADATA, + 'temporal-order': TEMPORAL_ORDER_CONFIG_METADATA, + 'two-sum': TWO_SUM_CONFIG_METADATA, + zeros: ZEROS_CONFIG_METADATA +} as const; + +// Aggregate defaults for all datasets +export const TOY_DATASET_CONFIG_DEFAULTS = { + addition: ADDITION_CONFIG_DEFAULTS, + 'copy-memory': COPY_MEMORY_CONFIG_DEFAULTS, + dyck: DYCK_CONFIG_DEFAULTS, + elman: ELMAN_CONFIG_DEFAULTS, + 'marked-addition': MARKED_ADDITION_CONFIG_DEFAULTS, + 'modular-addition': MODULAR_ADDITION_CONFIG_DEFAULTS, + parity: PARITY_CONFIG_DEFAULTS, + random: RANDOM_CONFIG_DEFAULTS, + repeat: REPEAT_CONFIG_DEFAULTS, + reverse: REVERSE_CONFIG_DEFAULTS, + slapjack: SLAPJACK_CONFIG_DEFAULTS, + sort: SORT_CONFIG_DEFAULTS, + 'temporal-order': TEMPORAL_ORDER_CONFIG_DEFAULTS, + 'two-sum': TWO_SUM_CONFIG_DEFAULTS, + zeros: ZEROS_CONFIG_DEFAULTS +} as const; diff --git a/examples/piston-train-toy/src/lib/train/data/toy/copyMemory.ts b/examples/piston-train-toy/src/lib/train/data/toy/copyMemory.ts new file mode 100644 index 00000000..f68b74a0 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/copyMemory.ts @@ -0,0 +1,154 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface CopyMemoryConfig { + prefixLength: number; // length of the subsequence to memorize (A,B,C) + distractorLength: number; // length of distractor stream between prefix and recall signal + vocabSize: number; // vocabulary for prefix and distractors (A..) + recallTokenEnabled: boolean; // include '?' token to signal recall time + includeSeparators: boolean; // optional commas between tokens +} + +export const COPY_MEMORY_CONFIG_METADATA = { + name: 'Copy Memory with Distractors', + description: 'Memorize a subsequence and reproduce it at the end after distractors.', + citations: { + entries: [ + { + name: 'Hochreiter & Schmidhuber, 1997', + url: 'https://ieeexplore.ieee.org/abstract/document/6795963' + } + ] + }, + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + prefixLength: { + name: 'Prefix Length', + description: 'Length of subsequence to memorize', + type: 'number' as const, + min: 1, + max: 32, + step: 1, + default: 3 + }, + distractorLength: { + name: 'Distractor Length', + description: 'Number of distractor tokens before recall', + type: 'number' as const, + min: 0, + max: 512, + step: 1, + default: 12 + }, + vocabSize: { + name: 'Vocab Size', + description: 'Number of token types used for prefix/distractors', + type: 'number' as const, + min: 2, + max: 128, + step: 1, + default: 10 + }, + recallTokenEnabled: { + name: 'Include Recall Token (?)', + description: 'Append a ? token to signal recall time', + type: 'boolean' as const, + default: true + }, + includeSeparators: { + name: 'Include Separators', + description: 'Insert commas between tokens', + type: 'boolean' as const, + default: false + } + } +} as const; + +export const COPY_MEMORY_CONFIG_DEFAULTS: CopyMemoryConfig = { + prefixLength: COPY_MEMORY_CONFIG_METADATA.parameters.prefixLength.default, + distractorLength: COPY_MEMORY_CONFIG_METADATA.parameters.distractorLength.default, + vocabSize: COPY_MEMORY_CONFIG_METADATA.parameters.vocabSize.default, + recallTokenEnabled: COPY_MEMORY_CONFIG_METADATA.parameters.recallTokenEnabled.default, + includeSeparators: COPY_MEMORY_CONFIG_METADATA.parameters.includeSeparators.default +}; + +export const COPY_MEMORY_SHORT_DESCRIPTIONS = { + prefixLength: 'prefix', + distractorLength: 'distractor', + vocabSize: 'vocab', + recallTokenEnabled: 'recall', + includeSeparators: 'separators' +}; + +export class CopyMemoryDataset extends ToyDataset { + /** + * Vocab: + * - letters L0..L{vocabSize-1} + * - optional comma separator + * - optional recall token '?' + */ + protected buildVocab(): string[] { + const { vocabSize, recallTokenEnabled, includeSeparators } = this.config; + const padWidth = vocabSize.toString().length; + const vocab = Array.from( + { length: vocabSize + 1 }, + (_, i) => `L${i.toString().padStart(padWidth, '0')}` + ); + if (includeSeparators) vocab.push(','); + if (recallTokenEnabled) vocab.push('?'); + return vocab; + } + + /** + * Prompt layout: P0 P1 ... P{prefix-1} [distractors...] [?] + * Target: P0 P1 ... P{prefix-1} + */ + public generateSequence(): ToySequence { + const { prefixLength, distractorLength, vocabSize, recallTokenEnabled, includeSeparators } = + this.config; + const prompt: number[] = []; + + // Build prefix + const prefix: number[] = []; + for (let i = 0; i < prefixLength; i++) { + prefix.push(this.generator.integer(0, vocabSize - 1)); + } + + const comma = this.tokenizer.vocab[',']; + + // Push prefix into prompt + for (let i = 0; i < prefix.length; i++) { + prompt.push(prefix[i]); + if (includeSeparators) { + if (i < prefix.length - 1 || distractorLength > 0 || recallTokenEnabled) { + prompt.push(comma); + } + } + } + + // Add distractors + for (let i = 0; i < distractorLength; i++) { + prompt.push(this.generator.integer(0, vocabSize - 1)); + if (includeSeparators && (i < distractorLength - 1 || recallTokenEnabled)) { + prompt.push(comma); + } + } + + // Add recall token + if (recallTokenEnabled) { + prompt.push(this.tokenizer.vocab['?']); + } + + // Target is the original prefix + const target = [...prefix]; + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/dataset.ts b/examples/piston-train-toy/src/lib/train/data/toy/dataset.ts new file mode 100644 index 00000000..c094831d --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/dataset.ts @@ -0,0 +1,234 @@ +import { IterableDataset } from '@piston-ml/piston-web'; +import { MersenneTwister19937, Random } from 'random-js'; + +import type { ToyTokenizer, ToyValidationMetrics } from './types'; + +export const BOS = ''; +export const EOS = ''; +export const MASK = ''; + +export interface SpecialTokensConfig { + includeBos: boolean; + includeEos: boolean; + includeMask: boolean; +} + +export type SpecialTokenSet = { + bos: string; + eos?: string; + mask?: string; +}; + +export interface ToySequence { + prompt?: number[]; + target: number[]; + mask?: boolean[]; + // Absolute sample index within this run (for deterministic per-sample behavior) + absoluteIndex?: number; +} + +// Raw format interfaces that preserve prompt/target distinction and show mask tokens +export interface CollatedRawSequence { + // The full sequence with all tokens visible (including mask tokens, BOS, EOS) + fullSequence: number[]; + // Original prompt tokens (if any) + prompt?: number[]; + // Original target tokens + target?: number[]; + // Which tokens are masked out (-100 in labels) + ignored?: boolean[]; +} + +export interface ToyAutoregressiveBatch { + // Tensor outputs for training + tensors: [T, T]; // [input, target] + // Raw format for visualization/debugging + raw: CollatedRawSequence[]; + // Original uncollated samples + samples: ToySequence[]; +} + +export interface ToyBidirectionalBatch { + tensors: [T, T, T]; // [input, labels, attentionMask] + raw: CollatedRawSequence[]; + samples: ToySequence[]; +} + +export interface ToyEncoderDecoderBatch { + tensors: [T, T, T]; // [encoderInput, decoderInput, decoderTarget] + raw: CollatedRawSequence[]; + samples: ToySequence[]; +} + +export interface ToyDatasetLike extends IterableDataset { + readonly config: DatasetConfig; + readonly tokenizer: ToyTokenizer; + readonly generator: Random; + readonly bosId: number | null; + readonly eosId: number | null; + readonly maskId: number | null; + readonly hasCanonicalTargets?: boolean; + readonly disableValidation?: boolean; + baseSeed: number; + readonly datasetName: string; + generateSequence(): ToySequence; + generateSequenceAt(index: number): ToySequence; + computeMetrics( + completion: number[], + target: number[] | [number[], boolean[]] + ): ToyValidationMetrics; +} + +function hashString32(input: string): number { + // Simple 32-bit FNV-1a hash + let hash = 0x811c9dc5; + for (let i = 0; i < input.length; i++) { + hash ^= input.charCodeAt(i); + hash = Math.imul(hash, 0x01000193); + } + return hash >>> 0; +} + +function mix32(x: number): number { + x = Math.imul(x ^ (x >>> 16), 0x7feb352d); + x = Math.imul(x ^ (x >>> 15), 0x846ca68b); + x = x ^ (x >>> 16); + return x >>> 0; +} + +export function deriveToySampleSeed( + baseSeed: number, + datasetName: string, + index: number, + scope: string = 'sample' +): number { + const nameHash = hashString32(datasetName); + const scopeHash = hashString32(scope); + const mixed = mix32(baseSeed ^ mix32(nameHash) ^ mix32(index) ^ mix32(scopeHash)); + // Ensure non-zero seed for MT engine + return mixed >>> 0 || 0x1; +} + +abstract class ToyDataset + extends IterableDataset + implements ToyDatasetLike +{ + readonly config: DatasetConfig; + readonly tokenizer: ToyTokenizer; + generator: Random; + public readonly bosId: number | null; + public readonly eosId: number | null; + public readonly maskId: number | null; + public readonly hasCanonicalTargets: boolean = true; + baseSeed: number; + public readonly datasetName: string; + public cursor: number = 0; + + private readonly _originalGenerateSequence: () => ToySequence; + + constructor( + config: DatasetConfig, + generator: Random, + specialTokensConfig: SpecialTokensConfig, + datasetName: string, + baseSeed: number + ) { + super(); + this.config = config; + + // Build vocabulary with special tokens + const coreVocab = this.buildVocab(); + const specialTokens: string[] = []; + + if (specialTokensConfig.includeBos) specialTokens.push(BOS); + if (specialTokensConfig.includeEos) specialTokens.push(EOS); + if (specialTokensConfig.includeMask) specialTokens.push(MASK); + + const fullVocab = [...coreVocab, ...specialTokens]; + + this.tokenizer = this.tokenizerFromVocab(fullVocab); + this.bosId = specialTokensConfig.includeBos ? this.tokenizer.vocab[BOS] : null; + this.eosId = specialTokensConfig.includeEos ? this.tokenizer.vocab[EOS] : null; + this.maskId = specialTokensConfig.includeMask ? this.tokenizer.vocab[MASK] : null; + this.generator = generator; + this.datasetName = datasetName; + this.baseSeed = baseSeed; + + // Wrap subclass-defined generateSequence with index-based deterministic generation + this._originalGenerateSequence = this.generateSequence.bind(this); + // Replace generateSequence to advance cursor and delegate to generateSequenceAt + this.generateSequence = () => this.generateSequenceAt(this.cursor++); + } + + protected abstract buildVocab(): string[]; + public abstract generateSequence(): ToySequence; + public abstract computeMetrics( + completion: number[], + target: number[] | [number[], boolean[]] + ): ToyValidationMetrics; + + private tokenizerFromVocab(vocab: string[]): ToyTokenizer { + const vocabMap = vocab.reduce( + (acc, token, index) => { + acc[token] = index; + return acc; + }, + {} as Record + ); + const ids = vocab.reduce( + (acc, token, index) => { + acc[index] = token; + return acc; + }, + {} as Record + ); + const tokenizer: ToyTokenizer = { + vocab: vocabMap, + ids, + lastToken: vocab.length - 1, + decode: (tokens) => tokens.map((token) => ids[token] ?? '').join(' ') + }; + + return tokenizer; + } + + public getItem(_index: number): ToySequence { + return this.generateSequence(); + } + + public [Symbol.iterator](): Iterator { + return { + next: () => { + const sequence = this.generateSequence(); + return { value: sequence, done: false }; + } + }; + } + + /** + * Generate a deterministic sequence at an absolute index without affecting cursor or RNG state elsewhere. + */ + public generateSequenceAt(index: number): ToySequence { + const seed = deriveToySampleSeed(this.baseSeed, this.datasetName, index, 'toy-sample'); + const rng = new Random(MersenneTwister19937.seed(seed)); + const previous = this.generator; + try { + this.generator = rng; + const seq = this._originalGenerateSequence(); + return { ...seq, absoluteIndex: index }; + } finally { + this.generator = previous; + } + } +} + +export function tokenMatches(completion: number[], target: number[]): boolean[] { + if (completion.length !== target.length) return completion.map(() => false); + return completion.map((t, i) => t === target[i]); +} + +export function mustMatchAccuracy(completion: number[], target: number[]): number { + return completion.every((t, i) => t === target[i]) ? 1 : 0; +} + +export default ToyDataset; diff --git a/examples/piston-train-toy/src/lib/train/data/toy/dyck.ts b/examples/piston-train-toy/src/lib/train/data/toy/dyck.ts new file mode 100644 index 00000000..165ceb32 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/dyck.ts @@ -0,0 +1,182 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface DyckConfig { + sequenceLength: number; + order: number; + onlyTrainOnClosingBrackets: boolean; +} + +export const DYCK_CONFIG_METADATA = { + name: 'Dyck Language', + description: 'Generate balanced bracket sequences (Dyck words)', + supportsModelTypes: ['encoder'], + parameters: { + sequenceLength: { + name: 'Sequence Length', + description: 'Length of the sequence (must be even)', + type: 'number' as const, + min: 2, + max: 100, + step: 2, + default: 10 + }, + order: { + name: 'Order (Dyck-n)', + description: 'Number of bracket types (Dyck-n)', + type: 'number' as const, + min: 1, + max: 10, + default: 2 + }, + onlyTrainOnClosingBrackets: { + name: 'Only Train on Closing Brackets', + description: 'Only train on closing brackets', + type: 'boolean' as const, + default: false + } + } +} as const; + +export const DYCK_CONFIG_DEFAULTS: DyckConfig = { + sequenceLength: DYCK_CONFIG_METADATA.parameters.sequenceLength.default, + order: DYCK_CONFIG_METADATA.parameters.order.default, + onlyTrainOnClosingBrackets: DYCK_CONFIG_METADATA.parameters.onlyTrainOnClosingBrackets.default +}; + +export const DYCK_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len', + order: 'order', + onlyTrainOnClosingBrackets: 'only train `)`' +}; + +export class DyckDataset extends ToyDataset { + /** + * For Dyck dataset: + * - Generate bracket pairs based on order + * - For order > 3, use regular number notation (e.g., (2, [2, {2) + */ + protected buildVocab(): string[] { + const { order } = this.config; + const vocab: string[] = []; + + // Base bracket types + const baseBrackets = ['()', '[]', '{}']; + + // Generate bracket pairs based on order + for (let i = 0; i < order; i++) { + const baseIndex = i % 3; + const level = Math.floor(i / 3); + + let openBracket = baseBrackets[baseIndex][0]; + let closeBracket = baseBrackets[baseIndex][1]; + + // Add number notation for levels > 0 + if (order > 3) { + const numberSuffix = (level + 1).toString(); + openBracket += numberSuffix; + closeBracket += numberSuffix; + } + + vocab.push(openBracket); + vocab.push(closeBracket); + } + + return vocab; + } + + /** + * Generate a random Dyck word (balanced brackets) of exact length. + * The algorithm uses a stack to track unclosed openings and ensures + * the result is properly balanced. + */ + public generateSequence(): ToySequence { + const { sequenceLength, order, onlyTrainOnClosingBrackets } = this.config; + + // Ensure length is even + const length = sequenceLength % 2 === 0 ? sequenceLength : sequenceLength - 1; + + // Generate bracket pairs + const pairs: [string, string][] = []; + const vocab = this.buildVocab(); + + for (let i = 0; i < order; i++) { + const openBracket = vocab[i * 2]; + const closeBracket = vocab[i * 2 + 1]; + pairs.push([openBracket, closeBracket]); + } + + let openLeft = length / 2; // how many openings remain + let closeLeft = length / 2; // how many closings remain + const stack: [string, string][] = []; // track unclosed openings + const result: string[] = []; // output characters + + while (openLeft > 0 || closeLeft > 0) { + if (openLeft === 0) { + // must close + const pair = stack.pop()!; + result.push(pair[1]); + closeLeft -= 1; + } else if (closeLeft === 0) { + // must open + const pair = pairs[this.generator.integer(0, pairs.length - 1)]; + stack.push(pair); + result.push(pair[0]); + openLeft -= 1; + } else { + // we can choose to open or close (if stack not empty) + if (stack.length > 0 && this.generator.real(0, 1) < 0.5) { + // close + const pair = stack.pop()!; + result.push(pair[1]); + closeLeft -= 1; + } else { + // open + const pair = pairs[this.generator.integer(0, pairs.length - 1)]; + stack.push(pair); + result.push(pair[0]); + openLeft -= 1; + } + } + } + + // Convert to tokens + const tokens = result.map((char) => this.tokenizer.vocab[char]); + + // Only apply mask if onlyTrainOnClosingBrackets is true + if (onlyTrainOnClosingBrackets) { + // Create a set of closing bracket characters for easy lookup + const closingBrackets = new Set(); + for (let i = 0; i < order; i++) { + const closeBracket = vocab[i * 2 + 1]; // Closing brackets are at odd indices + closingBrackets.add(closeBracket); + } + + // Create boolean mask - true for closing brackets, false for opening brackets + const mask = result.map((char) => closingBrackets.has(char)); + + return { target: tokens, mask }; + } else { + // Return tokens without masking + return { target: tokens }; + } + } + + public computeMetrics( + completion: number[], + target: number[] | [number[], boolean[]] + ): ToyValidationMetrics { + const targetTokens = + Array.isArray(target) && + target.length === 2 && + Array.isArray(target[0]) && + Array.isArray(target[1]) + ? target[0] + : (target as number[]); + return { + matches: tokenMatches(completion, targetTokens), + accuracy: mustMatchAccuracy(completion, targetTokens) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/elman.ts b/examples/piston-train-toy/src/lib/train/data/toy/elman.ts new file mode 100644 index 00000000..4f18c3e7 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/elman.ts @@ -0,0 +1,136 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { type ToySequence } from './dataset'; + +export const ELMAN_CONFIG_METADATA = { + name: 'Elman Grammar', + description: 'Generate simplified sentences based on Elman (1990) grammar.', + citations: { + entries: [ + { + name: 'Elman, 1990', + url: 'https://onlinelibrary.wiley.com/doi/abs/10.1207/s15516709cog1402_1' + } + ] + }, + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: {} +} as const; + +export const ELMAN_CONFIG_DEFAULTS = {} as const; +export type ElmanConfig = object; + +export class ElmanDataset extends ToyDataset { + public readonly hasCanonicalTargets = false; + // Compact placeholder dictionary + private static categories: Record = { + NOUN_HUM: ['man', 'woman', 'boy', 'girl'], + NOUN_ANIM: ['cat', 'mouse', 'dog'], + NOUN_INANIM: ['book', 'rock', 'pencil'], + NOUN_AGRESS: ['dragon', 'monster'], + NOUN_FRAG: ['glass', 'plate'], + NOUN_FOOD: ['apple', 'banana', 'orange', 'pie', 'pizza', 'sandwich'], + VERB_INTRAN: ['think', 'sleep'], + VERB_TRAN: ['see', 'chase'], + VERB_AGPAT: ['move', 'break'], + VERB_PERCEPT: ['smell', 'see'], + VERB_DESTROY: ['break', 'smash'], + VERB_EAT: ['eat', 'consume'] + }; + + // Precompiled templates with exactly 3 positions; null means PAD + private static templates: (string[] | null)[][] = [ + ['NOUN_HUM', 'VERB_EAT', 'NOUN_FOOD'], + ['NOUN_HUM', 'VERB_PERCEPT', 'NOUN_INANIM'], + ['NOUN_HUM', 'VERB_DESTROY', 'NOUN_FRAG'], + ['NOUN_HUM', 'VERB_INTRAN', 'PAD'], + ['NOUN_HUM', 'VERB_TRAN', 'NOUN_HUM'], + ['NOUN_HUM', 'VERB_AGPAT', 'NOUN_INANIM'], + ['NOUN_HUM', 'VERB_AGPAT', 'PAD'], + ['NOUN_ANIM', 'VERB_EAT', 'NOUN_FOOD'], + ['NOUN_ANIM', 'VERB_TRAN', 'NOUN_ANIM'], + ['NOUN_ANIM', 'VERB_AGPAT', 'NOUN_INANIM'], + ['NOUN_ANIM', 'VERB_AGPAT', 'PAD'], + ['NOUN_INANIM', 'VERB_AGPAT', 'PAD'], + ['NOUN_AGRESS', 'VERB_DESTROY', 'NOUN_FRAG'], + ['NOUN_AGRESS', 'VERB_EAT', 'NOUN_HUM'], + ['NOUN_AGRESS', 'VERB_EAT', 'NOUN_ANIM'], + ['NOUN_AGRESS', 'VERB_EAT', 'NOUN_FOOD'] + ].map((tpl) => tpl.map((slot) => (slot === 'PAD' ? null : ElmanDataset.categories[slot]))); + + protected buildVocab(): string[] { + // Union of all words used in generation, plus 'who' + const vocabSet = new Set(); + // console.log(ElmanDataset.categories); + for (const list of Object.values(ElmanDataset.categories)) { + for (const w of list) { + vocabSet.add(w); + } + } + vocabSet.add('PAD'); + return Array.from(vocabSet); + } + + public generateSequence(): ToySequence { + // Choose a precompiled template + const template = + ElmanDataset.templates[this.generator.integer(0, ElmanDataset.templates.length - 1)]; + + // Replace placeholders with dictionary lookup (null => PAD) + const sentenceWords: string[] = template.map((slot) => { + if (slot === null) return 'PAD'; + const idx = this.generator.integer(0, slot.length - 1); + return slot[idx] as string; + }); + + // Map words to token IDs + const target = sentenceWords.map((w) => this.tokenizer.vocab[w]); + return { target }; + } + + public computeMetrics(completion: number[], _target: number[]): ToyValidationMetrics { + // Remove EOS if present at the end + let seq = completion.slice(); + if (this.eosId !== null && seq.length > 0 && seq[seq.length - 1] === this.eosId) { + seq = seq.slice(0, -1); + } + + // Decode to words for template matching + const words = seq.map((id) => this.tokenizer.ids[id] ?? ''); + + // Best-prefix match across templates (only first n tokens, in order) + let bestPrefix = 0; + for (const tpl of ElmanDataset.templates) { + let prefix = 0; + for (let i = 0; i < Math.min(3, words.length); i++) { + const slot = tpl[i]; + const w = words[i]; + const ok = slot === null ? w === 'PAD' : slot.includes(w); + if (!ok) break; + prefix++; + if (prefix === 3) break; // early exit for full match + } + if (prefix > bestPrefix) { + bestPrefix = prefix; + if (bestPrefix === 3) break; // early exit if any template fully matches + } + } + + // Per-token matches for up to 3 tokens + const matches: boolean[] = []; + for (let i = 0; i < Math.min(3, words.length); i++) { + matches.push(i < bestPrefix); + } + + const length_correct = seq.length === 3; + const matched_template = bestPrefix === 3; + const valid_statement = matched_template; + + return { + length_correct, + matched_template, + valid_statement, + matches + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/index.ts b/examples/piston-train-toy/src/lib/train/data/toy/index.ts new file mode 100644 index 00000000..cfc2ef8b --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/index.ts @@ -0,0 +1,139 @@ +import type { Config } from '$lib/workspace/config'; +import type { Random } from 'random-js'; + +import type ToyDataset from './dataset'; +import type { SpecialTokensConfig } from './dataset'; + +import { type AdditionConfig, AdditionDataset } from './addition'; +import { type CopyMemoryConfig, CopyMemoryDataset } from './copyMemory'; +import { type DyckConfig, DyckDataset } from './dyck'; +import { type ElmanConfig, ElmanDataset } from './elman'; +import { type MarkedAdditionConfig, MarkedAdditionDataset } from './markedAddition'; +import { type ModularAdditionConfig, ModularAdditionDataset } from './modularAddition'; +import { type ParityConfig, ParityDataset } from './parity'; +import { type RandomConfig, RandomDataset } from './random'; +import { type RepeatConfig, RepeatDataset } from './repeat'; +import { type ReverseConfig, ReverseDataset } from './reverse'; +import { type SlapjackConfig, SlapjackDataset } from './slapjack'; +import { type SortConfig, SortDataset } from './sort'; +import { type TemporalOrderConfig, TemporalOrderDataset } from './temporalOrder'; +import { type TwoSumConfig, TwoSumDataset } from './twoSum'; +import { type ZerosConfig, ZerosDataset } from './zeros'; +export { type AdditionConfig, AdditionDataset } from './addition'; +export { type CopyMemoryConfig, CopyMemoryDataset } from './copyMemory'; +export { mustMatchAccuracy, tokenMatches, default as ToyDataset } from './dataset'; +export { type DyckConfig, DyckDataset } from './dyck'; +export { type ElmanConfig, ElmanDataset } from './elman'; +export { type MarkedAdditionConfig, MarkedAdditionDataset } from './markedAddition'; +export { type ModularAdditionConfig, ModularAdditionDataset } from './modularAddition'; +export { type RepeatConfig, RepeatDataset } from './repeat'; +export { type ReverseConfig, ReverseDataset } from './reverse'; +export { type SlapjackConfig, SlapjackDataset } from './slapjack'; +export { type SortConfig, SortDataset } from './sort'; +export { type TemporalOrderConfig, TemporalOrderDataset } from './temporalOrder'; +export { type TwoSumConfig, TwoSumDataset } from './twoSum'; + +export interface DatasetConfigs { + 'two-sum': TwoSumConfig; + sort: SortConfig; + repeat: RepeatConfig; + reverse: ReverseConfig; + addition: AdditionConfig; + 'modular-addition': ModularAdditionConfig; + zeros: ZerosConfig; + slapjack: SlapjackConfig; + dyck: DyckConfig; + parity: ParityConfig; + random: RandomConfig; + 'marked-addition': MarkedAdditionConfig; + 'copy-memory': CopyMemoryConfig; + 'temporal-order': TemporalOrderConfig; + elman: ElmanConfig; +} + +export function buildToyDataset(config: Config, generator: Random): ToyDataset { + const datasetName = config.data.dataset; + // Derive a baseSeed for deterministic per-sample generation; if the run is seeded, + // the provided generator will be seeded deterministically. If not, we still persist + // baseSeed in checkpoints for resumption. + const baseSeed = generator.int32() >>> 0; + + const isEncoderOnly = config.model.topology === 'encoder'; + const specialTokensConfig: SpecialTokensConfig = { + includeBos: !isEncoderOnly, + includeEos: !isEncoderOnly && config.data.specialTokens.includeEos, + includeMask: isEncoderOnly + }; + + if (datasetName === 'sort') { + const sortConfig = config.data.datasets.sort; + return new SortDataset(sortConfig, generator, specialTokensConfig, datasetName, baseSeed); + } else if (datasetName === 'repeat') { + const repeatConfig = config.data.datasets.repeat; + return new RepeatDataset(repeatConfig, generator, specialTokensConfig, datasetName, baseSeed); + } else if (datasetName === 'reverse') { + const reverseConfig = config.data.datasets.reverse; + return new ReverseDataset(reverseConfig, generator, specialTokensConfig, datasetName, baseSeed); + } else if (datasetName === 'addition') { + const additionConfig = config.data.datasets.addition; + return new AdditionDataset( + additionConfig, + generator, + specialTokensConfig, + datasetName, + baseSeed + ); + } else if (datasetName === 'modular-addition') { + const modularAdditionConfig = config.data.datasets['modular-addition']; + return new ModularAdditionDataset( + modularAdditionConfig, + generator, + specialTokensConfig, + datasetName, + baseSeed + ); + } else if (datasetName === 'two-sum') { + const twoSumConfig = config.data.datasets['two-sum']; + return new TwoSumDataset(twoSumConfig, generator, specialTokensConfig, datasetName, baseSeed); + } else if (datasetName === 'zeros') { + const zerosConfig = config.data.datasets.zeros; + return new ZerosDataset(zerosConfig, generator, specialTokensConfig, datasetName, baseSeed); + } else if (datasetName === 'slapjack') { + const slapjackConfig = config.data.datasets.slapjack; + return new SlapjackDataset( + slapjackConfig, + generator, + specialTokensConfig, + datasetName, + baseSeed + ); + } else if (datasetName === 'dyck') { + const dyckConfig = config.data.datasets.dyck; + return new DyckDataset(dyckConfig, generator, specialTokensConfig, datasetName, baseSeed); + } else if (datasetName === 'parity') { + const parityConfig = config.data.datasets.parity; + return new ParityDataset(parityConfig, generator, specialTokensConfig, datasetName, baseSeed); + } else if (datasetName === 'random') { + const randomConfig = config.data.datasets.random; + return new RandomDataset(randomConfig, generator, specialTokensConfig, datasetName, baseSeed); + } else if (datasetName === 'marked-addition') { + const markedAdditionConfig = config.data.datasets['marked-addition']; + return new MarkedAdditionDataset( + markedAdditionConfig, + generator, + specialTokensConfig, + datasetName, + baseSeed + ); + } else if (datasetName === 'temporal-order') { + const cfg = config.data.datasets['temporal-order']; + return new TemporalOrderDataset(cfg, generator, specialTokensConfig, datasetName, baseSeed); + } else if (datasetName === 'copy-memory') { + const cfg = config.data.datasets['copy-memory']; + return new CopyMemoryDataset(cfg, generator, specialTokensConfig, datasetName, baseSeed); + } else if (datasetName === 'elman') { + return new ElmanDataset(null, generator, specialTokensConfig, datasetName, baseSeed); + } else { + throw new Error(`Unknown dataset: ${datasetName}`); + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/markedAddition.ts b/examples/piston-train-toy/src/lib/train/data/toy/markedAddition.ts new file mode 100644 index 00000000..26938e0b --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/markedAddition.ts @@ -0,0 +1,143 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface MarkedAdditionConfig { + sequenceLength: number; // number of (value, marker) pairs + maxInputValue: number; // largest value sampled per element in the sequence + maxSumValue: number; // largest representable sum (numeric vocab range) + includeEqualsToken: boolean; // include '=' delimiter at end of prompt +} + +export const MARKED_ADDITION_CONFIG_METADATA = { + name: 'Marked Addition', + description: + 'Sum values whose associated marker equals 1. Not real-valued, unlike the original task.', + citations: { + entries: [ + { + name: 'Hochreiter & Schmidhuber, 1997', + url: 'https://ieeexplore.ieee.org/abstract/document/6795963' + } + ] + }, + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + sequenceLength: { + name: 'Sequence Length', + description: 'Number of (value, marker) pairs', + type: 'number' as const, + min: 2, + max: 64, + step: 1, + default: 8 + }, + maxSumValue: { + name: 'Max Sum Value', + description: 'Largest representable sum (controls numeric vocab range)', + type: 'number' as const, + min: 1, + max: 10000, + step: 1, + default: 100 + }, + maxInputValue: { + name: 'Max Input Value', + description: 'Largest value sampled per element in the sequence', + type: 'number' as const, + min: 1, + max: 1000, + step: 1, + default: 10 + }, + includeEqualsToken: { + name: 'Include Equals Token', + description: 'Append an = token after the sequence', + type: 'boolean' as const, + default: true + } + } +} as const; + +export const MARKED_ADDITION_CONFIG_DEFAULTS: MarkedAdditionConfig = { + sequenceLength: MARKED_ADDITION_CONFIG_METADATA.parameters.sequenceLength.default, + maxInputValue: MARKED_ADDITION_CONFIG_METADATA.parameters.maxInputValue.default, + maxSumValue: MARKED_ADDITION_CONFIG_METADATA.parameters.maxSumValue.default, + includeEqualsToken: MARKED_ADDITION_CONFIG_METADATA.parameters.includeEqualsToken.default +}; + +export const MARKED_ADDITION_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len', + maxInputValue: 'max in num', + maxSumValue: 'max sum', + includeEqualsToken: 'include =' +}; + +export class MarkedAdditionDataset extends ToyDataset { + /** + * Vocab design: + * - values are discretized based on precision: for precision p, there are (10^p + 1) buckets + * 0.0..1.0. + * We emit tokens V0..V{numBuckets-1} representing these buckets. + * - markers are tokens 'M0' and 'M1'. + * - optionally BOS/EOS/mask are appended by ToyDataset constructor based on specialTokensConfig. + */ + protected buildVocab(): string[] { + const { maxSumValue, includeEqualsToken } = this.config; + const padWidth = maxSumValue.toString().length; + const vocab = Array.from({ length: maxSumValue + 1 }, (_, i) => + i.toString().padStart(padWidth, '0') + ); + vocab.push('M0', 'M1'); + if (includeEqualsToken) { + vocab.push('='); + } + return vocab; + } + + /** + * Generate a sequence of (value, marker) pairs and target the sum of values with marker==1. + * Representation: + * - Discretize each value in [0,1] to bucket index b in [0, buckets-1]. Token is Vb. + * - Marker token is M0 or M1. + * - Prompt layout: Vb0 Mx0 Vb1 Mx1 ... Vb{n-1} Mx{n-1} '=' + * - Target: a single token VbSum where bSum is the discretized sum clamped to [0, 1]. + */ + public generateSequence(): ToySequence { + const { sequenceLength, maxInputValue, maxSumValue, includeEqualsToken } = this.config; + + const prompt: number[] = []; + let sum = 0; + const padWidth = maxSumValue.toString().length; + + for (let i = 0; i < sequenceLength; i++) { + const value = this.generator.integer(0, maxInputValue); + const marker = this.generator.integer(0, 1); + + prompt.push(value); + prompt.push(this.tokenizer.vocab[`M${marker}`]); + + if (marker === 1) sum += value; + } + + if (includeEqualsToken) { + prompt.push(this.tokenizer.vocab['=']); + } + + if (sum > maxSumValue) { + sum = maxSumValue; // clamp to representable range + } + + const targetTokenStr = sum.toString().padStart(padWidth, '0'); + const target = [this.tokenizer.vocab[targetTokenStr]]; + + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/modularAddition.ts b/examples/piston-train-toy/src/lib/train/data/toy/modularAddition.ts new file mode 100644 index 00000000..faf9295d --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/modularAddition.ts @@ -0,0 +1,110 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface ModularAdditionConfig { + maxNumber: number; + modulo: number; + includeExpressionTokens: boolean; +} + +export const MODULAR_ADDITION_CONFIG_METADATA = { + name: 'Modular Addition', + description: 'Add two numbers with modulo', + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + maxNumber: { + name: 'Max Number', + type: 'number' as const, + min: 5, + max: 1000, + default: 500 + }, + modulo: { + name: 'Modulo', + type: 'number' as const, + min: 2, + max: 113, + default: 113 + }, + includeExpressionTokens: { + name: 'Include Expression Tokens (+, =)', + type: 'boolean' as const, + default: true + } + } +} as const; + +export const MODULAR_ADDITION_CONFIG_DEFAULTS: ModularAdditionConfig = { + maxNumber: MODULAR_ADDITION_CONFIG_METADATA.parameters.maxNumber.default, + modulo: MODULAR_ADDITION_CONFIG_METADATA.parameters.modulo.default, + includeExpressionTokens: + MODULAR_ADDITION_CONFIG_METADATA.parameters.includeExpressionTokens.default +}; + +export const MODULAR_ADDITION_SHORT_DESCRIPTIONS = { + maxNumber: 'max num', + modulo: 'mod', + includeExpressionTokens: 'incl expr toks' +}; + +export class ModularAdditionDataset extends ToyDataset { + /** + * Tokenizer for modular addition. + * - tokens 0 .. maxNum represent the numbers 0 … maxNum + * (When converting to a string, token id i <= maxNum becomes `<${i}>` with zero-padding.) + * - token maxNum+1 -> "+" + * - token maxNum+2 -> "=" + */ + protected buildVocab(): string[] { + const { maxNumber, includeExpressionTokens } = this.config; + const vocab: string[] = []; + const padWidth = maxNumber.toString().length; + // 1. Add tokens for numbers + vocab.push( + ...Array.from({ length: maxNumber + 1 }, (_, i) => i.toString().padStart(padWidth, '0')) + ); + // 2. Add tokens for + and = + if (includeExpressionTokens) { + vocab.push('+', '='); + } + return vocab; + } + + /** + * Modular Addition: generate two numbers (in [0, maxNum)) and compute (num1+num2)%maxNum. + * Example: <15>+<03>=<05> (if maxNum is 113 and 15+3=18, 18%113=18, but if 15+100=115, 115%113=2) + */ + public generateSequence(): ToySequence { + const { maxNumber, modulo, includeExpressionTokens } = this.config; + + // 1. Pick numbers and compute sum. Include maxNumber in the range. + const sum = this.generator.integer(0, maxNumber); + const num1 = sum > 0 ? this.generator.integer(0, sum - 1) : 0; + const num2 = sum - num1; + const sumModulo = sum % modulo; + + // 2. Convert to token IDs: + // - 0..maxNum-1 are numbers + // - maxNum is '+' + // - maxNum+1 is '=' + let prompt; + if (includeExpressionTokens) { + prompt = [num1, maxNumber + 1, num2, maxNumber + 2]; + } else { + prompt = [num1, num2]; + } + + // 3. Target is a single token corresponding to the modular sum + const target = [sumModulo]; + + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/parity.ts b/examples/piston-train-toy/src/lib/train/data/toy/parity.ts new file mode 100644 index 00000000..ceeda076 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/parity.ts @@ -0,0 +1,110 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface ParityConfig { + sequenceLength: number; + includeColon: boolean; +} + +export const PARITY_CONFIG_METADATA = { + name: 'Parity', + description: 'Determine if a bit string has even or odd number of 1s (parity)', + citations: { + entries: [ + { name: 'Minsky & Papert, 1969', url: 'https://psycnet.apa.org/record/1969-35017-000' } + ] + }, + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + sequenceLength: { + name: 'Sequence Length', + description: 'Number of bits in the string', + type: 'number' as const, + min: 1, + max: 20, + default: 4 + }, + includeColon: { + name: 'Include Colon', + type: 'boolean' as const, + default: true + } + } +} as const; + +export const PARITY_CONFIG_DEFAULTS: ParityConfig = { + sequenceLength: PARITY_CONFIG_METADATA.parameters.sequenceLength.default, + includeColon: PARITY_CONFIG_METADATA.parameters.includeColon.default +}; + +export const PARITY_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len', + includeColon: 'include :' +}; + +export class ParityDataset extends ToyDataset { + /** + * For parity dataset: + * - token 0 -> '0' + * - token 1 -> '1' + * - token 2 -> ':' (if includeColon is true) + * - token 3 -> 'even' + * - token 4 -> 'odd' + */ + protected buildVocab(): string[] { + const { includeColon } = this.config; + const vocab: string[] = ['0', '1']; + + if (includeColon) { + vocab.push(':'); + } + + vocab.push('even', 'odd'); + return vocab; + } + + /** + * Parity: generate a bit string and determine its parity: + * - prompt: the bit string with optional colon at the end + * - target: "even" if even number of 1s, "odd" if odd number of 1s + * Example: 10110:odd (3 ones = odd parity) + */ + public generateSequence(): ToySequence { + const { sequenceLength, includeColon } = this.config; + const bits = Array.from({ length: sequenceLength }, () => this.generator.integer(0, 1)); + + const prompt: number[] = []; + + // Add the bit string to the prompt + for (const bit of bits) { + prompt.push(bit); + } + + // Add colon if enabled + if (includeColon) { + const colon = this.tokenizer.vocab[':']; + prompt.push(colon); + } + + // Calculate parity (count number of 1s) + const onesCount = bits.filter((bit) => bit === 1).length; + const isEven = onesCount % 2 === 0; + + const target: number[] = []; + if (isEven) { + target.push(this.tokenizer.vocab['even']); + } else { + target.push(this.tokenizer.vocab['odd']); + } + + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/random.ts b/examples/piston-train-toy/src/lib/train/data/toy/random.ts new file mode 100644 index 00000000..cd9abe96 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/random.ts @@ -0,0 +1,97 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface RandomConfig { + sequenceLength: number; // length of the prompt + responseLength: number; // length of the response + vocabSize: number; // number of non-special tokens +} + +export const RANDOM_CONFIG_METADATA = { + name: 'Random Token Prediction', + description: + 'Prompt is random tokens and target is an independent random token. This is a good test case; there is no learnable mapping, so any model should struggle to learn this task.', + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + sequenceLength: { + name: 'Prompt Length', + description: 'Number of tokens in the prompt', + type: 'number' as const, + min: 0, + max: 512, + step: 1, + default: 5 + }, + responseLength: { + name: 'Response Length', + description: 'Number of tokens in the response', + type: 'number' as const, + min: 0, + max: 512, + step: 1, + default: 1 + }, + vocabSize: { + name: 'Vocab Size', + description: 'Number of distinct tokens (excluding special tokens)', + type: 'number' as const, + min: 2, + max: 1000, + step: 1, + default: 10 + } + } +} as const; + +export const RANDOM_CONFIG_DEFAULTS: RandomConfig = { + sequenceLength: RANDOM_CONFIG_METADATA.parameters.sequenceLength.default, + responseLength: RANDOM_CONFIG_METADATA.parameters.responseLength.default, + vocabSize: RANDOM_CONFIG_METADATA.parameters.vocabSize.default +}; + +export const RANDOM_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len', + responseLength: 'resp len', + vocabSize: 'vocab' +}; + +export class RandomDataset extends ToyDataset { + /** + * For the random dataset: + * - Create token strings V0..V{vocabSize-1} + */ + protected buildVocab(): string[] { + const { vocabSize } = this.config; + const vocab: string[] = []; + for (let i = 0; i < vocabSize; i++) { + vocab.push(`V${i}`); + } + return vocab; + } + + /** + * Generate a random prompt of length sequenceLength, and a single independent random target token. + * The target is sampled independently of the prompt, making the task unlearnable beyond chance. + */ + public generateSequence(): ToySequence { + const { sequenceLength, responseLength, vocabSize } = this.config; + const prompt: number[] = []; + for (let i = 0; i < sequenceLength; i++) { + prompt.push(this.generator.integer(0, vocabSize - 1)); + } + // placing predictable before unpredictable yields plausible loss + const target: number[] = []; + for (let i = 0; i < responseLength; i++) { + target.push(this.generator.integer(0, vocabSize - 1)); + } + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/repeat.ts b/examples/piston-train-toy/src/lib/train/data/toy/repeat.ts new file mode 100644 index 00000000..6b84ec76 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/repeat.ts @@ -0,0 +1,126 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface RepeatConfig { + sequenceLength: number; + maxNumber: number; + includeCommas: boolean; + includeColon: boolean; +} + +export const REPEAT_CONFIG_METADATA = { + name: 'Repeat', + description: 'Repeat a sequence of characters', + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + sequenceLength: { + name: 'Sequence Length', + description: 'Number of items to repeat', + type: 'number' as const, + min: 1, + max: 10, + default: 3 + }, + maxNumber: { + name: 'Max Alphabet Character', + description: 'Maximum alphabet character', + type: 'number' as const, + min: 5, + max: 26, + default: 5 + }, + includeCommas: { + name: 'Include Commas', + type: 'boolean' as const, + default: false + }, + includeColon: { + name: 'Include Colon', + type: 'boolean' as const, + default: true + } + } +} as const; + +export const REPEAT_CONFIG_DEFAULTS: RepeatConfig = { + sequenceLength: REPEAT_CONFIG_METADATA.parameters.sequenceLength.default, + maxNumber: REPEAT_CONFIG_METADATA.parameters.maxNumber.default, + includeCommas: REPEAT_CONFIG_METADATA.parameters.includeCommas.default, + includeColon: REPEAT_CONFIG_METADATA.parameters.includeColon.default +}; + +export const REPEAT_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len', + maxNumber: 'max num', + includeCommas: 'incl `,`', + includeColon: 'incl `:`' +}; + +export class RepeatDataset extends ToyDataset { + /** + * For repeat dataset: + * - tokens 0 .. maxNum represent the alphabet characters corresponding to the numbers 0 .. maxNum. + * - token maxNum+1 -> ':' + * - token maxNum+2 -> ',' + */ + protected buildVocab(): string[] { + const { maxNumber, includeColon, includeCommas } = this.config; + const vocab: string[] = []; + const aCharCode = 'A'.charCodeAt(0); + for (let n = 0; n < maxNumber; n++) { + const token = String.fromCharCode(aCharCode + n); + vocab.push(token); + } + if (includeColon) { + vocab.push(':'); + } + if (includeCommas) { + vocab.push(','); + } + return vocab; + } + + /** + * Repeat: generate a sequence of characters and return: + * - prompt: the sequence with commas (token 1) between numbers and a colon (token 0) at the end. + * - target: the same sequence with commas between numbers. + * Example: BBCA:BBCA + */ + public generateSequence(): ToySequence { + const { sequenceLength, maxNumber, includeCommas, includeColon } = this.config; + const nums = Array.from({ length: sequenceLength }, () => + this.generator.integer(0, maxNumber - 1) + ); + const prompt: number[] = []; + + const comma = this.tokenizer.vocab[',']; + const colon = this.tokenizer.vocab[':']; + + for (let i = 0; i < nums.length; i++) { + prompt.push(nums[i]); + if (includeCommas && i < nums.length - 1) { + prompt.push(comma); + } + } + if (includeColon) { + prompt.push(colon); + } + + const target: number[] = []; + for (let i = 0; i < nums.length; i++) { + target.push(nums[i]); + if (includeCommas && i < nums.length - 1) { + target.push(comma); + } + } + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/reverse.ts b/examples/piston-train-toy/src/lib/train/data/toy/reverse.ts new file mode 100644 index 00000000..0c81df77 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/reverse.ts @@ -0,0 +1,127 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface ReverseConfig { + sequenceLength: number; + maxNumber: number; + includeCommas: boolean; + includeColon: boolean; +} + +export const REVERSE_CONFIG_METADATA = { + name: 'Reverse', + description: 'Reverse a sequence of characters', + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + sequenceLength: { + name: 'Sequence Length', + description: 'Number of items to reverse', + type: 'number' as const, + min: 1, + max: 10, + default: 3 + }, + maxNumber: { + name: 'Max Alphabet Character', + description: 'Maximum alphabet character', + type: 'number' as const, + min: 5, + max: 26, + default: 5 + }, + includeCommas: { + name: 'Include Commas', + type: 'boolean' as const, + default: false + }, + includeColon: { + name: 'Include Colon', + type: 'boolean' as const, + default: true + } + } +} as const; + +export const REVERSE_CONFIG_DEFAULTS: ReverseConfig = { + sequenceLength: REVERSE_CONFIG_METADATA.parameters.sequenceLength.default, + maxNumber: REVERSE_CONFIG_METADATA.parameters.maxNumber.default, + includeCommas: REVERSE_CONFIG_METADATA.parameters.includeCommas.default, + includeColon: REVERSE_CONFIG_METADATA.parameters.includeColon.default +}; + +export const REVERSE_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len', + maxNumber: 'max num', + includeCommas: 'incl `,`', + includeColon: 'incl `:`' +}; + +export class ReverseDataset extends ToyDataset { + /** + * For reverse dataset: + * - tokens 0 .. maxNum represent the alphabet characters corresponding to the numbers 0 .. maxNum. + * - token maxNum+1 -> ':' + * - token maxNum+2 -> ',' + */ + protected buildVocab(): string[] { + const { maxNumber, includeColon, includeCommas } = this.config; + const vocab: string[] = []; + const aCharCode = 'A'.charCodeAt(0); + for (let n = 0; n < maxNumber; n++) { + const token = String.fromCharCode(aCharCode + n); + vocab.push(token); + } + if (includeColon) { + vocab.push(':'); + } + if (includeCommas) { + vocab.push(','); + } + return vocab; + } + + /** + * Reverse: generate a sequence of characters and return: + * - prompt: the sequence with commas (token 1) between numbers and a colon (token 0) at the end. + * - target: the reversed sequence with commas between numbers. + * Example: BBCA:ACBB + */ + public generateSequence(): ToySequence { + const { sequenceLength, maxNumber, includeCommas, includeColon } = this.config; + const nums = Array.from({ length: sequenceLength }, () => + this.generator.integer(0, maxNumber - 1) + ); + const prompt: number[] = []; + + const comma = this.tokenizer.vocab[',']; + const colon = this.tokenizer.vocab[':']; + + for (let i = 0; i < nums.length; i++) { + prompt.push(nums[i]); + if (includeCommas && i < nums.length - 1) { + prompt.push(comma); + } + } + if (includeColon) { + prompt.push(colon); + } + + const target: number[] = []; + const reversedNums = nums.reverse(); + for (let i = 0; i < reversedNums.length; i++) { + target.push(reversedNums[i]); + if (includeCommas && i < reversedNums.length - 1) { + target.push(comma); + } + } + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/slapjack.ts b/examples/piston-train-toy/src/lib/train/data/toy/slapjack.ts new file mode 100644 index 00000000..c7e320a1 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/slapjack.ts @@ -0,0 +1,129 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface SlapjackConfig { + sequenceLength: number; + slapOnDoubles: boolean; + slapOnSandwiches: boolean; + includeColon: boolean; + // onlyTrainOnSlaps: boolean; +} + +export const SLAPJACK_CONFIG_METADATA = { + name: 'Slapjack', + description: + '"Slap" (🖐️) when the card is a Jack, a double (same card twice in a row), or a sandwich (same card with one card in between). Otherwise, take no action (❌).', + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + sequenceLength: { + name: 'Sequence Length', + description: 'Total length of the sequence', + type: 'number' as const, + min: 5, + max: 200, + default: 5 + }, + slapOnDoubles: { + name: 'Slap on Doubles', + description: 'Slap on doubles (same card twice in a row)', + type: 'boolean' as const, + default: true + }, + slapOnSandwiches: { + name: 'Slap on Sandwiches', + description: 'Slap on sandwiches (same card with one card in between)', + type: 'boolean' as const, + default: true + }, + includeColon: { + name: 'Include Colon', + type: 'boolean' as const, + default: true + } + // onlyTrainOnSlaps: { + // name: 'Only Train on Slaps', + // description: 'Only train on slaps (not on cards)', + // type: 'boolean' as const, + // default: true + // } + } +} as const; + +export const SLAPJACK_CONFIG_DEFAULTS: SlapjackConfig = { + sequenceLength: SLAPJACK_CONFIG_METADATA.parameters.sequenceLength.default, + slapOnDoubles: SLAPJACK_CONFIG_METADATA.parameters.slapOnDoubles.default, + slapOnSandwiches: SLAPJACK_CONFIG_METADATA.parameters.slapOnSandwiches.default, + includeColon: SLAPJACK_CONFIG_METADATA.parameters.includeColon.default + // onlyTrainOnSlaps: SLAPJACK_CONFIG_METADATA.parameters.onlyTrainOnSlaps.default, +}; + +export const SLAPJACK_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len', + slapOnDoubles: 'slap doubles', + slapOnSandwiches: 'slap sandw', + includeColon: 'incl `:`' +}; + +const CARDS = 'A23456789JQK'; + +export class SlapjackDataset extends ToyDataset { + /** + * Vocabulary contains card tokens and action tokens (🖐️ slap, ❌ no slap). + */ + protected buildVocab(): string[] { + const { includeColon } = this.config; + const vocab = [...CARDS.split('')]; + if (includeColon) vocab.push(':'); + vocab.push('🖐️', '❌'); + return vocab; + } + + /** + * Slapjack: + * - Input (prompt): sequence of cards of length sequenceLength + * - Target: action at each position (🖐️ if slap, ❌ otherwise) + * Slap rules: + * - Slap on Jack (J) + * - Slap on doubles (same card twice in a row) if enabled + * - Slap on sandwiches (same card with one card in between) if enabled + */ + public generateSequence(): ToySequence { + const { sequenceLength, slapOnDoubles, slapOnSandwiches, includeColon } = this.config; + + // Build card sequence + const cardSeq = Array.from( + { length: sequenceLength }, + () => CARDS[this.generator.integer(0, CARDS.length - 1)] + ); + + // Map to token ids for prompt + const prompt: number[] = cardSeq.map((c) => this.tokenizer.vocab[c]); + if (includeColon) { + const colon = this.tokenizer.vocab[':']; + prompt.push(colon); + } + + // Compute action at each position + const slapId = this.tokenizer.vocab['🖐️']; + const noId = this.tokenizer.vocab['❌']; + const target: number[] = []; + for (let i = 0; i < cardSeq.length; i++) { + const c = cardSeq[i]; + const isJack = c === 'J'; + const isDouble = slapOnDoubles && i > 0 && c === cardSeq[i - 1]; + const isSandwich = slapOnSandwiches && i > 1 && c === cardSeq[i - 2]; + const shouldSlap = isJack || isDouble || isSandwich; + target.push(shouldSlap ? slapId : noId); + } + + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/sort.ts b/examples/piston-train-toy/src/lib/train/data/toy/sort.ts new file mode 100644 index 00000000..6f5644d4 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/sort.ts @@ -0,0 +1,125 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface SortConfig { + sequenceLength: number; + maxNumber: number; + includeCommas: boolean; + includeColon: boolean; +} + +export const SORT_CONFIG_METADATA = { + name: 'Alphabetical Sorting', + description: 'Sort a sequence of alphabet characters', + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + sequenceLength: { + name: 'Number of Items', + type: 'number' as const, + min: 2, + max: 10, + default: 5 + }, + maxNumber: { + name: 'Max Alphabet Character', + type: 'number' as const, + min: 3, + max: 26, + default: 26 + }, + includeCommas: { + name: 'Include Commas', + type: 'boolean' as const, + default: false + }, + includeColon: { + name: 'Include Colon', + type: 'boolean' as const, + default: true + } + } +} as const; + +export const SORT_CONFIG_DEFAULTS: SortConfig = { + sequenceLength: SORT_CONFIG_METADATA.parameters.sequenceLength.default, + maxNumber: SORT_CONFIG_METADATA.parameters.maxNumber.default, + includeCommas: SORT_CONFIG_METADATA.parameters.includeCommas.default, + includeColon: SORT_CONFIG_METADATA.parameters.includeColon.default +}; + +export const SORT_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len', + maxNumber: 'max num', + includeCommas: 'incl `,`', + includeColon: 'incl `:`' +}; + +export class SortDataset extends ToyDataset { + /** + * For sort dataset: + * - tokens 0 .. maxNum represent the alphabet characters corresponding to the numbers 0 .. maxNum. + * - token maxNum+1 -> ':' + * - token maxNum+2 -> ',' + */ + protected buildVocab(): string[] { + const { maxNumber, includeColon, includeCommas } = this.config; + const vocab: string[] = []; + const aCharCode = 'A'.charCodeAt(0); + for (let n = 0; n < maxNumber; n++) { + const token = String.fromCharCode(aCharCode + n); + vocab.push(token); + } + if (includeColon) { + vocab.push(':'); + } + if (includeCommas) { + vocab.push(','); + } + return vocab; + } + + /** + * Sorting: generate a sequence of characters and return: + * - prompt: the unsorted sequence with commas (token 1) between numbers and a colon (token 0) at the end. + * - target: the sorted sequence with commas between numbers. + * Example: BBCA:ABCBB + */ + public generateSequence(): ToySequence { + const { sequenceLength, maxNumber, includeCommas, includeColon } = this.config; + const nums = Array.from({ length: sequenceLength }, () => + this.generator.integer(0, maxNumber - 1) + ); + const sorted = [...nums].sort((a, b) => a - b); + const prompt: number[] = []; + + const comma = this.tokenizer.vocab[',']; + const colon = this.tokenizer.vocab[':']; + + for (let i = 0; i < nums.length; i++) { + prompt.push(nums[i]); + if (includeCommas && i < nums.length - 1) { + prompt.push(comma); + } + } + if (includeColon) { + prompt.push(colon); + } + + const target: number[] = []; + for (let i = 0; i < sorted.length; i++) { + target.push(sorted[i]); + if (includeCommas && i < sorted.length - 1) { + target.push(comma); + } + } + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/temporalOrder.ts b/examples/piston-train-toy/src/lib/train/data/toy/temporalOrder.ts new file mode 100644 index 00000000..4ffed629 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/temporalOrder.ts @@ -0,0 +1,130 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface TemporalOrderConfig { + sequenceLength: number; // total length including distractors and special symbols X/Y + vocabSize: number; // number of distractor letters (A..) + includeSeparators: boolean; // whether to include comma separators between tokens +} + +export const TEMPORAL_ORDER_CONFIG_METADATA = { + name: 'Temporal Order Recognition', + description: 'Classify whether X appears before Y (XY) or after Y (YX).', + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + citations: { + entries: [ + { + name: 'Hochreiter & Schmidhuber, 1997', + url: 'https://ieeexplore.ieee.org/abstract/document/6795963' + } + ] + }, + parameters: { + sequenceLength: { + name: 'Sequence Length', + description: 'Total number of tokens (including X and Y)', + type: 'number' as const, + min: 5, + max: 256, + step: 1, + default: 13 + }, + vocabSize: { + name: 'Distractor Vocab Size', + description: 'Number of distractor letters (A..)', + type: 'number' as const, + min: 2, + max: 100, + step: 1, + default: 10 + }, + includeSeparators: { + name: 'Include Separators', + description: 'Insert commas between tokens', + type: 'boolean' as const, + default: false + } + } +} as const; + +export const TEMPORAL_ORDER_CONFIG_DEFAULTS: TemporalOrderConfig = { + sequenceLength: TEMPORAL_ORDER_CONFIG_METADATA.parameters.sequenceLength.default, + vocabSize: TEMPORAL_ORDER_CONFIG_METADATA.parameters.vocabSize.default, + includeSeparators: TEMPORAL_ORDER_CONFIG_METADATA.parameters.includeSeparators.default +}; + +export const TEMPORAL_ORDER_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len', + vocabSize: 'vocab', + includeSeparators: 'incl `,`' +}; + +export class TemporalOrderDataset extends ToyDataset { + /** + * Vocab: + * - distractor letters D0..D{vocabSize-1} + * - special symbols 'X', 'Y' + * - classes 'XY', 'YX' + * - optional ',' separator + */ + protected buildVocab(): string[] { + const { vocabSize, includeSeparators } = this.config; + const padWidth = vocabSize.toString().length; + // 1. Add tokens for numbers + const vocab = Array.from( + { length: vocabSize + 1 }, + (_, i) => `D${i.toString().padStart(padWidth, '0')}` + ); + vocab.push('X', 'Y', 'XY', 'YX'); + if (includeSeparators) vocab.push(','); + return vocab; + } + + /** + * Build a sequence with exactly one X and one Y inserted among distractors. + * Prompt: sequence of tokens (with optional commas). Target: single class token. + */ + public generateSequence(): ToySequence { + const { sequenceLength, vocabSize, includeSeparators } = this.config; + if (sequenceLength < 2) { + throw new Error('sequenceLength must be at least 2 to place X and Y'); + } + const prompt: number[] = []; + + // Choose distinct positions for X and Y + const posX = this.generator.integer(0, sequenceLength - 1); + let posY = this.generator.integer(0, sequenceLength - 1); + while (posY === posX) posY = this.generator.integer(0, sequenceLength - 1); + + const comma = this.tokenizer.vocab[',']; + const x = this.tokenizer.vocab['X']; + const y = this.tokenizer.vocab['Y']; + + for (let i = 0; i < sequenceLength; i++) { + let tokenId: number; + if (i === posX) { + tokenId = x; + } else if (i === posY) { + tokenId = y; + } else { + tokenId = this.generator.integer(0, vocabSize - 1); + } + prompt.push(tokenId); + if (includeSeparators && i < sequenceLength - 1) { + prompt.push(comma); + } + } + + const classToken = posX < posY ? 'XY' : 'YX'; + const target = [this.tokenizer.vocab[classToken]]; + return { prompt, target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/twoSum.ts b/examples/piston-train-toy/src/lib/train/data/toy/twoSum.ts new file mode 100644 index 00000000..2c78cef0 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/twoSum.ts @@ -0,0 +1,194 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface TwoSumConfig { + sequenceLength: number; + maxNumber: number; + includeCommas: boolean; + includeExpressionTokens: boolean; +} + +export const TWO_SUM_CONFIG_METADATA = { + name: 'Two Sum', + description: 'Find two numbers in a sequence that sum to a target', + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + parameters: { + sequenceLength: { + name: 'Sequence Length', + description: 'Length of the sequence', + type: 'number' as const, + min: 2, + max: 10, + default: 4 + }, + maxNumber: { + name: 'Max Number', + description: 'Maximum value in the sequence', + type: 'number' as const, + min: 10, + max: 100, + default: 15 + }, + includeCommas: { + name: 'Include Commas', + type: 'boolean' as const, + default: false + }, + includeExpressionTokens: { + name: 'Include Expression Tokens', + type: 'boolean' as const, + default: true + } + } +} as const; + +export const TWO_SUM_CONFIG_DEFAULTS: TwoSumConfig = { + sequenceLength: TWO_SUM_CONFIG_METADATA.parameters.sequenceLength.default, + maxNumber: TWO_SUM_CONFIG_METADATA.parameters.maxNumber.default, + includeCommas: TWO_SUM_CONFIG_METADATA.parameters.includeCommas.default, + includeExpressionTokens: TWO_SUM_CONFIG_METADATA.parameters.includeExpressionTokens.default +}; + +export const TWO_SUM_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len', + maxNumber: 'max num', + includeCommas: 'incl `,`', + includeExpressionTokens: 'incl expr toks' +}; + +export class TwoSumDataset extends ToyDataset { + /** + * For Two-Sum dataset: + * - tokens 0 .. maxNum represent the numbers 0 .. maxNum. + * - token maxNum+1 -> ':' + * - token maxNum+2 -> '=' + * - token maxNum+3 -> ',' + */ + protected buildVocab(): string[] { + const { maxNumber, includeCommas, includeExpressionTokens } = this.config; + const vocab: string[] = []; + const padWidth = maxNumber.toString().length; + // 1. Add tokens for numbers + vocab.push( + ...Array.from({ length: maxNumber + 1 }, (_, i) => i.toString().padStart(padWidth, '0')) + ); + // 2. Add tokens for operators + if (includeExpressionTokens) { + vocab.push(':'); + vocab.push('='); + } + if (includeCommas) { + vocab.push(','); + } + return vocab; + } + + /** + * Two-Sum: generate a sequence of numbers (each in [0, maxNum]) and two random indices. + * The prompt consists of: + * - the sequence (with commas if enabled) + * - a colon, the sum and an equals sign. + * The target is the two numbers (from the chosen indices) separated by a comma. + * Example: ABC:D=AB or ABC:D=A,B + */ + public generateSequence(): ToySequence { + const { sequenceLength, maxNumber, includeCommas, includeExpressionTokens } = this.config; + // Generate a two-sum instance with a unique solution using a target-first constructive method. + const MAX_RETRIES = 10; + const maxElement = Math.floor(maxNumber / 2); + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + // 1) Choose two distinct indices for the special pair + const i = this.generator.integer(0, sequenceLength - 1); + let j = this.generator.integer(0, sequenceLength - 1); + while (j === i) { + j = this.generator.integer(0, sequenceLength - 1); + } + // 2) Choose their values from the usable range so t stays in-vocab + const a = this.generator.integer(0, maxElement); + const b = this.generator.integer(0, maxElement); + const t = a + b; // target sum + + // 3) Fill the rest while avoiding any other pair summing to t + const nums = new Array(sequenceLength); + nums[i] = a; + nums[j] = b; + + const seenNonSpecial: number[] = []; + let feasible = true; + + for (let k = 0; k < sequenceLength; k++) { + if (k === i || k === j) continue; + + // Build the set of disallowed values for this position + const disallowed = new Set(); + disallowed.add(a); + disallowed.add(b); + for (const y of seenNonSpecial) { + const complement = t - y; + if (complement >= 0 && complement <= maxElement) { + disallowed.add(complement); + } + } + + // Enumerate allowed candidates in the usable range + const allowed: number[] = []; + for (let val = 0; val <= maxElement; val++) { + if (!disallowed.has(val)) { + allowed.push(val); + } + } + + if (allowed.length === 0) { + feasible = false; + break; + } + + const choice = allowed[this.generator.integer(0, allowed.length - 1)]; + nums[k] = choice; + seenNonSpecial.push(choice); + } + + if (!feasible) { + continue; + } + + // 4) Build prompt and target + const prompt: number[] = []; + const comma = this.tokenizer.vocab[',']; + + for (let k = 0; k < nums.length; k++) { + prompt.push(nums[k]); + if (includeCommas && k < nums.length - 1) { + prompt.push(comma); + } + } + + if (includeExpressionTokens) { + prompt.push(this.tokenizer.vocab[':']); + } + + prompt.push(t); + + if (includeExpressionTokens) { + prompt.push(this.tokenizer.vocab['=']); + } + + const target: number[] = [nums[i]]; + if (includeCommas) { + target.push(comma); + } + target.push(nums[j]); + + return { prompt, target }; + } + throw new Error('Failed to generate a unique-solution two-sum instance after 10 attempts'); + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/data/toy/types.ts b/examples/piston-train-toy/src/lib/train/data/toy/types.ts new file mode 100644 index 00000000..c2f1e912 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/types.ts @@ -0,0 +1,10 @@ +export interface ToyTokenizer { + vocab: Record; + ids: Record; + lastToken: number; + decode(tokens: number[]): string; +} + +export type ToyValidationMetrics = { + matches?: boolean[]; +} & Record; diff --git a/examples/piston-train-toy/src/lib/train/data/toy/zeros.ts b/examples/piston-train-toy/src/lib/train/data/toy/zeros.ts new file mode 100644 index 00000000..41b5a4ea --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/data/toy/zeros.ts @@ -0,0 +1,68 @@ +import type { ToyValidationMetrics } from './types'; + +import ToyDataset, { mustMatchAccuracy, tokenMatches, type ToySequence } from './dataset'; + +export interface ZerosConfig { + sequenceLength: number; +} + +export const ZEROS_CONFIG_METADATA = { + name: 'Zeros', + description: + 'Output only zeros (useful test case; if a model fails to learn this, something is wrong)', + supportsModelTypes: ['encoder', 'encoder-decoder', 'decoder'], + disableValidation: true, + parameters: { + sequenceLength: { + name: 'Sequence Length', + description: 'Length of the sequence', + type: 'number' as const, + min: 2, + max: 10, + default: 5 + } + } +} as const; + +export const ZEROS_CONFIG_DEFAULTS: ZerosConfig = { + sequenceLength: ZEROS_CONFIG_METADATA.parameters.sequenceLength.default +}; + +export const ZEROS_SHORT_DESCRIPTIONS = { + sequenceLength: 'seq len' +}; + +export class ZerosDataset extends ToyDataset { + /** + * For zeros dataset (baseline/debug task): + */ + protected buildVocab(): string[] { + // We have at least two tokens so this is actually technically a classification task + return '01'.split(''); + } + + public readonly disableValidation = true; + + /** + * Zeros: (Baseline) returns a sequence of zeros. + * prompt: single '0' + * target: sequence of zeros (sequenceLength - 1) + * Example: 0:0000 + */ + public generateSequence(): ToySequence { + const { sequenceLength } = this.config; + const zeroToken = this.tokenizer.vocab['0']; + + // Target is sequenceLength zeros + const target = Array(sequenceLength).fill(zeroToken); + + return { target }; + } + + public computeMetrics(completion: number[], target: number[]): ToyValidationMetrics { + return { + matches: tokenMatches(completion, target), + accuracy: mustMatchAccuracy(completion, target) + }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/generate.ts b/examples/piston-train-toy/src/lib/train/generate.ts new file mode 100644 index 00000000..d49859a0 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/generate.ts @@ -0,0 +1,407 @@ +/** + * @fileoverview Shared text generation utilities for different model types + */ + +import type { Device, Tensor } from '@piston-ml/piston-web'; + +import { int32, tensor, WeakTensorFunctionMode } from '@piston-ml/piston-web'; + +import type { GeneratableModel } from './types'; + +import { createEmptyDecoderKVCache, type DecoderKVCache } from './model/cache'; +import { RNNDecoder, RNNEncoderDecoder } from './model/rnn'; +import { DecoderTransformer, EncoderDecoderTransformer } from './model/transformer'; + +export interface GenerationConfig { + maxTokens?: number; + stopTokens?: number | number[]; + device?: string | Device; + startToken?: number; + maxTargetLength?: number; + temperature?: number; + useKvCache?: boolean; +} + +export interface GenerationResult { + sequences: number[][]; + // Raw probabilities (post-softmax) for generated tokens + probs?: Tensor; + // Running average throughput since start of generation (tokens/second) + tokensPerSecond?: number; +} + +function normalizeStopTokens(stopTokens: number | number[] = []): Set { + return new Set(Array.isArray(stopTokens) ? stopTokens : [stopTokens]); +} + +/** + * Create tensor from token sequences with proper device and dtype + */ +function createInputTensor(sequences: number[][], device: Device): Tensor { + return tensor(sequences, { device, dtype: int32 }); +} + +/** + * Convert tensor output to array of token IDs + */ +async function tensorToTokens(tokenTensor: Tensor): Promise { + return (await (await tokenTensor.to('cpu')).toVec()) as Int32Array; +} + +/** + * Check if any sequence should continue generating (hasn't hit stop tokens) + */ +function shouldContinueGeneration( + results: number[][], + newTokens: Int32Array, + stopTokenSet: Set +): boolean { + let shouldContinue = false; + for (let i = 0; i < results.length; i++) { + // If this sequence already ended with a stop token, do not append anything further + const sequence = results[i]; + const lastToken = sequence.length > 0 ? sequence[sequence.length - 1] : undefined; + const alreadyStopped = lastToken !== undefined && stopTokenSet.has(lastToken); + + if (alreadyStopped) { + continue; + } + + const token = newTokens[i]; + // Always append the newly generated token + sequence.push(token); + // Continue only if we did not just append a stop token + if (!stopTokenSet.has(token)) { + shouldContinue = true; + } + } + return shouldContinue; +} + +/** + * Create a simple running tokens/sec tracker using the Performance API + */ +function startTokensPerSecondTracker(): (tokensProducedThisStep: number) => number { + const now = + typeof performance !== 'undefined' && typeof performance.now === 'function' + ? () => performance.now() + : () => Date.now(); + const startMs = now(); + let totalTokens = 0; + return (tokensProducedThisStep: number) => { + if (tokensProducedThisStep > 0) totalTokens += tokensProducedThisStep; + const elapsedMs = Math.max(1, now() - startMs); + return totalTokens / (elapsedMs / 1000); + }; +} + +/** + * Generate tokens for GPT (decoder-only) model + */ +export async function* generateGPTStream( + model: DecoderTransformer, + input: number[] | number[][], + config: GenerationConfig = {} +): AsyncGenerator { + const stopTokenSet = normalizeStopTokens(config.stopTokens); + const isBatch = Array.isArray(input[0]); + const sequences = isBatch ? (input as number[][]) : [input as number[]]; + + // Initialize results with copies of input sequences + const results = sequences.map((seq) => [...seq]); + const device = model.lmHead.weight.device; + + const kvCache: DecoderKVCache | null = + (config.useKvCache ?? true) ? createEmptyDecoderKVCache(model.config.nLayers) : null; + + let seeded = false; + let step = 0; + const getTokensPerSecond = startTokensPerSecondTracker(); + while (true) { + const weakMode = new WeakTensorFunctionMode(); + try { + weakMode.markWeak(kvCache); + // Seed cache once with full sequences, then switch to single-token steps + const prevLengths = results.map((seq) => seq.length); + let inputTensor: Tensor; + if (kvCache && seeded) { + const latestTokens = results.map((seq) => [seq[seq.length - 1] ?? 0]); + inputTensor = createInputTensor(latestTokens, device); + } else { + const { padded } = rightPadSequences(results); + inputTensor = createInputTensor(padded, device); + } + + // Forward pass to get logits + const [logits, _] = model.forward(inputTensor, { kvCache }); + weakMode.pin(kvCache); + + // Get logits for the last token in each sequence + const [batchSize, seqLen, vocabSize] = logits.size(); + let lastTokenLogits = logits + .slice([ + [0, batchSize], + [seqLen - 1, seqLen], + [0, vocabSize] + ]) + .view([batchSize, vocabSize]); + + if (config.temperature && config.temperature > 0) { + lastTokenLogits = lastTokenLogits.div(config.temperature); + } + + lastTokenLogits = lastTokenLogits.softmax(-1); + + // Choose next tokens: sample when temperature > 0, else greedy argmax + const nextTokenTensor = + config.temperature && config.temperature > 0 + ? lastTokenLogits.multinomial(1, { replacement: false }) + : lastTokenLogits.argmax({ dim: -1 }); + const nextTokensArray = await tensorToTokens(nextTokenTensor); + + // Update sequences and check for stop conditions + const shouldContinue = shouldContinueGeneration(results, nextTokensArray, stopTokenSet); + // Compute tokens appended this step across active sequences + let appendedThisStep = 0; + for (let i = 0; i < results.length; i++) { + appendedThisStep += results[i].length - prevLengths[i]; + } + const tokensPerSecond = getTokensPerSecond(appendedThisStep); + + weakMode.pin([results, lastTokenLogits]); + // Yield current state with sequences and logits + yield { + sequences: isBatch ? results.map((seq) => [...seq]) : [results[0].slice()], + probs: lastTokenLogits, // Provide the softmax'd logits for the last token + tokensPerSecond + }; + + // Mark cache as seeded after first forward with cache + if (kvCache && !seeded) seeded = true; + + // If all sequences hit stop tokens, break + if (!shouldContinue) { + break; + } + + step++; + + if (config.maxTokens !== undefined && step >= config.maxTokens) { + break; + } + } finally { + weakMode[Symbol.dispose](); + } + } +} + +/** + * Generate tokens for Transformer (encoder-decoder) model + */ +export async function* generateTransformerStream( + model: EncoderDecoderTransformer, + sourceInput: number[] | number[][], + config: GenerationConfig & { startToken?: number; maxTargetLength?: number } = {} +): AsyncGenerator { + const stopTokenSet = normalizeStopTokens(config.stopTokens); + const isBatch = Array.isArray(sourceInput[0]); + const sourceSequences = isBatch ? (sourceInput as number[][]) : [sourceInput as number[]]; + const startToken = config.startToken ?? 0; // Default start token + const maxTargetLength = config.maxTargetLength ?? config.maxTokens ?? 50; + + // Create source tensor and encode once + const device = model.lmHead.weight.device; + const sourceTensor = createInputTensor(sourceSequences, device); + const encoderHiddenStates = model.encode(sourceTensor); + + // Initialize target sequences with start token + const targetResults = sourceSequences.map(() => [startToken]); + + let step = 0; + const kvCache: DecoderKVCache | null = + (config.useKvCache ?? true) ? createEmptyDecoderKVCache(model.config.nDecoderLayer) : null; + let seeded = false; + const getTokensPerSecond = startTokensPerSecondTracker(); + + while (step < maxTargetLength) { + const weakMode = new WeakTensorFunctionMode(); + try { + weakMode.markWeak(kvCache); + // Seed cache once with full targets, then switch to single-token steps + const prevLengths = targetResults.map((seq) => seq.length); + let targetTensor: Tensor; + let tgtPaddingMask: Tensor | null = null; + if (kvCache && seeded) { + const latestTargets = targetResults.map((seq) => [seq[seq.length - 1] ?? startToken]); + targetTensor = createInputTensor(latestTargets, device); + // No padding mask needed for single-token incremental step + tgtPaddingMask = null; + } else { + const { padded: paddedTargets, paddingMask } = rightPadSequences( + targetResults, + Array.isArray(config.stopTokens) ? config.stopTokens[0] : config.stopTokens + ); + targetTensor = createInputTensor(paddedTargets, device); + tgtPaddingMask = tensor(paddingMask, { device, dtype: int32 }); + } + + // Use the model's forward method with pre-computed encoder hidden states + const [logits, _] = model.forward(null, targetTensor, { + tgtPaddingMask, + encoderHiddenStates, + kvCache + }); + weakMode.pin(kvCache); + + // Get logits for the last token in each sequence + const [batchSize, seqLen, vocabSize] = logits.size(); + let lastTokenLogits = logits + .slice([ + [0, batchSize], + [seqLen - 1, seqLen], + [0, vocabSize] + ]) + .view([batchSize, vocabSize]); + + if (config.temperature) { + lastTokenLogits = lastTokenLogits.div(config.temperature); + } + + lastTokenLogits = lastTokenLogits.softmax(-1); + + // Choose next tokens: sample when temperature > 0, else greedy argmax + const nextTokenTensor = + config.temperature && config.temperature > 0 + ? lastTokenLogits.multinomial(1, { replacement: false }) + : lastTokenLogits.argmax({ dim: -1 }); + const nextTokensArray = await tensorToTokens(nextTokenTensor); + + // Update sequences and check for stop conditions + const shouldContinue = shouldContinueGeneration(targetResults, nextTokensArray, stopTokenSet); + + // Yield current state with sequences and logits + weakMode.pin([targetResults, lastTokenLogits]); + let appendedThisStep = 0; + for (let i = 0; i < targetResults.length; i++) { + appendedThisStep += targetResults[i].length - prevLengths[i]; + } + const tokensPerSecond = getTokensPerSecond(appendedThisStep); + yield { + sequences: isBatch ? targetResults.map((seq) => [...seq]) : [targetResults[0].slice()], + probs: lastTokenLogits, // Provide the logits for the last token + tokensPerSecond + }; + + // Mark cache as seeded after first forward with cache + if (kvCache && !seeded) seeded = true; + + // If all sequences hit stop tokens, break + if (!shouldContinue) { + break; + } + + step++; + + if (config.maxTokens !== undefined && step >= config.maxTokens) { + break; + } + } finally { + weakMode[Symbol.dispose](); + } + } +} + +/** + * Unified generate function that works with any supported model + */ +export async function* generateStream( + model: GeneratableModel, + input: number[] | number[][], + config: GenerationConfig = {} +): AsyncGenerator { + // Check model type and delegate to appropriate generator, returning inner summary + if (model instanceof EncoderDecoderTransformer) { + // This is a Transformer (encoder-decoder) model + return yield* generateTransformerStream(model, input, config); + } else if (model instanceof DecoderTransformer || model instanceof RNNDecoder) { + // This is a GPT (decoder-only) model + return yield* generateGPTStream(model as DecoderTransformer, input, config); + } else if (model instanceof RNNEncoderDecoder) { + // RNN encoder-decoder: reuse transformer-style generation loop + return yield* generateTransformerStream( + model as unknown as EncoderDecoderTransformer, + input, + config + ); + } else { + throw new Error('Unsupported model type for generation'); + } +} + +/** + * Standard generate function that collects all tokens (backward compatible) + */ +export async function generate( + model: GeneratableModel, + input: number[] | number[][], + config: GenerationConfig = {} +): Promise { + const results: number[][] = []; + let tokenCount = 0; + const maxTokens = config.maxTokens ?? 50; + + for await (const generationResult of generateStream(model, input, config)) { + results.length = 0; + results.push(...generationResult.sequences); + tokenCount++; + + if (tokenCount >= maxTokens) { + break; + } + } + + return results; +} + +/** + * Right-pad sequences to a uniform length for batched forward passes. + * Returns padded sequences and a padding mask (1 for real tokens, 0 for padding). + * Note: The original sequences array is NOT modified. + */ +export function rightPadSequences( + sequences: number[][], + padToken?: number +): { padded: number[][]; paddingMask: number[][] } { + const maxLen = sequences.reduce((m, s) => Math.max(m, s.length), 0); + const padded: number[][] = new Array(sequences.length); + const paddingMask: number[][] = new Array(sequences.length); + + for (let i = 0; i < sequences.length; i++) { + const seq = sequences[i]; + const realLen = seq.length; + const rowMask: number[] = new Array(maxLen).fill(0); + for (let j = 0; j < realLen; j++) rowMask[j] = 1; + + if (realLen === maxLen) { + padded[i] = [...seq]; + paddingMask[i] = rowMask; + continue; + } + + let padVal: number; + if (padToken !== undefined) { + padVal = padToken; + } else if (realLen > 0) { + padVal = seq[realLen - 1]; + } else { + padVal = 0; // degenerate case + } + + const numPad = maxLen - realLen; + padded[i] = + realLen > 0 ? [...seq, ...new Array(numPad).fill(padVal)] : new Array(maxLen).fill(padVal); + paddingMask[i] = rowMask; + } + + return { padded, paddingMask }; +} diff --git a/examples/piston-train-toy/src/lib/train/model/bidirectional.ts b/examples/piston-train-toy/src/lib/train/model/bidirectional.ts new file mode 100644 index 00000000..3cf5c5a7 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/bidirectional.ts @@ -0,0 +1,89 @@ +import type { LayerNormalizationConfig } from '$lib/workspace/config'; + +import { nn, Tensor } from '@piston-ml/piston-web'; + +import { createNorm } from './modules/norm'; + +/** + * MLM Head for masked language modeling + */ +export class MLMHead extends nn.Module { + private readonly transform: nn.Linear; + private readonly layernorm?: nn.Module<[Tensor], Tensor>; + private readonly decoder: nn.Linear; + + /** + * @param hiddenSize - Hidden dimension + * @param vocabSize - Vocabulary size + * @param layerNormalization - Layer normalization configuration + * @param embeddings - Optional embedding layer to share weights with + */ + constructor( + hiddenSize: number, + vocabSize: number, + layerNormalization: LayerNormalizationConfig, + embeddings?: nn.Embedding + ) { + super(); + + this.transform = new nn.Linear(hiddenSize, hiddenSize); + if (layerNormalization.transformer.present) { + this.layernorm = createNorm(hiddenSize, layerNormalization); + } + + if (embeddings) { + // Share weights with embedding layer + this.decoder = new nn.Linear(hiddenSize, vocabSize, false); + // Tie weights + this.decoder.weight = new nn.Parameter(embeddings.weight); + } else { + this.decoder = new nn.Linear(hiddenSize, vocabSize, false); + } + } + + /** + * @param hiddenStates - Hidden states from encoder + * @returns MLM logits + */ + forward(hiddenStates: Tensor): Tensor { + let x = this.transform.forward(hiddenStates); + x = x.gelu(); // Standard activation for MLM head + if (this.layernorm) { + x = this.layernorm.forward(x) as Tensor; + } + return this.decoder.forward(x); + } +} + +/** + * Pooler for classification tasks + */ +export class Pooler extends nn.Module { + private readonly dense: nn.Linear; + + /** + * @param hiddenSize - Hidden dimension + */ + constructor(hiddenSize: number) { + super(); + this.dense = new nn.Linear(hiddenSize, hiddenSize); + } + + /** + * @param hiddenStates - Hidden states from encoder [B, T, H] + * @returns Pooled representation [B, H] + */ + forward(hiddenStates: Tensor): Tensor { + // Take the first token (CLS token) representation + let pooled = hiddenStates + .slice([ + [0, hiddenStates.size(0)], + [0, 1], + [0, hiddenStates.size(2)] + ]) + .squeeze({ dim: 1 }); + + pooled = this.dense.forward(pooled); + return pooled.tanh(); + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/cache.ts b/examples/piston-train-toy/src/lib/train/model/cache.ts new file mode 100644 index 00000000..7dd33524 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/cache.ts @@ -0,0 +1,20 @@ +import type { Tensor } from '@piston-ml/piston-web'; + +export type SelfAttentionCache = { + k: Tensor; + v: Tensor; + length: number; +}; + +export type DecoderLayerCache = { + self?: SelfAttentionCache; + cross?: SelfAttentionCache; +}; + +export type DecoderKVCache = { + layers: DecoderLayerCache[]; +}; + +export function createEmptyDecoderKVCache(nLayers: number): DecoderKVCache { + return { layers: Array.from({ length: nLayers }, () => ({})) }; +} diff --git a/examples/piston-train-toy/src/lib/train/model/config.ts b/examples/piston-train-toy/src/lib/train/model/config.ts new file mode 100644 index 00000000..396f29d2 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/config.ts @@ -0,0 +1,111 @@ +import type { + Config, + LayerNormalizationConfig, + MLPConfig, + PositionEncodingConfig, + RNNDropoutConfig, + RNNEmbeddingConfig, + RNNHiddenStateProjectionConfig, + RNNInitializationConfig, + TransformerAttentionConfig, + TransformerDropoutConfig, + TransformerInitializationConfig, + TransformerNormalizationConfig +} from '$lib/workspace/config'; + +export interface TransformerModuleConfig { + vocabSize: number; + embeddingSize: number; + attention: TransformerAttentionConfig; + layerNormalization: LayerNormalizationConfig; + normalization: TransformerNormalizationConfig; + mlp: MLPConfig; + positionalEncoding: PositionEncodingConfig; + dropout: TransformerDropoutConfig; + initialization: TransformerInitializationConfig; +} + +export interface RNNModuleConfig { + cellType: 'gru' | 'lstm' | 'rnn'; + embeddingSize: number; + vocabSize: number; + hiddenSize: number; + baseHiddenSize: number; + projectionSize?: number; + embedding: RNNEmbeddingConfig; + dropout: RNNDropoutConfig; + layerNormalization: LayerNormalizationConfig; + initialization: RNNInitializationConfig; + hiddenStateProjection: RNNHiddenStateProjectionConfig; + tieEmbeddingsAndLmHead: boolean; +} + +export function buildRNNConfigCommon(config: Config, vocabSize: number): RNNModuleConfig { + const effectiveEmbeddingSize = + config.model.rnn.embedding.type === 'learned' + ? config.model.rnn.embedding.learned.size + : vocabSize; + const rawHiddenSize = config.model.rnn.separateHiddenSize.present + ? config.model.rnn.separateHiddenSize.value + : effectiveEmbeddingSize; + const projectionSize = config.model.rnn.hiddenStateProjection.present + ? config.model.rnn.hiddenStateProjection.size + : undefined; + const baseHiddenSize = projectionSize ?? rawHiddenSize; + + return { + vocabSize: vocabSize, + cellType: config.model.rnn.cellType, + embeddingSize: effectiveEmbeddingSize, + hiddenSize: rawHiddenSize, + baseHiddenSize, + projectionSize, + embedding: config.model.rnn.embedding, + layerNormalization: config.model.layerNormalization, + hiddenStateProjection: config.model.rnn.hiddenStateProjection, + initialization: config.model.rnn.initialization, + tieEmbeddingsAndLmHead: config.model.tieEmbeddingsAndLmHead, + dropout: (({ present, embedding: embd, rnn }: RNNDropoutConfig) => { + return { + present, + embedding: present ? embd : 0, + rnn: { + interLayer: present ? rnn.interLayer : 0 + } + }; + })(config.training.dropout) + }; +} + +export function buildTransformerConfigCommon( + config: Config, + vocabSize: number +): TransformerModuleConfig { + const effectiveHeads = config.model.transformer.attention.present + ? config.model.transformer.attention.nKeyValueHeads * + (config.model.transformer.attention.groupedQueryAttention.present + ? config.model.transformer.attention.groupedQueryAttention.queryHeadsPerKeyValueHead + : 1) + : 1; + + return { + vocabSize: vocabSize, + embeddingSize: config.model.transformer.headDim * effectiveHeads, + attention: config.model.transformer.attention, + mlp: config.model.transformer.mlp, + positionalEncoding: config.model.transformer.positionalEncoding, + layerNormalization: config.model.layerNormalization, + normalization: config.model.transformer.normalization, + initialization: config.model.transformer.initialization, + dropout: (({ present, embedding: embd, transformer }: TransformerDropoutConfig) => { + return { + present, + embedding: present ? embd : 0, + transformer: { + attention: present ? transformer.attention : 0, + residual: present ? transformer.residual : 0 + } + }; + })(config.training.dropout) + }; +} diff --git a/examples/piston-train-toy/src/lib/train/model/modules/attention.ts b/examples/piston-train-toy/src/lib/train/model/modules/attention.ts new file mode 100644 index 00000000..e2569ace --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/modules/attention.ts @@ -0,0 +1,406 @@ +import { + AlibiEmbedding, + cat, + gpu, + Linear, + nn, + RotaryEmbedding, + Tensor, + zeros +} from '@piston-ml/piston-web'; + +import type { SelfAttentionCache } from '../cache'; +import type { TransformerModuleConfig } from '../config'; + +import { createCausalMask, maskedFill } from '../utils'; +import { SimpleHadamardGate } from './gate'; +import { applySoftcap } from './utils'; + +abstract class CoreAttention extends nn.Module { + protected readonly nHeads: number; + protected readonly nKvHeads: number; + protected readonly embeddingSize: number; + protected readonly headDim: number; + protected abstract cProj: Linear; + protected attnDropout?: nn.Dropout; + protected residDropout?: nn.Dropout; + protected rope?: RotaryEmbedding; + protected alibi?: AlibiEmbedding; + protected sinks?: nn.Parameter; + protected readonly isCausal: boolean; + protected readonly config: TransformerModuleConfig; + protected readonly gateAfterSdpa?: SimpleHadamardGate; + protected readonly gateAfterQ?: SimpleHadamardGate; + protected readonly gateAfterK?: SimpleHadamardGate; + protected readonly gateAfterV?: SimpleHadamardGate; + protected readonly gateAfterFinal?: SimpleHadamardGate; + + constructor(embeddingSize: number, config: TransformerModuleConfig, isCausal: boolean = false) { + super(); + + this.nHeads = + config.attention.nKeyValueHeads * + (config.attention.groupedQueryAttention.present + ? config.attention.groupedQueryAttention.queryHeadsPerKeyValueHead + : 1); + this.nKvHeads = config.attention.nKeyValueHeads; + this.embeddingSize = embeddingSize; + this.headDim = embeddingSize / this.nHeads; + this.isCausal = isCausal; + + this.config = config; + + if (config.attention.sinks?.present) { + this.sinks = new nn.Parameter(zeros([this.nHeads], { device: gpu })); + } + + const gatingConfig = config.attention.gating; + if (gatingConfig?.present) { + const controlDim = this.embeddingSize; + const act = gatingConfig.activation; + if (gatingConfig.sites.afterSdpaOutput) { + this.gateAfterSdpa = new SimpleHadamardGate(controlDim, this.embeddingSize, act); + } + if (gatingConfig.sites.afterFinalOutputProjection) { + this.gateAfterFinal = new SimpleHadamardGate(controlDim, this.embeddingSize, act); + } + if (gatingConfig.sites.afterQueryProjection) { + this.gateAfterQ = new SimpleHadamardGate(controlDim, this.headDim, act); + } + if (gatingConfig.sites.afterKeyProjection) { + this.gateAfterK = new SimpleHadamardGate(controlDim, this.headDim, act); + } + if (gatingConfig.sites.afterValueProjection) { + this.gateAfterV = new SimpleHadamardGate(controlDim, this.headDim, act); + } + } + } + + protected applyQkvGating( + control: Tensor, + q: Tensor, + k: Tensor, + v: Tensor + ): [Tensor, Tensor, Tensor] { + if (this.gateAfterQ) { + q = this.gateAfterQ.forward(q, control); + } + if (this.gateAfterK) { + k = this.gateAfterK.forward(k, control); + } + if (this.gateAfterV) { + v = this.gateAfterV.forward(v, control); + } + return [q, k, v]; + } + + protected runAttention( + input: Tensor, + q: Tensor, + k: Tensor, + v: Tensor, + options: { + attentionMask?: Tensor | null; + cache?: SelfAttentionCache | null; + ropeOffsets?: { qOffset: number; kOffset: number } | null; + } + ): [Tensor, SelfAttentionCache | null] { + const B = q.size(0); + const T_q = q.size(2); + + // Optional QK norm and gating + [q, k] = this.applyQKNorm(q, k); + [q, k, v] = this.applyQkvGating(input, q, k, v); + + // Optional RoPE with separate offsets for q and k + if (options.ropeOffsets) { + [q, k] = this.applyRoPE(q, k, options.ropeOffsets.qOffset, options.ropeOffsets.kOffset); + } + + // Concatenate with cache if present + const kCat = options.cache ? cat([options.cache.k, k], { dim: 2 }) : k; + const vCat = options.cache ? cat([options.cache.v, v], { dim: 2 }) : v; + const T_kv = kCat.size(2); + + // Compute attention scores + let att = q.matmul(kCat, { transRhs: true }).div(Math.sqrt(this.headDim)); + + if (this.config.normalization.softcap.attention.present) { + att = applySoftcap(att, this.config.normalization.softcap.attention.value); + } + + // Apply ALiBi if configured + if (this.alibi) { + att = this.alibi.forward(att); + } + + // Apply causal mask if needed + if (this.isCausal) { + att = this.applyCausalMask(att, T_q, T_kv); + } + if (options.attentionMask) { + att = this.applyAttentionMask(att, options.attentionMask); + } + + // Append sinks bias column if configured: concatenate along last dim before softmax + if (this.sinks) { + // Shape to [B, nHeads, 1, 1] then broadcast to [B, nHeads, T_q, 1] + // TODO: Support proper expand (shouldn't be hard) + const sinksCol = this.sinks + .unsqueeze(0) + .unsqueeze(2) + .unsqueeze(3) + .broadcastTo([B, this.nHeads, T_q, 1]); + att = cat([att, sinksCol], { dim: 3 }); + } + + // Apply softmax over keys plus optional sink column + att = att.softmax(3); + att = this.attnDropout ? this.attnDropout.forward(att) : att; + + if (this.sinks) { + // If sinks were appended, drop the sink column from attention before matmul with V + att = att.slice([ + [0, B], + [0, this.nHeads], + [0, T_q], + [0, att.size(3) - 1] + ]); + } + + let y = att.matmul(vCat).transpose(1, 2).view([B, T_q, this.embeddingSize]); + if (this.gateAfterSdpa) { + y = this.gateAfterSdpa.forward(y, input); + } + + // Apply output projection + y = this.cProj.forward(y); + + // Optional gating after final output projection + if (this.gateAfterFinal) { + y = this.gateAfterFinal.forward(y, input); + } + + if (this.residDropout) { + y = this.residDropout.forward(y); + } + + const newCache: SelfAttentionCache | null = { + k: kCat, + v: vCat, + length: T_kv + }; + return [y, newCache]; + } + + applyRoPE(q: Tensor, k: Tensor, qOffset: number, kOffset?: number) { + if (this.rope) { + q = this.rope.forward(q, qOffset); + k = this.rope.forward(k, kOffset ?? qOffset); + } + return [q, k]; + } + + applyAttentionMask(att: Tensor, attentionMask: Tensor) { + // Convert attention mask to appropriate shape [B, 1, 1, T_kv] and broadcast + const mask = attentionMask.unsqueeze(1).unsqueeze(2).broadcastTo(att.size()); + return maskedFill(att, mask, -1e9); + } + + applyCausalMask(att: Tensor, queryLen: number, keyLen: number) { + // Apply causal mask if needed + return queryLen <= 1 + ? att + : (() => { + const mask = createCausalMask(queryLen, keyLen).broadcastTo(att.size()); + return maskedFill(att, mask, -1e9); + })(); + } + + applyQKNorm(q: Tensor, k: Tensor): [Tensor, Tensor] { + if (this.config.normalization.qkNorm.present) { + if (this.config.normalization.qkNorm.type === 'rmsnorm') { + q = q.rmsNorm({ eps: this.config.normalization.qkNorm.eps }); + k = k.rmsNorm({ eps: this.config.normalization.qkNorm.eps }); + } else if (this.config.normalization.qkNorm.type === 'layernorm') { + q = q.layerNorm({ eps: this.config.normalization.qkNorm.eps }); + k = k.layerNorm({ eps: this.config.normalization.qkNorm.eps }); + } else { + throw new Error(`Unknown QKNorm type: ${this.config.normalization.qkNorm.type}`); + } + } + return [q, k]; + } + + /** + * Apply grouped-query attention broadcasting to key and value tensors + * @param k Key tensor [B, nKvHeads, seqLen, headDim] + * @param v Value tensor [B, nKvHeads, seqLen, headDim] + * @param B Batch size + * @param seqLen Sequence length + * @returns Broadcasted [k, v] tensors [B, nHeads, seqLen, headDim] + */ + applyGroupedQueryBroadcast(k: Tensor, v: Tensor, B: number, seqLen: number): [Tensor, Tensor] { + if (this.nHeads !== this.nKvHeads) { + const repeatFactor = this.nHeads / this.nKvHeads; + const th = seqLen * this.headDim; + + k = k + .view([B, this.nKvHeads, th]) + .unsqueeze(2) + .broadcastTo([B, this.nKvHeads, repeatFactor, th]) + .view([B, this.nHeads, seqLen, this.headDim]); + v = v + .view([B, this.nKvHeads, th]) + .unsqueeze(2) + .broadcastTo([B, this.nKvHeads, repeatFactor, th]) + .view([B, this.nHeads, seqLen, this.headDim]); + } + return [k, v]; + } +} + +export class SelfAttention extends CoreAttention { + private readonly cAttn: Linear; + protected readonly cProj: Linear; + + constructor(embeddingSize: number, config: TransformerModuleConfig, isCausal: boolean = false) { + super(embeddingSize, config, isCausal); + + const kvDim = this.headDim * this.nKvHeads; + const qkvOutDim = embeddingSize + 2 * kvDim; + this.cAttn = new nn.Linear(embeddingSize, qkvOutDim); + this.cProj = new nn.Linear(embeddingSize, embeddingSize); + + if (config.dropout.transformer.attention > 0) { + this.attnDropout = new nn.Dropout(config.dropout.transformer.attention); + } + + if (config.dropout.transformer.residual > 0) { + this.residDropout = new nn.Dropout(config.dropout.transformer.residual); + } + + if (config.positionalEncoding.present && config.positionalEncoding.type === 'rope') { + this.rope = new RotaryEmbedding(this.headDim, config.positionalEncoding.rope.base); + } + + if (config.positionalEncoding.present && config.positionalEncoding.type === 'alibi') { + this.alibi = new AlibiEmbedding(config.positionalEncoding.alibi.maxBias); + } + } + + private projectQkv(input: Tensor): [Tensor, Tensor, Tensor] { + const [B, T, _] = input.size(); + const kvDim = this.headDim * this.nKvHeads; + const keyPos = this.embeddingSize; + const valuePos = this.embeddingSize + kvDim; + const qkv = this.cAttn.forward(input); + let q = qkv.slice([ + [0, B], + [0, T], + [0, this.embeddingSize] + ]); + let k = qkv.slice([ + [0, B], + [0, T], + [keyPos, keyPos + kvDim] + ]); + let v = qkv.slice([ + [0, B], + [0, T], + [valuePos, valuePos + kvDim] + ]); + const qShape = [B, T, this.nHeads, this.headDim]; + const kvShape = [B, T, this.nKvHeads, this.headDim]; + q = q.view(qShape)?.transpose(1, 2); + k = k.view(kvShape)?.transpose(1, 2); + v = v.view(kvShape)?.transpose(1, 2); + [k, v] = this.applyGroupedQueryBroadcast(k, v, B, T); + return [q, k, v]; + } + + forward( + input: Tensor, + options: { attentionMask?: Tensor | null; cache?: SelfAttentionCache | null } = {} + ): { output: Tensor; pastKeyValues?: SelfAttentionCache } { + const qkv = this.projectQkv(input); + const [q, k, v] = qkv; + const pastLen = options.cache?.length ?? 0; + const [y, newCache] = this.runAttention(input, q, k, v, { + attentionMask: options.attentionMask ?? null, + cache: options.cache ?? null, + ropeOffsets: { qOffset: pastLen, kOffset: pastLen } + }); + return { output: y, pastKeyValues: newCache ?? undefined }; + } +} + +export class CrossAttention extends CoreAttention { + private readonly qProj: Linear; + private readonly kvProj: Linear; + protected readonly cProj: Linear; + + constructor(embeddingSize: number, config: TransformerModuleConfig) { + super(embeddingSize, config); + this.qProj = new nn.Linear(embeddingSize, embeddingSize); + const kvDim = this.headDim * this.nKvHeads; + this.kvProj = new nn.Linear(embeddingSize, 2 * kvDim); + this.cProj = new nn.Linear(embeddingSize, embeddingSize); + + if (config.dropout.transformer.attention > 0) { + this.attnDropout = new nn.Dropout(config.dropout.transformer.attention); + } + + if (config.dropout.transformer.residual > 0) { + this.residDropout = new nn.Dropout(config.dropout.transformer.residual); + } + } + + forward( + query: Tensor, + keyValue: Tensor, + options: { attentionMask?: Tensor | null; cache?: SelfAttentionCache | null } = {} + ): { output: Tensor; pastKeyValues?: SelfAttentionCache } { + const [B, T_q, _q] = query.size(); + let q = this.qProj.forward(query); + const qShape = [B, T_q, this.nHeads, this.headDim]; + q = q.view(qShape)?.transpose(1, 2); + + let k: Tensor; + let v: Tensor; + let returnCache: SelfAttentionCache | null = null; + if (options.cache) { + // Reuse precomputed base encoder K/V from cache for cross-attention + k = options.cache.k; + v = options.cache.v; + returnCache = options.cache; + } else { + // Compute and reshape encoder K/V once, cache base K/V for reuse across decoding steps + const kvDim = this.headDim * this.nKvHeads; + const kv = this.kvProj.forward(keyValue); + const kProj = kv.slice([ + [0, B], + [0, keyValue.size(1)], + [0, kvDim] + ]); + const vProj = kv.slice([ + [0, B], + [0, keyValue.size(1)], + [kvDim, kvDim + kvDim] + ]); + const kvShape = [B, keyValue.size(1), this.nKvHeads, this.headDim]; + k = kProj.view(kvShape)?.transpose(1, 2); + v = vProj.view(kvShape)?.transpose(1, 2); + [k, v] = this.applyGroupedQueryBroadcast(k, v, B, keyValue.size(1)); + returnCache = { k, v, length: keyValue.size(1) }; + } + + const [y, _ignored] = this.runAttention(query, q, k, v, { + attentionMask: options.attentionMask ?? null, + // No KV concatenation for cross-attention; K/V are static from encoder + cache: null, + ropeOffsets: { qOffset: 0, kOffset: 0 } + }); + return { output: y, pastKeyValues: returnCache ?? undefined }; + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/modules/decoder.ts b/examples/piston-train-toy/src/lib/train/model/modules/decoder.ts new file mode 100644 index 00000000..ed44a466 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/modules/decoder.ts @@ -0,0 +1,135 @@ +import type { LayerNormPosition } from '$lib/workspace/config'; + +import { nn, Tensor } from '@piston-ml/piston-web'; + +import type { DecoderLayerCache } from '../cache'; +import type { TransformerModuleConfig } from '../config'; + +import { CrossAttention, SelfAttention } from './attention'; +import { MLP } from './mlp'; +import { createNorm } from './norm'; + +export type DecoderLayerForwardOptions = { + encoderHiddenStates?: Tensor | null; + srcPaddingMask?: Tensor | null; + tgtPaddingMask?: Tensor | null; + cache?: DecoderLayerCache | null; +}; + +export type DecoderLayerForwardResult = { + output: Tensor; + pastKeyValues?: DecoderLayerCache; +}; + +export class DecoderLayer extends nn.Module { + private readonly lnSelfAttn?: nn.Module<[Tensor], Tensor>; + private readonly selfAttn?: SelfAttention; + private readonly lnCrossAttn?: nn.Module<[Tensor], Tensor>; + private readonly crossAttn?: CrossAttention; + private readonly lnMlp?: nn.Module<[Tensor], Tensor>; + private readonly mlp?: MLP; + private readonly dropout?: nn.Dropout; + private readonly layernormPosition: LayerNormPosition; + + constructor(config: TransformerModuleConfig, crossAttention: boolean = false) { + super(); + + if (config.attention.present) { + this.selfAttn = new SelfAttention(config.embeddingSize, config, true); + + if (config.layerNormalization.transformer.present) { + this.lnSelfAttn = createNorm(config.embeddingSize, config.layerNormalization); + } + } + + if (crossAttention && config.attention.present) { + this.crossAttn = new CrossAttention(config.embeddingSize, config); + if (config.layerNormalization.transformer.present) { + this.lnCrossAttn = createNorm(config.embeddingSize, config.layerNormalization); + } + } + + if (config.mlp.present) { + this.mlp = new MLP(config.embeddingSize, config.mlp); + if (config.layerNormalization.transformer.present) { + this.lnMlp = createNorm(config.embeddingSize, config.layerNormalization); + } + } + + if (config.dropout.transformer.residual > 0) { + this.dropout = new nn.Dropout(config.dropout.transformer.residual); + } + + this.layernormPosition = config.layerNormalization.transformer.position; + } + + forward(input: Tensor, options: DecoderLayerForwardOptions = {}): DecoderLayerForwardResult { + const encoderHiddenStates = options.encoderHiddenStates ?? null; + const srcPaddingMask = options.srcPaddingMask ?? null; + const tgtPaddingMask = options.tgtPaddingMask ?? null; + const cache = options.cache ?? null; + let x = input; + let selfCache = cache?.self ?? null; + + if (this.selfAttn) { + const residual = input; + if (this.lnSelfAttn && this.layernormPosition === 'pre') { + x = this.lnSelfAttn.forward(input) as Tensor; + } + const selfResult = this.selfAttn.forward(x, { + attentionMask: tgtPaddingMask ?? null, + cache: selfCache ?? null + }); + selfCache = selfResult.pastKeyValues ?? null; + x = residual.add(selfResult.output); + if (this.lnSelfAttn && this.layernormPosition === 'post') { + x = this.lnSelfAttn.forward(x) as Tensor; + } + } + + if (this.crossAttn) { + if (!encoderHiddenStates) { + throw new Error('Encoder hidden states are required for cross-attention'); + } + const residual2 = x; + if (this.lnCrossAttn && this.layernormPosition === 'pre') { + x = this.lnCrossAttn.forward(x) as Tensor; + } + const crossResult = this.crossAttn.forward(x, encoderHiddenStates, { + attentionMask: srcPaddingMask ?? null, + cache: cache?.cross ?? null + }); + // If caching is enabled, store cross-attn K/V once + if (cache) { + if (!cache.cross && crossResult.pastKeyValues) { + cache.cross = crossResult.pastKeyValues; + } + } + x = residual2.add(crossResult.output); + if (this.lnCrossAttn && this.layernormPosition === 'post') { + x = this.lnCrossAttn.forward(x) as Tensor; + } + } + + if (this.mlp) { + const residual3 = x; + if (this.lnMlp && this.layernormPosition === 'pre') { + x = this.lnMlp.forward(x) as Tensor; + } + x = this.mlp.forward(x); + if (this.dropout) { + x = this.dropout.forward(x); + } + x = residual3.add(x); + if (this.lnMlp && this.layernormPosition === 'post') { + x = this.lnMlp.forward(x) as Tensor; + } + } + + const result: DecoderLayerForwardResult = { output: x }; + if (cache) { + result.pastKeyValues = { self: selfCache ?? undefined, cross: cache.cross ?? undefined }; + } + return result; + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/modules/encoder.ts b/examples/piston-train-toy/src/lib/train/model/modules/encoder.ts new file mode 100644 index 00000000..b01686bc --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/modules/encoder.ts @@ -0,0 +1,74 @@ +import type { LayerNormPosition } from '$lib/workspace/config'; + +import { type Module, nn, Tensor } from '@piston-ml/piston-web'; + +import type { TransformerModuleConfig } from '../config'; + +import { SelfAttention } from './attention'; +import { MLP } from './mlp'; +import { createNorm } from './norm'; + +export class EncoderLayer extends nn.Module { + private readonly lnAttn?: Module<[Tensor], Tensor>; + private readonly attn?: SelfAttention; + private readonly lnMlp?: Module<[Tensor], Tensor>; + private readonly mlp?: MLP; + private readonly dropout?: nn.Dropout; + private readonly layernormPosition: LayerNormPosition; + + constructor(config: TransformerModuleConfig) { + super(); + + if (config.attention.present) { + this.attn = new SelfAttention(config.embeddingSize, config, false); + if (config.layerNormalization.transformer.present) { + this.lnAttn = createNorm(config.embeddingSize, config.layerNormalization); + } + } + + if (config.mlp.present) { + this.mlp = new MLP(config.embeddingSize, config.mlp); + if (config.layerNormalization.transformer.present) { + this.lnMlp = createNorm(config.embeddingSize, config.layerNormalization); + } + } + + if (config.dropout.transformer.residual > 0) { + this.dropout = new nn.Dropout(config.dropout.transformer.residual); + } + + this.layernormPosition = config.layerNormalization.transformer.position; + } + + forward(input: Tensor, attentionMask: Tensor | null = null): Tensor { + let x = input; + if (this.attn) { + const residual = input; + if (this.lnAttn && this.layernormPosition === 'pre') { + x = this.lnAttn.forward(input) as Tensor; + } + const attnOutput = this.attn.forward(x, { attentionMask }); + x = residual.add(attnOutput.output); + if (this.lnAttn && this.layernormPosition === 'post') { + x = this.lnAttn.forward(x) as Tensor; + } + } + + if (this.mlp) { + const residual2 = x; + if (this.lnMlp && this.layernormPosition === 'pre') { + x = this.lnMlp.forward(x) as Tensor; + } + x = this.mlp.forward(x); + if (this.dropout) { + x = this.dropout.forward(x); + } + x = residual2.add(x); + if (this.lnMlp && this.layernormPosition === 'post') { + x = this.lnMlp.forward(x) as Tensor; + } + } + + return x; + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/modules/gate.ts b/examples/piston-train-toy/src/lib/train/model/modules/gate.ts new file mode 100644 index 00000000..c700e20e --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/modules/gate.ts @@ -0,0 +1,19 @@ +import type { Activation } from '$lib/workspace/config'; + +import { nn, Tensor } from '@piston-ml/piston-web'; + +export class SimpleHadamardGate extends nn.Module { + private readonly tau: nn.Linear; + private readonly activation: (x: Tensor) => Tensor; + + constructor(controlDim: number, targetDim: number, activationName: Activation) { + super(); + this.tau = new nn.Linear(controlDim, targetDim); + this.activation = (x: Tensor): Tensor => x[activationName](); + } + + forward(gatedTensor: Tensor, gateInput: Tensor): Tensor { + const g = this.activation(this.tau.forward(gateInput)); + return gatedTensor.mul(g.size().length === gatedTensor.size().length ? g : g.unsqueeze(1)); + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/modules/mlp.ts b/examples/piston-train-toy/src/lib/train/model/modules/mlp.ts new file mode 100644 index 00000000..6569fa28 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/modules/mlp.ts @@ -0,0 +1,46 @@ +import type { MLPConfig } from '$lib/workspace/config'; + +import { nn, Tensor } from '@piston-ml/piston-web'; + +export class MLP extends nn.Module { + private readonly gateProj: nn.Linear | undefined; + private readonly upProj: nn.Linear; + private readonly downProj: nn.Linear; + private readonly activation: (x: Tensor) => Tensor; + private readonly config: MLPConfig; + + /** + * @param config - Model configuration + */ + constructor(nEmbed: number, mlpConfig: MLPConfig) { + super(); + + this.config = mlpConfig; + + const intermediateSize = this.config.hiddenExpansionFactor * nEmbed; + + if (this.config.variant === 'gated') { + this.gateProj = new nn.Linear(nEmbed, intermediateSize); + } + + this.upProj = new nn.Linear(nEmbed, intermediateSize); + this.downProj = new nn.Linear(intermediateSize, nEmbed); + + this.activation = (x: Tensor): Tensor => x[mlpConfig.activation](); + } + + /** + * Forward pass through the MLP + * @param input - Input tensor + * @returns Output tensor + */ + forward(input: Tensor): Tensor { + let h = this.upProj.forward(input); + h = this.activation(h); + if (this.gateProj) { + const gate = this.gateProj.forward(input); + h = h.mul(gate); + } + return this.downProj.forward(h); + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/modules/norm.ts b/examples/piston-train-toy/src/lib/train/model/modules/norm.ts new file mode 100644 index 00000000..a2e21808 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/modules/norm.ts @@ -0,0 +1,15 @@ +import type { LayerNormalizationConfig } from '$lib/workspace/config'; + +import { nn } from '@piston-ml/piston-web'; + +export type NormModule = nn.LayerNorm | nn.RMSNorm; + +export function createNorm(normalizedShape: number, config: LayerNormalizationConfig): NormModule { + if (config.type === 'layernorm') { + return new nn.LayerNorm(normalizedShape, { eps: config.eps }); + } else if (config.type === 'rmsnorm') { + return new nn.RMSNorm(normalizedShape, { eps: config.eps }); + } else { + throw new Error(`Unknown norm type: ${config.type}`); + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/modules/oneHot.ts b/examples/piston-train-toy/src/lib/train/model/modules/oneHot.ts new file mode 100644 index 00000000..ea5cf045 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/modules/oneHot.ts @@ -0,0 +1,14 @@ +import { float32, nn, oneHot, type Tensor } from '@piston-ml/piston-web'; + +export class OneHotEmbedding extends nn.Module { + private readonly vocabSize: number; + + constructor(vocabSize: number) { + super(); + this.vocabSize = vocabSize; + } + + forward(inputIds: Tensor): Tensor { + return oneHot(inputIds, this.vocabSize).cast(float32); + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/modules/positional.ts b/examples/piston-train-toy/src/lib/train/model/modules/positional.ts new file mode 100644 index 00000000..ae312b5b --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/modules/positional.ts @@ -0,0 +1,47 @@ +import { arange, cat, float32, gpu, nn, type Tensor } from '@piston-ml/piston-web'; + +export interface SinusoidalPositionalEncodingConfig { + dropout?: number; + maxLen?: number; +} + +/** + * Implements sinusoidal positional encodings as described in "Attention Is All You Need". + */ +export class SinusoidalEncoding extends nn.Module<[Tensor], Tensor> { + private dropout: nn.Dropout; + private pe: nn.Buffer; + + constructor(dModel: number, config: SinusoidalPositionalEncodingConfig = {}) { + super(); + const { dropout = 0.1, maxLen = 500 } = config; + this.dropout = new nn.Dropout(dropout); + + // Create positional encoding matrix + const position = arange({ end: maxLen, dtype: float32, device: gpu }).unsqueeze(1); + const divTerm = arange({ end: dModel, dtype: float32, device: gpu }) + .mul(-Math.log(10000.0) / dModel) + .exp(); + const angles = position.mul(divTerm.unsqueeze(0)); + + // Build [sin, cos] pairs and flatten -> (max_len, d_model) + const pe = cat([angles.sin(), angles.cos()], { dim: -1 }).flatten({ startDim: 1 }).unsqueeze(0); + + this.pe = new nn.Buffer(pe); + } + + forward(x: Tensor, offset: number = 0): Tensor { + // x shape: (batch, seqLen, dModel) + const seqLen = x.size(1); + const start = offset; + const end = offset + seqLen; + x = x.add( + this.pe.slice([ + [0, 1], + [start, end], + [0, x.size(2)] + ]) + ); + return this.dropout.forward(x); + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/modules/utils.ts b/examples/piston-train-toy/src/lib/train/model/modules/utils.ts new file mode 100644 index 00000000..e00a5bbb --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/modules/utils.ts @@ -0,0 +1,5 @@ +import type { Tensor } from '@piston-ml/piston-web'; + +export function applySoftcap(logits: Tensor, softcap: number): Tensor { + return logits.div(softcap).tanh().mul(softcap); +} diff --git a/examples/piston-train-toy/src/lib/train/model/rnn.ts b/examples/piston-train-toy/src/lib/train/model/rnn.ts new file mode 100644 index 00000000..2dfc8e3e --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/rnn.ts @@ -0,0 +1,958 @@ +/** + * @fileoverview Decoder-only RNN models (LSTM/GRU) styled similarly to GPT + */ + +import type { + Config, + LayerNormalizationConfig, + MultiplicativeRNNAttentionConfig, + RNNAttentionConfig +} from '$lib/workspace/config'; +import type { CrossEntropyLoss, Tensor } from '@piston-ml/piston-web'; + +import * as piston from '@piston-ml/piston-web'; +import { nn } from '@piston-ml/piston-web'; + +import { MLMHead } from './bidirectional'; +import { buildRNNConfigCommon, type RNNModuleConfig } from './config'; +import { createNorm, type NormModule } from './modules/norm'; +import { + buildLmHeadRNN, + computeAutoregressiveCrossEntropyLoss, + createCrossEntropyCriterion, + createPossiblyOneHotRNNWordEmbedding, + maskedFill, + type RNNWordEmbedding +} from './utils'; + +export type RNNEncoderConfig = RNNModuleConfig & { + typeVocabSize: number; + nLayers: number; + bidirectional: boolean; + mlmHead: { present: boolean; shareEmbeddings: boolean }; +}; + +export type RNNDecoderConfig = RNNModuleConfig & { + nLayers: number; +}; + +export type RNNEncoderDecoderConfig = RNNModuleConfig & { + nEncoderLayer: number; + nDecoderLayer: number; + bidirectional: boolean; + encoderDecoderAttention: RNNAttentionConfig; +}; + +type RNNCellType = GRUCell | LSTMCell | RNNCell; + +// Optional attention inputs for sequence decoding inside BaseRNN +type RNNForwardAttentionOptions = { + attentionModule: AdditiveRNNAttention | MultiplicativeRNNAttention; + encoderOutputs: Tensor; + srcPaddingMask?: Tensor | null; + outProjection: nn.Linear; // projects concat([hidden, context]) -> hidden + inputFeedingProjection?: nn.Linear; // projects concat([x_t, prevContext]) -> x_t + decoderStateProjection?: nn.Linear; // projects decoder state H -> baseHidden if needed +}; + +abstract class BaseRNN extends nn.Module { + protected readonly layer: nn.ModuleList; + protected readonly interLayerDropout?: nn.Dropout; + protected readonly interLayerNorms?: nn.ModuleList; + protected readonly nLayers: number; + protected readonly bidirectional: boolean; + protected readonly hiddenSize: number; + protected readonly projectionSize?: number; + protected readonly projections?: nn.ModuleList< + nn.ModuleDict<{ forwardProj: nn.Linear; reverseProj?: nn.Linear }>[] + >; + + constructor( + inputSize: number, + hiddenSize: number, + layerNormalization: LayerNormalizationConfig, + options: RNNOptions = {} + ) { + super(); + const nLayers = options.nLayers ?? 1; + const bidirectional = options.bidirectional ?? false; + const dropout = options.dropout ?? 0; + const projectionSize = options.projectionSize; + + this.nLayers = nLayers; + this.bidirectional = bidirectional; + this.hiddenSize = hiddenSize; + this.projectionSize = projectionSize; + + const cells: CellType[] = []; + for (let l = 0; l < nLayers; l++) { + // Input size calculation: first layer uses inputSize, subsequent layers use previous layer + // output size + let layerInputSize: number; + if (l === 0) { + layerInputSize = inputSize; + } else { + // Previous layer output size depends on projection and bidirectionality + const prevLayerOutputSize = projectionSize || hiddenSize; + layerInputSize = bidirectional ? prevLayerOutputSize * 2 : prevLayerOutputSize; + } + cells.push(this.createCell(layerInputSize, hiddenSize, layerNormalization)); + } + this.layer = new nn.ModuleList(cells); + + // Create projection layers if projectionSize is specified + if (projectionSize) { + const projLayers: nn.ModuleDict<{ forwardProj: nn.Linear; reverseProj?: nn.Linear }>[] = []; + for (let l = 0; l < nLayers; l++) { + const layerProjs: { forwardProj: nn.Linear; reverseProj?: nn.Linear } = { + forwardProj: new nn.Linear(hiddenSize, projectionSize) + }; + if (bidirectional) { + layerProjs.reverseProj = new nn.Linear(hiddenSize, projectionSize); + } + projLayers.push(new nn.ModuleDict(layerProjs)); + } + this.projections = new nn.ModuleList(projLayers); + } + + if (layerNormalization.rnn.betweenLayers && nLayers > 1) { + const normDim = (bidirectional ? 2 : 1) * (projectionSize ?? hiddenSize); + const norms: nn.Module[] = []; + for (let l = 0; l < nLayers - 1; l++) { + norms.push( + createNorm(normDim, layerNormalization) as unknown as nn.Module + ); + } + this.interLayerNorms = new nn.ModuleList(norms); + } + + if (dropout > 0) { + this.interLayerDropout = new nn.Dropout(dropout); + } + + this.resetParameters(); + } + + resetParameters(): void { + // This is consistent with what PyTorch does internally + // Initialize only linear layers; leave norms at their defaults (gamma=1, beta=0) + const stdv = 1.0 / Math.sqrt(this.hiddenSize); + this.apply((module: nn.Module) => { + if (module instanceof nn.Linear) { + piston.initUniform_(module.weight, { low: -stdv, high: stdv }); + if (module.bias) { + piston.initUniform_(module.bias, { low: -stdv, high: stdv }); + } + } + }); + } + + protected abstract createCell( + inputSize: number, + hiddenSize: number, + layerNormalization: LayerNormalizationConfig + ): CellType; + + private stepThroughCell( + cell: CellType, + x_t: Tensor, + hPrev: Tensor, + cPrev: Tensor | null + ): [Tensor, Tensor | null] { + if (cell instanceof LSTMCell) { + const [hNew, cNew] = cell.forward(x_t, hPrev, cPrev!); + return [hNew, cNew]; + } else { + const hNew = (cell as GRUCell | RNNCell).forward(x_t, hPrev); + return [hNew, null]; + } + } + + private applyProjection( + projection: nn.Linear | undefined, + outputs: Tensor, + batchSize: number, + seqLen: number + ): Tensor { + if (!projection) { + return outputs; + } + return projection + .forward(outputs.view([-1, this.hiddenSize])) + .view([batchSize, seqLen, this.projectionSize!]); + } + + forward( + x: Tensor, + initial?: Parameters[1], + attention?: RNNForwardAttentionOptions + ): [Tensor, Tensor, Tensor | null] { + if (attention && this.bidirectional) { + throw new Error('Attention decoding with bidirectional RNN is not supported'); + } + + let layerInput = x; + + // Track final states for each layer + const finalHiddenStates: Tensor[] = []; + const finalCellStates: (Tensor | null)[] = []; + + for (let l = 0; l < this.nLayers; l++) { + const [layerOutput, layerFinalH, layerFinalC] = this.runSingleLayer( + layerInput, + l, + l === this.nLayers - 1 ? initial : undefined, + l === this.nLayers - 1 ? attention : undefined + ); + + finalHiddenStates.push(layerFinalH); + finalCellStates.push(layerFinalC); + + // Apply between-layer normalization and dropout (excluding the last layer) + if (l < this.nLayers - 1) { + let nextInput: Tensor = layerOutput; + if (this.interLayerNorms) { + nextInput = this.interLayerNorms[l]!.forward(nextInput) as Tensor; + } + if (this.interLayerDropout) { + nextInput = this.interLayerDropout.forward(nextInput) as Tensor; + } + layerInput = nextInput; + } else { + layerInput = layerOutput; + } + } + + // Return the output of the last layer and concatenated final states + const finalH = piston.cat(finalHiddenStates, { dim: 0 }); + let finalC: Tensor | null = null; + if (finalCellStates.some((c) => c !== null)) { + const validCellStates = finalCellStates.filter((c) => c !== null) as Tensor[]; + if (validCellStates.length > 0) { + finalC = piston.cat(validCellStates, { dim: 0 }); + } + } + + return [layerInput, finalH, finalC]; + } + + private runSingleLayer( + yIn: Tensor, + layerIdx: number, + initial?: Tensor | [Tensor, Tensor], + attention?: RNNForwardAttentionOptions + ): [Tensor, Tensor, Tensor | null] { + const [batchSize, seqLen, _hiddenSize] = yIn.size(); + const cell = this.layer[layerIdx]!; + + // Get the projection module dict for this layer + const layerProjections = + this.projectionSize && this.projections ? this.projections[layerIdx] : null; + + const initializeStates = ( + initialForLayer?: Tensor | [Tensor, Tensor] + ): { + hState: Tensor; + cState: Tensor | null; + } => { + if (initialForLayer) { + if (Array.isArray(initialForLayer)) { + return { hState: initialForLayer[0], cState: initialForLayer[1] }; + } else { + return { hState: initialForLayer, cState: null }; + } + } else { + // Create initial state directly with correct hidden size (avoid slicing yIn) + const hState = piston.zeros([batchSize, this.hiddenSize], { device: yIn.device }); + const cState = cell instanceof LSTMCell ? piston.zerosLike(hState) : null; + return { hState, cState }; + } + }; + + const runDirection = ( + seq: Tensor, + allowAttention: boolean, + initialForLayer?: Tensor | [Tensor, Tensor] + ): [Tensor, Tensor, Tensor | null] => { + const { hState, cState } = initializeStates(initialForLayer); + let currentH = hState; + let currentC = cState; + let prevContext: Tensor | null = null; + const outputs: Tensor[] = []; + + for (let t = 0; t < seqLen; t++) { + let x_t = seq + .slice([ + [0, batchSize], + [t, t + 1], + [0, seq.size(2)] + ]) + .squeeze({ dim: 1 }); + + if (allowAttention && attention && attention.inputFeedingProjection && prevContext) { + const cat = piston.cat([x_t, prevContext], { dim: 1 }); + x_t = attention.inputFeedingProjection.forward(cat); + } + + const [newH, newC] = this.stepThroughCell(cell, x_t, currentH, currentC); + currentH = newH; + currentC = newC; + + let stepOut: Tensor; + if (allowAttention && attention) { + const query = attention.decoderStateProjection + ? attention.decoderStateProjection.forward(currentH) + : currentH; + const [context] = attention.attentionModule.forward( + query, + attention.encoderOutputs, + attention.srcPaddingMask ?? null + ); + const combined = piston.cat([query, context], { dim: 1 }); + stepOut = attention.outProjection.forward(combined); + prevContext = context; + } else { + stepOut = currentH; + } + + outputs.push(stepOut.unsqueeze(1)); + } + + const y = piston.cat(outputs, { dim: 1 }); + return [y, currentH, currentC]; + }; + + // Forward direction + const [yF, lastHF, lastCF] = runDirection(yIn, !!attention, initial); + + const projectedOutputsForward = this.applyProjection( + layerProjections?.dict.forwardProj, + yF, + batchSize, + seqLen + ); + const projectedLastHForward = layerProjections?.dict.forwardProj?.forward(lastHF) ?? lastHF; + + if (!this.bidirectional) { + return [projectedOutputsForward, projectedLastHForward, lastCF]; + } + + // Backward direction (reverse sequence) + const [yB, lastHB, lastCB] = runDirection(yIn.flip(1), false); + // Flip back to align with forward + const yBFlipped = yB.flip(1); + + // Apply projections if specified + const projectedOutputsReverse = this.applyProjection( + layerProjections?.dict.reverseProj, + yBFlipped, + batchSize, + seqLen + ); + + const projectedLastHB = layerProjections?.dict.reverseProj?.forward(lastHB) ?? lastHB; + + // Concatenate forward and backward outputs + const y = piston.cat([projectedOutputsForward, projectedOutputsReverse], { dim: 2 }); + const lastH = piston.cat([projectedLastHForward, projectedLastHB], { dim: 1 }); + + let lastC: Tensor | null = null; + if (lastCF && lastCB) { + lastC = piston.cat([lastCF, lastCB], { dim: 1 }); + } + + return [y, lastH, lastC]; + } +} + +export class GRUCell extends nn.Module { + readonly WGates: nn.Linear; + readonly WCandidate: nn.Linear; + readonly normGates?: NormModule; + readonly normCandidate?: NormModule; + + constructor(inputSize: number, hiddenSize: number, layerNormalization: LayerNormalizationConfig) { + super(); + + this.WGates = new nn.Linear(inputSize + hiddenSize, 2 * hiddenSize); + this.WCandidate = new nn.Linear(inputSize + hiddenSize, hiddenSize); + + if (layerNormalization.rnn.withinCell) { + this.normGates = createNorm(2 * hiddenSize, layerNormalization); + this.normCandidate = createNorm(hiddenSize, layerNormalization); + } + } + + // Unified step API used by sequence runners + forward(x: Tensor, hPrev: Tensor): Tensor { + const combined = piston.cat([x, hPrev], { dim: 1 }); + let gates = this.WGates.forward(combined); + if (this.normGates) { + gates = this.normGates.forward(gates); + } + let [r, z] = piston.chunk(gates, 2, { dim: 1 }); + r = piston.sigmoid(r); + z = piston.sigmoid(z); + const combinedCandidate = piston.cat([x, r.mul(hPrev)], { dim: 1 }); + let candidate = this.WCandidate.forward(combinedCandidate); + if (this.normCandidate) { + candidate = this.normCandidate.forward(candidate); + } + const hTilde = piston.tanh(candidate); + const hNext = hPrev.mul(z.neg().add(1)).add(hTilde.mul(z)); + return hNext; + } +} + +export class LSTMCell extends nn.Module { + readonly W: nn.Linear; + readonly normGates?: NormModule; + readonly normCell?: NormModule; + + constructor(inputSize: number, hiddenSize: number, layerNormalization: LayerNormalizationConfig) { + super(); + this.W = new nn.Linear(inputSize + hiddenSize, 4 * hiddenSize); + if (layerNormalization.rnn.withinCell) { + this.normGates = createNorm(4 * hiddenSize, layerNormalization); + this.normCell = createNorm(hiddenSize, layerNormalization); + } + } + + // Unified step API used by sequence runners + forward(x: Tensor, hPrev: Tensor, cPrev: Tensor): [Tensor, Tensor] { + const combined = piston.cat([x, hPrev], { dim: 1 }); + let gateOutputs = this.W.forward(combined); + if (this.normGates) { + gateOutputs = this.normGates.forward(gateOutputs) as Tensor; + } + let [i, f, g, o] = piston.chunk(gateOutputs, 4, { dim: 1 }); + i = piston.sigmoid(i); + f = piston.sigmoid(f); + g = piston.tanh(g); + o = piston.sigmoid(o); + let cNext = f.mul(cPrev).add(i.mul(g)); + if (this.normCell) { + cNext = this.normCell.forward(cNext) as Tensor; + } + const hNext = o.mul(piston.tanh(cNext)); + return [hNext, cNext]; + } +} + +export class RNNCell extends nn.Module { + readonly WIh: nn.Linear; + readonly WHh: nn.Linear; + readonly normPreact?: NormModule; + constructor(inputSize: number, hiddenSize: number, layerNormalization: LayerNormalizationConfig) { + super(); + this.WIh = new nn.Linear(inputSize, hiddenSize); + this.WHh = new nn.Linear(hiddenSize, hiddenSize); + if (layerNormalization.rnn.withinCell) { + this.normPreact = createNorm(hiddenSize, layerNormalization); + } + } + + // Unified step API used by sequence runners + forward(x: Tensor, hPrev: Tensor): Tensor { + let preact = this.WIh.forward(x).add(this.WHh.forward(hPrev)); + if (this.normPreact) { + preact = this.normPreact.forward(preact) as Tensor; + } + return preact.tanh(); + } +} + +export interface RNNOptions { + nLayers?: number; + bidirectional?: boolean; + dropout?: number; + projectionSize?: number; +} + +class GRU extends BaseRNN { + protected createCell( + inputSize: number, + hiddenSize: number, + layerNormalization: LayerNormalizationConfig + ): GRUCell { + return new GRUCell(inputSize, hiddenSize, layerNormalization); + } +} + +class LSTM extends BaseRNN { + protected createCell( + inputSize: number, + hiddenSize: number, + layerNormalization: LayerNormalizationConfig + ): LSTMCell { + return new LSTMCell(inputSize, hiddenSize, layerNormalization); + } +} + +class RNN extends BaseRNN { + protected createCell( + inputSize: number, + hiddenSize: number, + layerNormalization: LayerNormalizationConfig + ): RNNCell { + return new RNNCell(inputSize, hiddenSize, layerNormalization); + } +} + +export function createRNN( + cellType: 'gru' | 'lstm' | 'rnn', + inputSize: number, + hiddenSize: number, + layerNormalization: LayerNormalizationConfig, + options?: RNNOptions +): GRU | LSTM | RNN { + switch (cellType) { + case 'gru': + return new GRU(inputSize, hiddenSize, layerNormalization, options); + case 'lstm': + return new LSTM(inputSize, hiddenSize, layerNormalization, options); + case 'rnn': + return new RNN(inputSize, hiddenSize, layerNormalization, options); + default: + throw new Error(`Invalid RNN cell type: ${cellType}`); + } +} + +class AdditiveRNNAttention extends nn.Module { + private readonly Wh: nn.Linear; + private readonly Ws: nn.Linear; + private readonly v: nn.Linear; // projects attn dim -> 1 + + constructor(hiddenSize: number, attnDim: number) { + super(); + this.Wh = new nn.Linear(hiddenSize, attnDim); + this.Ws = new nn.Linear(hiddenSize, attnDim); + this.v = new nn.Linear(attnDim, 1); + } + + forward( + decoderState: Tensor, + encoderOutputs: Tensor, + srcPaddingMask: Tensor | null = null + ): [Tensor, Tensor] { + const [B, S, _H] = encoderOutputs.size(); + const projEnc = this.Wh.forward(encoderOutputs); // [B,S,A] + const projDec = this.Ws.forward(decoderState) + .unsqueeze(1) + .broadcastTo([B, S, projEnc.size(2)]); // [B,S,A] + const e = projEnc.add(projDec).tanh(); // [B,S,A] + let scores = this.v.forward(e).squeeze({ dim: -1 }); // [B,S] + if (srcPaddingMask) { + const mask = srcPaddingMask; + scores = maskedFill(scores, mask, -1e9); + } + const alpha = scores.softmax(1); // [B,S] + const context = encoderOutputs.mul(alpha.unsqueeze(-1)).sum({ dim: 1 }); // [B,H] + return [context, alpha]; + } +} + +class MultiplicativeRNNAttention extends nn.Module { + private readonly Wa: nn.Linear; // general form s^T Wa h + private readonly config: MultiplicativeRNNAttentionConfig; + constructor(hiddenSize: number, config: MultiplicativeRNNAttentionConfig) { + super(); + this.Wa = new nn.Linear(hiddenSize, hiddenSize, false); + this.config = config; + } + + forward( + decoderState: Tensor, + encoderOutputs: Tensor, + srcPaddingMask: Tensor | null = null + ): [Tensor, Tensor] { + const [_B, _S, H] = encoderOutputs.size(); + const encProj = this.Wa.forward(encoderOutputs); // [B,S,H] + let scores = encProj.mul(decoderState.unsqueeze(1)).sum({ dim: -1 }); // [B,S] + if (this.config.scaleByInverseSqrtHiddenSize) { + scores = scores.div(Math.sqrt(H)); + } + if (srcPaddingMask) { + const mask = srcPaddingMask; + scores = maskedFill(scores, mask, -1e9); + } + const alpha = scores.softmax(1); // [B,S] + const context = encoderOutputs.mul(alpha.unsqueeze(-1)).sum({ dim: 1 }); // [B,H] + return [context, alpha]; + } +} + +export class RNNDecoder extends nn.Module { + public config: RNNDecoderConfig; + private readonly wordEmbeddings: RNNWordEmbedding; + private readonly embeddingDropout?: nn.Dropout; + private readonly decoder: GRU | LSTM | RNN; + readonly lmHead: nn.Module<[Tensor], Tensor>; + private readonly criterion: CrossEntropyLoss; + + constructor(config: Config, vocabSize: number) { + super(); + + this.config = { ...buildRNNConfigCommon(config, vocabSize), nLayers: config.model.layers }; + + this.embeddingDropout = this.config.dropout.present + ? new nn.Dropout(this.config.dropout.embedding) + : undefined; + + // Embedding selection: learned vs one-hot + this.wordEmbeddings = createPossiblyOneHotRNNWordEmbedding( + this.config.embedding, + this.config.vocabSize, + this.config.embeddingSize + ); + + // Build the RNN stack once so parameters are registered and trainable + this.decoder = createRNN( + this.config.cellType, + this.config.embeddingSize, + this.config.hiddenSize, + this.config.layerNormalization, + { + nLayers: this.config.nLayers, + // Decoder-only, so no bidirectional + bidirectional: false, + dropout: this.config.dropout.rnn.interLayer, + projectionSize: this.config.projectionSize + } + ); + + this.lmHead = buildLmHeadRNN(this.config, this.wordEmbeddings); + this.criterion = createCrossEntropyCriterion(config); + } + + /** + * @param input - Input tensor of token IDs [batch_size, seq_len] + * @param targets - Target tensor of token IDs [batch_size, seq_len] + * @returns [logits, loss] + */ + forward( + input: Tensor, + { targets }: { targets: Tensor | null } = { targets: null } + ): [Tensor, Tensor | null] { + const [_batchSize, seqLen] = input.size(); + if (!seqLen) { + throw new Error( + 'Input tensor has no sequence length (did you forget to pass input as batches?)' + ); + } + + // Embedding + let embeddings = this.wordEmbeddings.forward(input); + + if (this.embeddingDropout) { + embeddings = this.embeddingDropout.forward(embeddings) as Tensor; + } + + // Process full sequence via pre-built core RNN + const [hiddenStatesStacked] = this.decoder.forward(embeddings); + + // Project to vocabulary + const logits = this.lmHead.forward(hiddenStatesStacked); + + const loss = computeAutoregressiveCrossEntropyLoss(logits, targets, this.criterion); + + return [logits, loss ?? null]; + } +} + +export class RNNEncoder extends nn.Module { + public config: RNNEncoderConfig; + private readonly wordEmbedding: RNNWordEmbedding; + private readonly tokenTypeEmbeddings?: nn.Embedding; + private readonly embeddingDropout?: nn.Dropout; + private readonly encoder: GRU | LSTM | RNN; + private readonly criterion: CrossEntropyLoss; + private readonly mlmHead?: MLMHead; + private readonly mlmPreProj?: nn.Linear; + + constructor(config: Config, vocabSize: number) { + super(); + + this.config = { + ...buildRNNConfigCommon(config, vocabSize), + nLayers: config.model.layers, + bidirectional: config.model.rnn.encoder.bidirectional, + typeVocabSize: 2, + mlmHead: { + present: true, + shareEmbeddings: config.model.tieEmbeddingsAndLmHead + } + }; + + this.embeddingDropout = this.config.dropout.present + ? new nn.Dropout(this.config.dropout.embedding) + : undefined; + this.wordEmbedding = createPossiblyOneHotRNNWordEmbedding( + this.config.embedding, + this.config.vocabSize, + this.config.embeddingSize + ); + this.tokenTypeEmbeddings = new nn.Embedding( + this.config.typeVocabSize, + this.config.embeddingSize + ); + + // Build the encoder RNN once so params are registered + this.encoder = createRNN( + this.config.cellType, + this.config.embeddingSize, + this.config.hiddenSize, + this.config.layerNormalization, + { + nLayers: this.config.nLayers, + bidirectional: this.config.bidirectional, + dropout: this.config.dropout.rnn.interLayer, + projectionSize: this.config.hiddenStateProjection.present + ? this.config.hiddenStateProjection.size + : undefined + } + ); + + if (this.config.mlmHead.present) { + const finalHiddenSize = this.config.bidirectional + ? this.config.baseHiddenSize * 2 + : this.config.baseHiddenSize; + const canTie = + this.config.embedding.type === 'learned' && this.config.mlmHead.shareEmbeddings; + // If tying and dims differ, add pre-projection to embedding size + if (canTie && finalHiddenSize !== this.config.embeddingSize) { + this.mlmPreProj = new nn.Linear(finalHiddenSize, this.config.embeddingSize); + } + this.mlmHead = new MLMHead( + canTie ? this.config.embeddingSize : finalHiddenSize, + this.config.vocabSize, + this.config.layerNormalization, + canTie ? (this.wordEmbedding as nn.Embedding) : undefined + ); + } + + this.criterion = createCrossEntropyCriterion(config); + } + + forward( + inputIds: Tensor, + { tokenTypeIds, targets }: { tokenTypeIds?: Tensor | null; targets?: Tensor | null } = { + tokenTypeIds: null, + targets: null + } + ): [Tensor, Tensor | null, Tensor | null] { + const [batchSize, seqLen] = inputIds.size(); + if (!seqLen) { + throw new Error( + 'Input tensor has no sequence length (did you forget to pass input as batches?)' + ); + } + + // Get token embeddings + let wordEmbeddings = this.wordEmbedding.forward(inputIds); + + // Add segment/token type embeddings + if (this.tokenTypeEmbeddings) { + if (tokenTypeIds == null) { + tokenTypeIds = piston.zeros([batchSize, seqLen], { + device: inputIds.device, + dtype: piston.int32 + }); + } + const typeEmbeddings = this.tokenTypeEmbeddings.forward(tokenTypeIds!); + wordEmbeddings = wordEmbeddings.add(typeEmbeddings); + } + + if (this.embeddingDropout) { + wordEmbeddings = this.embeddingDropout.forward(wordEmbeddings) as Tensor; + } + + let x = wordEmbeddings; + const [xSeq] = this.encoder.forward(x); + x = xSeq; + + // If present, reconcile dims for tied MLM head + if (this.mlmPreProj) { + x = this.mlmPreProj.forward(x) as Tensor; + } + + // MLM head + let mlmLogits: Tensor | null = null; + if (this.mlmHead) { + mlmLogits = this.mlmHead.forward(x); + } + + // Calculate MLM loss if targets are provided + let loss: Tensor | null = null; + if (targets && mlmLogits) { + // Only compute loss on masked positions (where targets != -100) + const flatLogits = mlmLogits.view([-1, mlmLogits.size(-1)]); + const flatTargets = targets.view(-1); + loss = this.criterion.forward(flatLogits, flatTargets); + } + + return [x, mlmLogits, loss]; + } +} + +export class RNNEncoderDecoder extends nn.Module { + public config: RNNEncoderDecoderConfig; + + private readonly criterion: CrossEntropyLoss; + readonly lmHead: nn.Module<[Tensor], Tensor>; + private readonly encoderEmbedding: RNNWordEmbedding; + private readonly decoderEmbedding: RNNWordEmbedding; + private readonly encoder: GRU | LSTM | RNN; + private readonly decoder: GRU | LSTM | RNN; + private readonly attention?: AdditiveRNNAttention | MultiplicativeRNNAttention; + private readonly outProj: nn.Linear; + private readonly decoderInputProj?: nn.Linear; + private readonly decoderStateProj?: nn.Linear; + private readonly encCombineProjections?: nn.ModuleList; + + constructor(config: Config, vocabSize: number) { + super(); + + this.config = { + ...buildRNNConfigCommon(config, vocabSize), + nEncoderLayer: config.model.encoderDecoder.encoderLayers, + nDecoderLayer: config.model.encoderDecoder.decoderLayers, + bidirectional: config.model.rnn.encoder.bidirectional, + encoderDecoderAttention: config.model.rnn.encoderDecoderAttention + }; + + // Effective hidden width used throughout encoder/decoder stacks + const baseHidden = this.config.baseHiddenSize; + + this.encoderEmbedding = createPossiblyOneHotRNNWordEmbedding( + this.config.embedding, + this.config.vocabSize, + this.config.embeddingSize + ); + this.decoderEmbedding = createPossiblyOneHotRNNWordEmbedding( + this.config.embedding, + this.config.vocabSize, + this.config.embeddingSize + ); + + // Build encoder/decoder stacks once so params are registered + this.encoder = createRNN( + this.config.cellType, + this.config.embeddingSize, + this.config.hiddenSize, + this.config.layerNormalization, + { + nLayers: this.config.nEncoderLayer, + bidirectional: this.config.bidirectional, + dropout: this.config.dropout.rnn.interLayer, + projectionSize: this.config.projectionSize + } + ); + + this.decoder = createRNN( + this.config.cellType, + this.config.embeddingSize, + this.config.hiddenSize, + this.config.layerNormalization, + { + nLayers: this.config.nDecoderLayer, + bidirectional: false, + dropout: this.config.dropout.rnn.interLayer, + projectionSize: this.config.projectionSize + } + ); + + // Encoder bidirectionality support: per-layer 2H->H projections + if (this.config.bidirectional) { + this.encCombineProjections = new nn.ModuleList( + Array.from( + { length: this.config.nEncoderLayer }, + () => new nn.Linear(baseHidden * 2, baseHidden) as nn.Module + ) + ); + } + + // Attention selection + if (config.model.rnn.encoderDecoderAttention?.present) { + const attnType = config.model.rnn.encoderDecoderAttention.type; + if (attnType === 'additive') { + this.attention = new AdditiveRNNAttention(baseHidden, baseHidden); + } else { + this.attention = new MultiplicativeRNNAttention( + baseHidden, + this.config.encoderDecoderAttention.multiplicative + ); + } + if (config.model.rnn.encoderDecoderAttention.inputFeedingProjection) { + const topIn = + this.config.nDecoderLayer === 1 ? this.config.embeddingSize : this.config.baseHiddenSize; + this.decoderInputProj = new nn.Linear(topIn + baseHidden, topIn); + } + } + + // Attention out projection maps back to the cell hidden size; projection (if any) + // will be applied afterwards by BaseRNN + this.outProj = new nn.Linear(baseHidden * 2, this.config.hiddenSize); + + // If hidden state differs from baseHidden (due to projection), project decoder + // state to baseHidden for attention computations + if (this.config.hiddenSize !== baseHidden) { + this.decoderStateProj = new nn.Linear(this.config.hiddenSize, baseHidden); + } + this.lmHead = buildLmHeadRNN(this.config, this.decoderEmbedding); + + this.criterion = createCrossEntropyCriterion(config); + } + + encode(inputIdsSrc: Tensor): Tensor { + const [_batchSize, seqLen] = inputIdsSrc.size(); + if (!seqLen) { + throw new Error('Source input tensor has no sequence length'); + } + const x = this.encoderEmbedding.forward(inputIdsSrc); // [B,S,H] + const [encSeq] = this.encoder.forward(x); // [B,S, H or 2H] + if (this.config.bidirectional && this.encCombineProjections) { + const proj = this.encCombineProjections[this.config.nEncoderLayer - 1]!; + return proj.forward(encSeq); + } + return encSeq; + } + + forward( + inputIdsSrc: Tensor, + inputIdsTgt: Tensor, + { + targets, + srcPaddingMask, + tgtPaddingMask: _tgtPaddingMask, + encoderHiddenStates + }: { + targets?: Tensor | null; + srcPaddingMask?: Tensor | null; + tgtPaddingMask?: Tensor | null; + encoderHiddenStates?: Tensor | null; + } = { + targets: null, + srcPaddingMask: null, + tgtPaddingMask: null, + encoderHiddenStates: null + } + ): [Tensor, Tensor | null] { + const encStates = encoderHiddenStates ?? this.encode(inputIdsSrc); + const y = this.decoderEmbedding.forward(inputIdsTgt); // [B,T,H] + const attentionOptions: RNNForwardAttentionOptions | undefined = this.attention + ? { + attentionModule: this.attention, + encoderOutputs: encStates, + srcPaddingMask: srcPaddingMask ?? null, + outProjection: this.outProj, + inputFeedingProjection: this.decoderInputProj, + decoderStateProjection: this.decoderStateProj + } + : undefined; + const [hiddenStates] = this.decoder.forward(y, undefined, attentionOptions); + const logits = this.lmHead.forward(hiddenStates); + const loss = computeAutoregressiveCrossEntropyLoss(logits, targets ?? null, this.criterion); + return [logits, loss]; + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/transformer.ts b/examples/piston-train-toy/src/lib/train/model/transformer.ts new file mode 100644 index 00000000..b1df2fa7 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/transformer.ts @@ -0,0 +1,606 @@ +/** + * @fileoverview Implementation of generic encoder-decoder, encoder-only, and decoder-only + * transformer models + */ + +import type { Config } from '$lib/workspace/config'; +import type { CrossEntropyLoss, Tensor } from '@piston-ml/piston-web'; + +import { nn } from '@piston-ml/piston-web'; +import * as piston from '@piston-ml/piston-web'; + +import type { DecoderKVCache } from './cache'; +import type { SinusoidalEncoding } from './modules/positional'; + +import { MLMHead, Pooler } from './bidirectional'; +import { createEmptyDecoderKVCache } from './cache'; +import { buildTransformerConfigCommon, type TransformerModuleConfig } from './config'; +import { DecoderLayer } from './modules/decoder'; +import { EncoderLayer } from './modules/encoder'; +import { + addPositionalEncodingToEmbeddings as addPositionalEncodingToEmbedding, + buildLmHead, + computeAutoregressiveCrossEntropyLoss, + createCrossEntropyCriterion, + maybeApplyLogitsSoftcap, + maybeCreateFinalLayerNorm, + maybeCreateLearnedPositionEmbedding, + maybeCreateSinusoidalEncoding +} from './utils'; + +export type EncoderDecoderTransformerConfig = TransformerModuleConfig & { + nEncoderLayer: number; + nDecoderLayer: number; + blockSizeSrc: number; + blockSizeTgt: number; +}; + +export type EncoderForEncoderDecoderDict = { + drop?: nn.Dropout; + wordEmbedding: nn.Embedding; + positionEmbedding?: nn.Embedding; + layer: nn.ModuleList; + layerNorm?: nn.Module<[Tensor], Tensor>; +}; + +export type DecoderForEncoderDecoderDict = { + drop?: nn.Dropout; + wordEmbedding: nn.Embedding; + positionEmbedding?: nn.Embedding; + layer: nn.ModuleList; + layerNorm?: nn.Module<[Tensor], Tensor>; +}; + +export type EncoderDecoderDict = { + encoder: nn.ModuleDict; + decoder: nn.ModuleDict; +}; + +/** + * Encoder-Decoder Transformer model implementation + */ +export type EncoderDecoderForwardOptions = { + targets?: Tensor | null; + srcPaddingMask?: Tensor | null; + tgtPaddingMask?: Tensor | null; + encoderHiddenStates?: Tensor | null; + kvCache?: DecoderKVCache | null; +}; + +export class EncoderDecoderTransformer extends nn.Module { + public config: EncoderDecoderTransformerConfig; + readonly lmHead: nn.Linear; + public encoder: nn.ModuleDict; + public decoder: nn.ModuleDict; + private readonly criterion: CrossEntropyLoss; + private readonly sinusoidalEncoding?: SinusoidalEncoding; + + constructor(config: Config, vocabSize: number, blockSizeSrc: number, blockSizeTgt: number) { + super(); + + this.config = { + ...buildTransformerConfigCommon(config, vocabSize), + nEncoderLayer: config.model.encoderDecoder.encoderLayers, + nDecoderLayer: config.model.encoderDecoder.decoderLayers, + blockSizeSrc, + blockSizeTgt + }; + + const encoderDict: EncoderForEncoderDecoderDict = { + drop: this.config.dropout.present ? new nn.Dropout(this.config.dropout.embedding) : undefined, + wordEmbedding: new nn.Embedding(this.config.vocabSize, this.config.embeddingSize), + layer: new nn.ModuleList( + Array.from({ length: this.config.nEncoderLayer }).map(() => new EncoderLayer(this.config)) + ) + }; + + const decoderDict: DecoderForEncoderDecoderDict = { + drop: this.config.dropout.present ? new nn.Dropout(this.config.dropout.embedding) : undefined, + wordEmbedding: new nn.Embedding(this.config.vocabSize, this.config.embeddingSize), + layer: new nn.ModuleList( + Array.from({ length: this.config.nDecoderLayer }).map( + // With cross-attention to consume encoder hidden states + () => new DecoderLayer(this.config, true) + ) + ) + }; + + // Learned positional embeddings (transformer Embedding variant) + encoderDict.positionEmbedding = maybeCreateLearnedPositionEmbedding( + this.config.positionalEncoding, + this.config.blockSizeSrc, + this.config.embeddingSize + ); + decoderDict.positionEmbedding = maybeCreateLearnedPositionEmbedding( + this.config.positionalEncoding, + this.config.blockSizeTgt, + this.config.embeddingSize + ); + + // Final layer norms if configured + encoderDict.layerNorm = maybeCreateFinalLayerNorm( + this.config.embeddingSize, + this.config.layerNormalization + ); + decoderDict.layerNorm = maybeCreateFinalLayerNorm( + this.config.embeddingSize, + this.config.layerNormalization + ); + + // Sinusoidal encoding module if configured + this.sinusoidalEncoding = maybeCreateSinusoidalEncoding( + this.config.positionalEncoding, + this.config.embeddingSize, + this.config.dropout.embedding + ); + + this.encoder = new nn.ModuleDict(encoderDict); + this.decoder = new nn.ModuleDict(decoderDict); + + this.lmHead = buildLmHead( + this.config.embeddingSize, + this.config.vocabSize, + config.model.tieEmbeddingsAndLmHead, + this.decoder.dict.wordEmbedding + ); + + this.criterion = createCrossEntropyCriterion(config); + } + + forward( + inputIdsSrc: Tensor | null, + inputIdsTgt: Tensor, + options: EncoderDecoderForwardOptions = {} + ): [Tensor, Tensor | null] { + const targets = options.targets ?? null; + const srcPaddingMask = options.srcPaddingMask ?? null; + const tgtPaddingMask = options.tgtPaddingMask ?? null; + const explicitEncStates = options.encoderHiddenStates ?? null; + const kvCache = options.kvCache ?? null; + // Encode source sequence if not provided + let finalEncoderHiddenStates: Tensor; + if (explicitEncStates) { + finalEncoderHiddenStates = explicitEncStates; + } else { + if (!inputIdsSrc) { + throw new Error('Either inputIdsSrc or encoderHiddenStates must be provided'); + } + finalEncoderHiddenStates = this.encode(inputIdsSrc, { srcPaddingMask }); + } + + // Decode target sequence + const [_batchSize, tgtSeqLen] = inputIdsTgt.size(); + + if (!tgtSeqLen) { + throw new Error( + 'Target input tensor has no sequence length (did you forget to pass input as batches?)' + ); + } + + // Get target embeddings + let targetWordEmbeddings = this.decoder.dict.wordEmbedding.forward(inputIdsTgt); + + // Use cache length (if any) as position offset for absolute encodings during incremental decoding + const posOffsetDec = kvCache?.layers?.[0]?.self?.length ?? 0; + + targetWordEmbeddings = addPositionalEncodingToEmbedding( + targetWordEmbeddings, + this.config.positionalEncoding, + this.sinusoidalEncoding, + this.decoder.dict.positionEmbedding, + this.decoder.dict.drop, + posOffsetDec + ); + + // Pass through each decoder layer + let hiddenStates = targetWordEmbeddings; + + const useCache = kvCache !== null; + const cacheObj = useCache ? kvCache! : createEmptyDecoderKVCache(this.config.nDecoderLayer); + for (let i = 0; i < this.config.nDecoderLayer; i++) { + const layerModule = this.decoder.dict.layer[i] as DecoderLayer; + const result = layerModule.forward(hiddenStates, { + encoderHiddenStates: finalEncoderHiddenStates, + srcPaddingMask, + tgtPaddingMask, + cache: cacheObj.layers[i] + }); + if (useCache) { + cacheObj.layers[i] = result.pastKeyValues!; + hiddenStates = result.output; + } else { + hiddenStates = result.output; + } + } + + // Apply final layer normalization + if (this.decoder.dict.layerNorm) { + hiddenStates = this.decoder.dict.layerNorm.forward(hiddenStates); + } + + // Project to vocabulary + let logits = this.lmHead.forward(hiddenStates); + + logits = maybeApplyLogitsSoftcap(logits, this.config.normalization); + + const loss = computeAutoregressiveCrossEntropyLoss(logits, targets, this.criterion); + + return [logits, loss]; + } + + /** + * Encode source sequence + * @param inputIdsSrc - Source input tensor + * @param srcPaddingMask - Source padding mask + * @returns Encoder hidden states + */ + encode( + inputIdsSrc: Tensor, + { srcPaddingMask }: { srcPaddingMask: Tensor | null } = { srcPaddingMask: null } + ): Tensor { + const [_batchSize, srcSeqLen] = inputIdsSrc.size(); + + if (!srcSeqLen) { + throw new Error( + 'Source input tensor has no sequence length (did you forget to pass input as batches?)' + ); + } + + // Get source embeddings + let sourceWordEmbeddings = this.encoder.dict.wordEmbedding.forward(inputIdsSrc); + + sourceWordEmbeddings = addPositionalEncodingToEmbedding( + sourceWordEmbeddings, + this.config.positionalEncoding, + this.sinusoidalEncoding, + this.encoder.dict.positionEmbedding, + this.encoder.dict.drop + ); + + // Pass through each encoder layer + let hiddenStates = sourceWordEmbeddings; + + for (let i = 0; i < this.config.nEncoderLayer; i++) { + const layerModule = this.encoder.dict.layer[i] as EncoderLayer; + const layerOutput = layerModule.forward(hiddenStates, srcPaddingMask); + hiddenStates = layerOutput; + } + + // Apply final layer normalization + if (this.encoder.dict.layerNorm) { + hiddenStates = this.encoder.dict.layerNorm.forward(hiddenStates) as Tensor; + } + + return hiddenStates; + } +} + +type DecoderTransformerConfig = TransformerModuleConfig & { + nLayers: number; + blockSize: number; +}; + +type DecoderTransformerDict = { + drop?: nn.Dropout; + wordEmbedding: nn.Embedding; + positionEmbedding?: nn.Embedding; + layer: nn.ModuleList; + lnF?: nn.Module<[Tensor], Tensor>; +}; + +export class DecoderTransformer extends nn.Module { + public config: DecoderTransformerConfig; + public decoder: nn.ModuleDict; + readonly lmHead: nn.Linear; + private readonly criterion: CrossEntropyLoss; + private readonly sinusoidalEncoding?: SinusoidalEncoding; + + constructor(config: Config, vocabSize: number, blockSize: number) { + super(); + + this.config = { + ...buildTransformerConfigCommon(config, vocabSize), + blockSize, + nLayers: config.model.layers + }; + + const transformerDict: DecoderTransformerDict = { + drop: this.config.dropout.present ? new nn.Dropout(this.config.dropout.embedding) : undefined, + wordEmbedding: new nn.Embedding(this.config.vocabSize, this.config.embeddingSize), + layer: new nn.ModuleList( + Array.from({ length: this.config.nLayers }).map(() => new DecoderLayer(this.config, false)) + ) + }; + + // Only create positional embedding layer for learned positional encoding + transformerDict.positionEmbedding = maybeCreateLearnedPositionEmbedding( + this.config.positionalEncoding, + this.config.blockSize, + this.config.embeddingSize + ); + + transformerDict.lnF = maybeCreateFinalLayerNorm( + this.config.embeddingSize, + this.config.layerNormalization + ); + + this.sinusoidalEncoding = maybeCreateSinusoidalEncoding( + this.config.positionalEncoding, + this.config.embeddingSize, + this.config.dropout.embedding + ); + + this.decoder = new nn.ModuleDict(transformerDict); + + // Output projection with optional weight tying to token embeddings + this.lmHead = buildLmHead( + this.config.embeddingSize, + this.config.vocabSize, + config.model.tieEmbeddingsAndLmHead, + this.decoder.dict.wordEmbedding + ); + + this.criterion = createCrossEntropyCriterion(config); + } + + /** + * @param input - Input tensor of token IDs [batch_size, seq_len] + * @param targets - Target tensor of token IDs [batch_size, + * seq_len] + * @returns [logits, loss] + */ + forward( + input: Tensor, + options: { targets?: Tensor | null; kvCache?: DecoderKVCache | null } = {} + ): [Tensor, Tensor | null] { + const targets = options.targets ?? null; + const kvCache = options.kvCache ?? null; + const [_batchSize, seqLen] = input.size(); + + if (!seqLen) { + throw new Error( + 'Input tensor has no sequence length (did you forget to pass input as batches?)' + ); + } + + // Get token embeddings + let wordEmbeddings = this.decoder.dict.wordEmbedding.forward(input); + + // Use cache length (if any) as position offset for absolute encodings during incremental decoding + const posOffset = kvCache?.layers?.[0]?.self?.length ?? 0; + + wordEmbeddings = addPositionalEncodingToEmbedding( + wordEmbeddings, + this.config.positionalEncoding, + this.sinusoidalEncoding, + this.decoder.dict.positionEmbedding, + this.decoder.dict.drop, + posOffset + ); + + // Pass through each transformer layer + let hiddenStates = wordEmbeddings; + + const useCache = kvCache !== null; + const cacheObj = useCache ? kvCache! : createEmptyDecoderKVCache(this.config.nLayers); + for (let i = 0; i < this.config.nLayers; i++) { + const layerModule = this.decoder.dict.layer[i] as DecoderLayer; + const result = layerModule.forward(hiddenStates, { cache: cacheObj.layers[i] }); + if (useCache) { + cacheObj.layers[i] = result.pastKeyValues!; + hiddenStates = result.output; + } else { + hiddenStates = result.output; + } + } + + // Apply final layer normalization + if (this.decoder.dict.lnF) { + hiddenStates = this.decoder.dict.lnF.forward(hiddenStates) as Tensor; + } + + // Project to vocabulary + let logits = this.lmHead.forward(hiddenStates); + + logits = maybeApplyLogitsSoftcap(logits, this.config.normalization); + + const loss = computeAutoregressiveCrossEntropyLoss(logits, targets, this.criterion); + + return [logits, loss ?? null]; + } +} + +export type EncoderTransformerConfig = TransformerModuleConfig & { + typeVocabSize: number; + nLayers: number; + blockSize: number; + attentionMasking: { + padMask: boolean; + }; + pooling: { + present: boolean; + }; + mlmHead: { + present: boolean; + shareEmbeddings: boolean; + }; +}; + +type EncoderTransformerDict = { + dropout?: nn.Dropout; + wordEmbedding: nn.Embedding; + positionEmbedding?: nn.Embedding; + tokenTypeEmbedding: nn.Embedding; + layer: nn.ModuleList; + layerNorm?: nn.Module<[Tensor], Tensor>; +}; + +export class EncoderTransformer extends nn.Module { + public config: EncoderTransformerConfig; + public encoder: nn.ModuleDict; + readonly mlmHead?: MLMHead; + readonly pooler?: Pooler; + private readonly criterion: CrossEntropyLoss; + private readonly sinusoidalEncoding?: SinusoidalEncoding; + + constructor(config: Config, vocabSize: number, blockSize: number, typeVocabSize: number = 2) { + super(); + + this.config = { + ...buildTransformerConfigCommon(config, vocabSize), + typeVocabSize: typeVocabSize, + nLayers: config.model.layers, + blockSize, + // We hardcode these for now, as all of our tasks focus on MLM right now. + attentionMasking: { + padMask: true + }, + pooling: { + present: false + }, + mlmHead: { + present: true, + shareEmbeddings: config.model.tieEmbeddingsAndLmHead + } + }; + + const encoderDict: EncoderTransformerDict = { + dropout: this.config.dropout.present + ? new nn.Dropout(this.config.dropout.embedding) + : undefined, + wordEmbedding: new nn.Embedding(this.config.vocabSize, this.config.embeddingSize), + tokenTypeEmbedding: new nn.Embedding(this.config.typeVocabSize, this.config.embeddingSize), + layer: new nn.ModuleList( + Array.from({ length: this.config.nLayers }).map(() => new EncoderLayer(this.config)) + ) + }; + + // Only create positional embedding layer for learned positional encoding + if ( + this.config.positionalEncoding.present && + this.config.positionalEncoding.type === 'learned' + ) { + encoderDict.positionEmbedding = new nn.Embedding( + this.config.blockSize, + this.config.embeddingSize + ); + } + + encoderDict.layerNorm = maybeCreateFinalLayerNorm( + this.config.embeddingSize, + this.config.layerNormalization + ) as nn.Module<[Tensor], Tensor> | undefined; + + this.sinusoidalEncoding = maybeCreateSinusoidalEncoding( + this.config.positionalEncoding, + this.config.embeddingSize, + this.config.dropout.embedding + ); + + this.encoder = new nn.ModuleDict(encoderDict); + + // MLM head for masked language modeling + if (this.config.mlmHead.present) { + this.mlmHead = new MLMHead( + this.config.embeddingSize, + this.config.vocabSize, + this.config.layerNormalization, + this.config.mlmHead.shareEmbeddings ? this.encoder.dict.wordEmbedding : undefined + ); + } + + // Pooler for classification tasks + if (this.config.pooling.present) { + this.pooler = new Pooler(this.config.embeddingSize); + } + + this.criterion = createCrossEntropyCriterion(config); + } + + /** + * @param inputIds - Input tensor of token IDs [batch_size, seq_len] + * @param tokenTypeIds - Token type/segment IDs [batch_size, seq_len] + * @param attentionMask - Attention mask [batch_size, seq_len] (1 for real tokens, 0 for padding) + * @param targets - Target tensor for MLM [batch_size, seq_len] (-100 for non-masked positions) + * @returns [lastHiddenState, pooledOutput?, mlmLogits?, loss?] + */ + forward( + inputIds: Tensor, + { + tokenTypeIds, + attentionMask, + targets + }: { tokenTypeIds?: Tensor | null; attentionMask?: Tensor | null; targets?: Tensor | null } = { + tokenTypeIds: null, + attentionMask: null, + targets: null + } + ): [Tensor, Tensor | null, Tensor | null, Tensor | null] { + const [batchSize, seqLen] = inputIds.size(); + + if (!seqLen) { + throw new Error( + 'Input tensor has no sequence length (did you forget to pass input as batches?)' + ); + } + + // Get token embeddings + let wordEmbedding = this.encoder.dict.wordEmbedding.forward(inputIds); + + // Add segment/token type embeddings + if (tokenTypeIds == null) { + tokenTypeIds = piston.zeros([batchSize, seqLen], { + device: inputIds.device, + dtype: piston.int32 + }); + } + const typeEmbeddings = this.encoder.dict.tokenTypeEmbedding.forward(tokenTypeIds!); + wordEmbedding = wordEmbedding.add(typeEmbeddings); + + wordEmbedding = addPositionalEncodingToEmbedding( + wordEmbedding, + this.config.positionalEncoding, + this.sinusoidalEncoding, + this.encoder.dict.positionEmbedding, + this.encoder.dict.dropout + ); + + // Pass through each encoder layer + let hiddenStates = wordEmbedding; + + for (let i = 0; i < this.config.nLayers; i++) { + const layerModule = this.encoder.dict.layer[i] as EncoderLayer; + const layerOutput = layerModule.forward(hiddenStates, attentionMask); + hiddenStates = layerOutput; + } + + // Apply final layer normalization + if (this.encoder.dict.layerNorm) { + hiddenStates = this.encoder.dict.layerNorm.forward(hiddenStates) as Tensor; + } + + // Pooled output for classification tasks + let pooledOutput: Tensor | null = null; + if (this.pooler) { + pooledOutput = this.pooler.forward(hiddenStates); + } + + // MLM logits + let mlmLogits: Tensor | null = null; + if (this.mlmHead) { + mlmLogits = this.mlmHead.forward(hiddenStates); + mlmLogits = maybeApplyLogitsSoftcap(mlmLogits, this.config.normalization); + } + + // Calculate MLM loss if targets are provided + let loss: Tensor | null = null; + if (targets && mlmLogits) { + // Only compute loss on masked positions (where targets != -100) + const flatLogits = mlmLogits.view([-1, mlmLogits.size(-1)]); + const flatTargets = targets.view(-1); + loss = this.criterion.forward(flatLogits, flatTargets); + } + + return [hiddenStates, pooledOutput, mlmLogits, loss]; + } +} diff --git a/examples/piston-train-toy/src/lib/train/model/utils.ts b/examples/piston-train-toy/src/lib/train/model/utils.ts new file mode 100644 index 00000000..84ef17c7 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/model/utils.ts @@ -0,0 +1,246 @@ +import type { + Config, + LayerNormalizationConfig, + PositionEncodingConfig, + RNNEmbeddingConfig, + TransformerNormalizationConfig +} from '$lib/workspace/config'; + +import { + arange, + CrossEntropyLoss, + Device, + gpu, + int32, + type Module as ModuleType, + nn, + Parameter, + type Tensor +} from '@piston-ml/piston-web'; + +import type { RNNModuleConfig } from './config'; + +import { createNorm } from './modules/norm'; +import { OneHotEmbedding } from './modules/oneHot'; +import { SinusoidalEncoding } from './modules/positional'; +import { applySoftcap } from './modules/utils'; + +/** + * Create a causal (lower triangular) mask. + * @param queryLen - Length of the current query. + * @param keyLen - Length of the key (which may include cached tokens). + * @returns Causal mask tensor of shape [1, numHeads, queryLen, keyLen]. + */ +export function createCausalMask(queryLen: number, keyLen: number): Tensor { + // General causal mask supporting past KV cache where keyLen may exceed queryLen. + // We want to mask future positions: for each query i, keys j > pastLen + i are masked. + // pastLen is inferred as keyLen - queryLen when using KV cache (else 0). + const pastLen = Math.max(0, keyLen - queryLen); + const i = arange({ end: queryLen, device: gpu, dtype: int32 }) + .unsqueeze(1) + .broadcastTo([queryLen, keyLen]); + const j = arange({ end: keyLen, device: gpu, dtype: int32 }) + .unsqueeze(0) + .broadcastTo([queryLen, keyLen]); + // Mask is true where positions are allowed: j <= pastLen + i + return j.le(i.add(pastLen)); +} + +/** + * Create position IDs tensor [0, 1, 2, ..., seqLen-1] and broadcast to batch size + * @param seqLen - Sequence length + * @param batchSize - Batch size + * @param device - Device to place tensor on + * @returns Position IDs tensor + */ +function createPositionIds( + seqLen: number, + batchSize: number, + device: Device, + offset: number = 0 +): Tensor { + // Create position IDs tensor [offset, offset+1, ..., offset+seqLen-1] and broadcast to batch + const positionIds = arange({ end: seqLen, device, dtype: int32 }).add(offset).cast(int32); + // Reshape to [1, seqLen] and broadcast to [batchSize, seqLen] + return positionIds.unsqueeze(0).broadcastTo([batchSize, seqLen]); +} + +/** + * Apply mask to attention scores + * @param onFalse - Attention scores + * @param mask - Mask tensor + * @param onTrueValue - Value to fill masked positions with + * @returns Masked scores + */ +export function maskedFill(onTrue: Tensor, mask: Tensor, onFalseValue: number): Tensor { + return onTrue.where(mask, onFalseValue); +} + +export function addPositionalEncodingToEmbeddings( + embeddings: Tensor, + positionalEncodingConfig: PositionEncodingConfig, + sinusoidalEncoding?: ModuleType<[Tensor], Tensor>, + positionEmbeddings?: ModuleType<[Tensor], Tensor>, + dropout?: ModuleType<[Tensor], Tensor>, + additionalPositionOffset: number = 0 +): Tensor { + const [batchSize, seqLen] = embeddings.size(); + if (positionalEncodingConfig.present) { + if (positionalEncodingConfig.type === 'learned') { + // Add positional embeddings + const positions = createPositionIds( + seqLen, + batchSize, + embeddings.device, + additionalPositionOffset + ); + const positionEmbeddingsOutput = positionEmbeddings!.forward(positions); + embeddings = embeddings.add(positionEmbeddingsOutput); + // Apply embedding dropout if configured + if (dropout) { + embeddings = dropout.forward(embeddings); + } + } else if (positionalEncodingConfig.type === 'sinusoidal') { + embeddings = (sinusoidalEncoding as SinusoidalEncoding).forward( + embeddings, + additionalPositionOffset + ); + } else if ( + positionalEncodingConfig.type === 'rope' || + positionalEncodingConfig.type === 'alibi' + ) { + // These are applied in the attention mechanism, so we only apply embedding dropout here, if + // configured + if (dropout) { + embeddings = dropout.forward(embeddings); + } + } + } + return embeddings; +} + +export function createCrossEntropyCriterion(config: Config) { + return new CrossEntropyLoss({ + labelSmoothing: config.training.labelSmoothing.present + ? config.training.labelSmoothing.value + : 0.0, + ignoreIndex: -100 + }); +} + +/** + * Optionally create a learned positional embedding layer (nn.Embedding variant) + */ +export function maybeCreateLearnedPositionEmbedding( + positionalEncoding: PositionEncodingConfig, + blockSize: number, + embeddingSize: number +): nn.Embedding | undefined { + if (positionalEncoding.present && positionalEncoding.type === 'learned') { + return new nn.Embedding(blockSize, embeddingSize); + } + return undefined; +} + +export type RNNWordEmbedding = nn.Embedding | OneHotEmbedding; + +/** + * Create a possibly one-hot RNN word embeddings module based on the embedding config + */ +export function createPossiblyOneHotRNNWordEmbedding( + embeddings: RNNEmbeddingConfig, + vocabSize: number, + embeddingSize: number +): RNNWordEmbedding { + return embeddings.type === 'learned' + ? new nn.Embedding(vocabSize, embeddingSize) + : new OneHotEmbedding(vocabSize); +} + +/** + * Optionally create sinusoidal encoding module based on positional encoding config + */ +export function maybeCreateSinusoidalEncoding( + positionalEncoding: PositionEncodingConfig, + embeddingSize: number, + dropout: number +): SinusoidalEncoding | undefined { + if (positionalEncoding.present && positionalEncoding.type === 'sinusoidal') { + return new SinusoidalEncoding(embeddingSize, { dropout }); + } + return undefined; +} + +/** + * Optionally create final layer norm if transformer normalization position is 'pre' + */ +export function maybeCreateFinalLayerNorm( + embeddingSize: number, + layerNormalization: LayerNormalizationConfig +): ModuleType<[Tensor], Tensor> | undefined { + if (layerNormalization.transformer.present && layerNormalization.transformer.position === 'pre') { + return createNorm(embeddingSize, layerNormalization) as unknown as ModuleType<[Tensor], Tensor>; + } + return undefined; +} + +/** + * Build a language modeling head using standard nn.Linear with optional weight tying. + */ +export function buildLmHead( + embeddingSize: number, + vocabSize: number, + tieEmbeddings: boolean, + tiedEmbeddings?: nn.Embedding +): nn.Linear { + const head = new nn.Linear(embeddingSize, vocabSize, false); + if (tieEmbeddings && tiedEmbeddings) { + head.weight = new Parameter(tiedEmbeddings.weight); + } + return head; +} + +export function buildLmHeadRNN( + config: RNNModuleConfig, + tiedEmbeddings?: RNNWordEmbedding +): nn.Module<[Tensor], Tensor> { + const canTie = config.embedding.type === 'learned' && config.tieEmbeddingsAndLmHead; + const inFeatures = canTie ? config.embeddingSize : config.baseHiddenSize; + let lmHead: nn.Module<[Tensor], Tensor> = buildLmHead( + inFeatures, + config.vocabSize, + canTie, + canTie ? (tiedEmbeddings as nn.Embedding) : undefined + ); + if (canTie && config.baseHiddenSize !== config.embeddingSize) { + lmHead = new nn.Sequential( + new nn.Linear(config.baseHiddenSize, config.embeddingSize) as nn.Module, + lmHead as nn.Module + ) as nn.Module<[Tensor], Tensor>; + } + return lmHead; +} + +export function maybeApplyLogitsSoftcap( + logits: Tensor, + normalization: TransformerNormalizationConfig +): Tensor { + if (normalization.softcap.logits.present) { + return applySoftcap(logits, normalization.softcap.logits.value); + } + return logits; +} + +/** + * Compute cross-entropy loss if both logits and targets are provided, flattening as needed + */ +export function computeAutoregressiveCrossEntropyLoss( + logits: Tensor | null, + targets: Tensor | null, + criterion: CrossEntropyLoss +): Tensor | null { + if (!logits || !targets) { + return null; + } + return criterion.forward(logits.view([-1, logits.size(-1)]), targets.view(-1)); +} diff --git a/examples/piston-train-toy/src/lib/train/moduleWorker.ts b/examples/piston-train-toy/src/lib/train/moduleWorker.ts new file mode 100644 index 00000000..2b4736a4 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/moduleWorker.ts @@ -0,0 +1,328 @@ +import * as piston from '@piston-ml/piston-web'; + +import type { WorkerCommand, WorkerEvent } from './protocol'; + +import { TrainingSession } from './session'; +import { inspectModel } from './utils/model'; + +let session: TrainingSession | undefined; +let pendingVisualizerCanvas: { canvas: OffscreenCanvas; labelPaddingCssPx: number } | null = null; + +// Console Interception +const originalConsole = { + log: console.log.bind(console), + error: console.error.bind(console), + warn: console.warn.bind(console), + info: console.info.bind(console), + debug: console.debug.bind(console) +}; + +function formatArgs(args: unknown[]) { + return args + .map((arg) => { + if (typeof arg === 'object' && arg !== null) { + try { + return JSON.stringify(arg); + } catch (_: unknown) { + return '[Unserializable Object]'; + } + } + return String(arg); + }) + .join(' '); +} + +interface LogInfo { + level: string; + message: string; + source: string; + lineno?: number; + colno?: number; +} + +function sendLog(level: string, message: string, source: string = '[Worker]') { + // Check if this is a WASM log and parse it + const wasmLogRegex = /^\[WASM ([^:]+):(\d+)(?::(\d+))?\] (.*)$/; + const match = message.match(wasmLogRegex); + + if (level === 'error' && message.startsWith('panicked at')) { + const lines = message.split('\n'); + if (lines[1].startsWith('VRAM limit exceeded')) { + self.postMessage({ type: 'log', level: 'error', message: lines[1] }); + self.postMessage({ + type: 'error', + runId: session?.runId, + message: 'VRAM limit exceeded', + name: 'VRAMLimitExceededError' + }); + return; + } else { + self.postMessage({ type: 'error', runId: session?.runId, message }); + } + } + + if (match) { + // Handle WASM logs with parsed source info + const [, filepath, lineno, colno, actualMessage] = match; + const logInfo: LogInfo = { + level, + message: actualMessage, + source: `[WASM] ${filepath}`, + lineno: parseInt(lineno, 10), + ...(colno && { colno: parseInt(colno, 10) }) + }; + self.postMessage({ type: 'log', ...logInfo }); + } else { + // Handle regular logs + const logInfo: LogInfo = { + level, + message, + source + }; + self.postMessage({ type: 'log', ...logInfo }); + } +} + +// Wrap console methods before importing Piston to catch its logs +Object.keys(originalConsole).forEach((level) => { + (console as unknown as Record void>)[level] = ( + ...args: unknown[] + ) => { + const message = formatArgs(args); + + // Use sendLog which will handle WASM log parsing internally + sendLog(level, message, currentExecutionSource); + + // Also call original console for debugging + originalConsole[level as keyof typeof originalConsole](...args); + }; +}); + +// Global error handler - catches unhandled errors +self.addEventListener('error', (event) => { + const errorMessage = `Uncaught Error: ${event.message} at ${event.filename}:${event.lineno}:${event.colno}`; + sendLog('error', errorMessage); + if (event.error?.stack) { + sendLog('error', `${event.error.stack}`); + } +}); + +// Unhandled promise rejection handler - catches unhandled promise rejections +self.addEventListener('unhandledrejection', (event) => { + const errorMessage = `Unhandled Promise Rejection: ${event.reason}`; + sendLog('error', errorMessage); + if (event.reason?.stack) { + sendLog('error', `${event.reason.stack}`); + } + // Prevent the default browser behavior (logging to console) + event.preventDefault(); +}); + +// Intercept and override the default error reporting +const originalOnError = self.onerror; +self.onerror = (message, source, lineno, colno, error) => { + const errorMessage = `Global Error: ${message} at ${source}:${lineno}:${colno}`; + sendLog('error', errorMessage); + if (error?.stack) { + sendLog('error', `${error.stack}`); + } + // Call original handler if it exists + if (originalOnError) { + return originalOnError(message, source, lineno, colno, error); + } + // Prevent default browser error handling + return true; +}; + +// +// End Console Interception +// + +// Track current execution context for logging (will be reassigned during execution) +let currentExecutionSource = '[Worker]'; + +import type { Config } from '$lib/workspace/config'; + +function postEvent(e: WorkerEvent) { + self.postMessage(e); +} + +function startTraining() { + if (!session) return; + const runId = session.runId; + session.start().catch((error: unknown) => { + console.error('Training error:', error); + self.postMessage({ + type: 'error', + runId, + message: error instanceof Error ? error.message : String(error), + name: error instanceof Error ? error.name : undefined, + stack: error instanceof Error ? error.stack : undefined + }); + }); +} + +// Message handler for worker +self.addEventListener('message', async (event) => { + const raw = event.data as WorkerCommand | { type: string; data?: unknown }; + const type: string = (raw as { type: string }).type; + const data: unknown = (raw as { data?: unknown }).data; + + switch (type) { + case 'save': { + session?.save(); + break; + } + case 'pause': { + session?.pause(); + break; + } + case 'resume': { + session?.resume(); + startTraining(); + break; + } + case 'step': { + if (!session) break; + await session.pause(); + await session.step({ manual: true }); + break; + } + case 'visualizer.updateScript': { + try { + const { script, example } = data as { script: string; example: string }; + session?.setVisualizationScript(example, script ?? null); + self.postMessage({ type: 'visualizer.ready' }); + } catch (e) { + console.error('Failed to update visualizer script', e); + self.postMessage({ type: 'visualizer.error', message: String(e) }); + } + break; + } + case 'visualizer.canvas': { + try { + const payload = data as { canvas: OffscreenCanvas; labelPaddingCssPx?: number }; + const labelPaddingCssPx = payload.labelPaddingCssPx ?? 0; + pendingVisualizerCanvas = { canvas: payload.canvas, labelPaddingCssPx }; + if (session) { + session.initVisualizerCanvas(payload.canvas, labelPaddingCssPx); + } + self.postMessage({ type: 'visualizer.ready' }); + } catch (e) { + console.error('Visualizer init failed', e); + self.postMessage({ type: 'visualizer.error', message: String(e) }); + } + break; + } + case 'visualizer.resize': { + try { + const { width } = data as { width: number }; + session?.resizeVisualizer(width); + } catch (e) { + console.error('Visualizer resize failed', e); + } + break; + } + case 'visualizer.setTarget': { + try { + const { target } = data as { target: 'train' | 'validation' }; + session?.setVisualizationTarget(target); + } catch (e) { + console.error('Visualizer set target failed', e); + } + break; + } + case 'visualizer.setSelectedValidation': { + try { + const { exampleIndex, tokenIndex } = data as { exampleIndex: number; tokenIndex: number }; + session?.setVisualizationSelectedValidation({ exampleIndex, tokenIndex }); + } catch (e) { + console.error('Visualizer set selected validation failed', e); + } + break; + } + case 'start': + try { + const { + runId: runIdFromData, + config, + resumeFrom + } = data as { + runId: string; + config: Config; + resumeFrom?: Uint8Array; + }; + currentExecutionSource = `[Training:${runIdFromData}]`; + + console.info(`Starting training for run ${runIdFromData}`); + session = new TrainingSession(runIdFromData, config, postEvent, resumeFrom); + if (pendingVisualizerCanvas) { + try { + session.initVisualizerCanvas( + pendingVisualizerCanvas.canvas, + pendingVisualizerCanvas.labelPaddingCssPx + ); + self.postMessage({ type: 'visualizer.ready' }); + } catch (e) { + console.error('Visualizer init (deferred) failed', e); + self.postMessage({ type: 'visualizer.error', message: String(e) }); + } + } + startTraining(); + } catch (error: unknown) { + console.error('Training error:', error); + self.postMessage({ + type: 'error', + runId: (data as { runId?: string })?.runId, + message: error instanceof Error ? error.message : String(error), + name: error instanceof Error ? error.name : undefined, + stack: error instanceof Error ? error.stack : undefined + }); + } + break; + case 'inspectModel': + try { + const { config, requestId } = data as { config: Config; requestId: string }; + currentExecutionSource = '[ModelInspection]'; + + console.debug('Inspecting model...'); + const result = inspectModel(config); + + self.postMessage({ + type: 'modelInspection', + requestId, + parameterCount: result.parameterCount, + modelIndex: result.modelIndex, + vocabSize: result.vocabSize, + blockSize: result.blockSize + }); + } catch (error: unknown) { + console.error('Model inspection error:', error); + self.postMessage({ + type: 'modelInspectionError', + requestId: (data as { requestId?: string })?.requestId ?? '', + message: error instanceof Error ? error.message : String(error) + }); + } + break; + + default: + console.warn(`Unknown message type: ${type}`); + break; + } +}); + +// Initialize Piston, then signal that the worker is ready +piston + .init() + .then(() => { + console.info('Piston initialized'); + self.postMessage({ type: 'ready' }); + }) + .catch((error: unknown) => { + console.error('Error initializing Piston:', error); + self.postMessage({ + type: 'error', + message: error instanceof Error ? error.message : String(error) + }); + }); diff --git a/examples/piston-train-toy/src/lib/train/protocol.ts b/examples/piston-train-toy/src/lib/train/protocol.ts new file mode 100644 index 00000000..9f2a2fce --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/protocol.ts @@ -0,0 +1,92 @@ +import type { Config } from '$lib/workspace/config'; +import type { StepData } from '$lib/workspace/runs.svelte'; +import type { IndexState } from '@piston-ml/piston-web'; + +type WithRunId = { runId: string }; + +export type WorkerCommand = + | { + type: 'start'; + data: { runId: string; config: Config; resumeFrom?: Uint8Array }; + } + | { type: 'pause' } + | { type: 'resume' } + | { type: 'step' } + | { type: 'save' } + | { type: 'stop' } + | { + type: 'visualizer.updateScript'; + data: { example: string; script: string | null }; + } + | { + type: 'visualizer.canvas'; + data: { canvas: OffscreenCanvas; labelPaddingCssPx?: number }; + } + | { type: 'visualizer.resize'; data: { width: number } } + | { type: 'visualizer.setTarget'; data: { target: 'train' | 'validation' } } + | { + type: 'visualizer.setSelectedValidation'; + data: { exampleIndex: number; tokenIndex: number }; + } + | { type: 'inspectModel'; data: { requestId: string; config: Config } }; + +type ReadyWorkerEvent = { + type: 'ready'; +}; + +type LogWorkerEvent = { + type: 'log'; + level: 'debug' | 'info' | 'warn' | 'error'; + message: string; + source?: string; + lineno?: number; + colno?: number; +}; + +type ErrorWorkerEvent = { + type: 'error'; + name?: string; + message: string; + stack?: string; +}; + +export type RunWorkerEventWithoutRunId = + | { + type: 'metrics'; + data: { [metricName: string]: Omit }; + metadata?: { step?: number }; + } + | { + type: 'capture'; + step: number; + boxes: unknown[]; + statsById: Record; + width: number; + height: number; + queries: unknown[]; + } + | { type: 'checkpoint'; buffer: Uint8Array } + | { type: 'restart'; buffer: Uint8Array } + | { type: 'paused' } + | { type: 'resumed' } + | { type: 'complete' } + | LogWorkerEvent + | ErrorWorkerEvent; + +export type RunWorkerEvent = RunWorkerEventWithoutRunId & WithRunId; + +export type WorkerEvent = + | ReadyWorkerEvent + | LogWorkerEvent + | ErrorWorkerEvent + | RunWorkerEvent + | { type: 'visualizer.ready' } + | { type: 'visualizer.error'; message: string } + | { + type: 'modelInspection'; + requestId: string; + parameterCount: number; + vocabSize: number; + modelIndex: IndexState; + } + | { type: 'modelInspectionError'; requestId: string; message: string }; diff --git a/examples/piston-train-toy/src/lib/train/session.ts b/examples/piston-train-toy/src/lib/train/session.ts new file mode 100644 index 00000000..5434e8a5 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/session.ts @@ -0,0 +1,995 @@ +import type { Config } from '$lib/workspace/config'; +import type { StepData } from '$lib/workspace/runs.svelte'; + +import { getEffectiveVisualizationScript } from '$lib/workspace/visualizationExamples'; +import { + CosineAnnealingLR, + ExponentialLR, + LinearLR, + type LRScheduler, + SequentialLR, + StepLR, + type Tensor +} from '@piston-ml/piston-web'; +import * as piston from '@piston-ml/piston-web'; + +import type { CaptureMatch, RemoteCaptureStep } from './capture'; +import type { BuiltData } from './data/pipeline'; +import type { + ToyAutoregressiveBatch, + ToyBidirectionalBatch, + ToyEncoderDecoderBatch, + ToySequence +} from './data/toy/dataset'; +import type { RNNEncoder, RNNEncoderDecoder } from './model/rnn'; +import type { RunWorkerEvent, RunWorkerEventWithoutRunId, WorkerEvent } from './protocol'; +import type { GeneratableModel, PistonCollateFnType, PistonDatasetType } from './types'; + +import { CaptureManager, makeCaptureMatchRemote, runValidationExampleForCapture } from './capture'; +import { buildDataset, tensorWrap } from './data'; +import { filterDatasetByHeldoutSamples } from './data/filter'; +import { NaturalLanguageDataset } from './data/natural'; +import { buildDataPipeline } from './data/pipeline'; +import ToyDataset from './data/toy/dataset'; +import { + DecoderTransformer, + EncoderDecoderTransformer, + EncoderTransformer +} from './model/transformer'; +import { + type AnySchedulerState, + buildCheckpoint, + type CheckpointDataState, + type CheckpointExtra, + splitLoadedState +} from './utils/checkpoint'; +import { + calculateBlockSize, + calculateParameterSum, + calculateVocabSize, + createCollateFn, + createDataloader, + createModel, + initializeModel, + seedPiston +} from './utils/model'; +import { MarkStepModeIfEnabled, WeakModeIfEnabled } from './utils/modes'; +import { configureOptimizerForModel } from './utils/optim'; +import { forkRandom, seededRandom } from './utils/random'; +import { + buildValidationExamplesSubset, + buildValidationLog, + computeLikelihoodMetrics, + computeNaturalValidationMetrics, + computeToyValidationMetrics, + type NaturalValidationExamples, + prepareValidationExamples, + type ToyValidationExamples, + type ValidationExamples, + type ValidationStep +} from './validation'; +import { Visualizer } from './visualizer'; + +// @ts-expect-error polyfill +Symbol.dispose ||= Symbol.for('Symbol.dispose'); + +export class TrainingSession { + readonly runId: string; + private config: Config; + private readonly post: (e: RunWorkerEventWithoutRunId) => void; + private readonly resumeFrom?: Uint8Array; + + private paused = false; + private resolvePause: (() => void) | null = null; + + private visualizer: Visualizer | null = null; + private visualizerDevice: GPUDevice | null = null; + private visualizerCanvas: OffscreenCanvas | null = null; + private visualizerLabelPaddingCssPx: number = 0; + private readonly currentVisualizationSelectedValidation: { + exampleIndex: number; + tokenIndex: number; + } = { + exampleIndex: 0, + tokenIndex: 0 + }; + + private model!: GeneratableModel; + private optimizer!: piston.Optimizer; + private scheduler: LRScheduler | undefined; + private trainDataset!: PistonDatasetType; + private blockSize!: number | { source: number; target: number }; + + private isSetup: boolean = false; + + private startTimeMs: number | null = null; + private lastLogTime: number | null = null; + private lastLogStep: number | null = null; + private stepCount: number = 0; + + private captureManager: CaptureManager | null = null; + + private dataPipeline!: BuiltData; + + private validationExamples: ValidationExamples | null = null; + private validationCollateFn: PistonCollateFnType | null = null; + private validationDataset: PistonDatasetType | null = null; + + constructor( + runId: string, + config: Config, + post: (e: WorkerEvent) => void, + resumeFrom?: Uint8Array + ) { + this.runId = runId; + this.config = config; + this.post = (e: RunWorkerEventWithoutRunId) => + // We only post the subset of events that have runId in the payload + (post as (e: RunWorkerEvent) => void)({ ...e, runId: this.runId }); + this.resumeFrom = resumeFrom; + } + + async pause() { + if (this.paused) return; + this.paused = true; + await new Promise((resolve) => { + this.resolvePause = resolve; + }); + this.resolvePause = null; + this.post({ type: 'paused' }); + } + + resume() { + this.paused = false; + this.post({ type: 'resumed' }); + } + + async save() { + try { + if (this.paused) { + try { + if (!this.model) { + // Defer save until model is ready + this.post({ type: 'log', level: 'info', message: 'Save requested before model ready' }); + return; + } + await piston.gpu.markStep(); + const buffer = await this.saveLatestCheckpoint(); + this.post({ type: 'checkpoint', buffer }); + } catch (e) { + this.post({ + type: 'error', + message: String(e) + }); + } + } else { + throw new Error('Saving during training is not supported'); + } + } catch (e) { + this.post({ type: 'error', message: String(e) }); + } + } + + setVisualizationScript(example: string, script: string | null) { + // If a model is already active, rebuild the plan immediately; otherwise setup() will build it + if (this.model) { + const newCaptureManager = new CaptureManager(); + newCaptureManager.build(this.model, { + enabled: this.config.training.enableVisualization, + script: script ?? undefined + }); + if (newCaptureManager.plan) { + this.captureManager = newCaptureManager; + this.config.visualization.example = example; + this.config.visualization.script = script; + } + } + } + + setVisualizationTarget(target: 'train' | 'validation') { + this.config.visualization.target = target; + } + + setVisualizationSelectedValidation({ + exampleIndex, + tokenIndex + }: { + exampleIndex: number; + tokenIndex: number; + }) { + this.currentVisualizationSelectedValidation.exampleIndex = exampleIndex; + this.currentVisualizationSelectedValidation.tokenIndex = tokenIndex; + } + + initVisualizerCanvas(canvas: OffscreenCanvas, labelPaddingCssPx: number = 0) { + // Dispose old visualizer if any + if (this.visualizer) { + this.visualizer.dispose(); + this.visualizer = null; + } + + this.visualizerCanvas = canvas; + this.visualizerLabelPaddingCssPx = labelPaddingCssPx; + const pistonDevice = piston.gpu.asWebGPUDevice(); + if (!pistonDevice) { + throw new Error('Failed to get WebGPU device from piston.gpu'); + } + this.visualizerDevice = pistonDevice; + this.visualizer = new Visualizer(this.visualizerDevice); + this.visualizer.init(this.visualizerCanvas); + this.visualizer.setCssLabelPadding(this.visualizerLabelPaddingCssPx); + } + + resizeVisualizer(width: number) { + this.visualizer?.resize(width); + } + + private async onCaptureMatches(step: number, matches: CaptureMatch[]) { + try { + await piston.gpu.markStep(); + const captureStep: Omit = { + type: 'capture', + matches: matches.map(makeCaptureMatchRemote) + }; + this.logMetrics({ 'visualization/matches': captureStep }, { step }); + const result = this.visualizer + ? await this.visualizer.renderCapture(matches) + : { boxes: [], statsById: {}, width: 1, height: 1 }; + + this.post({ + type: 'capture', + queries: this.captureManager?.queries ?? [], + step, + boxes: result.boxes.map((box) => ({ ...box, match: makeCaptureMatchRemote(box.match) })), + statsById: result.statsById, + width: result.width, + height: result.height + }); + } catch (err) { + // Fall back to logging the error and continue + this.post({ + type: 'log', + level: 'warn', + message: `Visualizer render failed: ${String(err)}` + }); + } + } + + async saveLatestCheckpoint(): Promise> { + if (!this.model) throw new Error('No model available to save'); + await piston.gpu.markStep(); + // Derive dataset state if available + let dataState: CheckpointDataState | undefined = undefined; + if (this.trainDataset) { + if (this.trainDataset instanceof NaturalLanguageDataset) { + dataState = { + blockSize: this.blockSize, + natural: this.trainDataset.exportState() + }; + } else if (this.trainDataset instanceof ToyDataset) { + dataState = { + blockSize: this.blockSize, + toy: { + cursor: this.trainDataset.cursor, + baseSeed: this.trainDataset.baseSeed, + datasetName: this.config.data.dataset + } + }; + } + } + const { tensors, extra } = buildCheckpoint( + this.model, + this.optimizer!, + this.stepCount, + this.config ? JSON.parse(JSON.stringify(this.config)) : null, + this.scheduler, + dataState, + this.startTimeMs ?? undefined + ); + return piston.save(tensors, extra); + } + + private logMetrics( + data: { [metricName: string]: Omit }, + metadata?: { step?: number } + ) { + this.post({ type: 'metrics', data, metadata }); + } + + private async setup() { + if (this.isSetup) { + return; + } + + // Log initial memory + const initialMemoryMB = Number(piston.gpu.usageBytes()) / (1024 * 1024); + console.debug(`Initial memory: ${initialMemoryMB} MB`); + + // If resuming from a checkpoint, parse and use checkpoint config + let resumePayload: { + modelState: Record; + optimizerPacked?: { state: Record; paramGroups: piston.ParamGroupConfig[] }; + schedulerState?: unknown; + numSteps: number; + config: Config; + dataState?: CheckpointDataState; + startTimeMs?: number; + } | null = null; + + if (this.resumeFrom) { + const loaded = piston.load(this.resumeFrom, piston.gpu); + const split = splitLoadedState( + loaded as { state: Record; extra?: CheckpointExtra } + ); + resumePayload = { + modelState: split.modelState, + optimizerPacked: split.optimizerState as unknown as { + state: Record; + paramGroups: piston.ParamGroupConfig[]; + }, + schedulerState: split.schedulerState, + numSteps: split.numSteps, + config: split.config, + dataState: split.dataState, + startTimeMs: split.startTimeMs + }; + if (resumePayload.config) { + this.config = resumePayload.config as Config; + } + // If blockSize present in extras, prefer it + if (split.dataState && split.dataState.blockSize !== undefined) { + this.blockSize = split.dataState.blockSize; + } + } + + // Determine model type and create appropriate dataloader/model + const isEncoderOnly = this.config.model.topology === 'encoder'; + const isDecoderOnly = this.config.model.topology === 'decoder'; + const isEncoderDecoder = this.config.model.topology === 'encoder-decoder'; + + const seed = seedPiston(this.config); + + if (this.config.training.vramLimitMb.present) { + piston.gpu.setVRAMLimit(BigInt(this.config.training.vramLimitMb.value * 1024 * 1024)); + } + + // Ensure shared-object allocation is enabled so buffer handles are stable across steps + piston.gpu.setSharedObjectAllocationEnabled(this.config.training.sharedObjectAllocation); + piston.gpu.setCachingEnabled(this.config.training.cachingEnabled); + piston.gpu.setInplaceSupport(this.config.training.inplaceSupport); + + // Set up dataset; we use two generators so we change validation parameters without affecting + // training + const trainGenerator = seededRandom(seed); + const maskGenerator = isEncoderOnly ? forkRandom(trainGenerator) : null; + + const trainDataset: PistonDatasetType = buildDataset(this.config, trainGenerator, 'train'); + // Restore dataset state if present + if (resumePayload && resumePayload.dataState) { + const dsState = resumePayload.dataState; + if (trainDataset instanceof NaturalLanguageDataset && dsState.natural) { + await trainDataset.importState(dsState.natural); + } else if (trainDataset instanceof ToyDataset && dsState.toy) { + // Restore cursor and baseSeed for toy datasets + trainDataset.cursor = dsState.toy.cursor | 0; + if (typeof dsState.toy.baseSeed === 'number') { + trainDataset.baseSeed = dsState.toy.baseSeed; + } + } + } + this.trainDataset = trainDataset; + + const validationDisabled = + ('disableValidation' in this.trainDataset && this.trainDataset.disableValidation) || false; + + this.validationExamples = null; + this.validationCollateFn = null; + this.validationDataset = null; + + if (this.config.training.validation.present && !validationDisabled) { + const validationGenerator = forkRandom(trainGenerator); + this.validationDataset = buildDataset(this.config, validationGenerator, 'val'); + this.validationCollateFn = createCollateFn( + this.config, + this.validationDataset, + maskGenerator, + tensorWrap + ); + this.validationExamples = await prepareValidationExamples( + this.config, + this.validationDataset, + { + isDecoderOnly, + isEncoderDecoder + } + ); + // Filter training dataset against holdout examples without duplication + let validationSequences: ToySequence[] | number[][]; + if ('toySequences' in this.validationExamples) { + validationSequences = this.validationExamples.toySequences; + this.trainDataset = filterDatasetByHeldoutSamples( + this.trainDataset, + this.config.data.dataset, + validationSequences + ); + } else if ('naturalSequences' in this.validationExamples) { + validationSequences = this.validationExamples.naturalSequences; + this.trainDataset = filterDatasetByHeldoutSamples( + this.trainDataset, + this.config.data.dataset, + validationSequences + ); + } else { + throw new Error('Unsupported validation dataset'); + } + console.debug( + `Prepared ${validationSequences.length} validation examples for batch generation` + ); + } + + if (validationDisabled) { + console.debug('Validation disabled by dataset; skipping validation and holdout filtering.'); + } + + // Calculate vocab size using shared utility + const vocabSize = calculateVocabSize(this.config, this.trainDataset); + const [trainDataloaderForSizing] = createDataloader( + this.config, + this.trainDataset, + trainGenerator, + tensorWrap + ); + const blockSize = + this.blockSize !== undefined + ? this.blockSize + : calculateBlockSize(this.config, trainDataloaderForSizing); + this.blockSize = blockSize; + + const datasetName = this.config.data.dataset; + + console.debug( + `Created dataset ${datasetName} with vocab size ${vocabSize} and block size ${blockSize}` + ); + + if (!isEncoderOnly && !isDecoderOnly && !isEncoderDecoder) { + throw new Error( + `Unsupported model type: ${this.config.model.topology}. Only 'encoder', 'decoder', and` + + ` 'encoder-decoder' are currently supported.` + ); + } + + // Create model + this.model = createModel(this.config, vocabSize, blockSize); + + // If starting from scratch, initialize model parameters + if (!resumePayload) { + initializeModel(this.config, this.model); + + // We need to flatten down initialization to the constant tensors they're on top of + await piston.gpu.markStep(); + + const parameterSum = new BigUint64Array( + new Float64Array([await (await calculateParameterSum(this.model).to('cpu')).item()]).buffer + ); + console.debug(`Initialization parameter sum: ${parameterSum}`); + } + + // Build or refresh capture plan using CaptureManager + this.captureManager = new CaptureManager(); + const scriptToUse = this.config.training.enableVisualization + ? getEffectiveVisualizationScript( + this.config.visualization.example, + this.config.visualization.script + ) + : null; + this.captureManager.build(this.model, { + enabled: this.config.training.enableVisualization, + script: scriptToUse + }); + + // Build and store the training data pipeline (iterator bound to current dataset/collate) + this.dataPipeline = await buildDataPipeline( + this.config, + trainGenerator, + maskGenerator, + this.trainDataset + ); + + // If resuming, load model state BEFORE creating the optimizer so param identities match + let startStep = 0; + if (resumePayload) { + this.model.loadStateDict(resumePayload.modelState, { strict: false }); + startStep = (resumePayload.numSteps ?? 0) + 1; + this.stepCount = startStep; + // If checkpoint carried a startTimeMs, use it for wall-clock continuity + if (typeof resumePayload.startTimeMs === 'number') { + this.startTimeMs = resumePayload.startTimeMs; + } + } + + // Create optimizer based on model type, using the (possibly restored) model parameters + const optimizer = configureOptimizerForModel( + this.model, + isEncoderOnly, + isEncoderDecoder, + this.config.optimizer, + piston.gpu + ); + this.optimizer = optimizer; + + // If resuming, load optimizer state NOW that groups refer to current model parameters + if (resumePayload && resumePayload.optimizerPacked) { + optimizer.loadStateDict(resumePayload.optimizerPacked as piston.StateDict); + } + + // Create learning rate scheduler if configured + if (this.config.optimizer.lrScheduler.present) { + const lrConfig = this.config.optimizer.lrScheduler; + switch (lrConfig.type) { + case 'step': + this.scheduler = new StepLR( + this.optimizer, + lrConfig.stepSchedule.stepSize, + lrConfig.stepSchedule.gamma + ); + break; + case 'cosine': + this.scheduler = new CosineAnnealingLR( + this.optimizer, + lrConfig.cosineAnnealingSchedule.tMax, + lrConfig.cosineAnnealingSchedule.etaMin + ); + break; + case 'exponential': + this.scheduler = new ExponentialLR(this.optimizer, lrConfig.exponentialSchedule.gamma); + break; + case 'linear': + this.scheduler = new LinearLR( + this.optimizer, + lrConfig.linearSchedule.startFactor, + lrConfig.linearSchedule.endFactor, + lrConfig.linearSchedule.totalIters + ); + break; + default: + throw new Error(`Unknown scheduler type: ${lrConfig.type}`); + } + + if (this.scheduler && this.config.optimizer.warmupSteps.present) { + const n = this.config.optimizer.warmupSteps.value; + if (n > 0) { + const warmup = new LinearLR(optimizer, 1e-8, 1.0, n); + this.scheduler = new SequentialLR(optimizer, [warmup, this.scheduler], [n]); + } + } + } else if (this.config.optimizer.warmupSteps.present) { + const n = this.config.optimizer.warmupSteps.value; + if (n > 0) { + this.scheduler = new LinearLR(optimizer, 1e-8, 1.0, n); + } + } + + // If resuming, load scheduler state after it is created + if (resumePayload && this.scheduler && resumePayload.schedulerState) { + this.scheduler.loadStateDict(resumePayload.schedulerState as AnySchedulerState); + } + + this.model.train(); + + this.isSetup = true; + } + + async step({ manual = false }: { manual?: boolean } = {}): Promise< + IteratorResult + > { + if (this.startTimeMs == null) { + this.startTimeMs = Date.now(); + } + if (this.lastLogStep == null) { + this.lastLogStep = this.stepCount; + } + try { + const iterNext = await this.dataPipeline.train.iterator.next(); + if (iterNext.done) { + return { done: true, value: 'completed' }; + } + const batch = iterNext.value; + performance.mark('stepStart'); + // Reset peak GPU memory tracking at the start of the step + piston.gpu.markUsageBytesStep(); + + let isLastStep = false; + if ( + this.config.training.limitTraining.present && + this.stepCount + 1 >= this.config.training.limitTraining.steps + ) { + console.log( + `Stopping training at step ${this.stepCount} because it reached the limit of ${this.config.training.limitTraining.steps} steps` + ); + isLastStep = true; + } + + const loggingStep = + manual || isLastStep || this.stepCount % this.config.training.logSteps === 0; + + let captureSession: piston.CaptureSession | null = null; + if (loggingStep && this.captureManager && this.config.visualization.target === 'train') { + // Create session on top of weak mode for this step to capture forward ops + captureSession = this.captureManager.createSession(); + } + + const weakModeUntilAfterBackward = new WeakModeIfEnabled( + this.config.training.useWeakTensorReferences, + { + label: 'train/forward_through_backward' + } + ); + + let loss: Tensor; + try { + if (this.config.model.topology === 'encoder') { + // For BERT: batch contains [inputIds, labels, attentionMask] + const { tensors } = batch as ToyBidirectionalBatch; + const [inputIds, bertLabels, attentionMask] = tensors; + + let computedLoss: Tensor | null = null; + if (this.model instanceof EncoderTransformer) { + [, , , computedLoss] = (this.model as EncoderTransformer).forward( + await inputIds.to('gpu'), + { + attentionMask: await attentionMask.to('gpu'), + targets: await bertLabels.to('gpu') + } + ); + } else { + [, , computedLoss] = (this.model as RNNEncoder).forward(await inputIds.to('gpu'), { + targets: await bertLabels.to('gpu') + }); + } + + if (!computedLoss) { + throw new Error('No loss tensor returned from encoder-only model'); + } + + loss = computedLoss; + } else if (this.config.model.topology === 'encoder-decoder') { + // For Transformer or RNN seq2seq: batch contains [encoderInputs, decoderInputs, decoderTargets] + const { tensors } = batch as ToyEncoderDecoderBatch; + const [encoderInputs, decoderInputs, decoderTargets] = tensors; + let computedLoss: Tensor | null; + if (this.model instanceof EncoderDecoderTransformer) { + [, computedLoss] = (this.model as EncoderDecoderTransformer).forward( + await encoderInputs.to('gpu'), + await decoderInputs.to('gpu'), + { targets: await decoderTargets.to('gpu') } + ); + } else { + [, computedLoss] = (this.model as RNNEncoderDecoder).forward( + await encoderInputs.to('gpu'), + await decoderInputs.to('gpu'), + { targets: await decoderTargets.to('gpu') } + ); + } + + if (!computedLoss) { + throw new Error('No loss tensor returned from encoder-decoder model'); + } + + loss = computedLoss; + } else { + // For GPT: batch contains [inputs, targets] + const { tensors } = batch as ToyAutoregressiveBatch; + const [inputs, gptTargets] = tensors; + const [, computedLoss] = (this.model as DecoderTransformer).forward( + await inputs.to('gpu'), + { + targets: await gptTargets.to('gpu') + } + ); + + if (!computedLoss) { + throw new Error('No loss tensor returned from decoder-only model'); + } + + loss = computedLoss; + } + + weakModeUntilAfterBackward.pin(loss); + + loss.backward(); + + if (captureSession && this.onCaptureMatches) { + try { + const matches = this.captureManager!.finalize(captureSession, 0); + await this.onCaptureMatches(this.stepCount, matches); + } finally { + captureSession[Symbol.dispose](); + } + captureSession = null; + } + } finally { + weakModeUntilAfterBackward[Symbol.dispose](); + } + + const weakModeForOptimizerStep = new WeakModeIfEnabled( + this.config.training.useWeakTensorReferences, + { + label: 'train/optimizer_step' + } + ); + + let gradNorm: Tensor | undefined; + try { + const weakMarkStepMode = new MarkStepModeIfEnabled( + this.config.training.useWeakTensorReferences + ); + weakModeForOptimizerStep.pin(loss); + + if (this.config.training.gradNorm.track) { + if (this.config.training.clipGradNorm.present) { + gradNorm = weakModeForOptimizerStep.pin( + piston.clipGradNorm_(this.model.parameters(), this.config.training.clipGradNorm.value) + ); + } else if (loggingStep) { + // If we're not clipping gradients, we can just get the total gradient norm + gradNorm = weakModeForOptimizerStep.pin( + piston.getTotalGradNorm(this.model.parameters()) + ); + } + } + + try { + await this.optimizer.step(); + } finally { + weakMarkStepMode[Symbol.dispose](); + } + } finally { + // TODO: decide if it's okay that we're disposing the mode twice here + weakModeForOptimizerStep[Symbol.dispose](); + } + + const finalWeakModeForStep = new WeakModeIfEnabled( + this.config.training.useWeakTensorReferences, + { + label: 'train/final' + } + ); + + try { + // We've kept loss strong; we'll want to make sure we get rid of it + // Batch tensors are created outside of weak mode, so we manually mark them as weak + finalWeakModeForStep.markWeak([loss, gradNorm, batch.tensors]); + + this.optimizer.zeroGrad(true); + + // Step learning rate scheduler if present + if (this.scheduler) { + this.scheduler.step(); + } + + if ( + this.config.training.validation.present && + (this.stepCount % this.config.training.validation.valSteps === 0 || isLastStep) && + this.validationExamples && + this.validationDataset && + this.validationCollateFn + ) { + try { + let valLoss = Number.NaN; + let perplexity = Number.NaN; + let validationLog: Record> = {}; + + if (this.validationExamples) { + if (this.config.training.validation.completions.present) { + let validationExamplesSubset: ValidationExamples | null = null; + if (this.config.training.validation.completions.amount === 'subset') { + validationExamplesSubset = buildValidationExamplesSubset( + this.validationExamples, + this.config.training.validation.completions.subsetSize + ); + } else { + validationExamplesSubset = this.validationExamples; + } + if (this.validationDataset instanceof ToyDataset) { + const validationStepData = await computeToyValidationMetrics( + this.model, + this.validationDataset, + validationExamplesSubset as ToyValidationExamples, + this.config.training.validation, + { + isDecoderOnly: this.config.model.topology === 'decoder', + isEncoderDecoder: this.config.model.topology === 'encoder-decoder', + includeTargets: + this.stepCount === 0 && (this.validationDataset.hasCanonicalTargets ?? true) + } + ); + validationLog = buildValidationLog(validationStepData); + } else if (this.validationDataset instanceof NaturalLanguageDataset) { + const validationStepData = await computeNaturalValidationMetrics( + this.model, + this.validationDataset, + validationExamplesSubset as NaturalValidationExamples, + this.config.training.validation, + { + isDecoderOnly: this.config.model.topology === 'decoder', + includeTargets: this.stepCount === 0, + maskRatio: this.config.data.maskRatio + } + ); + validationLog = buildValidationLog(validationStepData); + } + } + + const result = await computeLikelihoodMetrics( + this.model, + this.validationExamples!, + this.validationCollateFn! + ); + + valLoss = result.valLoss; + perplexity = result.perplexity; + + const logData: Record> = { + ...validationLog, + 'validation/loss': valLoss, + 'validation/perplexity': perplexity + }; + this.logMetrics(logData, { step: this.stepCount }); + } + } catch (error) { + console.error('Error during batch validation:', error); + } + } + + if (loggingStep) { + const currentTime = Date.now(); + const totalElapsedSeconds = (currentTime - this.startTimeMs!) / 1000; + + // Calculate delta time and steps since last log + const deltaTime = (currentTime - this.lastLogTime!) / 1000; + const deltaSteps = this.stepCount - this.lastLogStep!; + + // Calculate steps per second and words per second based on delta + const stepsPerSecond = deltaSteps > 0 ? deltaSteps / deltaTime : 0; + + // Calculate words per second (tokens per second) + // Get sequence length from the first tensor in the batch + let sequenceLength = 0; + + // Encoder-decoder will have three tensors in its batch, but we can just use the first one + const [inputs] = batch.tensors; + sequenceLength = inputs.shape[1]; // [batch_size, seq_len] + + const tokensPerStep = this.config.training.batchSize * sequenceLength; + const tokensPerSecond = deltaSteps > 0 ? (deltaSteps * tokensPerStep) / deltaTime : 0; + + const activeMap = piston.__pistonActiveTensors(); + const activeTensors = Array.from(activeMap.values()).reduce((s, v) => s + v.length, 0); + + let lossItem: number | null = null; + + const lossCpu = await loss.to('cpu'); + lossItem = await lossCpu.item(); + + if (lossItem === null) { + throw new Error('Loss item is null?'); + } + + const peakUsageMb = Number(piston.gpu.peakUsageBytes()) / (1024 * 1024); + + const logData: Record = { + 'train/loss': lossItem, + 'allocation/active_tensor_count': activeTensors, + 'allocation/gpu_memory_mb': peakUsageMb, + 'speed/steps_per_second': stepsPerSecond, + 'speed/step': this.stepCount, + 'speed/tokens_per_second': tokensPerSecond, + 'speed/wall_clock_seconds': totalElapsedSeconds + }; + + if (gradNorm) { + const gradNormCpu = await gradNorm.to('cpu'); + const gradNormItem = await gradNormCpu.item(); + if (this.config.training.gradNorm.errorIfNonfinite && !isFinite(gradNormItem)) { + throw new Error(`Gradient norm was nonfinite, so it cannot be clipped.`); + } + logData['train/grad_norm'] = gradNormItem; + } + + if ( + loggingStep && + this.captureManager && + this.validationExamples && + this.validationCollateFn && + this.onCaptureMatches && + this.config.visualization.target === 'validation' + ) { + // Create session on top of weak mode for this step to capture forward ops + captureSession = this.captureManager.createSession(); + + await runValidationExampleForCapture( + this.model, + this.validationExamples, + this.validationCollateFn, + this.currentVisualizationSelectedValidation.exampleIndex + ); + + // runValidationExampleForCapture actually ends up… training on the validation set, + // so we need to zero out the gradients here + this.optimizer.zeroGrad(true); + + try { + const matches = this.captureManager!.finalize( + captureSession!, + this.currentVisualizationSelectedValidation.exampleIndex + ); + await this.onCaptureMatches(this.stepCount, matches); + } finally { + captureSession![Symbol.dispose](); + } + captureSession = null; + } + + // Log current learning rate if scheduler is present + const currentLr = this.optimizer.paramGroups[0].lr; + if (currentLr) { + logData['optimizer/learning_rate'] = currentLr; + } + + this.logMetrics(logData, { step: this.stepCount }); + + // Update last log time and step + this.lastLogTime = currentTime; + this.lastLogStep = this.stepCount; + } + + // Trigger periodic restart if configured + const restartEvery = this.config.training.restartEverySteps ?? 0; + const willRestart = restartEvery > 0 && (this.stepCount + 1) % restartEvery === 0; + if (willRestart) { + console.debug(`Routine restart at step ${this.stepCount}`); + await piston.gpu.markStep(); + const bytes = await this.saveLatestCheckpoint(); + this.post({ type: 'restart', buffer: bytes }); + return { done: true, value: 'restarted' }; + } + + if (isLastStep) { + return { done: true, value: 'completed' }; + } + } finally { + finalWeakModeForStep[Symbol.dispose](); + } + + this.stepCount++; + + performance.mark('stepEnd'); + } catch (error) { + console.error(`Error during training: ${error}`); + throw error; + } + return { done: false, value: undefined }; + } + + async start(): Promise { + await this.setup(); + while (true) { + if (this.paused) { + if (this.resolvePause) { + this.resolvePause(); + } + return; + } + const { done, value } = await this.step(); + if (done) { + if (value === 'completed') { + this.post({ type: 'complete' }); + break; + } + if (value === 'restarted') { + continue; + } + } + } + } +} diff --git a/examples/piston-train-toy/src/lib/train/tokenizer.ts b/examples/piston-train-toy/src/lib/train/tokenizer.ts new file mode 100644 index 00000000..39d51f49 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/tokenizer.ts @@ -0,0 +1,2462 @@ +/** + * @fileoverview Simplified Tokenizer implementation adapted from huggingface/transformers.js + */ + +import { PUBLIC_DATA_URL } from '$env/static/public'; +import { Template } from '@huggingface/jinja'; +import { int32, Tensor, tensor } from '@piston-ml/piston-web'; + +import type { ToyTokenizer } from './data/toy/types'; + +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ +abstract class Callable { + /** + * Creates a new instance of the Callable class. + */ + constructor() { + /** + * Creates a closure that delegates to a private method 'call' with the given arguments. + * @param args Zero or more arguments to pass to the 'call' method. + * @returns The result of calling the 'call' method. + */ + const closure = ((...args: Args) => { + return (closure as unknown as { call: (...args: Args) => Return }).call(...args); + }) as unknown as (...args: Args) => Return; + return Object.setPrototypeOf(closure, new.target.prototype) as unknown as this & + ((...args: Args) => Return); + } + + /** + * This method should be implemented in subclasses to provide the + * functionality of the callable object. + * + * @param args Zero or more arguments to pass to the 'call' method. + * @throws {Error} If the subclass does not implement the `call` method. + */ + protected abstract call(..._args: Args): Return; +} +interface Callable { + (...args: Args): Return; +} + +// Discriminated config helpers +type WithType = { type: TType }; + +// Normalizer configs +type NormalizerSequenceConfig = WithType<'Sequence'> & { normalizers: NormalizerConfig[] }; +type NFCConfig = WithType<'NFC'>; +type NFDConfig = WithType<'NFD'>; +type NFKCConfig = WithType<'NFKC'>; +type NFKDConfig = WithType<'NFKD'>; +type StripConfig = WithType<'Strip'> & { stripLeft?: boolean; stripRight?: boolean }; +type LowercaseConfig = WithType<'Lowercase'>; +type PrependConfig = WithType<'Prepend'> & { prepend: string }; +type NormalizerConfig = + | NormalizerSequenceConfig + | NFCConfig + | NFDConfig + | NFKCConfig + | NFKDConfig + | StripConfig + | LowercaseConfig + | PrependConfig; + +// PreTokenizer configs and options +type PreTokenizeOptions = { sectionIndex?: number }; +type PreTokenizerSequenceConfig = WithType<'Sequence'> & { pretokenizers: PreTokenizerConfig[] }; +type WhitespacePreTokenizerConfig = WithType<'Whitespace'>; +type WhitespaceSplitConfig = WithType<'WhitespaceSplit'>; +type MetaspacePreTokenizerConfig = WithType<'Metaspace'> & { + addPrefixSpace: boolean; + replacement: string; + strRep?: string; + prependScheme?: 'first' | 'never' | 'always'; +}; +type ByteLevelPreTokenizerConfig = WithType<'ByteLevel'> & { + addPrefixSpace: boolean; + trimOffsets: boolean; + useRegex?: boolean; +}; +type PreTokenizerConfig = + | PreTokenizerSequenceConfig + | WhitespacePreTokenizerConfig + | WhitespaceSplitConfig + | MetaspacePreTokenizerConfig + | ByteLevelPreTokenizerConfig; + +// PostProcessor configs and options +type PostProcessorOptions = { addSpecialTokens?: boolean }; +type PostProcessorResult = { tokens: string[]; tokenTypeIds?: number[] }; +type PostProcessorSequenceConfig = WithType<'Sequence'> & { processors: PostProcessorConfig[] }; +type ByteLevelPostProcessorConfig = WithType<'ByteLevel'>; +type PostProcessorConfig = PostProcessorSequenceConfig | ByteLevelPostProcessorConfig; + +// Decoder configs +type ByteLevelDecoderConfig = WithType<'ByteLevel'> & { trimOffsets?: boolean }; +type ByteFallbackConfig = WithType<'ByteFallback'>; +type FuseDecoderConfig = WithType<'Fuse'>; +type StripDecoderConfig = WithType<'Strip'> & { content: string; start: number; stop: number }; +type DecoderSequenceConfig = WithType<'Sequence'> & { decoders: DecoderConfig[] }; +type BPEDecoderConfig = WithType<'BPEDecoder'> & { suffix: string }; +type DecoderConfig = + | ByteLevelDecoderConfig + | ByteFallbackConfig + | FuseDecoderConfig + | StripDecoderConfig + | DecoderSequenceConfig + | BPEDecoderConfig; + +// Model configs +export interface TokenizerModelConfig { + fuseUnk: boolean; + byteFallback: boolean; + ignoreMerges: boolean; +} + +type BPEConfig = WithType<'BPE'> & + TokenizerModelConfig & { + vocab: Record; + merges: string[] | [string, string][]; + unkToken: string; + endOfWordSuffix?: string; + continuingSubwordSuffix?: string | null; + }; + +type TokenizerModelFactoryConfig = BPEConfig; // Extend when additional models are added + +// Tokenizer JSON and runtime config +interface TokenizerJSON { + normalizer: NormalizerConfig | null; + preTokenizer: PreTokenizerConfig | null; + model: TokenizerModelFactoryConfig; + postProcessor: PostProcessorConfig | null; + decoder: DecoderConfig | null; + addedTokens: AddedTokenConfig[]; +} + +interface TokenizerConfig { + [key: string]: unknown; + additionalSpecialTokens?: string[]; + modelMaxLength: number; + removeSpace: boolean; + cleanUpTokenizationSpaces?: boolean; + paddingSide?: 'left' | 'right'; + addBosToken?: boolean; + addEosToken?: boolean; + chatTemplate?: null | Array<{ name: string; template: string }> | Record; +} + +const TOKENIZER_URL = PUBLIC_DATA_URL + 'tokenizer'; + +/** + * Loads a tokenizer from the specified path. + * @param tokenizerName The path to the tokenizer directory. + * @returns A promise that resolves with tokenizer JSON and config. + */ +async function loadTokenizer(tokenizerName: string): Promise<[TokenizerJSON, TokenizerConfig]> { + return Promise.all([ + fetchJSON(TOKENIZER_URL, `${tokenizerName}/tokenizer.json`).then( + camelCaseKeysDeep + ), + fetchJSON(TOKENIZER_URL, `${tokenizerName}/tokenizer_config.json`).then( + camelCaseKeysDeep + ) + ]); +} + +function isPlainObject(value: unknown): value is Record { + return ( + value !== null && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype + ); +} + +function toCamelKey(key: string): string { + return key.includes('_') + ? key.replace(/_+([a-zA-Z0-9])/g, (_m, c: string) => c.toUpperCase()) + : key; +} + +function camelCaseKeysDeep(input: T): T { + if (Array.isArray(input)) { + return input.map((item) => camelCaseKeysDeep(item)) as unknown as T; + } + if (isPlainObject(input)) { + const obj = input as Record; + const out: Record = Object.create(null); + for (const [key, value] of Object.entries(obj)) { + const transformed = camelCaseKeysDeep(value); + // Preserve original snake_case for compatibility + out[key] = transformed; + const camelKey = toCamelKey(key); + if (camelKey !== key && !(camelKey in out)) { + out[camelKey] = transformed; + } + } + return out as unknown as T; + } + return input; +} + +// Minimal fetch wrapper used here; replace with project-util if available +async function fetchJSON(basePath: string, fileName: string): Promise { + const url = `${basePath.replace(/\/$/, '')}/${fileName}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`Failed to load ${fileName} from ${url}`); + return res.json() as Promise; +} + +/** + * Helper function to convert an Object to a Map + * @param obj The object to convert. + * @returns The map. + */ +function objectToMap(obj: Record): Map { + return new Map(Object.entries(obj)); +} + +/** + * Helper function to fuse consecutive unknown tokens. + * @param arr The list of input tokens + * @param tokensToIds The mapping from tokens to token ids. + * @param unkTokenId The value to fuse on. + */ +function fuseUnk(arr: string[], tokensToIds: Map, unkTokenId: number): string[] { + const fused = []; + let i = 0; + while (i < arr.length) { + fused.push(arr[i]); + if ((tokensToIds.get(arr[i]) ?? unkTokenId) !== unkTokenId) { + ++i; + continue; + } + + while (++i < arr.length && (tokensToIds.get(arr[i]) ?? unkTokenId) === unkTokenId) { + if (tokensToIds.get(fused[fused.length - 1]) !== unkTokenId) { + fused[fused.length - 1] += arr[i]; + } + } + } + + return fused; +} + +/** + * Split a string on whitespace. + * @param text The text to split. + * @returns The split string. + */ +function whitespaceSplit(text: string): string[] { + return text.match(/\S+/g) || []; +} + +/** + * Represent a token added by the user on top of the existing Model vocabulary. + * AddedToken can be configured to specify the behavior they should have in various situations like: + * - Whether they should only match single words + * - Whether to include any whitespace on its left or right + */ +interface AddedTokenConfig { + content: string; + id: number; + singleWord?: boolean; + lstrip?: boolean; + rstrip?: boolean; + normalized?: boolean; + special?: boolean; +} +class AddedToken { + content: string; + id: number; + singleWord: boolean; + lstrip: boolean; + rstrip: boolean; + special: boolean; + normalized: boolean | null; + /** + * Creates a new instance of AddedToken. + * @param config Added token configuration object. + * @param config.content The content of the added token. + * @param config.id The id of the added token. + * @param config.singleWord Whether this token must be a single word or can break words. + * @param config.lstrip Whether this token should strip whitespaces on its left. + * @param config.rstrip Whether this token should strip whitespaces on its right. + * @param config.normalized Whether this token should be normalized. + * @param config.special Whether this token is special. + */ + constructor(config: AddedTokenConfig) { + this.content = config.content; + this.id = config.id; + this.singleWord = config.singleWord ?? false; + this.lstrip = config.lstrip ?? false; + this.rstrip = config.rstrip ?? false; + this.special = config.special ?? false; + this.normalized = config.normalized ?? null; + } +} + +export interface TokenizerModelConfig { + fuseUnk: boolean; + byteFallback: boolean; + ignoreMerges: boolean; +} + +/** + * Abstract base class for tokenizer models. + */ +export class TokenizerModel extends Callable<[string[]], string[]> { + config: TokenizerModelConfig; + vocab: string[]; + tokensToIds: Map; + unkTokenId?: number; + unkToken?: string; + endOfWordSuffix?: string; + fuseUnk: boolean; + /** + * Creates a new instance of TokenizerModel. + * @param config The configuration object for the TokenizerModel. + */ + constructor(config: TokenizerModelConfig) { + super(); + this.config = config; + + this.vocab = []; + + this.tokensToIds = new Map(); + + this.unkTokenId = undefined; + this.unkToken = undefined; + this.endOfWordSuffix = undefined; + + this.fuseUnk = this.config.fuseUnk ?? false; + } + + /** + * Instantiates a new TokenizerModel instance based on the configuration object provided. + * @param config The configuration object for the TokenizerModel. + * @param _args Optional arguments to pass to the specific TokenizerModel constructor. + * @returns A new instance of a TokenizerModel. + * @throws Will throw an error if the TokenizerModel type in the config is not recognized. + */ + static fromConfig(config: TokenizerModelFactoryConfig, ..._args: unknown[]): TokenizerModel { + switch (config.type) { + case 'BPE': + default: + return new BPE(config); + } + } + + /** + * Internal function to call the TokenizerModel instance. + * @param tokens The tokens to encode. + * @returns The encoded tokens. + */ + protected call(...[tokens]: [string[]]): string[] { + tokens = this.encode(tokens); + if (this.fuseUnk) { + // Fuse unknown tokens + tokens = fuseUnk(tokens, this.tokensToIds, this.unkTokenId as number); + } + return tokens; + } + + /** + * Encodes a list of tokens into a list of token IDs. + * @param tokens The tokens to encode. + * @returns The encoded tokens. + * @throws Will throw an error if not implemented in a subclass. + */ + encode(_tokens: string[]): string[] { + throw Error('encode should be implemented in subclass.'); + } + + /** + * Converts a list of tokens into a list of token IDs. + * @param tokens The tokens to convert. + * @returns The converted token IDs. + */ + convertTokensToIds(tokens: string[]): number[] { + return tokens.map((t) => this.tokensToIds.get(t) ?? (this.unkTokenId as number)); + } + + /** + * Converts a list of token IDs into a list of tokens. + * @param ids The token IDs to convert. + * @returns The converted tokens. + */ + convertIdsToTokens(ids: number[] | bigint[]): string[] { + return ids.map((i) => this.vocab[Number(i)] ?? (this.unkToken as string)); + } +} + +/** + * Returns list of utf-8 byte and a mapping to unicode strings. + * Specifically avoids mapping to whitespace/control characters the BPE code barfs on. + * @returns Object with utf-8 byte keys and unicode string values. + */ +const BYTES_TO_UNICODE = (() => { + // Returns list of utf-8 byte and a mapping to unicode strings. + // We specifically avoids mapping to whitespace/control characters the bpe code barfs on. + + const bs = [ + ...Array.from( + { length: '~'.charCodeAt(0) - '!'.charCodeAt(0) + 1 }, + (_, i) => i + '!'.charCodeAt(0) + ), + ...Array.from( + { length: '¬'.charCodeAt(0) - '¡'.charCodeAt(0) + 1 }, + (_, i) => i + '¡'.charCodeAt(0) + ), + ...Array.from( + { length: 'ÿ'.charCodeAt(0) - '®'.charCodeAt(0) + 1 }, + (_, i) => i + '®'.charCodeAt(0) + ) + ]; + const cs = bs.slice(); + let n = 0; + for (let b = 0; b < 256; ++b) { + if (!bs.includes(b)) { + bs.push(b); + cs.push(256 + n); + n += 1; + } + } + const ccs = cs.map((n) => String.fromCharCode(n)); + return Object.fromEntries(bs.map((b, i) => [b, ccs[i]])); +})(); + +const UNICODE_TO_BYTES = Object.fromEntries( + Object.entries(BYTES_TO_UNICODE).map(([key, value]) => [value, key]) +); + +interface BPENode { + token: string; + bias: number; + score?: number; + prev?: BPENode; + next?: BPENode; +} + +/** + * BPE class for encoding text into Byte-Pair-Encoding (BPE) tokens. + */ +class BPE extends TokenizerModel { + merges!: [string, string][]; + bpeRanks!: Map; + continuingSubwordSuffix!: string | null; + byteFallback!: boolean; + textEncoder!: TextEncoder; + ignoreMerges!: boolean; + maxLengthToCache!: number; + cacheCapacity!: number; + cache!: LRUCache; + + constructor(config: BPEConfig) { + super(config); + this.tokensToIds = objectToMap(config.vocab); + this.unkTokenId = this.tokensToIds.get(config.unkToken) as number; + this.unkToken = config.unkToken as string; + this.vocab = new Array(this.tokensToIds.size); + for (const [key, value] of this.tokensToIds) { + this.vocab[value] = key; + } + const useNewMergeFormat = Array.isArray(config.merges[0]); + this.merges = useNewMergeFormat + ? (config.merges as [string, string][]) + : (config.merges as string[]).map((x) => x.split(' ', 2) as [string, string]); + this.bpeRanks = new Map(this.merges.map((x, i) => [JSON.stringify(x), i])) as Map< + string, + number + >; + this.endOfWordSuffix = config.endOfWordSuffix as string | undefined; + this.continuingSubwordSuffix = (config.continuingSubwordSuffix ?? null) as string | null; + this.byteFallback = (this.config.byteFallback ?? false) as boolean; + if (this.byteFallback) { + this.textEncoder = new TextEncoder(); + } + this.ignoreMerges = (this.config.ignoreMerges ?? false) as boolean; + this.maxLengthToCache = 256; + this.cacheCapacity = 10000; + this.cache = new LRUCache(this.cacheCapacity); + } + clearCache() { + this.cache.clear(); + } + bpe(token: string): string[] { + if (token.length === 0) { + return []; + } + const cached = this.cache.get(token); + if (cached !== undefined) { + return cached; + } + const word = Array.from(token); + if (this.endOfWordSuffix) { + word[word.length - 1] += this.endOfWordSuffix; + } + let result: string[] = []; + if (word.length > 1) { + const queue = new PriorityQueue((a, b) => (a.score as number) < (b.score as number)); + let startingNode: BPENode = { + token: word[0], + bias: 0, + prev: undefined, + next: undefined + }; + let previousNode = startingNode; + for (let i = 1; i < word.length; ++i) { + const currentNode: BPENode = { + bias: i / word.length, + token: word[i], + prev: previousNode, + next: undefined + }; + previousNode.next = currentNode; + this.addNode(queue, previousNode); + previousNode = currentNode; + } + while (!queue.isEmpty()) { + const node = queue.pop() as BPENode & { + deleted?: boolean; + prev?: BPENode & { deleted?: boolean }; + next?: BPENode & { deleted?: boolean }; + }; + if (node.deleted || !node.next || node.next.deleted) continue; + node.deleted = true; + node.next.deleted = true; + if (node.prev) { + const newPreviousNode = { ...(node.prev as BPENode) } as BPENode; + node.prev.deleted = true; + node.prev = newPreviousNode; + if (newPreviousNode.prev) { + (newPreviousNode.prev as BPENode).next = newPreviousNode; + } else { + startingNode = newPreviousNode; + } + } + const merged: BPENode = { + token: node.token + (node.next as BPENode).token, + bias: node.bias, + prev: node.prev, + next: (node.next as BPENode).next + }; + if (merged.prev) { + (merged.prev as BPENode).next = merged; + this.addNode(queue, merged.prev as BPENode); + } else { + startingNode = merged; + } + if (merged.next) { + (merged.next as BPENode).prev = merged; + this.addNode(queue, merged); + } + } + for ( + let currentNode: BPENode | undefined = startingNode; + currentNode !== undefined; + currentNode = currentNode.next + ) { + result.push(currentNode.token); + } + } else { + result = word; + } + if (this.continuingSubwordSuffix) { + for (let i = 0; i < result.length - 1; ++i) { + result[i] += this.continuingSubwordSuffix; + } + } + if (token.length < this.maxLengthToCache) { + this.cache.put(token, result); + } + return result; + } + private addNode(queue: PriorityQueue, node: BPENode) { + const rank = this.bpeRanks.get(JSON.stringify([node.token, (node.next as BPENode).token])); + if (rank !== undefined) { + node.score = rank + node.bias; + queue.push(node); + } + } + encode(tokens: string[]): string[] { + const outputTokens: string[] = []; + for (const token of tokens) { + if (this.ignoreMerges && this.tokensToIds.has(token)) { + outputTokens.push(token); + continue; + } + const bpeTokenList = this.bpe(token); + for (const t of bpeTokenList) { + if (this.tokensToIds.has(t)) { + outputTokens.push(t); + } else if (this.byteFallback) { + const byteTokens = Array.from(this.textEncoder.encode(t)).map( + (x) => `<0x${x.toString(16).toUpperCase().padStart(2, '0')}>` + ); + if (byteTokens.every((x) => this.tokensToIds.has(x))) { + outputTokens.push(...byteTokens); + } else { + outputTokens.push(this.unkToken as string); + } + } else { + outputTokens.push(this.unkToken as string); + } + } + } + return outputTokens; + } +} + +/** + * A base class for text normalization. + */ +abstract class Normalizer extends Callable<[string], string> { + config: TConfig; + /** + * @param config The configuration object for the normalizer. + */ + constructor(config: TConfig) { + super(); + this.config = config; + } + static fromConfig(config: TConfig): Normalizer { + switch (config.type) { + case 'Sequence': + return new NormalizerSequence(config); + case 'NFC': + return new NFC(config); + case 'NFD': + return new NFD(config); + case 'NFKC': + return new NFKC(config); + case 'NFKD': + return new NFKD(config); + case 'Strip': + return new StripNormalizer(config); + case 'Lowercase': + return new Lowercase(config); + case 'Prepend': + return new Prepend(config); + } + } + + normalize(_text: string): string { + throw Error('normalize should be implemented in subclass.'); + } + + protected call(...[text]: [string]): string { + return this.normalize(text); + } +} + +/** + * A normalizer that applies Unicode normalization to the input text. + */ +abstract class UnicodeNormalizer extends Normalizer { + form: 'NFC' | 'NFD' | 'NFKC' | 'NFKD' | undefined = undefined; + + /** + * Normalize the input text by applying Unicode normalization. + * @param text The input text to be normalized. + * @returns The normalized text. + */ + normalize(text: string) { + text = text.normalize(this.form as 'NFC'); + return text; + } +} + +/** + * A normalizer that applies Unicode normalization form C (NFC) to the input text. + * Canonical Decomposition, followed by Canonical Composition. + */ +class NFC extends UnicodeNormalizer { + form = 'NFC' as const; +} + +/** + * A normalizer that applies Unicode normalization form D (NFD) to the input text. + * Canonical Decomposition. + */ +class NFD extends UnicodeNormalizer { + form = 'NFD' as const; +} + +/** + * A normalizer that applies Unicode normalization form KC (NFKC) to the input text. + * Compatibility Decomposition, followed by Canonical Composition. + */ +class NFKC extends UnicodeNormalizer { + form = 'NFKC' as const; +} + +/** + * A normalizer that applies Unicode normalization form KD (NFKD) to the input text. + * Compatibility Decomposition. + */ +class NFKD extends UnicodeNormalizer { + form = 'NFKD' as const; +} + +/** + * A normalizer that strips leading and/or trailing whitespace from the input text. + */ +class StripNormalizer extends Normalizer { + /** + * Strip leading and/or trailing whitespace from the input text. + * @param text The input text. + * @returns The normalized text. + */ + normalize(text: string) { + const cfg = this.config; + if (cfg.stripLeft && cfg.stripRight) { + // Fast path to avoid an extra trim call + text = text.trim(); + } else { + if (cfg.stripLeft) { + text = text.trimStart(); + } + if (cfg.stripRight) { + text = text.trimEnd(); + } + } + return text; + } +} + +/** + * A Normalizer that lowercases the input string. + */ +class Lowercase extends Normalizer { + /** + * Lowercases the input string. + * @param text The text to normalize. + * @returns The normalized text. + */ + normalize(text: string) { + text = text.toLowerCase(); + return text; + } +} + +/** + * A Normalizer that prepends a string to the input string. + */ +class Prepend extends Normalizer { + /** + * Prepends the input string. + * @param text The text to normalize. + * @returns The normalized text. + */ + normalize(text: string) { + const cfg = this.config; + text = cfg.prepend + text; + return text; + } +} + +/** + * A Normalizer that applies a sequence of Normalizers. + */ + +class NormalizerSequence extends Normalizer { + normalizers: Normalizer[]; + + constructor(config: NormalizerSequenceConfig) { + super(config); + this.normalizers = config.normalizers.map((x) => Normalizer.fromConfig(x)); + } + /** + * Apply a sequence of Normalizers to the input text. + * @param text The text to normalize. + * @returns The normalized text. + */ + normalize(text: string) { + return this.normalizers.reduce((t, normalizer) => { + return normalizer.normalize(t); + }, text); + } +} + +/** + * A callable class representing a pre-tokenizer used in tokenization. Subclasses + * should implement the `preTokenizeText` method to define the specific pre-tokenization logic. + */ +abstract class PreTokenizer extends Callable< + [string | string[], PreTokenizeOptions | undefined], + string[] +> { + /** + * Factory method that returns an instance of a subclass of `PreTokenizer` based on the provided configuration. + * + * @static + * @param config A configuration object for the pre-tokenizer. + * @returns An instance of a subclass of `PreTokenizer`. + * @throws If the provided configuration object does not correspond to any known pre-tokenizer. + */ + static fromConfig(config: PreTokenizerConfig): PreTokenizer { + switch (config.type) { + case 'Sequence': + return new PreTokenizerSequence(config); + case 'Whitespace': + return new WhitespacePreTokenizer(); + case 'WhitespaceSplit': + return new WhitespaceSplit(); + case 'Metaspace': + return new MetaspacePreTokenizer(config); + case 'ByteLevel': + return new ByteLevelPreTokenizer(config); + default: + throw new Error('Unknown PreTokenizer type'); + } + } + + /** + * Method that should be implemented by subclasses to define the specific pre-tokenization logic. + * + * @param text The text to pre-tokenize. + * @param options Additional options for the pre-tokenization logic. + * @returns The pre-tokenized text. + * @throws {Error} If the method is not implemented in the subclass. + */ + abstract preTokenizeText(text: string, options?: PreTokenizeOptions): string[]; + + /** + * Tokenizes the given text into pre-tokens. + * @param text The text or array of texts to pre-tokenize. + * @param options Additional options for the pre-tokenization logic. + * @returns An array of pre-tokens. + */ + preTokenize(text: string | string[], options?: PreTokenizeOptions): string[] { + return ( + Array.isArray(text) + ? (text as string[]).map((x) => this.preTokenizeText(x, options)) + : this.preTokenizeText(text as string, options) + ).flat(); + } + + /** + * Alias for {@link PreTokenizer#preTokenize}. + * @param text The text or array of texts to pre-tokenize. + * @param options Additional options for the pre-tokenization logic. + * @returns An array of pre-tokens. + */ + protected call( + ...[text, options]: [string | string[], PreTokenizeOptions | undefined] + ): string[] { + return this.preTokenize(text, options); + } +} + +/** + * A pre-tokenizer that splits text into Byte-Pair-Encoding (BPE) subwords. + * @extends PreTokenizer + */ + +class ByteLevelPreTokenizer extends PreTokenizer { + config: ByteLevelPreTokenizerConfig; + addPrefixSpace!: boolean; + trimOffsets!: boolean; + useRegex!: boolean; + pattern!: RegExp; + byteEncoder!: Record; + textEncoder!: TextEncoder; + constructor(config: ByteLevelPreTokenizerConfig) { + super(); + this.config = config; + this.addPrefixSpace = this.config.addPrefixSpace; + this.trimOffsets = this.config.trimOffsets; + this.useRegex = this.config.useRegex ?? true; + this.pattern = /'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+/gu; + this.byteEncoder = BYTES_TO_UNICODE as Record; + this.textEncoder = new TextEncoder(); + } + preTokenizeText(text: string, _options?: PreTokenizeOptions): string[] { + if (this.addPrefixSpace && !text.startsWith(' ')) { + text = ' ' + text; + } + const tokens = this.useRegex ? text.match(this.pattern) || [] : [text]; + return tokens.map((token) => + Array.from(this.textEncoder.encode(token), (byte) => this.byteEncoder[byte]).join('') + ); + } +} + +type PostProcessorArgs = [string[], (string[] | null | undefined)?, PostProcessorOptions?]; +abstract class PostProcessor extends Callable { + config: PostProcessorConfig; + constructor(config: PostProcessorConfig) { + super(); + this.config = config; + } + static fromConfig(config: PostProcessorConfig): PostProcessor { + switch (config.type) { + case 'ByteLevel': + return new ByteLevelPostProcessor(config); + case 'Sequence': + return new PostProcessorSequence(config); + default: + throw new Error('Unknown PostProcessor type'); + } + } + + abstract postProcess( + tokens: string[], + ...args: [string[] | null | undefined, PostProcessorOptions?] + ): PostProcessorResult; + + protected call( + ...[tokens, ...args]: [string[], (string[] | null | undefined)?, PostProcessorOptions?] + ): PostProcessorResult { + return this.postProcess(tokens, ...args); + } +} + +/** + * A PostProcessor that returns the given tokens as is. + */ +class ByteLevelPostProcessor extends PostProcessor { + postProcess(tokens: string[], tokensPair: string[] | null = null): { tokens: string[] } { + if (tokensPair) { + tokens = mergeArrays(tokens, tokensPair); + } + return { tokens }; + } +} + +/** + * A post-processor that applies multiple post-processors in sequence. + */ +class PostProcessorSequence extends PostProcessor { + processors: PostProcessor[]; + constructor(config: PostProcessorSequenceConfig) { + super(config); + this.processors = config.processors.map((x) => PostProcessor.fromConfig(x)); + } + postProcess( + tokens: string[], + tokensPair: string[] | null = null, + options: PostProcessorOptions = {} + ): { tokens: string[]; tokenTypeIds?: number[] } { + let tokenTypeIds: number[] | undefined; + for (const processor of this.processors) { + if (processor instanceof ByteLevelPostProcessor) { + const output = processor.postProcess(tokens); + tokens = output.tokens; + if (tokensPair) { + const pairOutput = processor.postProcess(tokensPair); + tokensPair = pairOutput.tokens; + } + } else { + const output = processor.postProcess(tokens, tokensPair ?? null, options); + tokens = output.tokens; + if (output.tokenTypeIds) { + tokenTypeIds = output.tokenTypeIds; + } + } + } + return { tokens, tokenTypeIds: tokenTypeIds }; + } +} + +/** + * The base class for token decoders. + */ +abstract class Decoder extends Callable< + [string[]], + string +> { + config: TConfig; + addedTokens: AddedToken[]; + endOfWordSuffix?: string; + constructor(config: TConfig) { + super(); + this.config = config; + this.addedTokens = []; + this.endOfWordSuffix = undefined; + } + static fromConfig(config: DecoderConfig): Decoder { + switch (config.type) { + case 'ByteLevel': + return new ByteLevelDecoder(config); + case 'ByteFallback': + return new ByteFallback(config); + case 'Fuse': + return new FuseDecoder(config); + case 'Strip': + return new StripDecoder(config); + case 'Sequence': + return new DecoderSequence(config); + case 'BPEDecoder': + return new BPEDecoder(config); + default: + throw new Error('Unknown Decoder type'); + } + } + protected call(...[tokens]: [string[]]): string { + return this.decode(tokens); + } + decode(tokens: string[]): string { + return this.decodeChain(tokens).join(''); + } + abstract decodeChain(tokens: string[]): string[]; +} + +class ByteFallback extends Decoder { + textDecoder!: TextDecoder; + constructor(config: ByteFallbackConfig) { + super(config); + this.textDecoder = new TextDecoder(); + } + decodeChain(tokens: string[]): string[] { + const newTokens: string[] = []; + let previousByteTokens: number[] = []; + for (const token of tokens) { + let bytes: number | null = null; + if (token.length === 6 && token.startsWith('<0x') && token.endsWith('>')) { + const byte = parseInt(token.slice(3, 5), 16); + if (!isNaN(byte)) { + bytes = byte; + } + } + if (bytes !== null) { + previousByteTokens.push(bytes); + } else { + if (previousByteTokens.length > 0) { + const string = this.textDecoder.decode(Uint8Array.from(previousByteTokens)); + newTokens.push(string); + previousByteTokens = []; + } + newTokens.push(token); + } + } + if (previousByteTokens.length > 0) { + const string = this.textDecoder.decode(Uint8Array.from(previousByteTokens)); + newTokens.push(string); + previousByteTokens = []; + } + return newTokens; + } +} + +/** + * Fuse simply fuses all tokens into one big string. + * It's usually the last decoding step anyway, but this decoder + * exists incase some decoders need to happen after that step + */ +class FuseDecoder extends Decoder { + /** @type {Decoder['decodeChain']} */ + decodeChain(tokens: string[]): string[] { + return [tokens.join('')]; + } +} + +class StripDecoder extends Decoder { + content!: string; + start!: number; + stop!: number; + constructor(config: StripDecoderConfig) { + super(config); + const cfg = this.config; + this.content = cfg.content; + this.start = cfg.start; + this.stop = cfg.stop; + } + /** @type {Decoder['decodeChain']} */ + decodeChain(tokens: string[]): string[] { + return tokens.map((token) => { + let startCut = 0; + for (let i = 0; i < this.start; ++i) { + if (token[i] === this.content) { + startCut = i + 1; + continue; + } else { + break; + } + } + let stopCut = token.length; + for (let i = 0; i < this.stop; ++i) { + const index = token.length - i - 1; + if (token[index] === this.content) { + stopCut = index; + continue; + } else { + break; + } + } + return token.slice(startCut, stopCut); + }); + } +} + +/** + * Byte-level decoder for tokenization output. Inherits from the `Decoder` class. + * @extends Decoder + */ +class ByteLevelDecoder extends Decoder { + byteDecoder!: Record; + textDecoder!: TextDecoder; + constructor(config: ByteLevelDecoderConfig) { + super(config); + this.byteDecoder = UNICODE_TO_BYTES as unknown as Record; + this.textDecoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true }); + this.endOfWordSuffix = undefined; + } + convertTokensToString(tokens: string[]): string { + const text = tokens.join(''); + const byteArray = new Uint8Array([...text].map((c) => this.byteDecoder[c])); + const decodedText = this.textDecoder.decode(byteArray); + return decodedText; + } + /** @type {Decoder['decodeChain']} */ + decodeChain(tokens: string[]): string[] { + const subTexts: string[] = []; + let currentSubText: string[] = []; + for (const token of tokens) { + if (this.addedTokens.find((x) => x.content === token) !== undefined) { + if (currentSubText.length > 0) { + subTexts.push(this.convertTokensToString(currentSubText)); + currentSubText = []; + } + subTexts.push(token); + } else { + currentSubText.push(token); + } + } + if (currentSubText.length > 0) { + subTexts.push(this.convertTokensToString(currentSubText)); + } + return subTexts; + } +} + +/** + * Apply a sequence of decoders. + * @extends Decoder + */ +class DecoderSequence extends Decoder { + decoders!: Decoder[]; + constructor(config: DecoderSequenceConfig) { + super(config); + this.decoders = config.decoders.map((x) => Decoder.fromConfig(x)); + } + /** @type {Decoder['decodeChain']} */ + decodeChain(tokens: string[]): string[] { + return this.decoders.reduce((toks: string[], decoder: Decoder) => { + return decoder.decodeChain(toks); + }, tokens); + } +} + +class BPEDecoder extends Decoder { + suffix!: string; + constructor(config: BPEDecoderConfig) { + super(config); + const cfg = this.config; + this.suffix = cfg.suffix; + } + /** @type {Decoder['decodeChain']} */ + decodeChain(tokens: string[]): string[] { + return tokens.map((token, i) => { + return token.replaceAll(this.suffix, i === tokens.length - 1 ? '' : ' '); + }); + } +} + +/** + * This PreTokenizer replaces spaces with the given replacement character, adds a prefix space if requested, + * and returns a list of tokens. + * @extends PreTokenizer + */ +class MetaspacePreTokenizer extends PreTokenizer { + addPrefixSpace: boolean; + replacement: string; + strRep: string; + prependScheme: 'first' | 'never' | 'always'; + /** + * @param {Object} config The configuration object for the MetaspacePreTokenizer. + * @param {boolean} config.addPrefixSpace Whether to add a prefix space to the first token. + * @param {string} config.replacement The character to replace spaces with. + * @param {string} [config.strRep=config.replacement] An optional string representation of the replacement character. + * @param {'first'|'never'|'always'} [config.prependScheme='always'] The metaspace prepending scheme. + */ + constructor(config: MetaspacePreTokenizerConfig) { + super(); + + this.addPrefixSpace = config.addPrefixSpace; + this.replacement = config.replacement; + this.strRep = config.strRep || this.replacement; + this.prependScheme = config.prependScheme ?? 'always'; + } + + /** + * This method takes a string, replaces spaces with the replacement character, + * adds a prefix space if requested, and returns a new list of tokens. + * @param text The text to pre-tokenize. + * @param options The options for the pre-tokenization. + * @param options.sectionIndex The index of the section to pre-tokenize. + * @returns A new list of pre-tokenized tokens. + */ + preTokenizeText( + text: string, + { sectionIndex: sectionIndex = undefined }: PreTokenizeOptions = {} + ) { + let normalized = text.replaceAll(' ', this.strRep); + + if ( + // We add a prefix space if: + // (1) The addPrefixSpace option is enabled and the normalized token does not already start + // with the replacement character. + this.addPrefixSpace && + !normalized.startsWith(this.replacement) && + // and (2) either: + // (a) prependScheme is 'always' + // (b) prependScheme is 'first' and this is the first section + (this.prependScheme === 'always' || (this.prependScheme === 'first' && sectionIndex === 0)) + ) { + normalized = this.strRep + normalized; + } + return [normalized]; + } +} + +/** + * A pre-tokenizer that applies a sequence of pre-tokenizers to the input text. + * @extends PreTokenizer + */ +class PreTokenizerSequence extends PreTokenizer { + tokenizers: PreTokenizer[]; + /** + * Creates an instance of PreTokenizerSequence. + * @param {Object} config The configuration object for the pre-tokenizer sequence. + * @param {Object[]} config.pretokenizers An array of pre-tokenizer configurations. + */ + constructor(config: PreTokenizerSequenceConfig) { + super(); + this.tokenizers = config.pretokenizers.map((x) => PreTokenizer.fromConfig(x)); + } + + /** + * Applies each pre-tokenizer in the sequence to the input text in turn. + * @param text The text to pre-tokenize. + * @param options Additional options for the pre-tokenization logic. + * @returns The pre-tokenized text. + */ + preTokenizeText(text: string, options: PreTokenizeOptions) { + // Use reduce to apply each tokenizer to the text + return this.tokenizers.reduce( + (preTokenizedText, tokenizer) => { + return tokenizer.preTokenize(preTokenizedText, options); + }, + [text] + ); + } +} + +/** + * Splits on word boundaries (using the following regular expression: `\w+|[^\w\s]+`). + */ +class WhitespacePreTokenizer extends PreTokenizer { + /** + * Creates an instance of WhitespacePreTokenizer. + * @param config The configuration object for the pre-tokenizer. + */ + constructor() { + super(); + } + /** + * Pre-tokenizes the input text by splitting it on word boundaries. + * @param text The text to be pre-tokenized. + * @param options Additional options for the pre-tokenization logic. + * @returns An array of tokens produced by splitting the input text on whitespace. + */ + preTokenizeText(text: string, _options: unknown) { + return text.match(/\w+|[^\w\s]+/g) || []; + } +} + +/** + * Splits a string of text by whitespace characters into individual tokens. + * @extends PreTokenizer + */ +class WhitespaceSplit extends PreTokenizer { + /** + * Creates an instance of WhitespaceSplit. + * @param config The configuration object for the pre-tokenizer. + */ + constructor() { + super(); + } + /** + * Pre-tokenizes the input text by splitting it on whitespace characters. + * @param text The text to be pre-tokenized. + * @param options Additional options for the pre-tokenization logic. + * @returns An array of tokens produced by splitting the input text on whitespace. + */ + preTokenizeText(text: string, _options: unknown) { + return whitespaceSplit(text); + } +} + +const SPECIAL_TOKEN_ATTRIBUTES = [ + 'bos_token', + 'eos_token', + 'unk_token', + 'sep_token', + 'pad_token', + 'cls_token', + 'mask_token' + // additional_special_tokens (TODO) +]; + +/** + * + * Helper function for padding values of an object, which are each arrays. + * NOTE: No additional checks are made here for validity of arguments. + * @param item The input object. + * @param length The length to pad to. + * @param valueFn Determine the value to fill the array, based on its key. + * @param side Which side to pad the array. + */ +function padHelper( + item: Record, + length: number, + valueFn: (key: string) => T, + side: 'right' | 'left' +) { + for (const key of Object.keys(item)) { + const diff = length - item[key].length; + const value = valueFn(key); + + const padData = new Array(diff).fill(value); + item[key] = + side === 'right' ? mergeArrays(item[key], padData) : mergeArrays(padData, item[key]); + } +} + +/** + * Helper function for truncating values of an object, which are each arrays. + * NOTE: No additional checks are made here for validity of arguments. + * @param item The input object. + * @param length The length to truncate to. + */ +function truncateHelper(item: Record, length: number) { + // Setting .length to a lower value truncates the array in-place: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length + for (const key of Object.keys(item)) { + item[key].length = length; + } +} + +interface DecodeArgs { + skipSpecialTokens?: boolean; + cleanUpTokenizationSpaces?: boolean; +} + +type BatchEncodingItem = number[] | number[][] | Tensor; + +interface BatchEncoding { + inputIds: BatchEncodingItem; + attentionMask: BatchEncodingItem; + tokenTypeIds?: BatchEncodingItem; +} + +interface Message { + role: string; + content: string; +} + +export class PreTrainedTokenizer extends Callable< + [ + string | string[], + { + textPair?: string | null; + addSpecialTokens?: boolean; + padding?: boolean | 'max_length'; + truncation?: boolean | null; + maxLength?: number | null; + returnTensor?: boolean; + returnTokenTypeIds?: boolean | null; + }? + ], + BatchEncoding +> { + config: TokenizerConfig; + normalizer!: ((text: string) => string) | Normalizer | null; + preTokenizer!: ((text: string, options?: PreTokenizeOptions) => string[]) | PreTokenizer | null; + model!: TokenizerModel; + postProcessor!: + | (( + tokens: string[], + tokensPair?: string[] | null, + options?: PostProcessorOptions + ) => PostProcessorResult) + | PostProcessor + | null; + decoder!: ((tokens: string[]) => string) | Decoder | null; + specialTokens: string[]; + allSpecialIds: number[]; + addedTokens: AddedToken[]; + additionalSpecialTokens: string[]; + addedTokensSplitter: DictionarySplitter; + addedTokensMap: Map; + maskToken?: string | null; + maskTokenId?: number; + padToken?: string | null; + padTokenId?: number; + sepToken?: string | null; + sepTokenId?: number; + unkToken?: string | null; + unkTokenId?: number; + bosToken?: string | null; + bosTokenId?: number; + eosToken?: string | null; + eosTokenId?: number; + modelMaxLength!: number; + removeSpace!: boolean; + cleanUpTokenizationSpaces!: boolean; + paddingSide: 'left' | 'right' = 'right'; + addBoxToken?: boolean; + addEosToken?: boolean; + chatTemplate: null | Record | Array<{ name: string; template: string }>; + returnTokenTypeIds = false; + private compiledTemplateCache: Map; + constructor(tokenizerJSON: TokenizerJSON, tokenizerConfig: TokenizerConfig) { + super(); + this.config = tokenizerConfig; + this.normalizer = tokenizerJSON.normalizer + ? Normalizer.fromConfig(tokenizerJSON.normalizer) + : null; + this.preTokenizer = tokenizerJSON.preTokenizer + ? PreTokenizer.fromConfig(tokenizerJSON.preTokenizer) + : null; + this.model = TokenizerModel.fromConfig(tokenizerJSON.model, tokenizerConfig); + this.postProcessor = tokenizerJSON.postProcessor + ? PostProcessor.fromConfig(tokenizerJSON.postProcessor) + : null; + this.decoder = tokenizerJSON.decoder ? Decoder.fromConfig(tokenizerJSON.decoder) : null; + this.specialTokens = []; + this.allSpecialIds = []; + this.addedTokens = []; + for (const addedToken of tokenizerJSON.addedTokens) { + const token = new AddedToken(addedToken); + this.addedTokens.push(token); + this.model.tokensToIds.set(token.content, token.id); + this.model.vocab[token.id] = token.content; + if (token.special) { + this.specialTokens.push(token.content); + this.allSpecialIds.push(token.id); + } + } + this.additionalSpecialTokens = tokenizerConfig.additionalSpecialTokens ?? []; + this.specialTokens.push(...this.additionalSpecialTokens); + this.specialTokens = [...new Set(this.specialTokens)]; + if (this.decoder) { + (this.decoder as Decoder).addedTokens = this.addedTokens; + (this.decoder as Decoder).endOfWordSuffix = this.model.endOfWordSuffix; + } + this.addedTokensSplitter = new DictionarySplitter(this.addedTokens.map((x) => x.content)); + this.addedTokensMap = new Map(this.addedTokens.map((x) => [x.content, x])); + this.maskToken = this.getToken('mask_token'); + this.maskTokenId = this.model.tokensToIds.get(this.maskToken as string); + this.padToken = this.getToken('pad_token', 'eos_token'); + this.padTokenId = this.model.tokensToIds.get(this.padToken as string); + this.sepToken = this.getToken('sep_token'); + this.sepTokenId = this.model.tokensToIds.get(this.sepToken as string); + this.unkToken = this.getToken('unk_token'); + this.unkTokenId = this.model.tokensToIds.get(this.unkToken as string); + this.bosToken = this.getToken('bos_token'); + this.bosTokenId = this.model.tokensToIds.get(this.bosToken as string); + this.eosToken = this.getToken('eos_token'); + this.eosTokenId = this.model.tokensToIds.get(this.eosToken as string); + this.modelMaxLength = tokenizerConfig.modelMaxLength as number; + this.removeSpace = tokenizerConfig.removeSpace as boolean; + this.cleanUpTokenizationSpaces = (tokenizerConfig.cleanUpTokenizationSpaces ?? true) as boolean; + if (tokenizerConfig.paddingSide) { + this.paddingSide = tokenizerConfig.paddingSide as 'left' | 'right'; + } + this.addBoxToken = tokenizerConfig.addBosToken as boolean; + this.addEosToken = tokenizerConfig.addEosToken as boolean; + this.chatTemplate = tokenizerConfig.chatTemplate ?? null; + if (Array.isArray(this.chatTemplate)) { + const chatTemplate: Record = Object.create(null); + for (const { name, template } of this.chatTemplate) { + if (typeof name !== 'string' || typeof template !== 'string') { + throw new Error( + 'Chat template must be a list of objects with "name" and "template" properties' + ); + } + chatTemplate[name] = template; + } + this.chatTemplate = chatTemplate; + } + this.compiledTemplateCache = new Map(); + } + getToken(...keys: string[]): string | null { + for (const key of keys) { + const item = this.config[key]; + if (!item) continue; + if (typeof item === 'object') { + const maybe = item as { type?: string; content?: string }; + if (maybe.type === 'AddedToken' && typeof maybe.content === 'string') { + return maybe.content; + } + throw Error(`Unknown token: ${String(item)}`); + } else { + return item as string; + } + } + return null; + } + + static async fromPretrained(tokenizerName: string): Promise { + const info = await loadTokenizer(tokenizerName); + return new this(...info); + } + + /** + * Encode/tokenize the given text(s). + * @param text The text to tokenize. + * @param options An optional object containing the following properties: + * @param options.textPair A second sequence to be encoded with the first. + * @param options.padding Whether to pad the input sequences. + * @param options.addSpecialTokens Whether or not to add the special tokens associated with the corresponding model. + * @param options.truncation Whether to truncate the input sequences. + * @param options.maxLength Maximum length of the returned list and optionally padding length. + * @param options.returnTensor Whether to return the results as Tensors or arrays. + * @param options.returnTokenTypeIds Whether to return the token type ids. + * @returns Object to be passed to the model. + */ + protected call( + text: string | string[], + { + textPair = null, + addSpecialTokens = true, + padding = false, + truncation = null, + maxLength = null, + returnTensor = true, + returnTokenTypeIds = null + }: { + textPair?: string | null; + addSpecialTokens?: boolean; + padding?: boolean | 'max_length'; + truncation?: boolean | null; + maxLength?: number | null; + returnTensor?: boolean; + returnTokenTypeIds?: boolean | null; + } = {} + ): BatchEncoding { + const isBatched = Array.isArray(text); + + let encodedTokens; + + if (isBatched) { + if (text.length === 0) { + throw Error('text array must be non-empty'); + } + encodedTokens = text.map((x) => + this.encodePlus(x, { + addSpecialTokens: addSpecialTokens, + returnTokenTypeIds: returnTokenTypeIds + }) + ); + } else { + if (text === null || text === undefined) { + throw Error('text may not be null or undefined'); + } + + if (Array.isArray(textPair)) { + throw Error( + 'When specifying `textPair`, since `text` is a string, `textPair` must also be a string (i.e., not an array).' + ); + } + + // For single input, we just wrap in an array, and then unwrap later. + encodedTokens = [ + this.encodePlus(text, { + addSpecialTokens: addSpecialTokens, + returnTokenTypeIds: returnTokenTypeIds + }) + ]; + } + // At this point, `encodedTokens` is batched, of shape [batchSize, tokens]. + // However, array may be jagged. So, we may need pad to maxLength. + if (maxLength === null) { + maxLength = this.modelMaxLength; + } else if (truncation === null) { + if (padding === true) { + console.warn( + '`maxLength` is ignored when `padding: true` and there is no truncation strategy. ' + + "To pad to max length, use `padding: 'maxLength'`." + ); + maxLength = this.modelMaxLength; + } else if (padding === false) { + console.warn( + 'Truncation was not explicitly activated but `maxLength` is provided a specific value, please use `truncation: true` to explicitly truncate examples to max length.' + ); + truncation = true; + } + } + + // padding: 'maxLength' doesn't require any additional calculation + // but padding: true has to calculate maxLength from the sequences + if (padding === true) { + maxLength = Math.min( + max(encodedTokens.map((x) => x.inputIds.length))[0], + maxLength ?? Infinity + ); + } + + // Ensure it is less than model max length + maxLength = Math.min(maxLength, this.modelMaxLength ?? Infinity); + + if (padding || truncation) { + // Perform padding and/or truncation + for (let i = 0; i < encodedTokens.length; ++i) { + if (encodedTokens[i].inputIds.length === maxLength) { + continue; + } else if (encodedTokens[i].inputIds.length > maxLength) { + // possibly truncate + if (truncation) { + truncateHelper(encodedTokens[i], maxLength); + } + } else { + // t.length < maxLength + // possibly pad + if (padding) { + padHelper( + encodedTokens[i], + maxLength, + (key) => (key === 'inputIds' ? this.padTokenId : 0), + this.paddingSide + ); + } + } + } + } + + const result: Record = {}; + + if (returnTensor) { + if (!(padding && truncation)) { + // Not, guaranteed that all items have same length, so + // we perform additional check + + if ( + encodedTokens.some((x) => { + for (const key of Object.keys(x)) { + if ( + (x as Record)[key].length !== + (encodedTokens[0] as Record)[key]?.length + ) { + return true; + } + } + return false; + }) + ) { + throw Error( + 'Unable to create tensor, you should probably activate truncation and/or padding ' + + "with 'padding=true' and 'truncation=true' to have batched tensors with the same length." + ); + } + } + + // Now we actually convert to tensor + // NOTE: In the same way as the python library, we return a batched tensor, regardless of + // whether we have a single input or multiple inputs. + const dims = [encodedTokens.length, encodedTokens[0].inputIds.length]; + + for (const key of Object.keys(encodedTokens[0])) { + result[key] = tensor( + Int32Array.from( + encodedTokens + .flatMap( + (x) => + (x as Record)[key] as (bigint | boolean | number | string)[] + ) + .map(Number) + ), + { shape: dims, dtype: int32 } + ); + } + } else { + for (const key of Object.keys(encodedTokens[0])) { + result[key] = encodedTokens.map((x) => (x as Record)[key]); + } + + // If not returning a tensor, we match the input type + if (!isBatched) { + // Input was not batched, so we unwrap + for (const key of Object.keys(result)) { + result[key] = (result[key] as unknown[])[0]; + } + } + } + + return result as unknown as BatchEncoding; + } + + /** + * Encodes a single text using the preprocessor pipeline of the tokenizer. + * + * @param {string|null} text The text to encode. + * @returns {string[]|null} The encoded tokens. + */ + private encodeText(text: string | null): string[] | null { + if (text === null) return null; + + // Actual function which does encoding, for a single text + // First, we take care of special tokens. Needed to avoid issues arising from + // normalization and/or pretokenization (which may not preserve special tokens) + const sections = this.addedTokensSplitter.split(text); + + // Process left/right stripping of added tokens + for (let i = 0; i < sections.length; ++i) { + const addedToken = this.addedTokensMap.get(sections[i]); + if (addedToken) { + if (addedToken.lstrip && i > 0) { + sections[i - 1] = sections[i - 1].trimEnd(); + } + if (addedToken.rstrip && i < sections.length - 1) { + sections[i + 1] = sections[i + 1].trimStart(); + } + } + } + + const tokens = sections.flatMap((x, sectionIndex) => { + if (x.length === 0) return []; + if (this.addedTokensMap.has(x)) return [x]; // Return added tokens unchanged + + if (this.removeSpace === true) { + x = x.trim().split(/\s+/).join(' '); + } + + if (this.normalizer !== null) { + x = this.normalizer(x); + } + + // If, after normalization, this section is empty (e.g., trimming whitespace), + // we return an empty array + if (x.length === 0) { + return []; + } + + const sectionTokens = + this.preTokenizer !== null + ? this.preTokenizer(x, { + sectionIndex: sectionIndex + }) + : [x]; + + const tokens = this.model(sectionTokens); + + return tokens; + }); + + return tokens; + } + + /** + * Encodes a single text or a pair of texts using the model's tokenizer. + * + * @param text The text to encode. + * @param options An optional object containing the following properties: + * @param options.textPair The optional second text to encode. + * @param options.addSpecialTokens Whether or not to add the special tokens associated with the corresponding model. + * @param options.returnTokenTypeIds Whether to return tokenTypeIds. + * @returns An object containing the encoded text. + */ + private encodePlus( + text: string, + { + textPair = null, + addSpecialTokens = true, + returnTokenTypeIds = null + }: { + textPair?: string | null; + addSpecialTokens?: boolean; + returnTokenTypeIds?: boolean | null; + } = {} + ) { + const { tokens, tokenTypeIds } = this.tokenizeHelper(text, { + pair: textPair, + addSpecialTokens + }); + + const inputIds = this.model.convertTokensToIds(tokens); + + const result = { + inputIds: inputIds, + attentionMask: new Array(inputIds.length).fill(1) + }; + if ((returnTokenTypeIds ?? this.returnTokenTypeIds) && tokenTypeIds) { + (result as { tokenTypeIds?: number[] }).tokenTypeIds = tokenTypeIds; + } + return result; + } + + /** + * Internal helper function to tokenize a text, and optionally a pair of texts. + * @param text The text to tokenize. + * @param options An optional object containing the following properties: + * @param options.pair The optional second text to tokenize. + * @param options.addSpecialTokens Whether or not to add the special tokens associated with the corresponding model. + * @returns An object containing the tokens and optionally the token type IDs. + */ + private tokenizeHelper( + text: string, + { + pair = null, + addSpecialTokens = false + }: { pair?: string | null; addSpecialTokens?: boolean } = {} + ) { + const tokens = this.encodeText(text); + const tokens2 = this.encodeText(pair); + + return this.postProcessor + ? this.postProcessor(tokens ?? [], tokens2 ?? null, { addSpecialTokens }) + : { tokens: mergeArrays(tokens ?? [], tokens2 ?? []) }; + } + + /** + * Converts a string into a sequence of tokens. + * @param text The sequence to be encoded. + * @param options An optional object containing the following properties: + * @param options.pair A second sequence to be encoded with the first. + * @param options.addSpecialTokens Whether or not to add the special tokens associated with the corresponding model. + * @returns The list of tokens. + */ + tokenize(text: string, { pair = null, addSpecialTokens = false } = {}) { + return this.tokenizeHelper(text, { pair, addSpecialTokens }).tokens; + } + + /** + * Encodes a single text or a pair of texts using the model's tokenizer. + * + * @param text The text to encode. + * @param options An optional object containing the following properties: + * @param options.addSpecialTokens Whether or not to add the special tokens associated with the corresponding model. + * @param options.returnTokenTypeIds Whether to return tokenTypeIds. + * @returns An array of token IDs representing the encoded text(s). + */ + encode(text: string, { addSpecialTokens = true, returnTokenTypeIds = null } = {}) { + return this.encodePlus(text, { + addSpecialTokens, + returnTokenTypeIds + }).inputIds; + } + + /** + * Decode a batch of tokenized sequences. + * @param batch List of tokenized input sequences. + * @param decodeArgs (Optional) Object with decoding arguments. + * @returns List of decoded sequences. + */ + batchDecode(batch: number[][], decodeArgs: DecodeArgs = {}) { + return batch.map((x) => this.decode(x, decodeArgs)); + } + + /** + * Decodes a sequence of token IDs back to a string. + * + * @param tokenIds List of token IDs to decode. + * @param decodeArgs (Optional) Object with decoding arguments. + * + * @returns The decoded string. + * @throws If `tokenIds` is not a non-empty array of integers. + */ + decode(tokenIds: number[], decodeArgs: DecodeArgs = {}) { + if ( + !Array.isArray(tokenIds) || + tokenIds.length === 0 || + !(Number.isInteger(tokenIds[0]) || typeof tokenIds[0] === 'bigint') + ) { + throw Error('tokenIds must be a non-empty array of integers.'); + } + + return this.decodeSingle(tokenIds, decodeArgs); + } + + /** + * Decode a single list of token ids to a string. + * @param tokenIds List of token ids to decode + * @param decodeArgs Optional arguments for decoding + * @param [decodeArgs.skipSpecialTokens=false] Whether to skip special tokens during decoding + * @param [decodeArgs.cleanUpTokenizationSpaces=null] Whether to clean up tokenization spaces + * during decoding. If null, the value is set to `this.decoder.cleanup` if it exists, falling + * back to `this.cleanUpTokenizationSpaces` if it exists, falling back to `true`. + * @returns The decoded string + */ + decodeSingle(tokenIds: number[], { skipSpecialTokens = false }: DecodeArgs = {}) { + let tokens = this.model.convertIdsToTokens(tokenIds); + if (skipSpecialTokens) { + tokens = tokens.filter((x) => !this.specialTokens.includes(x)); + } + + // If `this.decoder` is null, we just join tokens with a space: + // https://github.com/huggingface/tokenizers/blob/8edec536a737cb04494b454805be16c020abb14f/tokenizers/src/tokenizer/mod.rs#L835 + let decoded = this.decoder ? this.decoder(tokens) : tokens.join(' '); + + // Slight hack, but prevents having to pass `skipSpecialTokens` to each call to `decode`, which + // would lead to code duplication. + if (this.decoder && 'endOfWordSuffix' in this.decoder && this.decoder.endOfWordSuffix) { + decoded = decoded.replaceAll(this.decoder.endOfWordSuffix, ' '); + if (skipSpecialTokens) { + decoded = decoded.trim(); + } + } + + return decoded; + } + + /** + * Retrieve the chat template string used for tokenizing chat messages. This template is used + * internally by the `applyChatTemplate` method and can also be used externally to retrieve the + * model's chat template for better generation tracking. + * + * @param options An optional object containing the following properties: + * @param options.chatTemplate A Jinja template or the name of a template to use for this + * conversion. It is usually not necessary to pass anything to this argument, as the model's + * template will be used by default. + * @param options.tools A list of tools (callable functions) that will be accessible to the model. + * If the template does not support function calling, this argument will have no effect. Each + * tool should be passed as a JSON Schema, giving the name, description and argument types for + * the tool. See our + * [chat templating guide](https://huggingface.co/docs/transformers/main/en/chat_templating#automated-function-conversion-for-tool-use) + * for more information. + * @returns The chat template string. + */ + getChatTemplate({ + chatTemplate = null, + tools = null + }: { chatTemplate?: string | null; tools?: string[] | null } = {}): string { + // First, handle the cases when the model has a dict of multiple templates + if (this.chatTemplate && typeof this.chatTemplate === 'object') { + const templateDict = this.chatTemplate; + + if (chatTemplate !== null && Object.hasOwn(templateDict, chatTemplate)) { + // The user can pass the name of a template to the chat template argument instead of an + // entire template + chatTemplate = (templateDict as Record)[chatTemplate]; + } else if (chatTemplate === null) { + if (tools !== null && 'toolUse' in templateDict) { + chatTemplate = templateDict['toolUse']; + } else if ('default' in templateDict) { + chatTemplate = templateDict['default']; + } else { + throw Error( + `This model has multiple chat templates with no default specified! Please either pass` + + ` a chat template or the name of the template you wish to use to the 'chatTemplate'` + + ` argument. Available template names are ${Object.keys(templateDict).sort()}.` + ); + } + } + } else if (chatTemplate === null) { + // These are the cases when the model has a single template + // priority: `chatTemplate` argument > `tokenizer.chatTemplate` + if (this.chatTemplate) { + chatTemplate = this.chatTemplate; + } else { + throw Error( + 'Cannot use applyChatTemplate() because tokenizer.chatTemplate is not set and no template ' + + 'argument was passed! For information about writing templates and setting the ' + + 'tokenizer.chatTemplate attribute, please see the documentation at ' + + 'https://huggingface.co/docs/transformers/main/en/chat_templating' + ); + } + } + return chatTemplate; + } + + /** + * Converts a list of message objects with `"role"` and `"content"` keys to a list of token + * ids. This method is intended for use with chat models, and will read the tokenizer's chat_template attribute to + * determine the format and control tokens to use when converting. + * + * See [here](https://huggingface.co/docs/transformers/chat_templating) for more information. + * + * @param conversation A list of message objects with `"role"` and `"content"` keys, + * representing the chat history so far. + * @param options An optional object containing the following properties: + * @param options.chatTemplate A Jinja template to use for this conversion. If + * this is not passed, the model's chat template will be used instead. + * @param options.tools A list of tools (callable functions) that will be accessible to the model. + * If the template does not support function calling, this argument will have no effect. Each + * tool should be passed as a JSON Schema, giving the name, description and argument types for + * the tool. See our + * [chat templating guide](https://huggingface.co/docs/transformers/main/en/chat_templating#automated-function-conversion-for-tool-use) + * for more information. + * @param options.documents A list of dicts representing documents that will be accessible to the model if it is performing RAG + * (retrieval-augmented generation). If the template does not support RAG, this argument will have no + * effect. We recommend that each document should be a dict containing "title" and "text" keys. Please + * see the RAG section of the [chat templating guide](https://huggingface.co/docs/transformers/main/en/chat_templating#arguments-for-RAG) + * for examples of passing documents with chat templates. + * @param options.addGenerationPrompt Whether to end the prompt with the token(s) that indicate + * the start of an assistant message. This is useful when you want to generate a response from the + * model. Note that this argument will be passed to the chat template, and so it must be supported + * in the template for this argument to have any effect. + * @param options.tokenize Whether to tokenize the output. If false, the output will be a string. + * @param options.padding Whether to pad sequences to the maximum length. Has no effect if tokenize is false. + * @param options.truncation Whether to truncate sequences to the maximum length. Has no effect if tokenize is false. + * @param options.maxLength Maximum length (in tokens) to use for padding or truncation. Has no effect if tokenize is false. + * If not specified, the tokenizer's `max_length` attribute will be used as a default. + * @param options.returnTensor Whether to return the output as a Tensor or an Array. Has no effect if tokenize is false. + * @param options.returnDict Whether to return a dictionary with named outputs. Has no effect if tokenize is false. + * @param options.tokenizerKwargs Additional options to pass to the tokenizer. + * @returns The tokenized output. + */ + applyChatTemplate( + conversation: Message[], + { + tools = null, + documents = null, + chatTemplate = null, + addGenerationPrompt = false, + tokenize = true, + padding = false, + truncation = false, + maxLength = null, + returnTensor = true, + returnDict = false, + tokenizerKwargs = {}, + ...kwargs + }: { + tools?: string[] | null; + documents?: string[] | null; + chatTemplate?: string | null; + addGenerationPrompt?: boolean; + tokenize?: boolean; + padding?: boolean; + truncation?: boolean; + maxLength?: number | null; + returnTensor?: boolean; + returnDict?: boolean; + tokenizerKwargs?: Record; + } = {} + ) { + chatTemplate = this.getChatTemplate({ chatTemplate, tools }); + + if (typeof chatTemplate !== 'string') { + throw Error(`chat_template must be a string, but got ${typeof chatTemplate}`); + } + + // Compilation function uses a cache to avoid recompiling the same template + let compiledTemplate = this.compiledTemplateCache.get(chatTemplate); + if (compiledTemplate === undefined) { + compiledTemplate = new Template(chatTemplate); + this.compiledTemplateCache.set(chatTemplate, compiledTemplate); + } + + const specialTokensMap = Object.create(null); + for (const key of SPECIAL_TOKEN_ATTRIBUTES) { + const value = this.getToken(key); + if (value) { + specialTokensMap[key] = value; + } + } + + const rendered = compiledTemplate.render({ + messages: conversation, + addGenerationPrompt, + tools, + documents, + ...specialTokensMap, + ...kwargs + }); + + if (tokenize) { + const out = this.call(rendered, { + addSpecialTokens: false, + padding, + truncation, + maxLength, + returnTensor, + ...tokenizerKwargs + }); + return returnDict ? out : out.inputIds; + } + + return rendered; + } +} + +export function max(arr: T) { + if (arr.length === 0) throw Error('Array must not be empty'); + let max = arr[0]; + let indexOfMax = 0; + for (let i = 1; i < arr.length; ++i) { + if (arr[i] > max) { + max = arr[i]; + indexOfMax = i; + } + } + return [max, indexOfMax] as T extends bigint[] ? [bigint, number] : [number, number]; +} + +function mergeArrays(...arrs: T[]): T { + return Array.prototype.concat.apply([], arrs) as T; +} + +type TrieNode = { + /** + * If this node marks the end of a word, this property will + * contain the complete word. Otherwise, it's undefined. + */ + end?: string; + + /** + * An index signature to represent child nodes. Each key is a + * character, and each value is the next TrieNode in the sequence. + * The value is a union to satisfy TypeScript's index signature rules. + */ + [key: string]: TrieNode | string | undefined; +}; + +/** + * A data structure which uses a trie to split a string into tokens based on a dictionary. + * It can also use a regular expression to preprocess the input text before splitting. + * + * NOTE: To ensure multi-byte characters are handled correctly, we operate at byte-level instead of character-level. + */ +class DictionarySplitter { + trie: TrieNode; + /** + * @param dictionary The dictionary of words to use for splitting. + */ + constructor(dictionary: string[]) { + this.trie = this.buildTrie(dictionary); + } + + /** + * Builds a trie from the given dictionary. + * @param dictionary The dictionary of words to build the trie from. + * @returns The root node of the trie. + */ + private buildTrie(dictionary: string[]) { + const trie: TrieNode = Object.create(null); + for (const word of dictionary) { + let node = trie; + for (let i = 0; i < word.length; ++i) { + node = (node[word[i]] ??= Object.create(null)) as TrieNode; + } + node.end = word; + } + return trie; + } + + /** + * Splits the input text into tokens based on the dictionary. + * @param {string} text The input text to split. + * @returns {string[]} An array of tokens. + */ + split(text: string): string[] { + const result = []; + const n = text.length; + let start = 0; + let i = 0; + + while (i < n) { + let node = this.trie; + let match = null; + let j = i; + + while (j < n && (node = node[text[j]] as TrieNode)) { + if (node.end) { + // Always keep the last (i.e., longest) match. + match = node.end; + } + ++j; + } + + if (match) { + if (i > start) { + result.push(text.slice(start, i)); + } + result.push(match); + i += match.length; + start = i; + } else { + ++i; + } + } + if (start < n) { + result.push(text.slice(start)); + } + return result; + } +} + +/** + * Efficient Heap-based Implementation of a Priority Queue. + * It uses an array-based binary heap, where the root is at index `0`, and the + * children of node `i` are located at indices `2i + 1` and `2i + 2`, respectively. + * + * Adapted from the following sources: + * - https://stackoverflow.com/a/42919752/13989043 (original) + * - https://github.com/belladoreai/llama-tokenizer-js (minor improvements) + */ +class PriorityQueue { + private heap: T[]; + private comparator: (a: T, b: T) => boolean; + private maxSize: number; + /** + * Create a new PriorityQueue. + * @param comparator Comparator function to determine priority. Defaults to a MaxHeap. + */ + constructor(comparator = (a: T, b: T) => a > b, maxSize = Infinity) { + this.heap = []; + this.comparator = comparator; + this.maxSize = maxSize; + } + + /** + * The size of the queue + */ + get size() { + return this.heap.length; + } + + /** + * Check if the queue is empty. + * @returns `true` if the queue is empty, `false` otherwise. + */ + isEmpty() { + return this.size === 0; + } + + /** + * Return the element with the highest priority in the queue. + * @returns The highest priority element in the queue. + */ + peek() { + return this.heap[0]; + } + + /** + * Add one or more elements to the queue. + * @param values The values to push into the queue. + * @returns The new size of the queue. + */ + push(...values: T[]) { + return this.extend(values); + } + + /** + * Add multiple elements to the queue. + * @param values The values to push into the queue. + * @returns The new size of the queue. + */ + extend(values: T[]) { + for (const value of values) { + if (this.size < this.maxSize) { + this.heap.push(value); + this.siftUp(); + } else { + // Get index of value with the lowest priority + const smallest = this.smallest(); + + // If the new value has higher priority than the smallest value in the heap + // then replace the smallest value with the new value and update the heap + if (this.comparator(value, this.heap[smallest])) { + this.heap[smallest] = value; + this.siftUpFrom(smallest); + } + } + } + return this.size; + } + + /** + * Remove and return the element with the highest priority in the queue. + * @returns The element with the highest priority in the queue. + */ + pop() { + const poppedValue = this.peek(); + const bottom = this.size - 1; + if (bottom > 0) { + this.swap(0, bottom); + } + this.heap.pop(); + this.siftDown(); + return poppedValue; + } + + /** + * Replace the element with the highest priority in the queue with a new value. + * @param value The new value. + * @returns The replaced value. + */ + replace(value: T) { + const replacedValue = this.peek(); + this.heap[0] = value; + this.siftDown(); + return replacedValue; + } + + /** + * Compute the index for the parent of the node at index `i`. + * @param i The index of the node to get the parent of. + * @returns The index of the parent node. + */ + private parent(i: number) { + return ((i + 1) >>> 1) - 1; + } + + /** + * Compute the index for the left child of the node at index `i`. + * @param i The index of the node to get the left child of. + * @returns The index of the left child. + * + */ + private left(i: number) { + return (i << 1) + 1; + } + + /** + * Compute the index for the right child of the node at index `i`. + * @param i The index of the node to get the right child of. + * @returns The index of the right child. + */ + private right(i: number) { + return (i + 1) << 1; + } + + /** + * Check if the element at index `i` is greater than the element at index `j`. + * @param i The index of the first element to compare. + * @param j The index of the second element to compare. + * @returns `true` if the element at index `i` is greater than the element at index `j`, `false` otherwise. + * + */ + private greater(i: number, j: number) { + return this.comparator(this.heap[i], this.heap[j]); + } + + /** + * Swap the elements at indices `i` and `j`. + * @param i The index of the first element to swap. + * @param j The index of the second element to swap. + * + */ + private swap(i: number, j: number) { + const temp = this.heap[i]; + this.heap[i] = this.heap[j]; + this.heap[j] = temp; + } + + /** + * Maintain the heap property by updating positions in the heap, + * starting at the last element and moving up the heap. + */ + private siftUp() { + this.siftUpFrom(this.size - 1); + } + + /** + * Helper function to sift up from a given node. + * @param node The index of the node to start sifting up from. + */ + private siftUpFrom(node: number) { + while (node > 0 && this.greater(node, this.parent(node))) { + this.swap(node, this.parent(node)); + node = this.parent(node); + } + } + + /** + * Maintain the heap property by updating positions in the heap, + * starting at the first element and moving down the heap. + */ + private siftDown() { + let node = 0; + while ( + (this.left(node) < this.size && this.greater(this.left(node), node)) || + (this.right(node) < this.size && this.greater(this.right(node), node)) + ) { + const maxChild = + this.right(node) < this.size && this.greater(this.right(node), this.left(node)) + ? this.right(node) + : this.left(node); + this.swap(node, maxChild); + node = maxChild; + } + } + + /** + * Get the index of the smallest element in the heap. Since we use an array-based heap, + * the index can be computed without needing to traverse the heap. + */ + private smallest(): number { + return 2 ** Math.floor(Math.log2(this.size)) - 1; + } +} + +/** + * A simple Least Recently Used (LRU) cache implementation in JavaScript. + * This cache stores key-value pairs and evicts the least recently used item + * when the capacity is exceeded. + */ +class LRUCache { + capacity: number; + cache: Map; + /** + * Creates an LRUCache instance. + * @param capacity The maximum number of items the cache can hold. + */ + constructor(capacity: number) { + this.capacity = capacity; + this.cache = new Map(); + } + + /** + * Retrieves the value associated with the given key and marks the key as recently used. + * @param key The key to retrieve. + * @returns The value associated with the key, or undefined if the key does not exist. + */ + get(key: Key) { + if (!this.cache.has(key)) return undefined; + const value = this.cache.get(key); + this.cache.delete(key); + this.cache.set(key, value as Value); + return value; + } + + /** + * Inserts or updates the key-value pair in the cache. + * If the key already exists, it is updated and marked as recently used. + * If the cache exceeds its capacity, the least recently used item is evicted. + * @param key The key to add or update. + * @param value The value to associate with the key. + */ + put(key: Key, value: Value) { + if (this.cache.has(key)) { + this.cache.delete(key); + } + this.cache.set(key, value); + if (this.cache.size > this.capacity) { + this.cache.delete(this.cache.keys().next().value as Key); + } + } + + /** + * Clears the cache. + */ + clear() { + this.cache.clear(); + } +} + +export function decodeSingle( + value: number, + tokenizer: PreTrainedTokenizer | ToyTokenizer | null +): string { + if (tokenizer instanceof PreTrainedTokenizer) { + return tokenizer + .decodeSingle([value]) + .replaceAll('<|end_of_text|>', '▶️📄') + .replaceAll('<|im_start|>', '▶️💬') + .replaceAll('<|im_end|>', '⏹️💬'); + } + return tokenizer?.ids?.[value] || `<${value}>`; +} diff --git a/examples/piston-train-toy/src/lib/train/types.ts b/examples/piston-train-toy/src/lib/train/types.ts new file mode 100644 index 00000000..468259c4 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/types.ts @@ -0,0 +1,90 @@ +import type { DataLoader } from '@piston-ml/piston-web'; + +import type { + NaturalLanguageAutoregressiveBatch, + NaturalLanguageBidirectionalBatch, + NaturalLanguageDataset +} from './data/natural'; +import type { + ToyAutoregressiveBatch, + ToyBidirectionalBatch, + ToyDatasetLike, + ToyEncoderDecoderBatch, + ToySequence +} from './data/toy/dataset'; +import type { RNNDecoder, RNNEncoder, RNNEncoderDecoder } from './model/rnn'; +import type { + DecoderTransformer, + EncoderDecoderTransformer, + EncoderTransformer +} from './model/transformer'; + +type CollateFn = (batch: B) => T; + +type ToyCollateInput = ToySequence[]; +type NaturalCollateInput = number[][]; + +export type NaturalBatchType = + | NaturalLanguageBidirectionalBatch + | NaturalLanguageAutoregressiveBatch; + +export type ToyBatchType = + | ToyBidirectionalBatch + | ToyEncoderDecoderBatch + | ToyAutoregressiveBatch; + +export type EncoderDecoderBatchType = ToyEncoderDecoderBatch; +export type BidirectionalBatchType = + | ToyBidirectionalBatch + | NaturalLanguageBidirectionalBatch; +export type AutoregressiveBatchType = + | ToyAutoregressiveBatch + | NaturalLanguageAutoregressiveBatch; + +export type ToyAutoregressiveCollateFnType = CollateFn< + ToyCollateInput, + ToyAutoregressiveBatch +>; +export type ToyBidirectionalCollateFnType = CollateFn>; +export type ToyEncoderDecoderCollateFnType = CollateFn< + ToyCollateInput, + ToyEncoderDecoderBatch +>; + +export type NaturalAutoregressiveCollateFnType = CollateFn< + NaturalCollateInput, + NaturalLanguageAutoregressiveBatch +>; +export type NaturalBidirectionalCollateFnType = CollateFn< + NaturalCollateInput, + NaturalLanguageBidirectionalBatch +>; + +export type AutoregressiveCollateFnType = + | ToyAutoregressiveCollateFnType + | NaturalAutoregressiveCollateFnType; + +export type BidirectionalCollateFnType = + | ToyBidirectionalCollateFnType + | NaturalBidirectionalCollateFnType; + +export type EncoderDecoderCollateFnType = CollateFn>; + +export type NaturalCollateFnType = CollateFn>; +export type ToyCollateFnType = CollateFn>; +export type PistonCollateFnType = NaturalCollateFnType | ToyCollateFnType; + +export type NaturalDataloaderType = DataLoader>; +export type ToyDataloaderType = DataLoader>; +export type PistonDataloaderType = ToyDataloaderType | NaturalDataloaderType; + +export type AutoregressiveModelType = DecoderTransformer | RNNDecoder; +export type BidirectionalModelType = EncoderTransformer | RNNEncoder; +export type EncoderDecoderModelType = EncoderDecoderTransformer | RNNEncoderDecoder; + +export type GeneratableModel = + | AutoregressiveModelType + | BidirectionalModelType + | EncoderDecoderModelType; + +export type PistonDatasetType = ToyDatasetLike | NaturalLanguageDataset; diff --git a/examples/piston-train-toy/src/lib/train/utils/checkpoint.ts b/examples/piston-train-toy/src/lib/train/utils/checkpoint.ts new file mode 100644 index 00000000..afc2538d --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/utils/checkpoint.ts @@ -0,0 +1,224 @@ +import type { Config } from '$lib/workspace/config'; + +import * as piston from '@piston-ml/piston-web'; +import { + type ConstantConfig, + type CosineAnnealingConfig, + type ExponentialConfig, + type LinearConfig, + LRScheduler, + Optimizer, + type OptimizerParamState, + type ParamGroupConfig, + type SchedulerStateDict, + type StateDict, + type StepConfig, + Tensor +} from '@piston-ml/piston-web'; + +/** + * Recursively walks an object and extracts any Tensor values into `out` as Buffers. + * Replaces extracted tensors in the returned structure with a small marker object + * containing the tensor storage key that was used in `out`. + */ +export function splitTensorsFromObject( + value: unknown, + baseKey: string, + out: Record +): unknown { + if (value instanceof Tensor) { + out[baseKey] = new piston.Buffer(value, true); + return { __tensor__: baseKey }; + } + if (Array.isArray(value)) { + return value.map((v, i) => splitTensorsFromObject(v, `${baseKey}.${i}`, out)); + } + if (value && typeof value === 'object') { + const result: Record = {}; + for (const [k, v] of Object.entries(value)) { + result[k] = splitTensorsFromObject(v, `${baseKey}.${k}`, out); + } + return result; + } + return value; +} + +export type AnySchedulerState = SchedulerStateDict< + StepConfig | CosineAnnealingConfig | ExponentialConfig | LinearConfig | ConstantConfig | unknown +>; + +export type CheckpointOptimizerExtra = { + name: string; + // JSON with __tensor__ markers + state: unknown; + paramGroups: ParamGroupConfig[]; +} | null; + +export interface CheckpointDataState { + blockSize?: number | { source: number; target: number }; + toy?: { cursor: number; baseSeed?: number; datasetName?: string } | null; + natural?: { shardIndex: number; cursor: number } | null; +} + +export interface CheckpointExtra { + config: Config; + optimizer: CheckpointOptimizerExtra; + numSteps: number; + lrScheduler?: { state: AnySchedulerState }; + dataState?: CheckpointDataState; + // Optional wall-clock training start time in ms to persist across restarts + startTimeMs?: number; +} + +/** + * Builds a checkpoint payload by combining model parameters with optimizer state. + * - Model parameters/buffers go into `tensors` directly + * - Any Tensor values found inside optimizer.stateDict().state are lifted into `tensors` + * under keys prefixed with `optimizer/state/...` + * - Extra contains { config, optimizer, numSteps } + */ + +export function buildCheckpoint( + model: piston.Module, + optimizer: Optimizer, + numSteps: number, + configForExtra: Config, + scheduler?: LRScheduler, + dataState?: CheckpointDataState, + startTimeMs?: number +): { tensors: Record; extra: CheckpointExtra } { + const tensors: Record = model.stateDict(); + + let optimizerExtra: CheckpointOptimizerExtra = null; + try { + const name = optimizer.constructor.name ?? 'Optimizer'; + const packed = optimizer.stateDict(); + const tensorSlots: Record = {}; + const jsonState = splitTensorsFromObject(packed.state, 'optimizer.state', tensorSlots); + Object.assign(tensors, tensorSlots); + optimizerExtra = { + name, + state: jsonState, + paramGroups: packed.paramGroups + }; + } catch (e) { + console.warn('Failed to pack optimizer stateDict for checkpoint extra:', e); + } + + const extra: CheckpointExtra = { + config: configForExtra, + optimizer: optimizerExtra, + numSteps, + lrScheduler: scheduler ? { state: scheduler.stateDict() } : undefined, + dataState, + startTimeMs + }; + + return { tensors, extra }; +} + +/** + * Replace any marker objects of the form { __tensor__: key } inside a JSON structure + * with actual Tensors from the provided mapping. + */ +export function rehydrateTensorsInObject(value: unknown, lifted: Record): T { + if (value && typeof value === 'object' && !Array.isArray(value)) { + const marker = value as { __tensor__?: string }; + if (typeof marker.__tensor__ === 'string') { + const key = marker.__tensor__; + if (!(key in lifted)) { + throw new Error(`Missing lifted tensor for key '${key}' during optimizer rehydration`); + } + return lifted[key] as unknown as T; + } + const out: Record = {}; + for (const [k, v] of Object.entries(value)) { + out[k] = rehydrateTensorsInObject(v, lifted); + } + return out as unknown as T; + } + if (Array.isArray(value)) { + return value.map((v) => rehydrateTensorsInObject(v, lifted)) as unknown as T; + } + return value as T; +} + +export interface SplitLoadedStateResult { + modelState: Record; + schedulerState?: AnySchedulerState; + optimizerState: StateDict; + numSteps: number; + config: Config; + dataState?: CheckpointDataState; + startTimeMs?: number; +} + +/** + * Given loaded state from piston.load, split out model state from lifted optimizer tensors + * and rehydrate optimizer and scheduler states from extras. + */ +export function splitLoadedState(loaded: { + state: Record; + extra?: CheckpointExtra; +}): SplitLoadedStateResult { + const prefix = 'optimizer.state'; + const liftedOptimizerTensors: Record = {}; + const modelState: Record = {}; + + for (const [key, t] of Object.entries(loaded.state)) { + if (key.startsWith(prefix)) { + liftedOptimizerTensors[key] = t; + } else { + modelState[key] = t; + } + } + + let optimizerState: StateDict | undefined; + let schedulerState: AnySchedulerState | undefined = undefined; + let numSteps = 0; + let config: Config | null = null; + let dataState: CheckpointDataState | undefined = undefined; + let startTimeMs: number | undefined = undefined; + + const { extra } = loaded; + + if (extra) { + config = extra.config; + numSteps = extra.numSteps; + if (extra.optimizer) { + const rehydratedState = rehydrateTensorsInObject>( + extra.optimizer.state, + liftedOptimizerTensors + ); + optimizerState = { + state: rehydratedState, + paramGroups: extra.optimizer.paramGroups ?? [] + }; + } + if (extra.lrScheduler && extra.lrScheduler.state) { + schedulerState = extra.lrScheduler.state; + } + if (extra.dataState) { + dataState = extra.dataState; + } + if (typeof extra.startTimeMs === 'number') { + startTimeMs = extra.startTimeMs; + } + } + + if (!config) { + throw new Error('No config found in checkpoint'); + } + + if (numSteps == null) { + throw new Error('No numSteps found in checkpoint'); + } + + if (!optimizerState) { + throw new Error('No optimizer state found in checkpoint'); + } + + // Some runs don't use a scheduler, so we don't validate that it's present + + return { modelState, optimizerState, schedulerState, numSteps, config, dataState, startTimeMs }; +} diff --git a/examples/piston-train-toy/src/lib/train/utils/init.ts b/examples/piston-train-toy/src/lib/train/utils/init.ts new file mode 100644 index 00000000..93aad0d0 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/utils/init.ts @@ -0,0 +1,333 @@ +import type { + Config, + ProjectionInitializationConfig, + RNNInitializationConfig, + XavierInitializationDistribution +} from '$lib/workspace/config'; + +import { + initConstant_, + initNormal_, + initOnes_, + initOrthogonal_, + initXavierNormal_, + initXavierUniform_, + initZeros_, + LayerNorm, + nn, + Tensor +} from '@piston-ml/piston-web'; +import * as piston from '@piston-ml/piston-web'; + +import { GRUCell, LSTMCell, RNNCell } from '../model/rnn'; + +export function initTransformerParameters(self: nn.Module, config: Config): void { + const initializationConfig = config.model.transformer.initialization; + + if (!initializationConfig.present) { + return; + } + + const isEncoderDecoder = config.model.topology === 'encoder-decoder'; + + const initTransformerWeights = (module: nn.Module): void => { + if (module instanceof nn.Linear) { + initNormal_(module.weight, { mean: 0.0, std: initializationConfig.std }); + if (module.bias != null) { + initZeros_(module.bias); + } + } else if (module instanceof nn.Embedding) { + initNormal_(module.weight, { mean: 0.0, std: initializationConfig.std }); + } else if (module instanceof nn.LayerNorm) { + if (module.bias) { + initZeros_(module.bias); + } + initOnes_(module.weight); + } + }; + + const initProjection = ( + p: nn.Parameter, + projectionConfig: ProjectionInitializationConfig, + pn: string + ) => { + if (!projectionConfig.present) { + return; + } + const nLayers = isEncoderDecoder + ? pn.includes('transformer.encoder') + ? config.model.encoderDecoder.encoderLayers + : config.model.encoderDecoder.decoderLayers + : config.model.layers; + if (projectionConfig.strategy === 'layer-scaled') { + initNormal_(p, { mean: 0.0, std: 0.02 / Math.sqrt(2 * nLayers) }); + } else if (projectionConfig.strategy === 'zero') { + initZeros_(p); + } + }; + + self.apply(initTransformerWeights); + for (const [pn, p] of self.namedParameters()) { + // TODO: Make this respect whatever initialization config we have set up + if (pn.endsWith('cProj.weight')) { + initProjection(p, initializationConfig.projections.attention, pn); + } + if (pn.endsWith('downProj.weight')) { + initProjection(p, initializationConfig.projections.mlp, pn); + } + if (pn.endsWith('lmHead.weight')) { + initProjection(p, initializationConfig.projections.lmHead, pn); + } + } +} + +function initInputPart_( + part: Tensor, + dist: XavierInitializationDistribution, + enable: boolean +): Tensor { + if (!enable) return part; + return dist === 'uniform' ? initXavierUniform_(part) : initXavierNormal_(part); +} + +function initRecurrentParts_(recParts: Tensor[], doOrth: boolean, perGateOrth: boolean): Tensor[] { + if (!doOrth) return recParts; + if (perGateOrth) { + return recParts.map((p) => initOrthogonal_(p)); + } else { + // One tall orthogonal across all gates (works for rectangular via QR) + const tall = piston.cat(recParts, { dim: 0 }); + const tallInit = initOrthogonal_(tall); + return piston.chunk(tallInit, recParts.length, { dim: 0 }) as Tensor[]; + } +} + +function initGatedWeight_( + W: Tensor, // shape: (G*H) x (I+H) + I: number, + H: number, + G: number, + dist: XavierInitializationDistribution, + xavierInput: boolean, + orthRec: boolean, + perGateOrth: boolean +): Tensor { + // Split rows into gate blocks + const gates = piston.chunk(W, G, { dim: 0 }) as Tensor[]; // G × [H x (I+H)] + // For each gate, split columns into [input | recurrent] + const inParts: Tensor[] = []; + const recParts: Tensor[] = []; + for (const g of gates) { + const [inp, rec] = piston.split(g, [I, H], { dim: 1 }) as [Tensor, Tensor]; + inParts.push(inp); + recParts.push(rec); + } + const inInit = inParts.map((p) => initInputPart_(p, dist, xavierInput)); + const recInit = initRecurrentParts_(recParts, orthRec, perGateOrth); + + // Reassemble gates, then full matrix + const gateBlocks = inInit.map((inp, k) => piston.cat([inp, recInit[k]], { dim: 1 })); + return piston.cat(gateBlocks, { dim: 0 }); +} + +function buildParameterHacky(tensor: Tensor): Tensor { + return new nn.Parameter(tensor.debugTensor); +} + +const GRU_GATES_COUNT = 2; +const LSTM_GATES_COUNT = 4; + +export function initGRUCell_(cell: GRUCell, config: RNNInitializationConfig) { + const dist = config.xavierInputColumns.distribution; + + const [wgRows, wgCols] = cell.WGates.weight.size(); + const hiddenWG = wgRows / GRU_GATES_COUNT; + const inputWG = wgCols - hiddenWG; + + // WGates: shape (2H) x (I+H), rows chunked as [r, z] + cell.WGates.weight = buildParameterHacky( + initGatedWeight_( + cell.WGates.weight, + inputWG, + hiddenWG, + GRU_GATES_COUNT, + dist, + config.xavierInputColumns.present, + config.orthogonalRecurrentColumns, + config.perGateOrthogonalBlocks + ) + ); + + const [wcRows, wcCols] = cell.WCandidate.weight.size(); + const hiddenWCand = wcRows; + const inputWCand = wcCols - hiddenWCand; + const [inp, rec] = piston.split(cell.WCandidate.weight, [inputWCand, hiddenWCand], { dim: 1 }); + const inpInit = initInputPart_(inp, dist, config.xavierInputColumns.present); + const recInit = config.orthogonalRecurrentColumns ? initOrthogonal_(rec) : rec; + cell.WCandidate.weight = buildParameterHacky(piston.cat([inpInit, recInit], { dim: 1 })); + + const normGates = cell.normGates; + + // Biases + if (normGates instanceof LayerNorm && normGates.bias) { + // Keep Linear biases zeroed if requested + if (cell.WGates.bias) + cell.WGates.bias = config.zeroBiases + ? buildParameterHacky(initZeros_(cell.WGates.bias)) + : cell.WGates.bias; + if (cell.WCandidate.bias) + cell.WCandidate.bias = config.zeroBiases + ? buildParameterHacky(initZeros_(cell.WCandidate.bias)) + : cell.WCandidate.bias; + + // Apply update-gate bias to LN β on z block; r block to zero + const [beta_r, beta_z] = piston.chunk(normGates.bias, GRU_GATES_COUNT, { dim: 0 }); + const updateGateBias = + config.gru.updateGateBias?.present && config.gru.updateGateBias.value > 0 + ? config.gru.updateGateBias.value + : 0; + normGates.bias = buildParameterHacky( + piston.cat( + [ + initZeros_(beta_r), + updateGateBias !== 0 ? initConstant_(beta_z, updateGateBias) : initZeros_(beta_z) + ], + { dim: 0 } + ) + ); + } else { + // No LN on gates: write into WGates.bias directly (WCandidate bias stays zero if configured) + if (cell.WGates.bias) { + const [b_r, b_z] = piston.chunk(cell.WGates.bias, GRU_GATES_COUNT, { dim: 0 }); + const updateGateBias = + config.gru.updateGateBias?.present && config.gru.updateGateBias.value > 0 + ? config.gru.updateGateBias.value + : 0; + + cell.WGates.bias = buildParameterHacky( + piston.cat( + [ + config.zeroBiases ? initZeros_(b_r) : b_r, + updateGateBias !== 0 + ? initConstant_(b_z, updateGateBias) + : config.zeroBiases + ? initZeros_(b_z) + : b_z + ], + { dim: 0 } + ) + ); + } + if (cell.WCandidate.bias) { + cell.WCandidate.bias = config.zeroBiases + ? buildParameterHacky(initZeros_(cell.WCandidate.bias)) + : cell.WCandidate.bias; + } + } +} + +export function initLSTMCell_(cell: LSTMCell, config: RNNInitializationConfig) { + const [wRows, wCols] = cell.W.weight.size(); + const inferredHidden = wRows / LSTM_GATES_COUNT; + const inferredInput = wCols - inferredHidden; + + cell.W.weight = buildParameterHacky( + initGatedWeight_( + cell.W.weight, + inferredInput, + inferredHidden, + LSTM_GATES_COUNT, + config.xavierInputColumns.distribution, + config.xavierInputColumns.present, + config.orthogonalRecurrentColumns, + config.perGateOrthogonalBlocks + ) + ); + + // Biases/LN handling + const normGates = cell.normGates; + const forgetGateBiasPresent = config.lstm.forgetGateBias.present; + const forgetGateBiasValue = config.lstm.forgetGateBias.value; + + if (normGates instanceof LayerNorm && normGates.bias) { + // Keep Linear bias zero if requested (recommended with LN) + if (cell.W.bias) + cell.W.bias = config.zeroBiases ? buildParameterHacky(initZeros_(cell.W.bias)) : cell.W.bias; + + // LN beta blocks: [i, f, g, o]; set f to forgetVal, others to 0 + const [b_i, b_f, b_g, b_o] = piston.chunk(normGates.bias, LSTM_GATES_COUNT, { + dim: 0 + }); + normGates.bias = buildParameterHacky( + piston.cat( + [ + initZeros_(b_i), + forgetGateBiasPresent ? initConstant_(b_f, forgetGateBiasValue) : initZeros_(b_f), + initZeros_(b_g), + initZeros_(b_o) + ], + { dim: 0 } + ) + ); + } else if (cell.W.bias) { + const [b_i, b_f, b_g, b_o] = piston.chunk(cell.W.bias, LSTM_GATES_COUNT, { + dim: 0 + }); + cell.W.bias = buildParameterHacky( + piston.cat( + [ + config.zeroBiases ? initZeros_(b_i) : b_i, + forgetGateBiasPresent + ? initConstant_(b_f, forgetGateBiasValue) + : config.zeroBiases + ? initZeros_(b_f) + : b_f, + config.zeroBiases ? initZeros_(b_g) : b_g, + config.zeroBiases ? initZeros_(b_o) : b_o + ], + { dim: 0 } + ) + ); + } +} + +export function initRNNCell_(cell: RNNCell, config: RNNInitializationConfig) { + if (config.xavierInputColumns.present) { + const dist = config.xavierInputColumns.distribution; + cell.WIh.weight = buildParameterHacky( + dist === 'uniform' ? initXavierUniform_(cell.WIh.weight) : initXavierNormal_(cell.WIh.weight) + ); + } + + if (cell.WIh.bias) { + cell.WIh.bias = config.zeroBiases + ? buildParameterHacky(initZeros_(cell.WIh.bias)) + : cell.WIh.bias; + } + + if (config.orthogonalRecurrentColumns) { + cell.WHh.weight = buildParameterHacky(initOrthogonal_(cell.WHh.weight)); + } + + if (cell.WHh.bias) { + cell.WHh.bias = config.zeroBiases + ? buildParameterHacky(initZeros_(cell.WHh.bias)) + : cell.WHh.bias; + } +} + +export function initRNNParameters(root: nn.Module, config: Config) { + if (!config.model.rnn.initialization.present) return; + + const initializeRNNCell = (module: nn.Module) => { + if (module instanceof GRUCell) { + initGRUCell_(module, config.model.rnn.initialization); + } else if (module instanceof LSTMCell) { + initLSTMCell_(module, config.model.rnn.initialization); + } else if (module instanceof RNNCell) { + initRNNCell_(module, config.model.rnn.initialization); + } + }; + + root.apply(initializeRNNCell); +} diff --git a/examples/piston-train-toy/src/lib/train/utils/model.ts b/examples/piston-train-toy/src/lib/train/utils/model.ts new file mode 100644 index 00000000..be4722d3 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/utils/model.ts @@ -0,0 +1,374 @@ +import type { Random } from 'random-js'; + +import { + CaptureIndexMode, + DataLoader, + type IndexState, + Module, + Tensor, + weak +} from '@piston-ml/piston-web'; +import * as piston from '@piston-ml/piston-web'; + +import type { Config } from '../../workspace/config'; +import type { + NaturalBatchType, + NaturalCollateFnType, + NaturalDataloaderType, + PistonCollateFnType, + PistonDataloaderType, + PistonDatasetType, + ToyBatchType, + ToyCollateFnType, + ToyDataloaderType +} from '../types'; + +import { buildDataset, type CollateWrapFunction, tensorWrap } from '../data'; +import { + naturalLanguageAutoregressiveCollate, + naturalLanguageBidirectionalCollate, + NaturalLanguageDataset +} from '../data/natural'; +import { + toyDatasetAutoregressiveCollate, + toyDatasetBidirectionalCollate, + toyDatasetEncoderDecoderCollate +} from '../data/toy/collate'; +import { type ToyDatasetLike, type ToySequence } from '../data/toy/dataset'; +import { RNNDecoder, RNNEncoder, RNNEncoderDecoder } from '../model/rnn'; +import { + DecoderTransformer, + EncoderDecoderTransformer, + EncoderTransformer +} from '../model/transformer'; +import { initRNNParameters, initTransformerParameters } from './init'; +import { parseSeed, seededRandom } from './random'; + +type EncoderDecoderBlockSize = { source: number; target: number }; + +/** + * Calculate the vocabulary size based on the dataset and configuration + */ +export function calculateVocabSize(config: Config, dataset: PistonDatasetType): number { + let vocabSize = + 'vocabSize' in dataset + ? (dataset.vocabSize as number) + : Object.keys(dataset.tokenizer.vocab).length; + + // Round vocab size up to nearest multiple if configured + if (config.model.roundVocabSizeToNearestMultiple.present) { + vocabSize = + Math.ceil(vocabSize / config.model.roundVocabSizeToNearestMultiple.value) * + config.model.roundVocabSizeToNearestMultiple.value; + } + + return vocabSize; +} + +export function calculateBlockSize( + config: Config, + dataloader: PistonDataloaderType +): number | EncoderDecoderBlockSize { + if (dataloader.dataset instanceof NaturalLanguageDataset) { + return config.data.natural.contextSize; + } + + // For toy datasets, infer lengths without advancing iteration using deterministic index-based generation + const toy = dataloader.dataset as ToyDatasetLike; + + const sample = toy.generateSequenceAt(0); + + const promptLen = sample.prompt?.length ?? 0; + const targetLen = sample.target.length; + const eosPlus = toy.eosId !== null ? 1 : 0; + const bosPlus = toy.bosId !== null ? 1 : 0; + + if (config.model.topology === 'encoder-decoder') { + // Encoder input uses prompt (no BOS/EOS). Decoder target includes EOS if enabled. + return { source: promptLen, target: targetLen + eosPlus }; + } + + if (config.model.topology === 'encoder') { + // Encoder-only sees prompt+target (no specials) + return promptLen + targetLen; + } + + // Decoder-only sees BOS + prompt + target + EOS in full sequence + return bosPlus + promptLen + targetLen + eosPlus; +} + +// Overloads for strong typing based on dataset kind +export function createCollateFn( + config: Config, + dataset: NaturalLanguageDataset, + maskGenerator: Random | null, + wrapFunction?: CollateWrapFunction | null +): NaturalCollateFnType; +export function createCollateFn( + config: Config, + dataset: ToyDatasetLike, + maskGenerator: Random | null, + wrapFunction?: CollateWrapFunction | null +): ToyCollateFnType; +export function createCollateFn( + config: Config, + dataset: PistonDatasetType, + maskGenerator: Random | null, + wrapFunction?: CollateWrapFunction | null +): PistonCollateFnType; +export function createCollateFn( + config: Config, + dataset: PistonDatasetType, + maskGenerator: Random | null, + wrapFunction?: CollateWrapFunction | null +): PistonCollateFnType { + const isEncoderOnly = config.model.topology === 'encoder'; + const isEncoderDecoder = config.model.topology === 'encoder-decoder'; + const collateOptions = wrapFunction !== undefined ? { wrapFunction } : {}; + if (dataset instanceof NaturalLanguageDataset) { + if (isEncoderDecoder) { + throw new Error('Encoder-decoder is not supported for natural language datasets.'); + } + if (isEncoderOnly) { + return (batch: number[][]) => + naturalLanguageBidirectionalCollate(batch as number[][], { + maskRatio: config.data.maskRatio, + generator: maskGenerator!, + maskTokenId: dataset.maskId! as number, + ...collateOptions + }); + } else { + return (batch: number[][]) => + naturalLanguageAutoregressiveCollate(batch as number[][], { + ...collateOptions + }); + } + } else if (isEncoderOnly) { + return (batch: ToySequence[]) => + toyDatasetBidirectionalCollate(batch as ToySequence[], dataset, { + maskPrompt: config.data.trainOnPrompt, + maskRatio: config.data.maskRatio, + // Derive per-sample RNG from dataset/index internally (stateless) + ...collateOptions + }); + } else if (isEncoderDecoder) { + return (batch: ToySequence[]) => + toyDatasetEncoderDecoderCollate(batch as ToySequence[], dataset, { + ...collateOptions + }); + } else { + return (batch: ToySequence[]) => + toyDatasetAutoregressiveCollate(batch as ToySequence[], dataset, { + ignorePrompt: !config.data.trainOnPrompt, + ...collateOptions + }); + } +} + +export function createDataloader( + config: Config, + dataset: NaturalLanguageDataset, + generator: Random, + wrapFunction?: CollateWrapFunction | null +): [NaturalDataloaderType, NaturalCollateFnType]; +export function createDataloader( + config: Config, + dataset: ToyDatasetLike, + generator: Random, + wrapFunction?: CollateWrapFunction | null +): [ToyDataloaderType, ToyCollateFnType]; +export function createDataloader( + config: Config, + dataset: PistonDatasetType, + generator: Random, + wrapFunction?: CollateWrapFunction | null +): + | [NaturalDataloaderType, NaturalCollateFnType] + | [ToyDataloaderType, ToyCollateFnType]; +export function createDataloader( + config: Config, + dataset: PistonDatasetType, + generator: Random, + wrapFunction?: CollateWrapFunction | null +): + | [NaturalDataloaderType, NaturalCollateFnType] + | [ToyDataloaderType, ToyCollateFnType] { + if (dataset instanceof NaturalLanguageDataset) { + const collateFn = createCollateFn(config, dataset, generator, wrapFunction); + return [new DataLoader>(dataset, { collateFn }), collateFn]; + } else { + const toyDataset = dataset as ToyDatasetLike; + const collateFn = createCollateFn(config, toyDataset, generator, wrapFunction); + return [new DataLoader>(toyDataset, { collateFn }), collateFn]; + } +} + +/** + * Create a model instance based on the configuration + */ +export function createModel( + config: Config, + vocabSize: number, + blockSize: number | { source: number; target: number } +): + | DecoderTransformer + | EncoderTransformer + | EncoderDecoderTransformer + | RNNDecoder + | RNNEncoder + | RNNEncoderDecoder { + const isEncoderOnly = config.model.topology === 'encoder'; + const isDecoderOnly = config.model.topology === 'decoder'; + const isEncoderDecoder = config.model.topology === 'encoder-decoder'; + + if (!isEncoderOnly && !isDecoderOnly && !isEncoderDecoder) { + throw new Error( + `Unsupported model type: ${config.model.topology}. Only 'encoder', 'decoder', and 'encoder-decoder' are currently supported.` + ); + } + + if (isEncoderOnly) { + if (config.model.family === 'rnn') { + return new RNNEncoder(config, vocabSize); + } else { + return new EncoderTransformer(config, vocabSize, blockSize as number); + } + } else if (isEncoderDecoder) { + const { source, target } = blockSize as { source: number; target: number }; + if (config.model.family === 'rnn') { + return new RNNEncoderDecoder(config, vocabSize); + } else { + return new EncoderDecoderTransformer(config, vocabSize, source, target); + } + } else { + if (config.model.family === 'rnn') { + return new RNNDecoder(config, vocabSize); + } else { + return new DecoderTransformer(config, vocabSize, blockSize as number); + } + } +} + +export function initializeModel( + config: Config, + model: + | DecoderTransformer + | EncoderTransformer + | EncoderDecoderTransformer + | RNNDecoder + | RNNEncoder + | RNNEncoderDecoder +) { + if (config.model.family === 'rnn') { + initRNNParameters(model, config); + } else { + initTransformerParameters(model, config); + } +} + +export function calculateParameterSum(model: Module): Tensor { + const sums = model.parameters().map((param) => param.sum()); + return piston.stack(sums).sum(); +} + +export function countParameters( + model: + | DecoderTransformer + | EncoderTransformer + | EncoderDecoderTransformer + | RNNDecoder + | RNNEncoder + | RNNEncoderDecoder +): number { + let totalParams = 0; + + // Walk through all named parameters + for (const [_, param] of model.namedParameters()) { + if (param && param.shape) { + const paramCount = (param.shape as number[]).reduce( + (acc: number, dim: number) => acc * dim, + 1 + ); + totalParams += paramCount; + } + } + + return totalParams; +} + +/** + * Inspect model for a given configuration: count the number of parameters and capture an "index" + * of the model. + */ +export function inspectModel(config: Config): { + parameterCount: number; + modelIndex: IndexState; + vocabSize: number; + blockSize: number; +} { + return weak( + () => { + const seed = seedPiston(config); + const generator = seededRandom(seed); + const dataset = buildDataset(config, generator, 'train'); + const [dataloader] = createDataloader(config, dataset, generator, tensorWrap); + const isEncoderDecoder = config.model.topology === 'encoder-decoder'; + const blockSizeOrSizes = calculateBlockSize(config, dataloader); + const vocabSize = calculateVocabSize(config, dataset); + const model = createModel(config, vocabSize, blockSizeOrSizes); + const parameterCount = countParameters(model); + + let indexMode: CaptureIndexMode | null = null; + try { + indexMode = new CaptureIndexMode(model); + + // Run the model forward with an input from the dataloader + if (model instanceof DecoderTransformer || model instanceof RNNDecoder) { + model.forward(piston.zeros([1, blockSizeOrSizes as number], { dtype: piston.int32 })); + } else if (model instanceof EncoderTransformer || model instanceof RNNEncoder) { + model.forward(piston.zeros([1, blockSizeOrSizes as number], { dtype: piston.int32 })); + } else if ( + model instanceof EncoderDecoderTransformer || + model instanceof RNNEncoderDecoder + ) { + model.forward( + piston.zeros([1, (blockSizeOrSizes as EncoderDecoderBlockSize).source], { + dtype: piston.int32 + }), + piston.zeros([1, (blockSizeOrSizes as EncoderDecoderBlockSize).target], { + dtype: piston.int32 + }) + ); + } + + console.debug(`Model has ${parameterCount} parameters with vocab size ${vocabSize}`); + + const blockSize = isEncoderDecoder + ? Math.max( + (blockSizeOrSizes as { source: number; target: number }).source, + (blockSizeOrSizes as { source: number; target: number }).target + ) + : (blockSizeOrSizes as number); + return { parameterCount, vocabSize, blockSize, modelIndex: indexMode!.index }; + } finally { + indexMode![Symbol.dispose](); + } + }, + { + label: 'inspectModel' + } + ); +} + +export function seedPiston(config: Config) { + // Set up random number generator + const seed = parseSeed( + config.training.randomSeed.present ? config.training.randomSeed.value : undefined + ); + + if (seed !== undefined) { + piston.seed(seed); + } + + return seed; +} diff --git a/examples/piston-train-toy/src/lib/train/utils/modes.ts b/examples/piston-train-toy/src/lib/train/utils/modes.ts new file mode 100644 index 00000000..4be5a4e7 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/utils/modes.ts @@ -0,0 +1,96 @@ +import { + PistonFunctionMode, + PistonMarkStepMode, + WeakMarkStepMode, + WeakTensorFunctionMode, + type WeakTensorFunctionModeOptions +} from '@piston-ml/piston-web'; +import * as piston from '@piston-ml/piston-web'; + +export class DebugMode extends PistonFunctionMode { + constructor(public debugEnabled: boolean) { + super(); + } + + _pistonFunction( + func: (...args: unknown[]) => FT | Promise, + _types: unknown[], + args: unknown[], + kwargs: Record + ): T | Promise { + if (this.debugEnabled) { + console.log( + func.name, + args.reduce((acc: number[], a) => { + if (a instanceof piston.wasm.Tensor_wasm) { + return [...acc, a.id]; + } + return acc; + }, []) + ); + } + + const after = (result: T) => { + if (result instanceof piston.wasm.Tensor_wasm) { + console.log(func.name, 'result', result.id); + } + return result; + }; + + const result = func(...args, kwargs) as T | Promise; + if (result instanceof Promise) { + return result.then(after) as Promise; + } + + return after(result) as T; + } +} + +export class WeakModeIfEnabled { + private mode: WeakTensorFunctionMode | null = null; + + constructor( + public enabled: boolean, + public options: WeakTensorFunctionModeOptions + ) { + if (enabled) { + this.mode = new WeakTensorFunctionMode(options); + } + } + + markWeak(input: T) { + if (this.mode) { + this.mode.markWeak(input); + } + return input; + } + + pin(input: T) { + if (this.mode) { + this.mode.pin(input); + } + return input; + } + + [Symbol.dispose]() { + if (this.mode) { + this.mode[Symbol.dispose](); + } + } +} + +export class MarkStepModeIfEnabled { + private mode: PistonMarkStepMode | null = null; + + constructor(public enabled: boolean) { + if (enabled) { + this.mode = new WeakMarkStepMode(); + } + } + + [Symbol.dispose]() { + if (this.mode) { + this.mode[Symbol.dispose](); + } + } +} diff --git a/examples/piston-train-toy/src/lib/train/utils/optim.ts b/examples/piston-train-toy/src/lib/train/utils/optim.ts new file mode 100644 index 00000000..0abe58b1 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/utils/optim.ts @@ -0,0 +1,397 @@ +import type { OptimizerConfig } from '$lib/workspace/config'; + +import { + AdamW, + type Device, + type Module, + MuonWithAdamW, + type MuonWithAdamWParamGroup, + nn, + type Optimizer, + type Parameter as ParameterType, + type ParamGroup, + SGD +} from '@piston-ml/piston-web'; + +// Deterministic sorting helpers +function compareByName(a: [string, T], b: [string, T]): number { + return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0; +} + +function sortEntriesByName(entries: Array<[string, T]>): Array<[string, T]> { + return entries.sort(compareByName); +} + +function paramsFromNamesSorted( + names: Iterable, + paramDict: Map +): ParameterType[] { + return Array.from(names) + .sort() + .map((name) => paramDict.get(name)!) + .filter((p) => p != null); +} + +/** + * Validates that all model parameters are included in the parameter groups. + * Throws an error if any parameters are missing from the groups. + * @param model - The model to validate + * @param paramGroups - The parameter groups to check + * @throws Error if any model parameters are not included in the parameter groups + */ +function validateParameterGroups( + model: Module, + paramGroups: ParamGroup[], + paramDict: Map +): void { + // Get all parameters from the model + const allModelParams = new Set(); + for (const [_, param] of model.namedParameters()) { + allModelParams.add(param); + } + + // Get all parameters from the parameter groups + const groupParams = new Set(); + for (const group of paramGroups) { + for (const param of group.params) { + groupParams.add(param); + } + } + + // Find parameters that are in the model but not in any group + const missingParams: ParameterType[] = []; + for (const param of allModelParams) { + if (!groupParams.has(param)) { + missingParams.push(param); + } + } + + if (missingParams.length > 0) { + // Find the names of the missing parameters using paramDict + const missingParamNames: string[] = []; + for (const [name, param] of paramDict) { + if (missingParams.includes(param)) { + missingParamNames.push(name); + } + } + + throw new Error( + `Found ${missingParams.length} model parameters that are not included in any parameter group (${groupParams.size} included). ` + + `All model parameters must be assigned to a parameter group for training. ` + + `Missing parameters: ${missingParamNames.join(', ')}` + ); + } +} + +function getWeightDecayParams( + model: Module, + useWeightDecayGroups: boolean, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + whitelistWeightModules: (new (...args: any[]) => Module)[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + blacklistWeightModules: (new (...args: any[]) => Module)[] +): { paramDict: Map; decay: Set; noDecay: Set } { + const decay = new Set(); + const noDecay = new Set(); + + const paramDict = new Map(); + + for (const [mn, m] of model.namedModules()) { + for (const [pn, p] of m.namedParameters()) { + const fpn = mn ? `${mn}.${pn}` : pn; + + paramDict.set(fpn, p); + + if (useWeightDecayGroups) { + if (pn.endsWith('bias')) { + // All biases will not be decayed + noDecay.add(fpn); + } else if (pn.endsWith('weight')) { + if (whitelistWeightModules.some((cls) => m instanceof cls)) { + // Weights of whitelist modules will be weight decayed + decay.add(fpn); + } else if (blacklistWeightModules.some((cls) => m instanceof cls)) { + // Weights of blacklist modules will NOT be weight decayed + noDecay.add(fpn); + } + } else { + // Parameters that are not weights or biases (shouldn't exist in std models) + // Add to decay by default, adjust if necessary for specific models. + decay.add(fpn); + } + } else { + decay.add(fpn); + } + } + } + + if (useWeightDecayGroups) { + // Validate that we considered every parameter + const allParamNames = new Set( + Array.from(model.namedParameters()).map(([name]) => name) + ); + const interParams = new Set([...decay].filter((x) => noDecay.has(x))); + const unionParams = new Set([...decay, ...noDecay]); + + if (interParams.size !== 0) { + throw new Error( + `Parameters ${JSON.stringify(Array.from(interParams))} made it into both decay/noDecay sets` + ); + } + const missingParams = new Set([...allParamNames].filter((x) => !unionParams.has(x))); + if (missingParams.size !== 0) { + throw new Error( + `Parameters ${JSON.stringify( + Array.from(missingParams) + )} were not separated into either decay/noDecay set` + ); + } + } + + return { paramDict, decay, noDecay }; +} + +/** + * Based on what minGPT does: + * Configures the optimizer based on the training configuration. + * Separates parameters into weight decay and no weight decay groups. + * @param trainConfig - The optimizer + * configuration + * @param device - The computation device + * @returns The configured optimizer + */ +function configureOptimizers( + model: Module, + moduleLayersPrefixes: string[], + lmHeadPrefix: string, + trainConfig: OptimizerConfig, + device: Device +): Optimizer { + const whitelistWeightModules = [nn.Linear]; + const blacklistWeightModules = [nn.LayerNorm, nn.RMSNorm, nn.Embedding]; + + const effectiveWeightDecay = trainConfig.weightDecay.present + ? trainConfig.weightDecay.value + : 0.0; + + const { paramDict, decay, noDecay } = getWeightDecayParams( + model, + trainConfig.weightDecay.useWeightDecayGroups, + whitelistWeightModules, + blacklistWeightModules + ); + // Deterministic param lists by name + const decayParamsValues = paramsFromNamesSorted(decay, paramDict); + const noDecayParamsValues = paramsFromNamesSorted(noDecay, paramDict); + + if (trainConfig.type === 'AdamW' || trainConfig.type === 'Adam' || trainConfig.type === 'SGD') { + const optimGroups: ParamGroup[] = [ + { + params: decayParamsValues, + weightDecay: effectiveWeightDecay + }, + ...(noDecayParamsValues.length > 0 + ? [ + { + params: noDecayParamsValues, + weightDecay: 0.0 // no decay + } + ] + : []) + ]; + + validateParameterGroups(model, optimGroups, paramDict); + + // Create the AdamW optimizer + if (trainConfig.type === 'AdamW' || trainConfig.type === 'Adam') { + return new AdamW(optimGroups, device, { + lr: trainConfig.lr, + betas: [trainConfig.adam.beta1, trainConfig.adam.beta2], + eps: trainConfig.adam.eps, + weightDecay: effectiveWeightDecay, + amsgrad: trainConfig.adam.amsgrad + }); + } else if (trainConfig.type === 'SGD') { + return new SGD(optimGroups, device, { + lr: trainConfig.lr, + momentum: trainConfig.sgd.momentum, + dampening: trainConfig.sgd.dampening, + weightDecay: effectiveWeightDecay, + nesterov: trainConfig.sgd.nesterov + }); + } + } else if (trainConfig.type === 'Muon') { + // Get parameter groups by type + const paramEntries = sortEntriesByName(Array.from(paramDict.entries())); + const moduleLayersParams = paramEntries.filter(([n]) => + moduleLayersPrefixes.some((prefix) => n.startsWith(prefix)) + ); + // Sort each category deterministically by name + const hiddenMatrixParams = sortEntriesByName( + moduleLayersParams.filter(([n, p]) => p.ndim >= 2 && !n.toLowerCase().includes('embed')) + ); + const scalarParams = sortEntriesByName(moduleLayersParams.filter(([_, p]) => p.ndim < 2)); + const embedParams = sortEntriesByName( + paramEntries.filter(([n, _]) => n.toLowerCase().includes('embed')) + ); + const headParams = sortEntriesByName(paramEntries.filter(([n]) => n.startsWith(lmHeadPrefix))); + // Any other params we just throw to AdamW + const filteredParams = new Set([ + ...hiddenMatrixParams.map(([n]) => n), + ...scalarParams.map(([n]) => n), + ...embedParams.map(([n]) => n), + ...headParams.map(([n]) => n) + ]); + const remainingParams = paramEntries.filter(([n]) => !filteredParams.has(n)); + + if (remainingParams.length > 0) { + console.warn( + `Found ${remainingParams.length} parameters that don't fit Muon categorization and will be handled by AdamW:`, + remainingParams.map(([name]) => name) + ); + } + + // Apply weight decay grouping to each parameter type + const paramGroups: MuonWithAdamWParamGroup[] = []; + + // Hidden matrix parameters for Muon optimizer + if (trainConfig.weightDecay.useWeightDecayGroups) { + const hiddenDecay = hiddenMatrixParams.filter(([name]) => decay.has(name)).map(([_, p]) => p); + const hiddenNoDecay = hiddenMatrixParams + .filter(([name]) => noDecay.has(name)) + .map(([_, p]) => p); + + if (hiddenDecay.length > 0) { + paramGroups.push({ + optimizer: 'muon', + lr: trainConfig.lr, + weightDecay: effectiveWeightDecay, + momentum: trainConfig.muon.momentum, + nsSteps: trainConfig.muon.nsSteps, + nesterov: trainConfig.muon.nesterov, + params: hiddenDecay + }); + } + + if (hiddenNoDecay.length > 0) { + paramGroups.push({ + optimizer: 'muon', + lr: trainConfig.lr, + weightDecay: 0.0, // no decay + momentum: trainConfig.muon.momentum, + nsSteps: trainConfig.muon.nsSteps, + nesterov: trainConfig.muon.nesterov, + params: hiddenNoDecay + }); + } + } else { + if (hiddenMatrixParams.length > 0) { + paramGroups.push({ + optimizer: 'muon', + lr: trainConfig.lr, + weightDecay: effectiveWeightDecay, + momentum: trainConfig.muon.momentum, + nsSteps: trainConfig.muon.nsSteps, + nesterov: trainConfig.muon.nesterov, + params: hiddenMatrixParams.map(([_, p]) => p) + }); + } + } + + // Scalar, embedding, and head parameters for AdamW optimizer + const adamwParams = sortEntriesByName([ + ...scalarParams, + ...embedParams, + ...headParams, + ...remainingParams + ]); + + // Check if there is any overlap between the two optimizers getting overlap of adamWparams + const adamwParamSet = new Set(adamwParams.map(([n]) => n)); + const muonParamSet = new Set(hiddenMatrixParams.map(([n]) => n)); + const overlap = adamwParamSet.intersection(muonParamSet); + if (overlap.size > 0) { + throw new Error( + `Overlap between AdamW and Muon parameters: ${Array.from(overlap).join(', ')}` + ); + } + + if (trainConfig.weightDecay.useWeightDecayGroups) { + const adamwDecay = adamwParams.filter(([name]) => decay.has(name)).map(([_, p]) => p); + const adamwNoDecay = adamwParams.filter(([name]) => noDecay.has(name)).map(([_, p]) => p); + + if (adamwDecay.length > 0) { + paramGroups.push({ + optimizer: 'adamw', + lr: trainConfig.lr, + betas: [trainConfig.adam.beta1, trainConfig.adam.beta2], + eps: trainConfig.adam.eps, + weightDecay: effectiveWeightDecay, + amsgrad: trainConfig.adam.amsgrad, + params: adamwDecay + }); + } + + if (adamwNoDecay.length > 0) { + paramGroups.push({ + optimizer: 'adamw', + lr: trainConfig.lr, + betas: [trainConfig.adam.beta1, trainConfig.adam.beta2], + eps: trainConfig.adam.eps, + weightDecay: 0.0, // no decay + amsgrad: trainConfig.adam.amsgrad, + params: adamwNoDecay + }); + } + } else { + if (adamwParams.length > 0) { + paramGroups.push({ + optimizer: 'adamw', + lr: trainConfig.lr, + betas: [trainConfig.adam.beta1, trainConfig.adam.beta2], + eps: trainConfig.adam.eps, + weightDecay: effectiveWeightDecay, + amsgrad: trainConfig.adam.amsgrad, + params: adamwParams.map(([_, p]) => p) + }); + } + } + + validateParameterGroups(model, paramGroups, paramDict); + + return new MuonWithAdamW(paramGroups, device, { + muon: { + lr: trainConfig.lr, + weightDecay: effectiveWeightDecay, + momentum: trainConfig.muon.momentum, + nsSteps: trainConfig.muon.nsSteps, + nesterov: trainConfig.muon.nesterov + }, + adamw: { + lr: trainConfig.lr, + betas: [trainConfig.adam.beta1, trainConfig.adam.beta2], + eps: trainConfig.adam.eps, + weightDecay: effectiveWeightDecay, + amsgrad: trainConfig.adam.amsgrad + } + }); + } + + throw new Error(`Unknown optimizer type: ${trainConfig.type}`); +} + +export function configureOptimizerForModel( + model: Module, + isEncoderOnly: boolean, + isEncoderDecoder: boolean, + config: OptimizerConfig, + device: Device +): Optimizer { + if (isEncoderOnly) { + return configureOptimizers(model, ['encoder.layer'], 'mlmHead', config, device); + } else if (isEncoderDecoder) { + return configureOptimizers(model, ['decoder.layer', 'encoder.layer'], 'lmHead', config, device); + } else { + return configureOptimizers(model, ['decoder.layer'], 'lmHead', config, device); + } +} diff --git a/examples/piston-train-toy/src/lib/train/utils/random.ts b/examples/piston-train-toy/src/lib/train/utils/random.ts new file mode 100644 index 00000000..03756479 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/utils/random.ts @@ -0,0 +1,39 @@ +import { MersenneTwister19937, Random } from 'random-js'; + +export function parseSeed(seed?: string): number | undefined { + if (seed === undefined || seed === '') { + return undefined; + } + + const parsed = parseInt(seed); + if (!isNaN(parsed)) { + return parsed; + } + + // Simple hash function for string + let hash = 0; + for (let i = 0; i < seed.length; i++) { + const char = seed.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash); +} + +/** + * Creates a seeded random number generator from a string or undefined seed. + * If seed is undefined, auto-seeds the generator. + * If seed is a string that can be parsed as a number, uses the parsed number. + * Otherwise, uses a simple hash function to convert the string to a number. + */ +export function seededRandom(seed?: number): Random { + if (seed === undefined) { + return new Random(MersenneTwister19937.autoSeed()); + } + + return new Random(MersenneTwister19937.seed(seed)); +} + +export function forkRandom(random: Random): Random { + return new Random(MersenneTwister19937.seed(random.int32())); +} diff --git a/examples/piston-train-toy/src/lib/train/validation.ts b/examples/piston-train-toy/src/lib/train/validation.ts new file mode 100644 index 00000000..5dc4c54d --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/validation.ts @@ -0,0 +1,540 @@ +import type { Config, ValidationConfig } from '$lib/workspace/config'; +import type { BaseStepData, TokenRollout } from '$lib/workspace/runs.svelte'; + +import { type Tensor, weak } from '@piston-ml/piston-web'; +import { MersenneTwister19937, Random } from 'random-js'; + +import type { ToyValidationMetrics } from './data/toy/types'; +import type { + BidirectionalBatchType, + EncoderDecoderBatchType, + GeneratableModel, + NaturalCollateFnType, + PistonCollateFnType, + PistonDatasetType, + ToyCollateFnType +} from './types'; + +import { naturalLanguageBidirectionalCollate, NaturalLanguageDataset } from './data/natural'; +import { + toyDatasetAutoregressiveCollate, + toyDatasetBidirectionalCollate, + toyDatasetEncoderDecoderCollate +} from './data/toy/collate'; +import { type ToyDatasetLike, type ToySequence } from './data/toy/dataset'; +import { RNNDecoder, RNNEncoder, RNNEncoderDecoder } from './model/rnn'; +import { + DecoderTransformer, + EncoderDecoderTransformer, + EncoderTransformer +} from './model/transformer'; +import { + generateDecoderCompletions, + generateEncoderDecoderCompletions, + predictEncoderOnlyCompletions +} from './validationHelpers'; + +export type ValidationStep = BaseStepData & { + type: 'validation'; + completions: TokenRollout[]; + samplingParams: { + temperature: number; + }; + // Running average throughput across the generation in tokens/second (decoder/generative only) + tokensPerSecond?: number; + targets?: number[][]; // Only present in first step, what tokens should have been + encoderInputs?: number[][]; // Encoder/source inputs used (for encoder-decoder or encoder-only) + decoderPromptLengths?: number[]; // For decoder-only display: prompt token counts per example + matches?: boolean[][]; // Per-example, per-token correctness flags + metrics?: ToyValidationMetrics[]; // Per-example metrics computed by the dataset +}; + +export type ToyValidationExamples = { + toySequences: ToySequence[]; + prompts: number[][]; // decoder-only: prompt; encoder-decoder: encoder input (with specials) + targets: number[][]; + actualTargets: number[][]; +}; + +export type NaturalValidationExamples = { + naturalSequences: number[][]; +}; + +export type ValidationExamples = ToyValidationExamples | NaturalValidationExamples; + +export function buildValidationExamplesSubset( + examples: ValidationExamples, + subsetSize: number +): ValidationExamples { + if ('toySequences' in examples) { + return { + toySequences: examples.toySequences.slice(0, subsetSize), + prompts: examples.prompts.slice(0, subsetSize), + targets: examples.targets.slice(0, subsetSize), + actualTargets: examples.actualTargets.slice(0, subsetSize) + }; + } + + if ('naturalSequences' in examples) { + return { + naturalSequences: examples.naturalSequences.slice(0, subsetSize) + }; + } + + throw new Error('Unsupported validation examples'); +} + +export function prepareToyValidationExamples( + config: Config, + dataset: ToyDatasetLike, + options: { isDecoderOnly: boolean; isEncoderDecoder: boolean } +): ToyValidationExamples { + const { isDecoderOnly, isEncoderDecoder } = options; + + const valSequences: ToySequence[] = []; + const valPrompts: number[][] = []; + const valTargets: number[][] = []; + const valActualTargets: number[][] = []; + const it = dataset[Symbol.iterator](); + + for (let i = 0; i < config.training.validation.batchSize; i++) { + const sampleSequence = it.next().value as ToySequence; + valSequences.push(sampleSequence); + + let samplePrompt: number[] | undefined; + let sampleTarget: number[]; + let actualTarget: number[]; + + if (isDecoderOnly) { + const collateResult = toyDatasetAutoregressiveCollate([sampleSequence], dataset, { + ignorePrompt: !config.data.trainOnPrompt, + wrapFunction: null + }); + samplePrompt = collateResult.raw[0].prompt; + sampleTarget = collateResult.raw[0].target ?? []; + actualTarget = collateResult.tensors[1][0]; + } else if (isEncoderDecoder) { + const collateResult = toyDatasetEncoderDecoderCollate([sampleSequence], dataset, { + wrapFunction: null + }); + // Visualize the encoder input (with specials) as the prompt + samplePrompt = collateResult.raw[0].prompt; + sampleTarget = collateResult.raw[0].target ?? []; + // For encoder-decoder, tensors are [encoderInput, decoderInput, decoderTarget]. + // Use decoderTarget so it mirrors the training loss target (respects EOS setting). + actualTarget = collateResult.tensors[2][0]; + } else { + // Encoder-only models: build masked input and labels for visualization + const collateResult = toyDatasetBidirectionalCollate([sampleSequence], dataset, { + maskPrompt: config.data.trainOnPrompt, + maskRatio: config.data.maskRatio, + generator: dataset.generator, + wrapFunction: null + }); + // Show masked sequence (what encoder saw) as the prompt + samplePrompt = collateResult.raw[0].fullSequence; + // Use labels (with -100 for unmasked) as targets so viewer can compare only masked positions + sampleTarget = collateResult.tensors[1][0]; + // Actual training labels (with -100) for correctness comparison + actualTarget = collateResult.tensors[1][0]; + } + + valPrompts.push(samplePrompt ?? []); + valTargets.push(sampleTarget); + valActualTargets.push(actualTarget); + } + + if (valPrompts.length === 0) { + return { toySequences: [], prompts: [], targets: [], actualTargets: [] }; + } + + return { + toySequences: valSequences, + prompts: valPrompts, + targets: valTargets, + actualTargets: valActualTargets + }; +} + +export async function prepareNaturalValidationExamples( + config: Config, + dataset: NaturalLanguageDataset +): Promise { + const naturalSequences: number[][] = []; + + let count = 0; + for await (const sampleSequence of dataset) { + naturalSequences.push(sampleSequence); + count++; + if (count >= config.training.validation.batchSize) break; + } + + return { naturalSequences }; +} + +export async function prepareValidationExamples( + config: Config, + dataset: PistonDatasetType, + options: { isDecoderOnly: boolean; isEncoderDecoder: boolean } +): Promise { + if (dataset instanceof NaturalLanguageDataset) { + return await prepareNaturalValidationExamples(config, dataset); + } + return prepareToyValidationExamples(config, dataset, options); +} + +export async function computeToyValidationMetrics( + model: GeneratableModel, + dataset: ToyDatasetLike, + valExamples: ToyValidationExamples, + valConfig: ValidationConfig, + options: { isDecoderOnly: boolean; isEncoderDecoder: boolean; includeTargets: boolean } +): Promise> { + const { isDecoderOnly, isEncoderDecoder, includeTargets } = options; + + let tokensPerSecond: number | undefined; + let completions: TokenRollout[] = []; + + const maxTokens = Math.max(...valExamples.targets.map((t) => t.length)); + + if (isDecoderOnly && (model instanceof DecoderTransformer || model instanceof RNNDecoder)) { + const prompts = valExamples.prompts.map((prompt) => + prompt.length > 0 ? prompt : [dataset.bosId!] + ); + const result = await generateDecoderCompletions(model, prompts, { + maxTokens, + stopTokens: dataset.eosId !== null ? [dataset.eosId!] : [], + temperature: valConfig.temperature, + useKvCache: valConfig.useKvCache + }); + completions = result.completions; + tokensPerSecond = result.tokensPerSecond; + } else if ( + isEncoderDecoder && + (model instanceof EncoderDecoderTransformer || model instanceof RNNEncoderDecoder) + ) { + const sources = valExamples.prompts.map((prompt) => + prompt.length > 0 ? prompt : [dataset.bosId!] + ); + const result = await generateEncoderDecoderCompletions(model, sources, { + maxTokens, + startToken: dataset.bosId ?? undefined, + stopTokens: dataset.eosId !== null ? [dataset.eosId!] : [], + temperature: valConfig.temperature, + useKvCache: valConfig.useKvCache + }); + completions = result.completions; + tokensPerSecond = result.tokensPerSecond; + } else { + if (model instanceof EncoderTransformer || model instanceof RNNEncoder) { + const result = await predictEncoderOnlyCompletions( + model, + valExamples.prompts, + valExamples.actualTargets, + { temperature: valConfig.temperature } + ); + completions = result.completions; + } else { + throw new Error('Invalid model for encoder-only validation'); + } + } + + const validationStepData: Omit = { + type: 'validation', + completions, + samplingParams: { temperature: valConfig.temperature }, + targets: includeTargets ? valExamples.actualTargets : undefined, + // For display only: shave BOS from encoder inputs + encoderInputs: isEncoderDecoder || !isDecoderOnly ? valExamples.prompts : undefined, + // Only set for generative paths + tokensPerSecond + }; + + // Compute per-example metrics + try { + const batchSize = valExamples.prompts.length; + const metrics: Array = new Array(batchSize); + const matches: boolean[][] = []; + for (let bi = 0; bi < batchSize; bi++) { + const rollout = completions[bi]; + const generatedIds = rollout?.tokenIds ?? []; + + // Build predicted/target slices + let predictedSlice: number[] = []; + let targetSlice: number[] = []; + + if (!isDecoderOnly && !isEncoderDecoder) { + // Encoder-only: masked positions only + const labelsRow = valExamples.actualTargets[bi] ?? []; + const maskedIndices: number[] = []; + for (let i = 0; i < labelsRow.length; i++) { + if (labelsRow[i] !== -100) maskedIndices.push(i); + } + const filtered = maskedIndices.filter((i) => generatedIds[i] !== undefined); + predictedSlice = filtered.map((i) => generatedIds[i]); + targetSlice = filtered.map((i) => labelsRow[i]); + } else { + // Decoder-only or Encoder-decoder + const targetTokens = valExamples.targets[bi] ?? []; + const prefixLength = isDecoderOnly + ? (valExamples.prompts[bi]?.length ?? 0) + : isEncoderDecoder + ? 1 + : 0; + predictedSlice = generatedIds.slice(prefixLength, prefixLength + targetTokens.length); + targetSlice = targetTokens; + } + + // Single computeMetrics call and shared handling + const m = dataset.computeMetrics(predictedSlice, targetSlice); + metrics[bi] = m; + if (Array.isArray(m.matches)) { + matches[bi] = m.matches; + } + } + validationStepData.metrics = metrics; + validationStepData.matches = matches.length > 0 ? matches : undefined; + } catch (e) { + console.warn('Failed to compute validation metrics:', e); + } + + return validationStepData; +} + +export async function computeNaturalValidationMetrics( + model: GeneratableModel, + dataset: NaturalLanguageDataset, + valExamples: NaturalValidationExamples, + valConfig: ValidationConfig, + options: { isDecoderOnly: boolean; includeTargets: boolean; maskRatio: number } +): Promise> { + const { isDecoderOnly, maskRatio } = options; + + let promptLen = 0; + let encoderOnlyTargets: number[][] | null = null; + let tokensPerSecond: number | undefined; + let completions: TokenRollout[] = []; + + const contextSize = dataset.contextSize; + + if (isDecoderOnly && (model instanceof DecoderTransformer || model instanceof RNNDecoder)) { + // promptLen = Math.max(Math.floor(contextSize / 4), 1); + promptLen = 8; + const eosId = dataset.eosId as number; + const starts = valExamples.naturalSequences.map((seq) => seq.slice(0, promptLen)); + const maxTokens = Math.max(0, contextSize - promptLen); + const result = await generateDecoderCompletions(model, starts, { + maxTokens, + stopTokens: eosId !== null ? [eosId] : [], + temperature: valConfig.temperature, + useKvCache: valConfig.useKvCache + }); + completions = result.completions; + tokensPerSecond = result.tokensPerSecond; + } else { + // Encoder-only: predict masked tokens using MLM logits across the whole sequence + const maskTokenId = dataset.maskId as number; + const generator = new Random(MersenneTwister19937.autoSeed()); + + const collated = naturalLanguageBidirectionalCollate(valExamples.naturalSequences, { + maskRatio, + generator, + maskTokenId, + wrapFunction: null + }); + + const inputs = collated.tensors[0]; + const labels = collated.tensors[1]; // -100 for unmasked + encoderOnlyTargets = labels; + + if (model instanceof EncoderTransformer || model instanceof RNNEncoder) { + const attentionMask = model instanceof EncoderTransformer ? collated.tensors[2] : undefined; + const result = await predictEncoderOnlyCompletions(model, inputs, labels, { + attentionMask, + temperature: valConfig.temperature + }); + completions = result.completions; + } else { + throw new Error('Invalid model for encoder-only natural validation'); + } + } + + const validationStepData: Omit = { + type: 'validation', + completions, + samplingParams: { temperature: valConfig.temperature }, + decoderPromptLengths: isDecoderOnly + ? new Array(valExamples.naturalSequences.length).fill(promptLen) + : undefined, + targets: !isDecoderOnly && encoderOnlyTargets ? encoderOnlyTargets : undefined, + tokensPerSecond + }; + + // Compute metrics for natural encoder-only (MLM) similar to toy datasets + if (!isDecoderOnly && encoderOnlyTargets) { + try { + const B = completions.length; + const matches: boolean[][] = new Array(B); + const numericMetrics: Array = new Array(B); + for (let bi = 0; bi < B; bi++) { + const predIds: number[] = completions[bi]?.tokenIds ?? []; + const labelsRow: number[] = encoderOnlyTargets[bi] ?? []; + let correct = 0; + let total = 0; + const matchRow: boolean[] = new Array(labelsRow.length).fill(false); + for (let ti = 0; ti < labelsRow.length; ti++) { + const label = labelsRow[ti]; + if (label === -100) continue; // skip unmasked positions + total++; + const ok = predIds[ti] === label; + matchRow[ti] = ok; + if (ok) correct++; + } + const accuracy = total > 0 ? correct / total : 0; + numericMetrics[bi] = { + accuracy, + matches: matchRow + }; + matches[bi] = matchRow; + } + validationStepData.metrics = numericMetrics; + validationStepData.matches = matches; + } catch (e) { + console.warn('Failed to compute natural MLM validation metrics:', e); + } + } + + return validationStepData; +} + +export function buildValidationLog( + validationStepData: Omit +): Record> { + // Aggregate numeric-like metrics from per-example metrics; average arrays per-example; skip 'matches' + const aggregatedNumeric: Record = {}; + const counts: Record = {}; + const perExample = validationStepData.metrics; + if (perExample && perExample.length > 0) { + for (const m of perExample) { + for (const entry of Object.entries(m)) { + let [key] = entry; + const [_, value] = entry; + if (key === 'matches') { + key = 'character_level_accuracy'; + } + let numericValue: number | null = null; + if (typeof value === 'number' && Number.isFinite(value)) { + numericValue = value; + } else if (typeof value === 'boolean') { + numericValue = value ? 1 : 0; + } else if (Array.isArray(value)) { + // Average arrays of numbers or booleans + const arr = value; + if (arr.length > 0) { + const sum = arr.reduce((s: number, v: number | boolean) => { + const num = typeof v === 'boolean' ? (v ? 1 : 0) : Number.isFinite(v) ? v : 0; + return s + num; + }, 0); + numericValue = sum / arr.length; + } + } + if (numericValue !== null) { + aggregatedNumeric[key] = (aggregatedNumeric[key] ?? 0) + numericValue; + counts[key] = (counts[key] ?? 0) + 1; + } + } + } + for (const key of Object.keys(aggregatedNumeric)) { + aggregatedNumeric[key] = aggregatedNumeric[key] / (counts[key] || 1); + } + } + + // Compute number of unique completions by hashing tokenIds + const uniqueCompletionsCount = (() => { + const seen = new Set(); + for (const c of validationStepData.completions ?? []) { + const ids = c?.tokenIds ?? []; + seen.add(ids.join(',')); + } + return seen.size; + })(); + + const validationLog: Record = { + 'validation/completions': validationStepData, + 'validation/unique_completions': uniqueCompletionsCount + }; + if (typeof validationStepData.tokensPerSecond === 'number') { + validationLog['validation/tokens_per_second'] = validationStepData.tokensPerSecond; + } + for (const [k, v] of Object.entries(aggregatedNumeric)) { + validationLog[`validation/${k}`] = v; + } + return validationLog; +} + +export async function computeLikelihoodMetrics( + model: GeneratableModel, + sequences: ValidationExamples, + collateFn: PistonCollateFnType +): Promise<{ valLoss: number; perplexity: number }> { + return await weak(async () => { + model.eval(); + + let valLoss: number | null = null; + try { + let collated; + if ('toySequences' in sequences) { + collated = (collateFn as ToyCollateFnType)(sequences.toySequences); + } else { + collated = (collateFn as NaturalCollateFnType)(sequences.naturalSequences); + } + + let loss: Tensor | null = null; + let modelName = ''; + if (model instanceof DecoderTransformer || model instanceof RNNDecoder) { + const [inputs, targets] = collated.tensors; + [, loss] = model.forward(await inputs.to('gpu'), { + targets: await targets.to('gpu') + }); + modelName = 'decoder-only'; + } else if (model instanceof EncoderDecoderTransformer || model instanceof RNNEncoderDecoder) { + const [encoderInputs, decoderInputs, decoderTargets] = ( + collated as EncoderDecoderBatchType + ).tensors; + [, loss] = model.forward(await encoderInputs.to('gpu'), await decoderInputs.to('gpu'), { + targets: await decoderTargets.to('gpu') + }); + modelName = 'encoder-decoder'; + } else if (model instanceof EncoderTransformer || model instanceof RNNEncoder) { + // Encoder-only: compute MLM loss over masked tokens + const [inputs, labels, attentionMask] = (collated as BidirectionalBatchType) + .tensors; + modelName = 'encoder-only'; + if (model instanceof EncoderTransformer) { + [, , , loss] = model.forward(await inputs.to('gpu'), { + attentionMask: await attentionMask.to('gpu'), + targets: await labels.to('gpu') + }); + } else { + // No attention mask here + [, , loss] = model.forward(await inputs.to('gpu'), { targets: await labels.to('gpu') }); + } + } else { + throw new Error('Unsupported model for validation'); + } + + if (!loss) { + throw new Error(`No loss tensor returned from ${modelName} model during validation`); + } + valLoss = await (await loss.to('cpu')).item(); + if (valLoss === null) { + throw new Error(`Validation loss item is null for ${modelName} model`); + } + } finally { + model.train(); + } + + const perplexity = Math.exp(valLoss); + return { valLoss, perplexity }; + }); +} diff --git a/examples/piston-train-toy/src/lib/train/validationHelpers.ts b/examples/piston-train-toy/src/lib/train/validationHelpers.ts new file mode 100644 index 00000000..625eb3cf --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/validationHelpers.ts @@ -0,0 +1,196 @@ +import type { TokenRollout } from '$lib/workspace/runs.svelte'; + +import { pin, type Tensor, weak } from '@piston-ml/piston-web'; + +import { tensorWrap } from './data'; +import { generateStream } from './generate'; +import { RNNDecoder, RNNEncoder, RNNEncoderDecoder } from './model/rnn'; +import { + DecoderTransformer, + EncoderDecoderTransformer, + EncoderTransformer +} from './model/transformer'; + +export type DecoderGenerationOptions = { + maxTokens?: number; + stopTokens?: number[]; + temperature: number; + useKvCache: boolean; +}; + +export type EncoderDecoderGenerationOptions = { + maxTokens?: number; + startToken?: number; + stopTokens?: number[]; + temperature: number; + useKvCache: boolean; +}; + +export type EncoderOnlyPredictionOptions = { + attentionMask?: number[][]; // Only for transformer encoders + temperature?: number; +}; + +export async function generateDecoderCompletions( + model: DecoderTransformer | RNNDecoder, + startSequences: number[][], + options: DecoderGenerationOptions +): Promise<{ completions: TokenRollout[]; tokensPerSecond?: number }> { + const { maxTokens, stopTokens, temperature, useKvCache } = options; + + model.eval(); + + const completions: TokenRollout[] = []; + let lastTPS: number | undefined; + + for (let bi = 0; bi < startSequences.length; bi++) { + await weak(async () => { + const startTokens = startSequences[bi] ?? []; + let seq: number[] = []; + const perStepProbs: number[][] = []; + let stepIndex = 0; + for await (const generationResult of generateStream(model, startTokens, { + maxTokens, + stopTokens: stopTokens ?? [], + temperature, + useKvCache + })) { + seq = generationResult.sequences[0] ? [...generationResult.sequences[0]] : []; + lastTPS = generationResult.tokensPerSecond ?? lastTPS; + if (generationResult.probs) { + const probsArray = await (await generationResult.probs.to('cpu')).toVec(); + const [_b, v] = generationResult.probs.shape; + const row: number[] = []; + for (let vi = 0; vi < v; vi++) { + row.push(probsArray[0 * v + vi]); + } + perStepProbs.push(row); + } + stepIndex++; + if (typeof maxTokens === 'number' && stepIndex >= maxTokens) break; + } + completions[bi] = { tokenIds: seq, probs: pin(perStepProbs) }; + }); + } + + model.train(); + + return { completions, tokensPerSecond: lastTPS }; +} + +export async function generateEncoderDecoderCompletions( + model: EncoderDecoderTransformer | RNNEncoderDecoder, + sourceSequences: number[][], + options: EncoderDecoderGenerationOptions +): Promise<{ completions: TokenRollout[]; tokensPerSecond?: number }> { + const { maxTokens, startToken, stopTokens, temperature, useKvCache } = options; + + model.eval(); + + const completions: TokenRollout[] = []; + let lastTPS: number | undefined; + + for (let bi = 0; bi < sourceSequences.length; bi++) { + await weak(async () => { + const sourceTokens = sourceSequences[bi] ?? []; + let seq: number[] = []; + const perStepProbs: number[][] = []; + let stepIndex = 0; + for await (const generationResult of generateStream(model, sourceTokens, { + maxTokens, + startToken, + stopTokens: stopTokens ?? [], + temperature, + useKvCache + })) { + seq = generationResult.sequences[0] ? [...generationResult.sequences[0]] : []; + lastTPS = generationResult.tokensPerSecond ?? lastTPS; + if (generationResult.probs) { + const probsArray = await (await generationResult.probs.to('cpu')).toVec(); + const [_b, v] = generationResult.probs.shape; + const row: number[] = []; + for (let vi = 0; vi < v; vi++) { + row.push(probsArray[0 * v + vi]); + } + perStepProbs.push(row); + } + stepIndex++; + if (typeof maxTokens === 'number' && stepIndex >= maxTokens) break; + } + completions[bi] = { tokenIds: seq, probs: pin(perStepProbs) }; + }); + } + + model.train(); + + return { completions, tokensPerSecond: lastTPS }; +} + +export async function predictEncoderOnlyCompletions( + model: EncoderTransformer | RNNEncoder, + inputs: number[][], + labels: number[][], + options: EncoderOnlyPredictionOptions +): Promise<{ completions: TokenRollout[] }> { + const { attentionMask, temperature } = options; + + const completions = await weak(async () => { + model.eval(); + + const inputsTensor = tensorWrap(inputs); + let mlmLogits: Tensor | null = null; + if (model instanceof EncoderTransformer) { + if (attentionMask) { + const attentionMaskTensor = tensorWrap(attentionMask); + [, , mlmLogits] = model.forward(await inputsTensor.to('gpu'), { + attentionMask: await attentionMaskTensor.to('gpu') + }); + } else { + [, , mlmLogits] = model.forward(await inputsTensor.to('gpu')); + } + } else { + [, mlmLogits] = model.forward(await inputsTensor.to('gpu')); + } + + if (!mlmLogits) { + throw new Error('No mlmLogits returned from encoder-only model'); + } + + let logitsAdj = mlmLogits; + if (typeof temperature === 'number' && temperature > 0) { + logitsAdj = logitsAdj.div(temperature); + } + const probs = logitsAdj.softmax(-1); + + let pred: Tensor; // [B, T] + if (typeof temperature === 'number' && temperature > 0) { + const [B, T, V] = probs.size(); + pred = probs + .view([B * T, V]) + .multinomial(1, { replacement: false }) + .view([B, T]); + } else { + pred = probs.argmax({ dim: -1 }); + } + const predArr = await (await pred.to('cpu')).toVec(); + const [B, T] = pred.size(); + + const completions: TokenRollout[] = []; + for (let bi = 0; bi < B; bi++) { + const inputRow = inputs[bi] ?? []; + const labelsRow = labels[bi] ?? []; + const outRow: number[] = new Array(T); + for (let ti = 0; ti < T; ti++) { + const label = labelsRow[ti]; + const predId = predArr[bi * T + ti]; + outRow[ti] = label !== -100 ? predId : (inputRow[ti] ?? predId); + } + completions.push({ tokenIds: outRow, probs: [] }); + } + + model.train(); + return completions; + }); + + return { completions }; +} diff --git a/examples/piston-train-toy/src/lib/train/visualizer.ts b/examples/piston-train-toy/src/lib/train/visualizer.ts new file mode 100644 index 00000000..a3656dc3 --- /dev/null +++ b/examples/piston-train-toy/src/lib/train/visualizer.ts @@ -0,0 +1,1006 @@ +import { type IRectangle, MaxRectsPacker, PACKING_LOGIC } from 'maxrects-packer'; + +import { type CaptureMatch } from './capture'; + +export type MatchBox = { + matchId: number; + x: number; + y: number; + width: number; + height: number; + tileWidth: number; + tileHeight: number; + gridRows: number; + gridCols: number; + match: CaptureMatch; +}; + +export type MatchStats = { mean: number | null; variance: number | null } | null; + +type PreparedMatch = { + match: CaptureMatch & { buffer: GPUBuffer }; + gridRows: number; + gridCols: number; + tileWidth: number; + tileHeight: number; + width: number; + height: number; + scale: number; +}; + +type PackedRect = IRectangle & { id?: number; matchId: number }; + +const PADDING_PX = 4; +const GLOBAL_PADDING_PX = 3; + +export class Visualizer { + private device: GPUDevice; + private context: GPUCanvasContext | null = null; + private canvas: OffscreenCanvas | null = null; + private canvasFormat: GPUTextureFormat = 'bgra8unorm'; + private storageFormat: GPUTextureFormat = 'rgba8unorm'; + private composeTexture: GPUTexture | null = null; + private composeView: GPUTextureView | null = null; + private blitPipeline: GPURenderPipeline | null = null; + private computePipeline: GPUComputePipeline | null = null; + private reducePipelineStage1: GPUComputePipeline | null = null; + private reducePipelineStage2: GPUComputePipeline | null = null; + private blitBindGroupLayout: GPUBindGroupLayout | null = null; + private blitSampler: GPUSampler | null = null; + private cssLabelPaddingPx: number = 0; + private targetWidth: number = 1; + private targetHeight: number = 1; + private maxTextureDim: number = 8192; + private needsResize: boolean = false; + + constructor(device: GPUDevice) { + this.device = device; + } + + init(canvas: OffscreenCanvas, format?: GPUTextureFormat) { + this.canvas = canvas; + const context = canvas.getContext('webgpu') as unknown as GPUCanvasContext | null; + if (!context) { + throw new Error('OffscreenCanvas WebGPU context not available'); + } + this.context = context; + this.canvasFormat = format ?? navigator.gpu.getPreferredCanvasFormat(); + // Use negotiated device limit (not adapter), to avoid validation errors + this.maxTextureDim = Math.max(1, this.device.limits.maxTextureDimension2D | 0); + + context.configure({ + device: this.device, + format: this.canvasFormat, + alphaMode: 'premultiplied' + }); + + this.recreateComposeTargets(); + this.ensurePipelines(); + } + + setCssLabelPadding(pixels: number) { + this.cssLabelPaddingPx = Math.max(0, Math.floor(pixels || 0)); + } + + resize(width: number) { + if (!this.canvas) return; + // Clamp to device limits to prevent creating oversized textures + const clampedW = Math.max(1, Math.min(width | 0, this.maxTextureDim)); + // We intentionally ignore incoming height and derive it from content during render. + if (this.targetWidth === clampedW) return; + this.targetWidth = clampedW; + this.needsResize = true; + } + + private recreateComposeTargets() { + if (!this.canvas) return; + this.composeTexture?.destroy(); + this.composeTexture = this.device.createTexture({ + size: { width: this.canvas.width, height: this.canvas.height }, + format: this.storageFormat, + usage: + GPUTextureUsage.RENDER_ATTACHMENT | + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_SRC | + GPUTextureUsage.STORAGE_BINDING + }); + this.composeView = this.composeTexture.createView(); + } + + private ensurePipelines() { + if (!this.blitPipeline) { + this.blitBindGroupLayout = this.device.createBindGroupLayout({ + entries: [ + { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } }, + { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } } + ] + }); + const pipelineLayout = this.device.createPipelineLayout({ + bindGroupLayouts: [this.blitBindGroupLayout] + }); + this.blitPipeline = this.device.createRenderPipeline({ + layout: pipelineLayout, + vertex: { + module: this.device.createShaderModule({ code: BLIT_WGSL }), + entryPoint: 'vsMain' + }, + fragment: { + module: this.device.createShaderModule({ code: BLIT_WGSL }), + entryPoint: 'fsMain', + targets: [{ format: this.canvasFormat }] + }, + primitive: { topology: 'triangle-list' } + }); + this.blitSampler = this.device.createSampler({ magFilter: 'nearest', minFilter: 'nearest' }); + } + + if (!this.computePipeline) { + const layout = this.device.createBindGroupLayout({ + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, + { + binding: 1, + visibility: GPUShaderStage.COMPUTE, + storageTexture: { format: this.storageFormat, access: 'write-only' } + }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, + { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } } + ] + }); + const pipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: [layout] }); + this.computePipeline = this.device.createComputePipeline({ + layout: pipelineLayout, + compute: { + module: this.device.createShaderModule({ code: COPY_COMPUTE_WGSL }), + entryPoint: 'main' + } + }); + } + + // Reduction pipelines + if (!this.reducePipelineStage1) { + const layout1 = this.device.createBindGroupLayout({ + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } } + ] + }); + const pl1 = this.device.createPipelineLayout({ bindGroupLayouts: [layout1] }); + this.reducePipelineStage1 = this.device.createComputePipeline({ + layout: pl1, + compute: { + module: this.device.createShaderModule({ code: REDUCE_STAGE1_WGSL }), + entryPoint: 'main' + } + }); + } + + if (!this.reducePipelineStage2) { + const layout2 = this.device.createBindGroupLayout({ + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } } + ] + }); + const pl2 = this.device.createPipelineLayout({ bindGroupLayouts: [layout2] }); + this.reducePipelineStage2 = this.device.createComputePipeline({ + layout: pl2, + compute: { + module: this.device.createShaderModule({ code: REDUCE_STAGE2_WGSL }), + entryPoint: 'main' + } + }); + } + } + + private prepareMatches(matches: CaptureMatch[]): PreparedMatch[] { + const prepared: PreparedMatch[] = []; + for (const match of matches) { + if (!match.tensor) continue; + + const dims = match.type === 'parameter' ? match.shape : match.shape.slice(1); + + let tileWidth = 1; + let tileHeight = 1; + let gridRows = 1; + let gridCols = 1; + + if (dims.length === 0) { + tileWidth = 1; + tileHeight = 1; + gridRows = 1; + gridCols = 1; + } else if (dims.length === 1) { + tileWidth = dims[0]; + tileHeight = 1; + gridRows = 1; + gridCols = 1; + } else if (dims.length === 2) { + tileHeight = dims[0]; + tileWidth = dims[1]; + gridRows = 1; + gridCols = 1; + } else { + // Lay out last two dims as 2D tile, third-from-last horizontally, + // and any remaining leading dims stacked vertically. + tileHeight = dims[dims.length - 2]; + tileWidth = dims[dims.length - 1]; + gridCols = dims[dims.length - 3]; + gridRows = dims.slice(0, Math.max(0, dims.length - 3)).reduce((a, b) => a * b, 1); + } + + const width = gridCols * tileWidth; + const height = gridRows * tileHeight; + const scale = match.source.scale ?? 1.0; + + // Use precomputed buffer if available + const buffer = match.buffer ?? match.tensor.gpuBuffer(); + match.tensor.__pistonDrop(); + if (!buffer) continue; + + prepared.push({ + match: { ...match, buffer } as CaptureMatch & { buffer: GPUBuffer }, + gridRows, + gridCols, + tileWidth, + tileHeight, + width, + height, + scale + }); + } + return prepared; + } + + private layoutMatches(prepared: PreparedMatch[]): MatchBox[] { + const boxes: MatchBox[] = []; + if (prepared.length === 0) return boxes; + + const labelCss = this.cssLabelPaddingPx | 0; + const PAD = PADDING_PX | 0; + const HALF_PAD = Math.floor(PAD / 2); + + // Fix the container width to the current canvas width; height very large so we + // never overflow horizontally. Actual drawing will be clipped to the canvas height. + const canvasW = (this.canvas?.width ?? this.targetWidth ?? 1) | 0; + const containerInnerW = Math.max(1, canvasW - (GLOBAL_PADDING_PX << 1)); + const containerH = 1 << 30; // arbitrarily tall + + // Build batch rectangles for addArray, inflating each rectangle by PADDING_PX/2 on all sides, + // and including the label vertical space above the tile. + const rects: PackedRect[] = []; + const dimsById = new Map>(); + + // Helper to count power-of-two factors for 2-adic rewrapping + const v2 = (n: number): number => { + let c = 0; + let x = Math.max(1, n | 0); + while ((x & 1) === 0) { + x >>= 1; + c++; + } + return c; + }; + + for (const p of prepared) { + const baseTileWidth = p.tileWidth | 0; + const baseTileHeight = p.tileHeight | 0; + const baseRows = p.gridRows | 0; + const baseCols = p.gridCols | 0; + const scaledW0 = Math.ceil(p.width * p.scale); + + // Compute required halving power to fit within inner container width (including padding) + const needFactor = Math.max(1, (scaledW0 + PAD) / containerInnerW); + const nNeeded = Math.max(0, Math.ceil(Math.log2(needFactor))); + + // Capacity from 2-adic factors of tile width and grid columns + const aMax = v2(baseTileWidth); + const bMax = v2(baseCols); + const nCap = aMax + bMax; + const nUse = Math.min(nNeeded, nCap); + const a = Math.min(nUse, aMax); + const b = Math.min(nUse - a, bMax); + + const adjTileWidth = Math.max(1, baseTileWidth >> a); + const adjTileHeight = Math.max(1, baseTileHeight << a); + const adjCols = Math.max(1, baseCols >> b); + const adjRows = Math.max(1, baseRows << b); + + const scaledWidth = Math.ceil(adjCols * adjTileWidth * p.scale); + const scaledHeight = Math.ceil(adjRows * adjTileHeight * p.scale); + + const reqW = Math.max(1, scaledWidth + PAD); + const reqH = Math.max(1, scaledHeight + labelCss + PAD); + const qid = p.match.queryIndex; + rects.push({ + x: 0, + y: 0, + width: reqW, + height: reqH, + // id for caller use; set to queryIndex per request + id: qid, + // preserve original match id for mapping back to dims later + matchId: p.match.matchId + }); + dimsById.set(p.match.matchId, { + width: scaledWidth, + height: scaledHeight, + match: p.match, + tileWidth: adjTileWidth, + tileHeight: adjTileHeight, + gridRows: adjRows, + gridCols: adjCols + }); + } + + // Use MaxRectsPacker to pack within fixed width and very tall height + const packer = new MaxRectsPacker(containerInnerW, containerH, 0, { + smart: false, + pot: true, + square: false, + allowRotation: false, + tag: false, + border: 0, + logic: PACKING_LOGIC.MAX_EDGE + }); + packer.addArray(rects); + const posById = new Map(); + for (const bin of packer.bins) { + for (const rect of bin.rects) { + const mid = rect.matchId; + posById.set(mid, { x: rect.x | 0, y: rect.y | 0 }); + } + } + + // Preserve original order to match prepared[i] with boxes[i] + for (const p of prepared) { + const dims = dimsById.get(p.match.matchId)!; + const pos = posById.get(p.match.matchId); + + const x = GLOBAL_PADDING_PX + ((pos?.x ?? 0) | 0) + HALF_PAD; + const y = GLOBAL_PADDING_PX + ((pos?.y ?? 0) | 0) + HALF_PAD + labelCss; + + boxes.push({ + matchId: p.match.matchId, + match: p.match, + x, + y, + width: dims.width, + height: dims.height, + tileWidth: dims.tileWidth, + tileHeight: dims.tileHeight, + gridRows: dims.gridRows, + gridCols: dims.gridCols + }); + } + + return boxes; + } + + async renderCapture(matches: CaptureMatch[]): Promise<{ + boxes: MatchBox[]; + statsById: Record; + width: number; + height: number; + }> { + if (!this.context || !this.canvas || !this.composeTexture || !this.composeView) + return { boxes: [], statsById: {}, width: 1, height: 1 }; + + // Apply pending resize (width only) before computing layout + if (this.needsResize && this.canvas) { + this.canvas.width = this.targetWidth; + // We delay height adjustment until after computing layout + this.recreateComposeTargets(); + this.needsResize = false; + } + + this.ensurePipelines(); + + // Track transient per-frame buffers to explicitly destroy after GPU work completes + const transientBuffers: GPUBuffer[] = []; + + const prepared = this.prepareMatches(matches); + const boxes = this.layoutMatches(prepared); + + // Derive canvas height from content (max y + height), clamped to device limits + let derivedHeight = 1; + for (const b of boxes) { + const bottom = (b.y | 0) + (b.height | 0); + if (bottom > derivedHeight) derivedHeight = bottom; + } + derivedHeight = Math.max( + 1, + // Add a small global padding at bottom + Math.min((derivedHeight + (GLOBAL_PADDING_PX + PADDING_PX / 2)) | 0, this.maxTextureDim) + ); + if ((this.canvas.height | 0) !== derivedHeight) { + this.canvas.height = derivedHeight; + // Recreate compose targets to match new height + this.recreateComposeTargets(); + } + + const readbacks: { buffer: GPUBuffer; matchId: number }[] = []; + + const encoder = this.device.createCommandEncoder(); + let submissionSucceeded = false; + + try { + // Clear compose target + { + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: this.composeView, + clearValue: { r: 0, g: 0, b: 0, a: 1 }, + loadOp: 'clear', + storeOp: 'store' + } + ] + }); + pass.end(); + } + + // For each match: copy tensor slice to a readable storage buffer, run GPU reduction to + // compute mean/variance, then run compute to paint into compose texture using the computed + // stats + for (let i = 0; i < prepared.length; i++) { + const p = prepared[i]; + const box = boxes[i]; + + const elementCount = Math.max( + 1, + box.gridRows * box.gridCols * box.tileWidth * box.tileHeight + ); + const byteSize = elementCount * 4; + + const readable = this.device.createBuffer({ + size: byteSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST + }); + transientBuffers.push(readable); + + // Copy the selected batch index for non-parameter tensors; parameters ignore batchIndex + const isParameter = p.match.type === 'parameter'; + const batchCount = isParameter ? 1 : Math.max(1, p.match.shape[0] | 0); + const requestedBatchIndex = isParameter ? 0 : (p.match.batchIndex ?? 0); + const clampedBatchIndex = isParameter + ? 0 + : Math.max(0, Math.min(batchCount - 1, requestedBatchIndex | 0)); + const offsetBytes = isParameter ? 0 : (clampedBatchIndex * elementCount * 4) >>> 0; + const maxBytes = Math.max(0, p.match.buffer.size - offsetBytes); + const copyBytes = Math.min(byteSize, maxBytes); + if (copyBytes > 0) { + encoder.copyBufferToBuffer(p.match.buffer, offsetBytes, readable, 0, copyBytes); + } + + // Reduction to compute mean/variance on GPU + const WG_SIZE = 256; + const ELEMENTS_PER_THREAD = 4; + const perGroup = WG_SIZE * ELEMENTS_PER_THREAD; + const numPartials = Math.max(1, Math.ceil(elementCount / perGroup)); + + const partialsBuffer = this.device.createBuffer({ + size: numPartials * 8, + usage: GPUBufferUsage.STORAGE + }); + transientBuffers.push(partialsBuffer); + + const finalStats = this.device.createBuffer({ + size: 12, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC + }); + transientBuffers.push(finalStats); + + const reduceParams = new Uint32Array([ + elementCount >>> 0, + numPartials >>> 0, + WG_SIZE >>> 0, + ELEMENTS_PER_THREAD >>> 0 + ]); + const reduceUniform = this.device.createBuffer({ + size: Math.ceil(reduceParams.byteLength / 16) * 16, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST + }); + transientBuffers.push(reduceUniform); + this.device.queue.writeBuffer( + reduceUniform, + 0, + reduceParams.buffer, + reduceParams.byteOffset, + reduceParams.byteLength + ); + + // Stage 1 reduction + { + const bg = this.device.createBindGroup({ + layout: this.reducePipelineStage1!.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: readable } }, + { binding: 1, resource: { buffer: partialsBuffer } }, + { binding: 2, resource: { buffer: reduceUniform } } + ] + }); + const pass = encoder.beginComputePass(); + pass.setPipeline(this.reducePipelineStage1!); + pass.setBindGroup(0, bg); + pass.dispatchWorkgroups(numPartials); + pass.end(); + } + + // Stage 2 reduction + { + const bg = this.device.createBindGroup({ + layout: this.reducePipelineStage2!.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: partialsBuffer } }, + { binding: 1, resource: { buffer: finalStats } }, + { binding: 2, resource: { buffer: reduceUniform } } + ] + }); + const pass = encoder.beginComputePass(); + pass.setPipeline(this.reducePipelineStage2!); + pass.setBindGroup(0, bg); + pass.dispatchWorkgroups(1); + pass.end(); + } + + // Copy stats to a CPU-readable buffer + const readback = this.device.createBuffer({ + size: 12, + usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST + }); + readbacks.push({ buffer: readback, matchId: p.match.matchId }); + encoder.copyBufferToBuffer(finalStats, 0, readback, 0, 12); + + // Paint compute using computed stats + const scale = Math.fround(p.scale); + const scaleU32 = new Uint32Array(new Float32Array([scale]).buffer)[0]; + const eps = Math.fround(1e-6); + const epsU32 = new Uint32Array(new Float32Array([eps]).buffer)[0]; + const uniformData = new Uint32Array([ + box.x >>> 0, + box.y >>> 0, + box.tileWidth >>> 0, + box.tileHeight >>> 0, + box.gridRows >>> 0, + box.gridCols >>> 0, + scaleU32 >>> 0, + epsU32 >>> 0 + ]); + const uniform = this.device.createBuffer({ + size: Math.ceil(uniformData.byteLength / 16) * 16, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST + }); + transientBuffers.push(uniform); + this.device.queue.writeBuffer( + uniform, + 0, + uniformData.buffer, + uniformData.byteOffset, + uniformData.byteLength + ); + + const bindGroup = this.device.createBindGroup({ + layout: this.computePipeline!.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: readable } }, + { binding: 1, resource: this.composeView as GPUTextureView }, + { binding: 2, resource: { buffer: uniform } }, + { binding: 3, resource: { buffer: finalStats } } + ] + }); + + const pass = encoder.beginComputePass(); + pass.setPipeline(this.computePipeline!); + pass.setBindGroup(0, bindGroup); + const renderWidth = box.width | 0; + const renderHeight = box.height | 0; + // Clip dispatch region to current compose texture bounds to avoid OOB writes + const targetW = this.canvas.width | 0; + const targetH = this.canvas.height | 0; + const clipW = Math.max(0, Math.min(renderWidth, Math.max(0, targetW - box.x))); + const clipH = Math.max(0, Math.min(renderHeight, Math.max(0, targetH - box.y))); + if (clipW <= 0 || clipH <= 0) { + pass.end(); + continue; + } + const dispatchX = Math.ceil(clipW / 8); + const dispatchY = Math.ceil(clipH / 8); + pass.dispatchWorkgroups(dispatchX, dispatchY); + pass.end(); + } + + // Blit compose texture to the current swapchain texture + const currentTexture = this.context.getCurrentTexture(); + const currentView = currentTexture.createView(); + + const blitBindGroup = this.device.createBindGroup({ + layout: this.blitBindGroupLayout!, + entries: [ + { binding: 0, resource: this.composeView as GPUTextureView }, + { binding: 1, resource: this.blitSampler as GPUSampler } + ] + }); + + { + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: currentView, + clearValue: { r: 0, g: 0, b: 0, a: 1 }, + loadOp: 'clear', + storeOp: 'store' + } + ] + }); + pass.setPipeline(this.blitPipeline!); + pass.setBindGroup(0, blitBindGroup); + pass.draw(6, 1, 0, 0); + pass.end(); + } + + this.device.queue.submit([encoder.finish()]); + submissionSucceeded = true; + } catch (_err) { + // If we failed before submitting GPU work, destroy transient buffers immediately + if (!submissionSucceeded) { + for (const buf of transientBuffers) { + try { + buf.destroy(); + } catch (_e) { + /* ignore */ + } + } + } + throw _err; + } + + // Wait for GPU work to complete before mapping readback buffers + await this.device.queue.onSubmittedWorkDone(); + + // Schedule explicit destruction of transient buffers once the GPU has + // finished consuming this submission. This avoids leaking GPU memory + // across frames if GC is delayed. + void this.device.queue.onSubmittedWorkDone().then(() => { + for (const buf of transientBuffers) { + try { + buf.destroy(); + } catch (_e) { + // Ignore destruction errors; resource may already be collected + } + } + }); + + const statsById: Record = {}; + for (const rb of readbacks) { + try { + await rb.buffer.mapAsync(GPUMapMode.READ); + const range = rb.buffer.getMappedRange(); + const f32 = new Float32Array(range.slice(0)); + statsById[rb.matchId] = { mean: f32[0] ?? 0, variance: f32[1] ?? 1, l2: f32[2] ?? 0 }; + rb.buffer.unmap(); + rb.buffer.destroy(); + } catch (_e) { + // Ignore mapping errors for individual buffers + try { + rb.buffer.unmap(); + } catch (__ignore) { + void __ignore; + } + try { + rb.buffer.destroy(); + } catch (__ignore2) { + void __ignore2; + } + } + } + + return { + boxes, + statsById, + width: this.canvas?.width ?? 1, + height: this.canvas?.height ?? 1 + }; + } + + dispose() { + // Destroy long-lived GPU resources and release references + try { + this.composeTexture?.destroy(); + } catch (_e) { + void _e; + } + try { + this.context?.unconfigure(); + } catch (_e) { + void _e; + } + + this.composeTexture = null; + this.composeView = null; + this.blitPipeline = null; + this.computePipeline = null; + this.blitBindGroupLayout = null; + this.blitSampler = null; + this.context = null; + this.canvas = null; + } +} + +const BLIT_WGSL = /* wgsl */ ` +struct VSOut { + @builtin(position) pos : vec4f, + @location(0) uv : vec2f, +}; + +@vertex +fn vsMain(@builtin(vertex_index) vid: u32) -> VSOut { + var positions = array( + vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0), + vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0) + ); + var uvs = array( + vec2f(0.0, 1.0), vec2f(1.0, 1.0), vec2f(0.0, 0.0), + vec2f(0.0, 0.0), vec2f(1.0, 1.0), vec2f(1.0, 0.0) + ); + var out: VSOut; + out.pos = vec4f(positions[vid], 0.0, 1.0); + out.uv = uvs[vid]; + return out; +} + +@group(0) @binding(0) var img : texture_2d; +@group(0) @binding(1) var samp : sampler; + +@fragment +fn fsMain(in: VSOut) -> @location(0) vec4f { + return textureSample(img, samp, in.uv); +} +`; + +// Stage 1: parallel partials of sum and sum of squares +const REDUCE_STAGE1_WGSL = /* wgsl */ ` +@group(0) @binding(0) var src : array; +@group(0) @binding(1) var partials : array>; + +struct Params { + n : u32, + numPartials : u32, + wgSize : u32, + elemsPerThread : u32, +}; + +@group(0) @binding(2) var P : Params; + +var ssum : array; +var ssum2 : array; + +@compute @workgroup_size(256) +fn main(@builtin(local_invocation_id) lid: vec3, @builtin(workgroup_id) wid: vec3) { + let groupIndex = wid.x; + let localIndex = lid.x; + let start = groupIndex * (P.wgSize * P.elemsPerThread) + localIndex; + + var sum : f32 = 0.0; + var sum2 : f32 = 0.0; + for (var i: u32 = 0u; i < P.elemsPerThread; i = i + 1u) { + let idx = start + i * P.wgSize; + if (idx < P.n) { + let v = src[idx]; + sum = sum + v; + sum2 = sum2 + v * v; + } + } + + ssum[localIndex] = sum; + ssum2[localIndex] = sum2; + workgroupBarrier(); + + var offset : u32 = 256u / 2u; + loop { + if (offset == 0u) { break; } + if (localIndex < offset) { + ssum[localIndex] = ssum[localIndex] + ssum[localIndex + offset]; + ssum2[localIndex] = ssum2[localIndex] + ssum2[localIndex + offset]; + } + workgroupBarrier(); + offset = offset / 2u; + } + + if (localIndex == 0u) { + partials[groupIndex] = vec2(ssum[0], ssum2[0]); + } +} +`; + +// Stage 2: finalize mean and variance from partials +const REDUCE_STAGE2_WGSL = /* wgsl */ ` +@group(0) @binding(0) var partials : array>; +@group(0) @binding(1) var outStats : array; + +struct Params { + n : u32, + numPartials : u32, + wgSize : u32, + elemsPerThread : u32, +}; + +@group(0) @binding(2) var P : Params; + +var ssum : array; +var ssum2 : array; + +@compute @workgroup_size(256) +fn main(@builtin(local_invocation_id) lid: vec3) { + let localIndex = lid.x; + var sum : f32 = 0.0; + var sum2 : f32 = 0.0; + for (var i: u32 = localIndex; i < P.numPartials; i = i + 256u) { + let p = partials[i]; + sum = sum + p.x; + sum2 = sum2 + p.y; + } + ssum[localIndex] = sum; + ssum2[localIndex] = sum2; + workgroupBarrier(); + + var offset : u32 = 256u / 2u; + loop { + if (offset == 0u) { break; } + if (localIndex < offset) { + ssum[localIndex] = ssum[localIndex] + ssum[localIndex + offset]; + ssum2[localIndex] = ssum2[localIndex] + ssum2[localIndex + offset]; + } + workgroupBarrier(); + offset = offset / 2u; + } + + if (localIndex == 0u) { + let n = max(1.0, f32(P.n)); + let mean = ssum[0] / n; + let ex2 = ssum2[0] / n; + let variance = max(0.0, ex2 - mean * mean); + let l2 = sqrt(ssum2[0]); + outStats[0] = mean; + outStats[1] = variance; + outStats[2] = l2; + } +} +`; + +const COPY_COMPUTE_WGSL = /* wgsl */ ` +@group(0) @binding(0) var src : array; +@group(0) @binding(1) var outImg : texture_storage_2d; + +struct Uniforms { + originX : u32, + originY : u32, + tileW : u32, + tileH : u32, + gridRows : u32, + gridCols : u32, + scaleBits : u32, + epsBits : u32, +}; + +@group(0) @binding(2) var U : Uniforms; +@group(0) @binding(3) var stats : array; + +fn f32_from_bits(u: u32) -> f32 { + return bitcast(u); +} + +fn gamma_correct(v: vec3) -> vec3 { + let sign = select(vec3(-1.0), vec3(1.0), v >= vec3(0.0)); + let abs_v = abs(v); + + let power_term = sign * (1.055 * pow(abs_v, vec3(1.0/2.4)) - 0.055); + let linear_term = 12.92 * v; + + return select(linear_term, power_term, abs_v > vec3(0.0031308)); +} + +fn oklch_to_srgb(oklch: vec3) -> vec3 { + // Convert OKLCH to OKLab + let L = oklch.x; + let C = oklch.y; + let H = radians(oklch.z); + + let oklab = vec3( + L, + C * cos(H), + C * sin(H) + ); + + // Combined matrices from Python calculation + let oklab_to_lms = mat3x3( + 1.0000000000000000, 1.0000000000000000, 1.0000000000000000, + 0.3963377773761749, -0.1055613458156586, -0.0894841775298119, + 0.2158037573099136, -0.0638541728258133, -1.2914855480194092 + ); + + // Combined xyz_to_rgb * lms_to_xyz matrix + let xyz_rgb_lms = mat3x3( + 4.0767416360759583, -1.2684379732850317, -0.0041960761386756, + -3.3077115392580616, 2.6097573492876887, -0.7034186179359363, + 0.2309699031821043, -0.3413193760026573, 1.7076146940746117 + ); + + // Convert OKLab to LMS + let lms = oklab_to_lms * oklab; + + // Apply cubic transform + let lms_cubic = pow(lms, vec3(3.)); + + // Convert directly to RGB using combined matrix + let rgb_linear = xyz_rgb_lms * lms_cubic; + + // Apply gamma correction + return gamma_correct(rgb_linear); +} + +fn get_color(x: f32) -> vec3 { + let color1_pos = vec3f(0.0, 0.05, 20); + let color2_pos = vec3f(0.62, 0.18, 41); + let color1_neg = vec3f(0, 0.05, 247); + let color2_neg = vec3f(0.62, 0.18, 267); + + // x is now in terms of standard deviations + if (x < 0.0) { + return mix(color1_neg, color2_neg, -x / 2.8); + } else { + return mix(color1_pos, color2_pos, x / 2.8); + } +} + +@compute @workgroup_size(8, 8, 1) +fn main(@builtin(global_invocation_id) gid: vec3) { + let scale = max(0.0001, f32_from_bits(U.scaleBits)); + let eps = f32_from_bits(U.epsBits); + let mean = stats[0u]; + let variance = stats[1u]; + let l2 = stats[2u]; + + // Local pixel within this tile's box + let lx = i32(gid.x); + let ly = i32(gid.y); + + // Map local pixel to logical tensor pixel using scale + let logicalX = i32(floor(f32(lx) / scale)); + let logicalY = i32(floor(f32(ly) / scale)); + + let totalW = i32(U.tileW * U.gridCols); + let totalH = i32(U.tileH * U.gridRows); + + if (logicalX >= totalW || logicalY >= totalH) { + return; + } + + let tileCol = logicalX / i32(U.tileW); + let tileRow = logicalY / i32(U.tileH); + let pxX = logicalX % i32(U.tileW); + let pxY = logicalY % i32(U.tileH); + + let leadingIndex = tileRow * i32(U.gridCols) + tileCol; + let index = leadingIndex * i32(U.tileW) * i32(U.tileH) + pxY * i32(U.tileW) + pxX; + + let v = src[index]; + + // z-score color mapping computed on-GPU: invStd = rsqrt(var + eps) + let invStd = inverseSqrt(max(0.0, variance + eps)); + let z = (v - mean) * invStd; + // let z = v * invStd; + // let zc = clamp(z, -3.0, 3.0) / 3.0; + // let blue = max(0.0, zc); + // let red = max(0.0, -zc); + // let color = vec4(red, 0.0, blue, 1.0); + let color = vec4(oklch_to_srgb(get_color(z)), 1.0); + + // Write to destination at tile origin offset + let outX = U.originX + gid.x; + let outY = U.originY + gid.y; + textureStore(outImg, vec2u(outX, outY), color); +} +`; diff --git a/examples/piston-train-toy/src/lib/workspace/config.svelte.ts b/examples/piston-train-toy/src/lib/workspace/config.svelte.ts new file mode 100644 index 00000000..4b956c79 --- /dev/null +++ b/examples/piston-train-toy/src/lib/workspace/config.svelte.ts @@ -0,0 +1,701 @@ +import type { Config, ModelType } from '$lib/workspace/config'; + +import { buildDataset, DATASET_CONFIG_METADATA } from '$lib/train/data'; +import { getCollatedSampleData } from '$lib/train/data/collate'; +import { TOY_DATASET_CONFIG_DEFAULTS } from '$lib/train/data/toy/config'; +import { calculateBlockSize, calculateVocabSize, createDataloader } from '$lib/train/utils/model'; +import { seededRandom } from '$lib/train/utils/random'; +import { SvelteURL, SvelteURLSearchParams } from 'svelte/reactivity'; + +import { getPresetLayers } from './presets'; +import { getCurrentRun, getLatestRun } from './runs.svelte'; +import { getVisualizationExampleOptions } from './visualizationExamples'; + +export const MODEL_TYPES = [ + 'decoder', + 'encoder', + 'encoder-decoder' +] as const satisfies readonly ModelType[]; + +const CONFIG_DEFAULTS: Config = { + preset: null, + training: { + logSteps: 5, + batchSize: 32, + validation: { + present: true, + valSteps: 100, + batchSize: 8, + temperature: 0.0, + useKvCache: false, + completions: { + present: true, + decodingBatchSize: 1, + amount: 'subset', + subsetSize: 4 + } + }, + limitTraining: { + present: false, + steps: 50_000 + }, + labelSmoothing: { + present: false, + value: 1e-4 + }, + dropout: { + present: false, + embedding: 0.1, + transformer: { + attention: 0.1, + residual: 0.1 + }, + rnn: { + interLayer: 0.1 + } + }, + randomSeed: { + present: true, + value: 'sequence toy' + }, + gradNorm: { + track: true, + errorIfNonfinite: true + }, + clipGradNorm: { + present: false, + value: 1.0 + }, + useWeakTensorReferences: true, + sharedObjectAllocation: false, + cachingEnabled: false, + inplaceSupport: true, + enableVisualization: true, + vramLimitMb: { + present: true, + value: 4096 + }, + restartEverySteps: 1000 + }, + data: { + dataset: 'sort', + trainOnPrompt: false, + maskRatio: 0.15, + specialTokens: { + includeEos: true + }, + datasets: TOY_DATASET_CONFIG_DEFAULTS, + natural: { + contextSize: 32, + vocabSize: 1024 + } + }, + model: { + family: 'transformer', + topology: 'decoder', + layers: 1, + tieEmbeddingsAndLmHead: false, + roundVocabSizeToNearestMultiple: { + present: false, + value: 64 + }, + encoderDecoder: { + encoderLayers: 1, + decoderLayers: 1 + }, + layerNormalization: { + type: 'rmsnorm', + eps: 1e-5, + transformer: { + present: true, + position: 'pre' + }, + rnn: { + withinCell: true, + betweenLayers: false + } + }, + transformer: { + headDim: 16, + initialization: { + present: true, + std: 0.02, + projections: { + attention: { + present: true, + strategy: 'zero' + }, + mlp: { + present: true, + strategy: 'zero' + }, + lmHead: { + present: true, + strategy: 'layer-scaled' + } + } + }, + attention: { + present: true, + nKeyValueHeads: 4, + groupedQueryAttention: { + present: false, + queryHeadsPerKeyValueHead: 2 + }, + gating: { + present: true, + activation: 'sigmoid', + sites: { + afterSdpaOutput: true, + afterValueProjection: false, + afterKeyProjection: false, + afterQueryProjection: false, + afterFinalOutputProjection: false + } + }, + sinks: { + present: false + } + }, + positionalEncoding: { + present: true, + type: 'learned', + alibi: { + maxBias: 8.0 + }, + rope: { + base: 10000.0 + } + }, + normalization: { + qkNorm: { + present: true, + type: 'rmsnorm', + eps: 1e-5 + }, + softcap: { + attention: { + present: false, + value: 30 + }, + logits: { + present: false, + value: 30 + } + } + }, + mlp: { + present: true, + activation: 'relu2', + hiddenExpansionFactor: 4, + variant: 'gated' + } + }, + rnn: { + cellType: 'lstm', + embedding: { + type: 'learned', + learned: { + size: 16 + } + }, + separateHiddenSize: { + present: false, + value: 16 + }, + initialization: { + present: true, + xavierInputColumns: { + present: true, + distribution: 'uniform' + }, + orthogonalRecurrentColumns: true, + perGateOrthogonalBlocks: true, + zeroBiases: true, + gru: { + updateGateBias: { + present: false, + value: 0.0 + } + }, + lstm: { + forgetGateBias: { + present: true, + value: 1.0 + } + } + }, + hiddenStateProjection: { + present: false, + size: 16 + }, + encoder: { + bidirectional: false + }, + encoderDecoderAttention: { + present: false, + type: 'additive', + inputFeedingProjection: true, + multiplicative: { + scaleByInverseSqrtHiddenSize: true + } + } + } + }, + optimizer: { + type: 'Muon', + lr: 1e-3, + weightDecay: { + present: true, + value: 1e-2, + useWeightDecayGroups: true + }, + warmupSteps: { + present: true, + value: 100 + }, + lrScheduler: { + present: true, + type: 'cosine', + stepSchedule: { + stepSize: 100, + gamma: 0.8 + }, + constantSchedule: { + factor: 1 / 3, + totalIters: 100 + }, + cosineAnnealingSchedule: { + tMax: 500, + etaMin: 1e-4 + }, + exponentialSchedule: { + gamma: 0.999 + }, + linearSchedule: { + startFactor: 1.0, + endFactor: 1 / 3, + totalIters: 1000 + } + }, + adam: { + beta1: 0.9, + beta2: 0.999, + eps: 1e-8, + amsgrad: false + }, + sgd: { + momentum: 0.9, + dampening: 0, + nesterov: false + }, + muon: { + momentum: 0.95, + nsSteps: 5, + nesterov: true + } + }, + visualization: { + script: null, + example: 'attention-probabilities', + target: 'validation', + selectedValidation: { + exampleIndex: 0, + tokenIndex: 0 + } + }, + version: 1 +}; + +function computeEffectiveDefaults(presetId: string | null | undefined): Config { + // Start from root defaults + const base = JSON.parse(JSON.stringify(CONFIG_DEFAULTS)) as Config; + if (presetId) { + const layers = getPresetLayers(presetId); + for (const layer of layers) { + mergeDeep( + base as unknown as Record, + layer as unknown as Record + ); + } + base.preset = presetId; + } + return base; +} + +/** + * Parses a value based on the type of the default value in the config. This is not wildly general, + * but it seems to work for the current config. + * @param valueStr - The value to parse. + * @param defaultValue - The default value. + * @returns The parsed value. + */ +function parseValueBasedOnDefault(valueStr: string, defaultValue: unknown): unknown { + if (typeof defaultValue === 'boolean') { + return valueStr.toLowerCase() === 'true'; + } + if (typeof defaultValue === 'number') { + const num = parseFloat(valueStr); + return isNaN(num) ? defaultValue : num; + } + return valueStr; // Default to string if type is not boolean or number +} + +/** + * Builds a config from URL search params. + * @param params - The URL search params. + * @param defaults - The defaults to use if no URL search params are present. + * @returns The config. + */ +function buildConfigFromUrlParams(params: URLSearchParams, defaults: Config): Partial { + const configFromUrl: Record = {}; + + for (const [path, valueStr] of params) { + const keys = path.split('.'); + let currentLevel = configFromUrl; + let currentDefaultsLevel: unknown = defaults; + + try { + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if ( + currentDefaultsLevel === undefined || + typeof currentDefaultsLevel !== 'object' || + currentDefaultsLevel === null + ) { + throw new Error(`Invalid config path from URL: ${path}`); + } + currentDefaultsLevel = (currentDefaultsLevel as Record)[key]; + + if (i < keys.length - 1) { + if ( + !(currentLevel as Record)[key] || + typeof (currentLevel as Record)[key] !== 'object' + ) { + (currentLevel as Record)[key] = {}; + } + currentLevel = (currentLevel as Record)[key] as Record; + } else { + (currentLevel as Record)[key] = parseValueBasedOnDefault( + valueStr, + currentDefaultsLevel + ); + } + } + } catch (e) { + console.warn((e as Error).message); + continue; // Skip this parameter if path is invalid or type mismatch + } + } + return configFromUrl as Partial; +} + +function mergeDeep(target: Record, source: Record) { + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + const sourceVal = source[key]; + let targetKeyAsObject = target[key] as Record; + + if (sourceVal && typeof sourceVal === 'object' && !Array.isArray(sourceVal)) { + if ( + !targetKeyAsObject || + typeof targetKeyAsObject !== 'object' || + Array.isArray(targetKeyAsObject) + ) { + targetKeyAsObject = {}; + target[key] = targetKeyAsObject; + } + mergeDeep(targetKeyAsObject, sourceVal as Record); + } else if (sourceVal !== undefined) { + target[key] = sourceVal; + } + } + } +} + +/** + * Gets the initial config from the URL search params, or the defaults if no URL search params are + * present. + * @returns The initial config. + */ +function getInitialConfig(): Config { + // Start with effective defaults, possibly from URL 'preset' + let base: Config = JSON.parse(JSON.stringify(CONFIG_DEFAULTS)); + if (typeof window !== 'undefined' && window.location && window.URLSearchParams) { + try { + const params = new URLSearchParams(window.location.search); + const presetFromUrl = params.get('preset'); + base = computeEffectiveDefaults(presetFromUrl); + const configOverrides = buildConfigFromUrlParams(params, base); + const initial = JSON.parse(JSON.stringify(base)); + mergeDeep(initial, configOverrides); + return initial; + } catch (e) { + console.error('Error processing config from URL, using defaults:', e); + return JSON.parse(JSON.stringify(CONFIG_DEFAULTS)); + } + } + return base; +} + +export const config = $state(getInitialConfig()); +const configDefaults = $derived(computeEffectiveDefaults(config.preset)); + +/** + * Resets one or more config values to their defaults using dot-separated paths. + */ +export function resetConfigToDefaults(paths: string | string[]) { + const pathList = Array.isArray(paths) ? paths : [paths]; + + for (const path of pathList) { + const defaultValue = getValueAtPath(configDefaults as unknown as Record, path); + if (defaultValue === undefined) { + console.warn(`resetConfigToDefaults: Unknown config path "${path}"`); + continue; + } + // Deep clone to avoid mutating the CONFIG_DEFAULTS reference + const cloned = deepClone(defaultValue); + const ok = setValueAtPath(config as unknown as Record, path, cloned); + if (!ok) { + console.warn(`resetConfigToDefaults: Failed to set value for path "${path}"`); + } + } +} + +function deepClone(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +export function getConfigDefaultValue(path: string): unknown { + const val = getValueAtPath(configDefaults as unknown as Record, path); + return deepClone(val); +} + +export function equalsConfigDefault(path: string): boolean { + const current = getValueAtPath(config as unknown as Record, path); + const def = getValueAtPath(configDefaults as unknown as Record, path); + return valuesDeepEqual(current, def); +} + +function valuesDeepEqual(a: unknown, b: unknown): boolean { + try { + return JSON.stringify(a) === JSON.stringify(b); + } catch { + return a === b; + } +} + +function getValueAtPath(obj: Record, path: string): unknown { + const keys = path.split('.'); + let current: unknown = obj; + for (const key of keys) { + if ( + current === null || + current === undefined || + typeof current !== 'object' || + !(key in (current as Record)) + ) { + return undefined; + } + current = (current as Record)[key]; + } + return current; +} + +function setValueAtPath(target: Record, path: string, value: unknown): boolean { + const keys = path.split('.'); + let current: Record = target; + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + const next = current[key]; + if (next === undefined || next === null || typeof next !== 'object' || Array.isArray(next)) { + // Only create an object if we are not overwriting a non-object path + current[key] = {}; + } + current = current[key] as Record; + } + const lastKey = keys[keys.length - 1]; + current[lastKey] = value as unknown; + return true; +} + +/** + * Flattens only the non-default values from an object by comparing against a defaults object. + * Returns a map of dot-separated paths to stringified values. + */ +function flattenNonDefault( + obj: Record, + defaults: Record, + prefix: string = '' +): Record { + const params: Record = {}; + for (const key in obj) { + if (!Object.prototype.hasOwnProperty.call(obj, key)) continue; + const newPrefix = prefix ? `${prefix}.${key}` : key; + const value = obj[key]; + const defaultValue = (defaults ?? {})[key]; + + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { + const defaultChild = + defaultValue !== null && typeof defaultValue === 'object' && !Array.isArray(defaultValue) + ? (defaultValue as Record) + : ({} as Record); + const nested = flattenNonDefault(value as Record, defaultChild, newPrefix); + Object.assign(params, nested); + } else if (value !== undefined) { + if (defaultValue === undefined || !valuesDeepEqual(value, defaultValue)) { + params[newPrefix] = String(value); + } + } + } + return params; +} + +export function initSharedConfigUrlSync() { + if (typeof window !== 'undefined' && window.history && window.URL) { + $effect(() => { + const configSnapshot = $state.snapshot(config); + const flatParams = flattenNonDefault( + configSnapshot, + configDefaults as unknown as Record + ); + // Always include preset when set, so shared URLs preserve selection + if (configSnapshot.preset) flatParams['preset'] = String(configSnapshot.preset); + const searchParamsString = new SvelteURLSearchParams(flatParams).toString(); + + const currentUrl = new SvelteURL(window.location.href); + currentUrl.search = searchParamsString; // This replaces the entire search string + + // Only call replaceState if the URL actually changed to avoid flooding history + if (window.location.href !== currentUrl.href) { + window.history.replaceState({}, '', currentUrl.toString()); + } + }); + } +} + +export function setPreset(presetId: string) { + if (config.preset === presetId) return; + const next = computeEffectiveDefaults(presetId); + // Replace config fields in-place because we've made config a const and I don't want to figure + // that out right now. + config['preset'] = next.preset; + config['training'] = deepClone(next.training); + config['data'] = deepClone(next.data); + config['model'] = deepClone(next.model); + config['optimizer'] = deepClone(next.optimizer); + config['visualization'] = deepClone(next.visualization); + config['version'] = next.version; + validateConfig(); +} + +function ensureDatasetSupportsModelType() { + const datasetKey = config.data.dataset as keyof typeof DATASET_CONFIG_METADATA; + const meta = DATASET_CONFIG_METADATA[datasetKey]; + const allModels = MODEL_TYPES; + const supported = [ + ...('supportsModelTypes' in meta ? meta.supportsModelTypes : allModels) + ].toSorted(); + const isModelSupported = [...supported].includes(config.model.topology); + if (!isModelSupported) { + console.debug(`ensureDatasetSupportsModelType: setting model topology to ${supported[0]}`); + config.model.topology = supported[0] as ModelType; + } +} + +function datasetFromConfig(config: Config) { + ensureDatasetSupportsModelType(); + const generator = seededRandom(0); + const dataset = buildDataset(config, generator, 'train'); + const [dataloader, collateFn] = createDataloader(config, dataset, generator, null); + const blockSize = calculateBlockSize(config, dataloader); + const vocabSize = calculateVocabSize(config, dataset); + + const collatedData = getCollatedSampleData(dataset, collateFn, 4); + + return { + dataset, + vocabSize, + blockSize, + tokenizer: dataset.tokenizer, + sampleData: collatedData.then((data) => { + const firstSample = data.collated[0]; + return { + hasPrompt: 'prompt' in firstSample && (firstSample.prompt?.length ?? 0) > 0, + samples: data.samples, + collated: data.collated + }; + }) + }; +} + +const currentDataset = $derived(datasetFromConfig(config)); + +export function getCurrentDataset() { + return currentDataset; +} + +const currentRunDataset = $derived.by(() => { + const currentRun = getCurrentRun(); + return currentRun?.config ? datasetFromConfig(currentRun.config) : null; +}); + +export function getCurrentRunDataset() { + return currentRunDataset; +} + +const latestRunDataset = $derived.by(() => { + const latestRun = getLatestRun(); + return latestRun?.config ? datasetFromConfig(latestRun.config) : null; +}); + +export function getLatestRunDataset() { + return latestRunDataset; +} + +const transformerHiddenSize = $derived( + config.model.transformer.headDim * + (config.model.transformer.attention.groupedQueryAttention.present + ? config.model.transformer.attention.groupedQueryAttention.queryHeadsPerKeyValueHead + : 1) * + config.model.transformer.attention.nKeyValueHeads +); + +const mlpIntermediateSize = $derived( + transformerHiddenSize * config.model.transformer.mlp.hiddenExpansionFactor +); + +export function getHiddenSize() { + return transformerHiddenSize; +} + +export function getMlpIntermediateSize() { + return mlpIntermediateSize; +} + +export function validateConfig() { + // There are a few things that can still slip through the cracks, so we deal with those here. + + if ( + config.visualization.selectedValidation.exampleIndex >= config.training.validation.batchSize + ) { + config.visualization.selectedValidation.exampleIndex = 0; + } + + if ( + config.visualization.example !== 'custom' && + !getVisualizationExampleOptions(config).some((e) => e.value === config.visualization.example) + ) { + config.visualization.example = 'all-activations'; + } + + if ( + config.training.validation.completions.present && + config.training.validation.completions.amount === 'subset' && + config.training.validation.completions.subsetSize > config.training.validation.batchSize + ) { + config.training.validation.completions.subsetSize = config.training.validation.batchSize; + } + + ensureDatasetSupportsModelType(); +} diff --git a/examples/piston-train-toy/src/lib/workspace/config.ts b/examples/piston-train-toy/src/lib/workspace/config.ts new file mode 100644 index 00000000..a9391268 --- /dev/null +++ b/examples/piston-train-toy/src/lib/workspace/config.ts @@ -0,0 +1,704 @@ +import type { + ConstantConfig, + CosineAnnealingConfig, + ExponentialConfig, + LinearConfig, + StepConfig +} from '@piston-ml/piston-web'; + +export type { + AdditionConfig, + ModularAdditionConfig, + RepeatConfig, + SlapjackConfig, + SortConfig, + TwoSumConfig, + ZerosConfig +} from '../train/data/toy/config'; + +import type { DATASET_CONFIG_DEFAULTS } from '$lib/train/data'; + +import { ADDITION_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/addition'; +import { COPY_MEMORY_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/copyMemory'; +import { DYCK_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/dyck'; +import { MARKED_ADDITION_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/markedAddition'; +import { MODULAR_ADDITION_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/modularAddition'; +import { PARITY_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/parity'; +import { RANDOM_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/random'; +import { REPEAT_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/repeat'; +import { REVERSE_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/reverse'; +import { SLAPJACK_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/slapjack'; +import { SORT_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/sort'; +import { TEMPORAL_ORDER_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/temporalOrder'; +import { TWO_SUM_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/twoSum'; +import { ZEROS_SHORT_DESCRIPTIONS } from '$lib/train/data/toy/zeros'; + +import type { TOY_DATASET_CONFIG_DEFAULTS } from '../train/data/toy/config'; + +export interface DropoutConfig { + present: boolean; + embedding: number; + transformer: { + attention: number; + residual: number; + }; + rnn: { + interLayer: number; + }; +} + +export type TransformerDropoutConfig = Omit; +export type RNNDropoutConfig = Omit; + +export interface ValidationCompletionsConfig { + present: boolean; + decodingBatchSize: number; + amount: 'all' | 'subset'; + subsetSize: number; +} + +export interface ValidationConfig { + present: boolean; + valSteps: number; + batchSize: number; + temperature: number; + completions: ValidationCompletionsConfig; + useKvCache: boolean; +} + +export interface TrainingConfig { + logSteps: number; + limitTraining: { + present: boolean; + steps: number; + }; + batchSize: number; + dropout: DropoutConfig; + validation: ValidationConfig; + labelSmoothing: { + present: boolean; + value: number; + }; + randomSeed: { + present: boolean; + value: string; + }; + vramLimitMb: { + present: boolean; + value: number; + }; + gradNorm: { + track: boolean; + errorIfNonfinite: boolean; + }; + clipGradNorm: { + present: boolean; + value: number; + }; + useWeakTensorReferences: boolean; + sharedObjectAllocation: boolean; + cachingEnabled: boolean; + inplaceSupport: boolean; + enableVisualization: boolean; + restartEverySteps: number; +} + +export interface TransformerAttentionConfig { + present: boolean; + nKeyValueHeads: number; + groupedQueryAttention: { + present: boolean; + queryHeadsPerKeyValueHead: number; + }; + gating: AttentionGatingConfig; + sinks: { + present: boolean; + }; +} + +export interface DataConfig { + dataset: keyof typeof DATASET_CONFIG_DEFAULTS; + trainOnPrompt: boolean; + datasets: typeof TOY_DATASET_CONFIG_DEFAULTS; + maskRatio: number; + specialTokens: { + includeEos: boolean; + }; + natural: { + contextSize: number; + vocabSize: 'char' | 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768 | 65536; + }; +} + +export interface AlibiConfig { + maxBias: number; +} + +export interface RoPEConfig { + base: number; +} + +export interface PositionEncodingConfig { + present: boolean; + type: 'sinusoidal' | 'learned' | 'rope' | 'alibi'; + alibi: AlibiConfig; + rope: RoPEConfig; +} + +export type LayerNormPosition = 'pre' | 'post'; +export type NormalizationType = 'layernorm' | 'rmsnorm'; + +export interface LayerNormalizationConfig { + type: NormalizationType; + eps: number; + transformer: { + present: boolean; + position: LayerNormPosition; + }; + rnn: { + withinCell: boolean; + betweenLayers: boolean; + }; +} + +export interface QKNormConfig { + present: boolean; + type: NormalizationType; + eps: number; +} + +export interface SoftcapConfig { + attention: { + present: boolean; + value: number; + }; + logits: { + present: boolean; + value: number; + }; +} + +export interface TransformerNormalizationConfig { + qkNorm: QKNormConfig; + softcap: SoftcapConfig; +} + +export type Activation = 'relu' | 'relu2' | 'gelu' | 'silu' | 'sigmoid' | 'swiglu' | 'tanh'; + +export interface AttentionGatingSitesConfig { + afterSdpaOutput: boolean; + afterValueProjection: boolean; + afterKeyProjection: boolean; + afterQueryProjection: boolean; + afterFinalOutputProjection: boolean; +} + +export interface AttentionGatingConfig { + present: boolean; + activation: Activation; + sites: AttentionGatingSitesConfig; +} + +export interface MLPConfig { + present: boolean; + activation: Activation; + hiddenExpansionFactor: number; + variant: 'standard' | 'gated'; +} + +export type ModelType = 'decoder' | 'encoder' | 'encoder-decoder'; +export type ModelFamily = 'transformer' | 'rnn'; + +export interface MultiplicativeRNNAttentionConfig { + scaleByInverseSqrtHiddenSize: boolean; +} + +export interface RNNAttentionConfig { + present: boolean; + type: 'additive' | 'multiplicative'; + inputFeedingProjection: boolean; + multiplicative: MultiplicativeRNNAttentionConfig; +} + +export interface RNNHiddenStateProjectionConfig { + present: boolean; + size: number; +} + +export interface RNNEmbeddingConfig { + type: 'learned' | 'one-hot'; + learned: { + size: number; + }; +} + +export interface RNNConfig { + cellType: 'lstm' | 'gru' | 'rnn'; + // RNN token embedding configuration + embedding: RNNEmbeddingConfig; + separateHiddenSize: { + present: boolean; + value: number; + }; + initialization: RNNInitializationConfig; + hiddenStateProjection: RNNHiddenStateProjectionConfig; + encoder: { + bidirectional: boolean; + }; + encoderDecoderAttention: RNNAttentionConfig; +} + +export type ProjectionInitializationStrategy = 'layer-scaled' | 'zero'; +export interface ProjectionInitializationConfig { + present: boolean; + strategy: ProjectionInitializationStrategy; +} + +export interface TransformerInitializationConfig { + present: boolean; + std: number; + projections: { + attention: ProjectionInitializationConfig; + mlp: ProjectionInitializationConfig; + lmHead: ProjectionInitializationConfig; + }; +} + +export interface LSTMInitializationConfig { + forgetGateBias: { + present: boolean; + value: number; + }; +} + +export type XavierInitializationDistribution = 'uniform' | 'normal'; + +export interface RNNInitializationConfig { + present: boolean; + xavierInputColumns: { + present: boolean; + distribution: XavierInitializationDistribution; + }; + orthogonalRecurrentColumns: boolean; + perGateOrthogonalBlocks: boolean; + zeroBiases: boolean; + gru: { + updateGateBias: { + present: boolean; + value: number; + }; + }; + lstm: { + forgetGateBias: { + present: boolean; + value: number; + }; + }; +} + +export interface TransformerConfig { + headDim: number; + attention: TransformerAttentionConfig; + positionalEncoding: PositionEncodingConfig; + initialization: TransformerInitializationConfig; + normalization: TransformerNormalizationConfig; + mlp: MLPConfig; +} + +export interface ModelConfig { + family: ModelFamily; + topology: ModelType; + layers: number; + tieEmbeddingsAndLmHead: boolean; + roundVocabSizeToNearestMultiple: { + present: boolean; + value: number; + }; + encoderDecoder: { + decoderLayers: number; + encoderLayers: number; + }; + layerNormalization: LayerNormalizationConfig; + transformer: TransformerConfig; + rnn: RNNConfig; +} + +export interface OptimizerConfig { + type: 'AdamW' | 'Adam' | 'SGD' | 'Muon'; + lr: number; + weightDecay: { + present: boolean; + value: number; + useWeightDecayGroups: boolean; + }; + warmupSteps: { present: boolean; value: number }; + lrScheduler: { + present: boolean; + type: string; + stepSchedule: StepConfig; + constantSchedule: ConstantConfig; + cosineAnnealingSchedule: CosineAnnealingConfig; + exponentialSchedule: ExponentialConfig; + linearSchedule: LinearConfig; + }; + adam: { + beta1: number; + beta2: number; + eps: number; + amsgrad: boolean; + }; + sgd: { + dampening: number; + momentum: number; + nesterov: boolean; + }; + muon: { + nsSteps: number; + momentum: number; + nesterov: boolean; + }; +} + +export interface VisualizationConfig { + // If null, the effective script comes from the selected example + script: string | null; + // Selected example id or "custom" + example: string; + // current training step or the selected validation example + target: 'train' | 'validation'; + selectedValidation: { + exampleIndex: number; + tokenIndex: number; + }; +} + +export interface Config { + version: number; + // Currently selected layered preset id; null means no preset + preset: string | null; + training: TrainingConfig; + data: DataConfig; + model: ModelConfig; + optimizer: OptimizerConfig; + visualization: VisualizationConfig; +} + +export type ConfigItemDescription = + | { + shortName: string; + } + | string + | [string, number] + | null; + +type ReplaceValues = T extends object ? { [K in keyof T]: ReplaceValues } : V; + +export type ConfigValues = ReplaceValues; + +export const CONFIG_DESCRIPTIONS: ConfigValues = { + // No default preset; shown as a top-level selector only + preset: null, + training: { + logSteps: 'log steps', + batchSize: 'batch', + clipGradNorm: { + present: 'clip grad norm', + value: 'clip grad norm' + }, + validation: { + present: 'val', + valSteps: 'val steps', + batchSize: 'val size', + temperature: 'val temp', + useKvCache: 'val kv cache', + completions: { + present: 'completions', + decodingBatchSize: 'completions batch', + amount: 'completions amount strategy', + subsetSize: 'completions subset' + } + }, + limitTraining: { + present: 'limit train', + steps: 'max steps' + }, + labelSmoothing: { + present: 'smoothing', + value: 'smoothing' + }, + dropout: { + present: 'dropout', + embedding: 'dropout emb', + transformer: { + attention: 'dropout attn', + residual: 'dropout resid' + }, + rnn: { + interLayer: 'dropout rnn' + } + }, + randomSeed: { + present: 'seed', + value: 'seed' + }, + gradNorm: { + track: 'track grad norm', + errorIfNonfinite: 'error nonfinite' + }, + useWeakTensorReferences: 'weak tensor refs', + sharedObjectAllocation: 'shared objs', + cachingEnabled: 'caching', + inplaceSupport: 'inplace', + enableVisualization: 'viz', + vramLimitMb: { + present: 'vram lim', + value: 'vram lim' + }, + restartEverySteps: 'restart steps' + }, + data: { + dataset: 'dataset', + trainOnPrompt: 'train prompt', + maskRatio: 'mask ratio', + specialTokens: { + includeEos: 'eos' + }, + datasets: { + addition: ADDITION_SHORT_DESCRIPTIONS, + 'copy-memory': COPY_MEMORY_SHORT_DESCRIPTIONS, + dyck: DYCK_SHORT_DESCRIPTIONS, + elman: {}, + 'marked-addition': MARKED_ADDITION_SHORT_DESCRIPTIONS, + 'modular-addition': MODULAR_ADDITION_SHORT_DESCRIPTIONS, + parity: PARITY_SHORT_DESCRIPTIONS, + random: RANDOM_SHORT_DESCRIPTIONS, + repeat: REPEAT_SHORT_DESCRIPTIONS, + reverse: REVERSE_SHORT_DESCRIPTIONS, + slapjack: SLAPJACK_SHORT_DESCRIPTIONS, + sort: SORT_SHORT_DESCRIPTIONS, + 'temporal-order': TEMPORAL_ORDER_SHORT_DESCRIPTIONS, + 'two-sum': TWO_SUM_SHORT_DESCRIPTIONS, + zeros: ZEROS_SHORT_DESCRIPTIONS + }, + // datasets: TOY_DATASET_CONFIG_DEFAULTS, + natural: { + contextSize: 'ctx', + vocabSize: 'vocab' + } + }, + model: { + family: 'family', + topology: 'topology', + layers: 'layers', + tieEmbeddingsAndLmHead: 'tie emb + lm head', + roundVocabSizeToNearestMultiple: { + present: 'vocab mult', + value: 'vocab mult' + }, + encoderDecoder: { + encoderLayers: 'n enc', + decoderLayers: 'n dec' + }, + layerNormalization: { + type: 'ln', + eps: 'eps', + transformer: { + present: 'ln', + position: 'ln pos' + }, + rnn: { + withinCell: 'ln in cell', + betweenLayers: 'ln between' + } + }, + transformer: { + headDim: 'head dim', + initialization: { + present: 'init', + std: 'init std', + projections: { + attention: { + present: 'init attn', + strategy: 'init attn' + }, + mlp: { + present: 'init mlp', + strategy: 'init mlp' + }, + lmHead: { + present: 'init lmhead', + strategy: 'init lmhead' + } + } + }, + attention: { + present: 'attn', + nKeyValueHeads: 'n attn kv', + groupedQueryAttention: { + present: 'gqa', + queryHeadsPerKeyValueHead: 'n attn q/kv' + }, + gating: { + present: 'attn gate', + activation: 'attn gate act', + sites: { + afterSdpaOutput: 'gate@sdpa', + afterValueProjection: 'gate@value', + afterKeyProjection: 'gate@key', + afterQueryProjection: 'gate@query', + afterFinalOutputProjection: 'gate@proj' + } + }, + sinks: { + present: 'attn sinks' + } + }, + positionalEncoding: { + present: 'pos enc', + type: 'pos type', + alibi: { + maxBias: 'alibi max bias' + }, + rope: { + base: 'rope base' + } + }, + normalization: { + qkNorm: { + present: 'qk norm', + type: 'qk norm type', + eps: 'qk norm eps' + }, + softcap: { + attention: { + present: 'softcap attn', + value: 'softcap attn' + }, + logits: { + present: 'softcap logits', + value: 'softcap logits' + } + } + }, + mlp: { + present: 'mlp', + activation: 'mlp act', + hiddenExpansionFactor: 'mlp factor', + variant: 'mlp mode' + } + }, + rnn: { + cellType: 'cell', + embedding: { + type: 'cell emb', + learned: { + size: 'cell emb' + } + }, + separateHiddenSize: { + present: 'cell hid.', + value: 'cell hid.' + }, + initialization: { + present: 'init', + xavierInputColumns: { + present: 'xavier in. cols', + distribution: 'xavier in. dist' + }, + orthogonalRecurrentColumns: 'ortho cols', + perGateOrthogonalBlocks: 'ortho blocks', + zeroBiases: 'zero biases', + gru: { + updateGateBias: { + present: 'gru update gate bias', + value: 'gru update gate bias' + } + }, + lstm: { + forgetGateBias: { + present: 'forget gate bias', + value: 'forget gate bias' + } + } + }, + hiddenStateProjection: { + present: 'hid. proj', + size: 'hid. proj' + }, + encoder: { + bidirectional: 'enc bidir' + }, + encoderDecoderAttention: { + present: 'enc-dec attn', + type: 'enc-dec attn', + inputFeedingProjection: 'enc-dec proj', + multiplicative: { + scaleByInverseSqrtHiddenSize: 'x 1/sqrt(hid.)' + } + } + } + }, + optimizer: { + type: 'optim', + lr: 'lr', + weightDecay: { + present: 'decay', + value: 'decay', + useWeightDecayGroups: 'decay groups' + }, + warmupSteps: { + present: 'warmup', + value: 'warmup steps' + }, + lrScheduler: { + present: 'lr sched', + type: 'lr sched', + stepSchedule: { + stepSize: 'sched step', + gamma: 'sched gamma' + }, + constantSchedule: { + factor: 'const lr factor', + totalIters: 'const lr total' + }, + cosineAnnealingSchedule: { + tMax: 'cos lr tmax', + etaMin: 'cos lr eta min' + }, + exponentialSchedule: { + gamma: 'exp lr gamma' + }, + linearSchedule: { + startFactor: 'lin lr start', + endFactor: 'lin lr end', + totalIters: 'lin lr total' + } + }, + adam: { + beta1: 'adam beta1', + beta2: 'adam beta2', + eps: 'adam eps', + amsgrad: 'adam ams' + }, + sgd: { + momentum: 'sgd moment', + dampening: 'sgd damp', + nesterov: 'sgd nester' + }, + muon: { + momentum: 'muon moment', + nsSteps: 'muon nssteps', + nesterov: 'muon nester' + } + }, + visualization: { + script: null, + example: null, + target: null, + selectedValidation: { + exampleIndex: null, + tokenIndex: null + } + }, + version: null +}; diff --git a/examples/piston-train-toy/src/lib/workspace/localStorage.svelte.ts b/examples/piston-train-toy/src/lib/workspace/localStorage.svelte.ts new file mode 100644 index 00000000..5422c14f --- /dev/null +++ b/examples/piston-train-toy/src/lib/workspace/localStorage.svelte.ts @@ -0,0 +1,98 @@ +import { tick } from 'svelte'; + +export class LocalStorage { + #key: string; + #version = $state(0); + #listeners = 0; + #value: T | undefined; + + #handler = (e: StorageEvent) => { + if (e.storageArea !== localStorage) return; + if (e.key !== this.#key) return; + + this.#version += 1; + }; + + constructor(key: string, initial?: T) { + this.#key = key; + this.#value = initial; + + if (typeof localStorage !== 'undefined') { + if (localStorage.getItem(key) == null) { + localStorage.setItem(key, JSON.stringify(initial)); + } + } + } + + get current() { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this.#version; + + const localStorageItem = + typeof localStorage !== 'undefined' ? localStorage.getItem(this.#key) : null; + const root = localStorageItem !== null ? JSON.parse(localStorageItem) : this.#value; + + const proxies = new WeakMap(); + + const proxy = (value: unknown) => { + if (typeof value !== 'object' || value === null) { + return value; + } + + let p = proxies.get(value); + + if (!p) { + p = new Proxy(value, { + get: (target, property) => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this.#version; + return proxy(Reflect.get(target, property)); + }, + set: (target, property, value) => { + this.#version += 1; + Reflect.set(target, property, value); + + if (typeof localStorage !== 'undefined') { + localStorage.setItem(this.#key, JSON.stringify(root)); + } + + return true; + } + }); + + proxies.set(value, p); + } + + return p; + }; + + if ($effect.tracking()) { + $effect(() => { + if (this.#listeners === 0) { + window.addEventListener('storage', this.#handler); + } + + this.#listeners += 1; + + return () => { + tick().then(() => { + this.#listeners -= 1; + if (this.#listeners === 0) { + window.removeEventListener('storage', this.#handler); + } + }); + }; + }); + } + + return proxy(root); + } + + set current(value) { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(this.#key, JSON.stringify(value)); + } + + this.#version += 1; + } +} diff --git a/examples/piston-train-toy/src/lib/workspace/presets.ts b/examples/piston-train-toy/src/lib/workspace/presets.ts new file mode 100644 index 00000000..6b3fa432 --- /dev/null +++ b/examples/piston-train-toy/src/lib/workspace/presets.ts @@ -0,0 +1,289 @@ +import type { Config } from './config'; + +type DeepPartial = + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + T extends Function + ? T + : T extends Array + ? Array> + : T extends object + ? { [K in keyof T]?: DeepPartial } + : T; + +export type PresetDefinition = { + label: string; + // Layers are merged in order on top of root defaults + layers: Array>; + // If true, this preset is not shown in the preset selector but can be used as a layer + hidden?: boolean; +}; + +export const PRESET_DEFINITIONS: Record = { + 'transformer-toy-base': { + label: 'Transformer Toy (base)', + hidden: true, + layers: [ + { + model: { + family: 'transformer', + topology: 'encoder-decoder' + }, + training: { + validation: { + batchSize: 6, + completions: { + present: true, + decodingBatchSize: 6, + amount: 'all' + } + } + }, + optimizer: { + type: 'Muon', + lr: 1e-3 + } + } + ] + }, + 'encoder-toy-base': { + label: 'Encoder Toy (base)', + hidden: true, + layers: [ + { + model: { + family: 'transformer', + topology: 'encoder', + layers: 2 + }, + training: { + validation: { + batchSize: 6, + completions: { + present: true, + decodingBatchSize: 6, + amount: 'all' + } + } + }, + optimizer: { + lr: 1e-5, + lrScheduler: { + present: false, + cosineAnnealingSchedule: { + etaMin: 5e-6 + } + } + }, + data: { + trainOnPrompt: false + } + } + ] + }, + + 'sort-characters': { + label: 'Toy: Sort Characters', + layers: [{ preset: 'transformer-toy-base' }, { data: { dataset: 'sort' } }] + }, + 'reverse-sequence': { + label: 'Toy: Reverse Sequence', + layers: [{ preset: 'transformer-toy-base' }, { data: { dataset: 'reverse' } }] + }, + 'two-sum': { + label: 'Toy: Two Sum', + layers: [{ preset: 'transformer-toy-base' }, { data: { dataset: 'two-sum' } }] + }, + 'dyck-encoder': { + label: 'Toy: Dyck (Encoder)', + layers: [{ preset: 'encoder-toy-base' }, { data: { dataset: 'dyck' } }] + }, + + tinystories: { + label: 'TinyStories with ~15M parameters', + layers: [ + { + preset: 'transformer-toy-base', + model: { + family: 'transformer', + topology: 'decoder', + layers: 6, + transformer: { + headDim: 32, + attention: { + nKeyValueHeads: 10 + } + } + }, + data: { + trainOnPrompt: false, + natural: { + vocabSize: 8192 + } + }, + optimizer: { + lr: 1e-4, + lrScheduler: { + type: 'cosine', + cosineAnnealingSchedule: { + etaMin: 1e-5 + } + } + }, + training: { + logSteps: 20, + validation: { + batchSize: 16, + valSteps: 500, + completions: { + present: true, + decodingBatchSize: 1, + amount: 'subset', + subsetSize: 1 + } + } + } + }, + { + preset: 'tinystories', + data: { + dataset: 'tinystories' + } + } + ] + }, + + fineweb: { + label: 'FineWeb with ~GPT-2 sized model ⚠️', + layers: [ + { + preset: 'fineweb', + data: { + dataset: 'fineweb', + natural: { + vocabSize: 32_768, + contextSize: 64 + } + }, + model: { + family: 'transformer', + topology: 'decoder', + layers: 12, + transformer: { + headDim: 64, + attention: { + nKeyValueHeads: 12, + gating: { + present: false + } + }, + mlp: { + present: true, + activation: 'gelu', + hiddenExpansionFactor: 4, + variant: 'standard' + }, + normalization: { + qkNorm: { + present: false + } + }, + initialization: { + present: true, + std: 0.02, + projections: { + attention: { + strategy: 'layer-scaled' + }, + mlp: { + strategy: 'layer-scaled' + }, + lmHead: { + strategy: 'layer-scaled' + } + } + } + }, + layerNormalization: { + type: 'layernorm', + eps: 1e-5, + transformer: { + present: true, + position: 'pre' + } + } + }, + optimizer: { + lr: 1e-4, + lrScheduler: { + type: 'cosine', + cosineAnnealingSchedule: { + etaMin: 1e-5 + } + } + }, + training: { + enableVisualization: false, + logSteps: 20, + batchSize: 32, + dropout: { + present: true, + embedding: 0.1, + transformer: { + attention: 0.1, + residual: 0.1 + } + }, + vramLimitMb: { present: true, value: 32_768 }, + validation: { + valSteps: 500, + batchSize: 16, + temperature: 0.0, + completions: { + present: false + } + } + } + } + ] + } +}; + +export function getPresetOptions(): Array<{ value: string; text: string }> { + return Object.entries(PRESET_DEFINITIONS) + .filter(([, def]) => !def.hidden) + .map(([id, def]) => ({ value: id, text: def.label })); +} + +function expandPresetLayers( + presetId: string, + visiting: Set = new Set() +): Array> { + const def = PRESET_DEFINITIONS[presetId]; + if (!def) return []; + if (visiting.has(presetId)) return []; + visiting.add(presetId); + + const expanded = []; + for (const layer of def.layers) { + const maybePreset = layer.preset; + if (typeof maybePreset === 'string' && PRESET_DEFINITIONS[maybePreset]) { + if (!visiting.has(maybePreset)) { + expanded.push(...expandPresetLayers(maybePreset, visiting)); + } + const { preset: _omit, ...rest } = layer; + if (Object.keys(rest).length > 0) { + expanded.push(rest); + } + } else { + expanded.push(layer); + } + } + visiting.delete(presetId); + return expanded; +} + +export function getPresetLayers(presetId: string | null | undefined): Array> { + if (!presetId) return []; + const def = PRESET_DEFINITIONS[presetId]; + return def ? expandPresetLayers(presetId) : []; +} diff --git a/examples/piston-train-toy/src/lib/workspace/runs.svelte.ts b/examples/piston-train-toy/src/lib/workspace/runs.svelte.ts new file mode 100644 index 00000000..bdd99377 --- /dev/null +++ b/examples/piston-train-toy/src/lib/workspace/runs.svelte.ts @@ -0,0 +1,439 @@ +import type { CaptureStep } from '$lib/train/capture'; +import type { ValidationStep } from '$lib/train/validation'; + +import { generateMemorableName } from '$lib/workspace/utils'; +import { SvelteMap, SvelteSet } from 'svelte/reactivity'; + +import { type Config, CONFIG_DESCRIPTIONS } from './config'; + +export type BaseStepData = { step: number }; + +export type Point = BaseStepData & { y: number }; + +export type TokenRollout = { + tokenIds: number[]; + probs: number[][]; +}; + +export type VisualizationStep = BaseStepData & { + type: 'visualization'; +}; + +export type StepData = Point | ValidationStep | CaptureStep; + +export type MetricData = { + metricName: string; + data: StepData[]; +}; + +export type RunData = { + runId: string; + color: string; + config: Config; + metrics: SvelteMap; + step: number; + lastUpdated: number; + createdAt: number; + diffSummary: string; +}; + +export type RunMeta = { + runId: string | null; + config: Config | null; +}; + +// A palette of distinct colors for runs +const RUN_COLORS = ['#f5493b', '#dfa300', '#3bbc4a', '#00b7c0', '#4475f6', '#cc5cc5']; + +export const PREFIX_BOOSTS: Record = { data: 10 }; + +type DiffItem = { + label: string; + path: string[]; + display: string; + boost: number; +}; + +function pathToString(path: ReadonlyArray): string { + return path.join('.'); +} + +function getAtPath(obj: unknown, path: ReadonlyArray): unknown { + let cur = obj; + for (const key of path) { + if (cur == null || typeof cur !== 'object') return undefined; + cur = (cur as Record)[key]; + } + return cur; +} + +function getDescriptor(path: ReadonlyArray): { label: string | null; itemBoost?: number } { + let cur = CONFIG_DESCRIPTIONS as unknown; + + for (const key of path) { + if (cur == null || typeof cur !== 'object') return { label: null }; + cur = (cur as Record)[key]; + } + if (cur == null) return { label: null }; + if (Array.isArray(cur)) { + const [shortName, boost] = cur; + return { label: shortName, itemBoost: typeof boost === 'number' ? boost : undefined }; + } + if (typeof cur === 'string') return { label: cur }; + if (typeof cur === 'object' && 'shortName' in cur && typeof cur.shortName === 'string') { + return { label: cur.shortName }; + } + return { label: null }; +} + +function maxPrefixBoost(path: ReadonlyArray, boosts: Record): number { + let maxB = 0; + for (let i = 1; i <= path.length; i++) { + const pref = path.slice(0, i).join('.'); + const b = boosts[pref]; + if (typeof b === 'number' && b > maxB) maxB = b; + } + return maxB; +} + +function orderOfMagnitude(n: number): number { + if (n === 0) return 0; + return Math.floor(Math.log10(Math.abs(n))); +} + +function formatScientific(n: number, significantDigits = 1): string { + if (n === 0) return '0'; + const s = n.toExponential(Math.max(0, significantDigits - 1)); + const [mant, expRaw] = s.split('e'); + const mantTrim = mant + .replace(/\.0+$/, '') + .replace(/(\.[0-9]*?)0+$/, '$1') + .replace(/\.$/, ''); + const exp = String(parseInt(expRaw, 10)); + return `${mantTrim}e${exp}`; +} + +function formatNumberDiff(a: number, b: number): string { + const bothSmallInts = + Number.isInteger(a) && Number.isInteger(b) && Math.abs(a) < 100 && Math.abs(b) < 100; + if (bothSmallInts) return `${a}→${b}`; + + // If signs differ or either is non-integer, prefer concise scientific form + const expA = orderOfMagnitude(a); + const expB = orderOfMagnitude(b); + const bothInts = Number.isInteger(a) && Number.isInteger(b); + const base = Math.pow(10, expA); + if ( + bothInts && + Math.sign(a) >= 0 && + Math.sign(b) >= 0 && + expA === expB && + // Only use suffix form for small changes + Math.abs(b - a) < base + ) { + const lead = Math.floor(a / base); + const suffixA = a - lead * base; + const suffixB = b - lead * base; + return `${lead}e${expA}+(${suffixA}→${suffixB})`; + } + const sa = formatScientific(a, 1); + const sb = formatScientific(b, 1); + return `${sa}⥵${sb}`; +} + +function formatValue(v: unknown): string { + if (typeof v === 'number') return formatScientific(v, 1); + if (typeof v === 'string') return v; + if (typeof v === 'boolean') return v ? 'true' : 'false'; + return String(v); +} + +function comparePrimitive(a: unknown, b: unknown): boolean { + // Strict equality suffices for primitives we expect here + return a === b; +} + +function collectLeafPaths(obj: unknown, prefix: string[] = []): string[][] { + const out: string[][] = []; + if (obj == null || typeof obj !== 'object') return out; + for (const key of Object.keys(obj)) { + const nextPath = [...prefix, key]; + const val = (obj as Record)[key]; + if (val != null && typeof val === 'object') { + // Only traverse objects that are not arrays + if (!Array.isArray(val)) out.push(...collectLeafPaths(val, nextPath)); + } else { + out.push(nextPath); + } + } + return out; +} + +export function describeConfigDiff( + prev: Config | null, + curr: Config, + opts?: { topK?: number; prefixBoosts?: Record } +): string { + const topK = opts?.topK ?? 3; + const boosts = opts?.prefixBoosts ?? PREFIX_BOOSTS; + if (!prev) return 'initial experiment'; + + const prevPaths = collectLeafPaths(prev); + const currPaths = collectLeafPaths(curr); + const allKey = new SvelteSet(); + for (const p of prevPaths) allKey.add(pathToString(p)); + for (const p of currPaths) allKey.add(pathToString(p)); + + const diffs: DiffItem[] = []; + for (const key of allKey) { + const path = key.split('.'); + // if (shouldSkipPath(path)) continue; + const { label, itemBoost } = getDescriptor(path); + if (!label) continue; + const a = getAtPath(prev, path); + const b = getAtPath(curr, path); + if (comparePrimitive(a, b)) continue; + + const prefixB = maxPrefixBoost(path, boosts); + let effBoost = prefixB; + if (typeof itemBoost === 'number') { + if (itemBoost < prefixB) { + console.warn( + `item boost lower than parent prefix; path=${key} itemBoost=${itemBoost} parentMax=${prefixB}` + ); + } + effBoost = Math.max(effBoost, itemBoost); + } + + let display: string; + if (typeof a === 'boolean' && typeof b === 'boolean') { + display = b ? `+${label}` : `-${label}`; + } else if (typeof a === 'number' && typeof b === 'number') { + display = `${label}:${formatNumberDiff(a, b)}`; + } else { + display = `${label}:${formatValue(a)}→${formatValue(b)}`; + } + + diffs.push({ label, path, display, boost: effBoost }); + } + + diffs.sort((x, y) => { + if (y.boost !== x.boost) return y.boost - x.boost; + return x.label.localeCompare(y.label); + }); + + if (diffs.length === 0) return 'no changes'; + const top = diffs.slice(0, topK).map((d) => d.display); + const rest = diffs.length - top.length; + return rest > 0 ? `${top.join(', ')}, etc ${rest} more` : top.join(', '); +} + +export const runsMap = new SvelteMap(); +export const runCounter = $state({ current: 0 }); +export const currentRun = $state<{ current: RunMeta | null }>({ + current: null +}); + +export function getRuns(): ReadonlyArray { + return [...runsMap.values()].sort((a, b) => a.runId.localeCompare(b.runId)); +} + +export function getAllMetricNames(): ReadonlyArray { + const names = new SvelteSet(); + for (const run of runsMap.values()) { + for (const metricName of run.metrics.keys()) { + names.add(metricName); + } + } + return [...names].sort(); +} + +/** + * Gets all metric names from the last n runs. + * @param n - The number of recent runs to consider + * @returns Array of unique metric names sorted alphabetically + */ +export function getMetricNamesFromLastNRuns(n: number): ReadonlyArray { + const names = new SvelteSet(); + const recentRuns = getLastNRuns(n); + for (const run of recentRuns) { + for (const metricName of run.metrics.keys()) { + names.add(metricName); + } + } + return [...names].sort(); +} + +export function getCurrentRun(): RunMeta | null { + return currentRun.current; +} + +export function getLatestRun(): RunMeta | null { + if (currentRun.current !== null) { + return currentRun.current; + } + + const allRuns = [...runsMap.values()]; + allRuns.sort((a, b) => b.lastUpdated - a.lastUpdated); + return allRuns[0] ?? null; +} + +/** + * Gets the last n runs sorted by most recently updated, ensuring the current run is always + * included. + */ +export function getLastNRuns(n: number): ReadonlyArray { + const allRuns = [...runsMap.values()]; + + // Sort by lastUpdated descending (most recent first) + allRuns.sort((a, b) => b.lastUpdated - a.lastUpdated); + + // If we have fewer runs than requested, return all of them + if (allRuns.length <= n) { + return allRuns; + } + + return allRuns.slice(0, n); +} + +export function clearPastRuns(): void { + const keepRunId = currentRun.current; + if (keepRunId === null) { + // No current run tracked; clear everything + runsMap.clear(); + return; + } + for (const runId of [...runsMap.keys()]) { + if (runId !== keepRunId.runId) { + runsMap.delete(runId); + } + } +} + +export function newRun(config: Config, id?: string): RunData { + if (id && runsMap.has(id)) { + throw new Error(`Run with id ${id} already exists`); + } + + const runId = id ?? generateMemorableName(runCounter.current); + const color = RUN_COLORS[runCounter.current % RUN_COLORS.length]; + const now = Date.now(); + // Find baseline as immediately-previous-by-creation + const existingRuns = [...runsMap.values()]; + existingRuns.sort((a, b) => (a.createdAt ?? a.lastUpdated) - (b.createdAt ?? b.lastUpdated)); + const prevRun = existingRuns.length > 0 ? existingRuns[existingRuns.length - 1] : undefined; + const diffSummary = describeConfigDiff(prevRun?.config ?? null, config, { + topK: 3, + prefixBoosts: PREFIX_BOOSTS + }); + + const run = { + runId: runId, + config, + color: color, + metrics: new SvelteMap(), + step: 0, + lastUpdated: now, + createdAt: now, + diffSummary + }; + runsMap.set(runId, run); + runCounter.current += 1; + currentRun.current = { runId: runId, config: config }; + return run; +} + +export function endRun() { + currentRun.current = null; +} + +/** + * Logs metric data for a specific step in a run. + */ +export function log( + runId: string, + data: { [metricName: string]: Omit }, + { step }: { step?: number } = {} +): void { + const run = runsMap.get(runId); + const determinedStep = step !== undefined ? step : (runsMap.get(runId)?.step ?? 0); + + // Create run if it doesn't exist + if (!run) { + throw new Error(`Run with id ${runId} does not exist`); + } + + const currentStep = determinedStep; + + // Update metrics for the specified step + for (const [metricName, value] of Object.entries(data)) { + let metric = run.metrics.get(metricName); + if (!metric) { + metric = { + metricName, + data: [] + }; + run.metrics.set(metricName, metric); + } + + let stepData: StepData; + if (typeof value === 'number') { + stepData = { step: currentStep, y: value }; + } else if ('matches' in value) { + stepData = { step: currentStep, ...value } as CaptureStep; + } else { + stepData = { step: currentStep, ...value } as ValidationStep; + } + + const updatedMetric = { + ...metric, + data: [...metric.data, stepData].sort((a: StepData, b: StepData) => a.step - b.step) + }; + + run.metrics.set(metricName, updatedMetric); + } + + // Update step counter + if (step === undefined) { + run.step += 1; + } else if (step > run.step) { + run.step = step; + } + + run.lastUpdated = Date.now(); + runsMap.set(runId, run); +} + +export type LogFn = typeof log; + +export function resetWorkspace(): void { + runsMap.clear(); + runCounter.current = 0; + currentRun.current = null; + console.debug('[workspaceState] Reset.'); +} + +// Group metrics by prefix (everything before the first '/') +export function getMetricGroups(metricNames: ReadonlyArray): Record { + const groups: Record = {}; + + const allMetricNames = metricNames; + + allMetricNames.forEach((metricName) => { + // For now we're just special-casing this, but we might want to bring it into the metrics view + // anyway + if (metricName === 'visualization/matches') { + return; + } + + const parts = metricName.split('/'); + const groupName = parts.length > 1 ? parts[0] : 'default'; + + if (!groups[groupName]) { + groups[groupName] = []; + } + groups[groupName].push(metricName); + }); + + return groups; +} diff --git a/examples/piston-train-toy/src/lib/workspace/ui.svelte.ts b/examples/piston-train-toy/src/lib/workspace/ui.svelte.ts new file mode 100644 index 00000000..8d2f89cf --- /dev/null +++ b/examples/piston-train-toy/src/lib/workspace/ui.svelte.ts @@ -0,0 +1,302 @@ +import { config } from './config.svelte'; +import { LocalStorage } from './localStorage.svelte'; +import { newRun } from './runs.svelte'; +import { + trainingState, + workerPauseTraining, + workerReady, + workerRequestSave, + workerResumeTraining, + workerStartTraining, + workerStep, + workerStopTraining +} from './workers.svelte'; + +export const isMobile = $state({ current: false }); +export const activeTab: { current: 'about' | 'metrics' } = $state({ + current: 'about' +}); +export const hasWebGPU = $state({ current: false }); +export const browserInfo: { + current: { + type: + | 'chrome' + | 'edge' + | 'brave' + | 'arc' + | 'opera' + | 'vivaldi' + | 'safari' + | 'firefox' + | 'unknown'; + platform: 'ios' | 'macos' | 'windows' | 'android' | 'linux' | 'other'; + }; +} = $state({ + current: { type: 'unknown', platform: 'other' } +}); + +export const setupUI = () => { + // Check for WebGPU support + hasWebGPU.current = 'gpu' in navigator; + + // Browser/platform detection (best-effort; UA-CH not universally available yet) + const ua = navigator.userAgent.toLowerCase(); + const vendor = navigator.vendor?.toLowerCase?.() ?? ''; + + // Platform + let platform: 'ios' | 'macos' | 'windows' | 'android' | 'linux' | 'other' = 'other'; + if (/iphone|ipad|ipod/.test(ua)) platform = 'ios'; + else if (/macintosh|mac os x/.test(ua)) platform = 'macos'; + else if (/windows nt/.test(ua)) platform = 'windows'; + else if (/android/.test(ua)) platform = 'android'; + else if (/linux/.test(ua)) platform = 'linux'; + + // Chromium-family checks + // Distinguish some popular Chromium variants before generic Chrome + let type: + | 'chrome' + | 'edge' + | 'brave' + | 'arc' + | 'opera' + | 'vivaldi' + | 'safari' + | 'firefox' + | 'unknown' = 'unknown'; + if (/edg\//.test(ua)) type = 'edge'; + else if (/vivaldi/.test(ua)) type = 'vivaldi'; + else if (/opr\//.test(ua)) type = 'opera'; + else if (/brave/.test(ua)) type = 'brave'; + else if (/arc\//.test(ua)) type = 'arc'; + else if (/firefox/.test(ua)) type = 'firefox'; + else if (/safari/.test(ua) && /apple/.test(vendor) && !/chrome|crios|android/.test(ua)) + type = 'safari'; + else if (/chrome|crios/.test(ua)) type = 'chrome'; + + browserInfo.current = { type, platform }; + + const mediaQuery = window.matchMedia('(min-width: 40rem)'); + isMobile.current = !mediaQuery.matches; + + // Set configOpen based on media query if not already set by user + if (configOpen.current === null) { + configOpen.current = mediaQuery.matches; + } + + // Listen for changes in screen size + const handleMediaChange = (e: MediaQueryListEvent) => { + isMobile.current = !e.matches; + + // If switching to mobile and config is open, close it and reset tab + if (isMobile.current && configOpen.current) { + configOpen.current = false; + activeTab.current = 'about'; + } + }; + + mediaQuery.addEventListener('change', handleMediaChange); + + return () => { + mediaQuery.removeEventListener('change', handleMediaChange); + }; +}; + +// Function to handle tab selection with mobile behavior +export function selectTab(tabName: 'about' | 'metrics') { + activeTab.current = tabName; + if (isMobile.current && configOpen.current) { + configOpen.current = false; + } +} + +let flashVramLimit = $state(false); + +export function triggerVramLimitFlash() { + controlSectionsOpen.current.training = true; + + // Scroll to GPU memory limit after a brief delay to allow section to open + setTimeout(() => { + const trainingVramLimitElement = document.getElementById('training-vram-limit'); + flashVramLimit = true; + if (trainingVramLimitElement) { + trainingVramLimitElement.scrollIntoView({ + behavior: 'instant', + block: 'center' + }); + trainingVramLimitElement.classList.add('error-flash'); + setTimeout(() => { + trainingVramLimitElement.classList.remove('error-flash'); + flashVramLimit = false; + }, 1000); + } + }, 100); +} + +export function getFlashVramLimit() { + return flashVramLimit; +} + +let showLowDiversityDatasetError = $state(false); + +export function triggerLowDiversityDatasetError() { + controlSectionsOpen.current.task = true; + showLowDiversityDatasetError = true; + + // Scroll to GPU memory limit after a brief delay to allow section to open + setTimeout(() => { + const lowDiversityDatasetErrorElement = document.getElementById('low-diversity-dataset-error'); + if (lowDiversityDatasetErrorElement) { + lowDiversityDatasetErrorElement.scrollIntoView({ + behavior: 'instant', + block: 'center' + }); + } + }, 100); +} + +export function getShowLowDiversityDatasetError() { + return showLowDiversityDatasetError; +} + +export function resetLowDiversityDatasetError() { + showLowDiversityDatasetError = false; +} + +const iconStrokeWidth = $derived(isMobile ? 2 : 2.5); + +export function getIconStrokeWidth() { + return iconStrokeWidth; +} + +// Initialize sectionsOpen from localStorage or use defaults +export const controlSectionsOpen = new LocalStorage('controlSectionsOpen', { + runs: true, + training: true, + task: true, + model: true, + optimizer: true, + advanced: false +}); + +export function toggleControlSection(sectionName: keyof typeof controlSectionsOpen.current) { + controlSectionsOpen.current[sectionName] = !controlSectionsOpen.current[sectionName]; +} + +export const metricsSectionsOpen = new LocalStorage('metricsSectionsOpen', {}); + +export function toggleMetricsSection(sectionName: string) { + metricsSectionsOpen.current[sectionName] = !(metricsSectionsOpen.current[sectionName] ?? true); +} + +export const maxCompletions = new LocalStorage('maxCompletions', 4); + +export function setMaxCompletions(value: number) { + maxCompletions.current = value; +} + +// Visibility state for per-metric charts (user overrides only) +export const metricVisibility = new LocalStorage('metricVisibility', {}); + +// Initialize configOpen from localStorage with no default (null means use media query) +export const configOpen = new LocalStorage('configOpen', null); + +export const tourState = new LocalStorage<{ + startedExperiment: boolean; + restartedExperiment: boolean; +}>('tourState', { + startedExperiment: false, + restartedExperiment: false +}); + +export function openConfigAndScrollToControl( + controlId: string, + sectionsToOpen: Array, + { useLabelFor = false }: { useLabelFor?: boolean } = {} +) { + // Ensure left panel is open + configOpen.current = true; + // Open requested sections + for (const s of sectionsToOpen) { + controlSectionsOpen.current[s] = true; + } + // Scroll after a brief delay to allow layout to settle + setTimeout(() => { + const el = useLabelFor + ? document.querySelector(`label[for="${controlId}"]`) + : document.getElementById(controlId); + if (el) { + el.scrollIntoView({ behavior: 'instant', block: 'center' }); + // Manage manual focus class + const prev = document.querySelector('.is-focused'); + if (prev && prev !== el) prev.classList.remove('is-focused'); + el.classList.add('is-focused'); + } + }, 100); +} + +export function switchToMetrics() { + selectTab('metrics'); +} + +export function toggleConfig() { + configOpen.current = !configOpen.current; +} + +export function saveModel() { + workerRequestSave(); +} + +// Function to start training +export function startTraining() { + if (trainingState.current !== 'stopped' || !workerReady.current) return; + + trainingState.current = 'training'; + newRun(JSON.parse(JSON.stringify(config))); + + if (isMobile.current && configOpen.current) { + configOpen.current = false; + } + + // We don't want to wrench them away from the visualize tab, but if they're + // running an experiment, we want it to look like something is happening. + if (!tourState.current.startedExperiment || activeTab.current === 'about') { + switchToMetrics(); + } + + if (!tourState.current.startedExperiment) { + tourState.current.startedExperiment = true; + } + + if (getShowLowDiversityDatasetError()) { + resetLowDiversityDatasetError(); + } + + workerStartTraining(); +} + +// Function to stop training +export async function stopTraining() { + await workerStopTraining(); +} + +export function togglePause() { + if (trainingState.current === 'stopped') return; + if (trainingState.current === 'training') { + workerPauseTraining(); + } else { + workerResumeTraining(); + } +} + +export function stepForward() { + if (trainingState.current === 'stopped') return; + workerStep(); +} + +export async function restartTraining() { + await stopTraining(); + startTraining(); + if (!tourState.current.restartedExperiment) { + tourState.current.restartedExperiment = true; + } +} diff --git a/examples/piston-train-toy/src/lib/workspace/utils.ts b/examples/piston-train-toy/src/lib/workspace/utils.ts new file mode 100644 index 00000000..9ad244c5 --- /dev/null +++ b/examples/piston-train-toy/src/lib/workspace/utils.ts @@ -0,0 +1,39 @@ +import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator'; + +export function generateMemorableName(number: number) { + // by default uniqueNamesGenerator picks one word per dictionary + const twoWord = uniqueNamesGenerator({ + dictionaries: [adjectives, animals], + separator: '-', + style: 'lowerCase', + length: 2 + }); + return `${twoWord}-${number}`; +} + +/** + * Returns a new array sorted so that any items whose key is found in `priorityKeys` + * appear first in the specified order, followed by the remaining items sorted by + * their key alphabetically. + * + * Example: + * sortWithPriority(['b', 'a', 'c'], (x) => x, ['c']) -> ['c', 'a', 'b'] + */ +export function sortWithPriority( + items: ReadonlyArray, + getKey: (item: T) => string, + priorityKeys: ReadonlyArray +): T[] { + const priorityIndex = new Map(); + for (let i = 0; i < priorityKeys.length; i++) { + priorityIndex.set(priorityKeys[i], i); + } + return [...items].sort((a, b) => { + const ka = getKey(a); + const kb = getKey(b); + const ia = priorityIndex.has(ka) ? (priorityIndex.get(ka) as number) : Number.POSITIVE_INFINITY; + const ib = priorityIndex.has(kb) ? (priorityIndex.get(kb) as number) : Number.POSITIVE_INFINITY; + if (ia !== ib) return ia - ib; + return ka.localeCompare(kb); + }); +} diff --git a/examples/piston-train-toy/src/lib/workspace/visualizationExamples.ts b/examples/piston-train-toy/src/lib/workspace/visualizationExamples.ts new file mode 100644 index 00000000..4a33d359 --- /dev/null +++ b/examples/piston-train-toy/src/lib/workspace/visualizationExamples.ts @@ -0,0 +1,135 @@ +import type { Config } from './config'; + +export type VisualizationExample = { + label: string; + script: string; + predicate?: (config: Config) => boolean; +}; + +// Kitchen Sink example content taken from previous default, kept verbatim +const KITCHEN_SINK = `// Kitchen-sink Capture Query Language example + +// +// Retained gradient of first two heads of attention probabilities: +// +// 1. index 0 in the in the ModuleList named \`layer\`—the first decoder layer… +// 2. …with any descendant where the module class name matches the regex ./.*Attention/ (note the +// preceding \`.\`), not necessarily immediately… +// 3. …\`@\` ends the module selector and begins selecting operations… +// 4. …with Softmax immediately following WhereCond… +// 5. …where we call :grad(ient) to get the gradient… +// 6. …labeling the result with :label("Self-Attention Gradients")… +// 7. …using :scale(5) to scale the result by 5x… +// 8. …and we only want the first two heads, skipping the batch dimension. +layer[0] ./.*Attention/ @ WhereCond + Softmax :grad :label("self-attention $\\sigma$ gradients") :scale(5) [:,:2] + +// +// Positive values of input of every MLP module: +// +// 1. .MLP selects all modules with the class name \`MLP\`. +// 2. We can filter the captured value with JavaScript. In this case, the input +// to the module is a list of arguments, so we return the first argument before +// selecting for positive values. The input is passed in as the value \`it\`: +.MLP :input :scale(3) :label("mlp input ≥ 0") |{ + const tensor = it[0]; + return tensor.where(tensor.ge(0), 0); +} + +// +// Wildcard selector for every bias parameter +// +// +* #bias :label("all biases") :scale(3)`; + +const ATTENTION_PROBABILITIES = `./.*Attention/ @ Softmax :label("attention probabilities") :scale(5)`; +const ATTENTION_ACTIVATIONS = `layer[0] ./.*Attention/ @ * :label("attention activations") :scale(2)`; +const ATTENTION_GRADIENTS = `layer[0] ./.*Attention/ @ * :grad :label("attention gradients") :scale(2)`; +const ATTENTION_PARAMETERS = `layer[0] ./.*Attention/ * #* :label("attention weights (descendants)") :scale(2) +layer[0] ./.*Attention/ #* :label("attention weights") :scale(2)`; + +const MLP_ACTIVATIONS = `layer[0] .MLP @ * :label("mlp activations") :scale(2)`; + +const ATTENTION_PREDICATE = (config: Config) => { + return config.model.family === 'transformer'; +}; + +export const VISUALIZATION_EXAMPLES: Record = { + 'attention-probabilities': { + label: 'Attention Probabilities', + script: ATTENTION_PROBABILITIES, + predicate: ATTENTION_PREDICATE + }, + 'attention-activations': { + label: 'Attention Activations', + script: ATTENTION_ACTIVATIONS, + predicate: ATTENTION_PREDICATE + }, + 'attention-gradients': { + label: 'Attention Gradients', + script: ATTENTION_GRADIENTS, + predicate: ATTENTION_PREDICATE + }, + 'attention-parameters': { + label: 'Attention Parameters', + script: ATTENTION_PARAMETERS, + predicate: ATTENTION_PREDICATE + }, + 'mlp activations': { + label: 'MLP Activations', + script: MLP_ACTIVATIONS, + predicate: ATTENTION_PREDICATE + }, + 'kitchen-sink': { label: 'Kitchen Sink', script: KITCHEN_SINK, predicate: ATTENTION_PREDICATE }, + 'all-activations': { + label: 'All Activations', + script: '* @ * :scale(3)' + }, + 'all-gradients': { + label: 'All Gradients', + script: '* @ * :grad :scale(3)' + }, + 'all-parameters': { + label: 'All Parameters', + script: '* # * :scale(3)' + } +}; + +export const VISUALIZATION_EXAMPLES_BY_SCRIPT = new Map( + Object.entries(VISUALIZATION_EXAMPLES).map(([id, example]) => [ + example.script, + { ...example, id } + ]) +); + +const DEFAULT_VISUALIZATION_EXAMPLE = VISUALIZATION_EXAMPLES['attention-probabilities']; + +export function getVisualizationExampleById(id: string | null | undefined): VisualizationExample { + if (!id) return DEFAULT_VISUALIZATION_EXAMPLE; + return VISUALIZATION_EXAMPLES[id] ?? DEFAULT_VISUALIZATION_EXAMPLE; +} + +export function findExampleIdMatchingScript(script: string | null | undefined): string | null { + if (script == null) return null; + const match = VISUALIZATION_EXAMPLES_BY_SCRIPT.get(script); + return match ? match.id : null; +} + +export function getEffectiveVisualizationScript( + exampleId: string | null | undefined, + customScript: string | null | undefined +): string { + return exampleId === 'custom' + ? (customScript ?? '') + : getVisualizationExampleById(exampleId).script; +} + +export function getVisualizationExampleOptions( + config: Config +): Array<{ value: string; text: string }> { + return [ + ...Object.entries(VISUALIZATION_EXAMPLES) + .filter(([_, e]) => (e.predicate ? e.predicate(config) : true)) + .map(([id, e]) => ({ value: id, text: e.label })), + { value: 'custom', text: 'Custom' } + ]; +} diff --git a/examples/piston-train-toy/src/lib/workspace/workers.svelte.ts b/examples/piston-train-toy/src/lib/workspace/workers.svelte.ts new file mode 100644 index 00000000..9da9efbd --- /dev/null +++ b/examples/piston-train-toy/src/lib/workspace/workers.svelte.ts @@ -0,0 +1,503 @@ +import type { MatchBox } from '$lib/train/visualizer'; +import type { IndexState, TensorQuery } from '@piston-ml/piston-web'; + +import { config } from './config.svelte'; +import { currentRun, log } from './runs.svelte'; +import { triggerLowDiversityDatasetError, triggerVramLimitFlash } from './ui.svelte'; + +// Train state +let trainWorker: Worker | null = $state(null); +export const workerReady = $state({ current: false }); +export const workerVersion = $state({ current: 0 }); +export const trainingState = $state<{ current: 'training' | 'paused' | 'stopped' }>({ + current: 'stopped' +}); + +// Visualizer layout state +let visualizerBoxes = $state(null); +let visualizerQueries = $state(null); +let visualizerLayoutStep = $state(null); +let visualizerLayoutRunId = $state(null); +let visualizerRenderWidth = $state(null); +let visualizerRenderHeight = $state(null); + +// UA memory measurement state (main thread only) +let uaMemoryInterval: ReturnType | null = null; +let lastUAMemoryBytes: number | null = null; + +let screenWakeLock: WakeLockSentinel | null = null; + +async function acquireScreenWakeLock() { + // Only attempt in browser/secure contexts that support it + if (typeof navigator === 'undefined' || !('wakeLock' in navigator)) return; + try { + // Request a screen wake lock + screenWakeLock = await navigator.wakeLock.request('screen'); + // Ensure our local reference is cleared if the system revokes the lock + screenWakeLock?.addEventListener?.('release', () => { + screenWakeLock = null; + }); + } catch (err) { + console.warn('Screen Wake Lock request failed:', err); + } +} + +async function releaseScreenWakeLock() { + if (!screenWakeLock) return; + try { + await screenWakeLock.release(); + } catch (err) { + console.warn('Screen Wake Lock release failed:', err); + } finally { + screenWakeLock = null; + } +} + +export async function initializeWorker() { + return new Promise((resolve, reject) => { + try { + // Create the dedicated module worker + // eslint-disable-next-line svelte/prefer-svelte-reactivity + trainWorker = new Worker(new URL('$lib/train/moduleWorker.ts', import.meta.url), { + type: 'module', + name: 'moduleWorker' + }); + + console.log('[Main] Module worker created successfully.'); + + // Set up UA memory measurement (immediate + interval) on main thread (only once) + if (!uaMemoryInterval) { + const measure = ( + performance as Performance & { + measureUserAgentSpecificMemory?: () => Promise<{ bytes: number }>; + } + ).measureUserAgentSpecificMemory; + if (typeof measure === 'function') { + const measureAndStore = async () => { + try { + const { bytes } = await ( + performance as Performance & { + measureUserAgentSpecificMemory?: () => Promise<{ bytes: number }>; + } + ).measureUserAgentSpecificMemory!(); + if (typeof bytes === 'number' && Number.isFinite(bytes)) { + lastUAMemoryBytes = bytes; + } + } catch (err) { + console.warn('Error measuring UA memory:', err); + // Ignore measurement errors + } + }; + // Immediate measurement so first log can include it + void measureAndStore(); + uaMemoryInterval = setInterval(() => { + void measureAndStore(); + }, 10_000); + } else { + console.debug( + 'performance.measureUserAgentSpecificMemory is not available; skipping UA memory interval' + ); + } + } + + trainWorker.onmessage = (event) => { + const { type, ...data } = event.data; + + switch (type) { + case 'visualizer.ready': + console.log('[Main] Visualizer ready'); + break; + case 'capture': + visualizerQueries = data.queries as TensorQuery[]; + visualizerBoxes = data.boxes as MatchBox[]; + visualizerLayoutStep = data.step as number; + visualizerLayoutRunId = data.runId as string; + visualizerRenderWidth = data.width as number; + visualizerRenderHeight = data.height as number; + break; + case 'visualizer.error': + console.error('[Main] Visualizer error:', data.message); + break; + case 'ready': + console.log('[Main] Worker is ready'); + resolve(); + workerReady.current = true; + workerVersion.current += 1; + break; + + case 'metrics': { + // Handle training metric logs + if (!data.runId || !data.data) { + console.error('[Main] Invalid metrics data:', data); + return; + } + const step = data.metadata?.step as number | undefined; + const combinedMetrics: Record> = {}; + for (const [metricName, value] of Object.entries(data.data)) { + combinedMetrics[metricName] = value as number | Record; + } + if (lastUAMemoryBytes !== null) { + combinedMetrics['allocation/cpu_memory_mb'] = lastUAMemoryBytes / (1024 * 1024); + lastUAMemoryBytes = null; + } + log(data.runId, combinedMetrics, { step }); + break; + } + case 'complete': + console.log(`[Main] Training completed for run ${data.runId}`); + trainingState.current = 'stopped'; + currentRun.current = null; + void releaseScreenWakeLock(); + break; + + case 'restart': + console.log(`[Main] Worker requested restart for run ${data.runId}`); + const buffer = data.buffer as Uint8Array; + const runId = data.runId as string; + // Terminate and recreate worker + trainWorker?.terminate(); + workerReady.current = false; + // Ensure training state reflects continuity across restart + trainingState.current = 'training'; + initializeWorker().then(() => { + // Send start with resumeFrom to resume same run id + trainWorker!.postMessage({ + type: 'start', + data: { runId, config: $state.snapshot(config), resumeFrom: buffer } + }); + }); + break; + + case 'checkpoint': + let url; + const uint8array = data.buffer as Uint8Array | undefined; + if (uint8array && typeof URL !== 'undefined' && typeof Blob !== 'undefined') { + const blob = new Blob([uint8array.buffer as ArrayBuffer], { + type: 'application/octet-stream' + }); + url = URL.createObjectURL(blob); + } + + if (!url) { + console.error('[Main] checkpoint did not include a URL or buffer'); + break; + } + + const a = document.createElement('a'); + a.href = url; + a.download = `${currentRun.current?.runId}.safetensors`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + break; + + case 'error': + if (data.name === 'VRAMLimitExceededError') { + console.error(`[Main] VRAM limit exceeded for run ${data.runId}:`, data.message); + triggerVramLimitFlash(); + } else if (data.name === 'LowDiversityDatasetError') { + console.error( + `[Main] Low diversity dataset error for run ${data.runId}:`, + data.message + ); + triggerLowDiversityDatasetError(); + } else { + console.error(`[Main] Training error for run ${data.runId}:`, data.message); + } + workerStopTraining(); + break; + case 'paused': + console.log('[Main] Training paused'); + trainingState.current = 'paused'; + break; + case 'resumed': + console.log('[Main] Training resumed'); + trainingState.current = 'training'; + break; + } + }; + + trainWorker.onerror = (event) => { + console.error('[Main] Worker onerror:', event); + reject(new Error(event.error)); + }; + } catch (error) { + console.error('[Main] Failed to create worker:', error); + reject(error); + } + }); +} + +export function workerStartTraining() { + if (!trainWorker) { + throw new Error('Worker not initialized'); + } + + const run = currentRun.current; + + if (!run) { + throw new Error('No current run'); + } + + trainWorker.postMessage({ + type: 'start', + data: JSON.parse(JSON.stringify(run)) + }); + + trainingState.current = 'training'; + void acquireScreenWakeLock(); +} + +export function workerRequestSave() { + if (!trainWorker) { + throw new Error('Worker not initialized'); + } + trainWorker.postMessage({ type: 'save' }); +} + +export async function workerStopTraining() { + if (!trainWorker || trainingState.current === 'stopped') return; + void releaseScreenWakeLock(); + + // For now, we'll just terminate and recreate the worker + // In a more sophisticated implementation, we'd send a stop message + trainWorker.terminate(); + workerReady.current = false; + trainingState.current = 'stopped'; + currentRun.current = null; + + // Recreate worker + await initializeWorker(); +} + +export function workerPauseTraining() { + if (!trainWorker || trainingState.current !== 'training') return; + trainWorker.postMessage({ type: 'pause' }); +} + +export function workerResumeTraining() { + if (!trainWorker || trainingState.current !== 'paused') return; + trainWorker.postMessage({ type: 'resume' }); +} + +export function workerStep() { + if (!trainWorker || trainingState.current === 'stopped') return; + trainWorker.postMessage({ type: 'step' }); +} + +// +// Model inspection state +// + +let parameterCount = $state(null); +let modelIndex = $state(null); +let modelInspectionRequestId = $state(null); +let isInspectingModel = $state(false); +let modelInspectionWorker: Worker | null = $state(null); + +export function getParameterCount() { + return parameterCount; +} + +export function getModelIndex() { + return modelIndex; +} + +export function getIsInspectingModel() { + return isInspectingModel; +} + +export function setModelInspectionWorker(workerInstance: Worker | null) { + modelInspectionWorker = workerInstance; + // Trigger initial model inspection when worker is set + if (modelInspectionWorker && !isInspectingModel) { + setTimeout(() => requestModelInspection(), 0); + } +} + +// Export a function to manually trigger model inspection +export function triggerModelInspection() { + if (modelInspectionWorker && !isInspectingModel) { + setTimeout(() => requestModelInspection(), 0); + } +} + +function requestModelInspection() { + if (!modelInspectionWorker || isInspectingModel) return; + + isInspectingModel = true; + modelInspectionRequestId = crypto.randomUUID(); + + try { + modelInspectionWorker.postMessage({ + type: 'inspectModel', + data: { + config: $state.snapshot(config), + requestId: modelInspectionRequestId + } + }); + } catch (error) { + console.error('Failed to request model inspection:', error); + isInspectingModel = false; + modelInspectionRequestId = null; + } +} + +export function handleModelInspectionResponse(data: { + requestId: string; + parameterCount: number; + vocabSize: number; + modelIndex: IndexState; +}) { + if (data.requestId === modelInspectionRequestId) { + parameterCount = data.parameterCount; + modelIndex = data.modelIndex; + isInspectingModel = false; + modelInspectionRequestId = null; + } +} + +export function handleModelInspectionError(data: { requestId: string; message: string }) { + if (data.requestId === modelInspectionRequestId) { + console.error('Model inspection error:', data.message); + isInspectingModel = false; + modelInspectionRequestId = null; + } +} + +export async function initializeModelInspectionWorker() { + return new Promise((resolve, reject) => { + try { + // Create the dedicated model inspection worker + // eslint-disable-next-line svelte/prefer-svelte-reactivity + modelInspectionWorker = new Worker(new URL('$lib/train/moduleWorker.ts', import.meta.url), { + type: 'module', + name: 'modelInspectionWorker' + }); + + console.log('[Main] Model inspection worker created successfully.'); + + modelInspectionWorker.onmessage = (event) => { + const { type, ...data } = event.data; + + switch (type) { + case 'ready': + console.log('[Main] Model inspection worker is ready'); + resolve(); + // Set the worker reference for model inspection + setModelInspectionWorker(modelInspectionWorker); + break; + + case 'modelInspection': + handleModelInspectionResponse(data); + break; + + case 'modelInspectionError': + handleModelInspectionError(data); + break; + + case 'error': + console.error('[Main] Model inspection worker error:', data.message); + break; + } + }; + + modelInspectionWorker.onerror = (event) => { + console.error('[Main] Model inspection worker error:', event.message, event); + reject(new Error(event.error)); + }; + } catch (error) { + console.error('[Main] Failed to create model inspection worker:', error); + reject(error); + } + }); +} + +export async function initializeWorkers() { + return Promise.all([initializeWorker(), initializeModelInspectionWorker()]); +} + +export function cleanupWorkers() { + if (trainWorker) { + trainWorker.terminate(); + trainWorker = null; + } + if (modelInspectionWorker) { + modelInspectionWorker.terminate(); + modelInspectionWorker = null; + } + // Clear UA memory interval + if (uaMemoryInterval) { + clearInterval(uaMemoryInterval); + uaMemoryInterval = null; + } + lastUAMemoryBytes = null; + + void releaseScreenWakeLock(); +} + +// +// Visualizer APIs +// +// eslint-disable-next-line svelte/prefer-svelte-reactivity +const canvasesWithAttemptedInitialization = new Set(); +export function initializeVisualizerCanvas( + canvas: HTMLCanvasElement, + labelPaddingCssPx: number = 0 +) { + if (!trainWorker) throw new Error('Worker not initialized'); + if (canvasesWithAttemptedInitialization.has(canvas)) return; + const offscreen = canvas.transferControlToOffscreen(); + trainWorker.postMessage( + { type: 'visualizer.canvas', data: { canvas: offscreen, labelPaddingCssPx } }, + [offscreen] + ); + canvasesWithAttemptedInitialization.add(canvas); +} + +export function resizeVisualizer(width: number) { + if (!trainWorker) return; + trainWorker.postMessage({ type: 'visualizer.resize', data: { width } }); +} + +export function getVisualizerLayout() { + return { + boxes: visualizerBoxes, + queries: visualizerQueries, + step: visualizerLayoutStep, + runId: visualizerLayoutRunId, + width: visualizerRenderWidth, + height: visualizerRenderHeight + }; +} + +export function getWorkerVersion() { + return workerVersion; +} + +export function updateVisualizerScript(example: string, script: string | null) { + if (!trainWorker) return; + trainWorker.postMessage({ type: 'visualizer.updateScript', data: { example, script } }); +} + +export function updateVisualizerTarget(target: 'train' | 'validation') { + if (!trainWorker) return; + config.visualization.target = target; + trainWorker.postMessage({ type: 'visualizer.setTarget', data: { target } }); +} + +export function updateVisualizerSelectedValidation({ + exampleIndex, + tokenIndex +}: { + exampleIndex: number; + tokenIndex: number; +}) { + if (!trainWorker) return; + config.visualization.selectedValidation.exampleIndex = exampleIndex; + config.visualization.selectedValidation.tokenIndex = tokenIndex; + trainWorker.postMessage({ + type: 'visualizer.setSelectedValidation', + data: { exampleIndex, tokenIndex } + }); +} diff --git a/examples/piston-train-toy/src/routes/+layout.svelte b/examples/piston-train-toy/src/routes/+layout.svelte new file mode 100644 index 00000000..14545100 --- /dev/null +++ b/examples/piston-train-toy/src/routes/+layout.svelte @@ -0,0 +1,25 @@ + + + + sequence toy + + + + + + + + + + + + + + + + +{@render children()} diff --git a/examples/piston-train-toy/src/routes/+layout.ts b/examples/piston-train-toy/src/routes/+layout.ts new file mode 100644 index 00000000..189f71e2 --- /dev/null +++ b/examples/piston-train-toy/src/routes/+layout.ts @@ -0,0 +1 @@ +export const prerender = true; diff --git a/examples/piston-train-toy/src/routes/+page.svelte b/examples/piston-train-toy/src/routes/+page.svelte new file mode 100644 index 00000000..0c57c462 --- /dev/null +++ b/examples/piston-train-toy/src/routes/+page.svelte @@ -0,0 +1,297 @@ + + +{#snippet tabButton( + title: string, + icon: typeof ChartLine, + hideBorder: boolean, + isActive: boolean, + disabled: boolean, + highlighted: boolean, + hasActivity: boolean, + onClick: () => void +)} + {@const Icon = icon} + +{/snippet} + +
+
+ Sequence Toy + +
+
+ + + + {#if configOpen.current !== false} +
+
+ {#if trainingState.current !== 'stopped'} +
+ {currentRun.current?.runId} + +
+ {/if} +
+
+ {#if trainingState.current === 'stopped'} + + + + Start Training + + + {:else} +
+ + + {#if trainingState.current === 'paused'} + + {:else} + + {/if} + + + + + + + + + + + + + + + + + + {#if shouldSuggestRestart} + New Changes + {/if} + + +
+ {/if} +
+
+
+ +
+
+
+ {/if} + + + {#if shouldShowTabContent} +
+ {#if activeTab.current === 'metrics'} + + {:else if activeTab.current === 'about'} + + {/if} +
+ {/if} +
+
+ + diff --git a/examples/piston-train-toy/src/routes/tabs/About.svelte b/examples/piston-train-toy/src/routes/tabs/About.svelte new file mode 100644 index 00000000..2e46c098 --- /dev/null +++ b/examples/piston-train-toy/src/routes/tabs/About.svelte @@ -0,0 +1,633 @@ + + +
+
+
+ + {#if !hasWebGPU.current} + {@const isBrowserUnknown = browserInfo.current.type === 'unknown'} + +
+

+ Sequence Toy requires WebGPU support, and your browser doesn't support it yet. + {#if isBrowserUnknown} + You have a few options: + {/if} +

+
+
    + {#if isBrowserUnknown || (browserInfo.current.type !== 'firefox' && browserInfo.current.type !== 'safari')} +
  • + Use the latest version of Chrome, or a + Chromium-based browser (Edge, Arc, Brave, etc.) +
      +
    • + If that doesn't work, try Chrome Canary and ensure WebGPU is enabled. +
    • +
    +
  • + {/if} + {#if isBrowserUnknown || browserInfo.current.type === 'firefox'} +
  • + Use Firefox Nightly + +
  • + {/if} + {#if isBrowserUnknown || browserInfo.current.type === 'safari'} +
  • + Use the latest Safari Technology Preview or enable WebGPU via Feature + Flags. +
      + {#if isBrowserUnknown || browserInfo.current.platform === 'ios'} +
    • + iOS: System Settings > Apps > Safari > Advanced > Feature Flags > Enable + "WebGPU" +
    • + {/if} + {#if isBrowserUnknown || browserInfo.current.platform === 'macos'} +
    • + MacOS: Develop menu Feature Flags > Enable "WebGPU" +
    • + {/if} +
    +
  • + {/if} +
+
+ {/if} +
+
+ {#each Array.from({ length: 8 }) as _, i (i)} +
+ +
+ {/each} +
+ +

+ Sequence Toy +

+
+ +

+ Train a language model in your browser with WebGPU +

+ +
+
+

You could try…

+
+
+ {#snippet sClick(text: string, onClick: () => void)} + {@const _onClick = onClick} + {text} + {/snippet} + {#snippet sPreset(text: string, id: string, controlId?: string)} + {@render sClick(text, () => { + setPreset(id); + if (controlId) openConfigAndScrollToControl(controlId, ['task']); + })} + {/snippet} + {#snippet sToyPreset(text: string, id: string, controlId?: string)} + {@render sClick(text, () => { + setPreset(id); + config.model.family = 'transformer'; + validateConfig(); + if (controlId) openConfigAndScrollToControl(controlId, ['task']); + })} + {/snippet} + {#snippet sViz(text: string, exampleId: string)} + {@render sClick(text, () => { + config.visualization.example = exampleId; + config.visualization.script = null; + switchToMetrics(); + if (trainingState.current !== 'stopped') { + const script = getVisualizationExampleById(exampleId).script; + updateVisualizerScript(exampleId, script); + } + startTraining(); + })} + {/snippet} + {#snippet sModelTransform(text: string, controlId: string)} + {@render sClick(text, () => { + config.model.family = 'transformer'; + validateConfig(); + openConfigAndScrollToControl(controlId, ['model']); + })} + {/snippet} + {#snippet sModelRnn(text: string, controlId: string)} + {@render sClick(text, () => { + config.model.family = 'rnn'; + validateConfig(); + openConfigAndScrollToControl(controlId, ['model']); + })} + {/snippet} + {#snippet sModelRnnLstm(text: string, controlId: string)} + {@render sClick(text, () => { + config.model.family = 'rnn'; + config.model.rnn.cellType = 'lstm'; + validateConfig(); + openConfigAndScrollToControl(controlId, ['model']); + })} + {/snippet} + {#snippet sScrollTraining(text: string, controlId: string)} + {@render sClick(text, () => { + openConfigAndScrollToControl(controlId, ['training']); + })} + {/snippet} + {#snippet sScrollOptimizer(text: string, controlId: string)} + {@render sClick(text, () => { + openConfigAndScrollToControl(controlId, ['optimizer']); + })} + {/snippet} + {#snippet sEncoderBidirectional(text: string)} + {@render sClick(text, () => { + if (config.model.topology === 'decoder') config.model.topology = 'encoder'; + validateConfig(); + openConfigAndScrollToControl( + 'model-rnn-bidirectional-encoder-checkbox', + ['model'], + { + useLabelFor: true + } + ); + })} + {/snippet} + {#snippet sEnsureEncDec(text: string)} + {@render sClick(text, () => { + config.model.family = 'rnn'; + config.model.topology = 'encoder-decoder'; + validateConfig(); + openConfigAndScrollToControl('model-rnn-encoder-decoder-attention-group', [ + 'model' + ]); + })} + {/snippet} +

+ …training a + TransformerAttention is All You Need + to {@render sToyPreset( + 'sort characters', + 'sort-characters', + 'dataset-control' + )}, + {@render sToyPreset('reverse a sequence', 'reverse-sequence', 'dataset-control')}, + or + {@render sToyPreset( + 'find the numbers that add up to a sum', + 'two-sum', + 'dataset-control' + )}, then {@render sModelRnnLstm( + 'compare with an LSTM', + 'rnn-cell-type-control' + )}Long Short-Term Memory. + Visualize + {@render sViz('the gradients in the attention layer', 'attention-gradients')}, {@render sViz( + 'all parameters in the network', + 'all-parameters' + )}, or try + {@render sViz('writing your own visualization queries', 'kitchen-sink')}. + Learn to + {@render sPreset( + 'match parentheses in a Dyck language', + 'dyck-encoder', + 'dataset-control' + )}Wikipedia article on Dyck languages + using an encoder-only masked language modeling (MLM) objectiveBERT: Pre-training of Deep Bidirectional Transformers for Language + Understanding + . + Want a taste of natural language? + Try {@render sPreset( + 'training a GPT on TinyStories', + 'tinystories', + 'dataset-control' + )}GPT: Improving Language Understanding by Generative Pre-TrainingTinyStories: How Small Can Language Models Be and Still Speak Coherent + English?, a dataset of short stories generated by GPT-4—and try different tokenizer + sizes. + Play with + {@render sModelTransform('attention gating', 'model-attention-gating-group')}, + {@render sModelTransform('MLP variants', 'model-mlp-group')}, {@render sScrollOptimizer( + 'learning rate schedulers', + 'optimizer-lr-scheduler-group' + )}, + {@render sModelTransform('initialization', 'model-initialization-group')}, {@render sScrollTraining( + 'dropout', + 'training-dropout-group' + )}, or {@render sModelTransform( + 'QK normalization', + 'model-transformer-normalization-qk-norm-group' + )}. + Want to train an RNN? + Mess with + {@render sModelRnn('layer normalization', 'model-rnn-layer-normalization-group')}, + {@render sModelRnn('initialization', 'model-initialization-group')}, + {@render sEncoderBidirectional('bidirectional encoder layers')}, {@render sEnsureEncDec( + 'encoder-decoder attention variants' + )}, or + {@render sScrollTraining( + 'gradient norm clipping', + 'training-clip-grad-norm-group' + )}. + Have a lot of VRAM and want to try something untestedVin, who owns a M1 Pro Macbook with 16GB of unified memory, has never been + able to do this without running out of memory. Good luck!? + Try + {@render sPreset( + 'training a GPT-2-sized model on FineWeb', + 'fineweb', + 'dataset-control' + )}The FineWeb Datasets: Decanting the Web for the Finest Text Data at Scale (and + DM me if you get it to work!). +

+
+ {#if isOverflowing && !expanded} +
+ {/if} +
+ {#if isOverflowing} +
+ +
+ {/if} +
+
+ +

+ Vin Howe + built + Piston, a WebGPU automatic differentiation engine with a JavaScript API modeled after PyTorch, + for this project. It started life as a fork of + RatchetA ratchet is a device that allows motion in only one direction—Ratchet is + intentionally forward-only. A piston reciprocates back and forth quickly—Piston + adds support for reverse-mode automatic differentiation. + by + Christopher Fleetwood. +

+ +
+
+

+ This project was inspired by many other open-source projects: +

+ +
+ +
+

Helpful resources when building this project:

+
+ +
+
+ +
+
+
+

+ Thanks to Grant Pitt, + Christopher Fleetwood, and + Ben Gubler + for support, discussion, and feedback. 💜 +

+
+
+
+
+
+
+ +
+
diff --git a/examples/piston-train-toy/src/routes/tabs/Metrics.svelte b/examples/piston-train-toy/src/routes/tabs/Metrics.svelte new file mode 100644 index 00000000..9fde7764 --- /dev/null +++ b/examples/piston-train-toy/src/routes/tabs/Metrics.svelte @@ -0,0 +1,167 @@ + + +
+ {#if !tourState.current.restartedExperiment} + + Tinker with the experiment setup, then click New Changes + to try out your changes. You can probably break it! + Report issues on Github . + + {/if} + {#if Object.keys(metricGroups).length === 0} +
+

No metrics available. Start training to see charts.

+
+ {:else} +
+ {#if config.training.enableVisualization} + toggleMetricsSection('visualize')} + > + {#snippet chips()} + {#if (runConfig ?? config).training.validation.present} + config.visualization.target, updateVisualizerTarget} + options={[ + { value: 'validation', label: 'Selected Validation Example' }, + { value: 'train', label: 'Training Batch Example' } + ]} + /> + {/if} + {/snippet} + + + {/if} + + {#each Object.entries(metricGroups).sort(([a], [b]) => { + const order = ['validation', 'train', 'optimizer']; + const aPriority = order.indexOf(a); + const bPriority = order.indexOf(b); + return (aPriority === -1 ? 999 : aPriority) - (bPriority === -1 ? 999 : bPriority) || a.localeCompare(b); + }) as [groupName, metrics] (groupName)} + {@const filteredMetrics = getFilteredMetrics(groupName, metrics)} + {@const hasMetrics = filteredMetrics.length > 0} + {@const sectionOpen = (metricsSectionsOpen.current[groupName] ?? true) && hasMetrics} + toggleMetricsSection(groupName) : undefined} + > + {#snippet chips()} + + {/snippet} + {#each filteredMetrics as metricName (metricName)} +
+ {#if metricName === 'validation/completions'} + + {:else} + + {/if} +
+ {/each} +
+ {/each} +
+ {/if} +
diff --git a/examples/piston-train-toy/static/Berkeley Mono Variable.woff2 b/examples/piston-train-toy/static/Berkeley Mono Variable.woff2 new file mode 100644 index 00000000..831f3b18 Binary files /dev/null and b/examples/piston-train-toy/static/Berkeley Mono Variable.woff2 differ diff --git a/examples/piston-train-toy/static/_headers b/examples/piston-train-toy/static/_headers new file mode 100644 index 00000000..4b8e46b9 --- /dev/null +++ b/examples/piston-train-toy/static/_headers @@ -0,0 +1,4 @@ +# This allows us to use performance.measureUserAgentSpecificMemory on browsers that support it (Chrome) +/* + Cross-Origin-Embedder-Policy: require-corp + Cross-Origin-Opener-Policy: same-origin \ No newline at end of file diff --git a/examples/piston-train-toy/static/apple-touch-icon.png b/examples/piston-train-toy/static/apple-touch-icon.png new file mode 100644 index 00000000..299d463c Binary files /dev/null and b/examples/piston-train-toy/static/apple-touch-icon.png differ diff --git a/examples/piston-train-toy/static/favicon.ico b/examples/piston-train-toy/static/favicon.ico new file mode 100644 index 00000000..eef41614 Binary files /dev/null and b/examples/piston-train-toy/static/favicon.ico differ diff --git a/examples/piston-train-toy/static/icon.svg b/examples/piston-train-toy/static/icon.svg new file mode 100644 index 00000000..22bd8b98 --- /dev/null +++ b/examples/piston-train-toy/static/icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/examples/piston-train-toy/svelte.config.js b/examples/piston-train-toy/svelte.config.js new file mode 100644 index 00000000..64694ad1 --- /dev/null +++ b/examples/piston-train-toy/svelte.config.js @@ -0,0 +1,15 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + adapter: adapter() + } +}; + +export default config; diff --git a/examples/piston-train-toy/tsconfig.json b/examples/piston-train-toy/tsconfig.json new file mode 100644 index 00000000..57aab37d --- /dev/null +++ b/examples/piston-train-toy/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler", + "types": ["@webgpu/types"] + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/examples/piston-train-toy/vite.config.ts b/examples/piston-train-toy/vite.config.ts new file mode 100644 index 00000000..f10d1482 --- /dev/null +++ b/examples/piston-train-toy/vite.config.ts @@ -0,0 +1,73 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { execSync } from 'node:child_process'; +import fs from 'node:fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { defineConfig } from 'vite'; +import wasm from 'vite-plugin-wasm'; + +// Get the project root directory +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); + +// Remove large static subfolders we don't want to ship +const pruneStaticDirs = () => { + return { + name: 'prune-static-dirs', + // After Vite writes bundles (client/server into .svelte-kit/output/*), + // remove unwanted static folders from client output to avoid copying into final build + async writeBundle() { + const root = path.dirname(fileURLToPath(import.meta.url)); + const clientOut = path.resolve(root, '.svelte-kit', 'output', 'client'); + const targets = [path.resolve(clientOut, 'tokenized'), path.resolve(clientOut, 'tokenizer')]; + await Promise.all( + targets.map((p) => fs.rm(p, { recursive: true, force: true }).catch(() => {})) + ); + } + }; +}; + +const commitHash = execSync('git rev-parse --short HEAD').toString().trim(); + +export default defineConfig((_) => ({ + define: { + __COMMIT_HASH__: JSON.stringify(commitHash) + }, + plugins: [tailwindcss(), sveltekit(), wasm(), pruneStaticDirs()], + worker: { + format: 'es', + plugins: () => [wasm(), sveltekit()] + }, + resolve: { + dedupe: [ + '@codemirror/state', + '@codemirror/view', + '@codemirror/language', + '@codemirror/lang-javascript', + '@codemirror/lint', + 'codemirror', + '@lezer/highlight' + ] + }, + esbuild: { + supported: { 'top-level-await': true }, + keepNames: true + }, + server: { + fs: { + // Allow serving files from the project root and one level up + allow: [ + // Allow serving from the Svelte project directory + path.resolve(path.dirname(fileURLToPath(import.meta.url))), + // Allow serving from the entire ratchet project directory + projectRoot, + // Allow serving from the WASM file's directory + path.resolve(projectRoot, 'target', 'pkg', 'piston-web') + ] + }, + headers: { + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin' + } + } +})); diff --git a/examples/piston-train-toy/wrangler.jsonc b/examples/piston-train-toy/wrangler.jsonc new file mode 100644 index 00000000..871bccbb --- /dev/null +++ b/examples/piston-train-toy/wrangler.jsonc @@ -0,0 +1,12 @@ +{ + "name": "piston-train-toy", + "compatibility_date": "2025-07-11", + "assets": { + "directory": "./build/" + }, + "exclude": [ + "./static/tokenized", + "./static/tokenizer", + "./data" + ] +} diff --git a/examples/ratchet-moondream/.gitignore b/examples/ratchet-moondream/.gitignore deleted file mode 100644 index 4d29575d..00000000 --- a/examples/ratchet-moondream/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/examples/ratchet-moondream/README.md b/examples/ratchet-moondream/README.md deleted file mode 100644 index 5e7cf25e..00000000 --- a/examples/ratchet-moondream/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Getting Started with Create React App - -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. - -The page will reload when you make changes.\ -You may also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. diff --git a/examples/ratchet-moondream/package-lock.json b/examples/ratchet-moondream/package-lock.json deleted file mode 100644 index 06febf98..00000000 --- a/examples/ratchet-moondream/package-lock.json +++ /dev/null @@ -1,18080 +0,0 @@ -{ - "name": "ratchet-moondream", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ratchet-moondream", - "version": "0.1.0", - "dependencies": { - "@emotion/react": "^11.11.4", - "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.19", - "@mui/material": "^5.15.19", - "@ratchet-ml/ratchet-web": "file:../../target/pkg/ratchet-web", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" - }, - "devDependencies": { - "prettier": "3.3.1" - } - }, - "../../target/pkg/ratchet-web": { - "name": "@ratchet/ratchet-web", - "version": "0.3.0", - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/eslint-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz", - "integrity": "sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA==", - "dependencies": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0", - "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/@babel/eslint-parser/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", - "dependencies": { - "@babel/types": "^7.24.7", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", - "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", - "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", - "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", - "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", - "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", - "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", - "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-wrap-function": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", - "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", - "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", - "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", - "dependencies": { - "@babel/helper-function-name": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", - "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", - "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", - "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", - "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.7.tgz", - "integrity": "sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-decorators": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.7.tgz", - "integrity": "sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz", - "integrity": "sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", - "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", - "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", - "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", - "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", - "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", - "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", - "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", - "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", - "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", - "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", - "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/template": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", - "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", - "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", - "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", - "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", - "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", - "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.7.tgz", - "integrity": "sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-flow": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", - "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", - "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", - "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", - "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", - "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", - "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", - "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", - "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", - "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", - "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", - "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", - "dependencies": { - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", - "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", - "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", - "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", - "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", - "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", - "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", - "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", - "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", - "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", - "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", - "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", - "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", - "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", - "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.7.tgz", - "integrity": "sha512-7LidzZfUXyfZ8/buRW6qIIHBY8wAZ1OrY9c/wTr8YhZ6vMPo+Uc/CVFLYY1spZrEQlD4w5u8wjqk5NQ3OVqQKA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", - "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", - "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-jsx": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", - "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", - "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", - "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", - "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", - "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.1", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", - "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", - "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", - "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", - "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", - "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz", - "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-typescript": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", - "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", - "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", - "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", - "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", - "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", - "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.24.7", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.24.7", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoped-functions": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.24.7", - "@babel/plugin-transform-class-properties": "^7.24.7", - "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.24.7", - "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.7", - "@babel/plugin-transform-dotall-regex": "^7.24.7", - "@babel/plugin-transform-duplicate-keys": "^7.24.7", - "@babel/plugin-transform-dynamic-import": "^7.24.7", - "@babel/plugin-transform-exponentiation-operator": "^7.24.7", - "@babel/plugin-transform-export-namespace-from": "^7.24.7", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.24.7", - "@babel/plugin-transform-json-strings": "^7.24.7", - "@babel/plugin-transform-literals": "^7.24.7", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-member-expression-literals": "^7.24.7", - "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.7", - "@babel/plugin-transform-modules-systemjs": "^7.24.7", - "@babel/plugin-transform-modules-umd": "^7.24.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-new-target": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-numeric-separator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-object-super": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-property-literals": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-reserved-words": "^7.24.7", - "@babel/plugin-transform-shorthand-properties": "^7.24.7", - "@babel/plugin-transform-spread": "^7.24.7", - "@babel/plugin-transform-sticky-regex": "^7.24.7", - "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.7", - "@babel/plugin-transform-unicode-escapes": "^7.24.7", - "@babel/plugin-transform-unicode-property-regex": "^7.24.7", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.4", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.31.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", - "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.24.7", - "@babel/plugin-transform-react-jsx-development": "^7.24.7", - "@babel/plugin-transform-react-pure-annotations": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", - "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "@babel/plugin-syntax-jsx": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.7", - "@babel/plugin-transform-typescript": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, - "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", - "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" - }, - "node_modules/@csstools/normalize.css": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz", - "integrity": "sha512-YAYeJ+Xqh7fUou1d1j9XHl44BmsuThiTr4iNrgCQ3J27IbhXsxXDGZ1cXv8Qvs99d4rBbLiSKy3+WZiet32PcQ==" - }, - "node_modules/@csstools/postcss-cascade-layers": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", - "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", - "dependencies": { - "@csstools/selector-specificity": "^2.0.2", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-color-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", - "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", - "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", - "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", - "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", - "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-nested-calc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", - "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", - "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", - "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", - "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", - "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", - "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", - "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/postcss-unset-value": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", - "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", - "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.0.10" - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", - "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/serialize": "^1.1.2", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", - "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", - "dependencies": { - "@emotion/memoize": "^0.8.1", - "@emotion/sheet": "^1.2.2", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", - "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, - "node_modules/@emotion/react": { - "version": "11.11.4", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", - "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1", - "@emotion/weak-memoize": "^0.3.1", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", - "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", - "dependencies": { - "@emotion/hash": "^0.9.1", - "@emotion/memoize": "^0.8.1", - "@emotion/unitless": "^0.8.1", - "@emotion/utils": "^1.2.1", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", - "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" - }, - "node_modules/@emotion/styled": { - "version": "11.11.5", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", - "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.11.0", - "@emotion/is-prop-valid": "^1.2.2", - "@emotion/serialize": "^1.1.4", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", - "@emotion/utils": "^1.2.1" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", - "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", - "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", - "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", - "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", - "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", - "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", - "dependencies": { - "@floating-ui/utils": "^0.2.0" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", - "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", - "dependencies": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz", - "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", - "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", - "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/console/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/console/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", - "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/reporters": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^27.5.1", - "jest-config": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-resolve-dependencies": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "jest-watcher": "^27.5.1", - "micromatch": "^4.0.4", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/core/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/core/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/environment": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", - "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", - "dependencies": { - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", - "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", - "dependencies": { - "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", - "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", - "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/reporters/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/reporters/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", - "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", - "dependencies": { - "@sinclair/typebox": "^0.24.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", - "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", - "dependencies": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/source-map/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@jest/test-result": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", - "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", - "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", - "dependencies": { - "@jest/test-result": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-runtime": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", - "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", - "dependencies": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/transform/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/transform/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/@jest/transform/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" - }, - "node_modules/@mui/base": { - "version": "5.0.0-beta.40", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", - "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", - "@popperjs/core": "^2.11.8", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/core-downloads-tracker": { - "version": "5.15.19", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.19.tgz", - "integrity": "sha512-tCHSi/Tomez9ERynFhZRvFO6n9ATyrPs+2N80DMDzp6xDVirbBjEwhPcE+x7Lj+nwYw0SqFkOxyvMP0irnm55w==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/icons-material": { - "version": "5.15.19", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.19.tgz", - "integrity": "sha512-RsEiRxA5azN9b8gI7JRqekkgvxQUlitoBOtZglflb8cUDyP12/cP4gRwhb44Ea1/zwwGGjAj66ZJpGHhKfibNA==", - "dependencies": { - "@babel/runtime": "^7.23.9" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^5.0.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material": { - "version": "5.15.19", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.19.tgz", - "integrity": "sha512-lp5xQBbcRuxNtjpWU0BWZgIrv2XLUz4RJ0RqFXBdESIsKoGCQZ6P3wwU5ZPuj5TjssNiKv9AlM+vHopRxZhvVQ==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.40", - "@mui/core-downloads-tracker": "^5.15.19", - "@mui/system": "^5.15.15", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", - "@types/react-transition-group": "^4.4.10", - "clsx": "^2.1.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^18.2.0", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, - "node_modules/@mui/private-theming": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", - "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.14", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", - "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@emotion/cache": "^11.11.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "5.15.15", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", - "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.14", - "@mui/styled-engine": "^5.15.14", - "@mui/types": "^7.2.14", - "@mui/utils": "^5.15.14", - "clsx": "^2.1.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.2.14", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", - "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", - "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@types/prop-types": "^15.7.11", - "prop-types": "^15.8.1", - "react-is": "^18.2.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dependencies": { - "eslint-scope": "5.1.1" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", - "integrity": "sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==", - "dependencies": { - "ansi-html": "^0.0.9", - "core-js-pure": "^3.23.3", - "error-stack-parser": "^2.0.6", - "html-entities": "^2.1.0", - "loader-utils": "^2.0.4", - "schema-utils": "^4.2.0", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">= 10.13" - }, - "peerDependencies": { - "@types/webpack": "4.x || 5.x", - "react-refresh": ">=0.10.0 <1.0.0", - "sockjs-client": "^1.4.0", - "type-fest": ">=0.17.0 <5.0.0", - "webpack": ">=4.43.0 <6.0.0", - "webpack-dev-server": "3.x || 4.x || 5.x", - "webpack-hot-middleware": "2.x", - "webpack-plugin-serve": "0.x || 1.x" - }, - "peerDependenciesMeta": { - "@types/webpack": { - "optional": true - }, - "sockjs-client": { - "optional": true - }, - "type-fest": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - }, - "webpack-hot-middleware": { - "optional": true - }, - "webpack-plugin-serve": { - "optional": true - } - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@ratchet-ml/ratchet-web": { - "resolved": "../../target/pkg/ratchet-web", - "link": true - }, - "node_modules/@rollup/plugin-babel": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", - "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", - "dependencies": { - "@babel/helper-module-imports": "^7.10.4", - "@rollup/pluginutils": "^3.1.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", - "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "magic-string": "^0.25.7" - }, - "peerDependencies": { - "rollup": "^1.20.0 || ^2.0.0" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/@rollup/pluginutils/node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", - "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==" - }, - "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==" - }, - "node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@surma/rollup-plugin-off-main-thread": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", - "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", - "dependencies": { - "ejs": "^3.1.6", - "json5": "^2.2.0", - "magic-string": "^0.25.0", - "string.prototype.matchall": "^4.0.6" - } - }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", - "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", - "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", - "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", - "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", - "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", - "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/babel-preset": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", - "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", - "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", - "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", - "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", - "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", - "@svgr/babel-plugin-transform-svg-component": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/core": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", - "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", - "dependencies": { - "@svgr/plugin-jsx": "^5.5.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^7.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", - "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", - "dependencies": { - "@babel/types": "^7.12.6" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-jsx": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", - "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", - "dependencies": { - "@babel/core": "^7.12.3", - "@svgr/babel-preset": "^5.5.0", - "@svgr/hast-util-to-babel-ast": "^5.5.0", - "svg-parser": "^2.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-svgo": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", - "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", - "dependencies": { - "cosmiconfig": "^7.0.0", - "deepmerge": "^4.2.2", - "svgo": "^1.2.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/webpack": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", - "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/plugin-transform-react-constant-elements": "^7.12.1", - "@babel/preset-env": "^7.12.1", - "@babel/preset-react": "^7.12.5", - "@svgr/core": "^5.5.0", - "@svgr/plugin-jsx": "^5.5.0", - "@svgr/plugin-svgo": "^5.5.0", - "loader-utils": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" - }, - "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz", - "integrity": "sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==" - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.14", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", - "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" - }, - "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" - }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" - }, - "node_modules/@types/q": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", - "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==" - }, - "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" - }, - "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-transition-group": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", - "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/resolve": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", - "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" - }, - "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", - "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", - "dependencies": { - "@typescript-eslint/utils": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", - "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", - "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", - "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", - "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.reduce": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.7.tgz", - "integrity": "sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-array-method-boxes-properly": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==" - }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", - "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/babel-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", - "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", - "dependencies": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/babel-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/babel-jest/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-loader": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", - "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", - "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "engines": { - "node": ">= 8.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-loader/node_modules/schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", - "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/babel-plugin-named-asset-import": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", - "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", - "peerDependencies": { - "@babel/core": "^7.1.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", - "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", - "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.1", - "core-js-compat": "^3.36.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", - "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-transform-react-remove-prop-types": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", - "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==" - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", - "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", - "dependencies": { - "babel-plugin-jest-hoist": "^27.5.1", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/babel-preset-react-app": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz", - "integrity": "sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==", - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/plugin-proposal-class-properties": "^7.16.0", - "@babel/plugin-proposal-decorators": "^7.16.4", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", - "@babel/plugin-proposal-numeric-separator": "^7.16.0", - "@babel/plugin-proposal-optional-chaining": "^7.16.0", - "@babel/plugin-proposal-private-methods": "^7.16.0", - "@babel/plugin-transform-flow-strip-types": "^7.16.0", - "@babel/plugin-transform-react-display-name": "^7.16.0", - "@babel/plugin-transform-runtime": "^7.16.4", - "@babel/preset-env": "^7.16.4", - "@babel/preset-react": "^7.16.0", - "@babel/preset-typescript": "^7.16.0", - "@babel/runtime": "^7.16.3", - "babel-plugin-macros": "^3.1.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" - }, - "node_modules/bfj": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", - "integrity": "sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw==", - "dependencies": { - "bluebird": "^3.7.2", - "check-types": "^11.2.3", - "hoopy": "^0.1.4", - "jsonpath": "^1.1.1", - "tryer": "^1.0.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/bonjour-service": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", - "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" - }, - "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001629", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz", - "integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/case-sensitive-paths-webpack-plugin": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", - "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/check-types": { - "version": "11.2.3", - "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", - "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" - }, - "node_modules/clean-css": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", - "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dependencies": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==" - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "engines": { - "node": ">= 12" - } - }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/confusing-browser-globals": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", - "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/core-js": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", - "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", - "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", - "dependencies": { - "browserslist": "^4.23.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-pure": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.37.1.tgz", - "integrity": "sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/css-blank-pseudo": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", - "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "bin": { - "css-blank-pseudo": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-declaration-sorter": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", - "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-has-pseudo": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", - "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "bin": { - "css-has-pseudo": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz", - "integrity": "sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==", - "dependencies": { - "cssnano": "^5.0.6", - "jest-worker": "^27.0.2", - "postcss": "^8.3.5", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@parcel/css": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/css-prefers-color-scheme": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", - "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", - "bin": { - "css-prefers-color-scheme": "dist/cli.cjs" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" - }, - "node_modules/css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "dependencies": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-tree/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssdb": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", - "integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - } - ] - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "5.1.15", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", - "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", - "dependencies": { - "cssnano-preset-default": "^5.2.14", - "lilconfig": "^2.0.3", - "yaml": "^1.10.2" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-preset-default": { - "version": "5.2.14", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", - "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", - "dependencies": { - "css-declaration-sorter": "^6.3.1", - "cssnano-utils": "^3.1.0", - "postcss-calc": "^8.2.3", - "postcss-colormin": "^5.3.1", - "postcss-convert-values": "^5.1.3", - "postcss-discard-comments": "^5.1.2", - "postcss-discard-duplicates": "^5.1.0", - "postcss-discard-empty": "^5.1.1", - "postcss-discard-overridden": "^5.1.0", - "postcss-merge-longhand": "^5.1.7", - "postcss-merge-rules": "^5.1.4", - "postcss-minify-font-values": "^5.1.0", - "postcss-minify-gradients": "^5.1.1", - "postcss-minify-params": "^5.1.4", - "postcss-minify-selectors": "^5.2.1", - "postcss-normalize-charset": "^5.1.0", - "postcss-normalize-display-values": "^5.1.0", - "postcss-normalize-positions": "^5.1.1", - "postcss-normalize-repeat-style": "^5.1.1", - "postcss-normalize-string": "^5.1.0", - "postcss-normalize-timing-functions": "^5.1.0", - "postcss-normalize-unicode": "^5.1.1", - "postcss-normalize-url": "^5.1.0", - "postcss-normalize-whitespace": "^5.1.1", - "postcss-ordered-values": "^5.1.3", - "postcss-reduce-initial": "^5.1.2", - "postcss-reduce-transforms": "^5.1.0", - "postcss-svgo": "^5.1.0", - "postcss-unique-selectors": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/cssnano-utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", - "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" - }, - "node_modules/csso/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" - }, - "node_modules/data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" - }, - "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==" - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" - }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" - }, - "node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "deprecated": "Use your platform's native DOMException instead", - "dependencies": { - "webidl-conversions": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "engines": { - "node": ">=10" - } - }, - "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.794", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.794.tgz", - "integrity": "sha512-6FApLtsYhDCY0Vglq3AptsdxQ+PJLc6AxlAM0HjEihUAiOPPbkASEsq9gtxUeZY9o0sJIEa3WnF0vVH4VT4iug==" - }, - "node_modules/emittery": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", - "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", - "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "dependencies": { - "stackframe": "^1.3.4" - } - }, - "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", - "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz", - "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==" - }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-react-app": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", - "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", - "dependencies": { - "@babel/core": "^7.16.0", - "@babel/eslint-parser": "^7.16.3", - "@rushstack/eslint-patch": "^1.1.0", - "@typescript-eslint/eslint-plugin": "^5.5.0", - "@typescript-eslint/parser": "^5.5.0", - "babel-preset-react-app": "^10.0.1", - "confusing-browser-globals": "^1.0.11", - "eslint-plugin-flowtype": "^8.0.3", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jest": "^25.3.0", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-react": "^7.27.1", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-testing-library": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-flowtype": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", - "integrity": "sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==", - "dependencies": { - "lodash": "^4.17.21", - "string-natural-compare": "^3.0.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@babel/plugin-syntax-flow": "^7.14.5", - "@babel/plugin-transform-react-jsx": "^7.14.9", - "eslint": "^8.1.0" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-jest": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz", - "integrity": "sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==", - "dependencies": { - "@typescript-eslint/experimental-utils": "^5.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^4.0.0 || ^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz", - "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==", - "dependencies": { - "@babel/runtime": "^7.23.2", - "aria-query": "^5.3.0", - "array-includes": "^3.1.7", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", - "axobject-query": "^3.2.1", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.entries": "^1.1.7", - "object.fromentries": "^2.0.7" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.34.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz", - "integrity": "sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw==", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", - "array.prototype.tosorted": "^1.1.3", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.19", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.8", - "object.fromentries": "^2.0.8", - "object.hasown": "^1.1.4", - "object.values": "^1.2.0", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-testing-library": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz", - "integrity": "sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw==", - "dependencies": { - "@typescript-eslint/utils": "^5.58.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6" - }, - "peerDependencies": { - "eslint": "^7.5.0 || ^8.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", - "dependencies": { - "@types/eslint": "^7.29.0 || ^8.4.1", - "jest-worker": "^28.0.2", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/eslint-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", - "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", - "dependencies": { - "@jest/types": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-monkey": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", - "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" - }, - "node_modules/harmony-reflect": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", - "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==" - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/hoopy": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", - "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dependencies": { - "whatwg-encoding": "^1.0.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ] - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" - }, - "node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", - "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" - }, - "node_modules/identity-obj-proxy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", - "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", - "dependencies": { - "harmony-reflect": "^1.4.6" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", - "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" - } - }, - "node_modules/jackspeak": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", - "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", - "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jake/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jake/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", - "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", - "dependencies": { - "@jest/core": "^27.5.1", - "import-local": "^3.0.2", - "jest-cli": "^27.5.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", - "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", - "dependencies": { - "@jest/types": "^27.5.1", - "execa": "^5.0.0", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", - "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-circus/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-circus/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", - "dependencies": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-cli/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", - "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", - "dependencies": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-config/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-config/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-docblock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", - "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", - "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-each/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-each/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-each/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-environment-jsdom": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", - "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", - "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", - "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", - "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "micromatch": "^4.0.4", - "walker": "^1.0.7" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-jasmine2": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", - "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-jasmine2/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-jasmine2/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-jasmine2/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-jasmine2/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-jasmine2/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-leak-detector": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", - "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", - "dependencies": { - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", - "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-matcher-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-matcher-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", - "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^27.5.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-message-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-message-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", - "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", - "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", - "dependencies": { - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", - "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", - "dependencies": { - "@jest/types": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-snapshot": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-resolve/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-resolve/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-resolve/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-resolve/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", - "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.8.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-leak-detector": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runner/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-runner/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", - "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runtime/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-runtime/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-serializer": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", - "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", - "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", - "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", - "dependencies": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-snapshot/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", - "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-validate": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", - "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", - "dependencies": { - "@jest/types": "^27.5.1", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "leven": "^3.1.0", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-validate/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-validate/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-validate/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.1.0.tgz", - "integrity": "sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==", - "dependencies": { - "ansi-escapes": "^4.3.1", - "chalk": "^4.0.0", - "jest-regex-util": "^28.0.0", - "jest-watcher": "^28.0.0", - "slash": "^4.0.0", - "string-length": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "jest": "^27.0.0 || ^28.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/console": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", - "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/console/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/test-result": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", - "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", - "dependencies": { - "@jest/console": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@jest/types": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", - "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", - "dependencies": { - "@jest/schemas": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-watch-typeahead/node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-message-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", - "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^28.1.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-message-util/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", - "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", - "dependencies": { - "@jest/test-result": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^28.1.3", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-watch-typeahead/node_modules/jest-watcher/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watch-typeahead/node_modules/pretty-format": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", - "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", - "dependencies": { - "@jest/schemas": "^28.1.3", - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-watch-typeahead/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, - "node_modules/jest-watch-typeahead/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/string-length": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", - "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", - "dependencies": { - "char-regex": "^2.0.0", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watch-typeahead/node_modules/string-length/node_modules/char-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.1.tgz", - "integrity": "sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/jest-watch-typeahead/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", - "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", - "dependencies": { - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^27.5.1", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-watcher/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/jest-watcher/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jiti": { - "version": "1.21.3", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.3.tgz", - "integrity": "sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonpath": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", - "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", - "dependencies": { - "esprima": "1.2.2", - "static-eval": "2.0.2", - "underscore": "1.12.1" - } - }, - "node_modules/jsonpath/node_modules/esprima": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", - "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", - "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "engines": { - "node": ">=6" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/launch-editor": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", - "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", - "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "dependencies": { - "fs-monkey": "^1.0.4" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", - "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nwsapi": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", - "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz", - "integrity": "sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A==", - "dependencies": { - "array.prototype.reduce": "^1.0.6", - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "gopd": "^1.0.1", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dependencies": { - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-attribute-case-insensitive": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", - "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-browser-comments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", - "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "browserslist": ">=4", - "postcss": ">=8" - } - }, - "node_modules/postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", - "dependencies": { - "postcss-selector-parser": "^6.0.9", - "postcss-value-parser": "^4.2.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=7.6.0" - }, - "peerDependencies": { - "postcss": "^8.4.6" - } - }, - "node_modules/postcss-color-functional-notation": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", - "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-color-hex-alpha": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", - "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-rebeccapurple": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", - "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-colormin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", - "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-convert-values": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", - "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-custom-media": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", - "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/postcss-custom-properties": { - "version": "12.1.11", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", - "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-custom-selectors": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", - "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/postcss-dir-pseudo-class": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", - "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-discard-comments": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", - "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-empty": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-double-position-gradients": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", - "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-env-function": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", - "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-flexbugs-fixes": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "peerDependencies": { - "postcss": "^8.1.4" - } - }, - "node_modules/postcss-focus-visible": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", - "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-within": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", - "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-gap-properties": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", - "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-image-set-function": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", - "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-initial": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-lab-function": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", - "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", - "integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/postcss-loader": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", - "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", - "dependencies": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.5", - "semver": "^7.3.5" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-logical": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", - "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-media-minmax": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", - "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-merge-rules": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", - "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^3.1.0", - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", - "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", - "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", - "dependencies": { - "colord": "^2.9.1", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-params": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", - "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", - "dependencies": { - "browserslist": "^4.21.4", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", - "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nesting": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", - "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-normalize": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", - "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", - "dependencies": { - "@csstools/normalize.css": "*", - "postcss-browser-comments": "^4", - "sanitize.css": "*" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "browserslist": ">= 4", - "postcss": ">= 8" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", - "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", - "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", - "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-string": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", - "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", - "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", - "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", - "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", - "dependencies": { - "normalize-url": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", - "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-opacity-percentage": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", - "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", - "funding": [ - { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" - } - ], - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-ordered-values": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", - "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", - "dependencies": { - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-overflow-shorthand": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", - "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "peerDependencies": { - "postcss": "^8" - } - }, - "node_modules/postcss-place": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", - "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-preset-env": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", - "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", - "dependencies": { - "@csstools/postcss-cascade-layers": "^1.1.1", - "@csstools/postcss-color-function": "^1.1.1", - "@csstools/postcss-font-format-keywords": "^1.0.1", - "@csstools/postcss-hwb-function": "^1.0.2", - "@csstools/postcss-ic-unit": "^1.0.1", - "@csstools/postcss-is-pseudo-class": "^2.0.7", - "@csstools/postcss-nested-calc": "^1.0.0", - "@csstools/postcss-normalize-display-values": "^1.0.1", - "@csstools/postcss-oklab-function": "^1.1.1", - "@csstools/postcss-progressive-custom-properties": "^1.3.0", - "@csstools/postcss-stepped-value-functions": "^1.0.1", - "@csstools/postcss-text-decoration-shorthand": "^1.0.0", - "@csstools/postcss-trigonometric-functions": "^1.0.2", - "@csstools/postcss-unset-value": "^1.0.2", - "autoprefixer": "^10.4.13", - "browserslist": "^4.21.4", - "css-blank-pseudo": "^3.0.3", - "css-has-pseudo": "^3.0.4", - "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^7.1.0", - "postcss-attribute-case-insensitive": "^5.0.2", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.4", - "postcss-color-hex-alpha": "^8.0.4", - "postcss-color-rebeccapurple": "^7.1.1", - "postcss-custom-media": "^8.0.2", - "postcss-custom-properties": "^12.1.10", - "postcss-custom-selectors": "^6.0.3", - "postcss-dir-pseudo-class": "^6.0.5", - "postcss-double-position-gradients": "^3.1.2", - "postcss-env-function": "^4.0.6", - "postcss-focus-visible": "^6.0.4", - "postcss-focus-within": "^5.0.4", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^3.0.5", - "postcss-image-set-function": "^4.0.7", - "postcss-initial": "^4.0.1", - "postcss-lab-function": "^4.2.1", - "postcss-logical": "^5.0.4", - "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.2.0", - "postcss-opacity-percentage": "^1.1.2", - "postcss-overflow-shorthand": "^3.0.4", - "postcss-page-break": "^3.0.4", - "postcss-place": "^7.0.5", - "postcss-pseudo-class-any-link": "^7.1.6", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", - "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", - "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", - "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "peerDependencies": { - "postcss": "^8.0.3" - } - }, - "node_modules/postcss-selector-not": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", - "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", - "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^2.7.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/postcss-svgo/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/postcss-svgo/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" - }, - "node_modules/postcss-svgo/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postcss-svgo/node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", - "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.1.tgz", - "integrity": "sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promise": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", - "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", - "dependencies": { - "asap": "~2.0.6" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dependencies": { - "performance-now": "^2.1.0" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-app-polyfill": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", - "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", - "dependencies": { - "core-js": "^3.19.2", - "object-assign": "^4.1.1", - "promise": "^8.1.0", - "raf": "^3.4.1", - "regenerator-runtime": "^0.13.9", - "whatwg-fetch": "^3.6.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-app-polyfill/node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/react-dev-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-dev-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/react-dev-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/react-dev-utils/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-error-overlay": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", - "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, - "node_modules/react-refresh": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", - "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-scripts": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", - "integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==", - "dependencies": { - "@babel/core": "^7.16.0", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", - "@svgr/webpack": "^5.5.0", - "babel-jest": "^27.4.2", - "babel-loader": "^8.2.3", - "babel-plugin-named-asset-import": "^0.3.8", - "babel-preset-react-app": "^10.0.1", - "bfj": "^7.0.2", - "browserslist": "^4.18.1", - "camelcase": "^6.2.1", - "case-sensitive-paths-webpack-plugin": "^2.4.0", - "css-loader": "^6.5.1", - "css-minimizer-webpack-plugin": "^3.2.0", - "dotenv": "^10.0.0", - "dotenv-expand": "^5.1.0", - "eslint": "^8.3.0", - "eslint-config-react-app": "^7.0.1", - "eslint-webpack-plugin": "^3.1.1", - "file-loader": "^6.2.0", - "fs-extra": "^10.0.0", - "html-webpack-plugin": "^5.5.0", - "identity-obj-proxy": "^3.0.0", - "jest": "^27.4.3", - "jest-resolve": "^27.4.2", - "jest-watch-typeahead": "^1.0.0", - "mini-css-extract-plugin": "^2.4.5", - "postcss": "^8.4.4", - "postcss-flexbugs-fixes": "^5.0.2", - "postcss-loader": "^6.2.1", - "postcss-normalize": "^10.0.1", - "postcss-preset-env": "^7.0.1", - "prompts": "^2.4.2", - "react-app-polyfill": "^3.0.0", - "react-dev-utils": "^12.0.1", - "react-refresh": "^0.11.0", - "resolve": "^1.20.0", - "resolve-url-loader": "^4.0.0", - "sass-loader": "^12.3.0", - "semver": "^7.3.5", - "source-map-loader": "^3.0.0", - "style-loader": "^3.3.1", - "tailwindcss": "^3.0.2", - "terser-webpack-plugin": "^5.2.5", - "webpack": "^5.64.4", - "webpack-dev-server": "^4.6.0", - "webpack-manifest-plugin": "^4.0.2", - "workbox-webpack-plugin": "^6.4.1" - }, - "bin": { - "react-scripts": "bin/react-scripts.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - }, - "peerDependencies": { - "react": ">= 16", - "typescript": "^3.2.1 || ^4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regex-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", - "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==" - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-url-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz", - "integrity": "sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==", - "dependencies": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^7.0.35", - "source-map": "0.6.1" - }, - "engines": { - "node": ">=8.9" - }, - "peerDependencies": { - "rework": "1.0.1", - "rework-visit": "1.0.0" - }, - "peerDependenciesMeta": { - "rework": { - "optional": true - }, - "rework-visit": { - "optional": true - } - } - }, - "node_modules/resolve-url-loader/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/resolve-url-loader/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/resolve-url-loader/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/resolve-url-loader/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve.exports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", - "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup-plugin-terser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", - "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "jest-worker": "^26.2.1", - "serialize-javascript": "^4.0.0", - "terser": "^5.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/rollup-plugin-terser/node_modules/jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/rollup-plugin-terser/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/sanitize.css": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", - "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" - }, - "node_modules/sass-loader": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", - "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", - "dependencies": { - "klona": "^2.0.4", - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - } - } - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", - "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-loader": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", - "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", - "dependencies": { - "abab": "^2.0.5", - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead" - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" - }, - "node_modules/static-eval": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", - "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", - "dependencies": { - "escodegen": "^1.8.1" - } - }, - "node_modules/static-eval/node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/static-eval/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/static-eval/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-eval/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-natural-compare": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz", - "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", - "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "dependencies": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", - "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/stylehacks": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", - "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-hyperlinks/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" - }, - "node_modules/svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", - "dependencies": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/svgo/node_modules/css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "node_modules/svgo/node_modules/css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/svgo/node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/svgo/node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - }, - "node_modules/svgo/node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dependencies": { - "boolbase": "~1.0.0" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" - }, - "node_modules/tailwindcss": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/tempy": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", - "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", - "dependencies": { - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.31.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", - "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/throat": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", - "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==" - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tryer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/tsconfig-paths/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/underscore": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", - "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-to-istanbul": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", - "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dependencies": { - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/web-vitals": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", - "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==" - }, - "node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "engines": { - "node": ">=10.4" - } - }, - "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-server": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", - "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.4", - "ws": "^8.13.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/webpack-manifest-plugin": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", - "integrity": "sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==", - "dependencies": { - "tapable": "^2.0.0", - "webpack-sources": "^2.2.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "peerDependencies": { - "webpack": "^4.44.2 || ^5.47.0" - } - }, - "node_modules/webpack-manifest-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-manifest-plugin/node_modules/webpack-sources": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", - "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", - "dependencies": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dependencies": { - "iconv-lite": "0.4.24" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" - }, - "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" - }, - "node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", - "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/workbox-background-sync": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", - "integrity": "sha512-jkf4ZdgOJxC9u2vztxLuPT/UjlH7m/nWRQ/MgGL0v8BJHoZdVGJd18Kck+a0e55wGXdqyHO+4IQTk0685g4MUw==", - "dependencies": { - "idb": "^7.0.1", - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-broadcast-update": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.6.0.tgz", - "integrity": "sha512-nm+v6QmrIFaB/yokJmQ/93qIJ7n72NICxIwQwe5xsZiV2aI93MGGyEyzOzDPVz5THEr5rC3FJSsO3346cId64Q==", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-build": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-6.6.0.tgz", - "integrity": "sha512-Tjf+gBwOTuGyZwMz2Nk/B13Fuyeo0Q84W++bebbVsfr9iLkDSo6j6PST8tET9HYA58mlRXwlMGpyWO8ETJiXdQ==", - "dependencies": { - "@apideck/better-ajv-errors": "^0.3.1", - "@babel/core": "^7.11.1", - "@babel/preset-env": "^7.11.0", - "@babel/runtime": "^7.11.2", - "@rollup/plugin-babel": "^5.2.0", - "@rollup/plugin-node-resolve": "^11.2.1", - "@rollup/plugin-replace": "^2.4.1", - "@surma/rollup-plugin-off-main-thread": "^2.2.3", - "ajv": "^8.6.0", - "common-tags": "^1.8.0", - "fast-json-stable-stringify": "^2.1.0", - "fs-extra": "^9.0.1", - "glob": "^7.1.6", - "lodash": "^4.17.20", - "pretty-bytes": "^5.3.0", - "rollup": "^2.43.1", - "rollup-plugin-terser": "^7.0.0", - "source-map": "^0.8.0-beta.0", - "stringify-object": "^3.3.0", - "strip-comments": "^2.0.1", - "tempy": "^0.6.0", - "upath": "^1.2.0", - "workbox-background-sync": "6.6.0", - "workbox-broadcast-update": "6.6.0", - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-google-analytics": "6.6.0", - "workbox-navigation-preload": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-range-requests": "6.6.0", - "workbox-recipes": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0", - "workbox-streams": "6.6.0", - "workbox-sw": "6.6.0", - "workbox-window": "6.6.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", - "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", - "dependencies": { - "json-schema": "^0.4.0", - "jsonpointer": "^5.0.0", - "leven": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/workbox-build/node_modules/ajv": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", - "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/workbox-build/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/workbox-build/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/workbox-build/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/workbox-build/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/workbox-build/node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "node_modules/workbox-build/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/workbox-cacheable-response": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.6.0.tgz", - "integrity": "sha512-JfhJUSQDwsF1Xv3EV1vWzSsCOZn4mQ38bWEBR3LdvOxSPgB65gAM6cS2CX8rkkKHRgiLrN7Wxoyu+TuH67kHrw==", - "deprecated": "workbox-background-sync@6.6.0", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-core": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.6.0.tgz", - "integrity": "sha512-GDtFRF7Yg3DD859PMbPAYPeJyg5gJYXuBQAC+wyrWuuXgpfoOrIQIvFRZnQ7+czTIQjIr1DhLEGFzZanAT/3bQ==" - }, - "node_modules/workbox-expiration": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.6.0.tgz", - "integrity": "sha512-baplYXcDHbe8vAo7GYvyAmlS4f6998Jff513L4XvlzAOxcl8F620O91guoJ5EOf5qeXG4cGdNZHkkVAPouFCpw==", - "dependencies": { - "idb": "^7.0.1", - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-google-analytics": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.6.0.tgz", - "integrity": "sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==", - "deprecated": "It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained", - "dependencies": { - "workbox-background-sync": "6.6.0", - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" - } - }, - "node_modules/workbox-navigation-preload": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.6.0.tgz", - "integrity": "sha512-utNEWG+uOfXdaZmvhshrh7KzhDu/1iMHyQOV6Aqup8Mm78D286ugu5k9MFD9SzBT5TcwgwSORVvInaXWbvKz9Q==", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-precaching": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.6.0.tgz", - "integrity": "sha512-eYu/7MqtRZN1IDttl/UQcSZFkHP7dnvr/X3Vn6Iw6OsPMruQHiVjjomDFCNtd8k2RdjLs0xiz9nq+t3YVBcWPw==", - "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" - } - }, - "node_modules/workbox-range-requests": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.6.0.tgz", - "integrity": "sha512-V3aICz5fLGq5DpSYEU8LxeXvsT//mRWzKrfBOIxzIdQnV/Wj7R+LyJVTczi4CQ4NwKhAaBVaSujI1cEjXW+hTw==", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-recipes": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.6.0.tgz", - "integrity": "sha512-TFi3kTgYw73t5tg73yPVqQC8QQjxJSeqjXRO4ouE/CeypmP2O/xqmB/ZFBBQazLTPxILUQ0b8aeh0IuxVn9a6A==", - "dependencies": { - "workbox-cacheable-response": "6.6.0", - "workbox-core": "6.6.0", - "workbox-expiration": "6.6.0", - "workbox-precaching": "6.6.0", - "workbox-routing": "6.6.0", - "workbox-strategies": "6.6.0" - } - }, - "node_modules/workbox-routing": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.6.0.tgz", - "integrity": "sha512-x8gdN7VDBiLC03izAZRfU+WKUXJnbqt6PG9Uh0XuPRzJPpZGLKce/FkOX95dWHRpOHWLEq8RXzjW0O+POSkKvw==", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-strategies": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.6.0.tgz", - "integrity": "sha512-eC07XGuINAKUWDnZeIPdRdVja4JQtTuc35TZ8SwMb1ztjp7Ddq2CJ4yqLvWzFWGlYI7CG/YGqaETntTxBGdKgQ==", - "dependencies": { - "workbox-core": "6.6.0" - } - }, - "node_modules/workbox-streams": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.6.0.tgz", - "integrity": "sha512-rfMJLVvwuED09CnH1RnIep7L9+mj4ufkTyDPVaXPKlhi9+0czCu+SJggWCIFbPpJaAZmp2iyVGLqS3RUmY3fxg==", - "dependencies": { - "workbox-core": "6.6.0", - "workbox-routing": "6.6.0" - } - }, - "node_modules/workbox-sw": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.6.0.tgz", - "integrity": "sha512-R2IkwDokbtHUE4Kus8pKO5+VkPHD2oqTgl+XJwh4zbF1HyjAbgNmK/FneZHVU7p03XUt9ICfuGDYISWG9qV/CQ==" - }, - "node_modules/workbox-webpack-plugin": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.6.0.tgz", - "integrity": "sha512-xNZIZHalboZU66Wa7x1YkjIqEy1gTR+zPM+kjrYJzqN7iurYZBctBLISyScjhkJKYuRrZUP0iqViZTh8rS0+3A==", - "dependencies": { - "fast-json-stable-stringify": "^2.1.0", - "pretty-bytes": "^5.4.1", - "upath": "^1.2.0", - "webpack-sources": "^1.4.3", - "workbox-build": "6.6.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": "^4.4.0 || ^5.9.0" - } - }, - "node_modules/workbox-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/workbox-webpack-plugin/node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "node_modules/workbox-window": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-6.6.0.tgz", - "integrity": "sha512-L4N9+vka17d16geaJXXRjENLFldvkWy7JyGxElRD0JvBxvFEd8LOhr+uXCcar/NzAmIBRv9EZ+M+Qr4mOoBITw==", - "dependencies": { - "@types/trusted-types": "^2.0.2", - "workbox-core": "6.6.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/examples/ratchet-moondream/package.json b/examples/ratchet-moondream/package.json deleted file mode 100644 index 2192cb8d..00000000 --- a/examples/ratchet-moondream/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "ratchet-moondream", - "version": "0.1.0", - "private": true, - "dependencies": { - "@emotion/react": "^11.11.4", - "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.15.19", - "@mui/material": "^5.15.19", - "@ratchet-ml/ratchet-web": "file:../../target/pkg/ratchet-web", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "devDependencies": { - "prettier": "3.3.1" - } -} diff --git a/examples/ratchet-moondream/public/index.html b/examples/ratchet-moondream/public/index.html deleted file mode 100644 index 5d1e5bc2..00000000 --- a/examples/ratchet-moondream/public/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - Ratchet Moondream - - - -
- - - diff --git a/examples/ratchet-moondream/public/manifest.json b/examples/ratchet-moondream/public/manifest.json deleted file mode 100644 index 080d6c77..00000000 --- a/examples/ratchet-moondream/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/examples/ratchet-moondream/public/robots.txt b/examples/ratchet-moondream/public/robots.txt deleted file mode 100644 index e9e57dc4..00000000 --- a/examples/ratchet-moondream/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/examples/ratchet-moondream/src/App.css b/examples/ratchet-moondream/src/App.css deleted file mode 100644 index 74b5e053..00000000 --- a/examples/ratchet-moondream/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/examples/ratchet-moondream/src/App.js b/examples/ratchet-moondream/src/App.js deleted file mode 100644 index 340e48be..00000000 --- a/examples/ratchet-moondream/src/App.js +++ /dev/null @@ -1,242 +0,0 @@ -import "./App.css"; -import { Model, Quantization, default as init } from "@ratchet-ml/ratchet-web"; -import { styled } from "@mui/material/styles"; -import { useState, useEffect } from "react"; -import { - LinearProgress, - TextField, - Button, - Container, - Card, - CardMedia, - Stack, - Box, - Dialog, - DialogActions, - DialogContentText, - DialogTitle, - DialogContent, - Typography, - CardActions, - InputAdornment, - IconButton, -} from "@mui/material"; -import SendIcon from "@mui/icons-material/Send"; - -const VisuallyHiddenInput = styled("input")({ - clip: "rect(0 0 0 0)", - clipPath: "inset(50%)", - height: 1, - overflow: "hidden", - position: "absolute", - bottom: 0, - left: 0, - whiteSpace: "nowrap", - width: 1, -}); - -function App() { - const [question, setQuestion] = useState(""); - const [generatedText, setGeneratedText] = useState(""); - const [image, setImage] = useState(new Uint8Array()); - const [progress, setProgress] = useState(0); - const [isLoading, setIsLoading] = useState(true); - const [accepted, setAccepted] = useState(false); - const [isSupportedBrowser, setIsSupportedBrowser] = useState(true); - const [ratchetDBExists, setRatchetDBExists] = useState(false); - const [model, setModel] = useState(null); - const [isRunning, setIsRunning] = useState(false); - - useEffect(() => { - (async () => { - await init(); - setRatchetDBExists( - (await window.indexedDB.databases()) - .map((db) => db.name) - .includes("ratchet"), - ); - await setImage( - new Uint8Array( - await ( - await fetch( - "https://raw.githubusercontent.com/vikhyat/moondream/main/assets/demo-1.jpg", - ) - ).arrayBuffer(), - ), - ); - })(); - }, []); - - async function loadModel() { - setAccepted(true); - setProgress(2); - setModel( - await Model.load("Moondream", Quantization.Q8_0, (p) => setProgress(p)), - ); - setProgress(100); - setIsLoading(false); - } - - async function runModel() { - if (!model || isRunning) { - return; - } - - setGeneratedText(""); - - let cb = (s) => { - setGeneratedText((prevText) => { - return prevText + s; - }); - }; - - setIsRunning(true); - await model.run({ question: question, image_bytes: image, callback: cb }); - setIsRunning(false); - } - - async function handleUpload(e) { - if (e.target.files.length == 0) { - return; - } - setImage(new Uint8Array(await e.target.files[0].arrayBuffer())); - } - - async function keypress(e) { - if (e.key === "Enter") { - runModel(); - e.preventDefault(); - } - } - - async function deleteWeights() { - setAccepted(false); - setProgress(0); - setModel(null); - await window.indexedDB.deleteDatabase("ratchet"); - setIsLoading(true); - } - - return ( -
- - - - {navigator.gpu ? "Load Model" : "Unsupported Browser"} - - - - {navigator.gpu - ? "This app requires downloading a 2.2GB model which may take a few minutes. If the model has been previously downloaded, it will be loaded from cache." - : "This app requires a browser that supports webgpu"} - - - {navigator.gpu ? ( - - - - ) : ( - <> - )} - - - - - Moondream by{" "} - Vikhyat running - on WebGpu via{" "} - Ratchet - - - - - - - - - - - - - setQuestion(e.target.value)} - onKeyDown={keypress} - InputProps={{ - endAdornment: ( - - - - - - ), - }} - /> - -
- -
- {isLoading && progress < 99 ? ( - - Downloading Weights... - - ) : ( - <> - )} - {isLoading && progress > 99 ? ( - - Preparing Weights... - - ) : ( - <> - )} -
- {generatedText} -
-
-
-
- ); -} - -export default App; diff --git a/examples/ratchet-moondream/src/index.css b/examples/ratchet-moondream/src/index.css deleted file mode 100644 index 4a1df4db..00000000 --- a/examples/ratchet-moondream/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; -} diff --git a/examples/ratchet-moondream/src/index.js b/examples/ratchet-moondream/src/index.js deleted file mode 100644 index df868170..00000000 --- a/examples/ratchet-moondream/src/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import "./index.css"; -import App from "./App"; - -const root = ReactDOM.createRoot(document.getElementById("root")); -root.render( - - - , -); diff --git a/examples/ratchet-phi/.gitignore b/examples/ratchet-phi/.gitignore deleted file mode 100644 index fd3dbb57..00000000 --- a/examples/ratchet-phi/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/examples/ratchet-phi/README.md b/examples/ratchet-phi/README.md deleted file mode 100644 index c4033664..00000000 --- a/examples/ratchet-phi/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples/ratchet-phi/next.config.mjs b/examples/ratchet-phi/next.config.mjs deleted file mode 100644 index 15eff7c7..00000000 --- a/examples/ratchet-phi/next.config.mjs +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - output: 'export' -}; - -export default nextConfig; diff --git a/examples/ratchet-phi/package.json b/examples/ratchet-phi/package.json deleted file mode 100644 index 2be8c174..00000000 --- a/examples/ratchet-phi/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "ratchet-phi", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@ffmpeg/ffmpeg": "0.12.6", - "@ffmpeg/util": "^0.12.1", - "@ratchet-ml/ratchet-web": "link:../../target/pkg/ratchet-web", - "fix-webm-duration": "^1.0.5", - "next": "14.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-hot-toast": "^2.4.1", - "react-responsive-modal": "^6.4.2" - }, - "devDependencies": { - "@types/node": "^20.11.24", - "@types/react": "^18.2.61", - "@types/react-dom": "^18.2.19", - "autoprefixer": "^10.4.18", - "postcss": "^8.4.35", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3" - } -} diff --git a/examples/ratchet-phi/postcss.config.js b/examples/ratchet-phi/postcss.config.js deleted file mode 100644 index 33ad091d..00000000 --- a/examples/ratchet-phi/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/examples/ratchet-phi/src/app/components/WebGPUModal.tsx b/examples/ratchet-phi/src/app/components/WebGPUModal.tsx deleted file mode 100644 index 91ff1134..00000000 --- a/examples/ratchet-phi/src/app/components/WebGPUModal.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useState, useEffect } from "react"; -import Modal from "react-responsive-modal"; - -const WebGPUModal = () => { - const [hasWebGPU, setHasWebGPU] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); - - useEffect(() => { - //@ts-ignore - if (!navigator.gpu) { - setIsModalOpen(true); - return; - } - setHasWebGPU(true); - }, []); - - const handleModalClose = () => { - setIsModalOpen(false); - }; - - const closeIcon = ( - - - - - - - - - - - - - - - - - - ); - - return ( - <> - {!hasWebGPU ? ( - -
-
-

- Uh oh! It looks like your browser doesn't - support WebGPU. Please try again in supported browser (Chrome 121+). -

-
-
-
- ) : ( - <> - )} - - ); -}; - -export default WebGPUModal; - diff --git a/examples/ratchet-phi/src/app/components/progressBar.tsx b/examples/ratchet-phi/src/app/components/progressBar.tsx deleted file mode 100644 index 732d98fc..00000000 --- a/examples/ratchet-phi/src/app/components/progressBar.tsx +++ /dev/null @@ -1,19 +0,0 @@ -const ProgressBar = ({ progress }: any) => { - return ( - <> - {progress > 0 && progress < 100 && ( -
-
-
-
-
- )} - - ); -}; - -export default ProgressBar; - diff --git a/examples/ratchet-phi/src/app/components/warningModal.tsx b/examples/ratchet-phi/src/app/components/warningModal.tsx deleted file mode 100644 index 37e93d74..00000000 --- a/examples/ratchet-phi/src/app/components/warningModal.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from "react"; -import Modal from "react-responsive-modal"; - -interface WarningModalProps { - isModalOpen: boolean; - setIsModalOpen: (value: boolean) => void; - loadModel: () => void; -} - -const WarningModal = ({ isModalOpen, setIsModalOpen, loadModel }: WarningModalProps) => { - const handleModalClose = () => { - setIsModalOpen(false); - }; - - const closeIcon = ( - - - - - - - - - - - - - - - - - - ); - - return ( - <> - {isModalOpen ? ( - -
-
-

- ⚠️ You are about to download a 2.9GB file. Click to confirm. -

- -
-
-
- ) : ( - <> - )} - - ); -}; - -export default WarningModal; - diff --git a/examples/ratchet-phi/src/app/favicon.ico b/examples/ratchet-phi/src/app/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/examples/ratchet-phi/src/app/favicon.ico and /dev/null differ diff --git a/examples/ratchet-phi/src/app/globals.css b/examples/ratchet-phi/src/app/globals.css deleted file mode 100644 index f3990aba..00000000 --- a/examples/ratchet-phi/src/app/globals.css +++ /dev/null @@ -1,344 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -.loader { - animation: spin 1s linear infinite; - height: 10px; - width: 10px; - margin: -5px; - scale: 0.5; -} - -@keyframes spin { - 0% { - box-shadow: - 0px -30px #000, - 10px -30px #000, - 20px -20px #000, - 30px -10px #000, - 30px 0px #000, - 30px 10px #000, - 20px 20px #000, - 10px 30px #000, - 0px 30px transparent, - -10px 30px transparent, - -20px 20px transparent, - -30px 10px transparent, - -30px 0px transparent, - -30px -10px transparent, - -20px -20px transparent, - -10px -30px transparent; - } - 6.25% { - box-shadow: - 0px -30px transparent, - 10px -30px #000, - 20px -20px #000, - 30px -10px #000, - 30px 0px #000, - 30px 10px #000, - 20px 20px #000, - 10px 30px #000, - 0px 30px #000, - -10px 30px transparent, - -20px 20px transparent, - -30px 10px transparent, - -30px 0px transparent, - -30px -10px transparent, - -20px -20px transparent, - -10px -30px transparent; - } - 12.5% { - box-shadow: - 0px -30px transparent, - 10px -30px transparent, - 20px -20px #000, - 30px -10px #000, - 30px 0px #000, - 30px 10px #000, - 20px 20px #000, - 10px 30px #000, - 0px 30px #000, - -10px 30px #000, - -20px 20px transparent, - -30px 10px transparent, - -30px 0px transparent, - -30px -10px transparent, - -20px -20px transparent, - -10px -30px transparent; - } - 18.75% { - box-shadow: - 0px -30px transparent, - 10px -30px transparent, - 20px -20px transparent, - 30px -10px #000, - 30px 0px #000, - 30px 10px #000, - 20px 20px #000, - 10px 30px #000, - 0px 30px #000, - -10px 30px #000, - -20px 20px #000, - -30px 10px transparent, - -30px 0px transparent, - -30px -10px transparent, - -20px -20px transparent, - -10px -30px transparent; - } - 25% { - box-shadow: - 0px -30px transparent, - 10px -30px transparent, - 20px -20px transparent, - 30px -10px transparent, - 30px 0px #000, - 30px 10px #000, - 20px 20px #000, - 10px 30px #000, - 0px 30px #000, - -10px 30px #000, - -20px 20px #000, - -30px 10px #000, - -30px 0px transparent, - -30px -10px transparent, - -20px -20px transparent, - -10px -30px transparent; - } - 31.25% { - box-shadow: - 0px -30px transparent, - 10px -30px transparent, - 20px -20px transparent, - 30px -10px transparent, - 30px 0px transparent, - 30px 10px #000, - 20px 20px #000, - 10px 30px #000, - 0px 30px #000, - -10px 30px #000, - -20px 20px #000, - -30px 10px #000, - -30px 0px #000, - -30px -10px transparent, - -20px -20px transparent, - -10px -30px transparent; - } - 37.5% { - box-shadow: - 0px -30px transparent, - 10px -30px transparent, - 20px -20px transparent, - 30px -10px transparent, - 30px 0px transparent, - 30px 10px transparent, - 20px 20px #000, - 10px 30px #000, - 0px 30px #000, - -10px 30px #000, - -20px 20px #000, - -30px 10px #000, - -30px 0px #000, - -30px -10px #000, - -20px -20px transparent, - -10px -30px transparent; - } - 43.75% { - box-shadow: - 0px -30px transparent, - 10px -30px transparent, - 20px -20px transparent, - 30px -10px transparent, - 30px 0px transparent, - 30px 10px transparent, - 20px 20px transparent, - 10px 30px #000, - 0px 30px #000, - -10px 30px #000, - -20px 20px #000, - -30px 10px #000, - -30px 0px #000, - -30px -10px #000, - -20px -20px #000, - -10px -30px transparent; - } - 50% { - box-shadow: - 0px -30px transparent, - 10px -30px transparent, - 20px -20px transparent, - 30px -10px transparent, - 30px 0px transparent, - 30px 10px transparent, - 20px 20px transparent, - 10px 30px transparent, - 0px 30px #000, - -10px 30px #000, - -20px 20px #000, - -30px 10px #000, - -30px 0px #000, - -30px -10px #000, - -20px -20px #000, - -10px -30px #000; - } - 56.25% { - box-shadow: - 0px -30px #000, - 10px -30px transparent, - 20px -20px transparent, - 30px -10px transparent, - 30px 0px transparent, - 30px 10px transparent, - 20px 20px transparent, - 10px 30px transparent, - 0px 30px transparent, - -10px 30px #000, - -20px 20px #000, - -30px 10px #000, - -30px 0px #000, - -30px -10px #000, - -20px -20px #000, - -10px -30px #000; - } - 62.5% { - box-shadow: - 0px -30px #000, - 10px -30px #000, - 20px -20px transparent, - 30px -10px transparent, - 30px 0px transparent, - 30px 10px transparent, - 20px 20px transparent, - 10px 30px transparent, - 0px 30px transparent, - -10px 30px transparent, - -20px 20px #000, - -30px 10px #000, - -30px 0px #000, - -30px -10px #000, - -20px -20px #000, - -10px -30px #000; - } - 68.75% { - box-shadow: - 0px -30px #000, - 10px -30px #000, - 20px -20px #000, - 30px -10px transparent, - 30px 0px transparent, - 30px 10px transparent, - 20px 20px transparent, - 10px 30px transparent, - 0px 30px transparent, - -10px 30px transparent, - -20px 20px transparent, - -30px 10px #000, - -30px 0px #000, - -30px -10px #000, - -20px -20px #000, - -10px -30px #000; - } - 75% { - box-shadow: - 0px -30px #000, - 10px -30px #000, - 20px -20px #000, - 30px -10px #000, - 30px 0px transparent, - 30px 10px transparent, - 20px 20px transparent, - 10px 30px transparent, - 0px 30px transparent, - -10px 30px transparent, - -20px 20px transparent, - -30px 10px transparent, - -30px 0px #000, - -30px -10px #000, - -20px -20px #000, - -10px -30px #000; - } - 81.25% { - box-shadow: - 0px -30px #000, - 10px -30px #000, - 20px -20px #000, - 30px -10px #000, - 30px 0px #000, - 30px 10px transparent, - 20px 20px transparent, - 10px 30px transparent, - 0px 30px transparent, - -10px 30px transparent, - -20px 20px transparent, - -30px 10px transparent, - -30px 0px transparent, - -30px -10px #000, - -20px -20px #000, - -10px -30px #000; - } - 87.5% { - box-shadow: - 0px -30px #000, - 10px -30px #000, - 20px -20px #000, - 30px -10px #000, - 30px 0px #000, - 30px 10px #000, - 20px 20px transparent, - 10px 30px transparent, - 0px 30px transparent, - -10px 30px transparent, - -20px 20px transparent, - -30px 10px transparent, - -30px 0px transparent, - -30px -10px transparent, - -20px -20px #000, - -10px -30px #000; - } - 93.75% { - box-shadow: - 0px -30px #000, - 10px -30px #000, - 20px -20px #000, - 30px -10px #000, - 30px 0px #000, - 30px 10px #000, - 20px 20px #000, - 10px 30px transparent, - 0px 30px transparent, - -10px 30px transparent, - -20px 20px transparent, - -30px 10px transparent, - -30px 0px transparent, - -30px -10px transparent, - -20px -20px transparent, - -10px -30px #000; - } - 100% { - box-shadow: - 0px -30px #000, - 10px -30px #000, - 20px -20px #000, - 30px -10px #000, - 30px 0px #000, - 30px 10px #000, - 20px 20px #000, - 10px 30px #000, - 0px 30px transparent, - -10px 30px transparent, - -20px 20px transparent, - -30px 10px transparent, - -30px 0px transparent, - -30px -10px transparent, - -20px -20px transparent, - -10px -30px transparent; - } -} - diff --git a/examples/ratchet-phi/src/app/layout.tsx b/examples/ratchet-phi/src/app/layout.tsx deleted file mode 100644 index 90602f5d..00000000 --- a/examples/ratchet-phi/src/app/layout.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; -import { Toaster } from "react-hot-toast"; -import "react-responsive-modal/styles.css"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Ratchet + Phi", - description: "Simple demo of Phi.", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - -
- -
{children}
-
- - - ); -} diff --git a/examples/ratchet-phi/src/app/page.module.css b/examples/ratchet-phi/src/app/page.module.css deleted file mode 100644 index c13f8527..00000000 --- a/examples/ratchet-phi/src/app/page.module.css +++ /dev/null @@ -1,236 +0,0 @@ -.main { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - padding: 6rem; - min-height: 100vh; -} - -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); -} - -.description a { - display: flex; - justify-content: center; - align-items: center; - gap: 0.5rem; -} - -.description p { - position: relative; - margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); -} - -.code { - font-weight: 700; - font-family: var(--font-mono); -} - -.grid { - display: grid; - grid-template-columns: repeat(4, minmax(25%, auto)); - max-width: 100%; - width: var(--max-width); -} - -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; -} - -.card span { - display: inline-block; - transition: transform 200ms; -} - -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; -} - -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 30ch; - text-wrap: balance; -} - -.center { - display: flex; - justify-content: center; - align-items: center; - position: relative; - padding: 4rem 0; -} - -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; -} - -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ""; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo { - position: relative; -} -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); - } - - .card:hover span { - transform: translateX(4px); - } -} - -@media (prefers-reduced-motion) { - .card:hover span { - transform: none; - } -} - -/* Mobile */ -@media (max-width: 700px) { - .content { - padding: 4rem; - } - - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; - } - - .center::before { - transform: none; - height: 300px; - } - - .description { - font-size: 0.8rem; - } - - .description a { - padding: 1rem; - } - - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; - } - - .description p { - align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient( - to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5) - ); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient( - to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40% - ); - z-index: 1; - } -} - -/* Tablet and Smaller Desktop */ -@media (min-width: 701px) and (max-width: 1120px) { - .grid { - grid-template-columns: repeat(2, 50%); - } -} - -@media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - - .logo { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - to { - transform: rotate(0deg); - } -} - -.buttonsContainer { - display: flex; - justify-content: center; /* Center buttons horizontally */ - gap: 10px; /* Add some space between the buttons */ -} diff --git a/examples/ratchet-phi/src/app/page.tsx b/examples/ratchet-phi/src/app/page.tsx deleted file mode 100644 index d9f3aa86..00000000 --- a/examples/ratchet-phi/src/app/page.tsx +++ /dev/null @@ -1,140 +0,0 @@ -'use client' - -import { AvailableModels, Model, Quantization, default as init } from "@ratchet-ml/ratchet-web"; -import { useEffect, useState } from "react"; -import ProgressBar from "./components/progressBar"; -import WebGPUModal from "./components/WebGPUModal"; -import WarningModal from "./components/warningModal"; - -export default function Home() { - const [selectedModel, setSelectedModel] = useState({ Phi: "phi3" }); - const [loadedModel, setLoadedModel] = useState(null); - const [model, setModel] = useState(null); - const [generating, setGenerating] = useState(false); - const [progress, setProgress] = useState(0); - const [generatedText, setGeneratedText] = useState(""); - const [prompt, setPrompt] = useState("What is the meaning of life?"); - const [loadingModel, setLoadingModel] = useState(false); - const [isWarningOpen, setIsWarningOpen] = useState(false); - const [ratchetDBExists, setRatchetDBExists] = useState(false); - - - useEffect(() => { - (async () => { - await init(); - setRatchetDBExists((await window.indexedDB.databases()).map(db => db.name).includes("ratchet")); - })(); - }, []); - - async function loadModel() { - setLoadingModel(true); - setModel(await Model.load(selectedModel, Quantization.Q8_0, (p: number) => setProgress(p))); - setLoadedModel(selectedModel); - setProgress(0); - setLoadingModel(false); - } - - async function runModel() { - if (!model || generating) { - return; - } - - setGenerating(true); - setGeneratedText(""); - - let cb = (s: string) => { - setGeneratedText((prevText) => { - return prevText + s.replace(/\n/g, "
"); - }); - }; - - let input = { - prompt: prompt, - callback: cb, - }; - - await model.run(input); - setGenerating(false); - } - - return ( -
-
-

Ratchet + Phi3

- {generatedText ? -
-

-
- : <>} -
- -
- -