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 0000000..3372892 Binary files /dev/null and b/assets/run_2026-07-16 165731.png differ diff --git a/benches/python_benchmark.py b/benches/python_benchmark.py deleted file mode 100644 index 326a46d..0000000 --- a/benches/python_benchmark.py +++ /dev/null @@ -1,482 +0,0 @@ -"""Real PyTorch benchmark suite for llm.cpp. - -Measures the things an ML/AI engineer usually asks for: -model metadata, tokenizer/data throughput, forward latency, training-step -latency, autoregressive generation latency, memory, and JSON/CSV output. -""" - -from __future__ import annotations - -import argparse -import csv -import gc -import importlib.util -import json -import math -import platform -import statistics -import sys -import time -from dataclasses import dataclass, asdict -from pathlib import Path -from typing import Any, Callable - - -ROOT = Path(__file__).resolve().parents[1] -ENGINE_INFERENCE = ROOT / "engine" / "inference.py" -DEFAULT_DATA = ROOT / "engine" / "input.txt" -DEFAULT_OUT = ROOT / "benchmark" / "results" - - -def load_engine_module(): - spec = importlib.util.spec_from_file_location( - "quadtrix_engine_inference", ENGINE_INFERENCE) - if spec is None or spec.loader is None: - raise RuntimeError(f"Cannot import {ENGINE_INFERENCE}") - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def now_iso() -> 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)"; - }} - > -
-