From fd2634be1504136476d45fa06d9253227cd85283 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:15:29 +0000 Subject: [PATCH 1/2] chore(deps): bump docker/build-push-action from 6 to 7 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6...v7) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index c64ce3f..20963e0 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -59,7 +59,7 @@ jobs: type=raw,value=latest,enable=${{ inputs.push_image == 'true' }} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: .devops/Dockerfile.backend From 5d0f109c86d80dbfc993e8f73ee38d6309a42be8 Mon Sep 17 00:00:00 2001 From: Eamon Date: Thu, 23 Jul 2026 20:25:13 +0530 Subject: [PATCH 2/2] tensor: Add OpenMP multi-threading to AVX/SSE (#LMGNU58) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: bump postcss from 8.5.15 to 8.5.19 in /frontend (#29) Bumps [postcss](https://github.com/postcss/postcss) from 8.5.15 to 8.5.19. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.15...8.5.19) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.19 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Remove LibTorch C++ inference implementation * Remove LibTorch C++ inference implementation (#30) * Delete .env.example * Delete index.html * Delete manifest.webmanifest * Delete postcss.config.js * Delete icon.svg * Delete manifest.webmanifest * Delete sw.js * Delete chat.ts * Delete client.ts * Delete health.ts * Delete sessions.ts * Delete App.tsx * Delete ChatView.tsx * Delete EmptyState.tsx * Delete MessageAvatar.tsx * Delete MessageList.tsx * Delete torch_bridge.h * Delete tensor.h * Delete sampler.h * Delete MessageRow.tsx * Delete StarterPrompts.tsx * Delete ThinkingIndicator.tsx * Delete CharCounter.tsx * Delete InputBar.tsx * Delete AppLayout.tsx * Delete Sidebar.tsx * Delete Topbar.tsx * Delete ModelBadge.tsx * Delete StatsPanel.tsx * Delete SessionItem.tsx * mha: Clean up formatting and remove redundant headers in attention heads The current code has a bunch of sloppy header duplicates and messy whitespace in the comments. It’s readable, but it's unnecessarily bloated. Fix this by: - Stripping out the duplicate includes for "linear.h" and "tensor.h". - Cleaning up the constructor formatting for both Head and MultiHeadAttention so the initializers aren't stretched out across random lines. - Tidying up the shape tracking comments to keep the documentation consistent across the forward passes. No functional changes, just making the code look like it was written with care. * backward: Clean up formatting and remove redundant divider comments Tidies up the backward pass codebase by removing noisy visual dividers and flattening multi-line constructor initializers. Fix this by: - Wrapping extended single-line initializers across `GradLinear`, `GradHead`, `GradMHA`, `GradFFN`, and `GradBlock` into single, readable lines. - Stripping out bloated visual comment blocks (`// ===...`) that clutter the file layout. - Cleaning up formatting for primitive backward functions (`backward_linear`, `backward_layernorm`, `backward_bmm`) to enforce consistent horizontal spacing. No logical or structural alterations to the backward operations. * block: Clean up constructor initialization and comments in Transformer block Tidies up the `Block` struct implementation by consolidating the member initializer list and adjusting the documentation style. Fix this by: - Condensing the multi-line initializer list for `sa`, `ffwd`, `ln1`, and `ln2` into a single, clean line expression. - Cleaning up the formatting around the constructor's body braces. - Adjusting whitespace within the forward pass dimension tracking comments to match the surrounding codebase's style. No functional changes were made to the forward pass execution logic. * style: Add strict LLVM-based .clang-format configuration Establishes a unified styling standard across the codebase using a custom LLVM-derived configuration. Key rules enforced: - 6-space indentation width with tabs disabled. - Allman-style brace breaking for all blocks. - Column limit capped at 100 characters. - Line breaks before colons in constructor initializers. - Disabled argument/parameter bin-packing to keep listings vertical and clean. * llm: migrate main entry point to Objective-C++ Migrate the primary runtime file to Objective-C++ (.mm) to leverage native Foundation APIs for high-level operating system interactions. This transition retains the core C++ machine learning logic and underlying headers entirely intact while modernizing path manipulation, environment variable parsing, and command-line argument handling via NSFileManager, NSString, and NSProcessInfo. An @autoreleasepool block has been introduced to ensure proper memory management of the Foundation elements during runtime. Signed-off-by: Eamon * llm: port C architecture implementation to Objective-C Port the standalone C inference implementation to Objective-C (.mm) by integrating native Foundation framework components. The core matrix arithmetic, neural network layers, tensor reading, and memory allocations remain unchanged to preserve low-level performance. File path validation and loading configurations for 'vocab.bin' and 'weights.bin' have been migrated to use NSFileManager. Wrapped the primary runtime execution and file I/O operations inside an @autoreleasepool block to manage memory allocations safely within the Cocoa runtime ecosystem. * bench: Port the standalone C++ benchmark runner to Objective-C++ (.mm) by re-implementing environment and platform-level operations using the native Cocoa Foundation framework. The foundational C++ primitives, template architectures, network forwards, and AdamW backward passes remain unchanged to ensure that the benchmark metrics and latency tracking are strictly unaltered. Argument parsing has been migrated from standard char-array loops to NSProcessInfo and NSArray, while file/directory checks now utilize NSFileManager. The entire workload execution setup and execution sequence has been encapsulated within a top-level @autoreleasepool block to manage Foundation-scoped memory correctly. Signed-off-by: Eamon * Add CITATION.cff for llm.cpp software This file contains citation metadata for the llm.cpp software, including authors, identifiers, abstract, and keywords. * Rename executable from quadtrix.exe to llm.exe * Update executable name in README instructions * Add new image to README banner for llm.cpp Added a new image to the README for enhanced visual appeal. * Delete Makefile * chore: bump react-dom and @types/react-dom in /frontend (#25) * chore(deps): bump zustand from 5.0.12 to 5.0.14 in /frontend (#28) * chore(deps-dev): bump @vitejs/plugin-react from 4.7.0 to 6.0.3 in /frontend (#26) chore(deps-dev): bump @vitejs/plugin-react in /frontend Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.7.0 to 6.0.3. - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.3/packages/plugin-react) --- updated-dependencies: - dependency-name: "@vitejs/plugin-react" dependency-version: 6.0.3 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: Eamon2009 Co-authored-by: Eamon2009 * Update README with image and quick start section Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets (#35) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Configure GPT-style model hyperparameters and tokenizer (#36) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * V1.3.15 (#37) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * docs: Add README covering Python setups, PyTorch inference, and C++ builds (#39) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * docs: Add README covering Python setups, PyTorch inference, and C++ builds * doc: Add training and validation loss curves to assets (#38) * Update README with image and quick start section Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets (#35) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Configure GPT-style model hyperparameters and tokenizer (#36) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * V1.3.15 (#37) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * doc: Update training plots with generalization gap and 7k iter metrics (#40) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * docs: Add README covering Python setups, PyTorch inference, and C++ builds * doc: Add training and validation loss curves to assets (#38) * Update README with image and quick start section Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets (#35) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Configure GPT-style model hyperparameters and tokenizer (#36) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * V1.3.15 (#37) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * doc: Update training plots with generalization gap and 7k iter metrics * maint: Add config.hpp with default model hyperparameters and constants (#41) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * docs: Add README covering Python setups, PyTorch inference, and C++ builds * doc: Add training and validation loss curves to assets (#38) * Update README with image and quick start section Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets (#35) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Configure GPT-style model hyperparameters and tokenizer (#36) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * V1.3.15 (#37) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * doc: Update training plots with generalization gap and 7k iter metrics * main: Add config.h with default model hyperparameters and constants * data: Add script to stream and compile TinyStories dataset (#42) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * docs: Add README covering Python setups, PyTorch inference, and C++ builds * doc: Add training and validation loss curves to assets (#38) * Update README with image and quick start section Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets (#35) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Configure GPT-style model hyperparameters and tokenizer (#36) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * V1.3.15 (#37) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * doc: Update training plots with generalization gap and 7k iter metrics * main: Add config.h with default model hyperparameters and constants * data: Add script to stream and compile TinyStories dataset * data: Add script to stream and compile TinyStories dataset Introduce a data preparation script that streams the TinyStories dataset from Hugging Face and extracts stories sequentially into a flat text file. The script is configured to: - Enable dataset streaming mode (`streaming=True`) to minimize local storage and RAM footprints during processing. - Intercept and append structural newline spacing (`\n\n`) between discrete story blocks. - Track exact byte sizing to dynamically enforce an upper threshold limit (defaulting to a 10,000 MB cap) before safely closing out output generation. Saves the compiled corpus to 'input.txt' for downstream tokenization. * main: Add foundational training and inference libraries to requirements.txt (#43) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * docs: Add README covering Python setups, PyTorch inference, and C++ builds * doc: Add training and validation loss curves to assets (#38) * Update README with image and quick start section Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets (#35) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Configure GPT-style model hyperparameters and tokenizer (#36) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * V1.3.15 (#37) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * doc: Update training plots with generalization gap and 7k iter metrics * main: Add config.h with default model hyperparameters and constants * data: Add script to stream and compile TinyStories dataset * data: Add script to stream and compile TinyStories dataset Introduce a data preparation script that streams the TinyStories dataset from Hugging Face and extracts stories sequentially into a flat text file. The script is configured to: - Enable dataset streaming mode (`streaming=True`) to minimize local storage and RAM footprints during processing. - Intercept and append structural newline spacing (`\n\n`) between discrete story blocks. - Track exact byte sizing to dynamically enforce an upper threshold limit (defaulting to a 10,000 MB cap) before safely closing out output generation. Saves the compiled corpus to 'input.txt' for downstream tokenization. * main: Add foundational training and inference libraries to requirements.txt * ci: Add multi-platform automated compilation and release workflow (#44) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * docs: Add README covering Python setups, PyTorch inference, and C++ builds * doc: Add training and validation loss curves to assets (#38) * Update README with image and quick start section Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets (#35) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Configure GPT-style model hyperparameters and tokenizer (#36) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * V1.3.15 (#37) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * doc: Update training plots with generalization gap and 7k iter metrics * main: Add config.h with default model hyperparameters and constants * data: Add script to stream and compile TinyStories dataset * data: Add script to stream and compile TinyStories dataset Introduce a data preparation script that streams the TinyStories dataset from Hugging Face and extracts stories sequentially into a flat text file. The script is configured to: - Enable dataset streaming mode (`streaming=True`) to minimize local storage and RAM footprints during processing. - Intercept and append structural newline spacing (`\n\n`) between discrete story blocks. - Track exact byte sizing to dynamically enforce an upper threshold limit (defaulting to a 10,000 MB cap) before safely closing out output generation. Saves the compiled corpus to 'input.txt' for downstream tokenization. * main: Add foundational training and inference libraries to requirements.txt * ci: Add multi-platform automated compilation and release workflow * docker: Remove development Dockerfile for the frontend (#45) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * docs: Add README covering Python setups, PyTorch inference, and C++ builds * doc: Add training and validation loss curves to assets (#38) * Update README with image and quick start section Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets (#35) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Configure GPT-style model hyperparameters and tokenizer (#36) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * V1.3.15 (#37) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * doc: Update training plots with generalization gap and 7k iter metrics * main: Add config.h with default model hyperparameters and constants * data: Add script to stream and compile TinyStories dataset * data: Add script to stream and compile TinyStories dataset Introduce a data preparation script that streams the TinyStories dataset from Hugging Face and extracts stories sequentially into a flat text file. The script is configured to: - Enable dataset streaming mode (`streaming=True`) to minimize local storage and RAM footprints during processing. - Intercept and append structural newline spacing (`\n\n`) between discrete story blocks. - Track exact byte sizing to dynamically enforce an upper threshold limit (defaulting to a 10,000 MB cap) before safely closing out output generation. Saves the compiled corpus to 'input.txt' for downstream tokenization. * main: Add foundational training and inference libraries to requirements.txt * ci: Add multi-platform automated compilation and release workflow * docker: Remove development Dockerfile for the frontend * Add mypy configuration with strict settings for .py * maint: Add lmgnu as a Git submodule in libs/lmgnu (#46) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * docs: Add README covering Python setups, PyTorch inference, and C++ builds * doc: Add training and validation loss curves to assets (#38) * Update README with image and quick start section Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets (#35) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Configure GPT-style model hyperparameters and tokenizer (#36) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * V1.3.15 (#37) * Update README with image and quick start section (#34) Added an image and updated the quick start section. * doc: Add training and validation loss curves to assets Introduce visual plots tracking cross-entropy loss over 6,000 steps and ~78 minutes of wall-clock training time. These curves serve as a baseline reference for the model's convergence behavior. The plots highlight a steady decline in training loss down to ~3.4, while validation loss plateaus early on, achieving its best score of 4.1319 at step 3900. Adding these assets to the repository ensures we have a clear, permanent record of this run's performance to compare against future optimization and hyperparameter tuning passes. * main: Config model hyperparameters and tokenizer * Delete quadtrix_training_report.png * Delete run.md * doc: Update training plots with generalization gap and 7k iter metrics * main: Add config.h with default model hyperparameters and constants * data: Add script to stream and compile TinyStories dataset * data: Add script to stream and compile TinyStories dataset Introduce a data preparation script that streams the TinyStories dataset from Hugging Face and extracts stories sequentially into a flat text file. The script is configured to: - Enable dataset streaming mode (`streaming=True`) to minimize local storage and RAM footprints during processing. - Intercept and append structural newline spacing (`\n\n`) between discrete story blocks. - Track exact byte sizing to dynamically enforce an upper threshold limit (defaulting to a 10,000 MB cap) before safely closing out output generation. Saves the compiled corpus to 'input.txt' for downstream tokenization. * main: Add foundational training and inference libraries to requirements.txt * ci: Add multi-platform automated compilation and release workflow * docker: Remove development Dockerfile for the frontend * main: Add lmgnu as a Git submodule in libs/lmgnu * maint: Implement comprehensive .gitignore rule suite (#47) * Add lmgnu as a Git submodule in libs/lmgnu (#48) * Fix typos and improve wording in README.md * Delete backward.h * Delete block.h * Delete char_level.h * Delete dataloader.h * Delete embedding.h * Delete feedforward.h * Delete gpt.h * Delete layernorm.h * Delete linear.h * Delete quadtrix.h * math: Implement fundamental Tensor struct with AVX/SSE acceleration (#49) * math: Implement fundamental Tensor struct with AVX/SSE acceleration * Add Python version specification for code * Update README.md * Update README to enhance clarity on project purpose Clarified the purpose of the repository and emphasized the C++ core implementation for learning about GPT construction without framework dependencies. * Revise README for clarity and additional details Updated the README to enhance clarity and detail about the project's implementation, performance metrics, and contributions. Added a leaderboard section and improved descriptions of the C++ core and its comparison with PyTorch implementations. * config: Tune training scale hyperparameters (#50) * Enhance README with BPE and training instructions Added token level BPE implementation details and updated training instructions. * Delete benches/python_benchmark.py no need * Enhance release badge in README for clarity Updated release badge to use a versioning shield for better visibility. * Update README badges and license information Rearranged badge order in README and added GPL v3 license badge. * Update README.md with new BPE version of llm.cpp * Update README.md with license info * chore(deps-dev): bump vite from 8.0.16 to 8.1.5 in /frontend (#27) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.16 to 8.1.5. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.1.5/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 8.1.4 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump actions/upload-pages-artifact from 3 to 5 (#24) Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 5. - [Release notes](https://github.com/actions/upload-pages-artifact/releases) - [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v5) --- updated-dependencies: - dependency-name: actions/upload-pages-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: Eamon Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eamon2009 * Clean up README by removing obsolete sections Removed various sections related to GitHub configurations, frontend resources, and custom library modules from the README. * Enhance README with references and citation section Updated README to include links for references and citation. * refactor: overhaul logging loop Refactor core Transformer classes and modernize the training execution pass. * refactor: Rename model modules to MiniQuadtrix and overhaul logging loop Refactor core Transformer classes and modernize the training execution pass. * infer: Add distributed inference script with multi-GPU response gathering Introduce `infer.py` to support standalone evaluation and interactive chat sessions across single-device and distributed (DDP) runtime environments. Distributed Command Sync: Uses broadcast operations (`dist.broadcast`) to synchronize user CLI prompts across all GPU worker processes from rank 0. - Output Gathering: Executes autoregressive token generation across all nodes and aggregates distinct GPU responses via `dist.all_gather_object`. - Resource Management: Handles graceful process group teardown and cleanly manages CLI termination interrupts across cluster ranks. * train: Add DistributedDataParallel (DDP) training script with CLI chat interface Introduce an end-to-end PyTorch training and inference script (`train.py`) featuring multi-GPU DistributedDataParallel (DDP) initialization, mini-GPT architecture definitions, and interactive post-training CLI generation. Key highlights: - Multi-GPU Support: Auto-detects NCCL process groups (`RANK`, `LOCAL_RANK`, `WORLD_SIZE`) for DDP training; falls back cleanly to single-device CUDA/CPU. - Architecture: Standard Transformer decoder stack (`MiniQuadtrix`) with causal self-attention, multi-head projections, layer norms, and weight init. - Data Pipeline: Tiktoken `o200k_base` tokenization streaming directly from input datasets with randomized context-window batching (`get_batch`). - Runtime Monitoring: Calculates gradient L2 norms, tokens-per-second throughput, and tracks best validation loss checkpoints (`llm.pt`). - Terminal Interface: Includes a post-training interactive generation loop for prompt testing via `raw_model.generate()`. - Teardown: Safely closes out the distributed process group (`dist.destroy_process_group`). * math: Implement analytical backpropagation routines and activation tracking Introduce `backward.h` providing full analytical reverse-mode automatic differentiation across core Transformer modules without third-party frameworks. Key components: - Gradient Containers: Define specialized gradient accumulation structures (`GradLinear`, `GradEmbedding`, `GradLayerNorm`, `GradHead`, `GradMHA`, `GradFFN`, `GradBlock`, `Grads`) for parameter optimization. - Activation Checkpointing: Introduce forward-pass state structs (`SavedLN`, `SavedHead`, `SavedMHA`, `SavedFFN`, `SavedBlock`, `SavedForward`) to retain intermediate tensors, dropout masks, and LayerNorm scale parameters. - Backward Operations: - `backward_cross_entropy`: Scaled Softmax-with-Loss analytical gradient. - `backward_linear`: Dense matrix multiplication backprop computing `dX`, `dW`, and bias accumulation. - `backward_layernorm`: LayerNorm gradient following standard normalization standard. - `backward_relu` & `backward_dropout`: Mask-based element-wise activation backprop. - `backward_bmm` & `backward_softmax3d`: Causal self-attention matrix derivatives. * math: Add Head and MultiHeadAttention causal self-attention modules * data: improved mm C++ Byte-Pair Encoding (BPE) DataLoader and tokenizer implementation * main: Add CLI application entry point supporting training, chat, and generation modes * Fix image attributes in README.md Updated image attributes in README.md. * Revise file structure section in README Updated the file structure section to reflect changes in project organization and added details about new directories and files. * Delete package-lock.json * Delete package.json * Delete SettingsPanel.tsx * Delete NewChatButton.tsx * Delete SessionList.tsx * Delete Badge.tsx * Delete Button.tsx * Delete Input.tsx * Delete Slider.tsx * Delete Tooltip.tsx * Delete vite.config.ts * Delete tsconfig.json * Delete tailwind.config.ts * Delete sw.js * Delete useAutoScroll.ts * Delete useConnectionStatus.ts * Delete index.css * Delete useKeyboardShortcut.ts * Delete main.tsx * Delete registerServiceWorker.ts * Delete sessionStore.ts * Delete settingsStore.ts * Delete index.ts * Delete text.ts * Delete time.ts * build: Add Makefile for native compilation and script wrapper targets * Delete config.h * Delete attention.h * Delete backward.h * Delete dataloader.h * Delete train.mm * Delete tensor.h * build: Makefile for native compilation and script wrapper targets * cuda: Add CUDATensor container for host/device VRAM memory management * cuda: Add CUDA and cuBLAS accelerated main entry point Introduce `main.cu` to serve as the CUDA-accelerated driver for dataset loading, GPU initialization, backprop training, chat sessions, and token generation. * cuda: Add custom CUDA kernels for bias addition, ReLU, LayerNorm, and Softmax * cuda: Add CUDA_CHECK and CUBLAS_CHECK error handling macros * cuda: Add gpu_matmul, gpu_layernorm, and gpu_softmax layer wrappers * main: Add objective-C++/C++ main application entry point * Revise file structure in README.md reflect changes in project organization and added new directories for CI/CD workflows and assets. Updated file structure section in README.md to reflect changes in project organization and added new directories for CI/CD workflows and assets. * Delete engine/fineweb_dataset.py not used anymore * Delete engine/iGPU directory no longer in use * backward: Add analytical backprop ops and gradient accumulators Introduce foundational backpropagation gradient structures and primitive backward operations for transformer layer components. Key additions: - Gradient Accumulation Structures: Define `Grads` root container and child structs (`GradLinear`, `GradEmbedding`, `GradLayerNorm`, `GradHead`, `GradMHA`, `GradFFN`, `GradBlock`) equipped with zero-initialization helpers. - Activation Memory Structs: Define `SavedForward` and layer-specific intermediate tensors needed for reverse-mode automatic differentiation. - Primitive Backward Kernels: - `backward_cross_entropy`: Evaluates softmax derivatives scaled by batch size ($1 / BT$). - `backward_linear`: Computes input gradients ($dX = dOut \cdot W^T$) and accumulates weight/bias gradients ($dW$, $db$). - `backward_layernorm`: Implements full LayerNorm backprop incorporating mean/std jacobian terms along sequence channels. - `backward_relu`: Computes element-wise activation derivative masks. * Delete benches/bech(mm).mm no longer in use * Update README with OpenMP flag for build command * model: Add GPT language model architecture, loss function, and AdamW optimizer * bpe: Add Byte-Pair Encoding (BPE) tokenizer and DataLoader class * lm: Update GPT language model header with sampler repetition penalty * Update CI workflow to build and test llm.exe * attention: Add Head and MultiHeadAttention causal self-attention modules * Add C/C++ file extensions to workflow checks * Update CI workflow for consistency and fixes * Refactor PR comment checks workflow simplify trigger conditions and enhance comment formatting. Adjusted steps for extracting PR and comment metadata, and refined success/failure reporting. Updated PR comment checks workflow * Change environment name from 'lmgnu' to 'main' * tokenizer: Optimize DataLoader with OpenMP parallelization and merges * Update README.md * Enhance project structure documentation in README Updated project structure section with detailed descriptions for each directory and file. * tensor: Add OpenMP multi-threading to AVX/SSE vector operations --------- Signed-off-by: dependabot[bot] Signed-off-by: Eamon Signed-off-by: Eamon2009 Signed-off-by: Eamon Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .clang-format | 15 + .devops/Dockerfile.dev.frontend | 12 - .github/workflows/check.yml | 30 +- .github/workflows/ci.yml | 118 +- .github/workflows/jekyll-gh-pages.yml | 2 +- .github/workflows/{lmgnu.yml => main.yml} | 2 +- .github/workflows/pr-check.yml | 169 +- .github/workflows/release.yml | 8 - .gitignore | 3 + .gitmodules | 4 + .pyhton-version | 1 + CITATION.cff | 56 + Makefile | 94 +- README.md | 268 +- assets/run_2026-07-16 165731.png | Bin 0 -> 313489 bytes benches/python_benchmark.py | 482 ---- config/config.h | 14 +- data/data_set.py | 12 +- docs/run.md | 492 ---- ...raining_report.png => training_report.png} | Bin engine/distributed/infer.py | 189 ++ engine/distributed/train.py | 332 +++ engine/fineweb_dataset.py | 61 - engine/iGPU/inference.py | 298 --- engine/iGPU/main.py | 360 --- engine/inference.py | 337 +-- engine/llm.cpp/Makefile | 77 + .../llm.cpp}/include/attention.h | 0 .../llm.cpp}/include/backward.h | 98 +- engine/llm.cpp/include/bpe.h | 289 +++ engine/llm.cpp/include/cuda_kernels.cuh | 123 + engine/llm.cpp/include/cuda_utils.cuh | 32 + engine/llm.cpp/include/layer.cuh | 74 + engine/llm.cpp/include/lm.h | 250 ++ .../gpt.h => engine/llm.cpp/include/model.h | 61 +- engine/llm.cpp/include/tensor.cuh | 108 + engine/llm.cpp/llm.cu | 492 ++++ engine/llm.cpp/train.mm | 477 ++++ engine/main.py | 310 +-- frontend/.env.example | 1 - frontend/index.html | 20 - frontend/manifest.webmanifest | 20 - frontend/package-lock.json | 2184 ----------------- frontend/package.json | 29 - frontend/postcss.config.js | 6 - frontend/public/icon.svg | 53 - frontend/public/manifest.webmanifest | 20 - frontend/public/sw.js | 52 - frontend/src/App.tsx | 39 - frontend/src/api/chat.ts | 24 - frontend/src/api/client.ts | 37 - frontend/src/api/health.ts | 34 - frontend/src/api/sessions.ts | 66 - frontend/src/components/chat/ChatView.tsx | 107 - frontend/src/components/chat/EmptyState.tsx | 97 - .../src/components/chat/MessageAvatar.tsx | 53 - frontend/src/components/chat/MessageList.tsx | 33 - frontend/src/components/chat/MessageRow.tsx | 123 - .../src/components/chat/StarterPrompts.tsx | 22 - .../src/components/chat/ThinkingIndicator.tsx | 28 - frontend/src/components/input/CharCounter.tsx | 9 - frontend/src/components/input/InputBar.tsx | 158 -- frontend/src/components/layout/AppLayout.tsx | 32 - frontend/src/components/layout/Sidebar.tsx | 148 -- frontend/src/components/layout/Topbar.tsx | 94 - frontend/src/components/panels/ModelBadge.tsx | 52 - .../src/components/panels/SettingsPanel.tsx | 98 - frontend/src/components/panels/StatsPanel.tsx | 93 - .../src/components/sidebar/NewChatButton.tsx | 42 - .../src/components/sidebar/SessionItem.tsx | 96 - .../src/components/sidebar/SessionList.tsx | 28 - frontend/src/components/ui/Badge.tsx | 16 - frontend/src/components/ui/Button.tsx | 68 - frontend/src/components/ui/Input.tsx | 23 - frontend/src/components/ui/Slider.tsx | 18 - frontend/src/components/ui/Tooltip.tsx | 17 - frontend/src/hooks/useAutoScroll.ts | 13 - frontend/src/hooks/useConnectionStatus.ts | 10 - frontend/src/hooks/useKeyboardShortcut.ts | 25 - frontend/src/index.css | 67 - frontend/src/main.tsx | 26 - frontend/src/registerServiceWorker.ts | 11 - frontend/src/store/sessionStore.ts | 44 - frontend/src/store/settingsStore.ts | 53 - frontend/src/types/index.ts | 76 - frontend/src/utils/text.ts | 15 - frontend/src/utils/time.ts | 18 - frontend/sw.js | 52 - frontend/tailwind.config.ts | 24 - frontend/tsconfig.json | 22 - frontend/vite.config.ts | 9 - include/attention.h | 30 +- include/backward.h | 261 +- include/block.h | 16 +- include/tensor.h | 877 ++++--- include/tokenizer.h | 354 +++ llm.cpp/include/block.h | 61 - llm.cpp/include/char_level.h | 131 - llm.cpp/include/dataloader.h | 237 -- llm.cpp/include/embedding.h | 62 - llm.cpp/include/feedforward.h | 47 - llm.cpp/include/layernorm.h | 45 - llm.cpp/include/linear.h | 60 - llm.cpp/include/quadtrix.h | 250 -- llm.cpp/include/sampler.h | 50 - llm.cpp/include/tensor.h | 567 ----- llm.cpp/include/torch_bridge.h | 34 - llm.mm | 322 +++ mypy.ini | 7 + requirements.txt | 1 + run.md | 83 + train_test/test.cpp | 96 - train_test/train2.mm | 415 ++++ 113 files changed, 5023 insertions(+), 9238 deletions(-) delete mode 100644 .devops/Dockerfile.dev.frontend rename .github/workflows/{lmgnu.yml => main.yml} (98%) create mode 100644 .gitmodules create mode 100644 .pyhton-version create mode 100644 CITATION.cff create mode 100644 assets/run_2026-07-16 165731.png delete mode 100644 benches/python_benchmark.py delete mode 100644 docs/run.md rename docs/{quadtrix_training_report.png => training_report.png} (100%) create mode 100644 engine/distributed/infer.py create mode 100644 engine/distributed/train.py delete mode 100644 engine/fineweb_dataset.py delete mode 100644 engine/iGPU/inference.py delete mode 100644 engine/iGPU/main.py create mode 100644 engine/llm.cpp/Makefile rename {llm.cpp => engine/llm.cpp}/include/attention.h (100%) rename {llm.cpp => engine/llm.cpp}/include/backward.h (90%) create mode 100644 engine/llm.cpp/include/bpe.h create mode 100644 engine/llm.cpp/include/cuda_kernels.cuh create mode 100644 engine/llm.cpp/include/cuda_utils.cuh create mode 100644 engine/llm.cpp/include/layer.cuh create mode 100644 engine/llm.cpp/include/lm.h rename llm.cpp/include/gpt.h => engine/llm.cpp/include/model.h (83%) create mode 100644 engine/llm.cpp/include/tensor.cuh create mode 100644 engine/llm.cpp/llm.cu create mode 100644 engine/llm.cpp/train.mm delete mode 100644 frontend/.env.example delete mode 100644 frontend/index.html delete mode 100644 frontend/manifest.webmanifest delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/postcss.config.js delete mode 100644 frontend/public/icon.svg delete mode 100644 frontend/public/manifest.webmanifest delete mode 100644 frontend/public/sw.js delete mode 100644 frontend/src/App.tsx delete mode 100644 frontend/src/api/chat.ts delete mode 100644 frontend/src/api/client.ts delete mode 100644 frontend/src/api/health.ts delete mode 100644 frontend/src/api/sessions.ts delete mode 100644 frontend/src/components/chat/ChatView.tsx delete mode 100644 frontend/src/components/chat/EmptyState.tsx delete mode 100644 frontend/src/components/chat/MessageAvatar.tsx delete mode 100644 frontend/src/components/chat/MessageList.tsx delete mode 100644 frontend/src/components/chat/MessageRow.tsx delete mode 100644 frontend/src/components/chat/StarterPrompts.tsx delete mode 100644 frontend/src/components/chat/ThinkingIndicator.tsx delete mode 100644 frontend/src/components/input/CharCounter.tsx delete mode 100644 frontend/src/components/input/InputBar.tsx delete mode 100644 frontend/src/components/layout/AppLayout.tsx delete mode 100644 frontend/src/components/layout/Sidebar.tsx delete mode 100644 frontend/src/components/layout/Topbar.tsx delete mode 100644 frontend/src/components/panels/ModelBadge.tsx delete mode 100644 frontend/src/components/panels/SettingsPanel.tsx delete mode 100644 frontend/src/components/panels/StatsPanel.tsx delete mode 100644 frontend/src/components/sidebar/NewChatButton.tsx delete mode 100644 frontend/src/components/sidebar/SessionItem.tsx delete mode 100644 frontend/src/components/sidebar/SessionList.tsx delete mode 100644 frontend/src/components/ui/Badge.tsx delete mode 100644 frontend/src/components/ui/Button.tsx delete mode 100644 frontend/src/components/ui/Input.tsx delete mode 100644 frontend/src/components/ui/Slider.tsx delete mode 100644 frontend/src/components/ui/Tooltip.tsx delete mode 100644 frontend/src/hooks/useAutoScroll.ts delete mode 100644 frontend/src/hooks/useConnectionStatus.ts delete mode 100644 frontend/src/hooks/useKeyboardShortcut.ts delete mode 100644 frontend/src/index.css delete mode 100644 frontend/src/main.tsx delete mode 100644 frontend/src/registerServiceWorker.ts delete mode 100644 frontend/src/store/sessionStore.ts delete mode 100644 frontend/src/store/settingsStore.ts delete mode 100644 frontend/src/types/index.ts delete mode 100644 frontend/src/utils/text.ts delete mode 100644 frontend/src/utils/time.ts delete mode 100644 frontend/sw.js delete mode 100644 frontend/tailwind.config.ts delete mode 100644 frontend/tsconfig.json delete mode 100644 frontend/vite.config.ts create mode 100644 include/tokenizer.h delete mode 100644 llm.cpp/include/block.h delete mode 100644 llm.cpp/include/char_level.h delete mode 100644 llm.cpp/include/dataloader.h delete mode 100644 llm.cpp/include/embedding.h delete mode 100644 llm.cpp/include/feedforward.h delete mode 100644 llm.cpp/include/layernorm.h delete mode 100644 llm.cpp/include/linear.h delete mode 100644 llm.cpp/include/quadtrix.h delete mode 100644 llm.cpp/include/sampler.h delete mode 100644 llm.cpp/include/tensor.h delete mode 100644 llm.cpp/include/torch_bridge.h create mode 100644 llm.mm create mode 100644 mypy.ini create mode 100644 run.md delete mode 100644 train_test/test.cpp create mode 100644 train_test/train2.mm diff --git a/.clang-format b/.clang-format index f990767..f67a9c5 100644 --- a/.clang-format +++ b/.clang-format @@ -1,14 +1,27 @@ +--- +# Clang-Format Configuration File +# ---------------------------------------------------- BasedOnStyle: LLVM +Language: Cpp + +# Indentation and Tab Settings IndentWidth: 6 UseTab: Never TabWidth: 6 ContinuationIndentWidth: 6 + +# Brace Wrapping BreakBeforeBraces: Allman + +# Formatting Geometry & Constraints ColumnLimit: 100 PointerAlignment: Right ReferenceAlignment: Right + +# Include Management SortIncludes: CaseInsensitive IncludeBlocks: Regroup +# Spacing rules SpaceBeforeParens: ControlStatements SpaceAfterCStyleCast: false SpaceBeforeAssignmentOperators: true @@ -23,5 +36,7 @@ AllowShortIfStatementsOnASingleLine: Never AllowShortLoopsOnASingleLine: false AllowShortFunctionsOnASingleLine: Empty AllowShortBlocksOnASingleLine: Empty +# Braced initializer list formatting (e.g. vector/array initialization) Cpp11BracedListStyle: true SpacesInAngles: Never +... \ No newline at end of file diff --git a/.devops/Dockerfile.dev.frontend b/.devops/Dockerfile.dev.frontend deleted file mode 100644 index de054d6..0000000 --- a/.devops/Dockerfile.dev.frontend +++ /dev/null @@ -1,12 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app - -COPY frontend/package*.json ./ -RUN npm ci - -COPY frontend/ ./ - -EXPOSE 5173 - -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0a9af06..a962c59 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -7,12 +7,16 @@ on: - "**/*.py" - "**/*.cpp" - "**/*.c" + - "**/*.h" + - "**/*.hpp" pull_request: branches: [ "master" ] paths: - "**/*.py" - "**/*.cpp" - "**/*.c" + - "**/*.h" + - "**/*.hpp" jobs: syntax-check: @@ -35,38 +39,32 @@ jobs: - name: Lint Python Files run: | - # Only checks syntax and severe errors - flake8 . --count --select=E999,F821,F822,F823 --show-source --statistics - - # --- C/C++ Syntax Check --- - - name: Install C/C++ Compiler (GCC) - run: | - sudo apt-get update - sudo apt-get install -y gcc g++ - + # E9, F63, F7, F82 checks syntax errors & undefined variables + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - name: Check C/C++ Syntax run: | echo "Checking C and C++ files for syntax errors..." errors=0 - # Using redirect loops to properly catch errors without subshell issues + # Loop through C files (-I. includes local root directory for header lookups) while read -r file; do if [ -z "$file" ]; then continue; fi echo "Checking $file" - if ! gcc -fsyntax-only "$file"; then - echo "Syntax error in $file" + if ! gcc -fsyntax-only -I. "$file"; then + echo "::error file=$file::Syntax error in $file" errors=$((errors + 1)) fi - done < <(find . -name "*.c") + done < <(find . -type f -name "*.c") + # Loop through C++ files while read -r file; do if [ -z "$file" ]; then continue; fi echo "Checking $file" - if ! g++ -fsyntax-only "$file"; then - echo "syntax error in $file" + if ! g++ -fsyntax-only -I. "$file"; then + echo "::error file=$file::Syntax error in $file" errors=$((errors + 1)) fi - done < <(find . -name "*.cpp") + done < <(find . -type f -name "*.cpp") if [ $errors -ne 0 ]; then echo "$errors file(s) failed syntax verification." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20b8142..570e77d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ name: Build & Test on: push: branches: [ master, main, develop ] + jobs: cpp-quality: runs-on: ubuntu-latest @@ -22,7 +23,7 @@ jobs: - name: Clang-Format Check id: clang-format run: | - echo "### 🔍 Clang-Format Code Style Check" >> $GITHUB_STEP_SUMMARY + echo "### Clang-Format Code Style Check" >> $GITHUB_STEP_SUMMARY find . -name "*.cpp" -o -name "*.h" -o -name "*.hpp" | grep -v build > cpp_files.txt if [ -s cpp_files.txt ]; then @@ -32,10 +33,10 @@ jobs: cat format_output.txt >> $GITHUB_STEP_SUMMARY exit 1 else - echo " Code formatting is correct" >> $GITHUB_STEP_SUMMARY + echo "Code formatting is correct" >> $GITHUB_STEP_SUMMARY fi else - echo " No C++ files found" >> $GITHUB_STEP_SUMMARY + echo "No C++ files found" >> $GITHUB_STEP_SUMMARY fi continue-on-error: true @@ -55,7 +56,7 @@ jobs: echo "Issues detected:" >> $GITHUB_STEP_SUMMARY cat cppcheck_output.txt >> $GITHUB_STEP_SUMMARY else - echo " No critical issues found" >> $GITHUB_STEP_SUMMARY + echo "No critical issues found" >> $GITHUB_STEP_SUMMARY fi continue-on-error: true @@ -88,7 +89,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' cache: 'pip' @@ -101,10 +102,10 @@ jobs: - name: Black Format Check id: black run: | - echo "### Black Code Formatting" >> $GITHUB_STEP_SUMMARY + echo "### Black Code Formatting" >> $GITHUB_STEP_SUMMARY black --check --diff . 2>&1 | tee black_output.txt || true if [ $? -ne 0 ]; then - echo " Formatting issues detected:" >> $GITHUB_STEP_SUMMARY + echo "Formatting issues detected:" >> $GITHUB_STEP_SUMMARY head -30 black_output.txt >> $GITHUB_STEP_SUMMARY else echo "Black formatting passed" >> $GITHUB_STEP_SUMMARY @@ -127,27 +128,27 @@ jobs: - name: Flake8 Linting id: flake8 run: | - echo "### Flake8 Linting" >> $GITHUB_STEP_SUMMARY + echo "### Flake8 Linting" >> $GITHUB_STEP_SUMMARY flake8 . --max-line-length=127 --count --statistics 2>&1 | tee flake8_output.txt || true if [ $? -ne 0 ]; then echo "Linting violations:" >> $GITHUB_STEP_SUMMARY cat flake8_output.txt >> $GITHUB_STEP_SUMMARY else - echo " Flake8 passed" >> $GITHUB_STEP_SUMMARY + echo "Flake8 passed" >> $GITHUB_STEP_SUMMARY fi continue-on-error: true - name: Pylint Analysis id: pylint run: | - echo "### Pylint Code Analysis" >> $GITHUB_STEP_SUMMARY + echo "### Pylint Code Analysis" >> $GITHUB_STEP_SUMMARY find . -name "*.py" -type f | grep -v __pycache__ | xargs pylint \ --fail-under=7.0 \ --disable=C0111,C0103,R0913,W0212 \ 2>&1 | tee pylint_output.txt || true if grep -q "rated at" pylint_output.txt; then - echo " Pylint analysis complete" >> $GITHUB_STEP_SUMMARY + echo "Pylint analysis complete" >> $GITHUB_STEP_SUMMARY tail -3 pylint_output.txt >> $GITHUB_STEP_SUMMARY fi continue-on-error: true @@ -155,10 +156,10 @@ jobs: - name: Bandit Security Scan id: bandit run: | - echo "### Bandit Security Check" >> $GITHUB_STEP_SUMMARY + echo "### Bandit Security Check" >> $GITHUB_STEP_SUMMARY bandit -r . -ll 2>&1 | tee bandit_output.txt || true if grep -q "Issue:" bandit_output.txt; then - echo " Security issues found:" >> $GITHUB_STEP_SUMMARY + echo "Security issues found:" >> $GITHUB_STEP_SUMMARY grep -A1 "Issue:" bandit_output.txt >> $GITHUB_STEP_SUMMARY else echo "No security issues detected" >> $GITHUB_STEP_SUMMARY @@ -171,6 +172,10 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] fail-fast: false + + defaults: + run: + shell: bash steps: - uses: actions/checkout@v4 @@ -181,12 +186,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y build-essential g++ - - - name: Install dependencies (macOS) - if: runner.os == 'macOS' - run: | - brew install gcc + sudo apt-get install -y build-essential g++ libomp-dev - name: Install dependencies (Windows) if: runner.os == 'Windows' @@ -196,39 +196,57 @@ jobs: - name: Build inference executable id: build-inference run: | - echo "### 🔨 Building Inference Executable" >> $GITHUB_STEP_SUMMARY + echo "### Building Inference Executable" >> $GITHUB_STEP_SUMMARY - if [ -f "main.cpp" ]; then - g++ -std=c++17 -O2 -I. -Iinclude -o quadtrix_inference main.cpp 2>&1 | tee inference_build.txt || true - - if [ -f "quadtrix_inference" ] || [ -f "quadtrix_inference.exe" ]; then - echo " Inference build successful" >> $GITHUB_STEP_SUMMARY + if [ "$RUNNER_OS" == "macOS" ]; then + echo "Building for macOS with Metal framework support..." + if [ -f "llm.mm" ] && [ -f "main.cpp" ]; then + clang++ -std=c++17 -O3 -I. -Iinclude main.cpp llm.mm -framework Foundation -framework Metal -framework MetalPerformanceShaders -o llm 2>&1 | tee inference_build.txt || true + elif [ -f "main.cpp" ]; then + clang++ -std=c++17 -O3 -I. -Iinclude main.cpp -o llm 2>&1 | tee inference_build.txt || true + fi + + if [ -f "llm" ]; then + echo "Inference build successful (macOS)" >> $GITHUB_STEP_SUMMARY else - echo "Inference build failed" >> $GITHUB_STEP_SUMMARY + echo "Inference build failed (macOS)" >> $GITHUB_STEP_SUMMARY cat inference_build.txt >> $GITHUB_STEP_SUMMARY fi + else - echo "main.cpp not found - skipping inference build" >> $GITHUB_STEP_SUMMARY + echo "Building for $RUNNER_OS with GCC..." + if [ -f "main.cpp" ]; then + g++ -std=c++17 -O3 -march=native -fopenmp -I. -Iinclude -o llm.exe main.cpp 2>&1 | tee inference_build.txt || true + + if [ -f "llm.exe" ]; then + echo "Inference build successful ($RUNNER_OS)" >> $GITHUB_STEP_SUMMARY + else + echo "Inference build failed ($RUNNER_OS)" >> $GITHUB_STEP_SUMMARY + cat inference_build.txt >> $GITHUB_STEP_SUMMARY + fi + else + echo "main.cpp not found - skipping inference build" >> $GITHUB_STEP_SUMMARY + fi fi continue-on-error: true - name: Test inference executable id: test-inference run: | - echo "### Testing Inference Executable" >> $GITHUB_STEP_SUMMARY + echo "### Testing Inference Executable" >> $GITHUB_STEP_SUMMARY - if [ -f "quadtrix_inference" ] || [ -f "quadtrix_inference.exe" ]; then - BINARY="quadtrix_inference" - if [ ! -f "$BINARY" ]; then - BINARY="quadtrix_inference.exe" - fi - + BINARY="llm.exe" + if [ "$RUNNER_OS" == "macOS" ]; then + BINARY="llm" + fi + + if [ -f "$BINARY" ]; then if [ -f "data/input.txt" ]; then ./$BINARY data/input.txt 2>&1 | tee inference_test.txt || true echo "Inference test completed" >> $GITHUB_STEP_SUMMARY head -20 inference_test.txt >> $GITHUB_STEP_SUMMARY else - echo " data/input.txt not found - skipping inference test" >> $GITHUB_STEP_SUMMARY + echo "data/input.txt not found - skipping inference test" >> $GITHUB_STEP_SUMMARY fi else echo "Inference executable not found" >> $GITHUB_STEP_SUMMARY @@ -244,29 +262,29 @@ jobs: g++ -std=c++17 -O2 -I. -Iinclude llm.cpp train.cpp -o quadtrix_train 2>&1 | tee training_build.txt || true if [ -f "quadtrix_train" ] || [ -f "quadtrix_train.exe" ]; then - echo " Training build successful" >> $GITHUB_STEP_SUMMARY + echo "Training build successful" >> $GITHUB_STEP_SUMMARY else - echo " Training build failed" >> $GITHUB_STEP_SUMMARY + echo "Training build failed" >> $GITHUB_STEP_SUMMARY cat training_build.txt >> $GITHUB_STEP_SUMMARY fi elif [ -f "train.cpp" ]; then - echo " train.cpp found but llm.cpp not found - skipping training build" >> $GITHUB_STEP_SUMMARY + echo "train.cpp found but llm.cpp not found - skipping training build" >> $GITHUB_STEP_SUMMARY else - echo " train.cpp not found - skipping training build" >> $GITHUB_STEP_SUMMARY + echo "train.cpp not found - skipping training build" >> $GITHUB_STEP_SUMMARY fi continue-on-error: true - name: Test training executable id: test-training run: | - echo "### Testing Training Executable" >> $GITHUB_STEP_SUMMARY + echo "### Testing Training Executable" >> $GITHUB_STEP_SUMMARY - if [ -f "quadtrix_train" ] || [ -f "quadtrix_train.exe" ]; then - BINARY="quadtrix_train" - if [ ! -f "$BINARY" ]; then - BINARY="quadtrix_train.exe" - fi - + BINARY="quadtrix_train" + if [ -f "quadtrix_train.exe" ]; then + BINARY="quadtrix_train.exe" + fi + + if [ -f "$BINARY" ]; then echo "Running training executable (timeout 30s)..." >> $GITHUB_STEP_SUMMARY timeout 30 ./$BINARY 2>&1 | tee training_test.txt || true echo "Training test completed" >> $GITHUB_STEP_SUMMARY @@ -289,7 +307,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -300,7 +318,7 @@ jobs: pip install pytest pytest-cov if [ -f "requirements.txt" ]; then - pip install -r requirements.txt 2>/dev/null || echo " Some requirements failed to install" + pip install -r requirements.txt 2>/dev/null || echo "Some requirements failed to install" fi - name: Run Python tests @@ -313,11 +331,11 @@ jobs: if [ $? -eq 0 ]; then echo "All tests passed" >> $GITHUB_STEP_SUMMARY else - echo " Some tests failed" >> $GITHUB_STEP_SUMMARY + echo "Some tests failed" >> $GITHUB_STEP_SUMMARY fi tail -30 pytest_output.txt >> $GITHUB_STEP_SUMMARY else - echo " No test files found" >> $GITHUB_STEP_SUMMARY + echo "No test files found" >> $GITHUB_STEP_SUMMARY fi continue-on-error: true diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml index 0c6456b..ca47c38 100644 --- a/.github/workflows/jekyll-gh-pages.yml +++ b/.github/workflows/jekyll-gh-pages.yml @@ -28,7 +28,7 @@ jobs: source: ./ destination: ./_site - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 deploy: environment: diff --git a/.github/workflows/lmgnu.yml b/.github/workflows/main.yml similarity index 98% rename from .github/workflows/lmgnu.yml rename to .github/workflows/main.yml index e284cd1..2a2d6c8 100644 --- a/.github/workflows/lmgnu.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest environment: - name: lmgnu + name: main steps: - name: Checkout Code with Full History diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 3c43c02..b318fbe 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -2,7 +2,7 @@ name: PR Comment Checks on: issue_comment: - types: [created, edited] + types: [created] permissions: contents: read @@ -13,41 +13,38 @@ permissions: jobs: check-trigger: runs-on: ubuntu-latest - if: | - github.event.issue.pull_request && - contains(github.event.comment.body, '/run-checks') && - ( - github.event.comment.author_association == 'OWNER' || - github.event.comment.author_association == 'MEMBER' - ) + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/run-checks') && (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR') }} outputs: - pr_number: ${{ github.event.issue.number }} + pr_number: ${{ steps.pr-data.outputs.pr_number }} + comment_id: ${{ steps.pr-data.outputs.comment_id }} steps: - - name: Add reaction to comment + - name: Extract PR and Comment Metadata + id: pr-data + run: | + echo "pr_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT + echo "comment_id=${{ github.event.comment.id }}" >> $GITHUB_OUTPUT + + - name: Add rocket reaction uses: actions/github-script@v7 with: script: | - (async () => { - await github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: 'rocket' - }); - })(); - - - name: Update comment with status + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'rocket' + }); + + - name: Acknowledge in comment uses: actions/github-script@v7 with: script: | - (async () => { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - body: `Running strict checks... (initiated by @${context.payload.comment.user.login})\n\nPlease wait for results...` - }); - })(); + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + body: `${context.payload.comment.body}\n\n> 🚀 **Strict PR checks triggered by @${context.payload.comment.user.login}...** Running analysis and build steps.` + }); strict-cpp-checks: runs-on: ubuntu-latest @@ -56,31 +53,30 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - ref: refs/pull/${{ needs.check-trigger.outputs.pr_number }}/merge + ref: refs/pull/${{ needs.check-trigger.outputs.pr_number }}/head - name: Install C++ tools run: | sudo apt-get update - sudo apt-get install -y clang-format clang-tidy cppcheck build-essential cmake g++ clang + sudo apt-get install -y clang-format clang-tidy cppcheck build-essential g++ clang - - name: Run clang-format check (strict) - id: clang-format + - name: Clang-Format Check run: | - echo "### Clang Format Check" >> $GITHUB_STEP_SUMMARY find . -type f \( -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) | grep -v /build/ > cpp_files.txt || true if [ -s cpp_files.txt ]; then - clang-format --dry-run -Werror $(cat cpp_files.txt) 2>&1 | tee format_output.txt || true - if [ -s format_output.txt ]; then - echo "Format violations found" >> $GITHUB_STEP_SUMMARY - cat format_output.txt >> $GITHUB_STEP_SUMMARY - exit 1 - else - echo "C++ formatting is correct" >> $GITHUB_STEP_SUMMARY - fi - else - echo "No C++ files found" >> $GITHUB_STEP_SUMMARY + clang-format --dry-run -Werror $(cat cpp_files.txt) fi + - name: CppCheck Analysis + run: | + cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem --suppress=unusedFunction --std=c++17 . + + - name: Clang-Tidy Analysis + run: | + find . -type f \( -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) | grep -v /build/ | while read file; do + clang-tidy "$file" -checks="*,-fuchsia-*,-google-*,-llvm-*,-readability-magic-numbers" -- -I. -Iinclude || exit 1 + done + strict-python-checks: runs-on: ubuntu-latest needs: check-trigger @@ -88,13 +84,29 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - ref: refs/pull/${{ needs.check-trigger.outputs.pr_number }}/merge + ref: refs/pull/${{ needs.check-trigger.outputs.pr_number }}/head - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - cache: 'pip' + + - name: Install Python Linters + run: | + python -m pip install --upgrade pip + pip install black isort flake8 pylint bandit + + - name: Black Formatting + run: black --check . + + - name: Isort Import Check + run: isort --check-only . + + - name: Flake8 Syntax & Code Quality + run: flake8 . --max-line-length=127 --select=E9,F63,F7,F82 + + - name: Bandit Security Scan + run: bandit -r . -ll build-test: runs-on: ubuntu-latest @@ -103,12 +115,25 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - ref: refs/pull/${{ needs.check-trigger.outputs.pr_number }}/merge + ref: refs/pull/${{ needs.check-trigger.outputs.pr_number }}/head + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' - - name: Install C++ dependencies + - name: Build C++ Inference Executable run: | - sudo apt-get update - sudo apt-get install -y cmake build-essential g++ clang + if [ -f "main.cpp" ]; then + g++ -std=c++17 -O3 -fopenmp -I. -Iinclude -o llm.exe main.cpp + fi + + - name: Run Python Pytest Suite + run: | + python -m pip install --upgrade pip + pip install pytest pytest-cov + if [ -f "requirements.txt" ]; then pip install -r requirements.txt || true; fi + if [ -d "tests" ] || [ -d "test" ]; then pytest; fi summary: runs-on: ubuntu-latest @@ -121,27 +146,25 @@ jobs: uses: actions/github-script@v7 with: script: | - (async () => { - const cpp_status = '${{ needs.strict-cpp-checks.result }}' === 'success' ? '✅' : '❌ ISSUES FOUND'; - const python_status = '${{ needs.strict-python-checks.result }}' === 'success' ? '✅' : '❌ ISSUES FOUND'; - const build_status = '${{ needs.build-test.result }}' === 'success' ? '✅' : '❌ ISSUES FOUND'; - const overall = (cpp_status === '✅' && python_status === '✅' && build_status === '✅') ? 'PASSED' : 'REVIEW REQUIRED'; - - const comment = `## Strict Check Results - - C++ Checks: ${cpp_status} - Python Checks: ${python_status} - Build & Tests: ${build_status} - - Overall Status: ${overall} - - See full run: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID} - `; - - await github.rest.issues.createComment({ - issue_number: Number(process.env.PR_NUMBER), - owner: context.repo.owner, - repo: context.repo.repo, - body: comment - }); - })(); + const cpp_status = '${{ needs.strict-cpp-checks.result }}' === 'success' ? '✅ Passed' : '❌ Failed'; + const python_status = '${{ needs.strict-python-checks.result }}' === 'success' ? '✅ Passed' : '❌ Failed'; + const build_status = '${{ needs.build-test.result }}' === 'success' ? '✅ Passed' : '❌ Failed'; + const overall = (cpp_status.includes('✅') && python_status.includes('✅') && build_status.includes('✅')) + ? '✅ **ALL CHECKS PASSED**' + : '❌ **CHECK FAILURES DETECTED**'; + + const comment = `## 📋 Maintainer Check Results\n\n` + + `| Check Category | Status |\n` + + `| :--- | :--- |\n` + + `| **C++ Strict Quality** | ${cpp_status} |\n` + + `| **Python Strict Quality** | ${python_status} |\n` + + `| **Build & Tests** | ${build_status} |\n\n` + + `**Overall Result**: ${overall}\n\n` + + `[View Full Execution Logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; + + await github.rest.issues.createComment({ + issue_number: Number(process.env.PR_NUMBER), + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fb1f49..6ee793a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,8 +32,6 @@ jobs: tag_name="v${raw_tag}" fi echo "tag_name=${tag_name}" >> "$GITHUB_OUTPUT" - - # macOS Jobs macos-arm64-cpu: name: macOS Apple Silicon (arm64) CPU needs: release-metadata @@ -134,8 +132,6 @@ jobs: path: llm.cpp-${{ needs.release-metadata.outputs.tag_name }}-ios-xcframework.tar.gz if-no-files-found: error retention-days: 30 - - # Ubuntu Jobs ubuntu-x64-cpu: name: Ubuntu x64 (CPU) needs: release-metadata @@ -457,8 +453,6 @@ jobs: path: llm.cpp-${{ needs.release-metadata.outputs.tag_name }}-bin-ubuntu-x64-sycl-fp16.tar.gz if-no-files-found: error retention-days: 30 - - # Android Job android-arm64-cpu: name: Android arm64 (CPU) needs: release-metadata @@ -516,8 +510,6 @@ jobs: path: llm.cpp-${{ needs.release-metadata.outputs.tag_name }}-bin-android-arm64-cpu.tar.gz if-no-files-found: error retention-days: 30 - - # Windows Jobs windows-x64-cpu: name: Windows x64 (CPU) needs: release-metadata diff --git a/.gitignore b/.gitignore index 0dfdee5..8df9a56 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ Thumbs.db *.swo .idea docker-compose.override.yml +libs/lmgnu/build/ +libs/lmgnu/dist/ +libs/lmgnu/*.egg-info/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..84e66c6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "libs/lmgnu"] + path = libs/lmgnu + url = https://github.com/LMGNU/LMGNU.git + branch = master diff --git a/.pyhton-version b/.pyhton-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/.pyhton-version @@ -0,0 +1 @@ +3.10 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..770d3e8 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,56 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: llm.cpp +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Eamon + name-particle: 'Eamon Sippy ' + email: eamon112009@gmail.com + family-names: EAMON +identifiers: + - type: url + value: 'https://github.com/LMGNU/llm.cpp' + description: LLM training in C++ & Python. +repository-code: 'https://github.com/LMGNU/llm.cpp' +abstract: >- + LLM training in C++ & Python. + + No third-party runtime dependency - it builds from + main.cpp, config/config.h, and include/*.h alone. + + + Character-level tokenizer built directly from the input + corpus + + Train/validation split via DataLoader + + Token + positional embeddings + + Multi-head causal self-attention with explicit QKV + projections + + Pre-layer-norm residual transformer blocks + + Feed-forward MLP with ReLU + + Cross-entropy loss + + Fully analytical backward pass —-every gradient + (attention, layer norm, MLP, embeddings) is derived and + coded in include/backward.h, not autograd + + AdamW optimizer (first/second moment estimates, weight + decay) + + Checkpoint save/load + + Autoregressive generation and terminal chat mode +keywords: + - llm + - transformer +license: GPL-3.0 diff --git a/Makefile b/Makefile index ccb6702..4524d11 100644 --- a/Makefile +++ b/Makefile @@ -1,104 +1,44 @@ -# ============================================================================= -# Quadtrix.cpp — Makefile (llama.cpp-style convenience targets) -# ============================================================================= +.PHONY: all release debug benchmark-bin clean-native format help -.PHONY: all build clean run dev gpu train bench logs ps shell help - -SHELL := /bin/bash -SCRIPT := ./scripts/build.sh - -# ── Native C++ ─────────────────────────────────────────────────────────────── +SHELL := /bin/bash CC := g++ CFLAGS := -std=c++17 -O3 -march=native IFLAGS := -I. -Iinclude -TARGET := quadtrix +TARGET := llm SRCS := main.cpp +HDRS := $(wildcard include/*.h) all: $(TARGET) -$(TARGET): $(SRCS) - $(CC) $(CFLAGS) $(IFLAGS) -o $@ $^ - @echo "✓ Built $(TARGET)" +$(TARGET): $(SRCS) $(HDRS) + $(CC) $(CFLAGS) $(IFLAGS) -o $@ $(SRCS) + @echo "Built $(TARGET)" -# Optimised release (same flags, explicit target) -release: $(SRCS) - $(CC) $(CFLAGS) $(IFLAGS) -DNDEBUG -o $(TARGET) $^ +release: $(SRCS) $(HDRS) + $(CC) $(CFLAGS) $(IFLAGS) -DNDEBUG -o $(TARGET) $(SRCS) strip $(TARGET) +debug: $(SRCS) $(HDRS) + $(CC) -std=c++17 -O0 -g -fsanitize=address,undefined $(IFLAGS) -o $(TARGET)-debug $(SRCS) -# Debug build -debug: $(SRCS) - $(CC) -std=c++17 -O0 -g -fsanitize=address,undefined \ - $(IFLAGS) -o $(TARGET)-debug $^ - -benchmark-bin: benchmark.cpp - $(CC) $(CFLAGS) $(IFLAGS) -o quadtrix-bench $^ +benchmark-bin: benchmark.cpp $(HDRS) + $(CC) $(CFLAGS) $(IFLAGS) -o llm-bench benchmark.cpp clean-native: - rm -f $(TARGET) $(TARGET)-debug quadtrix-bench - -# ── Docker / Compose targets ───────────────────────────────────────────────── -build: - $(SCRIPT) up - -run: build - @echo "Stack already started." - -dev: - $(SCRIPT) dev - -gpu: - $(SCRIPT) gpu - -train-cpp: - $(SCRIPT) train-cpp - -train-torch: - $(SCRIPT) train-torch + rm -f $(TARGET) $(TARGET)-debug llm-bench -bench: - $(SCRIPT) bench - -logs: - $(SCRIPT) logs - -ps: - $(SCRIPT) ps - -shell: - $(SCRIPT) shell $(SERVICE) - -clean: - $(SCRIPT) clean - -# ── Misc ───────────────────────────────────────────────────────────────────── format: find . \( -name "*.cpp" -o -name "*.h" \) \ ! -path "./build/*" \ | xargs clang-format -i --style=LLVM -lint-py: - ruff check backend/ engine/ - help: @echo "" - @echo " Quadtrix.cpp — make targets" + @echo " llm.cpp — native make targets" @echo "" - @echo " Native:" @echo " make Build C++ binary (native)" @echo " make release Stripped release binary" @echo " make debug Debug binary with ASan/UBSan" + @echo " make benchmark-bin Build benchmark binary" @echo " make clean-native Remove native build artifacts" @echo " make format Run clang-format on all C++ files" - @echo "" - @echo " Docker:" - @echo " make build docker compose up --build (CPU)" - @echo " make dev Hot-reload dev stack" - @echo " make gpu CUDA GPU stack" - @echo " make train-cpp Train with C++ inside Docker" - @echo " make train-torch Train with PyTorch inside Docker" - @echo " make bench Run benchmark" - @echo " make logs Tail all logs" - @echo " make ps Show container status" - @echo " make shell Shell into backend (SERVICE=frontend to change)" - @echo " make clean Remove containers + volumes" - @echo "" + @echo "" \ No newline at end of file diff --git a/README.md b/README.md index 0e6bc79..3320459 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,78 @@ # llm.cpp

-image +image +

-[![Build & Test](https://github.com/LMGNU/llm.cpp/actions/workflows/ci.yml/badge.svg)](https://github.com/LMGNU/llm.cpp/actions/workflows/ci.yml) [![Docker Images](https://github.com/LMGNU/llm.cpp/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/LMGNU/llm.cpp/actions/workflows/docker-publish.yml) [![Release](https://github.com/LMGNU/llm.cpp/actions/workflows/release.yml/badge.svg)](https://github.com/LMGNU/llm.cpp/actions/workflows/release.yml) [![Linting](https://github.com/LMGNU/llm.cpp/actions/workflows/test.yml/badge.svg)](https://github.com/LMGNU/llm.cpp/actions/workflows/test.yml) [![CI Approval](https://github.com/LMGNU/llm.cpp/actions/workflows/ci-approval.yml/badge.svg)](https://github.com/LMGNU/llm.cpp/actions/workflows/ci-approval.yml) +[![Release](https://img.shields.io/github/v/release/LMGNU/llm.cpp)](https://github.com/LMGNU/llm.cpp/releases) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg?logo=gnu)](https://www.gnu.org/licenses/gpl-3.0) [![Docker Images](https://github.com/LMGNU/llm.cpp/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/LMGNU/llm.cpp/actions/workflows/docker-publish.yml) + + +This project implements language models in dependency-free C++, eliminating the need for PyTorch or Python to train a transformer locally. The core implementation is a decoder-only ***GPT architecture*** featuring custom tensors, embeddings, multi-head causal self-attention, layer normalization, cross-entropy loss, and an analytical backward pass with the AdamW optimizer - all contained within [main.cpp](main.cpp) ,[llm.mm](llm.mm) and the [include/](include) directory also a ***token level [BPE]*** [tokenizer.h](include/tokenizer.h) implementation inside [include](include). With no autograd engine or external frameworks, every gradient is explicitly derived and written out. +The model achieves a validation loss of 1.6371 nats after 76 minutes of CPU training on 31.4 million characters, demonstrating that character-level language modeling at this scale is highly tractable on commodity hardware without external dependencies. On a GPU (CUDA/bfloat16), a validation loss of 2.3918 is reached in under 83 minutes, achieving a peak throughput of 19.6k tokens per second. -Language models in dependency-free C++, with no need for PyTorch or Python to make a transformer actually works. The native path is a decoder-only GPT: tensors, embeddings, multi-head causal self-attention, layer norm, cross-entropy, and a analytical backward pass with AdamW, all in [main.cpp](main.cpp) and [include/](include/). No autograd, no framework - every gradient is derived and written out. +## Board +| S.No. | time | val_bpb / Metric | scale | Date | Contributors | +|---|-------------|------------------|-------|------|--------------| +| 1 | 168 hours | 29.41 PPL (~0.93 BPB) | 124M (32x TPU v3) | Feb 2019 | OpenAI (GPT-2 Small) | +| 2 | 45 min | 3.28 Val Loss (~0.748 BPB) | 124M (8x H100) | May 2024 | Andrej Karpathy (llm.c) | +| 3 | 2.98 min | 3.28 Val Loss (~0.748 BPB) | 124M (8x H100) | Feb 2025 | Keller Jordan et al. (Modded-NanoGPT) | +| 4 | 72 hours | 22.7 PPL (~0.85 BPB) | 125M (32x A100) | Feb 2024 | Meta (MobileLLM-125M) | +| 5 | ~24 hours | ~1.02 BPB | 135M (64x H100) | Jul 2024 | Hugging Face (SmolLM-135M) | +| 6 | 61.3 min | 0.7176 | 10.82M (T4) | Mar 2026 | @Eamon2009 | +| 7 | 6.1 min | 0.9250 | 1.99M (T4) | Feb 2026 | @Eamon2009 | +| 8 | 39.4 min | 1.3145 | 0.82M (CPU) | July 2026 | @Eamon2009 | +| 9 | 76.2 min | 1.6371 | 0.82M (CPU) | Jan 2026 | @Eamon2009 | -Alongside it sits a parallel PyTorch implementation in [engine/main.py](engine/main.py) and [engine/inference.py](engine/inference.py), so you can train and generate the same architecture with `torch` + `tiktoken` when you want speed instead of transparency. There's also an experimental integrated-GPU path in [iGPU/](engine/iGPU/). +More broadly, the primary contribution of this work lies in its absolute transparency. Every gradient in the backward pass is explicitly written and readable, and every tensor operation is a standard C++ function. By exposing exactly what frameworks like PyTorch compute under the hood, this implementation provides a clear educational pathway. We believe that this fundamental understanding is the true foundation of genuine expertise in deep learning. +Alongside it sits a parallel PyTorch implementation in [engine/main.py](engine/main.py) and [engine/inference.py](engine/inference.py), so you can train and generate the same architecture with `torch` + `tiktoken` when you want speed instead of transparency. There's also an experimental integrated-GPU path in [iGPU/](engine/iGPU/). The point of this repo is the C++ core. The PyTorch exist to make the model usable, but if you're here to ***train a GPT without a framework*** doing the work for you, [include/backward.h](include/backward.h) is where to start reading. + +--- + +## From CPU to GPU: The LibTorch Port +The custom C++ backend is transparent but slow: a CPU executes scalar matrix multiplication at roughly 1-10 GFLOP/s. An NVIDIA RTX 4090 delivers ∼80 TFLOP/s-an 8,000–80,000× speedup for the same computation.The LibTorch port replaces the custom backend with PyTorch’s C++ API, gaining cuBLAS-accelerated matrix operations. The transformer architecture remains unchanged only the compute layer is modified. A single line migrates +the model to GPU: +```cpp +model->to(torch::kCUDA) +``` +which transfers all parameters to GPU memory; all subsequent `torch::matmul` calls dispatch to cuBLAS automatically. + +--- + +

+image + + +

-The point of this repo is the C++ core. The PyTorch, FastAPI, and frontend layers exist to make the model usable, but if you're here to learn how a GPT is actually built and trained without a framework doing the work for you, [include/backward.h](include/backward.h) is where to start reading. ***technical notes***: [docs](https://eamon2009.github.io/LLMs/) +--- -## quick start (C++, train + chat) +## quick start (CPU) The fastest way to see the whole pipeline - tokenize, train, checkpoint, generate - using the bundled character-level corpus: +get the data set first : + +``` shell +cd data # you set the file size for data set +python data_set.py # also get any dataset from hugging face datasets +``` + ```bash -g++ -std=c++17 -O2 -I. -Iinclude -o quadtrix.exe main.cpp -./quadtrix.exe data/input.txt +# run this +g++ -std=c++17 -O3 -march=native -fopenmp -I. -Iinclude -o llm.exe main.cpp +./llm.exe data/input.txt ``` This trains from scratch on `data/input.txt` and writes the best checkpoint to `best_model.bin`. Once you have a checkpoint, generate or chat with it: ```bash -./quadtrix.exe data/input.txt --generate -./quadtrix.exe data/input.txt --chat --chat-tokens 300 +./llm.exe data/input.txt --generate +./llm.exe data/input.txt --chat --chat-tokens 300 ``` debugging tip: drop `-O2` for `-g` when compiling if you want to step through `include/backward.h` or `include/gpt.h` in a debugger — the manual backward pass is much easier to follow one breakpoint at a time. @@ -37,8 +80,124 @@ debugging tip: drop `-O2` for `-g` when compiling if you want to step through `i ### runtime arguments ```bash -quadtrix.exe [data_path] [--generate] [--chat] [--chat-tokens N] +llm.exe [data_path] [--generate] [--chat] [--chat-tokens N] +``` +### PyTorch (single-GPU + CPU) +```bash +# to train +cd engine +python main.py +# for inference stay in engine/ +python inference.py +``` +### Multi-GPU +Single GPU or CPU Mode (Default Fallback) +If you do not pass any distributed environment parameters, the script automatically defaults to running locally on a single GPU (if available) or CPU. +```bash +cd engine +cd distributed +python train.py +``` +### Single-Node Multi-GPU Training (DDP via torchrun) +To train using multiple GPUs on a single machine, use PyTorch's torchrun launcher. Replace --nproc_per_node with the number of GPUs available on your machine (e.g., 2, 4, or 8). +```bash +cd engine +cd distributed +# Example: Running on 4 GPUs on a single machine +torchrun --standalone --nproc_per_node=4 train.py ``` +--- +## File structure + +```text +## 📂 Project Structure + +```text +. +├── .devops/ # DevOps, Docker, & proxy configurations +│ ├── docker-compose.dev.yml # Local development compose setup +│ ├── docker-compose.gpu.yml # Compose setup with GPU acceleration +│ ├── docker-compose.yml # Base Docker Compose file +│ ├── Dockerfile # Primary build setup +│ ├── Dockerfile.backend # Backend service Docker setup +│ ├── Dockerfile.cpp # Native C++ build container setup +│ ├── Dockerfile.frontend # Frontend UI Docker setup +│ └── nginx.conf # Reverse proxy configuration +│ +├── .github/ # GitHub Actions CI/CD workflows & repository templates +│ ├── ISSUE_TEMPLATE/ # Bug report & feature request templates +│ │ ├── bug_report.md +│ │ ├── config.yml +│ │ └── feature_request.md +│ ├── workflows/ # CI/CD automation pipelines +│ │ ├── check.yml +│ │ ├── ci-approval.yml +│ │ ├── ci.yml +│ │ ├── docker-publish.yml +│ │ ├── jekyll-gh-pages.yml +│ │ ├── main.yml +│ │ ├── pr-check.yml +│ │ ├── release.yml +│ │ └── test.yml +│ ├── dependabot.yml # Automated dependency update configuration +│ └── pull_request_template.md # Pull request contribution template +│ +├── assets/ # Visual execution benchmarks & screenshots +│ ├── run_2026-07-16 165731.png +│ ├── run_20260430_192930.png +│ ├── run_20260508_110726.png +│ └── run_20260530_165216 (1).png +│ +├── benches/ # Performance benchmarking suite +│ └── bench.cpp # C++ benchmark execution script +│ +├── config/ # Global project configurations +│ └── config.h # C++ global configuration header +│ +├── data/ # Dataset ingestion scripts & raw samples +│ ├── data_set.py # Data loader preparation script +│ └── input.txt # Sample raw dataset text +│ +├── docs/ # Documentation media & report graphics +│ └── training_report.png # Model training performance chart +│ +├── engine/ # Core execution & inference engines +│ ├── distributed/ # Multi-node / distributed training & inference +│ │ ├── infer.py # Distributed inference pipeline +│ │ └── train.py # Distributed training pipeline +│ ├── llm.cpp/ # Low-level CUDA/C++ runtime engine +│ │ ├── config/ # Engine-specific configurations +│ │ ├── include/ # CUDA kernels & C++ architecture headers +│ │ ├── best_model.bin # Trained binary weights checkpoint +│ │ ├── llm.cu # CUDA GPU execution source +│ │ ├── llm.exe # Compiled engine binary executable +│ │ ├── llm.py # Engine Python bindings/wrapper +│ │ ├── Makefile # Engine build compilation setup +│ │ └── train.mm # Metal training harness +│ ├── logs/ # Execution log files +│ ├── inference.py # Python model inference entry point +│ ├── main.py # Primary Python execution entry point +│ └── llm.pt # Pretrained PyTorch model checkpoint +│ +├── include/ # Core C++ neural network architecture headers +│ ├── attention.h # Multi-head attention implementation +│ ├── backward.h # Backpropagation & gradient calculation utilities +│ ├── block.h # Transformer block assembly +│ ├── embedding.h # Token & positional embedding logic +│ ├── feedforward.h # Feed-forward layer implementation +│ ├── gpt.h # Full GPT transformer architecture layout +│ ├── layernorm.h # Layer normalization operations +│ ├── linear.h # Fully connected dense layer +│ ├── lm.h # High-level language model interface +│ ├── sampler.h # Token sampling routines (Top-K, Top-P, Temperature) +│ ├── tensor.h # Multidimensional array data structures +│ ├── tokenizer.h # Text tokenization logic +│ └── torch_bridge.h # PyTorch interoperability layer +│ +├── scripts/ # Automation & compilation scripts +│ +``` +--- | Argument | Description | |---|---| @@ -52,47 +211,50 @@ quadtrix.exe [data_path] [--generate] [--chat] [--chat-tokens N] | `GPT_DATA_PATH` | `data/input.txt` | Override the default training corpus | | `GPT_MODEL_PATH` | `best_model.bin` | Override the checkpoint path | -## what's actually implemented in C++ +## What's Actually Implemented in C++ -No third-party runtime dependency — it builds from `main.cpp`, `config/config.h`, and `include/*.h` alone. +No third-party runtime dependency - it builds from `main.cpp`, `config/config.h`, and `include/*.h` alone. -- Character-level tokenizer built directly from the input corpus +- **Byte Pair Encoding (BPE) Tokenizer** - Built entirely from scratch. Compiles a custom vocabulary directly from the training corpus by running iterative token-pair merges until it hits a targeted vocabulary threshold. - Train/validation split via `DataLoader` - Token + positional embeddings - Multi-head causal self-attention with explicit QKV projections - Pre-layer-norm residual transformer blocks - Feed-forward MLP with ReLU - Cross-entropy loss -- **Fully analytical backward pass** — every gradient (attention, layer norm, MLP, embeddings) is derived and coded in `include/backward.h`, not autograd +- **Fully analytical backward pass** - every gradient (attention, layer norm, MLP, embeddings) is derived mathematically and coded explicitly in `include/backward.h`, not autograd - AdamW optimizer (first/second moment estimates, weight decay) - Checkpoint save/load - Autoregressive generation and terminal chat mode -Hyperparameters live in `config/config.h` and require a rebuild to take effect: +Hyperparameters live in `engine/llm.cpp/config/config.h` and require a rebuild to take effect: ```cpp -static const int BATCH_SIZE = 4; -static const int BLOCK_SIZE = 64; -static const int N_EMBD = 128; -static const int N_HEAD = 4; -static const int N_LAYER = 4; -static const float DROPOUT = 0.2f; -static const float LEARNING_RATE = 3e-4f; -static const int MAX_ITERS = 3000; +// note: The c++ version only runs on cpu not on GPU +static const unsigned int SEED = 1337; +static const double TRAIN_SPLIT = 0.9; +static const int BATCH_SIZE = 32; +static const int BLOCK_SIZE = 64; +static const int MAX_ITERS = 5000; +static const int EVAL_INTERVAL = 500; +static const float LEARNING_RATE = 5e-4f; +static const int EVAL_ITERS = 25; +static const int N_EMBD = 128; +static const int N_HEAD = 2; +static const int N_LAYER = 4; +static const float DROPOUT = 0.05f; +static const int BPE_VOCAB_SIZE = 2048; ``` -For an optimized native build: - -```bash -g++ -std=c++17 -O3 -march=native -I. -Iinclude -o quadtrix.exe main.cpp -``` ## the PyTorch reference path -[engine/main.py](engine/main.py) trains the same architectural idea with `torch`, `torch.nn`, and GPT-2 BPE tokenization via `tiktoken`, useful when you want to scale past what C++ loops can comfortably train on CPU. +[engine/main.py](engine/main.py) trains the same architectural idea with `torch`, `torch.nn`, and GPT-4 BPE tokenization via `tiktoken`, useful when you want to scale past what C++ loops can comfortably train on CPU. ```bash -python engine/main.py +cd engine +python fineweb_dataset.py # you can also use data/input.txt also +python main.py ``` It looks for `engine/input.txt` by default; point it elsewhere with `QUADTRIX_TRAIN_DATA` if needed. Run inference against a saved checkpoint: @@ -101,22 +263,7 @@ It looks for `engine/input.txt` by default; point it elsewhere with `QUADTRIX_TR python engine/inference.py --checkpoint engine/best_model.pt --prompt "Once upon a time" --max-new-tokens 100 ``` -## results so far -## Leaderboard - -Runs are ranked by validation loss. Lower is better. - -| # | Val Loss | Parameters | Time | Hardware | Description | -|--:|----------|------------|----------|----------|------------------------------------------| -| 1 | **0.7176** | 10.82M | 61.3 min | NVIDIA T4 | Large-scale run, coherent paragraphs, strong convergence | -| 2 | 0.9250 | 1.99M | 6.1 min | NVIDIA T4 | Optimised run, fast training, stable learning | -| 3 | 1.3145 | 0.82M | 39.4 min | x64 CPU | Baseline, small data | -| 4 | 1.6371 | 0.82M | 76.2 min | x64 CPU | Extended CPU training, 3000 iterations | - -All runs: @Eamon2009, 2026. - ## Benchmarks - ### Runs at a Glance | Metric | Character-Level | Small Scale | Large Scale | @@ -141,15 +288,34 @@ See [run.md](run.md) and the leaderboard in the full docs for more configuration |---|---|---|---| | nanoGPT / minGPT | Minimal, educational GPT training | Python | PyTorch | | llama2.c | Inference-only | C | None | -| **llm.cpp** | Training *and* inference, manual backward pass, web UI | C++ / Python / TypeScript | Manual (C++) + PyTorch | +| **llm.cpp** | Training *and* inference, manual backward pass | C++ / Python | Manual (C++) + PyTorch | -I'd like the C++ core (`main.cpp`, `include/`, `config/`) to stay dependency-free and to stay the part of this repo that explin transformer internals directly. The PyTorch engine, FastAPI middleware, and React frontend are welcome to grow more features, integrations, and UI polish. If you build a port to another language or framework, I'm happy to link to it from a notable-forks section; just open an issue or PR. +I'd like the C++ core (`main.cpp`, `include/`, `config/`) to stay dependency-free and to stay the part of this repo that explin transformer internals directly. The PyTorch engine, include, and ci are welcome to grow more features, integrations, and CI polish. If you build a port to another language or framework, I'm happy to link to it from a notable-forks section; just open an issue or PR. ## references -- Vaswani et al., "Attention Is All You Need", 2017 -- Radford et al., GPT-2 technical work, 2019 -- nanoGPT and minGPT as educational reference points +- Vaswani et al., ["Attention Is All You Need"](https://arxiv.org/abs/1706.03762), 2017 +- Radford et al., ["Language Models are Unsupervised Multitask Learners"](https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf) (GPT-2 technical work), 2019 +- Brown et al., ["Language Models are Few-Shot Learners"](https://arxiv.org/abs/2005.14165) (GPT-3 paper), 2020 +- Meta AI, ["The Llama 3 Herd of Models"](https://arxiv.org/abs/2407.21783) (Llama 3 paper), 2024 +- Andrej Karpathy, [nanoGPT](https://github.com/karpathy/nanoGPT) repository as an educational reference point +- [HuggingFace Datasets](https://huggingface.co/datasets) for FineWeb and other pretraining/fine-tuning datasets + +## Cite + +If you find llm.cpp helpful in your research cite as: +```bibtex +@misc{llm.cpp, + author = {Eamon Sippy}, + title = {llm.cpp: LLM training in C++ \& Python}, + year = {2026}, + publisher = {GitHub}, + journal = {GitHub repository}, + url = {https://github.com/LMGNU/llm.cpp} +} + +``` + ## license diff --git a/assets/run_2026-07-16 165731.png b/assets/run_2026-07-16 165731.png new file mode 100644 index 0000000000000000000000000000000000000000..3372892175d5d7f6bcd914068b55ddd84585f9e5 GIT binary patch literal 313489 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

#M|S)HYPV!5ENy=VLW|I6CD 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

+UMep|=9LDiVY- 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�Stz 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 str: - return time.strftime("%Y-%m-%dT%H:%M:%S%z") - - -def percentile(values: list[float], pct: float) -> float: - if not values: - return 0.0 - ordered = sorted(values) - pos = (len(ordered) - 1) * pct - lo = math.floor(pos) - hi = math.ceil(pos) - if lo == hi: - return ordered[lo] - return ordered[lo] + (ordered[hi] - ordered[lo]) * (pos - lo) - - -def summarize_ms(samples: list[float]) -> dict[str, float]: - mean = statistics.fmean(samples) - return { - "avg_ms": mean, - "median_ms": statistics.median(samples), - "min_ms": min(samples), - "max_ms": max(samples), - "p90_ms": percentile(samples, 0.90), - "p95_ms": percentile(samples, 0.95), - "std_ms": statistics.pstdev(samples) if len(samples) > 1 else 0.0, - } - - -def sync(torch: Any, device: Any) -> None: - if str(device).startswith("cuda"): - torch.cuda.synchronize() - - -def timed_samples( - torch: Any, - device: Any, - fn: Callable[[], Any], - runs: int, - warmup: int, -) -> tuple[list[float], Any]: - last = None - for _ in range(warmup): - last = fn() - sync(torch, device) - - samples: list[float] = [] - for _ in range(runs): - start = time.perf_counter() - last = fn() - sync(torch, device) - samples.append((time.perf_counter() - start) * 1000.0) - return samples, last - - -def cuda_memory(torch: Any, device: Any) -> dict[str, float]: - if not str(device).startswith("cuda"): - return {} - return { - "cuda_allocated_mb": torch.cuda.memory_allocated(device) / (1024**2), - "cuda_reserved_mb": torch.cuda.memory_reserved(device) / (1024**2), - "cuda_peak_allocated_mb": torch.cuda.max_memory_allocated(device) / (1024**2), - } - - -def process_rss_mb() -> float | None: - try: - import psutil - - return psutil.Process().memory_info().rss / (1024**2) - except Exception: - return None - - -@dataclass -class BenchRow: - suite: str - name: str - backend: str - batch_size: int = 0 - sequence_length: int = 0 - tokens: int = 0 - avg_ms: float = 0.0 - median_ms: float = 0.0 - min_ms: float = 0.0 - max_ms: float = 0.0 - p90_ms: float = 0.0 - p95_ms: float = 0.0 - std_ms: float = 0.0 - tokens_per_sec: float = 0.0 - samples: int = 0 - loss: float | None = None - memory_mb: float | None = None - notes: str = "" - - -class QuadtrixPythonBenchmark: - def __init__(self, args: argparse.Namespace): - self.args = args - self.engine = load_engine_module() - self.torch = __import__("torch") - self.torch.manual_seed(args.seed) - self.device = self.engine.device - self.rows: list[BenchRow] = [] - - if str(self.device).startswith("cuda"): - self.torch.cuda.reset_peak_memory_stats(self.device) - - self.model = self._make_model() - self.model.eval() - - def _make_model(self): - checkpoint = Path( - self.args.checkpoint) if self.args.checkpoint else self.engine.default_checkpoint_path() - if checkpoint.exists() and not self.args.random_weights: - return self.engine.load_model(checkpoint) - - model = self.engine.GPTLanguageModel().to(self.device) - model.eval() - return model - - def _record(self, row: BenchRow) -> None: - self.rows.append(row) - print( - f"{row.suite:<14} {row.name:<24} " - f"avg={row.avg_ms:9.3f} ms " - f"p95={row.p95_ms:9.3f} ms " - f"tok/s={row.tokens_per_sec:10.1f}" - ) - - def run(self) -> dict[str, Any]: - print("Benchmark") - print(f"Device: {self.device}") - print(f"Runs: {self.args.runs}, warmup: {self.args.warmup}") - - self.bench_tokenizer_and_data() - self.bench_primitives() - self.bench_forward() - self.bench_training_step() - self.bench_generation() - - return self.save() - - def bench_tokenizer_and_data(self) -> None: - data_path = Path(self.args.data) - text = data_path.read_text( - encoding="utf-8") if data_path.exists() else "llm.cpp-benchmark text. " * 512 - if self.args.max_data_chars and len(text) > self.args.max_data_chars: - text = text[: self.args.max_data_chars] - tokenizer = self.engine.tokenizer - - samples, encoded = timed_samples( - self.torch, - self.device, - lambda: tokenizer.encode(text), - self.args.runs, - self.args.warmup, - ) - stats = summarize_ms(samples) - self._record( - BenchRow( - suite="data", - name="tokenizer_encode", - backend="python", - tokens=len(encoded), - tokens_per_sec=len(encoded) / (stats["avg_ms"] / 1000.0), - samples=len(samples), - memory_mb=process_rss_mb(), - **stats, - ) - ) - - tensor = self.torch.tensor(encoded, dtype=self.torch.long) - max_block = min(self.engine.block_size, max(2, len(tensor) - 2)) - batch_size = min(self.args.batch_size, max( - 1, len(tensor) - max_block - 1)) - - def make_batch(): - ix = self.torch.randint(len(tensor) - max_block - 1, (batch_size,)) - x = self.torch.stack([tensor[i: i + max_block] - for i in ix]).to(self.device) - y = self.torch.stack([tensor[i + 1: i + max_block + 1] - for i in ix]).to(self.device) - return x, y - - samples, _ = timed_samples( - self.torch, self.device, make_batch, self.args.runs, self.args.warmup) - stats = summarize_ms(samples) - self._record( - BenchRow( - suite="data", - name="batch_sample_to_device", - backend="python", - batch_size=batch_size, - sequence_length=max_block, - tokens=batch_size * max_block, - tokens_per_sec=(batch_size * max_block) / - (stats["avg_ms"] / 1000.0), - samples=len(samples), - memory_mb=process_rss_mb(), - **stats, - ) - ) - - def bench_primitives(self) -> None: - torch = self.torch - cases = [ - ("matmul_3d", (1, 16, self.engine.n_embd), - (self.engine.n_embd, self.engine.n_embd)), - ("matmul_3d", (self.args.batch_size, self.engine.block_size, - self.engine.n_embd), (self.engine.n_embd, self.engine.n_embd)), - ("attention_scores", (self.args.batch_size, - self.engine.block_size, self.engine.n_embd), None), - ] - for name, x_shape, w_shape in cases: - x = torch.randn(*x_shape, device=self.device) - if name == "attention_scores": - def fn(): return torch.softmax( - (x @ x.transpose(-2, -1)) / math.sqrt(x_shape[-1]), dim=-1) - else: - w = torch.randn(*w_shape, device=self.device) - def fn(): return x @ w - samples, _ = timed_samples( - torch, self.device, fn, self.args.runs, self.args.warmup) - stats = summarize_ms(samples) - tokens = x_shape[0] * x_shape[1] - self._record( - BenchRow( - suite="primitive", - name=f"{name}_{x_shape[0]}x{x_shape[1]}", - backend="python", - batch_size=x_shape[0], - sequence_length=x_shape[1], - tokens=tokens, - tokens_per_sec=tokens / (stats["avg_ms"] / 1000.0), - samples=len(samples), - memory_mb=process_rss_mb(), - **stats, - ) - ) - - def bench_forward(self) -> None: - torch = self.torch - cases = [(1, 8), (1, self.engine.block_size), - (self.args.batch_size, self.engine.block_size)] - self.model.eval() - for batch_size, seq_len in cases: - idx = torch.randint(self.engine.vocab_size, - (batch_size, seq_len), device=self.device) - targets = torch.randint( - self.engine.vocab_size, (batch_size, seq_len), device=self.device) - - @torch.no_grad() - def fn(): - return self.model(idx, targets) - - samples, last = timed_samples( - torch, self.device, fn, self.args.runs, self.args.warmup) - stats = summarize_ms(samples) - loss = float(last[1].item()) - tokens = batch_size * seq_len - self._record( - BenchRow( - suite="forward", - name=f"batch{batch_size}_seq{seq_len}", - backend="python", - batch_size=batch_size, - sequence_length=seq_len, - tokens=tokens, - tokens_per_sec=tokens / (stats["avg_ms"] / 1000.0), - samples=len(samples), - loss=loss, - memory_mb=process_rss_mb(), - **stats, - ) - ) - - def bench_training_step(self) -> None: - torch = self.torch - model = self.engine.GPTLanguageModel().to(self.device) - model.train() - optimizer = torch.optim.AdamW( - model.parameters(), lr=self.args.learning_rate) - batch_size = self.args.batch_size - seq_len = self.engine.block_size - idx = torch.randint(self.engine.vocab_size, - (batch_size, seq_len), device=self.device) - targets = torch.randint(self.engine.vocab_size, - (batch_size, seq_len), device=self.device) - - def fn(): - optimizer.zero_grad(set_to_none=True) - _, loss = model(idx, targets) - loss.backward() - optimizer.step() - return loss.detach() - - samples, loss = timed_samples( - torch, self.device, fn, self.args.train_steps, self.args.warmup) - stats = summarize_ms(samples) - tokens = batch_size * seq_len - self._record( - BenchRow( - suite="training", - name=f"adamw_step_b{batch_size}_s{seq_len}", - backend="python", - batch_size=batch_size, - sequence_length=seq_len, - tokens=tokens, - tokens_per_sec=tokens / (stats["avg_ms"] / 1000.0), - samples=len(samples), - loss=float(loss.item()), - memory_mb=process_rss_mb(), - **stats, - ) - ) - del model - gc.collect() - - def bench_generation(self) -> None: - torch = self.torch - tokenizer = self.engine.tokenizer - prompts = [ - ("empty", ""), - ("short", "The future of local AI is"), - ("long", "llama.cpp is a compact transformer benchmark that measures " * 4), - ] - self.model.eval() - for label, prompt in prompts: - encoded = tokenizer.encode(prompt) or [0] - encoded = encoded[-self.engine.block_size:] - idx = torch.tensor([encoded], dtype=torch.long, device=self.device) - - @torch.no_grad() - def fn(): - return self.model.generate(idx, self.args.generate_tokens, temperature=1.0, top_k=self.args.top_k) - - samples, _ = timed_samples( - torch, self.device, fn, self.args.runs, self.args.warmup) - stats = summarize_ms(samples) - self._record( - BenchRow( - suite="generation", - name=label, - backend="python", - batch_size=1, - sequence_length=len(encoded), - tokens=self.args.generate_tokens, - tokens_per_sec=self.args.generate_tokens / - (stats["avg_ms"] / 1000.0), - samples=len(samples), - memory_mb=process_rss_mb(), - **stats, - ) - ) - - def save(self) -> dict[str, Any]: - out_dir = Path(self.args.out) - out_dir.mkdir(parents=True, exist_ok=True) - n_params = sum(p.numel() for p in self.model.parameters()) - result = { - "schema_version": 1, - "timestamp": now_iso(), - "backend": "python", - "system": { - "platform": platform.platform(), - "python": sys.version.split()[0], - "torch": self.torch.__version__, - "device": str(self.device), - "cuda": getattr(self.torch.version, "cuda", None), - "rss_mb": process_rss_mb(), - **cuda_memory(self.torch, self.device), - }, - "model": { - "vocab_size": self.engine.vocab_size, - "block_size": self.engine.block_size, - "n_embd": self.engine.n_embd, - "n_head": self.engine.n_head, - "n_layer": self.engine.n_layer, - "dropout": self.engine.dropout, - "parameters": n_params, - "parameter_mb_fp32": n_params * 4 / (1024**2), - }, - "config": {key: str(value) if isinstance(value, Path) else value for key, value in vars(self.args).items()}, - "results": [asdict(row) for row in self.rows], - } - json_path = out_dir / "python_benchmark.json" - csv_path = out_dir / "python_benchmark.csv" - json_path.write_text(json.dumps(result, indent=2), encoding="utf-8") - with csv_path.open("w", newline="", encoding="utf-8") as f: - writer = csv.DictWriter( - f, fieldnames=list(asdict(self.rows[0]).keys())) - writer.writeheader() - for row in self.rows: - writer.writerow(asdict(row)) - print(f"Saved {json_path}") - print(f"Saved {csv_path}") - return result - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Benchmarks.") - parser.add_argument("--data", type=Path, default=DEFAULT_DATA) - parser.add_argument("--checkpoint", type=Path, default=None) - parser.add_argument("--out", type=Path, default=DEFAULT_OUT) - parser.add_argument("--runs", type=int, default=10) - parser.add_argument("--warmup", type=int, default=3) - parser.add_argument("--batch-size", type=int, default=4) - parser.add_argument("--train-steps", type=int, default=5) - parser.add_argument("--generate-tokens", type=int, default=32) - parser.add_argument("--learning-rate", type=float, default=3e-4) - parser.add_argument("--top-k", type=int, default=None) - parser.add_argument("--seed", type=int, default=1337) - parser.add_argument("--max-data-chars", type=int, default=1_000_000) - parser.add_argument("--random-weights", action="store_true", - help="Do not load a checkpoint even if one exists.") - parser.add_argument("--quick", action="store_true", - help="Short run for smoke tests.") - args = parser.parse_args() - if args.quick: - args.runs = 2 - args.warmup = 1 - args.train_steps = 1 - args.generate_tokens = 4 - args.max_data_chars = min(args.max_data_chars, 50_000) - return args - - -def main() -> int: - try: - benchmark = QuadtrixPythonBenchmark(parse_args()) - benchmark.run() - return 0 - except ImportError as exc: - print(f"Missing Python benchmark dependency: {exc}", file=sys.stderr) - print("Install the engine requirements, including torch and tiktoken.", file=sys.stderr) - return 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/config/config.h b/config/config.h index b88622d..58403e6 100644 --- a/config/config.h +++ b/config/config.h @@ -5,15 +5,15 @@ static const std::string DEFAULT_CLEANED_PATH = "data/input.txt"; static const std::string DATA_PATH_ENV_VAR = "GPT_DATA_PATH"; static const unsigned int SEED = 1337; static const double TRAIN_SPLIT = 0.9; // 90% train, 10% val -static const int BATCH_SIZE = 16; -static const int BLOCK_SIZE = 64; // Context length -static const int MAX_ITERS = 50; -static const int EVAL_INTERVAL = 250; -static const float LEARNING_RATE = 5e-4f; -static const int EVAL_ITERS = 100; +static const int BATCH_SIZE = 16; +static const int BLOCK_SIZE = 64; // Context length +static const int MAX_ITERS = 50; +static const int EVAL_INTERVAL = 250; +static const float LEARNING_RATE = 1e-4f; +static const int EVAL_ITERS = 100; static const int N_EMBD = 128; static const int N_HEAD = 4; static const int N_LAYER = 4; -static const float DROPOUT = 0.05f; +static const float DROPOUT = 0.05f; static const std::string BEST_MODEL_PATH = "best_model.bin"; static const std::string MODEL_PATH_ENV_VAR = "GPT_MODEL_PATH"; diff --git a/data/data_set.py b/data/data_set.py index dd10640..069e97d 100644 --- a/data/data_set.py +++ b/data/data_set.py @@ -2,9 +2,9 @@ import os # Settings -target_size_mb = 30 +target_size_mb = 10_000 target_size_bytes = target_size_mb * 1024 * 1024 -output_file = "cleaned.txt" +output_file = "input.txt" print(f"Streaming TinyStories until {target_size_mb} MB is reached...") @@ -15,14 +15,14 @@ with open(output_file, "w", encoding="utf-8") as f: for entry in dataset: story_text = entry["text"] + "\n\n" - + # Calculate size of this story in bytes story_bytes = len(story_text.encode('utf-8')) - + if current_size + story_bytes > target_size_bytes: break - + f.write(story_text) current_size += story_bytes -print(f"Done! Created '{output_file}' ({current_size / (1024*1024):.2f} MB)") \ No newline at end of file +print(f"Done! Created '{output_file}' ({current_size / (1024*1024):.2f} MB)") diff --git a/docs/run.md b/docs/run.md deleted file mode 100644 index a2c0e65..0000000 --- a/docs/run.md +++ /dev/null @@ -1,492 +0,0 @@ -# Quadtrix.cpp - -Quadtrix.cpp is a local GPT-style language model project with multiple runtime paths: - -- Native C++ inference and training through `Quadtrix.exe` / `main.cpp` -- PyTorch checkpoint inference through `engine/inference.py` and `engine/best_model .pt` -- FastAPI middleware in `backend/` -- React + TypeScript chat UI in `frontend/` - -The web interface can chat with both model backends: - -- `C++`: calls the C++ HTTP server on port `8080` -- `.pt`: loads the PyTorch checkpoint directly from `engine/best_model .pt` - -## Project Layout - -```text -Quadtrix.cpp/ - Quadtrix.exe - main.cpp - config/ - include/ - data/ - engine/ - inference.py - main.py - fine-tune/main.py - best_model .pt - fineweb_30mb.txt - backend/ - main.py - inference.py - requirements.txt - frontend/ - package.json - src/ -``` - -## Requirements - -- Python 3.10+ -- Node.js 18+ -- npm -- C++17 compiler if you want to rebuild the C++ executable - -## 1. Python Setup - -From the repo root: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp -python -m venv .venv -.\.venv\Scripts\python.exe -m pip install --upgrade pip -``` - -Install backend and PyTorch inference dependencies: - -```powershell -cd backend -..\.venv\Scripts\python.exe -m pip install -r requirements.txt -``` - -## 2. Frontend Setup - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp\frontend -npm.cmd install -npm.cmd run build -``` - -Run the frontend: - -```powershell -npm.cmd run dev -``` - -Frontend URL: - -```text -http://localhost:5173 -``` - -## Install as a Web App - -The frontend is configured as an installable PWA. It includes: - -- `frontend/manifest.webmanifest` -- `frontend/sw.js` -- `frontend/public/manifest.webmanifest` -- `frontend/public/sw.js` -- service worker registration in `frontend/src/registerServiceWorker.ts` - -For the clean installable version, build and preview the frontend: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp\frontend -npm.cmd run build -npm.cmd run preview -``` - -Open the preview URL, usually: - -```text -http://localhost:4173 -``` - -Then install from the browser: - -- Chrome / Edge: click the install icon in the address bar -- Or open browser menu -> Apps -> Install this site as an app - -The installed app still talks to the backend at: - -```text -http://localhost:3001 -``` - -So keep the FastAPI backend running when chatting. - -## 3. Run the PyTorch `.pt` Model in the Web UI - -The `.pt` model does not need a separate model server. The FastAPI backend loads it directly from: - -```text -engine/best_model .pt -``` - -Start the backend: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp\backend -..\.venv\Scripts\python.exe -m uvicorn main:app --host 127.0.0.1 --port 3001 -``` - -Start the frontend in another terminal: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp\frontend -npm.cmd run dev -``` - -Open: - -```text -http://localhost:5173 -``` - -Select `.pt` in the top bar. - -## 4. Run the C++ Model in the Web UI - -Start the C++ inference server: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp -.\Quadtrix.exe data\input.txt --server --port 8080 -``` - -Start the backend: - -```powershell -cd backend -..\.venv\Scripts\python.exe -m uvicorn main:app --host 127.0.0.1 --port 3001 -``` - -Start the frontend: - -```powershell -cd ..\frontend -npm.cmd run dev -``` - -Open: - -```text -http://localhost:5173 -``` - -Select `C++` in the top bar. - -## 5. Run Both Backends Together - -Use three terminals. - -Terminal 1: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp -.\Quadtrix.exe data\input.txt --server --port 8080 -``` - -Terminal 2: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp\backend -..\.venv\Scripts\python.exe -m uvicorn main:app --host 127.0.0.1 --port 3001 -``` - -Terminal 3: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp\frontend -npm.cmd run dev -``` - -Open: - -```text -http://localhost:5173 -``` - -Switch between `C++` and `.pt` from the model selector. - -## 6. Backend API - -Base URL: - -```text -http://localhost:3001 -``` - -Routes: - -```text -GET /api/health -GET /api/stats -POST /api/chat -GET /api/sessions -POST /api/sessions -DELETE /api/sessions/{id} -GET /api/sessions/{id}/messages -POST /api/feedback -``` - -Example `.pt` chat request: - -```powershell -Invoke-RestMethod ` - -Uri http://localhost:3001/api/chat ` - -Method Post ` - -ContentType "application/json" ` - -Body '{ - "session_id": null, - "prompt": "Once upon a time", - "max_tokens": 100, - "temperature": 1.0, - "stream": false, - "model_backend": "torch" - }' -``` - -Example C++ chat request: - -```powershell -Invoke-RestMethod ` - -Uri http://localhost:3001/api/chat ` - -Method Post ` - -ContentType "application/json" ` - -Body '{ - "session_id": null, - "prompt": "Once upon a time", - "max_tokens": 100, - "temperature": 1.0, - "stream": false, - "model_backend": "cpp" - }' -``` - -## 7. Environment Variables - -Backend defaults are in `backend/.env.example`: - -```text -API_PORT=3001 -CORS_ORIGINS=http://localhost:5173 -REDIS_URL= -LOG_LEVEL=INFO -MAX_SESSIONS=1000 -SESSION_TTL_HOURS=24 -CPP_SERVER_URL=http://localhost:8080 -TORCH_CHECKPOINT_PATH=../engine/best_model .pt -REQUEST_TIMEOUT_SECONDS=60 -``` - -Create `backend/.env` if you want overrides. - -Frontend defaults are in `frontend/.env.example`: - -```text -VITE_API_BASE_URL=http://localhost:3001 -``` - -## 8. PyTorch CLI Inference - -Interactive chat: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp -.\.venv\Scripts\python.exe engine\inference.py --checkpoint "engine\best_model .pt" -``` - -Generate once: - -```powershell -.\.venv\Scripts\python.exe engine\inference.py --checkpoint "engine\best_model .pt" --prompt "Hello" --max-new-tokens 100 --temperature 1.0 -``` - -## 9. PyTorch Training - -Main training: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp -.\.venv\Scripts\python.exe engine\main.py -``` - -Fine-tuning: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp -.\.venv\Scripts\python.exe engine\fine-tune\main.py -``` - -## 10. C++ Build and Run - -Build manually: - -```powershell -g++ -std=c++17 -O2 -I. -Iinclude -o Quadtrix.exe main.cpp -``` - -Train from scratch: - -```powershell -.\Quadtrix.exe data\input.txt -``` - -Terminal chat: - -```powershell -.\Quadtrix.exe data\input.txt --chat -``` - -Raw generation: - -```powershell -.\Quadtrix.exe data\input.txt --generate -``` - -HTTP server: - -```powershell -.\Quadtrix.exe data\input.txt --server --port 8080 -``` - -## 11. Health Checks - -Backend: - -```powershell -Invoke-RestMethod http://localhost:3001/api/health -``` - -C++ server: - -```powershell -Invoke-RestMethod http://localhost:8080/health -``` - -Frontend: - -```text -http://localhost:5173 -``` - -When only `.pt` is available, backend health should show: - -```json -{ - "status": "degraded", - "api": "ok", - "cpp_server": "unreachable", - "torch_model": "ok" -} -``` - -When both are available, backend health should show: - -```json -{ - "status": "ok", - "api": "ok", - "cpp_server": "ok", - "torch_model": "ok" -} -``` - -## 12. Troubleshooting - -### PowerShell blocks `npm` - -Use `npm.cmd`: - -```powershell -npm.cmd run dev -npm.cmd run build -``` - -### `.pt` model is unavailable - -Check that this file exists: - -```text -engine/best_model .pt -``` - -Then check Python dependencies: - -```powershell -cd backend -..\.venv\Scripts\python.exe -c "import torch, tiktoken; print(torch.__version__)" -``` - -### Backend cannot import FastAPI - -Install dependencies into the repo venv: - -```powershell -cd backend -..\.venv\Scripts\python.exe -m pip install -r requirements.txt -``` - -### C++ option is offline - -Start the C++ server: - -```powershell -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp -.\Quadtrix.exe data\input.txt --server --port 8080 -``` - -### Frontend cannot reach backend - -Check: - -```text -http://localhost:3001/api/health -``` - -Make sure frontend config points to: - -```text -VITE_API_BASE_URL=http://localhost:3001 -``` - -### Port already in use - -```powershell -Get-NetTCPConnection -LocalPort 3001 -Get-NetTCPConnection -LocalPort 5173 -Get-NetTCPConnection -LocalPort 8080 -``` - -## Recommended Daily Run - -```powershell -# Terminal 1 -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp -.\Quadtrix.exe data\input.txt --server --port 8080 -``` - -```powershell -# Terminal 2 -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp\backend -..\.venv\Scripts\python.exe -m uvicorn main:app --host 127.0.0.1 --port 3001 -``` - -```powershell -# Terminal 3 -cd C:\Users\Admin\Documents\GitHub\Quadtrix.cpp\frontend -npm.cmd run dev -``` - -Open: - -```text -http://localhost:5173 -``` - -## License - -MIT diff --git a/docs/quadtrix_training_report.png b/docs/training_report.png similarity index 100% rename from docs/quadtrix_training_report.png rename to docs/training_report.png diff --git a/engine/distributed/infer.py b/engine/distributed/infer.py new file mode 100644 index 0000000..8112c10 --- /dev/null +++ b/engine/distributed/infer.py @@ -0,0 +1,189 @@ +import os +import sys +import tiktoken +import torch +import torch.distributed as dist +import torch.nn as nn +from torch.nn import functional as F +BLOCK_SIZE = 20 +N_EMBD = 6 +N_HEAD = 4 +N_LAYER = 4 +DROPOUT = 0.0 +MODEL_WEIGHTS_PATH = "llm.pt" +ddp = int(os.environ.get("RANK", -1)) != -1 +if ddp: + dist.init_process_group(backend="nccl") + ddp_rank = int(os.environ.get("RANK")) + ddp_local_rank = int(os.environ.get("LOCAL_RANK")) + ddp_world_size = int(os.environ.get("WORLD_SIZE")) + device = f"cuda:{ddp_local_rank}" + torch.cuda.set_device(device) + master_process = ddp_rank == 0 +else: + ddp_rank = 0 + ddp_local_rank = 0 + ddp_world_size = 1 + master_process = True + device = "cuda" if torch.cuda.is_available() else "cpu" +torch.manual_seed(1337 + ddp_rank) + + +class MiniQuadtrixHead(nn.Module): + def __init__(self, head_size): + super().__init__() + self.key = nn.Linear(N_EMBD, head_size, bias=False) + self.query = nn.Linear(N_EMBD, head_size, bias=False) + self.value = nn.Linear(N_EMBD, head_size, bias=False) + self.register_buffer("tril", torch.tril( + torch.ones(BLOCK_SIZE, BLOCK_SIZE))) + self.dropout = nn.Dropout(DROPOUT) + + def forward(self, x): + B, T, C = x.shape + k = self.key(x) + q = self.query(x) + wei = q @ k.transpose(-2, -1) * k.shape[-1] ** -0.5 + wei = wei.masked_fill(self.tril[:T, :T] == 0, float("-inf")) + wei = F.softmax(wei, dim=-1) + wei = self.dropout(wei) + return wei @ self.value(x) + + +class MiniQuadtrixMHA(nn.Module): + def __init__(self, num_heads, head_size): + super().__init__() + self.heads = nn.ModuleList( + [MiniQuadtrixHead(head_size) for _ in range(num_heads)]) + self.proj = nn.Linear(head_size * num_heads, N_EMBD) + self.dropout = nn.Dropout(DROPOUT) + + def forward(self, x): + out = torch.cat([h(x) for h in self.heads], dim=-1) + return self.dropout(self.proj(out)) + + +class MiniQuadtrixFFN(nn.Module): + def __init__(self, n_embd): + super().__init__() + self.net = nn.Sequential( + nn.Linear(n_embd, 4 * n_embd), + nn.ReLU(), + nn.Linear(4 * n_embd, n_embd), + nn.Dropout(DROPOUT), + ) + + def forward(self, x): + return self.net(x) + + +class MiniQuadtrixBlock(nn.Module): + def __init__(self, n_embd, n_head): + super().__init__() + head_size = n_embd // n_head + self.sa = MiniQuadtrixMHA(n_head, head_size) + self.ffwd = MiniQuadtrixFFN(n_embd) + self.ln1 = nn.LayerNorm(n_embd) + self.ln2 = nn.LayerNorm(n_embd) + + def forward(self, x): + x = x + self.sa(self.ln1(x)) + x = x + self.ffwd(self.ln2(x)) + return x + + +class MiniQuadtrix(nn.Module): + def __init__(self, vocab_size): + super().__init__() + self.token_embedding_table = nn.Embedding(vocab_size, N_EMBD) + self.position_embedding_table = nn.Embedding(BLOCK_SIZE, N_EMBD) + self.blocks = nn.Sequential( + *[MiniQuadtrixBlock(N_EMBD, n_head=N_HEAD) for _ in range(N_LAYER)] + ) + self.ln_f = nn.LayerNorm(N_EMBD) + self.lm_head = nn.Linear(N_EMBD, vocab_size) + + def forward(self, idx): + B, T = idx.shape + tok_emb = self.token_embedding_table(idx) + pos_emb = self.position_embedding_table( + torch.arange(T, device=idx.device)) + x = tok_emb + pos_emb + x = self.blocks(x) + x = self.ln_f(x) + logits = self.lm_head(x) + return logits + + def generate(self, idx, max_new_tokens): + for _ in range(max_new_tokens): + idx_cond = idx[:, -BLOCK_SIZE:] + logits = self(idx_cond) + logits = logits[:, -1, :] + probs = F.softmax(logits, dim=-1) + idx_next = torch.multinomial(probs, num_samples=1) + idx = torch.cat((idx, idx_next), dim=1) + return idx + + +@torch.inference_mode() +def main(): + tokenizer = tiktoken.get_encoding("o200k_base") + vocab_size = tokenizer.n_vocab + model = MiniQuadtrix(vocab_size).to(device) + model.load_state_dict(torch.load(MODEL_WEIGHTS_PATH, + map_location=device, weights_only=True)) + model.eval() + + if master_process: + print(f"Cluster inference online. Total GPUs active: {ddp_world_size}") + + while True: + prompt_str = "" + if master_process: + try: + prompt_str = input("user > ").strip() + if not prompt_str: + prompt_str = "IGNORE_EMPTY" + except (KeyboardInterrupt, EOFError): + prompt_str = "EXIT_ENGINE" + if ddp: + str_len = torch.tensor( + [len(prompt_str)], dtype=torch.long, device=device) + dist.broadcast(str_len, src=0) + char_tensor = torch.tensor( + [ord(c) for c in prompt_str], dtype=torch.long, device=device) + if not master_process: + char_tensor = torch.zeros( + str_len.item(), dtype=torch.long, device=device) + dist.broadcast(char_tensor, src=0) + prompt_str = "".join([chr(x) for x in char_tensor.tolist()]) + + if prompt_str in ("exit", "quit", "q", "EXIT_ENGINE"): + break + if prompt_str == "IGNORE_EMPTY": + continue + + tokens = tokenizer.encode(prompt_str) + context = torch.tensor([tokens], dtype=torch.long, device=device) + output_ids = model.generate(context, max_new_tokens=100) + new_tokens = output_ids[0][len(tokens):].tolist() + local_response = tokenizer.decode(new_tokens).strip() + if ddp: + + gather_objects = [None] * ddp_world_size + dist.all_gather_object(gather_objects, local_response) + else: + gather_objects = [local_response] + + if master_process: + print(f"\n--- Outputs across {ddp_world_size} GPUs ---") + for rank_id, resp in enumerate(gather_objects): + print(f"[GPU {rank_id}] > {resp}") + print("-" * 40 + "\n") + + if ddp: + dist.destroy_process_group() + + +if __name__ == "__main__": + main() diff --git a/engine/distributed/train.py b/engine/distributed/train.py new file mode 100644 index 0000000..55510c8 --- /dev/null +++ b/engine/distributed/train.py @@ -0,0 +1,332 @@ +import os +import sys +import time +from pathlib import Path +import tiktoken +import torch +import torch.distributed as dist +import torch.nn as nn +from torch.nn import functional as F +from torch.nn.parallel import DistributedDataParallel as DDP + +SCRIPT_DIR = Path(__file__).resolve().parent + +# ------------------------------------- +ddp = int(os.environ.get("RANK", -1)) != -1 +if ddp: + dist.init_process_group(backend="nccl") + ddp_rank = int(os.environ.get("RANK")) + ddp_local_rank = int(os.environ.get("LOCAL_RANK")) + ddp_world_size = int(os.environ.get("WORLD_SIZE")) + device = f"cuda:{ddp_local_rank}" + torch.cuda.set_device(device) + master_process = ddp_rank == 0 +else: + # Fallback to local single-device execution + ddp_rank = 0 + ddp_local_rank = 0 + ddp_world_size = 1 + master_process = True + device = "cuda" if torch.cuda.is_available() else "cpu" + +if master_process: + print("[llm] - Distributed Cluster Mode Enabled") + print(f"PyTorch Version: {torch.__version__}") + print(f"Total Cluster GPUs (World Size): {ddp_world_size if ddp else 1}") + +start = time.time() +cleaned_path = Path(os.environ.get("data", SCRIPT_DIR / "input.txt")) +train_split = 0.9 +seed = 1337 +batch_size = 2 +block_size = 20 +max_iters = 10000 +eval_interval = 50 +learning_rate = 3e-4 +eval_iters = 20 +n_embd = 6 +n_head = 4 +n_layer = 4 +dropout = 0.0 +torch.manual_seed(seed + ddp_rank) + + +def get_miniq_tokenizer(encoding_name="o200k_base"): + miniq_tokenizer = tiktoken.get_encoding(encoding_name) + miniq_vocab_size = miniq_tokenizer.n_vocab + return miniq_tokenizer, miniq_vocab_size + + +def miniq_encode(text, tokenizer): + return tokenizer.encode(text) + + +def miniq_decode(tokens, tokenizer): + return tokenizer.decode(tokens) + + +# All processes load the dataset +with open(cleaned_path, "r", encoding="utf-8") as f: + text = f.read() + +miniq_tokenizer, vocab_size = get_miniq_tokenizer("o200k_base") +encoded_data = miniq_encode(text, miniq_tokenizer) +data = torch.tensor(encoded_data, dtype=torch.long) +n = int(train_split * len(data)) +train_data = data[:n] +val_data = data[n:] + + +def get_batch(split): + data_split = train_data if split == "train" else val_data + ix = torch.randint(len(data_split) - block_size, (batch_size,)) + x = torch.stack([data_split[i: i + block_size] for i in ix]) + y = torch.stack([data_split[i + 1: i + block_size + 1] for i in ix]) + x, y = x.to(device), y.to(device) + return x, y + + +@torch.no_grad() +def estimate_loss(): + out = {} + raw_model.eval() # Use un-wrapped model instance references + for split in ["train", "val"]: + losses = torch.zeros(eval_iters) + for k in range(eval_iters): + X, Y = get_batch(split) + _, loss = miniq_model(X, Y) + losses[k] = loss.item() + out[split] = losses.mean() + raw_model.train() + return out + + +class MiniQuadtrixHead(nn.Module): + + def __init__(self, head_size): + super().__init__() + self.key = nn.Linear(n_embd, head_size, bias=False) + self.query = nn.Linear(n_embd, head_size, bias=False) + self.value = nn.Linear(n_embd, head_size, bias=False) + self.register_buffer( + "tril", torch.tril(torch.ones(block_size, block_size)) + ) + self.dropout = nn.Dropout(dropout) + + def forward(self, x): + B, T, C = x.shape + k = self.key(x) + q = self.query(x) + wei = q @ k.transpose(-2, -1) * k.shape[-1] ** -0.5 + wei = wei.masked_fill(self.tril[:T, :T] == 0, float("-inf")) + wei = F.softmax(wei, dim=-1) + wei = self.dropout(wei) + return wei @ self.value(x) + + +class MiniQuadtrixMHA(nn.Module): + + def __init__(self, num_heads, head_size): + super().__init__() + self.heads = nn.ModuleList( + [MiniQuadtrixHead(head_size) for _ in range(num_heads)] + ) + self.proj = nn.Linear(head_size * num_heads, n_embd) + self.dropout = nn.Dropout(dropout) + + def forward(self, x): + out = torch.cat([h(x) for h in self.heads], dim=-1) + return self.dropout(self.proj(out)) + + +class MiniQuadtrixFFN(nn.Module): + + def __init__(self, n_embd): + super().__init__() + self.net = nn.Sequential( + nn.Linear(n_embd, 4 * n_embd), + nn.ReLU(), + nn.Linear(4 * n_embd, n_embd), + nn.Dropout(dropout), + ) + + def forward(self, x): + return self.net(x) + + +class MiniQuadtrixBlock(nn.Module): + + def __init__(self, n_embd, n_head): + super().__init__() + head_size = n_embd // n_head + self.sa = MiniQuadtrixMHA(n_head, head_size) + self.ffwd = MiniQuadtrixFFN(n_embd) + self.ln1 = nn.LayerNorm(n_embd) + self.ln2 = nn.LayerNorm(n_embd) + + def forward(self, x): + x = x + self.sa(self.ln1(x)) + x = x + self.ffwd(self.ln2(x)) + return x + + +class MiniQuadtrix(nn.Module): + + def __init__(self): + super().__init__() + self.token_embedding_table = nn.Embedding(vocab_size, n_embd) + self.position_embedding_table = nn.Embedding(block_size, n_embd) + self.blocks = nn.Sequential( + *[ + MiniQuadtrixBlock(n_embd, n_head=n_head) + for _ in range(n_layer) + ] + ) + self.ln_f = nn.LayerNorm(n_embd) + self.lm_head = nn.Linear(n_embd, vocab_size) + self.apply(self._init_weights) + + def _init_weights(self, module): + if isinstance(module, nn.Linear): + torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) + if module.bias is not None: + torch.nn.init.zeros_(module.bias) + elif isinstance(module, nn.Embedding): + torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) + + def forward(self, idx, targets=None): + B, T = idx.shape + tok_emb = self.token_embedding_table(idx) + pos_emb = self.position_embedding_table( + torch.arange(T, device=idx.device) + ) + x = tok_emb + pos_emb + x = self.blocks(x) + x = self.ln_f(x) + logits = self.lm_head(x) + + if targets is None: + loss = None + else: + B, T, C = logits.shape + logits = logits.view(B * T, C) + targets = targets.view(B * T) + loss = F.cross_entropy(logits, targets) + return logits, loss + + def generate(self, idx, max_new_tokens): + for _ in range(max_new_tokens): + idx_cond = idx[:, -block_size:] + logits, _ = self(idx_cond) + logits = logits[:, -1, :] + probs = F.softmax(logits, dim=-1) + idx_next = torch.multinomial(probs, num_samples=1) + idx = torch.cat((idx, idx_next), dim=1) + return idx + + +miniq_model = MiniQuadtrix().to(device) +raw_model = miniq_model + +if ddp: + miniq_model = DDP(miniq_model, device_ids=[ddp_local_rank]) + raw_model = miniq_model.module + +miniq_n_params = sum(p.numel() for p in raw_model.parameters()) +miniq_optimizer = torch.optim.AdamW(miniq_model.parameters(), lr=learning_rate) + +if master_process: + print("CONFIG") + print(f"Seed Configuration: {seed}") + print(f"Per-GPU Batch size: {batch_size}") + print(f"Effective Cluster Batch size: {batch_size * ddp_world_size}") + print(f"Block size: {block_size}") + print(f"Learning rate: {learning_rate}") + print(f"Parameters: {miniq_n_params:,}") + print(f"Train tokens: {len(train_data):,}") + print() + +best_val_loss = float("inf") +train_start = time.time() + +for iter in range(max_iters): + if iter % eval_interval == 0 or iter == max_iters - 1: + losses = estimate_loss() + elapsed = time.time() - train_start + total_norm = 0 + for p in miniq_model.parameters(): + if p.grad is not None: + param_norm = p.grad.detach().data.norm(2) + total_norm += param_norm.item() ** 2 + total_norm = total_norm ** 0.5 + + tokens_per_sec = ( + (iter + 1) * batch_size * ddp_world_size * block_size / elapsed + if elapsed > 0 + else 0 + ) + if master_process: + is_best = losses["val"] < best_val_loss + if is_best: + best_val_loss = losses["val"] + torch.save(raw_model.state_dict(), "llm.pt") + + print( + f"step {iter} | loss: {losses['train']:.6f} | val_loss: {losses['val']:.6f} | norm: {total_norm:.4f} | dt: {elapsed*1000:.2f}ms | tok/sec: {tokens_per_sec:.2f}" + ) + sys.stdout.flush() + + xb, yb = get_batch("train") + logits, loss = miniq_model(xb, yb) + miniq_optimizer.zero_grad(set_to_none=True) + loss.backward() + miniq_optimizer.step() + +total_time = time.time() - train_start + +if master_process: + print("\n" + "=" * 50) + print( + f"Training Duration: {int(total_time // 60)}m {int(total_time % 60):02d}s") + print(f"Best validation loss recorded: {best_val_loss:.4f}") + print("=" * 50 + "\n") + try: + raw_model.load_state_dict( + torch.load("llm.pt", map_location=device, weights_only=True) + ) + raw_model.eval() + + print("INFERENCE TERMINAL CHAT MODE") + print("Type 'exit' to terminate session.\n") + + while True: + prompt = input("user > ").strip() + if prompt.lower() in ("quit", "exit", "q"): + print("\nSession ended.") + break + if not prompt: + continue + + encoded_prompt = miniq_encode(prompt, miniq_tokenizer) + context = torch.tensor( + [encoded_prompt], dtype=torch.long, device=device + ) + + with torch.no_grad(): + output_ids = raw_model.generate(context, max_new_tokens=100) + + new_tokens = output_ids[0][len(encoded_prompt):].tolist() + response = miniq_decode(new_tokens, miniq_tokenizer).strip() + + print(f"\nModel > {response}\n") + + except (KeyboardInterrupt, FileNotFoundError): + print("\nInference interrupted or weight metrics file bypassed.") + + wall_clock = time.time() - start + print( + f"\nTotal process lifecycle runtime: {int(wall_clock // 60)}m {int(wall_clock % 60):02d}s\n") + +# Shutdown distributed worker nodes gracefully +if ddp: + dist.destroy_process_group() diff --git a/engine/fineweb_dataset.py b/engine/fineweb_dataset.py deleted file mode 100644 index 9a0107f..0000000 --- a/engine/fineweb_dataset.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Download 30MB of FineWeb dataset from Hugging Face. -FineWeb is a large-scale web text dataset for language model training. -""" - -from datasets import load_dataset -import os - -def download_fineweb_sample(output_dir="engine", target_size_mb=30): - """ - Download approximately 30MB of FineWeb dataset.- - """ - print(f"Downloading ~{target_size_mb}MB of FineWeb dataset...") - print("This may take a few minutes depending on your connection speed.\n") - os.makedirs(output_dir, exist_ok=True) - dataset = load_dataset( - "HuggingFaceFW/fineweb", - name="sample-10BT", - split="train", - streaming=True - ) - - # Download and save data until we reach ~30MB - target_bytes = target_size_mb * 1024 * 1024 - current_bytes = 0 - samples = [] - - print("Streaming and collecting samples...") - for i, sample in enumerate(dataset): - sample_size = len(sample['text'].encode('utf-8')) - - if current_bytes + sample_size > target_bytes: - break - - samples.append(sample) - current_bytes += sample_size - - if (i + 1) % 100 == 0: - print(f"Collected {i + 1} samples ({current_bytes / (1024*1024):.2f} MB)") - - print(f"\nDownloaded {len(samples)} samples ({current_bytes / (1024*1024):.2f} MB)") - output_file = os.path.join(output_dir, "input.txt") - with open(output_file, 'w', encoding='utf-8') as f: - for sample in samples: - f.write(sample['text']) - f.write('\n\n' + '='*80 + '\n\n') # Separator between documents - - print(f"Data saved to: {output_file}") - print(f"Total samples: {len(samples)}") - print(f"Total size: {current_bytes / (1024*1024):.2f} MB") - - return output_file - -if __name__ == "__main__": - try: - download_fineweb_sample() - print("\nDownload completed successfully!") - except Exception as e: - print(f"\ Error: {e}") - print("\nMake sure you have the 'datasets' library installed:") - print(" pip install datasets") \ No newline at end of file diff --git a/engine/iGPU/inference.py b/engine/iGPU/inference.py deleted file mode 100644 index 7f56f38..0000000 --- a/engine/iGPU/inference.py +++ /dev/null @@ -1,298 +0,0 @@ -import argparse -from pathlib import Path -import time - -import torch -import torch.nn as nn -from torch.nn import functional as F -import tiktoken - -try: - import torch_directml -except ImportError: - torch_directml = None - - -W = 78 -DOUBLE = "=" * W -SINGLE = "-" * W -ARROW = "->" - -block_size = 32 -n_embd = 64 -n_head = 4 -n_layer = 4 -dropout = 0.1 - - -def header(title, subtitle=""): - print(f"\n{DOUBLE}") - print(f" {title}") - if subtitle: - print(f" {subtitle}") - print(DOUBLE) - - -def row(label, value="", unit="", note=""): - label_col = f" {label:<28}" - value_col = f"{str(value):<20}" - unit_col = f"{unit:<8}" - note_col = f" {note}" if note else "" - print(f"{label_col}{value_col}{unit_col}{note_col}") - - -def rule(): - print(f" {SINGLE}") - - -def blank(): - print() - - -def get_device(): - if torch_directml is not None: - return torch_directml.device() - if torch.cuda.is_available(): - return torch.device("cuda") - return torch.device("cpu") - - -def get_tokenizer(encoding_name="gpt2"): - tokenizer = tiktoken.get_encoding(encoding_name) - return tokenizer, tokenizer.n_vocab - - -def encode(text, tokenizer): - return tokenizer.encode(text) - - -def decode(tokens, tokenizer): - return tokenizer.decode(tokens) - - -tokenizer, vocab_size = get_tokenizer("gpt2") -device = get_device() - - -class Head(nn.Module): - def __init__(self, head_size): - super().__init__() - self.key = nn.Linear(n_embd, head_size, bias=False) - self.query = nn.Linear(n_embd, head_size, bias=False) - self.value = nn.Linear(n_embd, head_size, bias=False) - self.register_buffer("tril", torch.tril(torch.ones(block_size, block_size))) - self.dropout = nn.Dropout(dropout) - - def forward(self, x): - _, T, _ = x.shape - k = self.key(x) - q = self.query(x) - wei = q @ k.transpose(-2, -1) * k.shape[-1] ** -0.5 - wei = wei.masked_fill(self.tril[:T, :T] == 0, float("-inf")) - wei = F.softmax(wei, dim=-1) - wei = self.dropout(wei) - return wei @ self.value(x) - - -class MultiHeadAttention(nn.Module): - def __init__(self, num_heads, head_size): - super().__init__() - self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)]) - self.proj = nn.Linear(head_size * num_heads, n_embd) - self.dropout = nn.Dropout(dropout) - - def forward(self, x): - out = torch.cat([h(x) for h in self.heads], dim=-1) - return self.dropout(self.proj(out)) - - -class FeedForward(nn.Module): - def __init__(self, n_embd): - super().__init__() - self.net = nn.Sequential( - nn.Linear(n_embd, 4 * n_embd), - nn.ReLU(), - nn.Linear(4 * n_embd, n_embd), - nn.Dropout(dropout), - ) - - def forward(self, x): - return self.net(x) - - -class Block(nn.Module): - def __init__(self, n_embd, n_head): - super().__init__() - head_size = n_embd // n_head - self.sa = MultiHeadAttention(n_head, head_size) - self.ffwd = FeedForward(n_embd) - self.ln1 = nn.LayerNorm(n_embd) - self.ln2 = nn.LayerNorm(n_embd) - - def forward(self, x): - x = x + self.sa(self.ln1(x)) - x = x + self.ffwd(self.ln2(x)) - return x - - -class GPTLanguageModel(nn.Module): - def __init__(self): - super().__init__() - self.token_embedding_table = nn.Embedding(vocab_size, n_embd) - self.position_embedding_table = nn.Embedding(block_size, n_embd) - self.blocks = nn.Sequential(*[Block(n_embd, n_head=n_head) for _ in range(n_layer)]) - self.ln_f = nn.LayerNorm(n_embd) - self.lm_head = nn.Linear(n_embd, vocab_size) - - def forward(self, idx, targets=None): - B, T = idx.shape - tok_emb = self.token_embedding_table(idx) - pos_emb = self.position_embedding_table(torch.arange(T, device=device)) - x = tok_emb + pos_emb - x = self.blocks(x) - x = self.ln_f(x) - logits = self.lm_head(x) - - if targets is None: - loss = None - else: - B, T, C = logits.shape - logits = logits.view(B * T, C) - targets = targets.view(B * T) - loss = F.cross_entropy(logits, targets) - return logits, loss - - def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None): - for _ in range(max_new_tokens): - idx_cond = idx[:, -block_size:] - logits, _ = self(idx_cond) - logits = logits[:, -1, :] / max(temperature, 1e-6) - - if top_k is not None: - values, _ = torch.topk(logits, min(top_k, logits.size(-1))) - logits[logits < values[:, [-1]]] = float("-inf") - - probs = F.softmax(logits, dim=-1) - idx_next = torch.multinomial(probs, num_samples=1) - idx = torch.cat((idx, idx_next), dim=1) - return idx - - -def default_checkpoint_path(): - script_dir = Path(__file__).resolve().parent - candidates = [ - script_dir / "best_model.pt", - Path.cwd() / "best_model.pt", - Path.cwd() / "iGPU" / "best_model.pt", - ] - for candidate in candidates: - if candidate.exists(): - return candidate - return script_dir / "best_model.pt" - - -def load_model(checkpoint_path): - checkpoint_path = Path(checkpoint_path) - if not checkpoint_path.exists(): - raise FileNotFoundError( - f"Checkpoint not found: {checkpoint_path}\n" - "Train first with iGPU/main.py, or pass --checkpoint path/to/best_model.pt" - ) - - model = GPTLanguageModel().to(device) - state_dict = torch.load(checkpoint_path, map_location=device) - model.load_state_dict(state_dict) - model.eval() - return model - - -def generate_response(model, prompt, max_new_tokens, temperature, top_k): - encoded_prompt = encode(prompt, tokenizer) - context = torch.tensor([encoded_prompt], dtype=torch.long, device=device) - - with torch.no_grad(): - output_ids = model.generate( - context, - max_new_tokens=max_new_tokens, - temperature=temperature, - top_k=top_k, - ) - - new_tokens = output_ids[0][len(encoded_prompt):].tolist() - return decode(new_tokens, tokenizer).strip() - - -def chat(model, args): - header("INFERENCE", "quit / exit / q -> end session") - blank() - - while True: - prompt = input(f" user {ARROW} ").strip() - if prompt.lower() in ("quit", "exit", "q"): - blank() - print(" Session ended.") - break - if not prompt: - continue - - response = generate_response( - model, - prompt, - args.max_new_tokens, - args.temperature, - args.top_k, - ) - blank() - print(f" Model {ARROW} {response}") - blank() - - -def parse_args(): - parser = argparse.ArgumentParser(description="Run inference from an iGPU trained .pt checkpoint.") - parser.add_argument( - "--checkpoint", - type=Path, - default=default_checkpoint_path(), - help="Path to the .pt file generated by iGPU/main.py.", - ) - parser.add_argument("--prompt", type=str, default=None, help="Generate once from this prompt.") - parser.add_argument("--max-new-tokens", type=int, default=200) - parser.add_argument("--temperature", type=float, default=1.0) - parser.add_argument("--top-k", type=int, default=None) - return parser.parse_args() - - -def main(): - args = parse_args() - start = time.time() - - print(f"{'Quadtrix-v1.0':^{W}}") - blank() - row("Started", time.strftime("%Y-%m-%d %H:%M:%S")) - row("Device", str(device)) - row("PyTorch", torch.__version__) - row("Checkpoint", args.checkpoint) - rule() - - model = load_model(args.checkpoint) - - if args.prompt: - response = generate_response( - model, - args.prompt, - args.max_new_tokens, - args.temperature, - args.top_k, - ) - blank() - print(response) - else: - chat(model, args) - - blank() - row("Total", f"{time.time() - start:.2f}s") - print(DOUBLE) - - -if __name__ == "__main__": - main() diff --git a/engine/iGPU/main.py b/engine/iGPU/main.py deleted file mode 100644 index d0db7a7..0000000 --- a/engine/iGPU/main.py +++ /dev/null @@ -1,360 +0,0 @@ -import torch -import torch_directml -import torch.nn as nn -from torch.nn import functional as F -import time -import sys -from pathlib import Path -import tiktoken - -# LOGGING UTILITIES -W = 78 -DOUBLE = "=" * W -SINGLE = "-" * W -TICK = "best" -ARROW = ">" - -LOG_DIR = Path(__file__).resolve().parent / "logs" -LOG_DIR.mkdir(parents=True, exist_ok=True) -LOG_PATH = LOG_DIR / f"run_{time.strftime('%Y%m%d_%H%M%S')}.txt" - -def log(message=""): - line = "" if message == "" else f"{time.strftime('%Y-%m-%d %H:%M:%S')} | {message}" - print(line) - with open(LOG_PATH, "a", encoding="utf-8") as f: - f.write(f"{line}\n") - -def header(title, subtitle=""): - log() - log(DOUBLE) - log(f" {title}") - if subtitle: - log(f" {subtitle}") - log(DOUBLE) - -def row(label, value="", unit="", note=""): - label_col = f" {label:<28}" - value_col = f"{str(value):<20}" - unit_col = f"{unit:<8}" - note_col = f" {note}" if note else "" - log(f"{label_col}{value_col}{unit_col}{note_col}") - -def rule(): log(f" {SINGLE}") -def blank(): log() -def info(msg): log(f" {ARROW} {msg}") -def success(msg): log(f" ok {msg}") - - -# SESSION - - - -log(f"{'Quadtrix-v1.0':^{W}}") -blank() -row("Started", time.strftime('%Y-%m-%d %H:%M:%S')) -row("Device", str(torch_directml.device())) -row("PyTorch", torch.__version__) -row("Log file", str(LOG_PATH)) - -start = time.time() - -# CONFIGURATION - - -cleaned_path = "engine\data\cleaned.txt" -train_split = 0.9 -seed = 1337 - -batch_size = 16 -block_size = 32 -max_iters = 3000 -eval_interval = 100 -learning_rate = 1e-3 -device = torch_directml.device() -eval_iters = 20 -n_embd = 64 -n_head = 4 -n_layer = 4 -dropout = 0.1 - -torch.manual_seed(seed) - - -# tokenizer - -def get_tokenizer(encoding_name="gpt2"): - tokenizer = tiktoken.get_encoding(encoding_name) - vocab_size = tokenizer.n_vocab - return tokenizer, vocab_size - -def encode(text, tokenizer): return tokenizer.encode(text) -def decode(tokens, tokenizer): return tokenizer.decode(tokens) - - - -# DATA - -with open(cleaned_path, 'r', encoding='utf-8') as f: - text = f.read() - -tokenizer, vocab_size = get_tokenizer("gpt2") -encoded_data = encode(text, tokenizer) - -data = torch.tensor(encoded_data, dtype=torch.long) -n = int(train_split * len(data)) -train_data = data[:n] -val_data = data[n:] - -# Batch and LOSS - -def get_batch(split): - data_split = train_data if split == 'train' else val_data - ix = torch.randint(len(data_split) - block_size, (batch_size,)) - x = torch.stack([data_split[i:i + block_size] for i in ix]) - y = torch.stack([data_split[i + 1:i + block_size + 1] for i in ix]) - x, y = x.to(device), y.to(device) - return x, y - -@torch.no_grad() -def estimate_loss(): - out = {} - model.eval() - for split in ['train', 'val']: - losses = torch.zeros(eval_iters) - for k in range(eval_iters): - X, Y = get_batch(split) - _, loss = model(X, Y) - losses[k] = loss.item() - out[split] = losses.mean() - model.train() - return out - -# model - -class Head(nn.Module): - def __init__(self, head_size): - super().__init__() - self.key = nn.Linear(n_embd, head_size, bias=False) - self.query = nn.Linear(n_embd, head_size, bias=False) - self.value = nn.Linear(n_embd, head_size, bias=False) - self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size))) - self.dropout = nn.Dropout(dropout) - - def forward(self, x): - B, T, C = x.shape - k = self.key(x) - q = self.query(x) - wei = q @ k.transpose(-2, -1) * k.shape[-1] ** -0.5 - wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf')) - wei = F.softmax(wei, dim=-1) - wei = self.dropout(wei) - return wei @ self.value(x) - -class MultiHeadAttention(nn.Module): - def __init__(self, num_heads, head_size): - super().__init__() - self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)]) - self.proj = nn.Linear(head_size * num_heads, n_embd) - self.dropout = nn.Dropout(dropout) - - def forward(self, x): - out = torch.cat([h(x) for h in self.heads], dim=-1) - return self.dropout(self.proj(out)) - -class FeedFoward(nn.Module): - def __init__(self, n_embd): - super().__init__() - self.net = nn.Sequential( - nn.Linear(n_embd, 4 * n_embd), - nn.ReLU(), - nn.Linear(4 * n_embd, n_embd), - nn.Dropout(dropout), - ) - - def forward(self, x): - return self.net(x) - -class Block(nn.Module): - def __init__(self, n_embd, n_head): - super().__init__() - head_size = n_embd // n_head - self.sa = MultiHeadAttention(n_head, head_size) - self.ffwd = FeedFoward(n_embd) - self.ln1 = nn.LayerNorm(n_embd) - self.ln2 = nn.LayerNorm(n_embd) - - def forward(self, x): - x = x + self.sa(self.ln1(x)) - x = x + self.ffwd(self.ln2(x)) - return x - -class GPTLanguageModel(nn.Module): - def __init__(self): - super().__init__() - self.token_embedding_table = nn.Embedding(vocab_size, n_embd) - self.position_embedding_table = nn.Embedding(block_size, n_embd) - self.blocks = nn.Sequential(*[Block(n_embd, n_head=n_head) for _ in range(n_layer)]) - self.ln_f = nn.LayerNorm(n_embd) - self.lm_head = nn.Linear(n_embd, vocab_size) - self.apply(self._init_weights) - - def _init_weights(self, module): - if isinstance(module, nn.Linear): - torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) - if module.bias is not None: - torch.nn.init.zeros_(module.bias) - elif isinstance(module, nn.Embedding): - torch.nn.init.normal_(module.weight, mean=0.0, std=0.02) - - def forward(self, idx, targets=None): - B, T = idx.shape - tok_emb = self.token_embedding_table(idx) - pos_emb = self.position_embedding_table(torch.arange(T, device=device)) - x = tok_emb + pos_emb - x = self.blocks(x) - x = self.ln_f(x) - logits = self.lm_head(x) - - if targets is None: - loss = None - else: - B, T, C = logits.shape - logits = logits.view(B * T, C) - targets = targets.view(B * T) - loss = F.cross_entropy(logits, targets) - return logits, loss - - def generate(self, idx, max_new_tokens): - for _ in range(max_new_tokens): - idx_cond = idx[:, -block_size:] - logits, _ = self(idx_cond) - logits = logits[:, -1, :] - probs = F.softmax(logits, dim=-1) - idx_next = torch.multinomial(probs, num_samples=1) - idx = torch.cat((idx, idx_next), dim=1) - return idx - - - -# INITIALISE - -model = GPTLanguageModel().to(device) -n_params = sum(p.numel() for p in model.parameters()) -optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate) - -header("CONFIG") -row("Seed", seed) -row("Batch size", batch_size) -row("Block size", block_size) -row("Learning rate", learning_rate) -row("Layers", n_layer) -row("Heads", n_head) -row("Embedding dim", n_embd) -row("Dropout", dropout) -row("Parameters", f"{n_params:,}") -row("Train tokens", f"{len(train_data):,}") -row("Val tokens", f"{len(val_data):,}") - - -#training -header("TRAINING", f"{max_iters:,} steps | eval every {eval_interval} | checkpoint on improvement") -blank() - -log(" training loop started") - -best_val_loss = float('inf') -train_start = time.time() - -for iter in range(max_iters): - - if iter % eval_interval == 0 or iter == max_iters - 1: - losses = estimate_loss() - elapsed = time.time() - train_start - pct = 100 * iter / max_iters - eta_secs = (elapsed / (iter + 1)) * (max_iters - iter - 1) if iter > 0 else 0 - is_best = losses['val'] < best_val_loss - status = f"{TICK} saved" if is_best else "-" - elapsed_fmt = f"{int(elapsed // 60)}m {int(elapsed % 60):02d}s" - eta_fmt = f"{int(eta_secs // 60)}m {int(eta_secs % 60):02d}s" - - if is_best: - best_val_loss = losses['val'] - torch.save(model.state_dict(), 'best_model.pt') - log(f" ckpt path=best_model.pt val_loss={best_val_loss:.4f} step={iter}") - - log( - f" train step={iter}/{max_iters} pct={pct:.1f}% " - f"loss_train={losses['train']:.4f} loss_val={losses['val']:.4f} " - f"elapsed={elapsed_fmt} eta={eta_fmt} status={status}" - ) - sys.stdout.flush() - - xb, yb = get_batch('train') - logits, loss = model(xb, yb) - optimizer.zero_grad(set_to_none=True) - loss.backward() - optimizer.step() - -total_time = time.time() - train_start -blank() -rule() -row("Duration", f"{int(total_time // 60)}m {int(total_time % 60):02d}s") -row("Best val loss", f"{best_val_loss:.4f}", "", TICK) -row("Checkpoint", "best_model.pt", "", TICK) -rule() - - - -# RESTORE CHECKPOIN -blank() -model.load_state_dict(torch.load('best_model.pt', map_location=device)) -model.eval() -success(f"Restored best_model.pt | val loss {best_val_loss:.4f}") - -# INFERENCE - - -header("INFERENCE", "quit / exit / q -> end session") -blank() - -try: - while True: - prompt = input(f" user {ARROW} ").strip() - log(f" user {ARROW} {prompt}") - - if prompt.lower() in ("quit", "exit", "q"): - blank() - success("Session ended.") - break - - if not prompt: - continue - - encoded_prompt = encode(prompt, tokenizer) - context = torch.tensor([encoded_prompt], dtype=torch.long, device=device) - - with torch.no_grad(): - output_ids = model.generate(context, max_new_tokens=200) - - new_tokens = output_ids[0][len(encoded_prompt):].tolist() - response = decode(new_tokens, tokenizer).strip() - - blank() - log(f" Model {ARROW} {response}") - blank() - -except KeyboardInterrupt: - blank() - success("Interrupted.") - - -end = time.time() -wall_clock = end - start - -blank() -rule() -row("Training", f"{int(total_time // 60)}m {int(total_time % 60):02d}s") -row("Total", f"{int(wall_clock // 60)}m {int(wall_clock % 60):02d}s", "", TICK) -rule() -blank() -log(f"{DOUBLE}\n") diff --git a/engine/inference.py b/engine/inference.py index d4ce649..c4d7bc9 100644 --- a/engine/inference.py +++ b/engine/inference.py @@ -1,74 +1,41 @@ -import argparse -from pathlib import Path -import time - +import os +import sys +import tiktoken import torch import torch.nn as nn from torch.nn import functional as F -import tiktoken - - -ARROW = "->" - -block_size = 32 -n_embd = 64 -n_head = 4 -n_layer = 4 -dropout = 0.1 - -device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - - -def header(title, subtitle=""): - print(f" {title}") - if subtitle: - print(f" {subtitle}") - - -def row(label, value="", unit="", note=""): - label_col = f" {label:<28}" - value_col = f"{str(value):<20}" - unit_col = f"{unit:<8}" - note_col = f" {note}" if note else "" - print(f"{label_col}{value_col}{unit_col}{note_col}") - - -def rule(): - print(f"") +# hyperparameters (Must match your trained model exactly) +BLOCK_SIZE = 20 +N_EMBD = 6 +N_HEAD = 4 +N_LAYER = 4 +DROPOUT = 0.0 # Set to 0 for deterministic inference evaluation +MODEL_WEIGHTS_PATH = "llm.pt" +# ----------------------------------- +DEVICE = ( + "cuda" + if torch.cuda.is_available() + else "mps" + if torch.backends.mps.is_available() + else "cpu" +) + + +class MiniQuadtrixHead(nn.Module): -def blank(): - print() - - -def get_tokenizer(encoding_name="gpt2"): - tokenizer = tiktoken.get_encoding(encoding_name) - return tokenizer, tokenizer.n_vocab - - -def encode(text, tokenizer): - return tokenizer.encode(text) - - -def decode(tokens, tokenizer): - return tokenizer.decode(tokens) - - -tokenizer, vocab_size = get_tokenizer("gpt2") - - -class Head(nn.Module): def __init__(self, head_size): super().__init__() - self.key = nn.Linear(n_embd, head_size, bias=False) - self.query = nn.Linear(n_embd, head_size, bias=False) - self.value = nn.Linear(n_embd, head_size, bias=False) - self.register_buffer("tril", torch.tril( - torch.ones(block_size, block_size))) - self.dropout = nn.Dropout(dropout) + self.key = nn.Linear(N_EMBD, head_size, bias=False) + self.query = nn.Linear(N_EMBD, head_size, bias=False) + self.value = nn.Linear(N_EMBD, head_size, bias=False) + self.register_buffer( + "tril", torch.tril(torch.ones(BLOCK_SIZE, BLOCK_SIZE)) + ) + self.dropout = nn.Dropout(DROPOUT) def forward(self, x): - _, T, _ = x.shape + B, T, C = x.shape k = self.key(x) q = self.query(x) wei = q @ k.transpose(-2, -1) * k.shape[-1] ** -0.5 @@ -78,38 +45,43 @@ def forward(self, x): return wei @ self.value(x) -class MultiHeadAttention(nn.Module): +class MiniQuadtrixMHA(nn.Module): + def __init__(self, num_heads, head_size): super().__init__() - self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)]) - self.proj = nn.Linear(head_size * num_heads, n_embd) - self.dropout = nn.Dropout(dropout) + self.heads = nn.ModuleList( + [MiniQuadtrixHead(head_size) for _ in range(num_heads)] + ) + self.proj = nn.Linear(head_size * num_heads, N_EMBD) + self.dropout = nn.Dropout(DROPOUT) def forward(self, x): out = torch.cat([h(x) for h in self.heads], dim=-1) return self.dropout(self.proj(out)) -class FeedForward(nn.Module): +class MiniQuadtrixFFN(nn.Module): + def __init__(self, n_embd): super().__init__() self.net = nn.Sequential( nn.Linear(n_embd, 4 * n_embd), nn.ReLU(), nn.Linear(4 * n_embd, n_embd), - nn.Dropout(dropout), + nn.Dropout(DROPOUT), ) def forward(self, x): return self.net(x) -class Block(nn.Module): +class MiniQuadtrixBlock(nn.Module): + def __init__(self, n_embd, n_head): super().__init__() head_size = n_embd // n_head - self.sa = MultiHeadAttention(n_head, head_size) - self.ffwd = FeedForward(n_embd) + self.sa = MiniQuadtrixMHA(n_head, head_size) + self.ffwd = MiniQuadtrixFFN(n_embd) self.ln1 = nn.LayerNorm(n_embd) self.ln2 = nn.LayerNorm(n_embd) @@ -119,164 +91,115 @@ def forward(self, x): return x -class GPTLanguageModel(nn.Module): - def __init__(self): +class MiniQuadtrix(nn.Module): + + def __init__(self, vocab_size): super().__init__() - self.token_embedding_table = nn.Embedding(vocab_size, n_embd) - self.position_embedding_table = nn.Embedding(block_size, n_embd) + self.token_embedding_table = nn.Embedding(vocab_size, N_EMBD) + self.position_embedding_table = nn.Embedding(BLOCK_SIZE, N_EMBD) self.blocks = nn.Sequential( - *[Block(n_embd, n_head=n_head) for _ in range(n_layer)]) - self.ln_f = nn.LayerNorm(n_embd) - self.lm_head = nn.Linear(n_embd, vocab_size) + *[ + MiniQuadtrixBlock(N_EMBD, n_head=N_HEAD) + for _ in range(N_LAYER) + ] + ) + self.ln_f = nn.LayerNorm(N_EMBD) + self.lm_head = nn.Linear(N_EMBD, vocab_size) - def forward(self, idx, targets=None): + def forward(self, idx): B, T = idx.shape tok_emb = self.token_embedding_table(idx) - pos_emb = self.position_embedding_table(torch.arange(T, device=device)) + pos_emb = self.position_embedding_table( + torch.arange(T, device=idx.device) + ) x = tok_emb + pos_emb x = self.blocks(x) x = self.ln_f(x) logits = self.lm_head(x) + return logits - if targets is None: - loss = None - else: - B, T, C = logits.shape - logits = logits.view(B * T, C) - targets = targets.view(B * T) - loss = F.cross_entropy(logits, targets) - return logits, loss - - def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None): + def generate(self, idx, max_new_tokens): + # idx shape: (Batch_Size, Context_Length) for _ in range(max_new_tokens): - idx_cond = idx[:, -block_size:] - logits, _ = self(idx_cond) - logits = logits[:, -1, :] / max(temperature, 1e-6) - - if top_k is not None: - values, _ = torch.topk(logits, min(top_k, logits.size(-1))) - logits[logits < values[:, [-1]]] = float("-inf") - + idx_cond = idx[:, -BLOCK_SIZE:] + logits = self(idx_cond) + # Extract last token slice for all batches + logits = logits[:, -1, :] probs = F.softmax(logits, dim=-1) idx_next = torch.multinomial(probs, num_samples=1) idx = torch.cat((idx, idx_next), dim=1) - yield idx_next.item() - - -def default_checkpoint_path(): - script_dir = Path(__file__).resolve().parent - candidates = [ - script_dir / "best_model.pt", - Path.cwd() / "best_model.pt", - Path.cwd() / "engine" / "best_model.pt", - ] - for candidate in candidates: - if candidate.exists(): - return candidate - return script_dir / "best_model.pt" - - -def load_model(checkpoint_path): - checkpoint_path = Path(checkpoint_path) - if not checkpoint_path.exists(): - raise FileNotFoundError( - f"Checkpoint not found: {checkpoint_path}\n" - "Train first with engine/main.py, or pass --checkpoint path/to/best_model.pt" - ) - - model = GPTLanguageModel().to(device) - state_dict = torch.load(checkpoint_path, map_location=device) - model.load_state_dict(state_dict) - model.eval() - return model + return idx -def stream_response(model, prompt, max_new_tokens, temperature, top_k): - encoded_prompt = encode(prompt, tokenizer) - context = torch.tensor([encoded_prompt], dtype=torch.long, device=device) - - with torch.no_grad(): - for token_id in model.generate( - context, - max_new_tokens=max_new_tokens, - temperature=temperature, - top_k=top_k, - ): - word = decode([token_id], tokenizer) - yield word - +def load_tokenizer(encoding_name="o200k_base"): + tokenizer = tiktoken.get_encoding(encoding_name) + return tokenizer, tokenizer.n_vocab -def generate_response(model, prompt, max_new_tokens, temperature, top_k): - return "".join(stream_response(model, prompt, max_new_tokens, temperature, top_k)).strip() +@torch.inference_mode() +def main(): + print("=" * 50) + print("llm Inference Engine") + print(f"Target Device: {DEVICE.upper()}") + print("=" * 50) + + tokenizer, vocab_size = load_tokenizer("o200k_base") + if not os.path.exists(MODEL_WEIGHTS_PATH): + print( + f"Error: Weights file '{MODEL_WEIGHTS_PATH}' not found. Please train the model first." + ) + sys.exit(1) -def chat(model, args): - header("INFERENCE", "quit / exit / q -> end session") - blank() + model = MiniQuadtrix(vocab_size).to(DEVICE) + model.load_state_dict( + torch.load(MODEL_WEIGHTS_PATH, map_location=DEVICE, weights_only=True) + ) + model.eval() + try: + # Fuses operations and parallelizes execution sub-graphs natively + model = torch.compile(model) + print("Graph compilation successful (torch.compile applied).") + except Exception: + print("Proceeding without torch.compile graph optimizations.") + + print("\nEntering Chat Mode. Type 'exit' or 'quit' to stop.") + print( + "Parallel Generation Mode: Returns 3 alternative paths simultaneously.\n" + ) while True: - prompt = input(f" >> ").strip() - if prompt.lower() in ("quit", "exit", "q"): - blank() - print(" Session ended.") + try: + prompt = input("User > ").strip() + if prompt.lower() in ("quit", "exit", "q"): + print("\nGoodbye.") + break + if not prompt: + continue + + # Tokenize input + tokens = tokenizer.encode(prompt) + + # Leverage Batch Parallelism: Replicate the prompt tensor across dimension 0 + # This forces the GPU/CPU to execute 3 generations in parallel operations + num_parallel_samples = 3 + context = ( + torch.tensor([tokens], dtype=torch.long, device=DEVICE) + .repeat(num_parallel_samples, 1) + ) + + output_ids = model.generate(context, max_new_tokens=60) + + print( + f"\nModel Responses ({num_parallel_samples} Parallel Paths):") + for i in range(num_parallel_samples): + new_tokens = output_ids[i][len(tokens):].tolist() + response = tokenizer.decode(new_tokens).strip() + print(f" Path {i+1} -> {response}") + print("-" * 50) + + except KeyboardInterrupt: + print("\nSession interrupted.") break - if not prompt: - continue - - blank() - print(f" ", end="", flush=True) - for word in stream_response( - model, - prompt, - args.max_new_tokens, - args.temperature, - args.top_k, - ): - print(word, end="", flush=True) - blank() - blank() - - -def parse_args(): - parser = argparse.ArgumentParser( - description="Run inference from an engine trained .pt checkpoint.") - parser.add_argument( - "--checkpoint", - type=Path, - default=default_checkpoint_path(), - help="Path to the .pt file generated by engine/main.py.", - ) - parser.add_argument("--prompt", type=str, default=None, - help="Generate once from this prompt.") - parser.add_argument("--max-new-tokens", type=int, default=200) - parser.add_argument("--temperature", type=float, default=1.0) - parser.add_argument("--top-k", type=int, default=None) - return parser.parse_args() - - -def main(): - args = parse_args() - start = time.time() - blank() - rule() - - model = load_model(args.checkpoint) - - if args.prompt: - for word in stream_response( - model, - args.prompt, - args.max_new_tokens, - args.temperature, - args.top_k, - ): - print(word, end="", flush=True) - blank() - else: - chat(model, args) - - blank() - row("Total", f"{time.time() - start:.2f}s") if __name__ == "__main__": diff --git a/engine/llm.cpp/Makefile b/engine/llm.cpp/Makefile new file mode 100644 index 0000000..87d3ee2 --- /dev/null +++ b/engine/llm.cpp/Makefile @@ -0,0 +1,77 @@ +.PHONY: all build clean run dev gpu train bench logs ps shell help release debug benchmark-bin clean-native format + +SHELL := /bin/bash +SCRIPT := ./scripts/build.sh +CC := g++ +CFLAGS := -std=c++17 -O3 -march=native -fopenmp +IFLAGS := -I. -Iinclude +TARGET := llm +SRCS := llm.cpp +HDRS := $(wildcard include/*.h) + +all: $(TARGET) +$(TARGET): $(SRCS) $(HDRS) + $(CC) $(CFLAGS) $(IFLAGS) -o $@ $(SRCS) + @echo "Built $(TARGET) with OpenMP parallel support." + +release: $(SRCS) $(HDRS) + $(CC) $(CFLAGS) $(IFLAGS) -DNDEBUG -o $(TARGET) $(SRCS) + strip $(TARGET) +debug: $(SRCS) $(HDRS) + $(CC) -std=c++17 -O0 -g -fsanitize=address,undefined $(IFLAGS) -o $(TARGET)-debug $(SRCS) + +benchmark-bin: benchmark.cpp $(HDRS) + $(CC) $(CFLAGS) $(IFLAGS) -o llm-bench benchmark.cpp + +clean-native: + rm -f $(TARGET) $(TARGET)-debug llm-bench + +build: + $(SCRIPT) up + +run: build + @echo "Stack already started." + +dev: + $(SCRIPT) dev + +gpu: + $(SCRIPT) gpu + +train-cpp: + $(SCRIPT) train-cpp + +train-torch: + $(SCRIPT) train-torch + +bench: + $(SCRIPT) bench + +logs: + $(SCRIPT) logs + +ps: + $(SCRIPT) ps + +shell: + $(SCRIPT) shell $(SERVICE) + +clean: + $(SCRIPT) clean + +format: + find . \( -name "*.cpp" -o -name "*.h" \) \ + ! -path "./build/*" \ + | xargs clang-format -i --style=LLVM + +help: + @echo "" + @echo " llm.cpp — make targets" + @echo "" + @echo " Native:" + @echo " make Build C++ binary with OpenMP parallel support" + @echo " make release Stripped release binary" + @echo " make debug Debug binary with ASan/UBSan" + @echo " make clean-native Remove native build artifacts" + @echo " make format Run clang-format on all C++ files" + @echo "" \ No newline at end of file diff --git a/llm.cpp/include/attention.h b/engine/llm.cpp/include/attention.h similarity index 100% rename from llm.cpp/include/attention.h rename to engine/llm.cpp/include/attention.h diff --git a/llm.cpp/include/backward.h b/engine/llm.cpp/include/backward.h similarity index 90% rename from llm.cpp/include/backward.h rename to engine/llm.cpp/include/backward.h index 2528314..3076960 100644 --- a/llm.cpp/include/backward.h +++ b/engine/llm.cpp/include/backward.h @@ -1,24 +1,17 @@ -#pragma once -// ============================================================ // include/backward.h – Full analytical backpropagation // Each backward_* function: // - receives dOut (gradient of loss w.r.t. this layer's output) // - receives saved activations from the forward pass // - fills gradients into Grad structs (mirroring weight layout) // - returns dX (gradient of loss w.r.t. this layer's input) -// ============================================================ +#pragma once #include "config/config.h" #include "tensor.h" #include #include #include - -// ============================================================= -// Gradient stores (one per learnable weight tensour) -// ============================================================ - struct GradLinear { Tensor dW; // same shape as weight [in, out] @@ -155,12 +148,6 @@ struct Grads lm_head.zero(); } }; - -// ============================================================ -// Saved activations from the forward pass -// (we need these to compute gradients) -// ============================================================ - struct SavedHead { Tensor x; // input [B, T, n_embd] @@ -192,9 +179,9 @@ struct SavedFFN struct SavedLN { - Tensor x; // input [B, T, C] + Tensor x; // inpu [B, T, C] Tensor xhat; // normalized, before gamma/beta [B,T,C] - Tensor inv_std; // 1/sqrt(var+eps) per row stored as [B, T, 1] → flat + Tensor inv_std; // 1/sqrt(var+eps) per row stored as [B, T, 1] flat std::vector mu_vec, invstd_vec; // [B*T] each }; @@ -227,11 +214,9 @@ struct SavedForward std::vector targets; }; -// ============================================================ -// Primitive backward ops -// ============================================================ +// Primitive backward opss -// ---- cross-entropy backward --------------------------------- +// ---- cross-entropy backwadrd --------------------------------- // dlogits [B*T, V]: softmax(logits) - one_hot(targets) / BT inline Tensor backward_cross_entropy(const Tensor &logits2d, const std::vector &targets) { @@ -260,7 +245,7 @@ inline Tensor backward_cross_entropy(const Tensor &logits2d, const std::vector backward_bmm(const Tensor &dOut, // [B, T, T2] const Tensor &a, // [B, T, D] @@ -427,7 +412,7 @@ inline Tensor backward_softmax3d(const Tensor &dwei, // [B, T, T] return dpre; } -// ---- cat_last backward [B,T,D_total] → slice per head ------ +// ---- cat_last backward [B,T,D_total] slice per head ------ inline std::vector backward_cat_last(const Tensor &dConcat, const std::vector &head_sizes) { @@ -447,10 +432,6 @@ inline std::vector backward_cat_last(const Tensor &dConcat, return out; } -// ============================================================ -// Forward pass WITH activation saving -// ============================================================ - // ---- saved layernorm forward -------------------------------- inline Tensor forward_ln_save(const Tensor &x, const Tensor &gamma, @@ -621,10 +602,8 @@ inline Tensor forward_ffn_save(const Tensor &x, return out; } -// ============================================================ // Full model forward with all activations saved -// ============================================================ -#include "quadtrix.h" // for GPTLanguageModel layout +#include "lm.h" inline SavedForward forward_save(GPTLanguageModel &model, const std::vector &idx, @@ -640,8 +619,6 @@ inline SavedForward forward_save(GPTLanguageModel &model, s.targets = targets; int C = model.n_embd; int V = model.vocab_size; - - // ── Embeddings ─────────────────────────────────────────── s.tok_out = model.token_emb.forward(idx, B, T); s.pos_out = model.pos_emb.forward_pos(T); s.emb_sum = Tensor({B, T, C}); @@ -650,7 +627,7 @@ inline SavedForward forward_save(GPTLanguageModel &model, for (int d = 0; d < C; ++d) s.emb_sum.at(b, t, d) = s.tok_out.at(b, t, d) + s.pos_out.at(0, t, d); - // ── Transformer blocks ─────────────────────────────────── + // Transformer s.blocks.resize(model.n_layer); Tensor x = s.emb_sum; for (int l = 0; l < model.n_layer; ++l) @@ -700,12 +677,12 @@ inline SavedForward forward_save(GPTLanguageModel &model, x = add(sb.x_after_mha, ffn); } - // ── Final LN + lm_head ─────────────────────────────────── + // Final LN + lm_head s.lm_in = forward_ln_save(x, model.ln_f.gamma, model.ln_f.beta, s.ln_f); s.logits3d = matmul(s.lm_in, model.lm_head.weight); s.logits3d = add_bias(s.logits3d, model.lm_head.bias); - // reshape [B,T,V] → [B*T, V] + // reshape [B,T,V] [B*T, V] s.logits2d = Tensor({B * T, V}); for (int i = 0; i < B * T; ++i) for (int v = 0; v < V; ++v) @@ -714,9 +691,7 @@ inline SavedForward forward_save(GPTLanguageModel &model, return s; } -// ============================================================ // Full backward pass -// ============================================================ inline Grads backward(GPTLanguageModel &model, const SavedForward &s) { @@ -725,8 +700,6 @@ inline Grads backward(GPTLanguageModel &model, const SavedForward &s) int n_head = model.n_head, hs = C / n_head; Grads g(V, C, n_head, model.n_layer, model.block_size); - - // ── dLoss / dLogits [B*T, V] ──────────────────────────── Tensor dlogits2d = backward_cross_entropy(s.logits2d, s.targets); // reshape to [B, T, V] @@ -735,28 +708,23 @@ inline Grads backward(GPTLanguageModel &model, const SavedForward &s) for (int v = 0; v < V; ++v) dlogits3d.data[i * V + v] = dlogits2d.at(i, v); - // ── lm_head backward [B,T,V] → [B,T,C] ────────────────── Tensor dx = backward_linear(dlogits3d, s.lm_in, model.lm_head.weight, g.lm_head); - - // ── final layernorm backward ────────────────────────────── dx = backward_layernorm(dx, s.ln_f, model.ln_f.gamma, g.ln_f); - - // ── transformer blocks (reverse order) ─────────────────── for (int l = model.n_layer - 1; l >= 0; --l) { auto &blk = model.blocks[l]; auto &sb = s.blocks[l]; auto &gb = g.blocks[l]; - // ---- FFN residual: dx is d(x_after_mha + ffn_out) ── + // ---- FFN residual: dx is d(x_after_mha + ffn_out) - // dffn = dx (residual, pass-through) // dx += dx (will be added after LN2 backward below) Tensor dffn_out = dx; // gradient to the ffn output branch // residual adds straight through: // d(x_after_mha) gets dx directly (accumulated below) - // ---- LN2 + FFN backward ───────────────────────────── - // dffn_out → (dropout bwd) → fc2 bwd → relu bwd → fc1 bwd → dx_ln2 + // LN2 + FFN backward + // dffn_out (dropout bwd) fc2 bwd relu bwd fc1 bwd dx_ln2 Tensor dffn = dffn_out; if (sb.ffn.used_dropout) dffn = backward_dropout(dffn, sb.ffn.dropout_mask, DROPOUT); @@ -768,21 +736,21 @@ inline Grads backward(GPTLanguageModel &model, const SavedForward &s) // fc1 backward Tensor dx_ln2 = backward_linear(dh_pre, sb.ffn.x, blk.ffwd.fc1.weight, gb.ffwd.dfc1); - // LN2 backward → d(x_after_mha) from FFN branch + // LN2 backward d(x_after_mha) from FFN branch Tensor dx_after_mha_ffn = backward_layernorm(dx_ln2, sb.ln2, blk.ln2.gamma, gb.ln2); // total d(x_after_mha) = residual-pass + LN2 branch Tensor dx_after_mha = add(dx, dx_after_mha_ffn); - // ---- MHA residual: x_after_mha = x_in + attn_out ─── + // ---- MHA residual: x_after_mha = x_in + attn_out Tensor dattn_out = dx_after_mha; // gradient to attn branch // d(x_in) from this residual = dx_after_mha (passed through) - // ---- MHA backward ──────────────────────────────────── + // ---- MHA backward Tensor dmha = dattn_out; if (sb.mha.used_dropout) dmha = backward_dropout(dmha, sb.mha.dropout_mask, DROPOUT); - // proj backward [B,T,n_embd] → [B,T,n_head*hs] + // proj backward [B,T,n_embd] [B,T,n_head*hs] Tensor dconcat = backward_linear(dmha, sb.mha.concat, blk.sa.proj.weight, gb.sa.proj); // split concat grad back to each head @@ -819,18 +787,18 @@ inline Grads backward(GPTLanguageModel &model, const SavedForward &s) : sh.wei; // d(out_h) = dhead_outs[h] - // out_h = wei_used @ v → backward of bmm - // da = dOut @ b^T → d_wei_used = dout_h @ v^T [B,T,T] - // db = a^T @ dOut → dv = wei_used^T @ dout_h [B,T,hs] + // out_h = wei_used @ v backward of bmm + // da = dOut @ b^T d_wei_used = dout_h @ v^T [B,T,T] + // db = a^T @ dOut dv = wei_used^T @ dout_h [B,T,hs] Tensor vT = transpose23(sh.v); // [B, hs, T] // d_wei_drop [B,T,T] = dout_h @ vT (but vT is [B,hs,T], need [B,T,hs]@... ) // Use bmm with v transposed - // dout_h [B,T,hs], v [B,T,hs] → vT [B,hs,T] + // dout_h [B,T,hs], v [B,T,hs] vT [B,hs,T] // d_wei_drop = bmm(dout_h, vT) = [B,T,hs]@[B,hs,T] = [B,T,T] Tensor d_wei_drop = bmm(dout_h, vT); // [B,T,T] // dv = wei_used^T @ dout_h = [B,T,T]^T @ [B,T,hs] = [B,T,hs] - Tensor wei_usedT = transpose23(wei_used); // [B, T, T] transposed → [B, T, T] + Tensor wei_usedT = transpose23(wei_used); // [B, T, T] transposed [B, T, T] // bmm needs [B,T,T] x [B,T,hs]: wei_usedT [B,T,T] x dout_h [B,T,hs] // but bmm signature is [B,T,D]x[B,D,T2] // wei_usedT as [B,T,T] x dout_h [B,T,hs]: first transpose wei_used to get [B,T,T] @@ -838,7 +806,7 @@ inline Grads backward(GPTLanguageModel &model, const SavedForward &s) // back Tensor dv = bmm(transpose23(sh.wei), dout_h); // [B,T,hs] - // attention dropout backward on d_wei_drop → d_wei + // attention dropout backward on d_wei_drop d_wei Tensor d_wei = d_wei_drop; if (sh.used_dropout) { @@ -866,13 +834,13 @@ inline Grads backward(GPTLanguageModel &model, const SavedForward &s) // dq = d_wei_pre @ k [B,T,T]@[B,T,hs]... need k [B,hs,T]^T = k // actual: d_wei_pre[b,i,j] = sum over... it's q[b,i,:] · k[b,j,:] // dq[b,i,d] = sum_j d_wei_pre[b,i,j] * k[b,j,d] - // = bmm(d_wei_pre, k) where k is [B,T,hs] → need [B,T,hs] directly + // = bmm(d_wei_pre, k) where k is [B,T,hs] need [B,T,hs] directly // = bmm with b=[B,hs,T]? No: bmm([B,T,T], [B,T,hs]) needs second arg [B,T,T] // Use: dq = d_wei_pre @ k with k as [B, hs, T]... no. - // dq[b,i,d] = sum_j d_pre[b,i,j] * k[b,j,d] → this IS bmm(d_pre, k) if + // dq[b,i,d] = sum_j d_pre[b,i,j] * k[b,j,d] this IS bmm(d_pre, k) if // k were [B,T,hs]... but bmm expects [B,D,T2]. So treat as matmul style: // bmm(d_pre [B,T,T], k [B,T,hs]) — but bmm signature needs [B,D,T2]: - // interpret as B batches, each [T,T] @ [T,hs] = [T,hs]: b's D=T, T2=hs → valid! + // interpret as B batches, each [T,T] @ [T,hs] = [T,hs]: b's D=T, T2=hs valid! Tensor dq = bmm(d_wei_pre, sh.k); // [B,T,hs] // dk[b,j,d] = sum_i d_pre[b,i,j] * q[b,i,d] @@ -898,7 +866,7 @@ inline Grads backward(GPTLanguageModel &model, const SavedForward &s) } } - // LN1 backward → d(x_in) + // LN1 backward d(x_in) Tensor dx_in_mha = backward_layernorm(dx_ln1, sb.ln1, blk.ln1.gamma, gb.ln1); // total dx for this block: residual pass-through + LN1/MHA branch + LN2/FFN branch @@ -907,7 +875,7 @@ inline Grads backward(GPTLanguageModel &model, const SavedForward &s) dx = add(dx_after_mha, dx_in_mha); } - // ── Embedding backward ─────────────────────────────────── + // Embedding backward // dx is now d(emb_sum) = d(tok_emb + pos_emb) // pos_emb grad: sum over batch for (int b = 0; b < B; ++b) @@ -927,9 +895,7 @@ inline Grads backward(GPTLanguageModel &model, const SavedForward &s) return g; } -// ============================================================ -// Apply Grads → model weights (AdamW update) -// ============================================================ +// AdamW update struct AdamWState { diff --git a/engine/llm.cpp/include/bpe.h b/engine/llm.cpp/include/bpe.h new file mode 100644 index 0000000..c96b0b5 --- /dev/null +++ b/engine/llm.cpp/include/bpe.h @@ -0,0 +1,289 @@ +// @Eamon2009 +#pragma once +#include "config/config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct DataLoader +{ + std::vector vocab; + std::map token_to_id; + std::vector> merges; + std::map, int> merge_rank; + int base_vocab_size{0}; + int vocab_size{0}; + + std::vector train_data; + std::vector val_data; + + struct BPEIndex + { + int id; + int prev; + int next; + }; + + struct PairFreq + { + long long key; + int count; + bool operator<(const PairFreq &o) const + { + return count < o.count; + } + }; + + void load(const std::string &path, + int target_vocab = BPE_VOCAB_SIZE, + double train_split = TRAIN_SPLIT) + { + std::ifstream f(path); + if (!f.is_open()) + throw std::runtime_error("[DataLoader] Cannot open file: " + path); + + std::ostringstream ss; + ss << f.rdbuf(); + std::string text = ss.str(); + if (text.empty()) + throw std::runtime_error("[DataLoader] File is empty: " + path); + + std::cout << "[BPE] Text length: " << text.size() << " characters\n"; + std::cout << "[BPE] Target vocab size: " << target_vocab << "\n"; + std::cout.flush(); + + std::vector data = train_bpe(text, target_vocab); + + int n = (int)(train_split * (double)data.size()); + train_data = std::vector(data.begin(), data.begin() + n); + val_data = std::vector(data.begin() + n, data.end()); + + if ((int)train_data.size() <= BLOCK_SIZE || (int)val_data.size() <= BLOCK_SIZE) + throw std::runtime_error("[DataLoader] Dataset too small for BLOCK_SIZE=" + + std::to_string(BLOCK_SIZE)); + + std::cout << "[DATA] Total tokens : " << data.size() << "\n"; + std::cout << "[DATA] Train tokens : " << train_data.size() << "\n"; + std::cout << "[DATA] Val tokens : " << val_data.size() << "\n"; + } + + std::vector encode(const std::string &text) const + { + return apply_merges(base_encode(text)); + } + std::string decode(const std::vector &ids) const + { + std::string out; + for (int id : ids) + if (id >= 0 && id < (int)vocab.size()) + out += vocab[id]; + return out; + } + + std::pair, std::vector> + get_batch(const std::string &split, int batch_size, int block_size, std::mt19937 &rng) const + { + const std::vector &d = (split == "train") ? train_data : val_data; + std::uniform_int_distribution dist(0, (int)d.size() - block_size - 1); + std::vector x(batch_size * block_size); + std::vector y(batch_size * block_size); + for (int b = 0; b < batch_size; ++b) + { + int start = dist(rng); + for (int t = 0; t < block_size; ++t) + { + x[b * block_size + t] = d[start + t]; + y[b * block_size + t] = d[start + t + 1]; + } + } + return {x, y}; + } + + private: + std::vector base_encode(const std::string &text) const + { + std::vector ids; + ids.reserve(text.size()); + for (char c : text) + { + auto it = token_to_id.find(std::string(1, c)); + if (it != token_to_id.end()) + ids.push_back(it->second); + } + return ids; + } + + std::vector apply_merges(std::vector ids) const + { + for (int rank = 0; rank < (int)merges.size(); ++rank) + { + int left = merges[rank].first; + int right = merges[rank].second; + int new_id = base_vocab_size + rank; + std::vector out; + out.reserve(ids.size()); + int i = 0; + while (i < (int)ids.size()) + { + if (i + 1 < (int)ids.size() && ids[i] == left && ids[i + 1] == right) + { + out.push_back(new_id); + i += 2; + } + else + { + out.push_back(ids[i]); + ++i; + } + } + ids = std::move(out); + } + return ids; + } + + std::vector train_bpe(const std::string &text, int target_vocab) + { + std::set chars(text.begin(), text.end()); + for (char c : chars) + { + std::string s(1, c); + token_to_id[s] = (int)vocab.size(); + vocab.push_back(s); + } + base_vocab_size = (int)vocab.size(); + if (target_vocab <= base_vocab_size) + { + vocab_size = base_vocab_size; + return base_encode(text); + } + + std::vector initial_ids = base_encode(text); + int n = initial_ids.size(); + + std::vector list(n); + for (int i = 0; i < n; ++i) + list[i] = {initial_ids[i], i - 1, i + 1}; + list[n - 1].next = -1; + + // Direct mapping tracking instead of nested maps + // Uses flat vectors to keep data compact in RAM for older CPUs + std::map pair_counts; + auto get_key = [](int left, int right) -> long long + { return ((long long)left << 32) | (unsigned int)right; }; + + for (int i = 0; i < n - 1; ++i) + { + pair_counts[get_key(list[i].id, list[i + 1].id)]++; + } + + int num_merges = target_vocab - base_vocab_size; + merges.reserve(num_merges); + std::cout << "[BPE] Running " << num_merges << " merges...\n"; + std::cout.flush(); + + for (int step = 0; step < num_merges; ++step) + { + long long best_key = 0; + int max_count = 0; + + // Old CPUs read flat contiguous structures incredibly fast + for (auto const &[key, count] : pair_counts) + { + if (count > max_count) + { + max_count = count; + best_key = key; + } + } + + if (max_count == 0) + break; + + int left = (int)(best_key >> 32); + int right = (int)(best_key & 0xFFFFFFFFLL); + + int new_id = (int)vocab.size(); + vocab.push_back(vocab[left] + vocab[right]); + token_to_id[vocab.back()] = new_id; + merges.push_back({left, right}); + merge_rank[{left, right}] = step; + + // Clear out targeted count map entry + pair_counts.erase(best_key); + + // Single pass linked list pointer update + int curr = 0; + while (curr != -1 && list[curr].next != -1) + { + int next_node = list[curr].next; + if (list[curr].id == left && list[next_node].id == right) + { + int prev_node = list[curr].prev; + int next_next_node = list[next_node].next; + + // Decrement counts for pairs broken by this merge + if (prev_node != -1) + pair_counts[get_key(list[prev_node].id, list[curr].id)]--; + if (next_next_node != -1) + pair_counts[get_key(list[next_node].id, + list[next_next_node].id)]--; + + // Mutate list nodes + list[curr].id = new_id; + list[curr].next = next_next_node; + if (next_next_node != -1) + list[next_next_node].prev = curr; + + // Increment counts for newly created pairs + if (prev_node != -1) + pair_counts[get_key(list[prev_node].id, list[curr].id)]++; + if (next_next_node != -1) + pair_counts[get_key(list[curr].id, list[next_next_node].id)]++; + + curr = next_next_node; + } + else + { + curr = next_node; + } + } + + // Clean out zero records to prevent lookups from bloating memory + for (auto it = pair_counts.begin(); it != pair_counts.end();) + { + if (it->second <= 0) + it = pair_counts.erase(it); + else + ++it; + } + + if ((step + 1) % 100 == 0 || step + 1 == num_merges) + { + std::cout << "[BPE] step " << (step + 1) << "/" << num_merges + << " vocab=" << (int)vocab.size() << "\n"; + std::cout.flush(); + } + } + + std::vector final_ids; + int curr = 0; + while (curr != -1) + { + final_ids.push_back(list[curr].id); + curr = list[curr].next; + } + + vocab_size = (int)vocab.size(); + std::cout << "[BPE] Done. Final vocab size: " << vocab_size << "\n"; + return final_ids; + } +}; \ No newline at end of file diff --git a/engine/llm.cpp/include/cuda_kernels.cuh b/engine/llm.cpp/include/cuda_kernels.cuh new file mode 100644 index 0000000..b8248c5 --- /dev/null +++ b/engine/llm.cpp/include/cuda_kernels.cuh @@ -0,0 +1,123 @@ +#ifndef CUDA_KERNELS_CUH +#define CUDA_KERNELS_CUH + +#include +#include + +// 1. Element-wise Bias Addition Kernel +__global__ void k_add_bias(float *out, const float *bias, int N, int E) +{ + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < N * E) + { + int e = idx % E; + out[idx] += bias[e]; + } +} + +// 2. Parallel ReLU Kernel +__global__ void k_relu(const float *in, float *out, int N) +{ + int idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx < N) + { + out[idx] = fmaxf(0.0f, in[idx]); + } +} + +// 3. Parallel Layer Normalization Kernel +__global__ void k_layernorm_forward(const float *x, + const float *gamma, + const float *beta, + float *out, + int C, + float eps) +{ + int row = blockIdx.x; // Each CUDA block handles one row (Batch * Sequence) + const float *row_x = x + row * C; + float *row_out = out + row * C; + + __shared__ float s_mean; + __shared__ float s_var; + + if (threadIdx.x == 0) + { + s_mean = 0.0f; + s_var = 0.0f; + } + __syncthreads(); + + // Sum mean + float local_sum = 0.0f; + for (int c = threadIdx.x; c < C; c += blockDim.x) + { + local_sum += row_x[c]; + } + atomicAdd(&s_mean, local_sum / C); + __syncthreads(); + + // Sum variance + float local_var = 0.0f; + for (int c = threadIdx.x; c < C; c += blockDim.x) + { + float diff = row_x[c] - s_mean; + local_var += diff * diff; + } + atomicAdd(&s_var, local_var / C); + __syncthreads(); + + // Normalize and scale + float inv_std = rsqrtf(s_var + eps); + for (int c = threadIdx.x; c < C; c += blockDim.x) + { + row_out[c] = (row_x[c] - s_mean) * inv_std * gamma[c] + beta[c]; + } +} + +// 4. Parallel Softmax Kernel +__global__ void k_softmax_forward(const float *in, float *out, int C) +{ + int row = blockIdx.x; + const float *row_in = in + row * C; + float *row_out = out + row * C; + + __shared__ float s_max; + __shared__ float s_sum; + + if (threadIdx.x == 0) + { + s_max = -1e30f; + s_sum = 0.0f; + } + __syncthreads(); + + // Find max value + float local_max = -1e30f; + for (int c = threadIdx.x; c < C; c += blockDim.x) + { + local_max = fmaxf(local_max, row_in[c]); + } + // Atomic max via integer conversion + atomicMax((int *)&s_max, __float_as_int(local_max)); + __syncthreads(); + + // Exponentiate and sum + float local_sum = 0.0f; + for (int c = threadIdx.x; c < C; c += blockDim.x) + { + float e = expf(row_in[c] - s_max); + row_out[c] = e; + local_sum += e; + } + atomicAdd(&s_sum, local_sum); + __syncthreads(); + + // Normalize + float inv_sum = 1.0f / s_sum; + for (int c = threadIdx.x; c < C; c += blockDim.x) + { + row_out[c] *= inv_sum; + } +} + +#endif \ No newline at end of file diff --git a/engine/llm.cpp/include/cuda_utils.cuh b/engine/llm.cpp/include/cuda_utils.cuh new file mode 100644 index 0000000..e1eff68 --- /dev/null +++ b/engine/llm.cpp/include/cuda_utils.cuh @@ -0,0 +1,32 @@ +#ifndef CUDA_UTILS_CUH +#define CUDA_UTILS_CUH + +#include +#include +#include + +#define CUDA_CHECK(call) \ + do \ + { \ + cudaError_t err = call; \ + if (err != cudaSuccess) \ + { \ + std::cerr << "CUDA Error: " << cudaGetErrorString(err) << " at " << __FILE__ \ + << ":" << __LINE__ << std::endl; \ + exit(EXIT_FAILURE); \ + } \ + } while (0) + +#define CUBLAS_CHECK(call) \ + do \ + { \ + cublasStatus_t stat = call; \ + if (stat != CUBLAS_STATUS_SUCCESS) \ + { \ + std::cerr << "cuBLAS Error Code " << stat << " at " << __FILE__ << ":" \ + << __LINE__ << std::endl; \ + exit(EXIT_FAILURE); \ + } \ + } while (0) + +#endif \ No newline at end of file diff --git a/engine/llm.cpp/include/layer.cuh b/engine/llm.cpp/include/layer.cuh new file mode 100644 index 0000000..83f8145 --- /dev/null +++ b/engine/llm.cpp/include/layer.cuh @@ -0,0 +1,74 @@ +#ifndef CUDA_LAYERS_CUH +#define CUDA_LAYERS_CUH + +#include "cuda_kernels.cuh" +#include "cuda_tensor.cuh" + +// Fast Matrix Multiplication via cuBLAS GEMM +inline void +gpu_matmul(cublasHandle_t handle, CUDATensor &A, CUDATensor &W, CUDATensor &C, int M, int N, int K) +{ + A.to_device(); + W.to_device(); + C.allocate_device(); + + float alpha = 1.0f; + float beta = 0.0f; + + // C (M x N) = A (M x K) * W (K x N) + CUBLAS_CHECK(cublasSgemm(handle, + CUBLAS_OP_N, + CUBLAS_OP_N, + N, + M, + K, + &alpha, + W.d_data, + N, + A.d_data, + K, + &beta, + C.d_data, + N)); +} + +// Forward LayerNorm Wrapper +inline void gpu_layernorm(CUDATensor &X, CUDATensor &Gamma, CUDATensor &Beta, CUDATensor &Out) +{ + X.to_device(); + Gamma.to_device(); + Beta.to_device(); + Out.allocate_device(); + + int total_rows = X.shape[0] * X.shape[1]; + int C = X.shape[2]; + + int threads = 256; + int blocks = total_rows; + + k_layernorm_forward<<>>(X.d_data, + Gamma.d_data, + Beta.d_data, + Out.d_data, + C, + 1e-5f); + CUDA_CHECK(cudaGetLastError()); +} + +// Forward Softmax Wrapper +inline void gpu_softmax(CUDATensor &In, CUDATensor &Out) +{ + In.to_device(); + Out.allocate_device(); + + int total_rows = In.numel() / In.shape.back(); + int C = In.shape.back(); + + int threads = 256; + int blocks = total_rows; + + k_softmax_forward<<>>(In.d_data, Out.d_data, C); + CUDA_CHECK(cudaGetLastError()); +} + +#endif \ No newline at end of file diff --git a/engine/llm.cpp/include/lm.h b/engine/llm.cpp/include/lm.h new file mode 100644 index 0000000..06113d5 --- /dev/null +++ b/engine/llm.cpp/include/lm.h @@ -0,0 +1,250 @@ +#pragma once + +#include "block.h" +#include "config/config.h" +#include "embedding.h" +#include "layernorm.h" +#include "linear.h" +#include "sampler.h" +#include "tensor.h" + +#include +#include +#include +#include +#include + +// Cross-entropy loss for language modelling. +// logits has shape BT by vocab_size (flat row-major). +// targets is a flat list of next-token indices of length BT. +inline float cross_entropy(const Tensor &logits, const std::vector &targets) +{ + int BT = logits.shape[0]; + int V = logits.shape[1]; + float loss = 0.0f; + for (int i = 0; i < BT; ++i) + { + float maxv = -1e30f; + for (int v = 0; v < V; ++v) + maxv = std::max(maxv, logits.at(i, v)); + float sumexp = 0.0f; + for (int v = 0; v < V; ++v) + sumexp += std::exp(logits.at(i, v) - maxv); + float log_prob = logits.at(i, targets[i]) - maxv - std::log(sumexp); + loss -= log_prob; + } + return loss / (float)BT; +} + +// AdamW optimiser: first and second moment estimates with weight decay. +struct AdamW +{ + float lr, beta1, beta2, eps, weight_decay; + int step_count; + std::vector params; + std::vector sizes; + std::vector> m, v; + + AdamW(float lr_ = 3e-4f, + float beta1_ = 0.9f, + float beta2_ = 0.999f, + float eps_ = 1e-8f, + float wd = 0.0f) + : lr(lr_), beta1(beta1_), beta2(beta2_), eps(eps_), weight_decay(wd), step_count(0) + { + } + + void add_param(std::vector &p) + { + params.push_back(p.data()); + sizes.push_back((int)p.size()); + m.emplace_back(p.size(), 0.0f); + v.emplace_back(p.size(), 0.0f); + } + + // grads must be passed in the same order params were added + void step(std::vector> &grads) + { + ++step_count; + float bc1 = 1.0f - std::pow(beta1, step_count); + float bc2 = 1.0f - std::pow(beta2, step_count); + for (int pi = 0; pi < (int)params.size(); ++pi) + { + for (int i = 0; i < sizes[pi]; ++i) + { + float g = grads[pi][i]; + m[pi][i] = beta1 * m[pi][i] + (1.0f - beta1) * g; + v[pi][i] = beta2 * v[pi][i] + (1.0f - beta2) * g * g; + float mhat = m[pi][i] / bc1; + float vhat = v[pi][i] / bc2; + params[pi][i] -= lr * mhat / (std::sqrt(vhat) + eps); + if (weight_decay > 0.0f) + params[pi][i] -= lr * weight_decay * params[pi][i]; + } + } + } +}; + +// Full GPT language model: token and position embeddings, +// N transformer blocks, final layer norm, and an output projection. +struct GPTLanguageModel +{ + std::mt19937 rng; + int vocab_size, n_embd, n_head, n_layer, block_size; + + Embedding token_emb; + Embedding pos_emb; + std::vector blocks; + LayerNorm ln_f; + Linear lm_head; + + GPTLanguageModel(int vocab, int embd, int heads, int layers, int blk_sz, unsigned int seed) + : vocab_size(vocab), n_embd(embd), n_head(heads), n_layer(layers), block_size(blk_sz), + rng(seed), token_emb(vocab, embd, rng), pos_emb(blk_sz, embd, rng), ln_f(embd), + lm_head(embd, vocab, true, rng) + { + for (int i = 0; i < layers; ++i) + blocks.emplace_back(embd, heads, rng); + } + + int num_params() const + { + int n = token_emb.num_params() + pos_emb.num_params() + ln_f.num_params() + + lm_head.num_params(); + for (auto &b : blocks) + n += b.num_params(); + return n; + } + + // Run the full forward pass. + // idx : flat list of B times T token indices + // B, T : batch size and sequence length + // targets : flat list of B times T next-token indices (empty means inference only) + // training: enables dropout when true + // returns : logits of shape BT by vocab, and scalar loss (0 when no targets) + std::pair forward(const std::vector &idx, + int B, + int T, + const std::vector &targets, + bool training) + { + Tensor tok = token_emb.forward(idx, B, T); + Tensor pos = pos_emb.forward_pos(T); + + Tensor x({B, T, n_embd}); + for (int b = 0; b < B; ++b) + for (int t = 0; t < T; ++t) + for (int d = 0; d < n_embd; ++d) + x.at(b, t, d) = tok.at(b, t, d) + pos.at(0, t, d); + + for (auto &blk : blocks) + x = blk.forward(x, training, rng); + + x = ln_f.forward(x); + + Tensor logits3d = lm_head.forward(x); + + Tensor logits({B * T, vocab_size}); + for (int i = 0; i < B * T; ++i) + for (int v = 0; v < vocab_size; ++v) + logits.at(i, v) = logits3d.data[i * vocab_size + v]; + + float loss = 0.0f; + if (!targets.empty()) + loss = cross_entropy(logits, targets); + + return {logits, loss}; + } + + // Autoregressively sample max_new_tokens tokens from context. + // Repetition penalty from params is applied to raw logits before softmax + // so that tokens seen recently in the window are less likely to repeat. + // params defaults to config.h values when not provided. + std::vector generate(std::vector context, + int max_new_tokens, + const SamplerParams ¶ms = SamplerParams()) + { + std::uniform_real_distribution udist(0.0f, 1.0f); + + for (int step = 0; step < max_new_tokens; ++step) + { + // crop context to model block size + int T = std::min((int)context.size(), block_size); + std::vector ctx(context.end() - T, context.end()); + + Tensor logits = forward(ctx, 1, T, std::vector(), false).first; + + // pull out the last time step logits only + int offset = (T - 1) * vocab_size; + std::vector last(vocab_size); + for (int v = 0; v < vocab_size; ++v) + last[v] = logits.data[offset + v]; + + // lower the score of tokens seen recently + apply_rep_penalty(last, context, params); + + // softmax to get a probability distribution + float maxv = *std::max_element(last.begin(), last.end()); + float sumv = 0.0f; + for (auto &lv : last) + { + lv = std::exp(lv - maxv); + sumv += lv; + } + for (auto &lv : last) + lv /= sumv; + + // multinomial sample from the distribution + float r = udist(rng); + float cumsum = 0.0f; + int next_tok = vocab_size - 1; + for (int v = 0; v < vocab_size; ++v) + { + cumsum += last[v]; + if (r < cumsum) + { + next_tok = v; + break; + } + } + context.push_back(next_tok); + } + return context; + } + + // Write all weights to a binary file in layer order. + void save(const std::string &path) const + { + std::ofstream f(path, std::ios::binary); + if (!f) + { + std::cerr << "[ERROR] Cannot open " << path << " for writing\n"; + return; + } + token_emb.save(f); + pos_emb.save(f); + for (auto &b : blocks) + b.save(f); + ln_f.save(f); + lm_head.save(f); + std::cout << "[SAVE] Weights written to " << path << "\n"; + } + + // Read all weights from a binary file in the same layer order as save. + void load(const std::string &path) + { + std::ifstream f(path, std::ios::binary); + if (!f) + { + std::cerr << "[ERROR] Cannot open " << path << " for reading\n"; + return; + } + token_emb.load(f); + pos_emb.load(f); + for (auto &b : blocks) + b.load(f); + ln_f.load(f); + lm_head.load(f); + std::cout << "[LOAD] Weights loaded from " << path << "\n"; + } +}; \ No newline at end of file diff --git a/llm.cpp/include/gpt.h b/engine/llm.cpp/include/model.h similarity index 83% rename from llm.cpp/include/gpt.h rename to engine/llm.cpp/include/model.h index 33b0991..830287c 100644 --- a/llm.cpp/include/gpt.h +++ b/engine/llm.cpp/include/model.h @@ -1,20 +1,19 @@ -#pragma once -// ============================================================ // include/gpt.h – GPT Language Model // Mirrors: class GPTLanguageModel in Python -// ============================================================ -#include "tensor.h" -#include "embedding.h" +#pragma once #include "block.h" +#include "config/config.h" +#include "embedding.h" #include "layernorm.h" #include "linear.h" -#include "config/config.h" -#include -#include +#include "tensor.h" + #include +#include #include #include +#include // ------------------------------------------------------------------ // Cross-entropy loss for language modelling @@ -41,9 +40,7 @@ inline float cross_entropy(const Tensor &logits, const std::vector &targets return loss / (float)BT; } -// ------------------------------------------------------------------ // AdamW optimiser (simple, stateful) -// ------------------------------------------------------------------ struct AdamW { float lr, beta1, beta2, eps, weight_decay; @@ -53,10 +50,13 @@ struct AdamW std::vector> m, v; // first/second moment AdamW(float lr_ = 3e-4f, - float beta1_ = 0.9f, float beta2_ = 0.999f, - float eps_ = 1e-8f, float wd = 0.0f) - : lr(lr_), beta1(beta1_), beta2(beta2_), - eps(eps_), weight_decay(wd), step_count(0) {} + float beta1_ = 0.9f, + float beta2_ = 0.999f, + float eps_ = 1e-8f, + float wd = 0.0f) + : lr(lr_), beta1(beta1_), beta2(beta2_), eps(eps_), weight_decay(wd), step_count(0) + { + } void add_param(std::vector &p) { @@ -89,7 +89,6 @@ struct AdamW } }; -// ------------------------------------------------------------------ // GPTLanguageModel // ------------------------------------------------------------------ struct GPTLanguageModel @@ -103,15 +102,10 @@ struct GPTLanguageModel LayerNorm ln_f; Linear lm_head; // [n_embd, vocab_size] - GPTLanguageModel(int vocab, int embd, int heads, int layers, - int blk_sz, unsigned int seed) - : vocab_size(vocab), n_embd(embd), n_head(heads), - n_layer(layers), block_size(blk_sz), - rng(seed), - token_emb(vocab, embd, rng), - pos_emb(blk_sz, embd, rng), - ln_f(embd), - lm_head(embd, vocab, true, rng) + GPTLanguageModel(int vocab, int embd, int heads, int layers, int blk_sz, unsigned int seed) + : vocab_size(vocab), n_embd(embd), n_head(heads), n_layer(layers), block_size(blk_sz), + rng(seed), token_emb(vocab, embd, rng), pos_emb(blk_sz, embd, rng), ln_f(embd), + lm_head(embd, vocab, true, rng) { for (int i = 0; i < layers; ++i) blocks.emplace_back(embd, heads, rng); @@ -119,7 +113,8 @@ struct GPTLanguageModel int num_params() const { - int n = token_emb.num_params() + pos_emb.num_params() + ln_f.num_params() + lm_head.num_params(); + int n = token_emb.num_params() + pos_emb.num_params() + ln_f.num_params() + + lm_head.num_params(); for (auto &b : blocks) n += b.num_params(); return n; @@ -131,9 +126,9 @@ struct GPTLanguageModel // B, T : batch and sequence length // targets: flat [B*T] next-token indices (empty = inference) // returns: (logits [B*T, vocab], loss) loss=0 when no targets - // ---------------------------------------------------------------- std::pair forward(const std::vector &idx, - int B, int T, + int B, + int T, const std::vector &targets, bool training) { @@ -155,7 +150,7 @@ struct GPTLanguageModel // final layer norm x = ln_f.forward(x); - // lm_head → logits [B, T, vocab] + // lm_head logits [B, T, vocab] Tensor logits3d = lm_head.forward(x); // [B, T, vocab] // reshape to [B*T, vocab] @@ -171,9 +166,6 @@ struct GPTLanguageModel return {logits, loss}; } - // ---------------------------------------------------------------- - // generate (greedy autoregressive sampling with temperature=1) - // ---------------------------------------------------------------- std::vector generate(std::vector context, int max_new_tokens) { std::uniform_real_distribution udist(0.0f, 1.0f); @@ -184,7 +176,8 @@ struct GPTLanguageModel int T = std::min((int)context.size(), block_size); std::vector ctx(context.end() - T, context.end()); - std::pair forward_result = forward(ctx, 1, T, std::vector(), false); + std::pair forward_result = + forward(ctx, 1, T, std::vector(), false); Tensor logits = forward_result.first; // pick last time-step logits [vocab] @@ -222,9 +215,7 @@ struct GPTLanguageModel return context; } - // ---------------------------------------------------------------- - // save / load weights - // ---------------------------------------------------------------- + // save / load weights void save(const std::string &path) const { std::ofstream f(path, std::ios::binary); diff --git a/engine/llm.cpp/include/tensor.cuh b/engine/llm.cpp/include/tensor.cuh new file mode 100644 index 0000000..b520bb8 --- /dev/null +++ b/engine/llm.cpp/include/tensor.cuh @@ -0,0 +1,108 @@ +#ifndef CUDA_TENSOR_CUH +#define CUDA_TENSOR_CUH + +#include "cuda_utils.cuh" + +#include +#include +#include + +struct CUDATensor +{ + std::vector shape; + float *d_data = nullptr; // VRAM device pointer + std::vector h_data; // Host RAM cache + bool is_on_device = false; + + CUDATensor() = default; + + CUDATensor(std::vector sh, float fill = 0.0f) : shape(std::move(sh)) + { + int total = numel(); + h_data.assign(total, fill); + } + + ~CUDATensor() + { + free_device(); + } + + // Move semantics for efficient memory transfers + CUDATensor(CUDATensor &&other) noexcept + : shape(std::move(other.shape)), h_data(std::move(other.h_data)), d_data(other.d_data), + is_on_device(other.is_on_device) + { + other.d_data = nullptr; + other.is_on_device = false; + } + + CUDATensor &operator=(CUDATensor &&other) noexcept + { + if (this != &other) + { + free_device(); + shape = std::move(other.shape); + h_data = std::move(other.h_data); + d_data = other.d_data; + is_on_device = other.is_on_device; + other.d_data = nullptr; + other.is_on_device = false; + } + return *this; + } + + int numel() const + { + int n = 1; + for (int d : shape) + n *= d; + return n; + } + + int ndim() const + { + return static_cast(shape.size()); + } + + void free_device() + { + if (d_data) + { + cudaFree(d_data); + d_data = nullptr; + is_on_device = false; + } + } + + void allocate_device() + { + if (!is_on_device) + { + size_t bytes = numel() * sizeof(float); + CUDA_CHECK(cudaMalloc(&d_data, bytes)); + is_on_device = true; + } + } + + void to_device() + { + allocate_device(); + if (!h_data.empty()) + { + size_t bytes = numel() * sizeof(float); + CUDA_CHECK(cudaMemcpy(d_data, h_data.data(), bytes, cudaMemcpyHostToDevice)); + } + } + + void to_host() + { + if (is_on_device) + { + h_data.resize(numel()); + size_t bytes = numel() * sizeof(float); + CUDA_CHECK(cudaMemcpy(h_data.data(), d_data, bytes, cudaMemcpyDeviceToHost)); + } + } +}; + +#endif \ No newline at end of file diff --git a/engine/llm.cpp/llm.cu b/engine/llm.cpp/llm.cu new file mode 100644 index 0000000..ea1c7f1 --- /dev/null +++ b/engine/llm.cpp/llm.cu @@ -0,0 +1,492 @@ +#include "config/config.h" +#include "include/backward.h" +#include "include/bpe.h" +#include "include/lm.h" +#include "include/sampler.h" + +// CUDA & cuBLAS Acceleration Headers +#include "include/cuda_kernels.cuh" +#include "include/cuda_layers.cuh" +#include "include/cuda_tensor.cuh" +#include "include/cuda_utils.cuh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Global cuBLAS context handle +cublasHandle_t g_cublas_handle = nullptr; + +static volatile bool g_interrupted = false; +static void sig_handler(int) +{ + g_interrupted = true; +} + +// Return the current wall-clock time as a formatted string. +static std::string now_str() +{ + std::time_t t = std::time(nullptr); + char buf[32]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t)); + return buf; +} + +// Return elapsed seconds since an arbitrary epoch using a monotonic clock. +static double wall_secs() +{ + using namespace std::chrono; + return duration(steady_clock::now().time_since_epoch()).count(); +} + +// Return true if the file at path exists and can be opened. +static bool file_exists(const std::string &path) +{ + std::ifstream f(path.c_str(), std::ios::binary); + return f.good(); +} + +// Return the directory portion of a file path, or "." for bare filenames. +static std::string dir_name(const std::string &path) +{ + std::string::size_type pos = path.find_last_of("/\\"); + if (pos == std::string::npos) + return "."; + if (pos == 0) + return path.substr(0, 1); + return path.substr(0, pos); +} + +// Return true if path starts with a drive letter or a slash. +static bool is_absolute_path(const std::string &path) +{ + if (path.empty()) + return false; + if (path.size() > 1 && path[1] == ':') + return true; + return path[0] == '/' || path[0] == '\\'; +} + +// Join base directory and child path with a separator. +static std::string join_path(const std::string &base, const std::string &child) +{ + if (base.empty() || base == ".") + return child; + char last = base[base.size() - 1]; + if (last == '/' || last == '\\') + return base + child; + return base + "/" + child; +} + +// Try the requested path first, then look next to the executable. +static std::string choose_existing_path(const std::string &requested_path, const std::string &argv0) +{ + if (requested_path.empty()) + return requested_path; + if (file_exists(requested_path)) + return requested_path; + if (is_absolute_path(requested_path)) + return requested_path; + + std::vector candidates; + candidates.push_back(join_path(dir_name(argv0), requested_path)); + candidates.push_back(join_path(".", requested_path)); + + for (size_t i = 0; i < candidates.size(); ++i) + { + if (file_exists(candidates[i])) + return candidates[i]; + } + return requested_path; +} + +// Choose a writable output path, preferring one next to the executable. +static std::string choose_output_path(const std::string &requested_path, const std::string &argv0) +{ + if (requested_path.empty() || is_absolute_path(requested_path)) + return requested_path; + + std::string exe_relative = join_path(dir_name(argv0), requested_path); + if (file_exists(requested_path) || !file_exists(exe_relative)) + return requested_path; + return exe_relative; +} + +// Print a one-line usage summary listing all supported flags. +static void print_usage(const char *argv0) +{ + std::cout << "Usage: " << argv0 << " [options] [data_file]\n" + << "\n" + << " --generate run inference only (needs saved weights)\n" + << " --chat start interactive chat mode\n" + << " --chat-tokens N max tokens per chat reply (default " + << DEFAULT_CHAT_TOKENS << ")\n" + << " --system TEXT system prompt prepended to every chat turn\n" + << " --rep-penalty F repetition penalty, 1.0 means off (default " + << DEFAULT_REP_PENALTY << ")\n" + << " --rep-window N recent token window for penalty (default " + << DEFAULT_REP_WINDOW << ")\n" + << " --help show this message\n"; +} + +// Sample n_tokens from the model using the given sampler params and print them. +static void +sample_tokens(GPTLanguageModel &model, DataLoader &dl, int n_tokens, const SamplerParams ¶ms) +{ + std::vector ctx = {0}; + for (int i = 0; i < n_tokens && !g_interrupted; ++i) + { + ctx = model.generate(ctx, 1, params); + CUDA_CHECK(cudaDeviceSynchronize()); + std::cout << dl.decode({ctx.back()}) << std::flush; + if ((int)ctx.size() > BLOCK_SIZE) + ctx = std::vector(ctx.end() - BLOCK_SIZE, ctx.end()); + } + std::cout << "\n"; +} + +// Estimate average cross-entropy loss over EVAL_ITERS random batches. +static float +estimate_loss(GPTLanguageModel &model, DataLoader &dl, const std::string &split, std::mt19937 &rng) +{ + float total = 0.0f; + for (int k = 0; k < EVAL_ITERS; ++k) + { + std::pair, std::vector> batch = + dl.get_batch(split, BATCH_SIZE, BLOCK_SIZE, rng); + std::pair result = + model.forward(batch.first, BATCH_SIZE, BLOCK_SIZE, batch.second, false); + CUDA_CHECK(cudaDeviceSynchronize()); + total += result.second; + } + return total / EVAL_ITERS; +} + +// Build the initial context for a chat turn. +static std::vector build_turn_context(const std::vector &sys_tokens, + const std::vector &user_tokens) +{ + std::vector ctx; + ctx.reserve(sys_tokens.size() + user_tokens.size()); + ctx.insert(ctx.end(), sys_tokens.begin(), sys_tokens.end()); + ctx.insert(ctx.end(), user_tokens.begin(), user_tokens.end()); + + if ((int)ctx.size() > BLOCK_SIZE) + ctx = std::vector(ctx.end() - BLOCK_SIZE, ctx.end()); + + return ctx; +} + +// Run an interactive chat loop. +static void +run_chat(GPTLanguageModel &model, DataLoader &dl, int max_new_tokens, const SamplerParams ¶ms) +{ + std::vector sys_tokens; + if (!params.system_prompt.empty()) + { + sys_tokens = dl.encode(params.system_prompt); + if (sys_tokens.empty()) + { + std::cerr << "[WARN] System prompt produced zero tokens. " + "All characters may be outside the training vocabulary.\n"; + } + else + { + std::cout << "[CHAT] System prompt active (" << sys_tokens.size() << " tokens, " + << BLOCK_SIZE - (int)sys_tokens.size() + << " tokens left for user input)\n"; + } + } + + std::cout << "\n" << std::string(60, '=') << "\n"; + std::cout << " CHAT MODE (CUDA Accelerated)\n"; + std::cout << " Type your prompt and press Enter.\n"; + std::cout << " Type quit or exit to leave.\n"; + std::cout << std::string(60, '=') << "\n\n"; + + while (!g_interrupted) + { + std::cout << "\033[1;32mYou>\033[0m "; + std::cout.flush(); + + std::string prompt; + if (!std::getline(std::cin, prompt)) + break; + + size_t s = prompt.find_first_not_of(" \t\r\n"); + size_t e = prompt.find_last_not_of(" \t\r\n"); + if (s == std::string::npos) + continue; + prompt = prompt.substr(s, e - s + 1); + + if (prompt == "quit" || prompt == "exit") + { + std::cout << "[Chat] Bye!\n"; + break; + } + + std::vector user_tokens = dl.encode(prompt); + if (user_tokens.empty()) + user_tokens = {0}; + + std::vector ctx = build_turn_context(sys_tokens, user_tokens); + + std::cout << "\033[1;36mllm>\033[0m "; + std::cout.flush(); + + for (int tok = 0; tok < max_new_tokens && !g_interrupted; ++tok) + { + ctx = model.generate(ctx, 1, params); + CUDA_CHECK(cudaDeviceSynchronize()); + std::cout << dl.decode({ctx.back()}) << std::flush; + + if ((int)ctx.size() > BLOCK_SIZE) + ctx = std::vector(ctx.end() - BLOCK_SIZE, ctx.end()); + } + std::cout << "\n\n"; + } +} + +// Entry point: parse flags, initialize CUDA context, load data, build model, then train or infer. +int main(int argc, char *argv[]) +{ + std::signal(SIGINT, sig_handler); + + std::cout << " [llm.cu - CUDA Engine]\n"; + + // Initialize GPU Driver and cuBLAS Context + int device_count = 0; + CUDA_CHECK(cudaGetDeviceCount(&device_count)); + if (device_count == 0) + { + std::cerr << "[ERROR] No CUDA-capable GPU detected!" << std::endl; + return 1; + } + + cudaDeviceProp prop; + CUDA_CHECK(cudaGetDeviceProperties(&prop, 0)); + std::cout << "[GPU] Active Device: " << prop.name + << " | VRAM: " << (prop.totalGlobalMem / (1024 * 1024)) << " MB" << std::endl; + + CUBLAS_CHECK(cublasCreate(&g_cublas_handle)); + + // Resolve data and model paths from defaults, then override with env vars. + std::string data_path = DEFAULT_CLEANED_PATH; + std::string model_path = BEST_MODEL_PATH; + + const char *env_data = std::getenv(DATA_PATH_ENV_VAR.c_str()); + const char *env_model = std::getenv(MODEL_PATH_ENV_VAR.c_str()); + if (env_data != nullptr && env_data[0] != '\0') + data_path = env_data; + if (env_model != nullptr && env_model[0] != '\0') + model_path = env_model; + + // Mode flags and sampler settings with their defaults. + bool gen_mode = false; + bool chat_mode = false; + int chat_tokens = DEFAULT_CHAT_TOKENS; + float rep_penalty = DEFAULT_REP_PENALTY; + int rep_window = DEFAULT_REP_WINDOW; + std::string system_prompt; + + for (int i = 1; i < argc; ++i) + { + std::string a = argv[i]; + + if (a == "--help") + { + print_usage(argv[0]); + CUBLAS_CHECK(cublasDestroy(g_cublas_handle)); + return 0; + } + else if (a == "--generate") + { + gen_mode = true; + } + else if (a == "--chat") + { + chat_mode = true; + } + else if (a == "--chat-tokens" && i + 1 < argc) + { + chat_tokens = std::atoi(argv[++i]); + } + else if (a == "--system" && i + 1 < argc) + { + system_prompt = argv[++i]; + } + else if (a == "--rep-penalty" && i + 1 < argc) + { + rep_penalty = (float)std::atof(argv[++i]); + } + else if (a == "--rep-window" && i + 1 < argc) + { + rep_window = std::atoi(argv[++i]); + } + else + { + data_path = a; + } + } + + data_path = choose_existing_path(data_path, argv[0]); + model_path = choose_output_path(model_path, argv[0]); + + SamplerParams sampler; + sampler.rep_penalty = rep_penalty; + sampler.rep_window = rep_window; + sampler.system_prompt = system_prompt; + + DataLoader dl; + try + { + dl.load(data_path); + } + catch (const std::exception &e) + { + std::cerr << e.what() << "\n"; + std::cerr << "[HINT] Put your text at " << DEFAULT_CLEANED_PATH + << ", pass a file path as the first argument, or set " << DATA_PATH_ENV_VAR + << ".\n"; + CUBLAS_CHECK(cublasDestroy(g_cublas_handle)); + return 1; + } + + GPTLanguageModel model(dl.vocab_size, N_EMBD, N_HEAD, N_LAYER, BLOCK_SIZE, SEED); + + long n_params = model.num_params(); + std::cout << "max_seq_len: " << BLOCK_SIZE << "\n"; + std::cout << "vocab_size: " << dl.vocab_size << "\n"; + std::cout << "num_layers: " << N_LAYER << "\n"; + std::cout << "num_heads: " << N_HEAD << "\n"; + std::cout << "channels: " << N_EMBD << "\n"; + std::cout << "num_parameters: " << n_params << "\n"; + std::cout << "rep_penalty: " << rep_penalty << "\n"; + std::cout << "rep_window: " << rep_window << "\n"; + + // Chat mode + if (chat_mode) + { + if (!file_exists(model_path)) + { + std::cerr << "[ERROR] Cannot start chat because model weights were not found at " + << model_path << "\n"; + std::cerr << "[HINT] Train first, or set " << MODEL_PATH_ENV_VAR + << " to an existing weights file.\n"; + CUBLAS_CHECK(cublasDestroy(g_cublas_handle)); + return 1; + } + + model.load(model_path); + std::cout << "weights: " << model_path << "\n"; + std::cout << "max_tokens: " << chat_tokens << "\n"; + + if (!sampler.system_prompt.empty()) + std::cout << "system: " << sampler.system_prompt << "\n"; + + run_chat(model, dl, chat_tokens, sampler); + CUBLAS_CHECK(cublasDestroy(g_cublas_handle)); + return 0; + } + + // Generate mode + if (gen_mode) + { + if (!file_exists(model_path)) + { + std::cerr << "[ERROR] Cannot generate because model weights were not found at " + << model_path << "\n"; + std::cerr << "[HINT] Train first, or set " << MODEL_PATH_ENV_VAR + << " to an existing weights file.\n"; + CUBLAS_CHECK(cublasDestroy(g_cublas_handle)); + return 1; + } + + model.load(model_path); + std::cout << "\ngenerating:\n"; + std::vector ctx = {0}; + while (!g_interrupted) + { + ctx = model.generate(ctx, 1, sampler); + CUDA_CHECK(cudaDeviceSynchronize()); + std::cout << dl.decode({ctx.back()}) << std::flush; + if ((int)ctx.size() > BLOCK_SIZE) + ctx = std::vector(ctx.end() - BLOCK_SIZE, ctx.end()); + } + std::cout << "\n"; + CUBLAS_CHECK(cublasDestroy(g_cublas_handle)); + return 0; + } + + // Training mode + AdamWState opt = build_optimizer(model, LEARNING_RATE); + std::mt19937 rng(SEED); + + float best_val_loss = 1e30f; + float last_val_loss = 0.0f; + + { + std::mt19937 init_rng(SEED); + last_val_loss = estimate_loss(model, dl, "val", init_rng); + } + + for (int iter = 1; iter <= MAX_ITERS && !g_interrupted; ++iter) + { + double step_start = wall_secs(); + + std::pair, std::vector> batch = + dl.get_batch("train", BATCH_SIZE, BLOCK_SIZE, rng); + + SavedForward saved = + forward_save(model, batch.first, BATCH_SIZE, BLOCK_SIZE, batch.second, true); + + float batch_loss = + model.forward(batch.first, BATCH_SIZE, BLOCK_SIZE, batch.second, false).second; + + Grads grads = backward(model, saved); + apply_grads(model, grads, opt); + + CUDA_CHECK(cudaDeviceSynchronize()); + + double step_ms = (wall_secs() - step_start) * 1000.0; + int tok_per_sec = + (step_ms > 0.0) ? (int)((long)BATCH_SIZE * BLOCK_SIZE / (step_ms / 1000.0)) : 0; + + bool better = false; + if (iter % EVAL_INTERVAL == 0 || iter == MAX_ITERS) + { + last_val_loss = estimate_loss(model, dl, "val", rng); + if (last_val_loss < best_val_loss) + { + best_val_loss = last_val_loss; + model.save(model_path); + better = true; + } + } + + std::cout << "step" << std::setw(5) << iter << "/" << MAX_ITERS << " | loss " + << std::fixed << std::setprecision(6) << batch_loss << " | val " << std::fixed + << std::setprecision(6) << last_val_loss << " | lr " << std::scientific + << std::setprecision(2) << (float)LEARNING_RATE << " | " << std::fixed + << std::setprecision(2) << step_ms << " ms" + << " | " << tok_per_sec << " tok/s" << (better ? " best" : "") << "\n"; + std::cout.flush(); + + if (iter % EVAL_INTERVAL == 0 || iter == MAX_ITERS) + { + std::cout << "generating:\n"; + sample_tokens(model, dl, iter == MAX_ITERS ? 10000 : 150, sampler); + } + } + + CUBLAS_CHECK(cublasDestroy(g_cublas_handle)); + return 0; +} \ No newline at end of file diff --git a/engine/llm.cpp/train.mm b/engine/llm.cpp/train.mm new file mode 100644 index 0000000..59ba9e1 --- /dev/null +++ b/engine/llm.cpp/train.mm @@ -0,0 +1,477 @@ +#import + +#import "config/config.h" +#import "include/backward.h" +#import "include/bpe.h" +#import "include/lm.h" +#import "include/sampler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static volatile bool g_interrupted = false; +static void sig_handler(int) +{ + g_interrupted = true; +} + +// Return the current wall-clock time as a formatted string. +static std::string now_str() +{ + std::time_t t = std::time(nullptr); + char buf[32]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t)); + return buf; +} + +// Return elapsed seconds since an arbitrary epoch using a monotonic clock. +static double wall_secs() +{ + using namespace std::chrono; + return duration(steady_clock::now().time_since_epoch()).count(); +} + +// Return true if the file at path exists and can be opened. +static bool file_exists(const std::string &path) +{ + std::ifstream f(path.c_str(), std::ios::binary); + return f.good(); +} + +// Return the directory portion of a file path, or "." for bare filenames. +static std::string dir_name(const std::string &path) +{ + std::string::size_type pos = path.find_last_of("/\\"); + if (pos == std::string::npos) + return "."; + if (pos == 0) + return path.substr(0, 1); + return path.substr(0, pos); +} + +// Return true if path starts with a drive letter or a slash. +static bool is_absolute_path(const std::string &path) +{ + if (path.empty()) + return false; + if (path.size() > 1 && path[1] == ':') + return true; + return path[0] == '/' || path[0] == '\\'; +} + +// Join base directory and child path with a separator. +static std::string join_path(const std::string &base, const std::string &child) +{ + if (base.empty() || base == ".") + return child; + char last = base[base.size() - 1]; + if (last == '/' || last == '\\') + return base + child; + return base + "/" + child; +} + +// Try the requested path first, then look next to the executable. +// Return whichever path exists, or the requested path if neither does. +static std::string choose_existing_path(const std::string &requested_path, const std::string &argv0) +{ + if (requested_path.empty()) + return requested_path; + if (file_exists(requested_path)) + return requested_path; + if (is_absolute_path(requested_path)) + return requested_path; + + std::vector candidates; + candidates.push_back(join_path(dir_name(argv0), requested_path)); + candidates.push_back(join_path(".", requested_path)); + + for (size_t i = 0; i < candidates.size(); ++i) + { + if (file_exists(candidates[i])) + return candidates[i]; + } + return requested_path; +} + +// Choose a writable output path, preferring one next to the executable +// only when the current directory copy does not already exist. +static std::string choose_output_path(const std::string &requested_path, const std::string &argv0) +{ + if (requested_path.empty() || is_absolute_path(requested_path)) + return requested_path; + + std::string exe_relative = join_path(dir_name(argv0), requested_path); + if (file_exists(requested_path) || !file_exists(exe_relative)) + return requested_path; + return exe_relative; +} + +// Print a one-line usage summary listing all supported flags. +static void print_usage(const char *argv0) +{ + std::cout << "Usage: " << argv0 << " [options] [data_file]\n" + << "\n" + << " --generate run inference only (needs saved weights)\n" + << " --chat start interactive chat mode\n" + << " --chat-tokens N max tokens per chat reply (default " + << DEFAULT_CHAT_TOKENS << ")\n" + << " --system TEXT system prompt prepended to every chat turn\n" + << " --rep-penalty F repetition penalty, 1.0 means off (default " + << DEFAULT_REP_PENALTY << ")\n" + << " --rep-window N recent token window for penalty (default " + << DEFAULT_REP_WINDOW << ")\n" + << " --help show this message\n"; +} + +// Sample n_tokens from the model using the given sampler params and print them. +static void +sample_tokens(GPTLanguageModel &model, DataLoader &dl, int n_tokens, const SamplerParams ¶ms) +{ + std::vector ctx = {0}; + for (int i = 0; i < n_tokens; ++i) + { + ctx = model.generate(ctx, 1, params); + std::cout << dl.decode({ctx.back()}) << std::flush; + if ((int)ctx.size() > BLOCK_SIZE) + ctx = std::vector(ctx.end() - BLOCK_SIZE, ctx.end()); + } + std::cout << "\n"; +} + +// Estimate average cross-entropy loss over EVAL_ITERS random batches. +// No gradients are computed and training mode is disabled. +static float +estimate_loss(GPTLanguageModel &model, DataLoader &dl, const std::string &split, std::mt19937 &rng) +{ + float total = 0.0f; + for (int k = 0; k < EVAL_ITERS; ++k) + { + std::pair, std::vector> batch = + dl.get_batch(split, BATCH_SIZE, BLOCK_SIZE, rng); + std::pair result = + model.forward(batch.first, BATCH_SIZE, BLOCK_SIZE, batch.second, false); + total += result.second; + } + return total / EVAL_ITERS; +} + +// Build the initial context for a chat turn. +// Places system tokens first (if any), then appends user tokens. +// Crops the combined context to BLOCK_SIZE from the right. +static std::vector build_turn_context(const std::vector &sys_tokens, + const std::vector &user_tokens) +{ + std::vector ctx; + ctx.reserve(sys_tokens.size() + user_tokens.size()); + ctx.insert(ctx.end(), sys_tokens.begin(), sys_tokens.end()); + ctx.insert(ctx.end(), user_tokens.begin(), user_tokens.end()); + + if ((int)ctx.size() > BLOCK_SIZE) + ctx = std::vector(ctx.end() - BLOCK_SIZE, ctx.end()); + + return ctx; +} + +// Run an interactive chat loop. +// The system prompt is encoded once and prepended to the context on every turn. +// Repetition penalty is applied during generation using params. +static void +run_chat(GPTLanguageModel &model, DataLoader &dl, int max_new_tokens, const SamplerParams ¶ms) +{ + // Encode the system prompt once at startup. + // Unknown characters are silently skipped because the model uses + // character-level tokenisation limited to the training vocabulary. + std::vector sys_tokens; + if (!params.system_prompt.empty()) + { + sys_tokens = dl.encode(params.system_prompt); + if (sys_tokens.empty()) + { + std::cerr << "[WARN] System prompt produced zero tokens. " + "All characters may be outside the training vocabulary.\n"; + } + else + { + std::cout << "[CHAT] System prompt active (" << sys_tokens.size() << " tokens, " + << BLOCK_SIZE - (int)sys_tokens.size() + << " tokens left for user input)\n"; + } + } + + std::cout << "\n" << std::string(60, '=') << "\n"; + std::cout << " CHAT MODE\n"; + std::cout << " Type your prompt and press Enter.\n"; + std::cout << " Type quit or exit to leave.\n"; + std::cout << std::string(60, '=') << "\n\n"; + + while (!g_interrupted) + { + std::cout << "\033[1;32mYou>\033[0m "; + std::cout.flush(); + + std::string prompt; + if (!std::getline(std::cin, prompt)) + break; + + // Strip leading and trailing whitespace. + size_t s = prompt.find_first_not_of(" \t\r\n"); + size_t e = prompt.find_last_not_of(" \t\r\n"); + if (s == std::string::npos) + continue; + prompt = prompt.substr(s, e - s + 1); + + if (prompt == "quit" || prompt == "exit") + { + std::cout << "[Chat] Bye!\n"; + break; + } + + // Encode the user text. Fall back to token 0 when nothing encodes. + std::vector user_tokens = dl.encode(prompt); + if (user_tokens.empty()) + user_tokens = {0}; + + // Build the starting context: system prompt then user input. + std::vector ctx = build_turn_context(sys_tokens, user_tokens); + + std::cout << "\033[1;36mllm>\033[0m "; + std::cout.flush(); + + // Generate tokens one by one and print each immediately. + for (int tok = 0; tok < max_new_tokens && !g_interrupted; ++tok) + { + ctx = model.generate(ctx, 1, params); + std::cout << dl.decode({ctx.back()}) << std::flush; + + if ((int)ctx.size() > BLOCK_SIZE) + ctx = std::vector(ctx.end() - BLOCK_SIZE, ctx.end()); + } + std::cout << "\n\n"; + } +} + +// Entry point: parse flags, load data, build model, then train or run inference. +int main(int argc, char *argv[]) +{ + @autoreleasepool { + std::signal(SIGINT, sig_handler); + + std::cout << " [llm.cpp]\n"; + + // Resolve data and model paths from defaults, then override with env vars. + std::string data_path = DEFAULT_CLEANED_PATH; + std::string model_path = BEST_MODEL_PATH; + + const char *env_data = std::getenv(DATA_PATH_ENV_VAR.c_str()); + const char *env_model = std::getenv(MODEL_PATH_ENV_VAR.c_str()); + if (env_data != nullptr && env_data[0] != '\0') + data_path = env_data; + if (env_model != nullptr && env_model[0] != '\0') + model_path = env_model; + + // Mode flags and sampler settings with their defaults. + bool gen_mode = false; + bool chat_mode = false; + int chat_tokens = DEFAULT_CHAT_TOKENS; + float rep_penalty = DEFAULT_REP_PENALTY; + int rep_window = DEFAULT_REP_WINDOW; + std::string system_prompt; + + for (int i = 1; i < argc; ++i) + { + std::string a = argv[i]; + + if (a == "--help") + { + print_usage(argv[0]); + return 0; + } + else if (a == "--generate") + { + gen_mode = true; + } + else if (a == "--chat") + { + chat_mode = true; + } + else if (a == "--chat-tokens" && i + 1 < argc) + { + chat_tokens = std::atoi(argv[++i]); + } + else if (a == "--system" && i + 1 < argc) + { + // Accept quoted or unquoted system prompt strings. + system_prompt = argv[++i]; + } + else if (a == "--rep-penalty" && i + 1 < argc) + { + rep_penalty = (float)std::atof(argv[++i]); + } + else if (a == "--rep-window" && i + 1 < argc) + { + rep_window = std::atoi(argv[++i]); + } + else + { + data_path = a; + } + } + + data_path = choose_existing_path(data_path, argv[0]); + model_path = choose_output_path(model_path, argv[0]); + + // Build the sampler params struct from parsed values. + SamplerParams sampler; + sampler.rep_penalty = rep_penalty; + sampler.rep_window = rep_window; + sampler.system_prompt = system_prompt; + + // Load and tokenise the training corpus. + DataLoader dl; + try + { + dl.load(data_path); + } + catch (const std::exception &e) + { + std::cerr << e.what() << "\n"; + std::cerr << "[HINT] Put your text at " << DEFAULT_CLEANED_PATH + << ", pass a file path as the first argument, or set " << DATA_PATH_ENV_VAR + << ".\n"; + return 1; + } + + GPTLanguageModel model(dl.vocab_size, N_EMBD, N_HEAD, N_LAYER, BLOCK_SIZE, SEED); + + long n_params = model.num_params(); + std::cout << "max_seq_len: " << BLOCK_SIZE << "\n"; + std::cout << "vocab_size: " << dl.vocab_size << "\n"; + std::cout << "num_layers: " << N_LAYER << "\n"; + std::cout << "num_heads: " << N_HEAD << "\n"; + std::cout << "channels: " << N_EMBD << "\n"; + std::cout << "num_parameters: " << n_params << "\n"; + std::cout << "rep_penalty: " << rep_penalty << "\n"; + std::cout << "rep_window: " << rep_window << "\n"; + + // Chat mode: load weights, print system prompt info, then enter the loop. + if (chat_mode) + { + if (!file_exists(model_path)) + { + std::cerr << "[ERROR] Cannot start chat because model weights were not found at " + << model_path << "\n"; + std::cerr << "[HINT] Train first, or set " << MODEL_PATH_ENV_VAR + << " to an existing weights file.\n"; + return 1; + } + + model.load(model_path); + std::cout << "weights: " << model_path << "\n"; + std::cout << "max_tokens: " << chat_tokens << "\n"; + + if (!sampler.system_prompt.empty()) + std::cout << "system: " << sampler.system_prompt << "\n"; + + run_chat(model, dl, chat_tokens, sampler); + return 0; + } + + // Generate mode: load weights and stream tokens until interrupted. + if (gen_mode) + { + if (!file_exists(model_path)) + { + std::cerr << "[ERROR] Cannot generate because model weights were not found at " + << model_path << "\n"; + std::cerr << "[HINT] Train first, or set " << MODEL_PATH_ENV_VAR + << " to an existing weights file.\n"; + return 1; + } + + model.load(model_path); + std::cout << "\ngenerating:\n"; + std::vector ctx = {0}; + while (!g_interrupted) + { + ctx = model.generate(ctx, 1, sampler); + std::cout << dl.decode({ctx.back()}) << std::flush; + if ((int)ctx.size() > BLOCK_SIZE) + ctx = std::vector(ctx.end() - BLOCK_SIZE, ctx.end()); + } + std::cout << "\n"; + return 0; + } + + // Training mode: build optimizer, then iterate. + AdamWState opt = build_optimizer(model, LEARNING_RATE); + std::mt19937 rng(SEED); + + float best_val_loss = 1e30f; + float last_val_loss = 0.0f; + + // Compute the initial validation loss before the first update. + { + std::mt19937 init_rng(SEED); + last_val_loss = estimate_loss(model, dl, "val", init_rng); + } + + for (int iter = 1; iter <= MAX_ITERS && !g_interrupted; ++iter) + { + double step_start = wall_secs(); + + std::pair, std::vector> batch = + dl.get_batch("train", BATCH_SIZE, BLOCK_SIZE, rng); + + SavedForward saved = + forward_save(model, batch.first, BATCH_SIZE, BLOCK_SIZE, batch.second, true); + + float batch_loss = + model.forward(batch.first, BATCH_SIZE, BLOCK_SIZE, batch.second, false).second; + + Grads grads = backward(model, saved); + apply_grads(model, grads, opt); + + double step_ms = (wall_secs() - step_start) * 1000.0; + int tok_per_sec = + (step_ms > 0.0) ? (int)((long)BATCH_SIZE * BLOCK_SIZE / (step_ms / 1000.0)) : 0; + + bool better = false; + if (iter % EVAL_INTERVAL == 0 || iter == MAX_ITERS) + { + last_val_loss = estimate_loss(model, dl, "val", rng); + if (last_val_loss < best_val_loss) + { + best_val_loss = last_val_loss; + model.save(model_path); + better = true; + } + } + + std::cout << "step" << std::setw(5) << iter << "/" << MAX_ITERS << " | loss " + << std::fixed << std::setprecision(6) << batch_loss << " | val " << std::fixed + << std::setprecision(6) << last_val_loss << " | lr " << std::scientific + << std::setprecision(2) << (float)LEARNING_RATE << " | " << std::fixed + << std::setprecision(2) << step_ms << " ms" + << " | " << tok_per_sec << " tok/s" << (better ? " best" : "") << "\n"; + std::cout.flush(); + + if (iter % EVAL_INTERVAL == 0 || iter == MAX_ITERS) + { + std::cout << "generating:\n"; + // Use sampler params so training samples also reflect the chosen settings. + sample_tokens(model, dl, iter == MAX_ITERS ? 10000 : 150, sampler); + } + } + } + + return 0; +} \ No newline at end of file diff --git a/engine/main.py b/engine/main.py index 5c15109..0e97417 100644 --- a/engine/main.py +++ b/engine/main.py @@ -6,106 +6,53 @@ import os from pathlib import Path import tiktoken -# LOGGING UTILITIES -W = 78 -DOUBLE = "=" * W -SINGLE = "-" * W -TICK = "best" -ARROW = ">" - -LOG_DIR = Path(__file__).resolve().parent / "logs" -LOG_DIR.mkdir(parents=True, exist_ok=True) -LOG_PATH = LOG_DIR / f"run_{time.strftime('%Y%m%d_%H%M%S')}.txt" -SCRIPT_DIR = Path(__file__).resolve().parent - - -def log(message=""): - line = "" if message == "" else f"{time.strftime('%Y-%m-%d %H:%M:%S')} | {message}" - print(line) - with open(LOG_PATH, "a", encoding="utf-8") as f: - f.write(f"{line}\n") - - -def header(title, subtitle=""): - log() - log(DOUBLE) - log(f" {title}") - if subtitle: - log(f" {subtitle}") - log(DOUBLE) - -def row(label, value="", unit="", note=""): - label_col = f" {label:<28}" - value_col = f"{str(value):<20}" - unit_col = f"{unit:<8}" - note_col = f" {note}" if note else "" - log(f"{label_col}{value_col}{unit_col}{note_col}") - - -def rule(): log(f" {SINGLE}") -def blank(): log() -def info(msg): log(f" {ARROW} {msg}") -def success(msg): log(f" ok {msg}") - - -# SESSION +SCRIPT_DIR = Path(__file__).resolve().parent +print("[llm]") +print() +print(f"Device: {'CUDA' if torch.cuda.is_available() else 'CPU'}") +print(f"PyTorch: {torch.__version__}") -log(f"{'Quadtrix':^{W}}") -blank() start = time.time() - -# CONFIGURATION - - cleaned_path = Path(os.environ.get("data", SCRIPT_DIR / "input.txt")) train_split = 0.9 seed = 1337 - - +batch_size = 2 +block_size = 20 +max_iters = 10000 +eval_interval = 1 +learning_rate = 3e-4 device = 'cuda' if torch.cuda.is_available() else 'cpu' -dropout = 0.1 -block_size = 256 -n_embd = 192 -n_head = 6 -n_layer = 6 -batch_size = 64 -max_iters = 5000 -eval_interval = 250 -learning_rate = 6e-4 -eval_iters = 200 -dropout = 0.1 +eval_iters = 1 +n_embd = 6 +n_head = 4 +n_layer = 4 +dropout = 0.0 torch.manual_seed(seed) -# tokenizer - -def get_tokenizer(encoding_name="o200k"): - tokenizer = tiktoken.get_encoding(encoding_name) - vocab_size = tokenizer.n_vocab - return tokenizer, vocab_size +def get_miniq_tokenizer(encoding_name="o200k_base"): + miniq_tokenizer = tiktoken.get_encoding(encoding_name) + miniq_vocab_size = miniq_tokenizer.n_vocab + return miniq_tokenizer, miniq_vocab_size -def encode(text, tokenizer): return tokenizer.encode(text) -def decode(tokens, tokenizer): return tokenizer.decode(tokens) +def miniq_encode(text, tokenizer): return tokenizer.encode(text) +def miniq_decode(tokens, tokenizer): return tokenizer.decode(tokens) -# DATA with open(cleaned_path, 'r', encoding='utf-8') as f: text = f.read() -tokenizer, vocab_size = get_tokenizer("o200k") -encoded_data = encode(text, tokenizer) - +miniq_tokenizer, vocab_size = get_miniq_tokenizer("o200k_base") +encoded_data = miniq_encode(text, miniq_tokenizer) data = torch.tensor(encoded_data, dtype=torch.long) n = int(train_split * len(data)) train_data = data[:n] val_data = data[n:] -# Batch and LOSS - def get_batch(split): data_split = train_data if split == 'train' else val_data @@ -119,21 +66,19 @@ def get_batch(split): @torch.no_grad() def estimate_loss(): out = {} - model.eval() + miniq_model.eval() for split in ['train', 'val']: losses = torch.zeros(eval_iters) for k in range(eval_iters): X, Y = get_batch(split) - _, loss = model(X, Y) + _, loss = miniq_model(X, Y) losses[k] = loss.item() out[split] = losses.mean() - model.train() + miniq_model.train() return out -# model - -class Head(nn.Module): +class MiniQuadtrixHead(nn.Module): def __init__(self, head_size): super().__init__() self.key = nn.Linear(n_embd, head_size, bias=False) @@ -154,10 +99,11 @@ def forward(self, x): return wei @ self.value(x) -class MultiHeadAttention(nn.Module): +class MiniQuadtrixMHA(nn.Module): def __init__(self, num_heads, head_size): super().__init__() - self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)]) + self.heads = nn.ModuleList( + [MiniQuadtrixHead(head_size) for _ in range(num_heads)]) self.proj = nn.Linear(head_size * num_heads, n_embd) self.dropout = nn.Dropout(dropout) @@ -166,7 +112,7 @@ def forward(self, x): return self.dropout(self.proj(out)) -class FeedFoward(nn.Module): +class MiniQuadtrixFFN(nn.Module): def __init__(self, n_embd): super().__init__() self.net = nn.Sequential( @@ -180,12 +126,12 @@ def forward(self, x): return self.net(x) -class Block(nn.Module): +class MiniQuadtrixBlock(nn.Module): def __init__(self, n_embd, n_head): super().__init__() head_size = n_embd // n_head - self.sa = MultiHeadAttention(n_head, head_size) - self.ffwd = FeedFoward(n_embd) + self.sa = MiniQuadtrixMHA(n_head, head_size) + self.ffwd = MiniQuadtrixFFN(n_embd) self.ln1 = nn.LayerNorm(n_embd) self.ln2 = nn.LayerNorm(n_embd) @@ -195,13 +141,13 @@ def forward(self, x): return x -class GPTLanguageModel(nn.Module): +class MiniQuadtrix(nn.Module): def __init__(self): super().__init__() self.token_embedding_table = nn.Embedding(vocab_size, n_embd) self.position_embedding_table = nn.Embedding(block_size, n_embd) self.blocks = nn.Sequential( - *[Block(n_embd, n_head=n_head) for _ in range(n_layer)]) + *[MiniQuadtrixBlock(n_embd, n_head=n_head) for _ in range(n_layer)]) self.ln_f = nn.LayerNorm(n_embd) self.lm_head = nn.Linear(n_embd, vocab_size) self.apply(self._init_weights) @@ -243,163 +189,103 @@ def generate(self, idx, max_new_tokens): return idx -# INITIALISE -model = GPTLanguageModel().to(device) -n_params = sum(p.numel() for p in model.parameters()) -optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate) - - -def count_activations(m, bs, seq_len, dev): - total = 0 - hooks = [] - - def _hook(module, inp, out): - nonlocal total - if isinstance(out, torch.Tensor): - total += out.numel() - for mod in m.modules(): - hooks.append(mod.register_forward_hook(_hook)) - dummy = torch.zeros(bs, seq_len, dtype=torch.long, device=dev) - with torch.no_grad(): - m(dummy) - for h in hooks: - h.remove() - return total - - -_num_activations = count_activations(model, batch_size, block_size, device) - -# -_train_batches = len(train_data) // (batch_size * block_size) -_val_batches = len(val_data) // (batch_size * block_size) - -row("Batch size", batch_size) -row("Block size", block_size) -row("Parameters", f"{n_params:,}") -blank() - -# training -header("TRAINING", - f"{max_iters:,} steps | eval every {eval_interval} | checkpoint on improvement") -blank() +miniq_model = MiniQuadtrix().to(device) +miniq_n_params = sum(p.numel() for p in miniq_model.parameters()) +miniq_optimizer = torch.optim.AdamW(miniq_model.parameters(), lr=learning_rate) + +print("CONFIG") +print(f"Seed: {seed}") +print(f"Batch size: {batch_size}") +print(f"Block size: {block_size}") +print(f"Learning rate: {learning_rate}") +print(f"Layers: {n_layer}") +print(f"Heads: {n_head}") +print(f"Embedding dim: {n_embd}") +print(f"Dropout: {dropout}") +print(f"Parameters: {miniq_n_params:,}") +print(f"Train tokens: {len(train_data):,}") +print(f"Val tokens: {len(val_data):,}") +print(f"Data file: {str(cleaned_path)}") +print() best_val_loss = float('inf') train_start = time.time() +prev_loss = None for iter in range(max_iters): if iter % eval_interval == 0 or iter == max_iters - 1: losses = estimate_loss() + elapsed = time.time() - train_start + total_norm = 0 + for p in miniq_model.parameters(): + if p.grad is not None: + param_norm = p.grad.detach().data.norm(2) + total_norm += param_norm.item() ** 2 + total_norm = total_norm ** 0.5 + + tokens_per_sec = (iter + 1) * batch_size * \ + block_size / elapsed if elapsed > 0 else 0 + is_best = losses['val'] < best_val_loss if is_best: best_val_loss = losses['val'] - torch.save(model.state_dict(), 'best_model.pt') - log(f" val loss {losses['val']:.6f}") + torch.save(miniq_model.state_dict(), 'llm.pt') + + print( + f"step {iter} | loss: {losses['train']:.6f} | lr {learning_rate:.4e} | norm: {total_norm:.4f} | dt: {elapsed*1000:.2f}ms | tok/sec: {tokens_per_sec:.2f}") sys.stdout.flush() - step_start = time.time() xb, yb = get_batch('train') - logits, loss = model(xb, yb) - optimizer.zero_grad(set_to_none=True) + logits, loss = miniq_model(xb, yb) + miniq_optimizer.zero_grad(set_to_none=True) loss.backward() - - # real grad norm (after backward, before step) - grad_norm = 0.0 - for p in model.parameters(): - if p.grad is not None: - grad_norm += p.grad.detach().data.norm(2).item() ** 2 - grad_norm = grad_norm ** 0.5 - - optimizer.step() - - # real per-step dt and tok/sec - step_dt_ms = (time.time() - step_start) * 1000 - tok_per_sec = (batch_size * block_size) / (step_dt_ms / 1000.0) - - # real current lr from optimizer state - cur_lr = optimizer.param_groups[0]['lr'] - - line = (f"step {iter} | loss: {loss.item():.6f} | lr {cur_lr:.4e} | norm: {grad_norm:.4f} | dt: {step_dt_ms:.2f}ms | tok/sec: {tok_per_sec:.2f}") - print(line) - with open(LOG_PATH, "a", encoding="utf-8") as f: - f.write(line + "\n") - - # sample 100 tokens every 50 steps - if (iter + 1) % 50 == 0: - model.eval() - context = torch.zeros((1, 1), dtype=torch.long, device=device) - with torch.no_grad(): - sample_ids = model.generate(context, max_new_tokens=100) - sample_text = decode(sample_ids[0].tolist(), tokenizer) - model.train() - blank() - log(f" [sample @ step {iter + 1}]") - log(f" {'-' * 60}") - log(f" {sample_text.strip()}") - log(f" {'-' * 60}") - blank() + miniq_optimizer.step() total_time = time.time() - train_start -blank() -rule() -row("Duration", f"{int(total_time // 60)}m {int(total_time % 60):02d}s") -row("Best val loss", f"{best_val_loss:.4f}", "", TICK) -row("Checkpoint", "best_model.pt", "", TICK) -rule() +print() +print(f"Duration: {int(total_time // 60)}m {int(total_time % 60):02d}s") +print(f"Best val loss: {best_val_loss:.4f}") +print() -# RESTORE CHECKPOINT -blank() -model.load_state_dict(torch.load( - 'best_model.pt', map_location=device, weights_only=True)) -model.eval() -success(f"Restored best_model.pt | val loss {best_val_loss:.4f}") +miniq_model.load_state_dict(torch.load( + 'mini-quadtrix.pt', map_location=device, weights_only=True)) +miniq_model.eval() -# INFERENCE - - -header("INFERENCE", "quit / exit / q -> end session") -blank() +print("INFERENCE") +print() try: while True: - prompt = input(f" user {ARROW} ").strip() - log(f" You {ARROW} {prompt}") - + prompt = input("user > ").strip() if prompt.lower() in ("quit", "exit", "q"): - blank() - success("Session ended.") + print() + print("Session ended.") break - if not prompt: continue - encoded_prompt = encode(prompt, tokenizer) + encoded_prompt = miniq_encode(prompt, miniq_tokenizer) context = torch.tensor( [encoded_prompt], dtype=torch.long, device=device) with torch.no_grad(): - output_ids = model.generate(context, max_new_tokens=200) + output_ids = miniq_model.generate(context, max_new_tokens=200) new_tokens = output_ids[0][len(encoded_prompt):].tolist() - response = decode(new_tokens, tokenizer).strip() + response = miniq_decode(new_tokens, miniq_tokenizer).strip() - blank() - log(f" Model {ARROW} {response}") - blank() + print() + print(f"Model > {response}") + print() except KeyboardInterrupt: - blank() - success("Interrupted.") - + print() + print("Interrupted.") -end = time.time() -wall_clock = end - start +wall_clock = time.time() - start -blank() -rule() -row("Training", f"{int(total_time // 60)}m {int(total_time % 60):02d}s") -row("Total", - f"{int(wall_clock // 60)}m {int(wall_clock % 60):02d}s", "", TICK) -rule() -blank() -log(f"{DOUBLE}\n") +print() +print(f"Training: {int(total_time // 60)}m {int(total_time % 60):02d}s") +print(f"Total: {int(wall_clock // 60)}m {int(wall_clock % 60):02d}s") +print() diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index 4558365..0000000 --- a/frontend/.env.example +++ /dev/null @@ -1 +0,0 @@ -VITE_API_BASE_URL=http://localhost:3001 diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index c694ea2..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - Quadtrix.cpp Chat - - -

- - - diff --git a/frontend/manifest.webmanifest b/frontend/manifest.webmanifest deleted file mode 100644 index 882922b..0000000 --- a/frontend/manifest.webmanifest +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Quadtrix.cpp Chat", - "short_name": "Quadtrix", - "description": "Installable local chat interface for Quadtrix C++ and PyTorch model backends.", - "start_url": "/", - "scope": "/", - "display": "standalone", - "background_color": "#0a0a0a", - "theme_color": "#0a0a0a", - "orientation": "any", - "categories": ["developer", "productivity", "utilities"], - "icons": [ - { - "src": "/icon.svg", - "sizes": "any", - "type": "image/svg+xml", - "purpose": "any maskable" - } - ] -} diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 3c0ec78..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,2184 +0,0 @@ -{ - "name": "quadtrix-chat", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "quadtrix-chat", - "version": "1.0.0", - "dependencies": { - "@tanstack/react-query": "^5.101.2", - "date-fns": "^4.1.0", - "framer-motion": "^12.42.2", - "react": "^19.2.7", - "react-dom": "^18.3.1", - "zustand": "^5.0.2" - }, - "devDependencies": { - "@types/react": "^19.2.17", - "@types/react-dom": "^18.3.5", - "@vitejs/plugin-react": "^4.3.4", - "autoprefixer": "^10.4.20", - "postcss": "^8.4.49", - "tailwindcss": "^4.3.2", - "typescript": "^7.0.2", - "vite": "^8.0.16" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "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/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tanstack/query-core": { - "version": "5.101.2", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", - "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.101.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz", - "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.101.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.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==", - "dev": true, - "license": "MIT", - "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.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", - "dev": true, - "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" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.24", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz", - "integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "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" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001791", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", - "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", - "dev": true, - "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" - } - ], - "license": "CC-BY-4.0" - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.344", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", - "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", - "dev": true, - "license": "ISC" - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", - "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.42.2", - "motion-utils": "^12.39.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "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==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/motion-dom": { - "version": "12.42.2", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", - "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.39.0" - } - }, - "node_modules/motion-utils": { - "version": "12.39.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", - "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", - "license": "MIT" - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "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" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.133.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tailwindcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", - "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc" - }, - "engines": { - "node": ">=16.20.0" - }, - "optionalDependencies": { - "@typescript/typescript-aix-ppc64": "7.0.2", - "@typescript/typescript-darwin-arm64": "7.0.2", - "@typescript/typescript-darwin-x64": "7.0.2", - "@typescript/typescript-freebsd-arm64": "7.0.2", - "@typescript/typescript-freebsd-x64": "7.0.2", - "@typescript/typescript-linux-arm": "7.0.2", - "@typescript/typescript-linux-arm64": "7.0.2", - "@typescript/typescript-linux-loong64": "7.0.2", - "@typescript/typescript-linux-mips64el": "7.0.2", - "@typescript/typescript-linux-ppc64": "7.0.2", - "@typescript/typescript-linux-riscv64": "7.0.2", - "@typescript/typescript-linux-s390x": "7.0.2", - "@typescript/typescript-linux-x64": "7.0.2", - "@typescript/typescript-netbsd-arm64": "7.0.2", - "@typescript/typescript-netbsd-x64": "7.0.2", - "@typescript/typescript-openbsd-arm64": "7.0.2", - "@typescript/typescript-openbsd-x64": "7.0.2", - "@typescript/typescript-sunos-x64": "7.0.2", - "@typescript/typescript-win32-arm64": "7.0.2", - "@typescript/typescript-win32-x64": "7.0.2" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "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" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/zustand": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", - "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 7f52cbf..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "quadtrix-chat", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite --host 0.0.0.0 --configLoader runner", - "build": "tsc && vite build --configLoader runner", - "preview": "vite preview" - }, - "dependencies": { - "@tanstack/react-query": "^5.101.2", - "date-fns": "^4.1.0", - "framer-motion": "^12.42.2", - "react": "^19.2.7", - "react-dom": "^18.3.1", - "zustand": "^5.0.2" - }, - "devDependencies": { - "@types/react": "^19.2.17", - "@types/react-dom": "^18.3.5", - "@vitejs/plugin-react": "^4.3.4", - "autoprefixer": "^10.4.20", - "postcss": "^8.4.49", - "tailwindcss": "^4.3.2", - "typescript": "^7.0.2", - "vite": "^8.0.16" - } -} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 2aa7205..0000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg deleted file mode 100644 index fe615d0..0000000 --- a/frontend/public/icon.svg +++ /dev/null @@ -1,53 +0,0 @@ - - LLM logo icon - A dark rounded-square icon with a smooth rainbow-gradient LLM wordmark, inspired by Apple TV aesthetic - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LLM - - - - - - - \ No newline at end of file diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest deleted file mode 100644 index 882922b..0000000 --- a/frontend/public/manifest.webmanifest +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Quadtrix.cpp Chat", - "short_name": "Quadtrix", - "description": "Installable local chat interface for Quadtrix C++ and PyTorch model backends.", - "start_url": "/", - "scope": "/", - "display": "standalone", - "background_color": "#0a0a0a", - "theme_color": "#0a0a0a", - "orientation": "any", - "categories": ["developer", "productivity", "utilities"], - "icons": [ - { - "src": "/icon.svg", - "sizes": "any", - "type": "image/svg+xml", - "purpose": "any maskable" - } - ] -} diff --git a/frontend/public/sw.js b/frontend/public/sw.js deleted file mode 100644 index 4f5d599..0000000 --- a/frontend/public/sw.js +++ /dev/null @@ -1,52 +0,0 @@ -const CACHE_NAME = 'quadtrix-chat-v1'; -const APP_SHELL = ['/', '/manifest.webmanifest', '/icon.svg']; - -self.addEventListener('install', (event) => { - event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))); - self.skipWaiting(); -}); - -self.addEventListener('activate', (event) => { - event.waitUntil( - caches.keys().then((keys) => - Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))) - ) - ); - self.clients.claim(); -}); - -self.addEventListener('fetch', (event) => { - if (event.request.method !== 'GET') { - return; - } - - const url = new URL(event.request.url); - - const isApiRequest = - url.pathname.startsWith('/api/') || - url.pathname === '/generate' || - url.pathname === '/health' || - url.pathname === '/status'; - - if (isApiRequest) { - return; - } - - event.respondWith( - caches.match(event.request).then((cached) => { - if (cached) { - return cached; - } - return fetch(event.request) - .then((response) => { - if (!response || response.status !== 200 || response.type === 'opaque') { - return response; - } - const copy = response.clone(); - caches.open(CACHE_NAME).then((cache) => cache.put(event.request, copy)); - return response; - }) - .catch(() => caches.match('/')); - }) - ); -}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 6141dcf..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useCallback } from "react"; - -import { ChatView } from "./components/chat/ChatView"; -import { AppLayout } from "./components/layout/AppLayout"; -import { useKeyboardShortcut } from "./hooks/useKeyboardShortcut"; -import { useCreateSession } from "./api/sessions"; -import { useSessionStore } from "./store/sessionStore"; -import { useSettingsStore } from "./store/settingsStore"; - -export default function App() { - const clearMessages = useSessionStore((state) => state.clearMessages); - const setActiveSession = useSessionStore((state) => state.setActiveSession); - const setMessages = useSessionStore((state) => state.setMessages); - const setSettingsOpen = useSettingsStore((state) => state.setSettingsOpen); - const createSession = useCreateSession(); - - const newConversation = useCallback(() => { - createSession.mutate(undefined, { - onSuccess: (session) => { - setActiveSession(session.id); - setMessages([]); - }, - }); - }, [createSession, setActiveSession, setMessages]); - - const openSettings = useCallback(() => setSettingsOpen(true), [setSettingsOpen]); - const closeSettings = useCallback(() => setSettingsOpen(false), [setSettingsOpen]); - - useKeyboardShortcut(["ctrl", "l"], clearMessages); - useKeyboardShortcut(["ctrl", "n"], newConversation); - useKeyboardShortcut(["ctrl", ","], openSettings); - useKeyboardShortcut(["escape"], closeSettings); - - return ( - - - - ); -} diff --git a/frontend/src/api/chat.ts b/frontend/src/api/chat.ts deleted file mode 100644 index 70ea5e0..0000000 --- a/frontend/src/api/chat.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -import { apiFetch } from "./client"; -import type { ChatRequest, ChatResponse } from "../types"; - -export function useSendMessage() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (payload: ChatRequest): Promise => { - try { - return await apiFetch("/api/chat", { - method: "POST", - body: JSON.stringify(payload), - }); - } catch (error) { - throw error; - } - }, - onSuccess: async (response) => { - await queryClient.invalidateQueries({ queryKey: ["sessions"] }); - await queryClient.invalidateQueries({ queryKey: ["messages", response.session_id] }); - }, - }); -} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts deleted file mode 100644 index 3299818..0000000 --- a/frontend/src/api/client.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useSettingsStore } from "../store/settingsStore"; -import type { ApiError } from "../types"; - -export class ApiClientError extends Error { - apiError: ApiError; - - constructor(apiError: ApiError) { - super(apiError.message); - this.apiError = apiError; - } -} - -export async function apiFetch(path: string, init?: RequestInit): Promise { - const baseUrl = useSettingsStore.getState().apiBaseUrl.replace(/\/$/, ""); - const headers = new Headers(init?.headers); - headers.set("Content-Type", "application/json"); - try { - const response = await fetch(`${baseUrl}${path}`, { - ...init, - headers, - }); - if (!response.ok) { - const fallback: ApiError = { error: "request_failed", message: response.statusText, code: response.status }; - const error = (await response.json().catch(() => fallback)) as ApiError; - throw new ApiClientError(error); - } - if (response.status === 204) { - return undefined as T; - } - return (await response.json()) as T; - } catch (error) { - if (error instanceof ApiClientError) { - throw error; - } - throw new ApiClientError({ error: "network_error", message: "Could not reach the API server", code: 0 }); - } -} diff --git a/frontend/src/api/health.ts b/frontend/src/api/health.ts deleted file mode 100644 index cb2da72..0000000 --- a/frontend/src/api/health.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; - -import { apiFetch } from "./client"; -import type { HealthResponse, ModelStats } from "../types"; - -export function useHealth() { - return useQuery({ - queryKey: ["health"], - queryFn: async (): Promise => { - try { - return await apiFetch("/api/health"); - } catch (error) { - throw error; - } - }, - refetchInterval: 30000, - retry: 1, - }); -} - -export function useStats() { - return useQuery({ - queryKey: ["stats"], - queryFn: async (): Promise => { - try { - return await apiFetch("/api/stats"); - } catch (error) { - throw error; - } - }, - refetchInterval: 30000, - retry: 1, - }); -} diff --git a/frontend/src/api/sessions.ts b/frontend/src/api/sessions.ts deleted file mode 100644 index 967089c..0000000 --- a/frontend/src/api/sessions.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; - -import { apiFetch } from "./client"; -import type { Message, Session } from "../types"; - -export function useSessions() { - return useQuery({ - queryKey: ["sessions"], - queryFn: async (): Promise => { - try { - return await apiFetch("/api/sessions"); - } catch (error) { - throw error; - } - }, - }); -} - -export function useCreateSession() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (title?: string): Promise => { - try { - return await apiFetch("/api/sessions", { - method: "POST", - body: JSON.stringify({ title }), - }); - } catch (error) { - throw error; - } - }, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ["sessions"] }); - }, - }); -} - -export function useDeleteSession() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (sessionId: string): Promise => { - try { - await apiFetch(`/api/sessions/${sessionId}`, { method: "DELETE" }); - } catch (error) { - throw error; - } - }, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ["sessions"] }); - }, - }); -} - -export function useSessionMessages(sessionId: string | null) { - return useQuery({ - queryKey: ["messages", sessionId], - enabled: Boolean(sessionId), - queryFn: async (): Promise => { - try { - return await apiFetch(`/api/sessions/${sessionId}/messages`); - } catch (error) { - throw error; - } - }, - }); -} diff --git a/frontend/src/components/chat/ChatView.tsx b/frontend/src/components/chat/ChatView.tsx deleted file mode 100644 index 68c29eb..0000000 --- a/frontend/src/components/chat/ChatView.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useEffect, useState } from "react"; - -import { ApiClientError } from "../../api/client"; -import { useSendMessage } from "../../api/chat"; -import { useSessionMessages } from "../../api/sessions"; -import { useHealth } from "../../api/health"; -import { useSessionStore } from "../../store/sessionStore"; -import { useSettingsStore } from "../../store/settingsStore"; -import type { Message } from "../../types"; -import { EmptyState } from "./EmptyState"; -import { MessageList } from "./MessageList"; -import { InputBar } from "../input/InputBar"; - -export function ChatView() { - const [draft, setDraft] = useState(""); - const activeSessionId = useSessionStore((state) => state.activeSessionId); - const messages = useSessionStore((state) => state.messages); - const setActiveSession = useSessionStore((state) => state.setActiveSession); - const setMessages = useSessionStore((state) => state.setMessages); - const addMessage = useSessionStore((state) => state.addMessage); - const replaceMessage = useSessionStore((state) => state.replaceMessage); - const maxTokens = useSettingsStore((state) => state.maxTokens); - const temperature = useSettingsStore((state) => state.temperature); - const modelBackend = useSettingsStore((state) => state.modelBackend); - const sendMessage = useSendMessage(); - const { data: health } = useHealth(); - const { data: fetchedMessages } = useSessionMessages(activeSessionId); - const online = modelBackend === "cpp" ? health?.cpp_server === "ok" : health?.torch_model === "ok"; - - useEffect(() => { - if (fetchedMessages) { - setMessages(fetchedMessages); - } - }, [fetchedMessages, setMessages]); - - const now = (): string => new Date().toISOString(); - - const handleSend = (prompt: string): void => { - const sessionId = activeSessionId ?? crypto.randomUUID(); - setActiveSession(sessionId); - const userMessage: Message = { - id: `local-${crypto.randomUUID()}`, - session_id: sessionId, - role: "user", - text: prompt, - chars: 0, - seconds: 0, - created_at: now(), - }; - const pendingId = `pending-${crypto.randomUUID()}`; - const pendingMessage: Message = { - id: pendingId, - session_id: sessionId, - role: "assistant", - text: "", - chars: 0, - seconds: 0, - created_at: now(), - pending: true, - }; - addMessage(userMessage); - addMessage(pendingMessage); - sendMessage.mutate( - { session_id: sessionId, prompt, max_tokens: maxTokens, temperature, stream: false, model_backend: modelBackend }, - { - onSuccess: (response) => { - setActiveSession(response.session_id); - replaceMessage(pendingId, { - id: response.id, - session_id: response.session_id, - role: "assistant", - text: response.text, - prompt: response.prompt, - chars: response.chars, - seconds: response.seconds, - created_at: response.created_at, - }); - }, - onError: (error) => { - const message = - error instanceof ApiClientError - ? error.apiError.message - : "Could not reach the selected model. Check the C++ server or engine checkpoint."; - replaceMessage(pendingId, { - ...pendingMessage, - text: message, - pending: false, - error: "model_unavailable", - }); - }, - }, - ); - }; - - return ( -
- {messages.length === 0 ? : } - -
- ); -} diff --git a/frontend/src/components/chat/EmptyState.tsx b/frontend/src/components/chat/EmptyState.tsx deleted file mode 100644 index abf94ec..0000000 --- a/frontend/src/components/chat/EmptyState.tsx +++ /dev/null @@ -1,97 +0,0 @@ -export function EmptyState() { - return ( -
-
- {/* Icon */} -
- - - - -
- -
-

- Quadtrix.cpp -

-

- Local char-level language model. Start a conversation below. -

-
- - {/* Hint chips */} -
- {["Fast local inference", "C++ & PyTorch backends", "No cloud required"].map((chip) => ( - - {chip} - - ))} -
-
-
- ); -} diff --git a/frontend/src/components/chat/MessageAvatar.tsx b/frontend/src/components/chat/MessageAvatar.tsx deleted file mode 100644 index c606c9d..0000000 --- a/frontend/src/components/chat/MessageAvatar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import type { Role } from "../../types"; - -interface MessageAvatarProps { - role: Role; -} - -export function MessageAvatar({ role }: MessageAvatarProps) { - const isUser = role === "user"; - - if (isUser) { - return ( -
- U -
- ); - } - - return ( -
- Q -
- ); -} diff --git a/frontend/src/components/chat/MessageList.tsx b/frontend/src/components/chat/MessageList.tsx deleted file mode 100644 index 5de6e62..0000000 --- a/frontend/src/components/chat/MessageList.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useRef } from "react"; -import type { Message } from "../../types"; -import { useAutoScroll } from "../../hooks/useAutoScroll"; -import { MessageRow } from "./MessageRow"; - -interface MessageListProps { - messages: Message[]; -} - -export function MessageList({ messages }: MessageListProps) { - const bottomRef = useRef(null); - useAutoScroll(bottomRef, messages); - - return ( -
-
- {messages.map((message) => ( - - ))} -
-
-
- ); -} diff --git a/frontend/src/components/chat/MessageRow.tsx b/frontend/src/components/chat/MessageRow.tsx deleted file mode 100644 index 8dd3910..0000000 --- a/frontend/src/components/chat/MessageRow.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { motion } from "framer-motion"; -import { useState } from "react"; - -import { formatRelativeTime } from "../../utils/time"; -import type { Message } from "../../types"; -import { MessageAvatar } from "./MessageAvatar"; -import { ThinkingIndicator } from "./ThinkingIndicator"; - -interface MessageRowProps { - message: Message; -} - -export function MessageRow({ message }: MessageRowProps) { - const [copied, setCopied] = useState(false); - const isUser = message.role === "user"; - - const copyText = async (): Promise => { - try { - await navigator.clipboard.writeText(message.text); - setCopied(true); - window.setTimeout(() => setCopied(false), 1200); - } catch (error) { - setCopied(false); - } - }; - - return ( - - {!isUser && } - -
- {/* Meta row */} -
- {isUser ? "You" : "Quadtrix"} - {formatRelativeTime(message.created_at)} - {!isUser && !message.pending && ( - - )} -
- - {/* Bubble */} -
- {message.pending ? ( - - ) : ( - {message.text} - )} -
-
- - {isUser && } -
- ); -} diff --git a/frontend/src/components/chat/StarterPrompts.tsx b/frontend/src/components/chat/StarterPrompts.tsx deleted file mode 100644 index fb10a22..0000000 --- a/frontend/src/components/chat/StarterPrompts.tsx +++ /dev/null @@ -1,22 +0,0 @@ -interface StarterPromptsProps { - onSelect: (prompt: string) => void; -} - -const prompts = ["Once upon a time", "Timmy is a", "hi how are you", "The little door opened"]; - -export function StarterPrompts({ onSelect }: StarterPromptsProps) { - return ( -
- {prompts.map((prompt) => ( - - ))} -
- ); -} diff --git a/frontend/src/components/chat/ThinkingIndicator.tsx b/frontend/src/components/chat/ThinkingIndicator.tsx deleted file mode 100644 index 7ec4a6c..0000000 --- a/frontend/src/components/chat/ThinkingIndicator.tsx +++ /dev/null @@ -1,28 +0,0 @@ -export function ThinkingIndicator() { - return ( -
- Generating - - {[0, 120, 240].map((delay) => ( - - ))} - - -
- ); -} diff --git a/frontend/src/components/input/CharCounter.tsx b/frontend/src/components/input/CharCounter.tsx deleted file mode 100644 index 5ddf619..0000000 --- a/frontend/src/components/input/CharCounter.tsx +++ /dev/null @@ -1,9 +0,0 @@ -interface CharCounterProps { - count: number; - max: number; -} - -export function CharCounter({ count, max }: CharCounterProps) { - const over = count > max; - return {count}/{max}; -} diff --git a/frontend/src/components/input/InputBar.tsx b/frontend/src/components/input/InputBar.tsx deleted file mode 100644 index 6b39639..0000000 --- a/frontend/src/components/input/InputBar.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { CharCounter } from "./CharCounter"; - -interface InputBarProps { - disabled: boolean; - isSending: boolean; - onSend: (prompt: string) => void; - initialValue?: string; - onDraftChange?: (value: string) => void; -} - -export function InputBar({ disabled, isSending, onSend, initialValue = "", onDraftChange }: InputBarProps) { - const [value, setValue] = useState(initialValue); - const ref = useRef(null); - - useEffect(() => { - setValue(initialValue); - }, [initialValue]); - - useEffect(() => { - onDraftChange?.(value); - }, [onDraftChange, value]); - - useEffect(() => { - if (!ref.current) return; - ref.current.style.height = "auto"; - ref.current.style.height = `${Math.min(ref.current.scrollHeight, 120)}px`; - }, [value]); - - const submit = (): void => { - const prompt = value.trim(); - if (!prompt || disabled || isSending) return; - setValue(""); - onSend(prompt); - }; - - return ( -
-
{ - (e.currentTarget as HTMLDivElement).style.borderColor = "var(--border-strong)"; - }} - onBlurCapture={(e) => { - (e.currentTarget as HTMLDivElement).style.borderColor = "var(--border-muted)"; - }} - > -
-