← back to Exo
prep repo for v1
0fcee70833a9d058881711543d9c04a1a60c7d02 · 2025-12-17 15:25:55 +0000 · Jake Hillion
Files touched
D .circleci/config.ymlD .gitattributesD .github/bench.pyD .github/bootstrap.shD .github/optimize_performance.shD .github/workflows/bench_job.ymlD .github/workflows/benchmarks.ymlD .gitignoreD .style.yapfD LICENSEM README.mdD configure_mlx.shD docs/exo-logo-black-bg.jpgD docs/exo-logo-transparent-black-text.pngD docs/exo-logo-transparent.pngD docs/exo-rounded.pngD docs/exo-screenshot.jpgD examples/astra/README.mdD examples/astra/astra.xcodeproj/project.pbxprojD examples/astra/astra.xcodeproj/project.xcworkspace/contents.xcworkspacedataD examples/astra/astra.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plistD examples/astra/astra.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolvedD examples/astra/astra/Assets.xcassets/AccentColor.colorset/Contents.jsonD examples/astra/astra/Assets.xcassets/AppIcon.appiconset/Contents.jsonD examples/astra/astra/Assets.xcassets/Contents.jsonD examples/astra/astra/ContentView.swiftD examples/astra/astra/Preview Content/Preview Assets.xcassets/Contents.jsonD examples/astra/astra/astra.entitlementsD examples/astra/astra/astraApp.swiftD examples/astra/astraTests/astraTests.swiftD examples/astra/astraUITests/astraUITests.swiftD examples/astra/astraUITests/astraUITestsLaunchTests.swiftD examples/chatgpt_api.shD examples/function_calling.pyD exo/__init__.pyD exo/api/__init__.pyD exo/api/chatgpt_api.pyD exo/apputil/__init__.pyD exo/apputil/anim.pyD exo/apputil/baseimages/image1.pngD exo/apputil/baseimages/image2.pngD exo/apputil/baseimages/image3.pngD exo/apputil/baseimages/image4.pngD exo/download/__init__.pyD exo/download/download_progress.pyD exo/download/hf/__init__.pyD exo/download/hf/hf_helpers.pyD exo/download/new_shard_download.pyD exo/download/shard_download.pyD exo/download/test_new_shard_download.pyD exo/helpers.pyD exo/inference/__init__.pyD exo/inference/debug_inference_engine.pyD exo/inference/dummy_inference_engine.pyD exo/inference/inference_engine.pyD exo/inference/mlx/__init__.pyD exo/inference/mlx/losses.pyD exo/inference/mlx/models/StableDiffusionPipeline.pyD exo/inference/mlx/models/__init__.pyD exo/inference/mlx/models/base.pyD exo/inference/mlx/models/deepseek_v2.pyD exo/inference/mlx/models/deepseek_v3.pyD exo/inference/mlx/models/gemma2.pyD exo/inference/mlx/models/llama.pyD exo/inference/mlx/models/llava.pyD exo/inference/mlx/models/phi3.pyD exo/inference/mlx/models/qwen2.pyD exo/inference/mlx/models/sd_models/clip.pyD exo/inference/mlx/models/sd_models/tokenizer.pyD exo/inference/mlx/models/sd_models/unet.pyD exo/inference/mlx/models/sd_models/vae.pyD exo/inference/mlx/perf_improvements.mdD exo/inference/mlx/sharded_inference_engine.pyD exo/inference/mlx/sharded_utils.pyD exo/inference/mlx/test_non_blocking.pyD exo/inference/mlx/test_sharded_model.pyD exo/inference/shard.pyD exo/inference/test_dummy_inference_engine.pyD exo/inference/test_inference_engine.pyD exo/inference/tinygrad/__init__.pyD exo/inference/tinygrad/inference.pyD exo/inference/tinygrad/losses.pyD exo/inference/tinygrad/models/__init__.pyD exo/inference/tinygrad/models/llama.pyD exo/inference/tinygrad/stateful_model.pyD exo/inference/tinygrad/tinygrad_helpers.pyD exo/inference/tokenizers.pyD exo/main.pyD exo/models.pyD exo/networking/__init__.pyD exo/networking/discovery.pyD exo/networking/grpc/__init__.pyD exo/networking/grpc/grpc_peer_handle.pyD exo/networking/grpc/grpc_server.pyD exo/networking/grpc/node_service.protoD exo/networking/grpc/node_service_pb2.pyD exo/networking/grpc/node_service_pb2_grpc.pyD exo/networking/manual/__init__.pyD exo/networking/manual/manual_discovery.pyD exo/networking/manual/network_topology_config.pyD exo/networking/manual/test_data/invalid_config.jsonD exo/networking/manual/test_data/invalid_json.jsonD exo/networking/manual/test_data/test_config.jsonD exo/networking/manual/test_data/test_config_single_node.jsonD exo/networking/manual/test_manual_discovery.pyD exo/networking/manual/test_network_topology_config.pyD exo/networking/peer_handle.pyD exo/networking/server.pyD exo/networking/tailscale/__init__.pyD exo/networking/tailscale/tailscale_discovery.pyD exo/networking/tailscale/tailscale_helpers.pyD exo/networking/tailscale/test_tailscale_discovery.pyD exo/networking/udp/__init__.pyD exo/networking/udp/test_udp_discovery.pyD exo/networking/udp/udp_discovery.pyD exo/orchestration/__init__.pyD exo/orchestration/node.pyD exo/orchestration/test_node.pyD exo/orchestration/tracing.pyD exo/test_callbacks.pyD exo/tinychat/common.cssD exo/tinychat/favicon.svgD exo/tinychat/index.cssD exo/tinychat/index.htmlD exo/tinychat/index.jsD exo/tinychat/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.jsD exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.jsD exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.jsD exo/tinychat/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.cssD exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.cssD exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.ttfD exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.woff2D exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.ttfD exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.woff2D exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.ttfD exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.woff2D exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.ttfD exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.woff2D exo/tinychat/static/fonts.googleapis.com/css2D exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.jsD exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.cssD exo/tinychat/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.jsD exo/tinychat/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.jsD exo/tinychat/static/unpkg.com/dompurify@3.1.5/dist/purify.min.jsD exo/tinychat/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.jsD exo/tinychat/static/unpkg.com/marked@13.0.0/marked.min.jsD exo/tinychat/update_deps.pyD exo/topology/__init__.pyD exo/topology/device_capabilities.pyD exo/topology/partitioning_strategy.pyD exo/topology/ring_memory_weighted_partitioning_strategy.pyD exo/topology/test_device_capabilities.pyD exo/topology/test_map_partitions.pyD exo/topology/test_ring_memory_weighted_partitioning_strategy.pyD exo/topology/topology.pyD exo/train/__init__.pyD exo/train/data/lora/test.jsonlD exo/train/data/lora/train.jsonlD exo/train/data/lora/valid.jsonlD exo/train/dataset.pyD exo/viz/__init__.pyD exo/viz/test_topology_viz.pyD exo/viz/topology_viz.pyD extra/dashboard/dashboard.pyD extra/dashboard/requirements.txtD extra/dashboard/sounds/gta5_wasted.mp3D extra/dashboard/sounds/pokemon_evolve.mp3D extra/line_counter.pyD extra/pipsize.pyD extra/start_openwebui.shD format.pyD install.shD scripts/build_exo.pyD scripts/compile_grpc.shD setup.pyD test/reconnect.shD test/test_model_helpers.pyD test/test_tokenizers.py
Diff
commit 0fcee70833a9d058881711543d9c04a1a60c7d02
Author: Jake Hillion <jake@hillion.co.uk>
Date: Wed Dec 17 15:25:55 2025 +0000
prep repo for v1
---
.circleci/config.yml | 376 ------
.gitattributes | 2 -
.github/bench.py | 401 -------
.github/bootstrap.sh | 330 ------
.github/optimize_performance.sh | 95 --
.github/workflows/bench_job.yml | 207 ----
.github/workflows/benchmarks.yml | 71 --
.gitignore | 175 ---
.style.yapf | 19 -
LICENSE | 675 -----------
README.md | 285 +----
configure_mlx.sh | 43 -
docs/exo-logo-black-bg.jpg | Bin 8089 -> 0 bytes
docs/exo-logo-transparent-black-text.png | 3 -
docs/exo-logo-transparent.png | 3 -
docs/exo-rounded.png | 3 -
docs/exo-screenshot.jpg | Bin 302470 -> 0 bytes
examples/astra/README.md | 3 -
examples/astra/astra.xcodeproj/project.pbxproj | 653 -----------
.../project.xcworkspace/contents.xcworkspacedata | 7 -
.../xcshareddata/IDEWorkspaceChecks.plist | 8 -
.../xcshareddata/swiftpm/Package.resolved | 33 -
.../AccentColor.colorset/Contents.json | 11 -
.../AppIcon.appiconset/Contents.json | 63 -
examples/astra/astra/Assets.xcassets/Contents.json | 6 -
examples/astra/astra/ContentView.swift | 729 ------------
.../Preview Assets.xcassets/Contents.json | 6 -
examples/astra/astra/astra.entitlements | 22 -
examples/astra/astra/astraApp.swift | 17 -
examples/astra/astraTests/astraTests.swift | 35 -
examples/astra/astraUITests/astraUITests.swift | 41 -
.../astraUITests/astraUITestsLaunchTests.swift | 32 -
examples/chatgpt_api.sh | 39 -
examples/function_calling.py | 111 --
exo/__init__.py | 1 -
exo/api/__init__.py | 1 -
exo/api/chatgpt_api.py | 645 -----------
exo/apputil/__init__.py | 1 -
exo/apputil/anim.py | 168 ---
exo/apputil/baseimages/image1.png | 3 -
exo/apputil/baseimages/image2.png | 3 -
exo/apputil/baseimages/image3.png | 3 -
exo/apputil/baseimages/image4.png | 3 -
exo/download/__init__.py | 0
exo/download/download_progress.py | 65 --
exo/download/hf/__init__.py | 0
exo/download/hf/hf_helpers.py | 98 --
exo/download/new_shard_download.py | 307 -----
exo/download/shard_download.py | 49 -
exo/download/test_new_shard_download.py | 14 -
exo/helpers.py | 372 ------
exo/inference/__init__.py | 0
exo/inference/debug_inference_engine.py | 58 -
exo/inference/dummy_inference_engine.py | 37 -
exo/inference/inference_engine.py | 77 --
exo/inference/mlx/__init__.py | 0
exo/inference/mlx/losses.py | 37 -
.../mlx/models/StableDiffusionPipeline.py | 307 -----
exo/inference/mlx/models/__init__.py | 0
exo/inference/mlx/models/base.py | 9 -
exo/inference/mlx/models/deepseek_v2.py | 127 --
exo/inference/mlx/models/deepseek_v3.py | 134 ---
exo/inference/mlx/models/gemma2.py | 118 --
exo/inference/mlx/models/llama.py | 125 --
exo/inference/mlx/models/llava.py | 585 ----------
exo/inference/mlx/models/phi3.py | 117 --
exo/inference/mlx/models/qwen2.py | 129 ---
exo/inference/mlx/models/sd_models/clip.py | 191 ---
exo/inference/mlx/models/sd_models/tokenizer.py | 131 ---
exo/inference/mlx/models/sd_models/unet.py | 629 ----------
exo/inference/mlx/models/sd_models/vae.py | 429 -------
exo/inference/mlx/perf_improvements.md | 7 -
exo/inference/mlx/sharded_inference_engine.py | 179 ---
exo/inference/mlx/sharded_utils.py | 257 -----
exo/inference/mlx/test_non_blocking.py | 81 --
exo/inference/mlx/test_sharded_model.py | 52 -
exo/inference/shard.py | 39 -
exo/inference/test_dummy_inference_engine.py | 47 -
exo/inference/test_inference_engine.py | 54 -
exo/inference/tinygrad/__init__.py | 0
exo/inference/tinygrad/inference.py | 157 ---
exo/inference/tinygrad/losses.py | 14 -
exo/inference/tinygrad/models/__init__.py | 0
exo/inference/tinygrad/models/llama.py | 327 ------
exo/inference/tinygrad/stateful_model.py | 22 -
exo/inference/tinygrad/tinygrad_helpers.py | 52 -
exo/inference/tokenizers.py | 63 -
exo/main.py | 402 -------
exo/models.py | 273 -----
exo/networking/__init__.py | 5 -
exo/networking/discovery.py | 17 -
exo/networking/grpc/__init__.py | 0
exo/networking/grpc/grpc_peer_handle.py | 226 ----
exo/networking/grpc/grpc_server.py | 173 ---
exo/networking/grpc/node_service.proto | 116 --
exo/networking/grpc/node_service_pb2.py | 90 --
exo/networking/grpc/node_service_pb2_grpc.py | 355 ------
exo/networking/manual/__init__.py | 0
exo/networking/manual/manual_discovery.py | 101 --
exo/networking/manual/network_topology_config.py | 31 -
.../manual/test_data/invalid_config.json | 17 -
exo/networking/manual/test_data/invalid_json.json | 0
exo/networking/manual/test_data/test_config.json | 32 -
.../manual/test_data/test_config_single_node.json | 18 -
exo/networking/manual/test_manual_discovery.py | 181 ---
.../manual/test_network_topology_config.py | 49 -
exo/networking/peer_handle.py | 56 -
exo/networking/server.py | 11 -
exo/networking/tailscale/__init__.py | 0
exo/networking/tailscale/tailscale_discovery.py | 178 ---
exo/networking/tailscale/tailscale_helpers.py | 125 --
.../tailscale/test_tailscale_discovery.py | 43 -
exo/networking/udp/__init__.py | 0
exo/networking/udp/test_udp_discovery.py | 77 --
exo/networking/udp/udp_discovery.py | 246 ----
exo/orchestration/__init__.py | 3 -
exo/orchestration/node.py | 628 ----------
exo/orchestration/test_node.py | 66 --
exo/orchestration/tracing.py | 166 ---
exo/test_callbacks.py | 50 -
exo/tinychat/common.css | 130 ---
exo/tinychat/favicon.svg | 25 -
exo/tinychat/index.css | 846 --------------
exo/tinychat/index.html | 454 --------
exo/tinychat/index.js | 889 --------------
.../toolkit@1.0.2/dist/cdn.min.js | 1 -
.../npm/@alpinejs/focus@3.x.x/dist/cdn.min.js | 15 -
.../npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js | 1 -
.../npm/purecss@3.0.0/build/base-min.css | 11 -
.../ajax/libs/font-awesome/6.5.2/css/all.min.css | 9 -
.../font-awesome/6.5.2/webfonts/fa-brands-400.ttf | Bin 209128 -> 0 bytes
.../6.5.2/webfonts/fa-brands-400.woff2 | Bin 117852 -> 0 bytes
.../font-awesome/6.5.2/webfonts/fa-regular-400.ttf | Bin 67860 -> 0 bytes
.../6.5.2/webfonts/fa-regular-400.woff2 | Bin 25392 -> 0 bytes
.../font-awesome/6.5.2/webfonts/fa-solid-900.ttf | Bin 420332 -> 0 bytes
.../font-awesome/6.5.2/webfonts/fa-solid-900.woff2 | Bin 156400 -> 0 bytes
.../6.5.2/webfonts/fa-v4compatibility.ttf | Bin 10832 -> 0 bytes
.../6.5.2/webfonts/fa-v4compatibility.woff2 | Bin 4792 -> 0 bytes
exo/tinychat/static/fonts.googleapis.com/css2 | 7 -
.../cdn-assets@11.9.0/highlight.min.js | 1213 --------------------
.../cdn-assets@11.9.0/styles/vs2015.min.css | 1 -
.../dist/alpine-autosize.min.js | 2 -
.../unpkg.com/alpinejs@3.x.x/dist/cdn.min.js | 5 -
.../unpkg.com/dompurify@3.1.5/dist/purify.min.js | 3 -
.../marked-highlight@2.1.2/lib/index.umd.js | 97 --
.../static/unpkg.com/marked@13.0.0/marked.min.js | 6 -
exo/tinychat/update_deps.py | 93 --
exo/topology/__init__.py | 0
exo/topology/device_capabilities.py | 294 -----
exo/topology/partitioning_strategy.py | 42 -
.../ring_memory_weighted_partitioning_strategy.py | 18 -
exo/topology/test_device_capabilities.py | 94 --
exo/topology/test_map_partitions.py | 81 --
...t_ring_memory_weighted_partitioning_strategy.py | 90 --
exo/topology/topology.py | 75 --
exo/train/__init__.py | 0
exo/train/data/lora/test.jsonl | 100 --
exo/train/data/lora/train.jsonl | 1000 ----------------
exo/train/data/lora/valid.jsonl | 100 --
exo/train/dataset.py | 80 --
exo/viz/__init__.py | 0
exo/viz/test_topology_viz.py | 129 ---
exo/viz/topology_viz.py | 377 ------
extra/dashboard/dashboard.py | 1147 ------------------
extra/dashboard/requirements.txt | 5 -
extra/dashboard/sounds/gta5_wasted.mp3 | 3 -
extra/dashboard/sounds/pokemon_evolve.mp3 | 3 -
extra/line_counter.py | 210 ----
extra/pipsize.py | 113 --
extra/start_openwebui.sh | 3 -
format.py | 33 -
install.sh | 11 -
scripts/build_exo.py | 64 --
scripts/compile_grpc.sh | 7 -
setup.py | 89 --
test/reconnect.sh | 21 -
test/test_model_helpers.py | 121 --
test/test_tokenizers.py | 42 -
178 files changed, 2 insertions(+), 22721 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index 0d80ff0c..00000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,376 +0,0 @@
-version: 2.1
-
-orbs:
- python: circleci/python@2
-
-commands:
- run_chatgpt_api_test:
- parameters:
- inference_engine:
- type: string
- model_id:
- type: string
- expected_output:
- type: string
- prompt:
- type: string
- steps:
- - run:
- name: Run chatgpt api integration test (<<parameters.inference_engine>>, <<parameters.model_id>>)
- command: |
- source env/bin/activate
-
- # Set CLANG=1 for tinygrad only
- if [ "<<parameters.inference_engine>>" = "tinygrad" ]; then
- pip install llvmlite
- export TOKENIZERS_PARALLELISM=true SUPPORT_BF16=0 CLANG=1
- fi
-
- # Start first instance
- EXO_HOME="$(pwd)/.exo_cache_node1" DEBUG_DISCOVERY=7 DEBUG=7 exo --inference-engine <<parameters.inference_engine>> \
- --node-id "node1" --listen-port 5678 --broadcast-port 5679 --chatgpt-api-port 8000 \
- --chatgpt-api-response-timeout 900 --disable-tui > output1.log &
- PID1=$!
- tail -f output1.log &
- TAIL1=$!
-
- # Start second instance
- EXO_HOME="$(pwd)/.exo_cache_node2" DEBUG_DISCOVERY=7 DEBUG=7 exo --inference-engine <<parameters.inference_engine>> \
- --node-id "node2" --listen-port 5679 --broadcast-port 5678 --chatgpt-api-port 8001 \
- --chatgpt-api-response-timeout 900 --disable-tui > output2.log &
- PID2=$!
- tail -f output2.log &
- TAIL2=$!
-
- # Remember to kill the tail processes at the end
- trap 'kill $TAIL1 $TAIL2' EXIT
-
- # Wait for discovery
- sleep 10
-
- # Function to check if processes are still running
- check_processes() {
- if ! kill -0 $PID1 2>/dev/null; then
- echo "First instance (PID $PID1) died unexpectedly. Log output:"
- cat output1.log
- exit 1
- fi
- if ! kill -0 $PID2 2>/dev/null; then
- echo "Second instance (PID $PID2) died unexpectedly. Log output:"
- cat output2.log
- exit 1
- fi
- }
-
- # Check processes before proceeding
- check_processes
-
- echo "Sending request to first instance..."
- response_1=$(curl -s http://localhost:8000/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "model": "<<parameters.model_id>>",
- "messages": [{"role": "user", "content": "<<parameters.prompt>>"}],
- "temperature": 0.7
- }')
- echo "Response 1: $response_1"
-
- # Check processes after first response
- check_processes
-
- echo "Sending request to second instance..."
- response_2=$(curl -s http://localhost:8001/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "model": "<<parameters.model_id>>",
- "messages": [{"role": "user", "content": "<<parameters.prompt>>"}],
- "temperature": 0.7
- }')
- echo "Response 2: $response_2"
-
- # Check processes after second response
- check_processes
-
- # Stop both instances
- kill $PID1 $PID2
-
- echo ""
- # Extract content using jq and check if it contains expected output
- content1=$(echo "$response_1" | jq -r '.choices[0].message.content')
- content2=$(echo "$response_2" | jq -r '.choices[0].message.content')
-
- if [[ "$content1" != *"<<parameters.expected_output>>"* ]] || [[ "$content2" != *"<<parameters.expected_output>>"* ]]; then
- echo "Test failed: Response does not match '<<parameters.expected_output>>'"
- echo "Response 1 content: $content1"
- echo ""
- echo "Response 2 content: $content2"
- echo "Output of first instance:"
- cat output1.log
- echo "Output of second instance:"
- cat output2.log
- exit 1
- else
- echo "Test passed: Response from both nodes matches '<<parameters.expected_output>>'"
- fi
-
-jobs:
- unit_test:
- macos:
- xcode: "16.0.0"
- resource_class: m2pro.large
- steps:
- - checkout
- - run:
- name: Set up Python
- command: |
- brew install python@3.12
- python3.12 -m venv env
- source env/bin/activate
- - run:
- name: Install dependencies
- command: |
- source env/bin/activate
- pip install --upgrade pip
- pip install .
- - run:
- name: Run tests
- command: |
- source env/bin/activate
- # set TEMPERATURE to 0 for deterministic sampling
- echo "Running inference engine tests..."
- METAL_DEVICE_WRAPPER_TYPE=1 METAL_DEBUG_ERROR_MODE=0 METAL_XCODE=1 TEMPERATURE=0 python3 -m exo.inference.test_inference_engine
- echo "Running tokenizer tests..."
- python3 ./test/test_tokenizers.py
- python3 ./test/test_model_helpers.py
-
- discovery_integration_test:
- macos:
- xcode: "16.0.0"
- steps:
- - checkout
- - run:
- name: Set up Python
- command: |
- brew install python@3.12
- python3.12 -m venv env
- source env/bin/activate
- - run:
- name: Install dependencies
- command: |
- source env/bin/activate
- pip install --upgrade pip
- pip install .
- - run:
- name: Run discovery integration test
- command: |
- source env/bin/activate
- DEBUG_DISCOVERY=7 DEBUG=7 exo --node-id "node1" --listen-port 5678 --broadcast-port 5679 --chatgpt-api-port 8000 --disable-tui > output1.log 2>&1 &
- PID1=$!
- DEBUG_DISCOVERY=7 DEBUG=7 exo --node-id "node2" --listen-port 5679 --broadcast-port 5678 --chatgpt-api-port 8001 --disable-tui > output2.log 2>&1 &
- PID2=$!
- sleep 10
- kill $PID1 $PID2
- if grep -q "Peer statuses: {\\'node2\\': \\'is_connected=True, health_check=True" output1.log && ! grep -q "Failed to connect peers:" output1.log && grep -q "Peer statuses: {\\'node1\\': \\'is_connected=True, health_check=True" output2.log && ! grep -q "Failed to connect peers:" output2.log; then
- echo "Test passed: Both instances discovered each other"
- exit 0
- else
- echo "Test failed: Devices did not discover each other"
- echo "Output of first instance:"
- cat output1.log
- echo "Output of second instance:"
- cat output2.log
- exit 1
- fi
-
- chatgpt_api_integration_test_mlx:
- macos:
- xcode: "16.0.0"
- resource_class: m2pro.large
- steps:
- - checkout
- - run:
- name: Set up Python
- command: |
- brew install python@3.12
- python3.12 -m venv env
- source env/bin/activate
- - run:
- name: Install dependencies
- command: |
- source env/bin/activate
- pip install --upgrade pip
- pip install .
- - run_chatgpt_api_test:
- inference_engine: mlx
- model_id: llama-3.2-1b
- prompt: "Keep responses concise. Who was the king of pop?"
- expected_output: "Michael Jackson"
-
- chatgpt_api_integration_test_dummy:
- macos:
- xcode: "16.0.0"
- resource_class: m2pro.large
- steps:
- - checkout
- - run:
- name: Set up Python
- command: |
- brew install python@3.12
- python3.12 -m venv env
- source env/bin/activate
- - run:
- name: Install dependencies
- command: |
- source env/bin/activate
- pip install --upgrade pip
- pip install .
- - run_chatgpt_api_test:
- inference_engine: dummy
- model_id: dummy
- prompt: "Dummy prompt."
- expected_output: "dummy"
-
- chatgpt_api_integration_test_tinygrad:
- macos:
- xcode: "16.0.0"
- resource_class: m2pro.large
- steps:
- - checkout
- - run:
- name: Set up Python
- command: |
- brew install python@3.12
- python3.12 -m venv env
- source env/bin/activate
- - run:
- name: Install dependencies
- command: |
- source env/bin/activate
- pip install --upgrade pip
- pip install .
- - run_chatgpt_api_test:
- inference_engine: tinygrad
- model_id: llama-3.2-1b
- prompt: "Keep responses concise. Who was the king of pop?"
- expected_output: "Michael Jackson"
-
- chatgpt_api_integration_test_tinygrad_linux:
- machine:
- image: ubuntu-2204:current
- resource_class: xlarge
- steps:
- - checkout
- - run:
- name: Set up Python
- command: |
- export DEBIAN_FRONTEND=noninteractive
- export DEBCONF_NONINTERACTIVE_SEEN=true
- sudo apt-get update
- sudo add-apt-repository -y ppa:deadsnakes/ppa
- sudo apt-get update
- sudo apt-get install -y python3.12 python3.12-venv clang
- python3.12 -m venv env
- source env/bin/activate
- - run:
- name: Install dependencies
- command: |
- source env/bin/activate
- pip install --upgrade pip
- pip install .
- - run_chatgpt_api_test:
- inference_engine: tinygrad
- model_id: llama-3.2-1b
- prompt: "Keep responses concise. Who was the king of pop?"
- expected_output: "Michael Jackson"
-
- measure_pip_sizes:
- macos:
- xcode: "16.0.0"
- steps:
- - checkout
- - run:
- name: Set up Python
- command: |
- brew install python@3.12
- python3.12 -m venv env
- source env/bin/activate
- - run:
- name: Install dependencies and measure sizes
- command: |
- source env/bin/activate
- pip install --upgrade pip
- pip install .
- python ./extra/pipsize.py --json ./pipsize.json
- - store_artifacts:
- path: ./pipsize.json
- destination: pip-sizes.json
-
- check_line_count:
- docker:
- - image: cimg/python:3.10
- steps:
- - checkout
-
- - run:
- name: Setup git for PR comparison
- command: |
- if [[ -n "$CIRCLE_PULL_REQUEST" ]]; then
- PR_NUMBER=$(echo $CIRCLE_PULL_REQUEST | rev | cut -d'/' -f1 | rev)
- BASE_BRANCH=$(curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
- "https://circleci.com/api/v2/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pipeline/$CIRCLE_WORKFLOW_ID" \
- | jq -r '.target_branch')
-
- git clone -b $BASE_BRANCH --single-branch \
- https://github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME.git \
- base_branch
- fi
-
- - run:
- name: Install dependencies
- command: |
- python -m pip install --upgrade pip
- pip install tabulate
-
- - run:
- name: Run line count check
- command: |
- if [[ -n "$CIRCLE_PULL_REQUEST" ]]; then
- python extra/line_counter.py base_branch .
- else
- python extra/line_counter.py .
- fi
-
- - store_artifacts:
- path: line-count-snapshot.json
- destination: line-count-snapshot.json
-
- - store_artifacts:
- path: line-count-diff.json
- destination: line-count-diff.json
-
- - run:
- name: Create test results directory
- command: |
- mkdir -p test-results/line-count
- cp line-count-*.json test-results/line-count/
-
- - store_test_results:
- path: test-results
-
-workflows:
- version: 2
- build_and_test:
- jobs:
- - check_line_count:
- filters:
- branches:
- only: /.*/
- tags:
- only: /.*/
- - unit_test
- - discovery_integration_test
- - chatgpt_api_integration_test_mlx
- - chatgpt_api_integration_test_tinygrad
- - chatgpt_api_integration_test_tinygrad_linux
- - chatgpt_api_integration_test_dummy
- - measure_pip_sizes
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index ab26661d..00000000
--- a/.gitattributes
+++ /dev/null
@@ -1,2 +0,0 @@
-*.mp3 filter=lfs diff=lfs merge=lfs -text
-*.png filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/bench.py b/.github/bench.py
deleted file mode 100644
index 9bc52e89..00000000
--- a/.github/bench.py
+++ /dev/null
@@ -1,401 +0,0 @@
-import aiohttp
-import asyncio
-import time
-import json
-import os
-import boto3
-from typing import Dict, Any
-from datetime import datetime
-import subprocess
-import psutil
-import platform
-from pathlib import Path
-
-
-def check_system_state():
- print("\n=== System State Check ===", flush=True)
-
- # Add macOS-specific checks
- try:
- # Check powermetrics with sudo
- try:
- power_metrics = subprocess.run(
- ['sudo', 'powermetrics', '-n', '1', '-i', '1000', '--samplers', 'cpu_power'],
- capture_output=True, text=True
- )
- print("\nPower Metrics:", power_metrics.stdout, flush=True)
- except Exception as e:
- print(f"Error getting power metrics: {e}", flush=True)
-
- # Check thermal state
- thermal_state = subprocess.run(['pmset', '-g', 'therm'], capture_output=True, text=True)
- print("\nThermal State:", thermal_state.stdout, flush=True)
-
- # Check if running under Rosetta
- arch = subprocess.run(['arch'], capture_output=True, text=True)
- print("\nArchitecture:", arch.stdout, flush=True)
-
- # Check MLX compilation mode - only if mlx is available
- try:
- import mlx.core as mx
- if hasattr(mx, 'build_info'):
- print("\nMLX Build Info:", mx.build_info(), flush=True)
- else:
- print("\nMLX Build Info: Not available in this version", flush=True)
- except ImportError:
- print("\nMLX: Not installed", flush=True)
- except Exception as e:
- print(f"\nError checking MLX: {e}", flush=True)
-
- except Exception as e:
- print(f"Error in macOS checks: {e}", flush=True)
-
- # CPU Info
- print("\nCPU Information:", flush=True)
- try:
- if platform.system() == 'Darwin' and platform.processor() == 'arm':
- # Use sysctl for Apple Silicon Macs
- cpu_info = subprocess.run(['sysctl', 'machdep.cpu'], capture_output=True, text=True)
- if cpu_info.returncode == 0:
- print(f"CPU Info (Apple Silicon):", cpu_info.stdout, flush=True)
-
- # Parse powermetrics output for clearer CPU frequency display
- try:
- power_metrics = subprocess.run(
- ['sudo', 'powermetrics', '-n', '1', '-i', '100', '--samplers', 'cpu_power'],
- capture_output=True, text=True
- )
- if power_metrics.returncode == 0:
- output = power_metrics.stdout
- print("\nDetailed CPU Frequency Information:")
-
- # Extract cluster frequencies and max frequencies
- current_cluster = None
- max_freqs = {'E': 0, 'P0': 0, 'P1': 0}
-
- for line in output.split('\n'):
- # Track which cluster we're processing
- if "E-Cluster" in line:
- current_cluster = 'E'
- elif "P0-Cluster" in line:
- current_cluster = 'P0'
- elif "P1-Cluster" in line:
- current_cluster = 'P1'
-
- # Get current frequencies
- if "HW active frequency:" in line:
- freq = line.split(':')[1].strip()
- if freq != "0 MHz":
- print(f"Current {current_cluster}-Cluster Frequency: {freq}")
-
- # Get max frequencies from residency lines
- if current_cluster and "active residency:" in line and "MHz:" in line:
- try:
- # Extract all frequency values
- freqs = []
- parts = line.split('MHz:')[:-1] # Skip last part as it's not a frequency
- for part in parts:
- freq_str = part.split()[-1]
- try:
- freq = float(freq_str)
- freqs.append(freq)
- except ValueError:
- continue
- if freqs:
- max_freqs[current_cluster] = max(max_freqs[current_cluster], max(freqs))
- except Exception:
- continue
-
- # Print max frequencies
- print("\nMaximum Available Frequencies:")
- for cluster, max_freq in max_freqs.items():
- if max_freq > 0:
- print(f"{cluster}-Cluster Max: {max_freq:.0f} MHz")
-
- except Exception as e:
- print(f"Error parsing powermetrics: {e}", flush=True)
- else:
- # Use psutil for other systems
- cpu_freq = psutil.cpu_freq()
- print(f"CPU Frequency - Current: {cpu_freq.current:.2f}MHz, Min: {cpu_freq.min:.2f}MHz, Max: {cpu_freq.max:.2f}MHz", flush=True)
-
- print(f"\nCPU Usage per Core: {psutil.cpu_percent(percpu=True)}%", flush=True)
-
- # Check if running in low power mode
- power_mode = subprocess.run(['pmset', '-g'], capture_output=True, text=True)
- print("\nPower Settings:", power_mode.stdout, flush=True)
- except Exception as e:
- print(f"Error getting CPU info: {e}", flush=True)
-
- # Memory Info
- print("\nMemory Information:", flush=True)
- try:
- mem = psutil.virtual_memory()
- print(f"Total: {mem.total/1024/1024/1024:.2f}GB", flush=True)
- print(f"Available: {mem.available/1024/1024/1024:.2f}GB", flush=True)
- print(f"Used: {mem.used/1024/1024/1024:.2f}GB ({mem.percent}%)", flush=True)
-
- # Check swap
- swap = psutil.swap_memory()
- print(f"Swap Used: {swap.used/1024/1024/1024:.2f}GB of {swap.total/1024/1024/1024:.2f}GB", flush=True)
- except Exception as e:
- print(f"Error getting memory info: {e}", flush=True)
-
- # GPU Info
- print("\nGPU Information:", flush=True)
- try:
- # Check MLX GPU settings
- print("MLX Environment Variables:", flush=True)
- mlx_vars = {k: v for k, v in os.environ.items() if k.startswith('MLX')}
- print(json.dumps(mlx_vars, indent=2), flush=True)
-
- # Check Metal GPU memory allocation
- gpu_mem = subprocess.run(['sysctl', 'iogpu'], capture_output=True, text=True)
- print("GPU Memory Settings:", gpu_mem.stdout, flush=True)
- except Exception as e:
- print(f"Error getting GPU info: {e}", flush=True)
-
- # Process Priority
- print("\nProcess Priority Information:", flush=True)
- try:
- current_process = psutil.Process()
- print(f"Process Nice Value: {current_process.nice()}", flush=True)
- # Only try to get ionice if the platform supports it
- if hasattr(current_process, 'ionice'):
- print(f"Process IO Nice Value: {current_process.ionice()}", flush=True)
- except Exception as e:
- print(f"Error getting process priority info: {e}", flush=True)
-
- # System Load
- print("\nSystem Load:", flush=True)
- try:
- load_avg = psutil.getloadavg()
- print(f"Load Average: {load_avg}", flush=True)
-
- # Get top processes by CPU and Memory
- print("\nTop Processes by CPU Usage:", flush=True)
- processes = []
- for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']):
- try:
- pinfo = proc.info
- if pinfo['cpu_percent'] is not None and pinfo['memory_percent'] is not None:
- processes.append(pinfo)
- except (psutil.NoSuchProcess, psutil.AccessDenied):
- continue
-
- # Sort and display top 5 CPU-consuming processes
- sorted_by_cpu = sorted(processes, key=lambda x: x['cpu_percent'] or 0, reverse=True)[:5]
- for proc in sorted_by_cpu:
- print(f"PID: {proc['pid']}, Name: {proc['name']}, CPU: {proc['cpu_percent']}%, Memory: {proc['memory_percent']:.1f}%")
- except Exception as e:
- print(f"Error getting system load info: {e}", flush=True)
-
- print("\n=== End System State Check ===\n", flush=True)
-
-
-def check_gpu_access():
- try:
- # Check if MLX can see the GPU
- import mlx.core as mx
- print("MLX device info:", mx.default_device())
-
- # Check Metal device availability
- result = subprocess.run(['system_profiler', 'SPDisplaysDataType'], capture_output=True, text=True)
- print("GPU Info:", result.stdout)
- except Exception as e:
- print(f"Failed to check GPU access: {e}")
-
-
-async def measure_performance(api_endpoint: str, prompt: str, model: str) -> Dict[str, Any]:
- """
- Measures the performance of an API endpoint by sending a prompt and recording metrics.
-
- Args:
- api_endpoint (str): The API endpoint URL.
- prompt (str): The prompt to send to the API.
-
- Returns:
- Dict[str, Any]: A dictionary containing performance metrics or error information.
- """
-
- results = {
- 'model': model,
- 'run_id': os.environ.get('GITHUB_RUN_ID', 'unknown'),
- 'branch': os.environ.get('GITHUB_REF_NAME', 'unknown'),
- 'commit': os.environ.get('GITHUB_SHA', 'unknown'),
- 'configuration': json.loads(os.environ.get('HARDWARE_CONFIG', '{}'))
- }
-
- # Get token count
- session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=600, connect=10, sock_read=600, sock_connect=10))
- try:
- response = await session.post(
- "http://localhost:52415/v1/chat/token/encode",
- json={
- "model": model,
- "messages": [{"role": "user", "content": prompt}]
- }
- )
- response.raise_for_status()
- token_data = await response.json()
- results['prompt_len'] = token_data['num_tokens']
- except Exception as e:
- await session.close()
- raise RuntimeError(f"Failed to get token count: {str(e)}")
-
- # Measure completion performance
- try:
- start_time = time.time()
- response = await session.post(
- api_endpoint,
- json={
- "model": model,
- "messages": [{"role": "user", "content": prompt}],
- "temperature": 0,
- "stream": True
- }
- )
- response.raise_for_status()
-
- first_token_time = None
- total_tokens = 0
-
- async for line in response.content.iter_chunks():
- line = line[0].decode('utf-8').strip()
- if not line.startswith('data: '):
- continue
-
- data = json.loads(line[6:]) # Skip 'data: ' prefix
- if content := data.get('choices', [{}])[0].get('delta', {}).get('content'):
- print(f"Received content: {content}", flush=True)
- if first_token_time is None:
- first_token_time = time.time()
- ttft = first_token_time - start_time
- results.update({
- 'ttft': ttft,
- 'prompt_tps': results['prompt_len'] / ttft
- })
- total_tokens += 1
-
- total_time = time.time() - start_time
- results.update({
- 'generation_tps': total_tokens / total_time,
- 'response_len': total_tokens,
- 'total_time': total_time
- })
-
- except Exception as e:
- raise RuntimeError(f"Performance measurement failed: {str(e)}")
- finally:
- await session.close()
-
- return results
-
-
-async def main() -> None:
- api_endpoint = "http://localhost:52415/v1/chat/completions"
-
- # Define prompts
- prompt_warmup = "what is the capital of France?"
- prompt_essay = "write an essay about cats"
-
- model = os.environ.get('model', 'llama-3.2-1b')
- # Warmup request
- print("\nPerforming warmup request...", flush=True)
- try:
- warmup_results = await measure_performance(api_endpoint, prompt_warmup, model)
- print("Warmup completed successfully", flush=True)
- except Exception as e:
- print(f"Warmup request failed: {e}", flush=True)
-
- # Measure performance for the essay prompt
- print("\nMeasuring performance for the essay prompt...", flush=True)
- results = await measure_performance(api_endpoint, prompt_essay, model)
-
- try:
- s3_client = boto3.client(
- 's3',
- aws_access_key_id=os.environ.get('aws_access_key_id'),
- aws_secret_access_key=os.environ.get('aws_secret_key')
- )
- job_name = os.environ.get('GITHUB_JOB')
-
- # Create S3 key with timestamp and commit info
- now = datetime.utcnow()
- timestamp = now.strftime('%H-%M-%S')
- commit_sha = os.environ.get('GITHUB_SHA', 'unknown')[:7]
- s3_key = f"{job_name}/{model}/{now.year}/{now.month}/{now.day}/{timestamp}_{commit_sha}.json"
-
- # Upload to S3
- s3_client.put_object(
- Bucket='exo-benchmarks',
- Key=s3_key,
- Body=json.dumps(results),
- ContentType='application/json'
- )
- print(f"Performance metrics uploaded to S3: s3://exo-benchmarks/{s3_key}", flush=True)
- except Exception as e:
- print(f"Failed to upload metrics to S3: {e}", flush=True)
-
- # Optionally print the metrics for visibility
- print("Performance metrics:", flush=True)
- print(json.dumps(results, indent=4), flush=True)
-
-
-def optimize_system_performance():
- """Set optimal system performance settings before running benchmark."""
- try:
- # Try to set high performance power mode
- subprocess.run(['sudo', 'pmset', '-a', 'powermode', '2'], check=False)
-
- # Ensure MLX uses performance cores and GPU
- os.environ['MLX_FORCE_P_CORES'] = '1'
- os.environ['MLX_METAL_PREWARM'] = '1'
- os.environ['MLX_USE_GPU'] = '1'
-
- # Set process priority
- current_process = psutil.Process()
- try:
- # Set highest priority
- subprocess.run(['sudo', 'renice', '-n', '-20', '-p', str(current_process.pid)], check=False)
-
- # Print current process state
- print("\nProcess State Before Benchmark:", flush=True)
- proc_info = subprocess.run(
- ['ps', '-o', 'pid,ppid,user,%cpu,%mem,nice,stat,pri,command', '-p', str(current_process.pid)],
- capture_output=True, text=True
- )
- print(proc_info.stdout, flush=True)
-
- # Verify power mode
- power_info = subprocess.run(['pmset', '-g'], capture_output=True, text=True)
- if 'powermode 0' in power_info.stdout:
- print("\nWarning: System still in normal power mode. Trying to set high performance mode again...", flush=True)
- subprocess.run(['sudo', 'pmset', '-a', 'powermode', '2'], check=False)
-
- except Exception as e:
- print(f"Warning: Could not set process priority: {e}", flush=True)
-
- except Exception as e:
- print(f"Warning: Could not optimize system performance: {e}", flush=True)
-
- # Print optimization status
- print("\nOptimization Settings:", flush=True)
- print("MLX Environment Variables:", flush=True)
- for var in ['MLX_FORCE_P_CORES', 'MLX_METAL_PREWARM', 'MLX_USE_GPU']:
- print(f"{var}: {os.environ.get(var, 'Not set')}", flush=True)
-
- try:
- nice_value = psutil.Process().nice()
- print(f"Process Nice Value: {nice_value}", flush=True)
- if nice_value != -20:
- print("Warning: Process not running at highest priority", flush=True)
- except Exception:
- pass
-
-
-if __name__ == "__main__":
- check_system_state()
- check_gpu_access()
- optimize_system_performance()
- asyncio.run(main())
diff --git a/.github/bootstrap.sh b/.github/bootstrap.sh
deleted file mode 100755
index 03b13079..00000000
--- a/.github/bootstrap.sh
+++ /dev/null
@@ -1,330 +0,0 @@
-#!/bin/bash
-set -e
-
-command_exists() {
- command -v "$1" >/dev/null 2>&1
-}
-
-log() {
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
-}
-
-if [ "$EUID" -eq 0 ]; then
- log "Please do not run as root. Run as regular user with sudo access."
- exit 1
-fi
-
-# Check for required arguments
-if [ -z "$1" ]; then
- log "Error: Runner token is required"
- log "Usage: $0 <runner-token> [tailscale-auth-key]"
- exit 1
-fi
-
-RUNNER_TOKEN=$1
-TAILSCALE_AUTH_KEY=$2
-REPO="exo-explore/exo"
-
-# Add sudoers configuration
-log "Configuring sudo access..."
-SUDOERS_CONTENT="$(whoami) ALL=(ALL) NOPASSWD: ALL"
-echo "$SUDOERS_CONTENT" | sudo tee /etc/sudoers.d/github-runner > /dev/null
-sudo chmod 440 /etc/sudoers.d/github-runner
-
-log "Configuring privacy permissions..."
-sudo tccutil reset All
-sudo tccutil reset SystemPolicyAllFiles
-sudo tccutil reset SystemPolicyNetworkVolumes
-
-# Configure power management for maximum performance
-log "Configuring power management..."
-sudo pmset -a powermode 2 # Force highest performance mode
-sudo pmset -a gpuswitch 2 # Force discrete/high-performance GPU
-sudo pmset -a lowpowermode 0
-sudo pmset -a lessbright 0
-sudo pmset -a disablesleep 1
-sudo pmset -a sleep 0
-sudo pmset -a hibernatemode 0
-sudo pmset -a autopoweroff 0
-sudo pmset -a standby 0
-sudo pmset -a powernap 0
-
-# For Python specifically
-PYTHON_PATH="/opt/homebrew/bin/python3.12"
-sudo chmod 755 "$PYTHON_PATH"
-
-# Add to firewall
-log "Configuring firewall access..."
-sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add "$PYTHON_PATH"
-sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblock "$PYTHON_PATH"
-
-# Set Homebrew paths based on architecture
-if [ "$(uname -p)" = "arm" ]; then
- BREW_PREFIX="/opt/homebrew"
-else
- BREW_PREFIX="/usr/local"
-fi
-
-# Install Homebrew if not present
-if ! command_exists brew; then
- log "Installing Homebrew..."
- /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zshrc
- eval "$(/opt/homebrew/bin/brew shellenv)"
-fi
-
-# Install required packages
-log "Installing required packages..."
-export HOMEBREW_NO_AUTO_UPDATE=1
-brew install python@3.12 coreutils
-
-# Optional Tailscale setup if auth key is provided
-if [ -n "$TAILSCALE_AUTH_KEY" ]; then
- log "Installing and configuring Tailscale..."
- brew install --quiet tailscale
- sudo brew services stop tailscale 2>/dev/null || true
- sudo rm -f /var/db/tailscale/tailscaled.state 2>/dev/null || true
- sudo brew services start tailscale
- sleep 2
- sudo tailscale up --authkey=$TAILSCALE_AUTH_KEY
-
- # Enable SSH and Screen Sharing
- log "Enabling remote access services..."
- sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist
- sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart \
- -activate \
- -configure -access -on \
- -configure -allowAccessFor -allUsers \
- -configure -restart -agent -privs -all
-
- # Create launch daemon for remote access
- sudo bash -c 'cat > /Library/LaunchDaemons/com.remote.access.setup.plist' << 'EOL'
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>Label</key>
- <string>com.remote.access.setup</string>
- <key>ProgramArguments</key>
- <array>
- <string>/bin/bash</string>
- <string>-c</string>
- <string>
- launchctl load -w /System/Library/LaunchDaemons/ssh.plist;
- /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -activate -configure -access -on
- </string>
- </array>
- <key>RunAtLoad</key>
- <true/>
-</dict>
-</plist>
-EOL
-
- sudo chmod 644 /Library/LaunchDaemons/com.remote.access.setup.plist
- sudo launchctl load -w /Library/LaunchDaemons/com.remote.access.setup.plist
-fi
-
-# Configure GitHub Actions Runner
-log "Gathering system metadata..."
-MACHINE_NAME=$(scutil --get ComputerName)
-MACHINE_NAME="runner-$(echo -n "$MACHINE_NAME" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]-')"
-
-# Enhanced Apple Silicon detection
-MACHINE_INFO=$(system_profiler SPHardwareDataType)
-CHIP_FULL=$(echo "$MACHINE_INFO" | grep "Chip" | cut -d: -f2 | xargs)
-if [[ $CHIP_FULL =~ "Apple" ]]; then
- CHIP_MODEL=$(echo "$CHIP_FULL" | sed 's/^Apple //' | tr -d ' ' | tr '[:lower:]' '[:upper:]')
- GPU_CORES=$(ioreg -l | grep "gpu-core-count" | awk -F'= ' '{print $2}')
- if [ -z "$GPU_CORES" ]; then
- GPU_CORES="N/A"
- fi
-else
- CHIP_MODEL="Intel"
- GPU_CORES="N/A"
-fi
-
-MEMORY=$(($(sysctl -n hw.memsize) / 1024 / 1024 / 1024))
-
-# Set up GitHub Runner
-RUNNER_DIR="$HOME/actions-runner"
-
-# Check if runner is already configured
-if [ -f "$RUNNER_DIR/.runner" ]; then
- log "Runner already configured. Stopping existing service..."
- sudo launchctl unload /Library/LaunchDaemons/com.github.runner.plist 2>/dev/null || true
-fi
-
-# Create runner directory if it doesn't exist
-mkdir -p "$RUNNER_DIR"
-cd "$RUNNER_DIR"
-
-CUSTOM_LABELS="self-hosted,macos,arm64,${CHIP_MODEL}_GPU${GPU_CORES}_${MEMORY}GB"
-
-# Only download and extract if not already present or if forced
-if [ ! -f "$RUNNER_DIR/run.sh" ] || [ "${FORCE_SETUP:-false}" = "true" ]; then
- log "Downloading GitHub Actions runner..."
- RUNNER_VERSION=$(curl -s https://api.github.com/repos/actions/runner/releases/latest | grep '"tag_name":' | cut -d'"' -f4)
- curl -o actions-runner.tar.gz -L "https://github.com/actions/runner/releases/download/${RUNNER_VERSION}/actions-runner-osx-arm64-${RUNNER_VERSION#v}.tar.gz"
- tar xzf actions-runner.tar.gz
- rm actions-runner.tar.gz
-else
- log "Runner already downloaded, skipping download step"
-fi
-
-log "Configuring runner with labels: $CUSTOM_LABELS"
-./config.sh --unattended \
- --url "https://github.com/${REPO}" \
- --token "${RUNNER_TOKEN}" \
- --name "${MACHINE_NAME}" \
- --labels "${CUSTOM_LABELS}" \
- --work "_work"
-
-# Set optimal performance settings
-log "Configuring system for optimal performance..."
-
-# Configure CPU performance
-log "Setting CPU performance controls..."
-# Disable timer coalescing
-sudo sysctl -w kern.timer.coalescing_enabled=0
-sudo sysctl -w kern.timer_coalesce_bg_scale=-5
-sudo sysctl -w kern.timer_resort_threshold_ns=0
-# Set minimum timer intervals
-sudo sysctl -w kern.wq_max_timer_interval_usecs=1000
-sudo sysctl -w kern.timer_coalesce_bg_ns_max=1000
-# Set minimum timer coalescing for all tiers
-sudo sysctl -w kern.timer_coalesce_tier0_scale=-5
-sudo sysctl -w kern.timer_coalesce_tier0_ns_max=1000
-sudo sysctl -w kern.timer_coalesce_tier1_scale=-5
-sudo sysctl -w kern.timer_coalesce_tier1_ns_max=1000
-sudo sysctl -w kern.timer_coalesce_tier2_scale=-5
-sudo sysctl -w kern.timer_coalesce_tier2_ns_max=1000
-sudo sysctl -w kern.timer_coalesce_tier3_scale=-5
-sudo sysctl -w kern.timer_coalesce_tier3_ns_max=1000
-sudo sysctl -w kern.timer_coalesce_tier4_scale=-5
-sudo sysctl -w kern.timer_coalesce_tier4_ns_max=1000
-# Disable QoS restrictions
-sudo sysctl -w net.qos.policy.restricted=0
-sudo sysctl -w net.qos.policy.restrict_avapps=0
-sudo sysctl -w net.qos.policy.wifi_enabled=0
-sudo sysctl -w net.qos.policy.capable_enabled=0
-# Set scheduler parameters
-sudo sysctl -w kern.sched_rt_avoid_cpu0=0
-sudo sysctl -w debug.sched=2
-sudo sysctl -w net.pktsched.netem.sched_output_ival_ms=1
-
-# Clean up any existing runner services
-log "Cleaning up existing runner services..."
-for service in com.github.runner com.github.runner.monitor com.github.runner.cpuaffinity com.github.runner.affinity; do
- sudo launchctl bootout system/$service 2>/dev/null || true
- sudo rm -f /Library/LaunchDaemons/$service.plist
-done
-
-# Create a simple runner service configuration
-sudo tee /Library/LaunchDaemons/com.github.runner.plist > /dev/null << EOF
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>Label</key>
- <string>com.github.runner</string>
- <key>UserName</key>
- <string>$(whoami)</string>
- <key>GroupName</key>
- <string>staff</string>
- <key>WorkingDirectory</key>
- <string>$RUNNER_DIR</string>
- <key>ProgramArguments</key>
- <array>
- <string>$RUNNER_DIR/run.sh</string>
- </array>
- <key>RunAtLoad</key>
- <true/>
- <key>KeepAlive</key>
- <dict>
- <key>SuccessfulExit</key>
- <false/>
- <key>Crashed</key>
- <true/>
- </dict>
- <key>ProcessType</key>
- <string>Interactive</string>
- <key>LowPriorityIO</key>
- <false/>
- <key>AbandonProcessGroup</key>
- <false/>
- <key>EnableTransactions</key>
- <true/>
- <key>ThrottleInterval</key>
- <integer>0</integer>
- <key>HardResourceLimits</key>
- <dict>
- <key>NumberOfFiles</key>
- <integer>524288</integer>
- <key>MemoryLock</key>
- <integer>-1</integer>
- </dict>
- <key>SoftResourceLimits</key>
- <dict>
- <key>NumberOfFiles</key>
- <integer>524288</integer>
- <key>MemoryLock</key>
- <integer>-1</integer>
- </dict>
- <key>QOSClass</key>
- <string>User-Interactive</string>
- <key>StandardOutPath</key>
- <string>$RUNNER_DIR/_diag/runner.log</string>
- <key>StandardErrorPath</key>
- <string>$RUNNER_DIR/_diag/runner.err</string>
- <key>EnvironmentVariables</key>
- <dict>
- <key>PATH</key>
- <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
- </dict>
- <key>Nice</key>
- <integer>-20</integer>
- </dict>
-</plist>
-EOF
-
-# Set proper permissions for the LaunchDaemon
-sudo chown root:wheel /Library/LaunchDaemons/com.github.runner.plist
-sudo chmod 644 /Library/LaunchDaemons/com.github.runner.plist
-
-# Remove any existing service
-sudo launchctl bootout system/com.github.runner 2>/dev/null || true
-
-# Load the new service using bootstrap
-sudo launchctl bootstrap system /Library/LaunchDaemons/com.github.runner.plist
-
-# Add Runner.Listener permissions (after runner installation)
-RUNNER_PATH="$RUNNER_DIR/bin/Runner.Listener"
-sudo chmod 755 "$RUNNER_PATH"
-sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add "$RUNNER_PATH"
-sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblock "$RUNNER_PATH"
-
-# Create connection info file if Tailscale is configured
-if [ -n "$TAILSCALE_AUTH_KEY" ]; then
- TAILSCALE_IP=$(tailscale ip)
- cat > "$HOME/remote_access_info.txt" << EOL
-Mac Remote Access Information
-============================
-Computer Name: $MACHINE_NAME
-Username: $USER
-Tailscale IP: $TAILSCALE_IP
-
-SSH Command: ssh $USER@$TAILSCALE_IP
-Screen Sharing: vnc://$TAILSCALE_IP
-EOL
- chmod 600 "$HOME/remote_access_info.txt"
-fi
-
-log "Verifying runner service status..."
-if sudo launchctl list | grep com.github.runner > /dev/null; then
- log "GitHub Actions runner service is running successfully!"
- log "Runner labels: $CUSTOM_LABELS"
- [ -n "$TAILSCALE_AUTH_KEY" ] && log "Remote access details saved to: $HOME/remote_access_info.txt"
-else
- log "Error: Failed to start GitHub Actions runner service"
- exit 1
-fi
\ No newline at end of file
diff --git a/.github/optimize_performance.sh b/.github/optimize_performance.sh
deleted file mode 100755
index 7b5c77be..00000000
--- a/.github/optimize_performance.sh
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/bin/bash
-set -e
-
-# Function to log with timestamp
-log() {
- echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
-}
-
-log "Applying comprehensive performance optimizations..."
-
-# System-wide power management
-log "Configuring power management..."
-sudo pmset -a lessbright 0
-sudo pmset -a disablesleep 1
-sudo pmset -a sleep 0
-sudo pmset -a hibernatemode 0
-sudo pmset -a autopoweroff 0
-sudo pmset -a standby 0
-sudo pmset -a powernap 0
-sudo pmset -a proximitywake 0
-sudo pmset -a tcpkeepalive 1
-sudo pmset -a powermode 2
-sudo pmset -a gpuswitch 2
-sudo pmset -a displaysleep 0
-sudo pmset -a disksleep 0
-
-# Memory and kernel optimizations
-log "Configuring memory and kernel settings..."
-sudo sysctl -w kern.memorystatus_purge_on_warning=0
-sudo sysctl -w kern.memorystatus_purge_on_critical=0
-sudo sysctl -w kern.timer.coalescing_enabled=0
-
-# Metal and GPU optimizations
-log "Configuring Metal and GPU settings..."
-defaults write com.apple.CoreML MPSEnableGPUValidation -bool false
-defaults write com.apple.CoreML MPSEnableMetalValidation -bool false
-defaults write com.apple.CoreML MPSEnableGPUDebug -bool false
-defaults write com.apple.Metal GPUDebug -bool false
-defaults write com.apple.Metal GPUValidation -bool false
-defaults write com.apple.Metal MetalValidation -bool false
-defaults write com.apple.Metal MetalCaptureEnabled -bool false
-defaults write com.apple.Metal MTLValidationBehavior -string "Disabled"
-defaults write com.apple.Metal EnableMTLDebugLayer -bool false
-defaults write com.apple.Metal MTLDebugLevel -int 0
-defaults write com.apple.Metal PreferIntegratedGPU -bool false
-defaults write com.apple.Metal ForceMaximumPerformance -bool true
-defaults write com.apple.Metal MTLPreferredDeviceGPUFrame -bool true
-
-# Create MPS cache directory with proper permissions
-sudo mkdir -p /tmp/mps_cache
-sudo chmod 777 /tmp/mps_cache
-
-# Process and resource limits
-log "Configuring process limits..."
-sudo launchctl limit maxfiles 524288 524288
-ulimit -n 524288 || log "Warning: Could not set file descriptor limit"
-ulimit -c 0
-ulimit -l unlimited || log "Warning: Could not set memory lock limit"
-
-# Export performance-related environment variables
-cat << 'EOF' > /tmp/performance_env.sh
-# Metal optimizations
-export MTL_DEBUG_LAYER=0
-export METAL_DEVICE_WRAPPER_TYPE=1
-export METAL_DEBUG_ERROR_MODE=0
-export METAL_FORCE_PERFORMANCE_MODE=1
-export METAL_DEVICE_PRIORITY=high
-export METAL_MAX_COMMAND_QUEUES=1024
-export METAL_LOAD_LIMIT=0
-export METAL_VALIDATION_ENABLED=0
-export METAL_ENABLE_VALIDATION_LAYER=0
-export OBJC_DEBUG_MISSING_POOLS=NO
-export MPS_CACHEDIR=/tmp/mps_cache
-
-# MLX optimizations
-export MLX_USE_GPU=1
-export MLX_METAL_COMPILE_ASYNC=1
-export MLX_METAL_PREALLOCATE=1
-export MLX_METAL_MEMORY_GUARD=0
-export MLX_METAL_CACHE_KERNELS=1
-export MLX_PLACEMENT_POLICY=metal
-export MLX_METAL_VALIDATION=0
-export MLX_METAL_DEBUG=0
-export MLX_FORCE_P_CORES=1
-export MLX_METAL_MEMORY_BUDGET=0
-export MLX_METAL_PREWARM=1
-
-# Python optimizations
-export PYTHONUNBUFFERED=1
-export PYTHONOPTIMIZE=2
-export PYTHONHASHSEED=0
-export PYTHONDONTWRITEBYTECODE=1
-EOF
-
-log "Performance optimizations completed. Environment variables written to /tmp/performance_env.sh"
\ No newline at end of file
diff --git a/.github/workflows/bench_job.yml b/.github/workflows/bench_job.yml
deleted file mode 100644
index 5e089283..00000000
--- a/.github/workflows/bench_job.yml
+++ /dev/null
@@ -1,207 +0,0 @@
-# This is the reusable workflow file
-name: Distributed Job Runner
-
-on:
- workflow_call:
- inputs:
- config:
- required: true
- type: string
- model:
- required: true
- type: string
- calling_job_name:
- required: true
- type: string
- network_interface:
- required: true
- type: string
-jobs:
- generate-matrix:
- runs-on: ubuntu-latest
- outputs:
- matrix: ${{ steps.set-matrix.outputs.matrix }}
- steps:
- - id: set-matrix
- env:
- CONFIG: ${{ inputs.config }}
- run: |
- MATRIX=$(echo $CONFIG | jq -c '{cpu: [to_entries | .[] | .key as $k | range(.value) | $k]}')
- echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
-
- run-distributed-job:
- needs: generate-matrix
- strategy:
- matrix: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
- runs-on: ['self-hosted', 'macOS', '${{ matrix.cpu }}']
- env:
- HARDWARE_CONFIG: ${{ inputs.config }}
- model: ${{ inputs.model }}
- # Add performance-related environment variables
- MTL_DEBUG_LAYER: 0
- METAL_VALIDATION_ENABLED: 0
- MLX_METAL_VALIDATION: 0
- MLX_METAL_DEBUG: 0
- MLX_FORCE_P_CORES: 1
- MLX_METAL_PREWARM: 1
- PYTHONOPTIMIZE: 2
- steps:
- - name: Cleanup workspace
- run: |
- sudo rm -rf "$GITHUB_WORKSPACE"
- sudo mkdir -p "$GITHUB_WORKSPACE"
- sudo chown -R $(whoami):$(id -g) "$GITHUB_WORKSPACE"
-
- - uses: actions/checkout@v4
-
- - name: Install dependencies
- run: |
- export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"
- python3.12 -m venv .venv || {
- echo "Failed to find python3.12. Checking installation locations:"
- ls -l /usr/local/bin/python* /opt/homebrew/bin/python* 2>/dev/null || true
- exit 1
- }
- source .venv/bin/activate
- pip install --upgrade pip
- pip install -e .
- pip install boto3==1.35.76
-
- - name: Apply Performance Optimizations
- run: |
- # Export performance-related environment variables
- cat << 'EOF' > /tmp/performance_env.sh
- # MLX and Metal optimizations
- export MTL_DEBUG_LAYER=0
- export METAL_VALIDATION_ENABLED=0
- export MLX_METAL_VALIDATION=0
- export MLX_METAL_DEBUG=0
- export MLX_FORCE_P_CORES=1
- export MLX_METAL_PREWARM=1
- export PYTHONOPTIMIZE=2
- EOF
-
- # Source the performance environment variables
- source /tmp/performance_env.sh
-
- # MLX Memory Settings
- ./configure_mlx.sh
-
- # Verify optimizations
- echo "Verifying performance settings..."
- env | grep -E "MLX_|METAL_|MTL_"
-
- - name: Run exo
- env:
- aws_access_key_id: ${{ secrets.S3_EXO_BENCHMARKS_AWS_ACCESS_KEY_ID }}
- aws_secret_key: ${{ secrets.S3_EXO_BENCHMARKS_AWS_SECRET_ACCESS_KEY }}
- run: |
- # Source performance environment variables
- source /tmp/performance_env.sh
-
- # Debug information
- echo "Current commit SHA: $GITHUB_SHA"
- git rev-parse HEAD
- git status
-
- CALLING_JOB="${{ inputs.calling_job_name }}"
- UNIQUE_JOB_ID="${CALLING_JOB}_${model}_${GITHUB_RUN_ID}"
- ALL_NODE_IDS=$(for i in $(seq ${{ strategy.job-total }} -1 0); do echo -n "${UNIQUE_JOB_ID}_${i},"; done | sed 's/,$//')
- MY_NODE_ID="${UNIQUE_JOB_ID}_${{ strategy.job-index }}"
-
- source .venv/bin/activate
- export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"
-
- echo "=== Before starting exo ==="
- ps -eo pid,ppid,user,%cpu,%mem,nice,state,pri,command | head -1
- ps -eo pid,ppid,user,%cpu,%mem,nice,state,pri,command | grep -i python
-
- echo "Starting exo daemon..."
-
- echo "Power mode settings:"
- sudo pmset -g
-
- # Start exo with explicit process control
- sudo taskpolicy -d default -g default -a -t 0 -l 0 .venv/bin/exo \
- --node-id="${MY_NODE_ID}" \
- --node-id-filter="${ALL_NODE_IDS}" \
- --interface-type-filter="${{ inputs.network_interface }}" \
- --disable-tui \
- --max-generate-tokens 250 \
- --chatgpt-api-response-timeout 900 \
- --chatgpt-api-port 52415 > output1.log 2>&1 &
- PID1=$!
-
- echo "Exo process started with PID: $PID1"
- tail -f output1.log &
- TAIL1=$!
-
- # Give process time to start
- sleep 2
-
- # Set additional process priorities
- sudo renice -n -20 -p $PID1
- sudo taskpolicy -t 4 -p $PID1
-
- echo "=== After starting exo ==="
- ps -eo pid,ppid,user,%cpu,%mem,nice,state,pri,command | head -1
- ps -eo pid,ppid,user,%cpu,%mem,nice,state,pri,command | grep $PID1
-
- echo "Additional process details:"
- sudo powermetrics -n 1 -i 1000 --show-process-energy | grep -A 5 $PID1 || true
-
- trap 'kill $TAIL1' EXIT
- trap 'kill $PID1' EXIT
-
- echo "Waiting for all nodes to connect..."
- for i in {1..20}; do
- echo "Attempt $i: Checking node count..."
- nodes=$(curl -s http://localhost:52415/topology | jq ".nodes | length")
- echo "Current node count: $nodes"
- if [ "$nodes" -eq "${{ strategy.job-total }}" ]; then
- echo "All nodes connected successfully!"
- break
- fi
- if [ $i -eq 20 ]; then
- echo "ERROR: Failed to connect all nodes after 20 attempts. Expected ${{ strategy.job-total }} nodes, but got $nodes"
- exit 1
- fi
- sleep 5
- done
-
- if ! kill -0 $PID1 2>/dev/null; then
- echo "ERROR: Instance (PID $PID1) died unexpectedly. Full log output:"
- cat output1.log
- exit 1
- fi
-
- if [ "${{ strategy.job-index }}" -eq "0" ]; then
- sleep 10
- echo "This is the primary node (index 0). Running benchmark..."
- GITHUB_JOB=$CALLING_JOB python .github/bench.py
- else
- echo "This is a secondary node (index ${{ strategy.job-index }}). Waiting for completion..."
- sleep 10
- while true; do
- echo "Checking if primary node is still running..."
- nodes=$(curl -s http://localhost:52415/topology | jq ".nodes | length")
- echo "Current node count: $nodes"
- if [ "$nodes" -lt "${{ strategy.job-total }}" ]; then
- echo "Primary node completed, exiting..."
- break
- fi
- sleep 5
- done
- fi
-
- - name: Check Final System State
- if: always()
- run: |
- echo "=== Final System State ==="
- sudo pmset -g
- sudo powermetrics -n 1 -i 1000 --show-process-energy || true
- system_profiler SPDisplaysDataType
- sysctl iogpu
- ps -eo pid,ppid,user,%cpu,%mem,nice,state,command | grep -i python
- env | grep -E "MLX_|METAL_|MTL_"
- echo "=== End Final System State ==="
diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
deleted file mode 100644
index 2750ac4a..00000000
--- a/.github/workflows/benchmarks.yml
+++ /dev/null
@@ -1,71 +0,0 @@
-name: Build and Test
-
-on:
- push:
- branches: [ '*' ]
- tags: [ '*' ]
- pull_request:
- branches: [ '*' ]
-
-jobs:
- single-m4-pro:
- strategy:
- matrix:
- model: ['llama-3.2-1b', 'llama-3.2-3b', 'llama-3.1-8b']
- uses: ./.github/workflows/bench_job.yml
- with:
- config: '{"M4PRO_GPU16_24GB": 1}'
- model: ${{ matrix.model }}
- calling_job_name: 'single-m4-pro'
- network_interface: 'Ethernet'
- secrets: inherit
-
- two-m4-pro-cluster:
- strategy:
- matrix:
- model: ['llama-3.2-1b', 'llama-3.2-3b', 'llama-3.1-8b']
- uses: ./.github/workflows/bench_job.yml
- with:
- config: '{"M4PRO_GPU16_24GB": 2}'
- model: ${{ matrix.model }}
- calling_job_name: 'two-m4-pro-cluster'
- network_interface: 'Ethernet'
- secrets: inherit
-
- # two-m4-pro-cluster-thunderbolt:
- # strategy:
- # matrix:
- # model: ['llama-3.2-1b', 'llama-3.2-3b', 'llama-3.1-8b']
- # uses: ./.github/workflows/bench_job.yml
- # with:
- # config: '{"M4PRO_GPU16_24GB": 2}'
- # model: ${{ matrix.model }}
- # calling_job_name: 'two-m4-pro-cluster-thunderbolt'
- # network_interface: 'Thunderbolt'
- # secrets: inherit
-
- three-m4-pro-cluster:
- strategy:
- matrix:
- model: ['llama-3.2-1b', 'llama-3.2-3b', 'llama-3.1-8b', 'llama-3.3-70b']
- fail-fast: false
- uses: ./.github/workflows/bench_job.yml
- with:
- config: '{"M4PRO_GPU16_24GB": 3}'
- model: ${{ matrix.model }}
- calling_job_name: 'three-m4-pro-cluster'
- network_interface: 'Ethernet'
- secrets: inherit
-
- # test-m3-single-node:
- # strategy:
- # matrix:
- # model: ['llama-3.2-1b']
- # fail-fast: false
- # uses: ./.github/workflows/bench_job.yml
- # with:
- # config: '{"M3MAX_GPU40_128GB": 1}'
- # model: ${{ matrix.model }}
- # calling_job_name: 'test-m3-cluster'
- # network_interface: 'Ethernet'
- # secrets: inherit
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index bc6a38d7..00000000
--- a/.gitignore
+++ /dev/null
@@ -1,175 +0,0 @@
-__pycache__/
-.venv*
-test_weights.npz
-.exo_used_ports
-.exo_node_id
-.idea
-.DS_Store
-
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-/.Python
-/develop-eggs/
-/dist/
-/downloads/
-/eggs/
-/.eggs/
-/lib/
-/lib64/
-/parts/
-/sdist/
-/var/
-/wheels/
-/share/python-wheels/
-/*.egg-info/
-/.installed.cfg
-/*.egg
-/MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-Untitled.ipynb
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
-# pdm
-# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-#pdm.lock
-# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
-# in version control.
-# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
-.pdm.toml
-.pdm-python
-.pdm-build/
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
-
-**/*.xcodeproj/*
-.aider*
-
-exo/tinychat/images/*.png
diff --git a/.style.yapf b/.style.yapf
deleted file mode 100644
index 7301910e..00000000
--- a/.style.yapf
+++ /dev/null
@@ -1,19 +0,0 @@
-[style]
-based_on_style = pep8
-indent_width = 2
-column_limit = 200
-allow_split_before_dict_value = False
-dedent_closing_brackets = True
-split_before_first_argument = False
-split_complex_comprehension = False
-continuation_indent_width = 2
-indent_dictionary_value = True
-allow_multiline_dictionary_keys = True
-each_dict_entry_on_separate_line = False
-allow_multiline_lambdas = True
-blank_line_before_nested_class_or_def = False
-arithmetic_precedence_indication = True
-no_spaces_around_selected_binary_operators = "*,/"
-coalesce_brackets = True
-space_between_ending_comma_and_closing_bracket = False
-split_before_expression_after_opening_paren = False
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 53d1f3d0..00000000
--- a/LICENSE
+++ /dev/null
@@ -1,675 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
- <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- <program> Copyright (C) <year> <name of author>
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<https://www.gnu.org/licenses/>.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-<https://www.gnu.org/licenses/why-not-lgpl.html>.
-
diff --git a/README.md b/README.md
index 905bd88a..ef097f9b 100644
--- a/README.md
+++ b/README.md
@@ -15,8 +15,7 @@ exo: Run your own AI cluster at home with everyday devices. Maintained by [exo l
</h3>
[](https://github.com/exo-explore/exo/stargazers)
-[](https://dl.circleci.com/status-badge/redirect/circleci/TrkofJDoGzdQAeL6yVHKsg/4i5hJuafuwZYZQxbRAWS71/tree/main)
-[](https://www.gnu.org/licenses/gpl-3.0)
+[](https://www.apache.org/licenses/LICENSE-2.0.html)
<a href="https://trendshift.io/repositories/11849" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11849" alt="exo-explore%2Fexo | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@@ -26,286 +25,6 @@ exo: Run your own AI cluster at home with everyday devices. Maintained by [exo l
> **EXO**
>
-> EXO started out of a desire to run research experiments on large language models using the hardware we already owned.
->
-> What began here is becoming part of something much larger.
->
-> soon™
->
-> \- The EXO Team
+> Coming soon. For legacy exo, see this repo's history or [exo-explore/ex-exo](https://github.com/exo-explore/ex-exo) for a snapshot.
---
-
-Unify your existing devices into one powerful GPU: iPhone, iPad, Android, Mac, NVIDIA, Raspberry Pi, pretty much any device!
-
-<div align="center">
- <h2>Update: exo is hiring. See <a href="https://exolabs.net">here</a> for more details.</h2>
-</div>
-
-## Get Involved
-
-exo is **experimental** software. Expect bugs early on. Create issues so they can be fixed. The [exo labs](https://x.com/exolabs) team will strive to resolve issues quickly.
-
-We also welcome contributions from the community. We have a list of bounties in [this sheet](https://docs.google.com/spreadsheets/d/1cTCpTIp48UnnIvHeLEUNg1iMy_Q6lRybgECSFCoVJpE/edit?usp=sharing).
-
-## Features
-
-### Wide Model Support
-
-exo supports different models including LLaMA ([MLX](exo/inference/mlx/models/llama.py) and [tinygrad](exo/inference/tinygrad/models/llama.py)), Mistral, LlaVA, Qwen, and Deepseek.
-
-### Dynamic Model Partitioning
-
-exo [optimally splits up models](exo/topology/ring_memory_weighted_partitioning_strategy.py) based on the current network topology and device resources available. This enables you to run larger models than you would be able to on any single device.
-
-### Automatic Device Discovery
-
-exo will [automatically discover](https://github.com/exo-explore/exo/blob/945f90f676182a751d2ad7bcf20987ab7fe0181e/exo/orchestration/node.py#L154) other devices using the best method available. Zero manual configuration.
-
-### ChatGPT-compatible API
-
-exo provides a [ChatGPT-compatible API](exo/api/chatgpt_api.py) for running models. It's a [one-line change](examples/chatgpt_api.sh) in your application to run models on your own hardware using exo.
-
-### Device Equality
-
-Unlike other distributed inference frameworks, exo does not use a master-worker architecture. Instead, exo devices [connect p2p](https://github.com/exo-explore/exo/blob/945f90f676182a751d2ad7bcf20987ab7fe0181e/exo/orchestration/node.py#L161). As long as a device is connected somewhere in the network, it can be used to run models.
-
-Exo supports different [partitioning strategies](exo/topology/partitioning_strategy.py) to split up a model across devices. The default partitioning strategy is [ring memory weighted partitioning](exo/topology/ring_memory_weighted_partitioning_strategy.py). This runs an inference in a ring where each device runs a number of model layers proportional to the memory of the device.
-
-
-
-## Installation
-
-The current recommended way to install exo is from source.
-
-### Prerequisites
-
-- Python>=3.12.0 is required because of [issues with asyncio](https://github.com/exo-explore/exo/issues/5) in previous versions.
-- For Linux with NVIDIA GPU support (Linux-only, skip if not using Linux or NVIDIA):
- - NVIDIA driver - verify with `nvidia-smi`
- - CUDA toolkit - install from [NVIDIA CUDA guide](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#cuda-cross-platform-installation), verify with `nvcc --version`
- - cuDNN library - download from [NVIDIA cuDNN page](https://developer.nvidia.com/cudnn-downloads), verify installation by following [these steps](https://docs.nvidia.com/deeplearning/cudnn/latest/installation/linux.html#verifying-the-install-on-linux:~:text=at%20a%20time.-,Verifying%20the%20Install%20on%20Linux,Test%20passed!,-Upgrading%20From%20Older)
-
-### Hardware Requirements
-
-- The only requirement to run exo is to have enough memory across all your devices to fit the entire model into memory. For example, if you are running llama 3.1 8B (fp16), you need 16GB of memory across all devices. Any of the following configurations would work since they each have more than 16GB of memory in total:
- - 2 x 8GB M3 MacBook Airs
- - 1 x 16GB NVIDIA RTX 4070 Ti Laptop
- - 2 x Raspberry Pi 400 with 4GB of RAM each (running on CPU) + 1 x 8GB Mac Mini
-- exo is designed to run on devices with heterogeneous capabilities. For example, you can have some devices with powerful GPUs and others with integrated GPUs or even CPUs. Adding less capable devices will slow down individual inference latency but will increase the overall throughput of the cluster.
-
-### From source
-
-
-```sh
-git clone https://github.com/exo-explore/exo.git
-cd exo
-pip install -e .
-# alternatively, with venv
-source install.sh
-```
-
-
-### Troubleshooting
-
-- If running on Mac, MLX has an [install guide](https://ml-explore.github.io/mlx/build/html/install.html) with troubleshooting steps.
-
-### Performance
-
-- There are a number of things users have empirically found to improve performance on Apple Silicon Macs:
-
-1. Upgrade to the latest version of macOS Sequoia.
-2. Run `./configure_mlx.sh`. This runs commands to optimize GPU memory allocation on Apple Silicon Macs.
-
-
-## Documentation
-
-### Example Usage on Multiple macOS Devices
-
-#### Device 1:
-
-```sh
-exo
-```
-
-#### Device 2:
-```sh
-exo
-```
-
-That's it! No configuration required - exo will automatically discover the other device(s).
-
-exo starts a ChatGPT-like WebUI (powered by [tinygrad tinychat](https://github.com/tinygrad/tinygrad/tree/master/examples/tinychat)) on http://localhost:52415
-
-For developers, exo also starts a ChatGPT-compatible API endpoint on http://localhost:52415/v1/chat/completions. Examples with curl:
-
-#### Llama 3.2 3B:
-
-```sh
-curl http://localhost:52415/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "model": "llama-3.2-3b",
- "messages": [{"role": "user", "content": "What is the meaning of exo?"}],
- "temperature": 0.7
- }'
-```
-
-#### Llama 3.1 405B:
-
-```sh
-curl http://localhost:52415/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "model": "llama-3.1-405b",
- "messages": [{"role": "user", "content": "What is the meaning of exo?"}],
- "temperature": 0.7
- }'
-```
-
-#### DeepSeek R1 (full 671B):
-
-```sh
-curl http://localhost:52415/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "model": "deepseek-r1",
- "messages": [{"role": "user", "content": "What is the meaning of exo?"}],
- "temperature": 0.7
- }'
-```
-
-#### Llava 1.5 7B (Vision Language Model):
-
-```sh
-curl http://localhost:52415/v1/chat/completions \
- -H "Content-Type: application/json" \
- -d '{
- "model": "llava-1.5-7b-hf",
- "messages": [
- {
- "role": "user",
- "content": [
- {
- "type": "text",
- "text": "What are these?"
- },
- {
- "type": "image_url",
- "image_url": {
- "url": "http://images.cocodataset.org/val2017/000000039769.jpg"
- }
- }
- ]
- }
- ],
- "temperature": 0.0
- }'
-```
-
-### Example Usage on Multiple Heterogenous Devices (macOS + Linux)
-
-#### Device 1 (macOS):
-
-```sh
-exo
-```
-
-Note: We don't need to explicitly tell exo to use the **tinygrad** inference engine. **MLX** and **tinygrad** are interoperable!
-
-#### Device 2 (Linux):
-```sh
-exo
-```
-
-Linux devices will automatically default to using the **tinygrad** inference engine.
-
-You can read about tinygrad-specific env vars [here](https://docs.tinygrad.org/env_vars/). For example, you can configure tinygrad to use the cpu by specifying `CLANG=1`.
-
-### Example Usage on a single device with "exo run" command
-
-```sh
-exo run llama-3.2-3b
-```
-
-With a custom prompt:
-
-```sh
-exo run llama-3.2-3b --prompt "What is the meaning of exo?"
-```
-
-### Model Storage
-
-Models by default are stored in `~/.cache/exo/downloads`.
-
-You can set a different model storage location by setting the `EXO_HOME` env var.
-
-## Model Downloading
-
-Models are downloaded from Hugging Face. If you are running exo in a country with strict internet censorship, you may need to download the models manually and put them in the `~/.cache/exo/downloads` directory.
-
-To download models from a proxy endpoint, set the `HF_ENDPOINT` environment variable. For example, to run exo with the huggingface mirror endpoint:
-
-```sh
-HF_ENDPOINT=https://hf-mirror.com exo
-```
-
-## Debugging
-
-Enable debug logs with the DEBUG environment variable (0-9).
-
-```sh
-DEBUG=9 exo
-```
-
-For the **tinygrad** inference engine specifically, there is a separate DEBUG flag `TINYGRAD_DEBUG` that can be used to enable debug logs (1-6).
-
-```sh
-TINYGRAD_DEBUG=2 exo
-```
-
-## Formatting
-
-We use [yapf](https://github.com/google/yapf) to format the code. To format the code, first install the formatting requirements:
-
-```sh
-pip3 install -e '.[formatting]'
-```
-
-Then run the formatting script:
-
-```sh
-python3 format.py ./exo
-```
-
-## Known Issues
-
-- On certain versions of Python on macOS, certificates may not installed correctly, potentially causing SSL errors (e.g., when accessing huggingface.co). To resolve this, run the `Install Certificates` command, typicall as follows:
-
-```sh
-/Applications/Python 3.x/Install Certificates.command
-```
-
-- 🚧 As the library is evolving so quickly, the iOS implementation has fallen behind Python. We have decided for now not to put out the buggy iOS version and receive a bunch of GitHub issues for outdated code. We are working on solving this properly and will make an announcement when it's ready. If you would like access to the iOS implementation now, please email alex@exolabs.net with your GitHub username explaining your use-case and you will be granted access on GitHub.
-
-## Inference Engines
-
-exo supports the following inference engines:
-
-- ✅ [MLX](exo/inference/mlx/sharded_inference_engine.py)
-- ✅ [tinygrad](exo/inference/tinygrad/inference.py)
-- 🚧 [PyTorch](https://github.com/exo-explore/exo/pull/139)
-- 🚧 [llama.cpp](https://github.com/exo-explore/exo/issues/167)
-
-## Discovery Modules
-
-- ✅ [UDP](exo/networking/udp)
-- ✅ [Manual](exo/networking/manual)
-- ✅ [Tailscale](exo/networking/tailscale)
-- 🚧 Radio
-- 🚧 Bluetooth
-
-# Peer Networking Modules
-
-- ✅ [GRPC](exo/networking/grpc)
-- 🚧 NCCL
diff --git a/configure_mlx.sh b/configure_mlx.sh
deleted file mode 100755
index f1cfe6e6..00000000
--- a/configure_mlx.sh
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/env bash
-
-# Get the total memory in MB
-TOTAL_MEM_MB=$(($(sysctl -n hw.memsize) / 1024 / 1024))
-
-# Calculate 80% and TOTAL_MEM_GB-5GB in MB
-EIGHTY_PERCENT=$(($TOTAL_MEM_MB * 80 / 100))
-MINUS_5GB=$((($TOTAL_MEM_MB - 5120)))
-
-# Calculate 70% and TOTAL_MEM_GB-8GB in MB
-SEVENTY_PERCENT=$(($TOTAL_MEM_MB * 70 / 100))
-MINUS_8GB=$((($TOTAL_MEM_MB - 8192)))
-
-# Set WIRED_LIMIT_MB to higher value
-if [ $EIGHTY_PERCENT -gt $MINUS_5GB ]; then
- WIRED_LIMIT_MB=$EIGHTY_PERCENT
-else
- WIRED_LIMIT_MB=$MINUS_5GB
-fi
-
-# Set WIRED_LWM_MB to higher value
-if [ $SEVENTY_PERCENT -gt $MINUS_8GB ]; then
- WIRED_LWM_MB=$SEVENTY_PERCENT
-else
- WIRED_LWM_MB=$MINUS_8GB
-fi
-
-# Display the calculated values
-echo "Total memory: $TOTAL_MEM_MB MB"
-echo "Maximum limit (iogpu.wired_limit_mb): $WIRED_LIMIT_MB MB"
-echo "Lower bound (iogpu.wired_lwm_mb): $WIRED_LWM_MB MB"
-
-# Apply the values with sysctl, but check if we're already root
-if [ "$EUID" -eq 0 ]; then
- sysctl -w iogpu.wired_limit_mb=$WIRED_LIMIT_MB
- sysctl -w iogpu.wired_lwm_mb=$WIRED_LWM_MB
-else
- # Try without sudo first, fall back to sudo if needed
- sysctl -w iogpu.wired_limit_mb=$WIRED_LIMIT_MB 2>/dev/null || \
- sudo sysctl -w iogpu.wired_limit_mb=$WIRED_LIMIT_MB
- sysctl -w iogpu.wired_lwm_mb=$WIRED_LWM_MB 2>/dev/null || \
- sudo sysctl -w iogpu.wired_lwm_mb=$WIRED_LWM_MB
-fi
\ No newline at end of file
diff --git a/docs/exo-logo-black-bg.jpg b/docs/exo-logo-black-bg.jpg
deleted file mode 100644
index c2905054..00000000
Binary files a/docs/exo-logo-black-bg.jpg and /dev/null differ
diff --git a/docs/exo-logo-transparent-black-text.png b/docs/exo-logo-transparent-black-text.png
deleted file mode 100644
index 50ec690e..00000000
--- a/docs/exo-logo-transparent-black-text.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1c6f0b66b68ffc11a42cf25fbd43a6fbea99869ed4ba82e5f480d8213e9b7061
-size 1296
diff --git a/docs/exo-logo-transparent.png b/docs/exo-logo-transparent.png
deleted file mode 100644
index 5cf5ea69..00000000
--- a/docs/exo-logo-transparent.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c7aeca6a876a195df706f3221f1bfd4792884e6042c2b355026f94cba0f7576d
-size 1296
diff --git a/docs/exo-rounded.png b/docs/exo-rounded.png
deleted file mode 100644
index a12c5650..00000000
--- a/docs/exo-rounded.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1316a53899f32ba6c33b083fca232b638aea4efbcf36bc99e640369169e6a1c9
-size 28651
diff --git a/docs/exo-screenshot.jpg b/docs/exo-screenshot.jpg
deleted file mode 100644
index ace09a46..00000000
Binary files a/docs/exo-screenshot.jpg and /dev/null differ
diff --git a/examples/astra/README.md b/examples/astra/README.md
deleted file mode 100644
index 2f36bf50..00000000
--- a/examples/astra/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-## Overview
-
-This example app is an open-source alternative to [Google's Project Astra](https://deepmind.google/technologies/gemini/project-astra/). It leverages the exo library to run on your own devices, providing a fully transparent and customizable experience compared to Google's closed-source API.
diff --git a/examples/astra/astra.xcodeproj/project.pbxproj b/examples/astra/astra.xcodeproj/project.pbxproj
deleted file mode 100644
index bcbf09ed..00000000
--- a/examples/astra/astra.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,653 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 56;
- objects = {
-
-/* Begin PBXBuildFile section */
- FA3E988F2C725A0200E4E795 /* astraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3E988E2C725A0200E4E795 /* astraApp.swift */; };
- FA3E98912C725A0200E4E795 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3E98902C725A0200E4E795 /* ContentView.swift */; };
- FA3E98932C725A0300E4E795 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA3E98922C725A0300E4E795 /* Assets.xcassets */; };
- FA3E98972C725A0300E4E795 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FA3E98962C725A0300E4E795 /* Preview Assets.xcassets */; };
- FA3E98A12C725A0300E4E795 /* astraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3E98A02C725A0300E4E795 /* astraTests.swift */; };
- FA3E98AB2C725A0300E4E795 /* astraUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3E98AA2C725A0300E4E795 /* astraUITests.swift */; };
- FA3E98AD2C725A0300E4E795 /* astraUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3E98AC2C725A0300E4E795 /* astraUITestsLaunchTests.swift */; };
- FA3E98BB2C725BF800E4E795 /* WhisperKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA3E98BA2C725BF800E4E795 /* WhisperKit */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXContainerItemProxy section */
- FA3E989D2C725A0300E4E795 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = FA3E98832C725A0200E4E795 /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = FA3E988A2C725A0200E4E795;
- remoteInfo = astra;
- };
- FA3E98A72C725A0300E4E795 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = FA3E98832C725A0200E4E795 /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = FA3E988A2C725A0200E4E795;
- remoteInfo = astra;
- };
-/* End PBXContainerItemProxy section */
-
-/* Begin PBXFileReference section */
- FA3E988B2C725A0200E4E795 /* astra.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = astra.app; sourceTree = BUILT_PRODUCTS_DIR; };
- FA3E988E2C725A0200E4E795 /* astraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = astraApp.swift; sourceTree = "<group>"; };
- FA3E98902C725A0200E4E795 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
- FA3E98922C725A0300E4E795 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
- FA3E98942C725A0300E4E795 /* astra.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = astra.entitlements; sourceTree = "<group>"; };
- FA3E98962C725A0300E4E795 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
- FA3E989C2C725A0300E4E795 /* astraTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = astraTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- FA3E98A02C725A0300E4E795 /* astraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = astraTests.swift; sourceTree = "<group>"; };
- FA3E98A62C725A0300E4E795 /* astraUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = astraUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- FA3E98AA2C725A0300E4E795 /* astraUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = astraUITests.swift; sourceTree = "<group>"; };
- FA3E98AC2C725A0300E4E795 /* astraUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = astraUITestsLaunchTests.swift; sourceTree = "<group>"; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- FA3E98882C725A0200E4E795 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- FA3E98BB2C725BF800E4E795 /* WhisperKit in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- FA3E98992C725A0300E4E795 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- FA3E98A32C725A0300E4E795 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- FA3E98822C725A0200E4E795 = {
- isa = PBXGroup;
- children = (
- FA3E988D2C725A0200E4E795 /* astra */,
- FA3E989F2C725A0300E4E795 /* astraTests */,
- FA3E98A92C725A0300E4E795 /* astraUITests */,
- FA3E988C2C725A0200E4E795 /* Products */,
- );
- sourceTree = "<group>";
- };
- FA3E988C2C725A0200E4E795 /* Products */ = {
- isa = PBXGroup;
- children = (
- FA3E988B2C725A0200E4E795 /* astra.app */,
- FA3E989C2C725A0300E4E795 /* astraTests.xctest */,
- FA3E98A62C725A0300E4E795 /* astraUITests.xctest */,
- );
- name = Products;
- sourceTree = "<group>";
- };
- FA3E988D2C725A0200E4E795 /* astra */ = {
- isa = PBXGroup;
- children = (
- FA3E988E2C725A0200E4E795 /* astraApp.swift */,
- FA3E98902C725A0200E4E795 /* ContentView.swift */,
- FA3E98922C725A0300E4E795 /* Assets.xcassets */,
- FA3E98942C725A0300E4E795 /* astra.entitlements */,
- FA3E98952C725A0300E4E795 /* Preview Content */,
- );
- path = astra;
- sourceTree = "<group>";
- };
- FA3E98952C725A0300E4E795 /* Preview Content */ = {
- isa = PBXGroup;
- children = (
- FA3E98962C725A0300E4E795 /* Preview Assets.xcassets */,
- );
- path = "Preview Content";
- sourceTree = "<group>";
- };
- FA3E989F2C725A0300E4E795 /* astraTests */ = {
- isa = PBXGroup;
- children = (
- FA3E98A02C725A0300E4E795 /* astraTests.swift */,
- );
- path = astraTests;
- sourceTree = "<group>";
- };
- FA3E98A92C725A0300E4E795 /* astraUITests */ = {
- isa = PBXGroup;
- children = (
- FA3E98AA2C725A0300E4E795 /* astraUITests.swift */,
- FA3E98AC2C725A0300E4E795 /* astraUITestsLaunchTests.swift */,
- );
- path = astraUITests;
- sourceTree = "<group>";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- FA3E988A2C725A0200E4E795 /* astra */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = FA3E98B02C725A0300E4E795 /* Build configuration list for PBXNativeTarget "astra" */;
- buildPhases = (
- FA3E98872C725A0200E4E795 /* Sources */,
- FA3E98882C725A0200E4E795 /* Frameworks */,
- FA3E98892C725A0200E4E795 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = astra;
- packageProductDependencies = (
- FA3E98BA2C725BF800E4E795 /* WhisperKit */,
- );
- productName = astra;
- productReference = FA3E988B2C725A0200E4E795 /* astra.app */;
- productType = "com.apple.product-type.application";
- };
- FA3E989B2C725A0300E4E795 /* astraTests */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = FA3E98B32C725A0300E4E795 /* Build configuration list for PBXNativeTarget "astraTests" */;
- buildPhases = (
- FA3E98982C725A0300E4E795 /* Sources */,
- FA3E98992C725A0300E4E795 /* Frameworks */,
- FA3E989A2C725A0300E4E795 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- FA3E989E2C725A0300E4E795 /* PBXTargetDependency */,
- );
- name = astraTests;
- productName = astraTests;
- productReference = FA3E989C2C725A0300E4E795 /* astraTests.xctest */;
- productType = "com.apple.product-type.bundle.unit-test";
- };
- FA3E98A52C725A0300E4E795 /* astraUITests */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = FA3E98B62C725A0300E4E795 /* Build configuration list for PBXNativeTarget "astraUITests" */;
- buildPhases = (
- FA3E98A22C725A0300E4E795 /* Sources */,
- FA3E98A32C725A0300E4E795 /* Frameworks */,
- FA3E98A42C725A0300E4E795 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- FA3E98A82C725A0300E4E795 /* PBXTargetDependency */,
- );
- name = astraUITests;
- productName = astraUITests;
- productReference = FA3E98A62C725A0300E4E795 /* astraUITests.xctest */;
- productType = "com.apple.product-type.bundle.ui-testing";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- FA3E98832C725A0200E4E795 /* Project object */ = {
- isa = PBXProject;
- attributes = {
- BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1540;
- LastUpgradeCheck = 1540;
- TargetAttributes = {
- FA3E988A2C725A0200E4E795 = {
- CreatedOnToolsVersion = 15.4;
- };
- FA3E989B2C725A0300E4E795 = {
- CreatedOnToolsVersion = 15.4;
- TestTargetID = FA3E988A2C725A0200E4E795;
- };
- FA3E98A52C725A0300E4E795 = {
- CreatedOnToolsVersion = 15.4;
- TestTargetID = FA3E988A2C725A0200E4E795;
- };
- };
- };
- buildConfigurationList = FA3E98862C725A0200E4E795 /* Build configuration list for PBXProject "astra" */;
- compatibilityVersion = "Xcode 14.0";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- en,
- Base,
- );
- mainGroup = FA3E98822C725A0200E4E795;
- packageReferences = (
- FA3E98B92C725BF800E4E795 /* XCRemoteSwiftPackageReference "whisperkit" */,
- );
- productRefGroup = FA3E988C2C725A0200E4E795 /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- FA3E988A2C725A0200E4E795 /* astra */,
- FA3E989B2C725A0300E4E795 /* astraTests */,
- FA3E98A52C725A0300E4E795 /* astraUITests */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- FA3E98892C725A0200E4E795 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- FA3E98972C725A0300E4E795 /* Preview Assets.xcassets in Resources */,
- FA3E98932C725A0300E4E795 /* Assets.xcassets in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- FA3E989A2C725A0300E4E795 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- FA3E98A42C725A0300E4E795 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- FA3E98872C725A0200E4E795 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- FA3E98912C725A0200E4E795 /* ContentView.swift in Sources */,
- FA3E988F2C725A0200E4E795 /* astraApp.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- FA3E98982C725A0300E4E795 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- FA3E98A12C725A0300E4E795 /* astraTests.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- FA3E98A22C725A0300E4E795 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- FA3E98AD2C725A0300E4E795 /* astraUITestsLaunchTests.swift in Sources */,
- FA3E98AB2C725A0300E4E795 /* astraUITests.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXTargetDependency section */
- FA3E989E2C725A0300E4E795 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = FA3E988A2C725A0200E4E795 /* astra */;
- targetProxy = FA3E989D2C725A0300E4E795 /* PBXContainerItemProxy */;
- };
- FA3E98A82C725A0300E4E795 /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = FA3E988A2C725A0200E4E795 /* astra */;
- targetProxy = FA3E98A72C725A0300E4E795 /* PBXContainerItemProxy */;
- };
-/* End PBXTargetDependency section */
-
-/* Begin XCBuildConfiguration section */
- FA3E98AE2C725A0300E4E795 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = dwarf;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = YES;
- GCC_C_LANGUAGE_STANDARD = gnu17;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "DEBUG=1",
- "$(inherited)",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
- MTL_FAST_MATH = YES;
- ONLY_ACTIVE_ARCH = YES;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- };
- name = Debug;
- };
- FA3E98AF2C725A0300E4E795 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- COPY_PHASE_STRIP = NO;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_USER_SCRIPT_SANDBOXING = YES;
- GCC_C_LANGUAGE_STANDARD = gnu17;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MTL_ENABLE_DEBUG_INFO = NO;
- MTL_FAST_MATH = YES;
- SWIFT_COMPILATION_MODE = wholemodule;
- };
- name = Release;
- };
- FA3E98B12C725A0300E4E795 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_ENTITLEMENTS = astra/astra.entitlements;
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_ASSET_PATHS = "\"astra/Preview Content\"";
- DEVELOPMENT_TEAM = 8NFAS2P4ND;
- ENABLE_HARDENED_RUNTIME = YES;
- ENABLE_PREVIEWS = YES;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_KEY_NSCameraUsageDescription = "Capture from camera to send to vision model";
- INFOPLIST_KEY_NSMicrophoneUsageDescription = "Uses your microphone for transcribing audio";
- "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
- "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- IPHONEOS_DEPLOYMENT_TARGET = 17.5;
- LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
- "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
- MACOSX_DEPLOYMENT_TARGET = 14.4;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = net.exolabs.astra;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- FA3E98B22C725A0300E4E795 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_ENTITLEMENTS = astra/astra.entitlements;
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_ASSET_PATHS = "\"astra/Preview Content\"";
- DEVELOPMENT_TEAM = 8NFAS2P4ND;
- ENABLE_HARDENED_RUNTIME = YES;
- ENABLE_PREVIEWS = YES;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_KEY_NSCameraUsageDescription = "Capture from camera to send to vision model";
- INFOPLIST_KEY_NSMicrophoneUsageDescription = "Uses your microphone for transcribing audio";
- "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
- "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
- "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
- "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
- IPHONEOS_DEPLOYMENT_TARGET = 17.5;
- LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
- "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
- MACOSX_DEPLOYMENT_TARGET = 14.4;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = net.exolabs.astra;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Release;
- };
- FA3E98B42C725A0300E4E795 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 8NFAS2P4ND;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 17.5;
- MACOSX_DEPLOYMENT_TARGET = 14.4;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = net.exolabs.astraTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
- SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/astra.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/astra";
- };
- name = Debug;
- };
- FA3E98B52C725A0300E4E795 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
- BUNDLE_LOADER = "$(TEST_HOST)";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 8NFAS2P4ND;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 17.5;
- MACOSX_DEPLOYMENT_TARGET = 14.4;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = net.exolabs.astraTests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
- SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_HOST = "$(BUILT_PRODUCTS_DIR)/astra.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/astra";
- };
- name = Release;
- };
- FA3E98B72C725A0300E4E795 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 8NFAS2P4ND;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 17.5;
- MACOSX_DEPLOYMENT_TARGET = 14.4;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = net.exolabs.astraUITests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
- SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_TARGET_NAME = astra;
- };
- name = Debug;
- };
- FA3E98B82C725A0300E4E795 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 8NFAS2P4ND;
- GENERATE_INFOPLIST_FILE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 17.5;
- MACOSX_DEPLOYMENT_TARGET = 14.4;
- MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = net.exolabs.astraUITests;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
- SWIFT_EMIT_LOC_STRINGS = NO;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- TEST_TARGET_NAME = astra;
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- FA3E98862C725A0200E4E795 /* Build configuration list for PBXProject "astra" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- FA3E98AE2C725A0300E4E795 /* Debug */,
- FA3E98AF2C725A0300E4E795 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- FA3E98B02C725A0300E4E795 /* Build configuration list for PBXNativeTarget "astra" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- FA3E98B12C725A0300E4E795 /* Debug */,
- FA3E98B22C725A0300E4E795 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- FA3E98B32C725A0300E4E795 /* Build configuration list for PBXNativeTarget "astraTests" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- FA3E98B42C725A0300E4E795 /* Debug */,
- FA3E98B52C725A0300E4E795 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
- FA3E98B62C725A0300E4E795 /* Build configuration list for PBXNativeTarget "astraUITests" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- FA3E98B72C725A0300E4E795 /* Debug */,
- FA3E98B82C725A0300E4E795 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Release;
- };
-/* End XCConfigurationList section */
-
-/* Begin XCRemoteSwiftPackageReference section */
- FA3E98B92C725BF800E4E795 /* XCRemoteSwiftPackageReference "whisperkit" */ = {
- isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/argmaxinc/whisperkit";
- requirement = {
- branch = main;
- kind = branch;
- };
- };
-/* End XCRemoteSwiftPackageReference section */
-
-/* Begin XCSwiftPackageProductDependency section */
- FA3E98BA2C725BF800E4E795 /* WhisperKit */ = {
- isa = XCSwiftPackageProductDependency;
- package = FA3E98B92C725BF800E4E795 /* XCRemoteSwiftPackageReference "whisperkit" */;
- productName = WhisperKit;
- };
-/* End XCSwiftPackageProductDependency section */
- };
- rootObject = FA3E98832C725A0200E4E795 /* Project object */;
-}
diff --git a/examples/astra/astra.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/astra/astra.xcodeproj/project.xcworkspace/contents.xcworkspacedata
deleted file mode 100644
index 919434a6..00000000
--- a/examples/astra/astra.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Workspace
- version = "1.0">
- <FileRef
- location = "self:">
- </FileRef>
-</Workspace>
diff --git a/examples/astra/astra.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/astra/astra.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
deleted file mode 100644
index 18d98100..00000000
--- a/examples/astra/astra.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>IDEDidComputeMac32BitWarning</key>
- <true/>
-</dict>
-</plist>
diff --git a/examples/astra/astra.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/examples/astra/astra.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
deleted file mode 100644
index edc15da2..00000000
--- a/examples/astra/astra.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "originHash" : "8f61689e55c5551e76f2c686d145061dc1fa621a58cbca576565ebfabc15c894",
- "pins" : [
- {
- "identity" : "swift-argument-parser",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/apple/swift-argument-parser.git",
- "state" : {
- "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41",
- "version" : "1.3.0"
- }
- },
- {
- "identity" : "swift-transformers",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/huggingface/swift-transformers.git",
- "state" : {
- "revision" : "74b94211bdc741694ed7e700a1104c72e5ba68fe",
- "version" : "0.1.7"
- }
- },
- {
- "identity" : "whisperkit",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/argmaxinc/whisperkit",
- "state" : {
- "branch" : "main",
- "revision" : "59aaa4e5f211622f9a5e133440220d9974641d3b"
- }
- }
- ],
- "version" : 3
-}
diff --git a/examples/astra/astra/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/astra/astra/Assets.xcassets/AccentColor.colorset/Contents.json
deleted file mode 100644
index eb878970..00000000
--- a/examples/astra/astra/Assets.xcassets/AccentColor.colorset/Contents.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "colors" : [
- {
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/examples/astra/astra/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/astra/astra/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index 532cd729..00000000
--- a/examples/astra/astra/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,63 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "16x16"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "16x16"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "32x32"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "32x32"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "128x128"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "128x128"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "256x256"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "256x256"
- },
- {
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "512x512"
- },
- {
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "512x512"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/examples/astra/astra/Assets.xcassets/Contents.json b/examples/astra/astra/Assets.xcassets/Contents.json
deleted file mode 100644
index 73c00596..00000000
--- a/examples/astra/astra/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/examples/astra/astra/ContentView.swift b/examples/astra/astra/ContentView.swift
deleted file mode 100644
index 52327fc2..00000000
--- a/examples/astra/astra/ContentView.swift
+++ /dev/null
@@ -1,729 +0,0 @@
-import SwiftUI
-import WhisperKit
-import AVFoundation
-import Foundation
-import Combine
-import Vision
-import AVFAudio
-
-actor CameraActor {
- let captureSession = AVCaptureSession()
- private let photoOutput = AVCapturePhotoOutput()
- private var isConfigured = false
- private var currentPhotoCaptureDelegate: PhotoCaptureDelegate?
-
- func configure() throws {
- guard !isConfigured else {
- print("Camera already configured")
- return
- }
-
- print("Starting camera configuration")
-
- guard let camera = AVCaptureDevice.default(for: .video) else {
- print("No camera device available")
- throw CameraError.cameraUnavailable
- }
-
- do {
- let input = try AVCaptureDeviceInput(device: camera)
- print("Camera input created successfully")
-
- guard captureSession.canAddInput(input) else {
- print("Cannot add camera input to session")
- throw CameraError.cannotAddInputOutput
- }
-
- guard captureSession.canAddOutput(photoOutput) else {
- print("Cannot add photo output to session")
- throw CameraError.cannotAddInputOutput
- }
-
- captureSession.beginConfiguration()
- captureSession.addInput(input)
- captureSession.addOutput(photoOutput)
- captureSession.commitConfiguration()
-
- print("Camera session configured successfully")
-
- Task.detached { [weak self] in
- self?.captureSession.startRunning()
- print("Camera session started running")
- }
-
- isConfigured = true
- print("Camera fully configured and ready")
- } catch {
- print("Error during camera configuration: \(error)")
- throw error
- }
- }
-
- func capturePhoto() async throws -> String {
- guard isConfigured else {
- throw CameraError.notConfigured
- }
-
- return try await withCheckedThrowingContinuation { continuation in
- let photoSettings = AVCapturePhotoSettings()
-
- let delegate = PhotoCaptureDelegate { result in
- self.currentPhotoCaptureDelegate = nil
- continuation.resume(with: result)
- }
-
- self.currentPhotoCaptureDelegate = delegate
-
- Task { @MainActor in
- self.photoOutput.capturePhoto(with: photoSettings, delegate: delegate)
- }
- }
- }
-}
-
-class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
- private let completionHandler: (Result<String, Error>) -> Void
-
- init(completionHandler: @escaping (Result<String, Error>) -> Void) {
- self.completionHandler = completionHandler
- }
-
- func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
- if let error = error {
- completionHandler(.failure(error))
- return
- }
-
- guard let imageData = photo.fileDataRepresentation() else {
- completionHandler(.failure(CameraError.imageProcessingFailed))
- return
- }
-
- let base64String = imageData.base64EncodedString()
- completionHandler(.success(base64String))
- }
-}
-
-enum CameraError: Error {
- case cameraUnavailable
- case cannotAddInputOutput
- case notConfigured
- case imageProcessingFailed
-}
-
-struct CameraPreview: UIViewControllerRepresentable {
- let cameraActor: CameraActor
-
- func makeUIViewController(context: Context) -> UIViewController {
- let viewController = UIViewController()
- let previewLayer = AVCaptureVideoPreviewLayer(session: cameraActor.captureSession)
- previewLayer.videoGravity = .resizeAspectFill
- viewController.view.layer.addSublayer(previewLayer)
- previewLayer.frame = viewController.view.bounds
- return viewController
- }
-
- func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
- if let previewLayer = uiViewController.view.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
- previewLayer.frame = uiViewController.view.bounds
- }
- }
-}
-
-struct ContentView: View {
- @State private var whisperKit: WhisperKit?
- @State private var isListening = false
- @State private var currentText = ""
- @State private var bufferSeconds: Double = 0.5 // or whatever the actual buffer size is
- @State private var modelState: ModelState = .unloaded
-
- @AppStorage("selectedModel") private var selectedModel: String = "large-v3"
- @AppStorage("selectedLanguage") private var selectedLanguage: String = "english"
- @AppStorage("selectedTask") private var selectedTask: String = "transcribe"
-
- @State private var isRecordingMemo = false
- @State private var currentMemo = ""
- @State private var lastVoiceActivityTime = Date()
- @State private var silenceTimer: Timer?
- @State private var voiceActivityThreshold: Float = 0.40
- @State private var silenceTimeThreshold = 1.0
- @State private var debugText = ""
- @State private var apiEndpoint = "http://192.168.212.74:52415/v1/chat/completions"
- @State private var audioBuffer: [Float] = []
- @State private var bufferDuration: Double = 0.5 // 0.5 seconds buffer
- @State private var isInitialTranscription = true
- @State private var streamingResponse = ""
- @State private var cancellables = Set<AnyCancellable>()
-
- @State private var cameraActor: CameraActor?
- @State private var showLiveCamera = false
- @State private var capturedImageBase64: String?
- @State private var errorMessage: String?
- @State private var isCameraReady = false
-
- @State private var speechSynthesizer = AVSpeechSynthesizer()
- @State private var speechBuffer = ""
- @State private var wordCount = 0
- let maxWords = 12
- @State private var originalSilenceThreshold: Float = 0.40
- @State private var isTTSActive: Bool = false
- @State private var canRecordAudio: Bool = true
- @State private var ttsFinishTime: Date?
-
- @State private var isRequestInProgress = false
- @State private var isFirst3WordsOfResponse = true
-
- var body: some View {
- ZStack {
- if showLiveCamera, isCameraReady, let actor = cameraActor {
- CameraPreview(cameraActor: actor)
- .edgesIgnoringSafeArea(.all)
- }
-
- ScrollView {
- VStack {
- Text(currentText)
- .padding()
-
- Text(isListening ? "Listening..." : "Not listening")
- .foregroundColor(isListening ? .green : .red)
-
- if isRecordingMemo {
- Text("Recording...")
- .foregroundColor(.blue)
- }
-
- Picker("Model", selection: $selectedModel) {
- Text("large-v3").tag("large-v3")
- Text("base").tag("base")
- Text("small").tag("small")
- }
- .pickerStyle(SegmentedPickerStyle())
- .padding()
-
- Button("Load Model") {
- loadModel(selectedModel)
- }
- .disabled(modelState == .loaded)
- .padding()
-
- Text("Model State: \(modelState.description)")
-
- Text(debugText)
- .font(.caption)
- .foregroundColor(.gray)
-
- Text("TTS Active: \(isTTSActive ? "Yes" : "No")")
- .font(.caption)
- .foregroundColor(isTTSActive ? .green : .red)
-
- Text("Current Silence Threshold: \(voiceActivityThreshold, specifier: "%.2f")")
- .font(.caption)
- .foregroundColor(.blue)
-
- Text("Original Silence Threshold: \(originalSilenceThreshold, specifier: "%.2f")")
- .font(.caption)
- .foregroundColor(.orange)
-
- Slider(value: $voiceActivityThreshold, in: 0.01...1.0) {
- Text("Voice Activity Threshold: \(voiceActivityThreshold, specifier: "%.2f")")
- }
-
- Text("API Response:")
- .font(.headline)
- .padding(.top)
-
- ScrollView {
- Text(streamingResponse)
- .padding()
- }
- .frame(height: 200)
- .border(Color.gray, width: 1)
-
- Toggle("Show Live Camera", isOn: $showLiveCamera)
- .padding()
- .onChange(of: showLiveCamera) { newValue in
- if newValue {
- Task {
- await setupCamera()
- }
- } else {
- cameraActor = nil
- isCameraReady = false
- print("Camera disabled")
- }
- }
-
- if !showLiveCamera {
- Text("Camera Ready: \(isCameraReady ? "Yes" : "No")")
- .padding()
-
- if let errorMessage = errorMessage {
- Text("Error: \(errorMessage)")
- .foregroundColor(.red)
- .padding()
- }
- }
- }
- }
- .opacity(showLiveCamera ? 0.7 : 1)
- }
- .onAppear {
- setupWhisperKit()
- startTTSMonitoring()
- }
- }
-
- private func setupWhisperKit() {
- Task {
- do {
- whisperKit = try await WhisperKit(verbose: true)
- print("WhisperKit initialized successfully")
- startListening()
- startAudioBuffering()
- } catch {
- print("Error initializing WhisperKit: \(error)")
- }
- }
- }
-
- private func startTTSMonitoring() {
- Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
- let newTTSActive = speechSynthesizer.isSpeaking
- if newTTSActive != isTTSActive {
- isTTSActive = newTTSActive
- canRecordAudio = !newTTSActive
- if isTTSActive {
- voiceActivityThreshold = 1.0 // Set to max to prevent recording
- whisperKit?.audioProcessor.purgeAudioSamples(keepingLast: 0) // Flush audio buffer
- print("TTS Started - Audio recording paused")
- } else {
- ttsFinishTime = Date()
- print("TTS Finished - Waiting 0.5 seconds before resuming audio recording")
- }
- updateDebugText()
- }
-
- if !isTTSActive, let finishTime = ttsFinishTime, Date().timeIntervalSince(finishTime) >= 0.5 {
- whisperKit?.audioProcessor.purgeAudioSamples(keepingLast: 0) // Flush audio buffer
- voiceActivityThreshold = originalSilenceThreshold
- canRecordAudio = true
- ttsFinishTime = nil
- print("Audio recording resumed after TTS delay")
- updateDebugText()
- }
- }
- }
-
- private func updateDebugText() {
- debugText += "\nTTS Active: \(isTTSActive)"
- debugText += "\nCurrent Silence Threshold: \(voiceActivityThreshold)"
- debugText += "\nOriginal Silence Threshold: \(originalSilenceThreshold)"
- debugText += "\n---"
- }
-
- private func startAudioBuffering() {
- Task {
- while true {
- if let samples = whisperKit?.audioProcessor.audioSamples {
- let bufferSize = Int(Double(WhisperKit.sampleRate) * bufferDuration)
- audioBuffer = Array(samples.suffix(bufferSize))
- }
- try await Task.sleep(nanoseconds: 100_000_000) // Update every 0.1 seconds
- }
- }
- }
-
- private func loadModel(_ model: String) {
- Task {
- let success = try await loadModel(selectedModel)
- if success {
- startListening()
- } else {
- print("Model failed to load, cannot start listening")
- }
- }
- }
-
- private func startListening() {
- guard let audioProcessor = whisperKit?.audioProcessor else {
- print("AudioProcessor not available")
- return
- }
-
- do {
- try audioProcessor.startRecordingLive { buffer in
- DispatchQueue.main.async {
- checkVoiceActivity()
- }
- }
- isListening = true
- } catch {
- print("Error starting listening: \(error)")
- }
- }
-
- private func checkVoiceActivity() {
- guard canRecordAudio, let audioProcessor = whisperKit?.audioProcessor else { return }
-
- let voiceDetected = AudioProcessor.isVoiceDetected(
- in: audioProcessor.relativeEnergy,
- nextBufferInSeconds: Float(bufferSeconds),
- silenceThreshold: Float(voiceActivityThreshold)
- )
-
- let energyValuesToConsider = Int(Float(bufferSeconds) / 0.1)
- let nextBufferEnergies = audioProcessor.relativeEnergy.suffix(energyValuesToConsider)
- let numberOfValuesToCheck = max(10, nextBufferEnergies.count - 10)
- let relevantEnergies = Array(nextBufferEnergies.prefix(numberOfValuesToCheck))
-
- debugText = """
- Buffer seconds: \(bufferSeconds)
- Energy values to consider: \(energyValuesToConsider)
- Number of values to check: \(numberOfValuesToCheck)
- Silence threshold: \(voiceActivityThreshold)
- Relevant energies: \(relevantEnergies)
- Max energy: \(relevantEnergies.max() ?? 0)
- Voice detected: \(voiceDetected)
- """
-
- if voiceDetected {
- lastVoiceActivityTime = Date()
- if !isRecordingMemo {
- startNewMemo()
- }
- } else {
- checkSilence()
- }
- }
-
- private func checkSilence() {
- let silenceDuration = Date().timeIntervalSince(lastVoiceActivityTime)
- debugText += "\nSilence duration: \(silenceDuration)"
-
- if silenceDuration > silenceTimeThreshold {
- endCurrentMemo()
- }
- }
-
- private func endCurrentMemo() {
- if isRecordingMemo {
- isRecordingMemo = false
- silenceTimer?.invalidate()
- silenceTimer = nil
- if !currentMemo.isEmpty {
- saveMemoToFile(currentMemo)
- currentMemo = ""
- }
- currentText = ""
- whisperKit?.audioProcessor.purgeAudioSamples(keepingLast: 0)
- print("Ended memo")
- debugText += "\nMemo ended"
- }
- }
-
- private func startNewMemo() {
- isRecordingMemo = true
- currentMemo = ""
- isInitialTranscription = true
- silenceTimer?.invalidate()
- silenceTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
- checkSilence()
- }
- transcribeInRealTime()
- print("Started new memo")
- }
-
- private func transcribeInRealTime() {
- Task {
- while isRecordingMemo {
- if canRecordAudio, let samples = whisperKit?.audioProcessor.audioSamples, samples.count > WhisperKit.sampleRate {
- do {
- let samplesToTranscribe: [Float]
- if isInitialTranscription {
- samplesToTranscribe = audioBuffer + samples
- isInitialTranscription = false
- } else {
- samplesToTranscribe = Array(samples)
- }
-
- let result = try await whisperKit?.transcribe(audioArray: samplesToTranscribe)
- await MainActor.run {
- let newText = result?.first?.text ?? ""
- if !newText.isEmpty {
- currentMemo = newText
- currentText = newText
- }
- }
- } catch {
- print("Transcription error: \(error)")
- }
- }
- try await Task.sleep(nanoseconds: 500_000_000) // Sleep for 0.5 seconds
- }
- }
- }
-
- private func saveMemoToFile(_ memo: String) {
- let dateFormatter = DateFormatter()
- dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
- let fileName = "memo_\(dateFormatter.string(from: Date())).txt"
-
- guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
- print("Unable to access documents directory")
- return
- }
-
- let fileURL = documentsDirectory.appendingPathComponent(fileName)
-
- do {
- try memo.write(to: fileURL, atomically: true, encoding: .utf8)
- print("Memo saved to: \(fileURL.path)")
- } catch {
- print("Error saving memo: \(error)")
- }
-
- Task {
- if !isCameraReady {
- print("Camera not ready, initializing...")
- await setupCamera()
- }
-
- if let imageBase64 = await capturePhotoBase64() {
- sendMemoToAPI(memo, imageBase64: imageBase64)
- } else {
- sendMemoToAPI(memo)
- }
- }
- }
-
- private func setupCamera() async {
- print("Setting up camera...")
- do {
- let actor = CameraActor()
- print("CameraActor instance created")
- try await actor.configure()
- print("Camera configured successfully")
- await MainActor.run {
- self.cameraActor = actor
- self.errorMessage = nil
- self.isCameraReady = true
- print("Camera setup complete, UI updated")
- }
- } catch {
- print("Camera setup failed: \(error)")
- await MainActor.run {
- self.errorMessage = "Failed to initialize camera: \(error.localizedDescription)"
- self.isCameraReady = false
- print("Camera setup failure reflected in UI")
- }
- }
- }
-
- private func capturePhotoBase64() async -> String? {
- print("Attempting to capture photo...")
- if !isCameraReady {
- print("Camera not ready, attempting to initialize...")
- await setupCamera()
- }
-
- guard let actor = cameraActor, isCameraReady else {
- print("Camera not initialized or not ready, cannot capture photo")
- await MainActor.run {
- self.errorMessage = "Camera not initialized or not ready"
- }
- return nil
- }
-
- do {
- let base64String = try await actor.capturePhoto()
- print("Photo captured successfully")
- await MainActor.run {
- self.errorMessage = nil
- }
- return base64String
- } catch {
- print("Error capturing photo: \(error)")
- await MainActor.run {
- self.errorMessage = "Failed to capture photo: \(error.localizedDescription)"
- }
- return nil
- }
- }
-
- private func sendMemoToAPI(_ memo: String, imageBase64: String? = nil) {
- Task {
- guard !isRequestInProgress else {
- print("A request is already in progress. Skipping this one.")
- return
- }
-
- isRequestInProgress = true
- isFirst3WordsOfResponse = true // Reset for new request
- defer { isRequestInProgress = false }
-
- do {
- print("Starting API request for memo: \(memo.prefix(50))...")
-
- guard let url = URL(string: apiEndpoint) else {
- print("Invalid API endpoint URL: \(apiEndpoint)")
- return
- }
-
- var payload: [String: Any] = [
- "model": "llava-1.5-7b-hf",
- "messages": [
- ["role": "user", "content": [
- ["type": "text", "text": "You are a helpful conversational assistant chatting with a Gen Z user using their iPhone for voice transcription and sending images to you with their iPhone camera. Be conversational and concise, with a laid back attitude and be cheerful with humour. User said: " + memo],
- ]]
- ],
- "temperature": 0.7,
- "stream": true
- ]
-
- if let imageBase64 = imageBase64 {
- if var userMessage = (payload["messages"] as? [[String: Any]])?.last,
- var content = userMessage["content"] as? [[String: Any]] {
- content.append(["type": "image_url", "image_url": ["url": "data:image/jpeg;base64,\(imageBase64)"]])
- userMessage["content"] = content
- payload["messages"] = [userMessage]
- }
- }
-
- guard let jsonData = try? JSONSerialization.data(withJSONObject: payload) else {
- print("Failed to serialize JSON payload")
- return
- }
-
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- request.httpBody = jsonData
-
- print("Sending request to \(url.absoluteString)")
-
- await MainActor.run {
- self.streamingResponse = ""
- }
-
- let (bytes, response) = try await URLSession.shared.bytes(for: request)
-
- guard let httpResponse = response as? HTTPURLResponse else {
- print("Invalid response")
- return
- }
-
- print("Response status code: \(httpResponse.statusCode)")
-
- for try await line in bytes.lines {
- print("Received line: \(line)")
- await processStreamLine(line)
- }
-
- print("Stream completed")
- } catch {
- print("Error: \(error.localizedDescription)")
- }
- }
- }
-
- private func processStreamLine(_ line: String) async {
- let jsonString: String
- if line.hasPrefix("data: ") {
- jsonString = String(line.dropFirst(6))
- } else {
- jsonString = line
- }
-
- if jsonString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
- return
- }
-
- if let jsonData = jsonString.data(using: .utf8),
- let json = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any],
- let choices = json["choices"] as? [[String: Any]],
- let firstChoice = choices.first,
- let delta = firstChoice["delta"] as? [String: String],
- let content = delta["content"] {
- print("Extracted content: \(content)")
- await MainActor.run {
- self.streamingResponse += content
- bufferContent(content)
- }
- }
- }
-
- private func bufferContent(_ content: String) {
- speechBuffer += content
- let words = speechBuffer.split(separator: " ")
- wordCount = words.count
-
- if isFirst3WordsOfResponse && wordCount >= 3 {
- isFirst3WordsOfResponse = false
- speakBufferedContent()
- } else if content.contains(".") || content.contains("!") || content.contains("?") || wordCount >= maxWords {
- speakBufferedContent()
- }
- }
-
- private func speakBufferedContent() {
- guard !speechBuffer.isEmpty else { return }
- speakContent(speechBuffer)
- speechBuffer = ""
- wordCount = 0
- }
-
- private func speakContent(_ content: String) {
- let utterance = AVSpeechUtterance(string: content)
- utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
- utterance.rate = 0.5
- speechSynthesizer.speak(utterance)
- }
-
- private func loadModel(_ model: String) async throws -> Bool {
- guard let whisperKit = whisperKit else {
- print("WhisperKit instance not initialized")
- return false
- }
- modelState = .loading
- do {
- print("Starting to load model: \(model)")
- try await whisperKit.loadModels()
- await MainActor.run {
- modelState = .loaded
- print("Model loaded successfully: \(model)")
- }
- return true
- } catch {
- print("Error loading model: \(error)")
- await MainActor.run { modelState = .unloaded }
- return false
- }
- }
-
- private func capturePhoto() async {
- print("Attempting to capture photo...")
- print("Camera ready: \(isCameraReady), CameraActor exists: \(cameraActor != nil)")
- guard let actor = cameraActor, isCameraReady else {
- print("Camera not initialized or not ready, cannot capture photo")
- await MainActor.run {
- self.errorMessage = "Camera not initialized or not ready"
- }
- return
- }
-
- do {
- let base64String = try await actor.capturePhoto()
- print("Photo captured successfully")
- await MainActor.run {
- self.capturedImageBase64 = base64String
- self.errorMessage = nil
- }
- } catch {
- print("Error capturing photo: \(error)")
- await MainActor.run {
- self.errorMessage = "Failed to capture photo: \(error.localizedDescription)"
- }
- }
- }
-}
diff --git a/examples/astra/astra/Preview Content/Preview Assets.xcassets/Contents.json b/examples/astra/astra/Preview Content/Preview Assets.xcassets/Contents.json
deleted file mode 100644
index 73c00596..00000000
--- a/examples/astra/astra/Preview Content/Preview Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/examples/astra/astra/astra.entitlements b/examples/astra/astra/astra.entitlements
deleted file mode 100644
index ee23443d..00000000
--- a/examples/astra/astra/astra.entitlements
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>com.apple.developer.kernel.increased-memory-limit</key>
- <true/>
- <key>com.apple.security.app-sandbox</key>
- <true/>
- <key>com.apple.security.device.audio-input</key>
- <true/>
- <key>com.apple.security.files.downloads.read-only</key>
- <true/>
- <key>com.apple.security.files.user-selected.read-write</key>
- <true/>
- <key>com.apple.security.network.client</key>
- <true/>
- <key>com.apple.security.network.server</key>
- <true/>
- <key>com.apple.security.device.camera</key>
- <true/>
-</dict>
-</plist>
diff --git a/examples/astra/astra/astraApp.swift b/examples/astra/astra/astraApp.swift
deleted file mode 100644
index 79cfe707..00000000
--- a/examples/astra/astra/astraApp.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-// astraApp.swift
-// astra
-//
-// Created by Alex on 18/08/2024.
-//
-
-import SwiftUI
-
-@main
-struct astraApp: App {
- var body: some Scene {
- WindowGroup {
- ContentView()
- }
- }
-}
diff --git a/examples/astra/astraTests/astraTests.swift b/examples/astra/astraTests/astraTests.swift
deleted file mode 100644
index 982e0c91..00000000
--- a/examples/astra/astraTests/astraTests.swift
+++ /dev/null
@@ -1,35 +0,0 @@
-//
-// astraTests.swift
-// astraTests
-//
-// Created by Alex on 18/08/2024.
-//
-
-import XCTest
-
-final class astraTests: XCTestCase {
-
- override func setUpWithError() throws {
- // Put setup code here. This method is called before the invocation of each test method in the class.
- }
-
- override func tearDownWithError() throws {
- // Put teardown code here. This method is called after the invocation of each test method in the class.
- }
-
- func testExample() throws {
- // This is an example of a functional test case.
- // Use XCTAssert and related functions to verify your tests produce the correct results.
- // Any test you write for XCTest can be annotated as throws and async.
- // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
- // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
- }
-
- func testPerformanceExample() throws {
- // This is an example of a performance test case.
- measure {
- // Put the code you want to measure the time of here.
- }
- }
-
-}
diff --git a/examples/astra/astraUITests/astraUITests.swift b/examples/astra/astraUITests/astraUITests.swift
deleted file mode 100644
index 97a293d9..00000000
--- a/examples/astra/astraUITests/astraUITests.swift
+++ /dev/null
@@ -1,41 +0,0 @@
-//
-// astraUITests.swift
-// astraUITests
-//
-// Created by Alex on 18/08/2024.
-//
-
-import XCTest
-
-final class astraUITests: XCTestCase {
-
- override func setUpWithError() throws {
- // Put setup code here. This method is called before the invocation of each test method in the class.
-
- // In UI tests it is usually best to stop immediately when a failure occurs.
- continueAfterFailure = false
-
- // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
- }
-
- override func tearDownWithError() throws {
- // Put teardown code here. This method is called after the invocation of each test method in the class.
- }
-
- func testExample() throws {
- // UI tests must launch the application that they test.
- let app = XCUIApplication()
- app.launch()
-
- // Use XCTAssert and related functions to verify your tests produce the correct results.
- }
-
- func testLaunchPerformance() throws {
- if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
- // This measures how long it takes to launch your application.
- measure(metrics: [XCTApplicationLaunchMetric()]) {
- XCUIApplication().launch()
- }
- }
- }
-}
diff --git a/examples/astra/astraUITests/astraUITestsLaunchTests.swift b/examples/astra/astraUITests/astraUITestsLaunchTests.swift
deleted file mode 100644
index d05a1484..00000000
--- a/examples/astra/astraUITests/astraUITestsLaunchTests.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-//
-// astraUITestsLaunchTests.swift
-// astraUITests
-//
-// Created by Alex on 18/08/2024.
-//
-
-import XCTest
-
-final class astraUITestsLaunchTests: XCTestCase {
-
- override class var runsForEachTargetApplicationUIConfiguration: Bool {
- true
- }
-
- override func setUpWithError() throws {
- continueAfterFailure = false
- }
-
- func testLaunch() throws {
- let app = XCUIApplication()
- app.launch()
-
- // Insert steps here to perform after app launch but before taking a screenshot,
- // such as logging into a test account or navigating somewhere in the app
-
- let attachment = XCTAttachment(screenshot: app.screenshot())
- attachment.name = "Launch Screen"
- attachment.lifetime = .keepAlways
- add(attachment)
- }
-}
diff --git a/examples/chatgpt_api.sh b/examples/chatgpt_api.sh
deleted file mode 100755
index 44401836..00000000
--- a/examples/chatgpt_api.sh
+++ /dev/null
@@ -1,39 +0,0 @@
-# exo provides an API that aims to be a drop-in replacements for the ChatGPT-API.
-# This example shows how you can use the API first without streaming and second with streaming.
-# This works the same in a single-node set up and in a multi-node setup.
-# You need to start exo before running this by running `python3 main.py`.
-
-API_ENDPOINT="http://${API_ENDPOINT:-$(ifconfig | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | head -n 1):52415}"
-MODEL="llama-3.1-8b"
-PROMPT="What is the meaning of exo?"
-TEMPERATURE=0.7
-
-echo ""
-echo ""
-echo "--- Output without streaming:"
-echo ""
-curl "${API_ENDPOINT}/v1/chat/completions" --silent \
- -H "Content-Type: application/json" \
- -d '{
- "model": "'"${MODEL}"'",
- "messages": [{"role": "user", "content": "'"${PROMPT}"'"}],
- "temperature": '"${TEMPERATURE}"'
- }'
-
-echo ""
-echo ""
-echo "--- Output with streaming:"
-echo ""
-curl "${API_ENDPOINT}/v1/chat/completions" --silent \
- -H "Content-Type: application/json" \
- -d '{
- "model": "'"${MODEL}"'",
- "messages": [{"role": "user", "content": "'"${PROMPT}"'"}],
- "temperature": '"${TEMPERATURE}"',
- "stream": true
- }' | while read -r line; do
- if [[ $line == data:* ]]; then
- content=$(echo "$line" | sed 's/^data: //')
- echo "$content" | jq -r '.choices[].delta.content' --unbuffered | tr -d '\n'
- fi
- done
\ No newline at end of file
diff --git a/examples/function_calling.py b/examples/function_calling.py
deleted file mode 100644
index dcab2a8d..00000000
--- a/examples/function_calling.py
+++ /dev/null
@@ -1,111 +0,0 @@
-import json
-import re
-import requests
-
-def get_current_weather(location: str, unit: str = "celsius"):
- """Mock weather data function"""
- # Hardcoded response for demo purposes
- return {
- "location": location,
- "temperature": 22 if unit == "celsius" else 72,
- "unit": unit,
- "forecast": "Sunny with light clouds"
- }
-
-def try_parse_tool_calls(content: str):
- """Try parse the tool calls."""
- tool_calls = []
- offset = 0
- for i, m in enumerate(re.finditer(r"<tool_call>\n(.+)?\n</tool_call>", content)):
- if i == 0:
- offset = m.start()
- try:
- func = json.loads(m.group(1))
- tool_calls.append({"type": "function", "function": func})
- if isinstance(func["arguments"], str):
- func["arguments"] = json.loads(func["arguments"])
- except json.JSONDecodeError as e:
- print(f"Failed to parse tool calls: the content is {m.group(1)} and {e}")
- pass
- if tool_calls:
- if offset > 0 and content[:offset].strip():
- c = content[:offset]
- else:
- c = ""
- return {"role": "assistant", "content": c, "tool_calls": tool_calls}
- return {"role": "assistant", "content": re.sub(r"<\|im_end\|>$", "", content)}
-
-def chat_completion(messages):
- """Send chat completion request to local server"""
- response = requests.post(
- "http://localhost:52415/v1/chat/completions",
- json={
- "model": "qwen-2.5-1.5b",
- "messages": messages,
- "tools": [{
- "type": "function",
- "function": {
- "name": "get_current_weather",
- "description": "Get the current weather in a given location",
- "parameters": {
- "type": "object",
- "properties": {
- "location": {
- "type": "string",
- "description": "The city and state, e.g. San Francisco, CA"
- },
- "unit": {
- "type": "string",
- "enum": ["celsius", "fahrenheit"]
- }
- },
- "required": ["location"]
- }
- }
- }],
- "tool_choice": "auto"
- }
- )
- return response.json()
-
-def main():
- # Initial conversation
- messages = [{
- "role": "user",
- "content": "Hi there, what's the weather in Boston?"
- }]
-
- # Get initial response
- response = chat_completion(messages)
- print(f"First response: {response}")
- assistant_message = try_parse_tool_calls(response["choices"][0]["message"]["content"])
- messages.append(assistant_message)
-
- # If there are tool calls, execute them and continue conversation
- if "tool_calls" in assistant_message:
- for tool_call in assistant_message["tool_calls"]:
- if tool_call["function"]["name"] == "get_current_weather":
- args = tool_call["function"]["arguments"]
- weather_data = get_current_weather(**args)
-
- # Add tool response to messages
- messages.append({
- "role": "tool",
- "content": json.dumps(weather_data),
- "name": tool_call["function"]["name"]
- })
-
- # Get final response with weather data
- response = chat_completion(messages)
- print(f"Final response: {response}")
- messages.append({
- "role": "assistant",
- "content": response["choices"][0]["message"]["content"]
- })
-
- # Print full conversation
- for msg in messages:
- print(f"\n{msg['role'].upper()}: {msg['content']}")
-
-if __name__ == "__main__":
- main()
diff --git a/exo/__init__.py b/exo/__init__.py
deleted file mode 100644
index 596559f3..00000000
--- a/exo/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from exo.helpers import DEBUG as DEBUG, DEBUG_DISCOVERY as DEBUG_DISCOVERY, VERSION as VERSION
\ No newline at end of file
diff --git a/exo/api/__init__.py b/exo/api/__init__.py
deleted file mode 100644
index 660e7507..00000000
--- a/exo/api/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from exo.api.chatgpt_api import ChatGPTAPI as ChatGPTAPI
diff --git a/exo/api/chatgpt_api.py b/exo/api/chatgpt_api.py
deleted file mode 100644
index 1020fdbc..00000000
--- a/exo/api/chatgpt_api.py
+++ /dev/null
@@ -1,645 +0,0 @@
-import uuid
-import time
-import asyncio
-import json
-import os
-from pathlib import Path
-from transformers import AutoTokenizer
-from typing import List, Literal, Union, Dict, Optional
-from aiohttp import web
-import aiohttp_cors
-import traceback
-import signal
-from exo import DEBUG, VERSION
-from exo.helpers import PrefixDict, shutdown, get_exo_images_dir
-from exo.inference.tokenizers import resolve_tokenizer
-from exo.orchestration import Node
-from exo.models import build_base_shard, build_full_shard, model_cards, get_repo, get_supported_models, get_pretty_name
-from typing import Callable, Optional
-from PIL import Image
-import numpy as np
-import base64
-from io import BytesIO
-import platform
-from exo.download.download_progress import RepoProgressEvent
-from exo.download.new_shard_download import delete_model
-import tempfile
-from exo.apputil import create_animation_mp4
-from collections import defaultdict
-
-if platform.system().lower() == "darwin" and platform.machine().lower() == "arm64":
- import mlx.core as mx
-else:
- import numpy as mx
-
-
-class Message:
- def __init__(self, role: str, content: Union[str, List[Dict[str, Union[str, Dict[str, str]]]]], tools: Optional[List[Dict]] = None):
- self.role = role
- self.content = content
- self.tools = tools
-
- def to_dict(self):
- data = {"role": self.role, "content": self.content}
- if self.tools:
- data["tools"] = self.tools
- return data
-
-
-class ChatCompletionRequest:
- def __init__(self, model: str, messages: List[Message], temperature: float, tools: Optional[List[Dict]] = None):
- self.model = model
- self.messages = messages
- self.temperature = temperature
- self.tools = tools
-
- def to_dict(self):
- return {"model": self.model, "messages": [message.to_dict() for message in self.messages], "temperature": self.temperature, "tools": self.tools}
-
-
-def generate_completion(
- chat_request: ChatCompletionRequest,
- tokenizer,
- prompt: str,
- request_id: str,
- tokens: List[int],
- stream: bool,
- finish_reason: Union[Literal["length", "stop"], None],
- object_type: Literal["chat.completion", "text_completion"],
-) -> dict:
- completion = {
- "id": f"chatcmpl-{request_id}",
- "object": object_type,
- "created": int(time.time()),
- "model": chat_request.model,
- "system_fingerprint": f"exo_{VERSION}",
- "choices": [{
- "index": 0,
- "message": {"role": "assistant", "content": tokenizer.decode(tokens)},
- "logprobs": None,
- "finish_reason": finish_reason,
- }],
- }
-
- if not stream:
- completion["usage"] = {
- "prompt_tokens": len(tokenizer.encode(prompt)),
- "completion_tokens": len(tokens),
- "total_tokens": len(tokenizer.encode(prompt)) + len(tokens),
- }
-
- choice = completion["choices"][0]
- if object_type.startswith("chat.completion"):
- key_name = "delta" if stream else "message"
- choice[key_name] = {"role": "assistant", "content": tokenizer.decode(tokens)}
- elif object_type == "text_completion":
- choice["text"] = tokenizer.decode(tokens)
- else:
- ValueError(f"Unsupported response type: {object_type}")
-
- return completion
-
-
-def remap_messages(messages: List[Message]) -> List[Message]:
- remapped_messages = []
- last_image = None
- for message in messages:
- if not isinstance(message.content, list):
- remapped_messages.append(message)
- continue
-
- remapped_content = []
- for content in message.content:
- if isinstance(content, dict):
- if content.get("type") in ["image_url", "image"]:
- image_url = content.get("image_url", {}).get("url") or content.get("image")
- if image_url:
- last_image = {"type": "image", "image": image_url}
- remapped_content.append({"type": "text", "text": "[An image was uploaded but is not displayed here]"})
- else:
- remapped_content.append(content)
- else:
- remapped_content.append(content)
- remapped_messages.append(Message(role=message.role, content=remapped_content))
-
- if last_image:
- # Replace the last image placeholder with the actual image content
- for message in reversed(remapped_messages):
- for i, content in enumerate(message.content):
- if isinstance(content, dict):
- if content.get("type") == "text" and content.get("text") == "[An image was uploaded but is not displayed here]":
- message.content[i] = last_image
- return remapped_messages
-
- return remapped_messages
-
-
-def build_prompt(tokenizer, _messages: List[Message], tools: Optional[List[Dict]] = None):
- messages = remap_messages(_messages)
- chat_template_args = {"conversation": [m.to_dict() for m in messages], "tokenize": False, "add_generation_prompt": True}
- if tools:
- chat_template_args["tools"] = tools
-
- try:
- prompt = tokenizer.apply_chat_template(**chat_template_args)
- if DEBUG >= 3: print(f"!!! Prompt: {prompt}")
- return prompt
- except UnicodeEncodeError:
- # Handle Unicode encoding by ensuring everything is UTF-8
- chat_template_args["conversation"] = [
- {k: v.encode('utf-8').decode('utf-8') if isinstance(v, str) else v
- for k, v in m.to_dict().items()}
- for m in messages
- ]
- prompt = tokenizer.apply_chat_template(**chat_template_args)
- if DEBUG >= 3: print(f"!!! Prompt (UTF-8 encoded): {prompt}")
- return prompt
-
-
-def parse_message(data: dict):
- if "role" not in data or "content" not in data:
- raise ValueError(f"Invalid message: {data}. Must have 'role' and 'content'")
- return Message(data["role"], data["content"], data.get("tools"))
-
-
-def parse_chat_request(data: dict, default_model: str):
- return ChatCompletionRequest(
- data.get("model", default_model),
- [parse_message(msg) for msg in data["messages"]],
- data.get("temperature", 0.0),
- data.get("tools", None),
- )
-
-
-class PromptSession:
- def __init__(self, request_id: str, timestamp: int, prompt: str):
- self.request_id = request_id
- self.timestamp = timestamp
- self.prompt = prompt
-
-
-class ChatGPTAPI:
- def __init__(
- self,
- node: Node,
- inference_engine_classname: str,
- response_timeout: int = 90,
- on_chat_completion_request: Callable[[str, ChatCompletionRequest, str], None] = None,
- default_model: Optional[str] = None,
- system_prompt: Optional[str] = None
- ):
- self.node = node
- self.inference_engine_classname = inference_engine_classname
- self.response_timeout = response_timeout
- self.on_chat_completion_request = on_chat_completion_request
- self.app = web.Application(client_max_size=100*1024*1024) # 100MB to support image upload
- self.prompts: PrefixDict[str, PromptSession] = PrefixDict()
- self.prev_token_lens: Dict[str, int] = {}
- self.stream_tasks: Dict[str, asyncio.Task] = {}
- self.default_model = default_model or "llama-3.2-1b"
- self.token_queues = defaultdict(asyncio.Queue)
-
- # Get the callback system and register our handler
- self.token_callback = node.on_token.register("chatgpt-api-token-handler")
- self.token_callback.on_next(lambda _request_id, tokens, is_finished: asyncio.create_task(self.handle_tokens(_request_id, tokens, is_finished)))
- self.system_prompt = system_prompt
-
- cors = aiohttp_cors.setup(self.app)
- cors_options = aiohttp_cors.ResourceOptions(
- allow_credentials=True,
- expose_headers="*",
- allow_headers="*",
- allow_methods="*",
- )
- cors.add(self.app.router.add_get("/models", self.handle_get_models), {"*": cors_options})
- cors.add(self.app.router.add_get("/v1/models", self.handle_get_models), {"*": cors_options})
- cors.add(self.app.router.add_post("/chat/token/encode", self.handle_post_chat_token_encode), {"*": cors_options})
- cors.add(self.app.router.add_post("/v1/chat/token/encode", self.handle_post_chat_token_encode), {"*": cors_options})
- cors.add(self.app.router.add_post("/chat/completions", self.handle_post_chat_completions), {"*": cors_options})
- cors.add(self.app.router.add_post("/v1/chat/completions", self.handle_post_chat_completions), {"*": cors_options})
- cors.add(self.app.router.add_post("/v1/image/generations", self.handle_post_image_generations), {"*": cors_options})
- cors.add(self.app.router.add_get("/v1/download/progress", self.handle_get_download_progress), {"*": cors_options})
- cors.add(self.app.router.add_get("/modelpool", self.handle_model_support), {"*": cors_options})
- cors.add(self.app.router.add_get("/healthcheck", self.handle_healthcheck), {"*": cors_options})
- cors.add(self.app.router.add_post("/quit", self.handle_quit), {"*": cors_options})
- cors.add(self.app.router.add_delete("/models/{model_name}", self.handle_delete_model), {"*": cors_options})
- cors.add(self.app.router.add_get("/initial_models", self.handle_get_initial_models), {"*": cors_options})
- cors.add(self.app.router.add_post("/create_animation", self.handle_create_animation), {"*": cors_options})
- cors.add(self.app.router.add_post("/download", self.handle_post_download), {"*": cors_options})
- cors.add(self.app.router.add_get("/v1/topology", self.handle_get_topology), {"*": cors_options})
- cors.add(self.app.router.add_get("/topology", self.handle_get_topology), {"*": cors_options})
-
- # Add static routes
- if "__compiled__" not in globals():
- self.static_dir = Path(__file__).parent.parent/"tinychat"
- self.app.router.add_get("/", self.handle_root)
- self.app.router.add_static("/", self.static_dir, name="static")
-
- # Always add images route, regardless of compilation status
- self.images_dir = get_exo_images_dir()
- self.images_dir.mkdir(parents=True, exist_ok=True)
- self.app.router.add_static('/images/', self.images_dir, name='static_images')
-
- self.app.middlewares.append(self.timeout_middleware)
- self.app.middlewares.append(self.log_request)
-
- async def handle_quit(self, request):
- if DEBUG >= 1: print("Received quit signal")
- response = web.json_response({"detail": "Quit signal received"}, status=200)
- await response.prepare(request)
- await response.write_eof()
- await shutdown(signal.SIGINT, asyncio.get_event_loop(), self.node.server)
-
- async def timeout_middleware(self, app, handler):
- async def middleware(request):
- try:
- return await asyncio.wait_for(handler(request), timeout=self.response_timeout)
- except asyncio.TimeoutError:
- return web.json_response({"detail": "Request timed out"}, status=408)
-
- return middleware
-
- async def log_request(self, app, handler):
- async def middleware(request):
- if DEBUG >= 2: print(f"Received request: {request.method} {request.path}")
- return await handler(request)
-
- return middleware
-
- async def handle_root(self, request):
- return web.FileResponse(self.static_dir/"index.html")
-
- async def handle_healthcheck(self, request):
- return web.json_response({"status": "ok"})
-
- async def handle_model_support(self, request):
- try:
- response = web.StreamResponse(status=200, reason='OK', headers={ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' })
- await response.prepare(request)
- async for path, s in self.node.shard_downloader.get_shard_download_status(self.inference_engine_classname):
- model_data = { s.shard.model_id: { "downloaded": s.downloaded_bytes == s.total_bytes, "download_percentage": 100 if s.downloaded_bytes == s.total_bytes else 100 * float(s.downloaded_bytes) / float(s.total_bytes), "total_size": s.total_bytes, "total_downloaded": s.downloaded_bytes } }
- await response.write(f"data: {json.dumps(model_data)}\n\n".encode())
- await response.write(b"data: [DONE]\n\n")
- return response
-
- except Exception as e:
- print(f"Error in handle_model_support: {str(e)}")
- traceback.print_exc()
- return web.json_response({"detail": f"Server error: {str(e)}"}, status=500)
-
- async def handle_get_models(self, request):
- models_list = [{"id": model_name, "object": "model", "owned_by": "exo", "ready": True} for model_name, _ in model_cards.items()]
- return web.json_response({"object": "list", "data": models_list})
-
- async def handle_post_chat_token_encode(self, request):
- data = await request.json()
- model = data.get("model", self.default_model)
- if model and model.startswith("gpt-"): # Handle gpt- model requests
- model = self.default_model
- if not model or model not in model_cards:
- if DEBUG >= 1: print(f"Invalid model: {model}. Supported: {list(model_cards.keys())}. Defaulting to {self.default_model}")
- model = self.default_model
- shard = build_base_shard(model, self.inference_engine_classname)
- messages = [parse_message(msg) for msg in data.get("messages", [])]
- tokenizer = await resolve_tokenizer(get_repo(shard.model_id, self.inference_engine_classname))
- prompt = build_prompt(tokenizer, messages, data.get("tools", None))
- tokens = tokenizer.encode(prompt)
- return web.json_response({
- "length": len(prompt),
- "num_tokens": len(tokens),
- "encoded_tokens": tokens,
- "encoded_prompt": prompt,
- })
-
- async def handle_get_download_progress(self, request):
- progress_data = {}
- for node_id, progress_event in self.node.node_download_progress.items():
- if isinstance(progress_event, RepoProgressEvent):
- if progress_event.status != "in_progress": continue
- progress_data[node_id] = progress_event.to_dict()
- else:
- print(f"Unknown progress event type: {type(progress_event)}. {progress_event}")
- return web.json_response(progress_data)
-
- async def handle_post_chat_completions(self, request):
- data = await request.json()
- if DEBUG >= 2: print(f"[ChatGPTAPI] Handling chat completions request from {request.remote}: {data}")
- stream = data.get("stream", False)
- chat_request = parse_chat_request(data, self.default_model)
- if chat_request.model and chat_request.model.startswith("gpt-"): # to be compatible with ChatGPT tools, point all gpt- model requests to default model
- chat_request.model = self.default_model
- if not chat_request.model or chat_request.model not in model_cards:
- if DEBUG >= 1: print(f"[ChatGPTAPI] Invalid model: {chat_request.model}. Supported: {list(model_cards.keys())}. Defaulting to {self.default_model}")
- chat_request.model = self.default_model
- shard = build_base_shard(chat_request.model, self.inference_engine_classname)
- if not shard:
- supported_models = [model for model, info in model_cards.items() if self.inference_engine_classname in info.get("repo", {})]
- return web.json_response(
- {"detail": f"Unsupported model: {chat_request.model} with inference engine {self.inference_engine_classname}. Supported models for this engine: {supported_models}"},
- status=400,
- )
-
- tokenizer = await resolve_tokenizer(get_repo(shard.model_id, self.inference_engine_classname))
- if DEBUG >= 4: print(f"[ChatGPTAPI] Resolved tokenizer: {tokenizer}")
-
- # Add system prompt if set
- if self.system_prompt and not any(msg.role == "system" for msg in chat_request.messages):
- chat_request.messages.insert(0, Message("system", self.system_prompt))
-
- prompt = build_prompt(tokenizer, chat_request.messages, chat_request.tools)
- request_id = str(uuid.uuid4())
- if self.on_chat_completion_request:
- try:
- self.on_chat_completion_request(request_id, chat_request, prompt)
- except Exception as e:
- if DEBUG >= 2: traceback.print_exc()
-
- if DEBUG >= 2: print(f"[ChatGPTAPI] Processing prompt: {request_id=} {shard=} {prompt=}")
-
- try:
- await asyncio.wait_for(asyncio.shield(asyncio.create_task(self.node.process_prompt(shard, prompt, request_id=request_id))), timeout=self.response_timeout)
-
- if DEBUG >= 2: print(f"[ChatGPTAPI] Waiting for response to finish. timeout={self.response_timeout}s")
-
- if stream:
- response = web.StreamResponse(
- status=200,
- reason="OK",
- headers={
- "Content-Type": "text/event-stream",
- "Cache-Control": "no-cache",
- },
- )
- await response.prepare(request)
-
- try:
- # Stream tokens while waiting for inference to complete
- while True:
- if DEBUG >= 2: print(f"[ChatGPTAPI] Waiting for token from queue: {request_id=}")
- tokens, is_finished = await asyncio.wait_for(
- self.token_queues[request_id].get(),
- timeout=self.response_timeout
- )
- if DEBUG >= 2: print(f"[ChatGPTAPI] Got token from queue: {request_id=} {tokens=} {is_finished=}")
-
- eos_token_id = None
- if not eos_token_id and hasattr(tokenizer, "eos_token_id"): eos_token_id = tokenizer.eos_token_id
- if not eos_token_id and hasattr(tokenizer, "_tokenizer"): eos_token_id = tokenizer.special_tokens_map.get("eos_token_id")
-
- finish_reason = None
- if is_finished: finish_reason = "stop" if tokens[-1] == eos_token_id else "length"
- if DEBUG >= 2: print(f"{eos_token_id=} {tokens[-1]=} {finish_reason=}")
-
- completion = generate_completion(
- chat_request,
- tokenizer,
- prompt,
- request_id,
- tokens,
- stream,
- finish_reason,
- "chat.completion",
- )
-
- await response.write(f"data: {json.dumps(completion)}\n\n".encode())
-
- if is_finished:
- break
-
- await response.write_eof()
- return response
-
- except asyncio.TimeoutError:
- if DEBUG >= 2: print(f"[ChatGPTAPI] Timeout waiting for token: {request_id=}")
- return web.json_response({"detail": "Response generation timed out"}, status=408)
-
- except Exception as e:
- if DEBUG >= 2:
- print(f"[ChatGPTAPI] Error processing prompt: {e}")
- traceback.print_exc()
- return web.json_response(
- {"detail": f"Error processing prompt: {str(e)}"},
- status=500
- )
-
- finally:
- # Clean up the queue for this request
- if request_id in self.token_queues:
- if DEBUG >= 2: print(f"[ChatGPTAPI] Cleaning up token queue: {request_id=}")
- del self.token_queues[request_id]
- else:
- tokens = []
- while True:
- _tokens, is_finished = await asyncio.wait_for(self.token_queues[request_id].get(), timeout=self.response_timeout)
- tokens.extend(_tokens)
- if is_finished:
- break
- finish_reason = "length"
- eos_token_id = None
- if not eos_token_id and hasattr(tokenizer, "eos_token_id"): eos_token_id = tokenizer.eos_token_id
- if not eos_token_id and hasattr(tokenizer, "_tokenizer"): eos_token_id = tokenizer.special_tokens_map.get("eos_token_id")
- if DEBUG >= 2: print(f"Checking if end of tokens result {tokens[-1]=} is {eos_token_id=}")
- if tokens[-1] == eos_token_id:
- finish_reason = "stop"
-
- return web.json_response(generate_completion(chat_request, tokenizer, prompt, request_id, tokens, stream, finish_reason, "chat.completion"))
- except asyncio.TimeoutError:
- return web.json_response({"detail": "Response generation timed out"}, status=408)
- except Exception as e:
- if DEBUG >= 2: traceback.print_exc()
- return web.json_response({"detail": f"Error processing prompt (see logs with DEBUG>=2): {str(e)}"}, status=500)
-
- async def handle_post_image_generations(self, request):
- data = await request.json()
-
- if DEBUG >= 2: print(f"Handling chat completions request from {request.remote}: {data}")
- stream = data.get("stream", False)
- model = data.get("model", "")
- prompt = data.get("prompt", "")
- image_url = data.get("image_url", "")
- if DEBUG >= 2: print(f"model: {model}, prompt: {prompt}, stream: {stream}")
- shard = build_base_shard(model, self.inference_engine_classname)
- if DEBUG >= 2: print(f"shard: {shard}")
- if not shard:
- return web.json_response({"error": f"Unsupported model: {model} with inference engine {self.inference_engine_classname}"}, status=400)
-
- request_id = str(uuid.uuid4())
- callback_id = f"chatgpt-api-wait-response-{request_id}"
- callback = self.node.on_token.register(callback_id)
- try:
- if image_url != "" and image_url != None:
- img = self.base64_decode(image_url)
- else:
- img = None
- await asyncio.wait_for(asyncio.shield(asyncio.create_task(self.node.process_prompt(shard, prompt, request_id=request_id, inference_state={"image": img}))), timeout=self.response_timeout)
-
- response = web.StreamResponse(status=200, reason='OK', headers={
- 'Content-Type': 'application/octet-stream',
- "Cache-Control": "no-cache",
- })
- await response.prepare(request)
-
- def get_progress_bar(current_step, total_steps, bar_length=50):
- # Calculate the percentage of completion
- percent = float(current_step)/total_steps
- # Calculate the number of hashes to display
- arrow = '-'*int(round(percent*bar_length) - 1) + '>'
- spaces = ' '*(bar_length - len(arrow))
-
- # Create the progress bar string
- progress_bar = f'Progress: [{arrow}{spaces}] {int(percent * 100)}% ({current_step}/{total_steps})'
- return progress_bar
-
- async def stream_image(_request_id: str, result, is_finished: bool):
- if isinstance(result, list):
- await response.write(json.dumps({'progress': get_progress_bar((result[0]), (result[1]))}).encode('utf-8') + b'\n')
-
- elif isinstance(result, np.ndarray):
- try:
- im = Image.fromarray(np.array(result))
- # Save the image to a file
- image_filename = f"{_request_id}.png"
- image_path = self.images_dir/image_filename
- im.save(image_path)
-
- # Get URL for the saved image
- try:
- image_url = request.app.router['static_images'].url_for(filename=image_filename)
- base_url = f"{request.scheme}://{request.host}"
- full_image_url = base_url + str(image_url)
-
- await response.write(json.dumps({'images': [{'url': str(full_image_url), 'content_type': 'image/png'}]}).encode('utf-8') + b'\n')
- except KeyError as e:
- if DEBUG >= 2: print(f"Error getting image URL: {e}")
- # Fallback to direct file path if URL generation fails
- await response.write(json.dumps({'images': [{'url': str(image_path), 'content_type': 'image/png'}]}).encode('utf-8') + b'\n')
-
- if is_finished:
- await response.write_eof()
-
- except Exception as e:
- if DEBUG >= 2: print(f"Error processing image: {e}")
- if DEBUG >= 2: traceback.print_exc()
- await response.write(json.dumps({'error': str(e)}).encode('utf-8') + b'\n')
-
- stream_task = None
-
- def on_result(_request_id: str, result, is_finished: bool):
- nonlocal stream_task
- stream_task = asyncio.create_task(stream_image(_request_id, result, is_finished))
- return _request_id == request_id and is_finished
-
- await callback.wait(on_result, timeout=self.response_timeout*10)
-
- if stream_task:
- # Wait for the stream task to complete before returning
- await stream_task
-
- return response
-
- except Exception as e:
- if DEBUG >= 2: traceback.print_exc()
- return web.json_response({"detail": f"Error processing prompt (see logs with DEBUG>=2): {str(e)}"}, status=500)
-
- async def handle_delete_model(self, request):
- model_id = request.match_info.get('model_name')
- try:
- if await delete_model(model_id, self.inference_engine_classname): return web.json_response({"status": "success", "message": f"Model {model_id} deleted successfully"})
- else: return web.json_response({"detail": f"Model {model_id} files not found"}, status=404)
- except Exception as e:
- if DEBUG >= 2: traceback.print_exc()
- return web.json_response({"detail": f"Error deleting model: {str(e)}"}, status=500)
-
- async def handle_get_initial_models(self, request):
- model_data = {}
- for model_id in get_supported_models([[self.inference_engine_classname]]):
- model_data[model_id] = {
- "name": get_pretty_name(model_id),
- "downloaded": None, # Initially unknown
- "download_percentage": None, # Change from 0 to null
- "total_size": None,
- "total_downloaded": None,
- "loading": True # Add loading state
- }
- return web.json_response(model_data)
-
- async def handle_create_animation(self, request):
- try:
- data = await request.json()
- replacement_image_path = data.get("replacement_image_path")
- device_name = data.get("device_name", "Local Device")
- prompt_text = data.get("prompt", "")
-
- if DEBUG >= 2: print(f"Creating animation with params: replacement_image={replacement_image_path}, device={device_name}, prompt={prompt_text}")
-
- if not replacement_image_path:
- return web.json_response({"error": "replacement_image_path is required"}, status=400)
-
- # Create temp directory if it doesn't exist
- tmp_dir = Path(tempfile.gettempdir())/"exo_animations"
- tmp_dir.mkdir(parents=True, exist_ok=True)
-
- # Generate unique output filename in temp directory
- output_filename = f"animation_{uuid.uuid4()}.mp4"
- output_path = str(tmp_dir/output_filename)
-
- if DEBUG >= 2: print(f"Animation temp directory: {tmp_dir}, output file: {output_path}, directory exists: {tmp_dir.exists()}, directory permissions: {oct(tmp_dir.stat().st_mode)[-3:]}")
-
- # Create the animation
- create_animation_mp4(replacement_image_path, output_path, device_name, prompt_text)
-
- return web.json_response({"status": "success", "output_path": output_path})
-
- except Exception as e:
- if DEBUG >= 2: traceback.print_exc()
- return web.json_response({"error": str(e)}, status=500)
-
- async def handle_post_download(self, request):
- try:
- data = await request.json()
- model_name = data.get("model")
- if not model_name: return web.json_response({"error": "model parameter is required"}, status=400)
- if model_name not in model_cards: return web.json_response({"error": f"Invalid model: {model_name}. Supported models: {list(model_cards.keys())}"}, status=400)
- shard = build_full_shard(model_name, self.inference_engine_classname)
- if not shard: return web.json_response({"error": f"Could not build shard for model {model_name}"}, status=400)
- asyncio.create_task(self.node.inference_engine.shard_downloader.ensure_shard(shard, self.inference_engine_classname))
-
- return web.json_response({"status": "success", "message": f"Download started for model: {model_name}"})
- except Exception as e:
- if DEBUG >= 2: traceback.print_exc()
- return web.json_response({"error": str(e)}, status=500)
-
- async def handle_get_topology(self, request):
- try:
- topology = self.node.current_topology
- if topology:
- return web.json_response(topology.to_json())
- else:
- return web.json_response({})
- except Exception as e:
- if DEBUG >= 2: traceback.print_exc()
- return web.json_response({"detail": f"Error getting topology: {str(e)}"}, status=500)
-
- async def handle_tokens(self, request_id: str, tokens: List[int], is_finished: bool):
- await self.token_queues[request_id].put((tokens, is_finished))
-
- async def run(self, host: str = "0.0.0.0", port: int = 52415):
- runner = web.AppRunner(self.app)
- await runner.setup()
- site = web.TCPSite(runner, host, port)
- await site.start()
-
- def base64_decode(self, base64_string):
- #decode and reshape image
- if base64_string.startswith('data:image'):
- base64_string = base64_string.split(',')[1]
- image_data = base64.b64decode(base64_string)
- img = Image.open(BytesIO(image_data))
- W, H = (dim - dim%64 for dim in (img.width, img.height))
- if W != img.width or H != img.height:
- if DEBUG >= 2: print(f"Warning: image shape is not divisible by 64, downsampling to {W}x{H}")
- img = img.resize((W, H), Image.NEAREST) # use desired downsampling filter
- img = mx.array(np.array(img))
- img = (img[:, :, :3].astype(mx.float32)/255)*2 - 1
- img = img[None]
- return img
diff --git a/exo/apputil/__init__.py b/exo/apputil/__init__.py
deleted file mode 100644
index b6ac6e25..00000000
--- a/exo/apputil/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from exo.apputil.anim import create_animation_mp4
\ No newline at end of file
diff --git a/exo/apputil/anim.py b/exo/apputil/anim.py
deleted file mode 100644
index b64aace3..00000000
--- a/exo/apputil/anim.py
+++ /dev/null
@@ -1,168 +0,0 @@
-from PIL import Image, ImageDraw, ImageFont, ImageFilter
-import os
-import numpy as np
-import cv2
-import sys
-
-def draw_rounded_rectangle(draw, coords, radius, fill):
- left, top, right, bottom = coords
- diameter = radius * 2
- draw.rectangle([left + radius, top, right - radius, bottom], fill=fill)
- draw.rectangle([left, top + radius, right, bottom - radius], fill=fill)
- draw.pieslice([left, top, left + diameter, top + diameter], 180, 270, fill=fill)
- draw.pieslice([right - diameter, top, right, top + diameter], 270, 360, fill=fill)
- draw.pieslice([left, bottom - diameter, left + diameter, bottom], 90, 180, fill=fill)
- draw.pieslice([right - diameter, bottom - diameter, right, bottom], 0, 90, fill=fill)
-
-def draw_centered_text_rounded(draw, text, font, rect_coords, radius=10, text_color="yellow", bg_color=(43,33,44)):
- bbox = font.getbbox(text)
- text_width = bbox[2] - bbox[0]
- text_height = bbox[3] - bbox[1]
- rect_left, rect_top, rect_right, rect_bottom = rect_coords
- rect_width = rect_right - rect_left
- rect_height = rect_bottom - rect_top
- text_x = rect_left + (rect_width - text_width) // 2
- text_y = rect_top + (rect_height - text_height) // 2
- draw_rounded_rectangle(draw, rect_coords, radius, bg_color)
- draw.text((text_x, text_y), text, fill=text_color, font=font)
-
-def draw_left_aligned_text_rounded(draw, text, font, rect_coords, padding_left=20, radius=10, text_color="yellow", bg_color=(43,33,44)):
- bbox = font.getbbox(text)
- text_height = bbox[3] - bbox[1]
- rect_left, rect_top, rect_right, rect_bottom = rect_coords
- rect_height = rect_bottom - rect_top
- text_y = rect_top + (rect_height - text_height) // 2
- text_x = rect_left + padding_left
- draw_rounded_rectangle(draw, rect_coords, radius, bg_color)
- draw.text((text_x, text_y), text, fill=text_color, font=font)
-
-def draw_right_text_dynamic_width_rounded(draw, text, font, base_coords, padding=20, radius=10, text_color="yellow", bg_color=(43,33,44)):
- bbox = font.getbbox(text)
- text_width = bbox[2] - bbox[0]
- text_height = bbox[3] - bbox[1]
- _, rect_top, rect_right, rect_bottom = base_coords
- rect_height = rect_bottom - rect_top
- new_rect_left = rect_right - (text_width + (padding * 2))
- text_y = rect_top + (rect_height - text_height) // 2
- text_x = new_rect_left + padding
- draw_rounded_rectangle(draw, (new_rect_left, rect_top, rect_right, rect_bottom), radius, bg_color)
- draw.text((text_x, text_y), text, fill=text_color, font=font)
- return new_rect_left
-
-def draw_progress_bar(draw, progress, coords, color="yellow", bg_color=(70, 70, 70)):
- left, top, right, bottom = coords
- total_width = right - left
- draw.rectangle(coords, fill=bg_color)
- progress_width = int(total_width * progress)
- if progress_width > 0:
- draw.rectangle((left, top, left + progress_width, bottom), fill=color)
-
-def crop_image(image, top_crop=70):
- width, height = image.size
- return image.crop((0, top_crop, width, height))
-
-def create_animation_mp4(
- replacement_image_path,
- output_path,
- device_name,
- prompt_text,
- fps=30,
- target_size=(512, 512),
- target_position=(139, 755),
- progress_coords=(139, 1285, 655, 1295),
- device_coords=(1240, 370, 1640, 416),
- prompt_coords=(332, 1702, 2662, 1745)
-):
- frames = []
- try:
- font = ImageFont.truetype("/System/Library/Fonts/SFNSMono.ttf", 20)
- promptfont = ImageFont.truetype("/System/Library/Fonts/SFNSMono.ttf", 24)
- except:
- font = ImageFont.load_default()
- promptfont = ImageFont.load_default()
-
- # Get the base directory for images when running as a bundled app
- if hasattr(sys, '_MEIPASS'):
- base_dir = os.path.join(sys._MEIPASS, "exo", "apputil", "baseimages")
- else:
- base_dir = os.path.join(os.path.dirname(__file__), "baseimages")
-
- # Process first frame
- base_img = Image.open(os.path.join(base_dir, "image1.png"))
- draw = ImageDraw.Draw(base_img)
- draw_centered_text_rounded(draw, device_name, font, device_coords)
- frames.extend([crop_image(base_img)] * 30) # 1 second at 30fps
-
- # Process second frame with typing animation
- base_img2 = Image.open(os.path.join(base_dir, "image2.png"))
- for i in range(len(prompt_text) + 1):
- current_frame = base_img2.copy()
- draw = ImageDraw.Draw(current_frame)
- draw_centered_text_rounded(draw, device_name, font, device_coords)
- if i > 0: # Only draw if we have at least one character
- draw_left_aligned_text_rounded(draw, prompt_text[:i], promptfont, prompt_coords)
- frames.extend([crop_image(current_frame)] * 2) # 2 frames per character for smooth typing
-
- # Hold the complete prompt for a moment
- frames.extend([frames[-1]] * 30) # Hold for 1 second
-
- # Create blur sequence
- replacement_img = Image.open(replacement_image_path)
- base_img = Image.open(os.path.join(base_dir, "image3.png"))
- blur_steps = [int(80 * (1 - i/8)) for i in range(9)]
-
- for i, blur_amount in enumerate(blur_steps):
- new_frame = base_img.copy()
- draw = ImageDraw.Draw(new_frame)
-
- replacement_copy = replacement_img.copy()
- replacement_copy.thumbnail(target_size, Image.Resampling.LANCZOS)
- if blur_amount > 0:
- replacement_copy = replacement_copy.filter(ImageFilter.GaussianBlur(radius=blur_amount))
-
- mask = replacement_copy.split()[-1] if replacement_copy.mode in ('RGBA', 'LA') else None
- new_frame.paste(replacement_copy, target_position, mask)
-
- draw_progress_bar(draw, (i + 1) / 9, progress_coords)
- draw_centered_text_rounded(draw, device_name, font, device_coords)
- draw_right_text_dynamic_width_rounded(draw, prompt_text, promptfont, (None, 590, 2850, 685), padding=30)
-
- frames.extend([crop_image(new_frame)] * 15) # 0.5 seconds at 30fps
-
- # Create and add final frame (image4)
- final_base = Image.open(os.path.join(base_dir, "image4.png"))
- draw = ImageDraw.Draw(final_base)
-
- draw_centered_text_rounded(draw, device_name, font, device_coords)
- draw_right_text_dynamic_width_rounded(draw, prompt_text, promptfont, (None, 590, 2850, 685), padding=30)
-
- replacement_copy = replacement_img.copy()
- replacement_copy.thumbnail(target_size, Image.Resampling.LANCZOS)
- mask = replacement_copy.split()[-1] if replacement_copy.mode in ('RGBA', 'LA') else None
- final_base.paste(replacement_copy, target_position, mask)
-
- frames.extend([crop_image(final_base)] * 30) # 1 second at 30fps
-
- # Convert frames to video using H.264 codec
- if frames:
- first_frame = np.array(frames[0])
- height, width = first_frame.shape[:2]
- fourcc = cv2.VideoWriter_fourcc(*'avc1')
- out = cv2.VideoWriter(
- output_path,
- fourcc,
- fps,
- (width, height),
- isColor=True
- )
-
- if not out.isOpened():
- print("Error: VideoWriter failed to open")
- return
-
- for frame in frames:
- frame_array = cv2.cvtColor(np.array(frame), cv2.COLOR_RGB2BGR)
- out.write(frame_array)
-
- out.release()
- print(f"Video saved successfully to {output_path}")
diff --git a/exo/apputil/baseimages/image1.png b/exo/apputil/baseimages/image1.png
deleted file mode 100644
index b8375e7e..00000000
--- a/exo/apputil/baseimages/image1.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:361fdadd67c277d45cd18b0bfc8c5ceea5fd89f2d65aef157fd915ce9cbb8599
-size 814460
diff --git a/exo/apputil/baseimages/image2.png b/exo/apputil/baseimages/image2.png
deleted file mode 100644
index b2d4f31c..00000000
--- a/exo/apputil/baseimages/image2.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:f0e3891bc6b4f4dfa7444af53fcaa4b3ba06b0549546202be3243f08a0e6bd7e
-size 814235
diff --git a/exo/apputil/baseimages/image3.png b/exo/apputil/baseimages/image3.png
deleted file mode 100644
index 9ee38a57..00000000
--- a/exo/apputil/baseimages/image3.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a2dc5b3378aef397d60fd1252da8a1c578ad97e202a859590ffa416b49551d19
-size 146633
diff --git a/exo/apputil/baseimages/image4.png b/exo/apputil/baseimages/image4.png
deleted file mode 100644
index e0959d7b..00000000
--- a/exo/apputil/baseimages/image4.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:dbc6883e2a3c5233ec7b844c98646922bdc4f5e42e1f424857eaff56f785dbcd
-size 668550
diff --git a/exo/download/__init__.py b/exo/download/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/download/download_progress.py b/exo/download/download_progress.py
deleted file mode 100644
index c02733a3..00000000
--- a/exo/download/download_progress.py
+++ /dev/null
@@ -1,65 +0,0 @@
-from typing import Dict, Callable, Coroutine, Any, Literal
-from exo.inference.shard import Shard
-from dataclasses import dataclass
-from datetime import timedelta
-
-
-@dataclass
-class RepoFileProgressEvent:
- repo_id: str
- repo_revision: str
- file_path: str
- downloaded: int
- downloaded_this_session: int
- total: int
- speed: int
- eta: timedelta
- status: Literal["not_started", "in_progress", "complete"]
- start_time: float
-
- def to_dict(self):
- return {
- "repo_id": self.repo_id, "repo_revision": self.repo_revision, "file_path": self.file_path, "downloaded": self.downloaded, "downloaded_this_session": self.downloaded_this_session,
- "total": self.total, "speed": self.speed, "eta": self.eta.total_seconds(), "status": self.status, "start_time": self.start_time
- }
-
- @classmethod
- def from_dict(cls, data):
- if 'eta' in data: data['eta'] = timedelta(seconds=data['eta'])
- return cls(**data)
-
-
-@dataclass
-class RepoProgressEvent:
- shard: Shard
- repo_id: str
- repo_revision: str
- completed_files: int
- total_files: int
- downloaded_bytes: int
- downloaded_bytes_this_session: int
- total_bytes: int
- overall_speed: int
- overall_eta: timedelta
- file_progress: Dict[str, RepoFileProgressEvent]
- status: Literal["not_started", "in_progress", "complete"]
-
- def to_dict(self):
- return {
- "shard": self.shard.to_dict(), "repo_id": self.repo_id, "repo_revision": self.repo_revision, "completed_files": self.completed_files, "total_files": self.total_files, "downloaded_bytes": self.downloaded_bytes,
- "downloaded_bytes_this_session": self.downloaded_bytes_this_session, "total_bytes": self.total_bytes, "overall_speed": self.overall_speed, "overall_eta": self.overall_eta.total_seconds(),
- "file_progress": {k: v.to_dict()
- for k, v in self.file_progress.items()}, "status": self.status
- }
-
- @classmethod
- def from_dict(cls, data):
- if 'overall_eta' in data: data['overall_eta'] = timedelta(seconds=data['overall_eta'])
- if 'file_progress' in data: data['file_progress'] = {k: RepoFileProgressEvent.from_dict(v) for k, v in data['file_progress'].items()}
- if 'shard' in data: data['shard'] = Shard.from_dict(data['shard'])
-
- return cls(**data)
-
-
-RepoFileProgressCallback = Callable[[RepoFileProgressEvent], Coroutine[Any, Any, None]]
-RepoProgressCallback = Callable[[RepoProgressEvent], Coroutine[Any, Any, None]]
diff --git a/exo/download/hf/__init__.py b/exo/download/hf/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/download/hf/hf_helpers.py b/exo/download/hf/hf_helpers.py
deleted file mode 100644
index 01cd8650..00000000
--- a/exo/download/hf/hf_helpers.py
+++ /dev/null
@@ -1,98 +0,0 @@
-import aiofiles.os as aios
-from typing import Union
-import os
-from typing import Callable, Optional, Dict, List, Union
-from fnmatch import fnmatch
-from pathlib import Path
-from typing import Generator, Iterable, TypeVar
-from exo.helpers import DEBUG
-from exo.inference.shard import Shard
-import aiofiles
-
-T = TypeVar("T")
-
-def filter_repo_objects(
- items: Iterable[T],
- *,
- allow_patterns: Optional[Union[List[str], str]] = None,
- ignore_patterns: Optional[Union[List[str], str]] = None,
- key: Optional[Callable[[T], str]] = None,
-) -> Generator[T, None, None]:
- if isinstance(allow_patterns, str):
- allow_patterns = [allow_patterns]
- if isinstance(ignore_patterns, str):
- ignore_patterns = [ignore_patterns]
- if allow_patterns is not None:
- allow_patterns = [_add_wildcard_to_directories(p) for p in allow_patterns]
- if ignore_patterns is not None:
- ignore_patterns = [_add_wildcard_to_directories(p) for p in ignore_patterns]
-
- if key is None:
- def _identity(item: T) -> str:
- if isinstance(item, str):
- return item
- if isinstance(item, Path):
- return str(item)
- raise ValueError(f"Please provide `key` argument in `filter_repo_objects`: `{item}` is not a string.")
- key = _identity
-
- for item in items:
- path = key(item)
- if allow_patterns is not None and not any(fnmatch(path, r) for r in allow_patterns):
- continue
- if ignore_patterns is not None and any(fnmatch(path, r) for r in ignore_patterns):
- continue
- yield item
-
-def _add_wildcard_to_directories(pattern: str) -> str:
- if pattern[-1] == "/":
- return pattern + "*"
- return pattern
-
-def get_hf_endpoint() -> str:
- return os.environ.get('HF_ENDPOINT', "https://huggingface.co")
-
-def get_hf_home() -> Path:
- """Get the Hugging Face home directory."""
- return Path(os.environ.get("HF_HOME", Path.home()/".cache"/"huggingface"))
-
-async def get_hf_token():
- """Retrieve the Hugging Face token from the user's HF_HOME directory."""
- token_path = get_hf_home()/"token"
- if await aios.path.exists(token_path):
- async with aiofiles.open(token_path, 'r') as f:
- return (await f.read()).strip()
- return None
-
-async def get_auth_headers():
- """Get authentication headers if a token is available."""
- token = await get_hf_token()
- if token:
- return {"Authorization": f"Bearer {token}"}
- return {}
-
-def extract_layer_num(tensor_name: str) -> Optional[int]:
- # This is a simple example and might need to be adjusted based on the actual naming convention
- parts = tensor_name.split('.')
- for part in parts:
- if part.isdigit():
- return int(part)
- return None
-
-def get_allow_patterns(weight_map: Dict[str, str], shard: Shard) -> List[str]:
- default_patterns = set(["*.json", "*.py", "tokenizer.model", "*.tiktoken", "*.txt"])
- shard_specific_patterns = set()
- if weight_map:
- for tensor_name, filename in weight_map.items():
- layer_num = extract_layer_num(tensor_name)
- if layer_num is not None and shard.start_layer <= layer_num <= shard.end_layer:
- shard_specific_patterns.add(filename)
- sorted_file_names = sorted(weight_map.values())
- if shard.is_first_layer():
- shard_specific_patterns.add(sorted_file_names[0])
- elif shard.is_last_layer():
- shard_specific_patterns.add(sorted_file_names[-1])
- else:
- shard_specific_patterns = set(["*.safetensors"])
- if DEBUG >= 3: print(f"get_allow_patterns {weight_map=} {shard=} {shard_specific_patterns=}")
- return list(default_patterns | shard_specific_patterns)
diff --git a/exo/download/new_shard_download.py b/exo/download/new_shard_download.py
deleted file mode 100644
index 02e9f7bf..00000000
--- a/exo/download/new_shard_download.py
+++ /dev/null
@@ -1,307 +0,0 @@
-from exo.inference.shard import Shard
-from exo.models import get_repo
-from pathlib import Path
-from exo.download.hf.hf_helpers import get_hf_endpoint, get_auth_headers, filter_repo_objects, get_allow_patterns
-from exo.download.shard_download import ShardDownloader
-from exo.download.download_progress import RepoProgressEvent, RepoFileProgressEvent
-from exo.helpers import AsyncCallbackSystem, DEBUG
-from exo.models import get_supported_models, build_full_shard
-import os
-import aiofiles.os as aios
-import aiohttp
-import aiofiles
-from urllib.parse import urljoin
-from typing import Callable, Union, Tuple, Dict, List, Optional, Literal, AsyncIterator
-import time
-from datetime import timedelta
-import asyncio
-import json
-import traceback
-import shutil
-import tempfile
-import hashlib
-
-def exo_home() -> Path:
- return Path(os.environ.get("EXO_HOME", Path.home()/".cache"/"exo"))
-
-def exo_tmp() -> Path:
- return Path(tempfile.gettempdir())/"exo"
-
-async def ensure_exo_home() -> Path:
- await aios.makedirs(exo_home(), exist_ok=True)
- return exo_home()
-
-async def ensure_exo_tmp() -> Path:
- await aios.makedirs(exo_tmp(), exist_ok=True)
- return exo_tmp()
-
-async def has_exo_home_read_access() -> bool:
- try: return await aios.access(exo_home(), os.R_OK)
- except OSError: return False
-
-async def has_exo_home_write_access() -> bool:
- try: return await aios.access(exo_home(), os.W_OK)
- except OSError: return False
-
-async def ensure_downloads_dir() -> Path:
- downloads_dir = exo_home()/"downloads"
- await aios.makedirs(downloads_dir, exist_ok=True)
- return downloads_dir
-
-async def delete_model(model_id: str, inference_engine_name: str) -> bool:
- repo_id = get_repo(model_id, inference_engine_name)
- model_dir = await ensure_downloads_dir()/repo_id.replace("/", "--")
- if not await aios.path.exists(model_dir): return False
- await asyncio.to_thread(shutil.rmtree, model_dir, ignore_errors=False)
- return True
-
-async def seed_models(seed_dir: Union[str, Path]):
- """Move model in resources folder of app to .cache/huggingface/hub"""
- source_dir = Path(seed_dir)
- dest_dir = await ensure_downloads_dir()
- for path in source_dir.iterdir():
- if path.is_dir() and path.name.startswith("models--"):
- dest_path = dest_dir/path.name
- if await aios.path.exists(dest_path): print('Skipping moving model to .cache directory')
- else:
- try: await aios.rename(str(path), str(dest_path))
- except:
- print(f"Error seeding model {path} to {dest_path}")
- traceback.print_exc()
-
-async def fetch_file_list_with_cache(repo_id: str, revision: str = "main") -> List[Dict[str, Union[str, int]]]:
- cache_file = (await ensure_exo_tmp())/f"{repo_id.replace('/', '--')}--{revision}--file_list.json"
- if await aios.path.exists(cache_file):
- async with aiofiles.open(cache_file, 'r') as f: return json.loads(await f.read())
- file_list = await fetch_file_list_with_retry(repo_id, revision)
- await aios.makedirs(cache_file.parent, exist_ok=True)
- async with aiofiles.open(cache_file, 'w') as f: await f.write(json.dumps(file_list))
- return file_list
-
-async def fetch_file_list_with_retry(repo_id: str, revision: str = "main", path: str = "") -> List[Dict[str, Union[str, int]]]:
- n_attempts = 30
- for attempt in range(n_attempts):
- try: return await _fetch_file_list(repo_id, revision, path)
- except Exception as e:
- if attempt == n_attempts - 1: raise e
- await asyncio.sleep(min(8, 0.1 * (2 ** attempt)))
-
-async def _fetch_file_list(repo_id: str, revision: str = "main", path: str = "") -> List[Dict[str, Union[str, int]]]:
- api_url = f"{get_hf_endpoint()}/api/models/{repo_id}/tree/{revision}"
- url = f"{api_url}/{path}" if path else api_url
-
- headers = await get_auth_headers()
- async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30, connect=10, sock_read=30, sock_connect=10)) as session:
- async with session.get(url, headers=headers) as response:
- if response.status == 200:
- data = await response.json()
- files = []
- for item in data:
- if item["type"] == "file":
- files.append({"path": item["path"], "size": item["size"]})
- elif item["type"] == "directory":
- subfiles = await _fetch_file_list(repo_id, revision, item["path"])
- files.extend(subfiles)
- return files
- else:
- raise Exception(f"Failed to fetch file list: {response.status}")
-
-async def calc_hash(path: Path, type: Literal["sha1", "sha256"] = "sha1") -> str:
- hash = hashlib.sha1() if type == "sha1" else hashlib.sha256()
- if type == "sha1":
- header = f"blob {(await aios.stat(path)).st_size}\0".encode()
- hash.update(header)
- async with aiofiles.open(path, 'rb') as f:
- while chunk := await f.read(8 * 1024 * 1024):
- hash.update(chunk)
- return hash.hexdigest()
-
-async def file_meta(repo_id: str, revision: str, path: str) -> Tuple[int, str]:
- url = urljoin(f"{get_hf_endpoint()}/{repo_id}/resolve/{revision}/", path)
- headers = await get_auth_headers()
- async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=1800, connect=60, sock_read=1800, sock_connect=60)) as session:
- async with session.head(url, headers=headers) as r:
- content_length = int(r.headers.get('x-linked-size') or r.headers.get('content-length') or 0)
- etag = r.headers.get('X-Linked-ETag') or r.headers.get('ETag') or r.headers.get('Etag')
- assert content_length > 0, f"No content length for {url}"
- assert etag is not None, f"No remote hash for {url}"
- if (etag[0] == '"' and etag[-1] == '"') or (etag[0] == "'" and etag[-1] == "'"): etag = etag[1:-1]
- return content_length, etag
-
-async def download_file_with_retry(repo_id: str, revision: str, path: str, target_dir: Path, on_progress: Callable[[int, int], None] = lambda _, __: None) -> Path:
- n_attempts = 30
- for attempt in range(n_attempts):
- try: return await _download_file(repo_id, revision, path, target_dir, on_progress)
- except Exception as e:
- if isinstance(e, FileNotFoundError) or attempt == n_attempts - 1: raise e
- print(f"Download error on attempt {attempt}/{n_attempts} for {repo_id=} {revision=} {path=} {target_dir=}")
- traceback.print_exc()
- await asyncio.sleep(min(8, 0.1 * (2 ** attempt)))
-
-async def _download_file(repo_id: str, revision: str, path: str, target_dir: Path, on_progress: Callable[[int, int], None] = lambda _, __: None) -> Path:
- if await aios.path.exists(target_dir/path): return target_dir/path
- await aios.makedirs((target_dir/path).parent, exist_ok=True)
- length, etag = await file_meta(repo_id, revision, path)
- remote_hash = etag[:-5] if etag.endswith("-gzip") else etag
- partial_path = target_dir/f"{path}.partial"
- resume_byte_pos = (await aios.stat(partial_path)).st_size if (await aios.path.exists(partial_path)) else None
- if resume_byte_pos != length:
- url = urljoin(f"{get_hf_endpoint()}/{repo_id}/resolve/{revision}/", path)
- headers = await get_auth_headers()
- if resume_byte_pos: headers['Range'] = f'bytes={resume_byte_pos}-'
- n_read = resume_byte_pos or 0
- async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=1800, connect=60, sock_read=1800, sock_connect=60)) as session:
- async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=1800, connect=60, sock_read=1800, sock_connect=60)) as r:
- if r.status == 404: raise FileNotFoundError(f"File not found: {url}")
- assert r.status in [200, 206], f"Failed to download {path} from {url}: {r.status}"
- async with aiofiles.open(partial_path, 'ab' if resume_byte_pos else 'wb') as f:
- while chunk := await r.content.read(8 * 1024 * 1024): on_progress(n_read := n_read + await f.write(chunk), length)
-
- final_hash = await calc_hash(partial_path, type="sha256" if len(remote_hash) == 64 else "sha1")
- integrity = final_hash == remote_hash
- if not integrity:
- try: await aios.remove(partial_path)
- except Exception as e: print(f"Error removing partial file {partial_path}: {e}")
- raise Exception(f"Downloaded file {target_dir/path} has hash {final_hash} but remote hash is {remote_hash}")
- await aios.rename(partial_path, target_dir/path)
- return target_dir/path
-
-
-def calculate_repo_progress(shard: Shard, repo_id: str, revision: str, file_progress: Dict[str, RepoFileProgressEvent], all_start_time: float) -> RepoProgressEvent:
- all_total_bytes = sum([p.total for p in file_progress.values()])
- all_downloaded_bytes = sum([p.downloaded for p in file_progress.values()])
- all_downloaded_bytes_this_session = sum([p.downloaded_this_session for p in file_progress.values()])
- elapsed_time = time.time() - all_start_time
- all_speed = all_downloaded_bytes_this_session / elapsed_time if elapsed_time > 0 else 0
- all_eta = timedelta(seconds=(all_total_bytes - all_downloaded_bytes) / all_speed) if all_speed > 0 else timedelta(seconds=0)
- status = "complete" if all(p.status == "complete" for p in file_progress.values()) else "in_progress" if any(p.status == "in_progress" for p in file_progress.values()) else "not_started"
- return RepoProgressEvent(shard, repo_id, revision, len([p for p in file_progress.values() if p.downloaded == p.total]), len(file_progress), all_downloaded_bytes, all_downloaded_bytes_this_session, all_total_bytes, all_speed, all_eta, file_progress, status)
-
-async def get_weight_map(repo_id: str, revision: str = "main") -> Dict[str, str]:
- target_dir = (await ensure_exo_tmp())/repo_id.replace("/", "--")
- index_file = await download_file_with_retry(repo_id, revision, "model.safetensors.index.json", target_dir)
- async with aiofiles.open(index_file, 'r') as f: index_data = json.loads(await f.read())
- return index_data.get("weight_map")
-
-async def resolve_allow_patterns(shard: Shard, inference_engine_classname: str) -> List[str]:
- try:
- weight_map = await get_weight_map(get_repo(shard.model_id, inference_engine_classname))
- return get_allow_patterns(weight_map, shard)
- except:
- if DEBUG >= 1: print(f"Error getting weight map for {shard.model_id=} and inference engine {inference_engine_classname}")
- if DEBUG >= 1: traceback.print_exc()
- return ["*"]
-
-async def get_downloaded_size(path: Path) -> int:
- partial_path = path.with_suffix(path.suffix + ".partial")
- if await aios.path.exists(path): return (await aios.stat(path)).st_size
- if await aios.path.exists(partial_path): return (await aios.stat(partial_path)).st_size
- return 0
-
-async def download_shard(shard: Shard, inference_engine_classname: str, on_progress: AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]], max_parallel_downloads: int = 8, skip_download: bool = False) -> tuple[Path, RepoProgressEvent]:
- if DEBUG >= 2 and not skip_download: print(f"Downloading {shard.model_id=} for {inference_engine_classname}")
- repo_id = get_repo(shard.model_id, inference_engine_classname)
- revision = "main"
- target_dir = await ensure_downloads_dir()/repo_id.replace("/", "--")
- if not skip_download: await aios.makedirs(target_dir, exist_ok=True)
-
- if repo_id is None:
- raise ValueError(f"No repo found for {shard.model_id=} and inference engine {inference_engine_classname}")
-
- allow_patterns = await resolve_allow_patterns(shard, inference_engine_classname)
- if DEBUG >= 2: print(f"Downloading {shard.model_id=} with {allow_patterns=}")
-
- all_start_time = time.time()
- file_list = await fetch_file_list_with_cache(repo_id, revision)
- filtered_file_list = list(filter_repo_objects(file_list, allow_patterns=allow_patterns, key=lambda x: x["path"]))
- file_progress: Dict[str, RepoFileProgressEvent] = {}
- def on_progress_wrapper(file: dict, curr_bytes: int, total_bytes: int):
- start_time = file_progress[file["path"]].start_time if file["path"] in file_progress else time.time()
- downloaded_this_session = file_progress[file["path"]].downloaded_this_session + (curr_bytes - file_progress[file["path"]].downloaded) if file["path"] in file_progress else curr_bytes
- speed = downloaded_this_session / (time.time() - start_time) if time.time() - start_time > 0 else 0
- eta = timedelta(seconds=(total_bytes - curr_bytes) / speed) if speed > 0 else timedelta(seconds=0)
- file_progress[file["path"]] = RepoFileProgressEvent(repo_id, revision, file["path"], curr_bytes, downloaded_this_session, total_bytes, speed, eta, "complete" if curr_bytes == total_bytes else "in_progress", start_time)
- on_progress.trigger_all(shard, calculate_repo_progress(shard, repo_id, revision, file_progress, all_start_time))
- if DEBUG >= 6: print(f"Downloading {file['path']} {curr_bytes}/{total_bytes} {speed} {eta}")
- for file in filtered_file_list:
- downloaded_bytes = await get_downloaded_size(target_dir/file["path"])
- file_progress[file["path"]] = RepoFileProgressEvent(repo_id, revision, file["path"], downloaded_bytes, 0, file["size"], 0, timedelta(0), "complete" if downloaded_bytes == file["size"] else "not_started", time.time())
-
- semaphore = asyncio.Semaphore(max_parallel_downloads)
- async def download_with_semaphore(file):
- async with semaphore:
- await download_file_with_retry(repo_id, revision, file["path"], target_dir, lambda curr_bytes, total_bytes: on_progress_wrapper(file, curr_bytes, total_bytes))
- if not skip_download: await asyncio.gather(*[download_with_semaphore(file) for file in filtered_file_list])
- final_repo_progress = calculate_repo_progress(shard, repo_id, revision, file_progress, all_start_time)
- on_progress.trigger_all(shard, final_repo_progress)
- if gguf := next((f for f in filtered_file_list if f["path"].endswith(".gguf")), None):
- return target_dir/gguf["path"], final_repo_progress
- else:
- return target_dir, final_repo_progress
-
-def new_shard_downloader(max_parallel_downloads: int = 8) -> ShardDownloader:
- return SingletonShardDownloader(CachedShardDownloader(NewShardDownloader(max_parallel_downloads)))
-
-class SingletonShardDownloader(ShardDownloader):
- def __init__(self, shard_downloader: ShardDownloader):
- self.shard_downloader = shard_downloader
- self.active_downloads: Dict[Shard, asyncio.Task] = {}
-
- @property
- def on_progress(self) -> AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]]:
- return self.shard_downloader.on_progress
-
- async def ensure_shard(self, shard: Shard, inference_engine_name: str) -> Path:
- if shard not in self.active_downloads: self.active_downloads[shard] = asyncio.create_task(self.shard_downloader.ensure_shard(shard, inference_engine_name))
- try: return await self.active_downloads[shard]
- finally:
- if shard in self.active_downloads and self.active_downloads[shard].done(): del self.active_downloads[shard]
-
- async def get_shard_download_status(self, inference_engine_name: str) -> AsyncIterator[tuple[Path, RepoProgressEvent]]:
- async for path, status in self.shard_downloader.get_shard_download_status(inference_engine_name):
- yield path, status
-
-class CachedShardDownloader(ShardDownloader):
- def __init__(self, shard_downloader: ShardDownloader):
- self.shard_downloader = shard_downloader
- self.cache: Dict[tuple[str, Shard], Path] = {}
-
- @property
- def on_progress(self) -> AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]]:
- return self.shard_downloader.on_progress
-
- async def ensure_shard(self, shard: Shard, inference_engine_name: str) -> Path:
- if (inference_engine_name, shard) in self.cache:
- if DEBUG >= 2: print(f"ensure_shard cache hit {shard=} for {inference_engine_name}")
- return self.cache[(inference_engine_name, shard)]
- if DEBUG >= 2: print(f"ensure_shard cache miss {shard=} for {inference_engine_name}")
- target_dir = await self.shard_downloader.ensure_shard(shard, inference_engine_name)
- self.cache[(inference_engine_name, shard)] = target_dir
- return target_dir
-
- async def get_shard_download_status(self, inference_engine_name: str) -> AsyncIterator[tuple[Path, RepoProgressEvent]]:
- async for path, status in self.shard_downloader.get_shard_download_status(inference_engine_name):
- yield path, status
-
-class NewShardDownloader(ShardDownloader):
- def __init__(self, max_parallel_downloads: int = 8):
- self.max_parallel_downloads = max_parallel_downloads
- self._on_progress = AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]]()
-
- @property
- def on_progress(self) -> AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]]:
- return self._on_progress
-
- async def ensure_shard(self, shard: Shard, inference_engine_name: str) -> Path:
- target_dir, _ = await download_shard(shard, inference_engine_name, self.on_progress, max_parallel_downloads=self.max_parallel_downloads)
- return target_dir
-
- async def get_shard_download_status(self, inference_engine_name: str) -> AsyncIterator[tuple[Path, RepoProgressEvent]]:
- if DEBUG >= 2: print("Getting shard download status for", inference_engine_name)
- tasks = [download_shard(build_full_shard(model_id, inference_engine_name), inference_engine_name, self.on_progress, skip_download=True) for model_id in get_supported_models([[inference_engine_name]])]
- for task in asyncio.as_completed(tasks):
- try:
- path, progress = await task
- yield (path, progress)
- except Exception as e:
- print("Error downloading shard:", e)
diff --git a/exo/download/shard_download.py b/exo/download/shard_download.py
deleted file mode 100644
index 9e06e40a..00000000
--- a/exo/download/shard_download.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import Optional, Tuple, Dict, AsyncIterator
-from pathlib import Path
-from exo.inference.shard import Shard
-from exo.download.download_progress import RepoProgressEvent
-from exo.helpers import AsyncCallbackSystem
-
-
-class ShardDownloader(ABC):
- @abstractmethod
- async def ensure_shard(self, shard: Shard, inference_engine_name: str) -> Path:
- """
- Ensures that the shard is downloaded.
- Does not allow multiple overlapping downloads at once.
- If you try to download a Shard which overlaps a Shard that is already being downloaded,
- the download will be cancelled and a new download will start.
-
- Args:
- shard (Shard): The shard to download.
- inference_engine_name (str): The inference engine used on the node hosting the shard
- """
- pass
-
- @property
- @abstractmethod
- def on_progress(self) -> AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]]:
- pass
-
- @abstractmethod
- async def get_shard_download_status(self, inference_engine_name: str) -> AsyncIterator[tuple[Path, RepoProgressEvent]]:
- """Get the download status of shards.
-
- Returns:
- Optional[Dict[str, float]]: A dictionary mapping shard IDs to their download percentage (0-100),
- or None if status cannot be determined
- """
- pass
-
-
-class NoopShardDownloader(ShardDownloader):
- async def ensure_shard(self, shard: Shard, inference_engine_name: str) -> Path:
- return Path("/tmp/noop_shard")
-
- @property
- def on_progress(self) -> AsyncCallbackSystem[str, Tuple[Shard, RepoProgressEvent]]:
- return AsyncCallbackSystem()
-
- async def get_shard_download_status(self, inference_engine_name: str) -> AsyncIterator[tuple[Path, RepoProgressEvent]]:
- if False: yield
diff --git a/exo/download/test_new_shard_download.py b/exo/download/test_new_shard_download.py
deleted file mode 100644
index 57b33c65..00000000
--- a/exo/download/test_new_shard_download.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from exo.download.new_shard_download import NewShardDownloader
-from exo.inference.shard import Shard
-import asyncio
-
-async def test_new_shard_download():
- shard_downloader = NewShardDownloader()
- shard_downloader.on_progress.register("test").on_next(lambda shard, event: print(shard, event))
- await shard_downloader.ensure_shard(Shard(model_id="llama-3.2-1b", start_layer=0, end_layer=0, n_layers=16), "MLXDynamicShardInferenceEngine")
- async for path, shard_status in shard_downloader.get_shard_download_status("MLXDynamicShardInferenceEngine"):
- print("Shard download status:", path, shard_status)
-
-if __name__ == "__main__":
- asyncio.run(test_new_shard_download())
-
diff --git a/exo/helpers.py b/exo/helpers.py
deleted file mode 100644
index ff0205f0..00000000
--- a/exo/helpers.py
+++ /dev/null
@@ -1,372 +0,0 @@
-import os
-import sys
-import asyncio
-from typing import Callable, TypeVar, Optional, Dict, Generic, Tuple, List
-import socket
-import random
-import platform
-import psutil
-import uuid
-from scapy.all import get_if_addr, get_if_list
-import re
-import subprocess
-from pathlib import Path
-import tempfile
-import json
-from concurrent.futures import ThreadPoolExecutor
-import traceback
-
-DEBUG = int(os.getenv("DEBUG", default="0"))
-DEBUG_DISCOVERY = int(os.getenv("DEBUG_DISCOVERY", default="0"))
-VERSION = "0.0.1"
-
-exo_text = r"""
- _____ _____
- / _ \ \/ / _ \
-| __/> < (_) |
- \___/_/\_\___/
- """
-
-# Single shared thread pool for subprocess operations
-subprocess_pool = ThreadPoolExecutor(max_workers=4, thread_name_prefix="subprocess_worker")
-
-
-def get_system_info():
- if psutil.MACOS:
- if platform.machine() == "arm64":
- return "Apple Silicon Mac"
- if platform.machine() in ["x86_64", "i386"]:
- return "Intel Mac"
- return "Unknown Mac architecture"
- if psutil.LINUX:
- return "Linux"
- return "Non-Mac, non-Linux system"
-
-
-def find_available_port(host: str = "", min_port: int = 49152, max_port: int = 65535) -> int:
- used_ports_file = os.path.join(tempfile.gettempdir(), "exo_used_ports")
-
- def read_used_ports():
- if os.path.exists(used_ports_file):
- with open(used_ports_file, "r") as f:
- return [int(line.strip()) for line in f if line.strip().isdigit()]
- return []
-
- def write_used_port(port, used_ports):
- with open(used_ports_file, "w") as f:
- print(used_ports[-19:])
- for p in used_ports[-19:] + [port]:
- f.write(f"{p}\n")
-
- used_ports = read_used_ports()
- available_ports = set(range(min_port, max_port + 1)) - set(used_ports)
-
- while available_ports:
- port = random.choice(list(available_ports))
- if DEBUG >= 2: print(f"Trying to find available port {port=}")
- try:
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
- s.bind((host, port))
- write_used_port(port, used_ports)
- return port
- except socket.error:
- available_ports.remove(port)
-
- raise RuntimeError("No available ports in the specified range")
-
-
-def print_exo():
- print(exo_text)
-
-
-def print_yellow_exo():
- yellow = "\033[93m" # ANSI escape code for yellow
- reset = "\033[0m" # ANSI escape code to reset color
- print(f"{yellow}{exo_text}{reset}")
-
-
-def terminal_link(uri, label=None):
- if label is None:
- label = uri
- parameters = ""
-
- # OSC 8 ; params ; URI ST <name> OSC 8 ;; ST
- escape_mask = "\033]8;{};{}\033\\{}\033]8;;\033\\"
-
- return escape_mask.format(parameters, uri, label)
-
-
-T = TypeVar("T")
-K = TypeVar("K")
-
-
-class AsyncCallback(Generic[T]):
- def __init__(self) -> None:
- self.condition: asyncio.Condition = asyncio.Condition()
- self.result: Optional[Tuple[T, ...]] = None
- self.observers: list[Callable[..., None]] = []
-
- async def wait(self, check_condition: Callable[..., bool], timeout: Optional[float] = None) -> Tuple[T, ...]:
- async with self.condition:
- await asyncio.wait_for(self.condition.wait_for(lambda: self.result is not None and check_condition(*self.result)), timeout)
- assert self.result is not None # for type checking
- return self.result
-
- def on_next(self, callback: Callable[..., None]) -> None:
- self.observers.append(callback)
-
- def set(self, *args: T) -> None:
- self.result = args
- for observer in self.observers:
- observer(*args)
- asyncio.create_task(self.notify())
-
- async def notify(self) -> None:
- async with self.condition:
- self.condition.notify_all()
-
-
-class AsyncCallbackSystem(Generic[K, T]):
- def __init__(self) -> None:
- self.callbacks: Dict[K, AsyncCallback[T]] = {}
-
- def register(self, name: K) -> AsyncCallback[T]:
- if name not in self.callbacks:
- self.callbacks[name] = AsyncCallback[T]()
- return self.callbacks[name]
-
- def deregister(self, name: K) -> None:
- if name in self.callbacks:
- del self.callbacks[name]
-
- def trigger(self, name: K, *args: T) -> None:
- if name in self.callbacks:
- self.callbacks[name].set(*args)
-
- def trigger_all(self, *args: T) -> None:
- for callback in self.callbacks.values():
- callback.set(*args)
-
-
-K = TypeVar('K', bound=str)
-V = TypeVar('V')
-
-
-class PrefixDict(Generic[K, V]):
- def __init__(self):
- self.items: Dict[K, V] = {}
-
- def add(self, key: K, value: V) -> None:
- self.items[key] = value
-
- def find_prefix(self, argument: str) -> List[Tuple[K, V]]:
- return [(key, value) for key, value in self.items.items() if argument.startswith(key)]
-
- def find_longest_prefix(self, argument: str) -> Optional[Tuple[K, V]]:
- matches = self.find_prefix(argument)
- if len(matches) == 0:
- return None
-
- return max(matches, key=lambda x: len(x[0]))
-
-
-def is_valid_uuid(val):
- try:
- uuid.UUID(str(val))
- return True
- except ValueError:
- return False
-
-
-def get_or_create_node_id():
- NODE_ID_FILE = Path(tempfile.gettempdir())/".exo_node_id"
- try:
- if NODE_ID_FILE.is_file():
- with open(NODE_ID_FILE, "r") as f:
- stored_id = f.read().strip()
- if is_valid_uuid(stored_id):
- if DEBUG >= 2: print(f"Retrieved existing node ID: {stored_id}")
- return stored_id
- else:
- if DEBUG >= 2: print("Stored ID is not a valid UUID. Generating a new one.")
-
- new_id = str(uuid.uuid4())
- with open(NODE_ID_FILE, "w") as f:
- f.write(new_id)
-
- if DEBUG >= 2: print(f"Generated and stored new node ID: {new_id}")
- return new_id
- except IOError as e:
- if DEBUG >= 2: print(f"IO error creating node_id: {e}")
- return str(uuid.uuid4())
- except Exception as e:
- if DEBUG >= 2: print(f"Unexpected error creating node_id: {e}")
- return str(uuid.uuid4())
-
-
-def pretty_print_bytes(size_in_bytes: int) -> str:
- if size_in_bytes < 1024:
- return f"{size_in_bytes} B"
- elif size_in_bytes < 1024**2:
- return f"{size_in_bytes / 1024:.2f} KB"
- elif size_in_bytes < 1024**3:
- return f"{size_in_bytes / (1024 ** 2):.2f} MB"
- elif size_in_bytes < 1024**4:
- return f"{size_in_bytes / (1024 ** 3):.2f} GB"
- else:
- return f"{size_in_bytes / (1024 ** 4):.2f} TB"
-
-
-def pretty_print_bytes_per_second(bytes_per_second: int) -> str:
- if bytes_per_second < 1024:
- return f"{bytes_per_second} B/s"
- elif bytes_per_second < 1024**2:
- return f"{bytes_per_second / 1024:.2f} KB/s"
- elif bytes_per_second < 1024**3:
- return f"{bytes_per_second / (1024 ** 2):.2f} MB/s"
- elif bytes_per_second < 1024**4:
- return f"{bytes_per_second / (1024 ** 3):.2f} GB/s"
- else:
- return f"{bytes_per_second / (1024 ** 4):.2f} TB/s"
-
-
-def get_all_ip_addresses_and_interfaces():
- ip_addresses = []
- for interface in get_if_list():
- try:
- ip = get_if_addr(interface)
- if ip.startswith("0.0."): continue
- simplified_interface = re.sub(r'^\\Device\\NPF_', '', interface)
- ip_addresses.append((ip, simplified_interface))
- except:
- if DEBUG >= 1: print(f"Failed to get IP address for interface {interface}")
- if DEBUG >= 1: traceback.print_exc()
- if not ip_addresses:
- if DEBUG >= 1: print("Failed to get any IP addresses. Defaulting to localhost.")
- return [("localhost", "lo")]
- return list(set(ip_addresses))
-
-
-
-async def get_macos_interface_type(ifname: str) -> Optional[Tuple[int, str]]:
- try:
- # Use the shared subprocess_pool
- output = await asyncio.get_running_loop().run_in_executor(
- subprocess_pool, lambda: subprocess.run(['system_profiler', 'SPNetworkDataType', '-json'], capture_output=True, text=True, close_fds=True).stdout
- )
-
- data = json.loads(output)
-
- for interface in data.get('SPNetworkDataType', []):
- if interface.get('interface') == ifname:
- hardware = interface.get('hardware', '').lower()
- type_name = interface.get('type', '').lower()
- name = interface.get('_name', '').lower()
-
- if 'thunderbolt' in name:
- return (5, "Thunderbolt")
- if hardware == 'ethernet' or type_name == 'ethernet':
- if 'usb' in name:
- return (4, "Ethernet [USB]")
- return (4, "Ethernet")
- if hardware == 'airport' or type_name == 'airport' or 'wi-fi' in name:
- return (3, "WiFi")
- if type_name == 'vpn':
- return (1, "External Virtual")
-
- except Exception as e:
- if DEBUG >= 2: print(f"Error detecting macOS interface type: {e}")
-
- return None
-
-
-async def get_interface_priority_and_type(ifname: str) -> Tuple[int, str]:
- # On macOS, try to get interface type using networksetup
- if psutil.MACOS:
- macos_type = await get_macos_interface_type(ifname)
- if macos_type is not None: return macos_type
-
- # Local container/virtual interfaces
- if (ifname.startswith(('docker', 'br-', 'veth', 'cni', 'flannel', 'calico', 'weave')) or 'bridge' in ifname):
- return (7, "Container Virtual")
-
- # Loopback interface
- if ifname.startswith('lo'):
- return (6, "Loopback")
-
- # Traditional detection for non-macOS systems or fallback
- if ifname.startswith(('tb', 'nx', 'ten')):
- return (5, "Thunderbolt")
-
- # Regular ethernet detection
- if ifname.startswith(('eth', 'en')) and not ifname.startswith(('en1', 'en0')):
- return (4, "Ethernet")
-
- # WiFi detection
- if ifname.startswith(('wlan', 'wifi', 'wl')) or ifname in ['en0', 'en1']:
- return (3, "WiFi")
-
- # Non-local virtual interfaces (VPNs, tunnels)
- if ifname.startswith(('tun', 'tap', 'vtun', 'utun', 'gif', 'stf', 'awdl', 'llw')):
- return (1, "External Virtual")
-
- # Other physical interfaces
- return (2, "Other")
-
-
-async def shutdown(signal, loop, server):
- """Gracefully shutdown the server and close the asyncio loop."""
- print(f"Received exit signal {signal.name}...")
- print("Thank you for using exo.")
- print_yellow_exo()
- server_tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
- [task.cancel() for task in server_tasks]
- print(f"Cancelling {len(server_tasks)} outstanding tasks")
- await asyncio.gather(*server_tasks, return_exceptions=True)
- await server.stop()
-
-
-def is_frozen():
- return getattr(sys, 'frozen', False) or os.path.basename(sys.executable) == "exo" \
- or ('Contents/MacOS' in str(os.path.dirname(sys.executable))) \
- or '__nuitka__' in globals() or getattr(sys, '__compiled__', False)
-
-async def get_mac_system_info() -> Tuple[str, str, int]:
- """Get Mac system information using system_profiler."""
- try:
- output = await asyncio.get_running_loop().run_in_executor(
- subprocess_pool,
- lambda: subprocess.check_output(["system_profiler", "SPHardwareDataType"]).decode("utf-8")
- )
-
- model_line = next((line for line in output.split("\n") if "Model Name" in line), None)
- model_id = model_line.split(": ")[1] if model_line else "Unknown Model"
-
- chip_line = next((line for line in output.split("\n") if "Chip" in line), None)
- chip_id = chip_line.split(": ")[1] if chip_line else "Unknown Chip"
-
- memory_line = next((line for line in output.split("\n") if "Memory" in line), None)
- memory_str = memory_line.split(": ")[1] if memory_line else "Unknown Memory"
- memory_units = memory_str.split()
- memory_value = int(memory_units[0])
- memory = memory_value * 1024 if memory_units[1] == "GB" else memory_value
-
- return model_id, chip_id, memory
- except Exception as e:
- if DEBUG >= 2: print(f"Error getting Mac system info: {e}")
- return "Unknown Model", "Unknown Chip", 0
-
-def get_exo_home() -> Path:
- if psutil.WINDOWS: docs_folder = Path(os.environ["USERPROFILE"])/"Documents"
- else: docs_folder = Path.home()/"Documents"
- if not docs_folder.exists(): docs_folder.mkdir(exist_ok=True)
- exo_folder = docs_folder/"Exo"
- if not exo_folder.exists(): exo_folder.mkdir(exist_ok=True)
- return exo_folder
-
-
-def get_exo_images_dir() -> Path:
- exo_home = get_exo_home()
- images_dir = exo_home/"Images"
- if not images_dir.exists(): images_dir.mkdir(exist_ok=True)
- return images_dir
diff --git a/exo/inference/__init__.py b/exo/inference/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/inference/debug_inference_engine.py b/exo/inference/debug_inference_engine.py
deleted file mode 100644
index d81ac023..00000000
--- a/exo/inference/debug_inference_engine.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from exo.inference.inference_engine import InferenceEngine
-from exo.inference.shard import Shard
-from exo.inference.tinygrad.inference import TinygradDynamicShardInferenceEngine
-import asyncio
-import numpy as np
-
-
-# An inference engine should work the same for any number of Shards, as long as the Shards are continuous.
-async def test_inference_engine(inference_engine_1: InferenceEngine, inference_engine_2: InferenceEngine, model_id: str):
- from exo.inference.tinygrad.inference import Tokenizer
- from pathlib import Path
-
- _tokenizer = Tokenizer(str(Path(model_id)/"tokenizer.model"))
-
- prompt = "In a single word only, what is the last name of the president of the United States? "
- resp_full = await inference_engine_1.infer_prompt("A", shard=Shard(model_id=model_id, start_layer=0, end_layer=31, n_layers=32), prompt=prompt)
- token_full = await inference_engine_1.sample(resp_full)
-
- next_resp_full, _ = await inference_engine_1.infer_tensor(
- "A",
- shard=Shard(model_id=model_id, start_layer=0, end_layer=31, n_layers=32),
- input_data=token_full,
- )
-
- resp1, _ = await inference_engine_1.infer_prompt("B", shard=Shard(model_id=model_id, start_layer=0, end_layer=30, n_layers=32), prompt=prompt)
- resp2, _ = await inference_engine_2.infer_tensor(
- "B",
- shard=Shard(model_id=model_id, start_layer=31, end_layer=31, n_layers=32),
- input_data=resp1,
- )
- token2 = await inference_engine_2.sample(resp2)
- resp3, _ = await inference_engine_1.infer_tensor(
- "B",
- shard=Shard(model_id=model_id, start_layer=0, end_layer=30, n_layers=32),
- input_data=token2,
- )
- resp4, _ = await inference_engine_2.infer_tensor(
- "B",
- shard=Shard(model_id=model_id, start_layer=31, end_layer=31, n_layers=32),
- input_data=resp3,
- )
-
- print(f"{resp2=}")
- print(f"full: {_tokenizer.decode(resp_full)}")
- print(f"next full: {_tokenizer.decode(next_resp_full)}")
- print(f"resp2: {_tokenizer.decode(resp2)}")
- print(f"{resp4=}")
- print(f"resp4: {_tokenizer.decode(resp4)}")
-
- assert np.array_equal(resp_full, resp2)
- assert np.array_equal(next_resp_full, resp4)
-
-
-asyncio.run(test_inference_engine(
- TinygradDynamicShardInferenceEngine(),
- TinygradDynamicShardInferenceEngine(),
- "llama3-8b-sfr",
-))
diff --git a/exo/inference/dummy_inference_engine.py b/exo/inference/dummy_inference_engine.py
deleted file mode 100644
index 98cf8894..00000000
--- a/exo/inference/dummy_inference_engine.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from typing import Optional, Tuple, TYPE_CHECKING
-import numpy as np
-from exo.inference.inference_engine import InferenceEngine
-from exo.inference.shard import Shard
-from exo.inference.tokenizers import DummyTokenizer
-
-class DummyInferenceEngine(InferenceEngine):
- def __init__(self):
- self.shard = None
- self.vocab_size = 1000
- self.hidden_size = 256
- self.eos_token_id = 0
- self.latency_mean = 0.1
- self.latency_stddev = 0.02
- self.num_generate_dummy_tokens = 10
- self.tokenizer = DummyTokenizer()
-
- async def encode(self, shard: Shard, prompt: str) -> np.ndarray:
- return np.array(self.tokenizer.encode(prompt))
-
- async def sample(self, x: np.ndarray, temp: float = 0.0, top_p: float = 1.0) -> np.ndarray:
- if x[0] > self.num_generate_dummy_tokens: return np.array([self.tokenizer.eos_token_id])
- return x
-
- async def decode(self, shard: Shard, tokens: np.ndarray) -> str:
- return self.tokenizer.decode(tokens)
-
- async def infer_tensor(self, request_id: str, shard: Shard, input_data: np.ndarray, inference_state: Optional[dict] = None) -> tuple[np.ndarray, Optional[dict]]:
- await self.ensure_shard(shard)
- return input_data + 1 if self.shard.is_last_layer() else input_data, None
-
- async def ensure_shard(self, shard: Shard):
- if self.shard == shard: return
- self.shard = shard
-
- async def load_checkpoint(self, shard: Shard, path: str):
- await self.ensure_shard(shard)
diff --git a/exo/inference/inference_engine.py b/exo/inference/inference_engine.py
deleted file mode 100644
index b6200037..00000000
--- a/exo/inference/inference_engine.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import numpy as np
-import os
-from exo.helpers import DEBUG # Make sure to import DEBUG
-
-from typing import Tuple, Optional
-from abc import ABC, abstractmethod
-from .shard import Shard
-from exo.download.shard_download import ShardDownloader
-
-
-class InferenceEngine(ABC):
- session = {}
-
- @abstractmethod
- async def encode(self, shard: Shard, prompt: str) -> np.ndarray:
- pass
-
- @abstractmethod
- async def sample(self, x: np.ndarray) -> np.ndarray:
- pass
-
- @abstractmethod
- async def decode(self, shard: Shard, tokens: np.ndarray) -> str:
- pass
-
- @abstractmethod
- async def infer_tensor(self, request_id: str, shard: Shard, input_data: np.ndarray, inference_state: Optional[dict] = None) -> tuple[np.ndarray, Optional[dict]]:
- pass
-
- @abstractmethod
- async def load_checkpoint(self, shard: Shard, path: str):
- pass
-
- async def save_checkpoint(self, shard: Shard, path: str):
- pass
-
- async def save_session(self, key, value):
- self.session[key] = value
-
- async def clear_session(self):
- self.session.empty()
-
- async def infer_prompt(self, request_id: str, shard: Shard, prompt: str, inference_state: Optional[dict] = None) -> tuple[np.ndarray, Optional[dict]]:
- tokens = await self.encode(shard, prompt)
- if shard.model_id != 'stable-diffusion-2-1-base':
- x = tokens.reshape(1, -1)
- else:
- x = tokens
- output_data, inference_state = await self.infer_tensor(request_id, shard, x, inference_state)
-
- return output_data, inference_state
-
-
-inference_engine_classes = {
- "mlx": "MLXDynamicShardInferenceEngine",
- "tinygrad": "TinygradDynamicShardInferenceEngine",
- "dummy": "DummyInferenceEngine",
-}
-
-
-def get_inference_engine(inference_engine_name: str, shard_downloader: ShardDownloader):
- if DEBUG >= 2:
- print(f"get_inference_engine called with: {inference_engine_name}")
- if inference_engine_name == "mlx":
- from exo.inference.mlx.sharded_inference_engine import MLXDynamicShardInferenceEngine
-
- return MLXDynamicShardInferenceEngine(shard_downloader)
- elif inference_engine_name == "tinygrad":
- from exo.inference.tinygrad.inference import TinygradDynamicShardInferenceEngine
- import tinygrad.helpers
- tinygrad.helpers.DEBUG.value = int(os.getenv("TINYGRAD_DEBUG", default="0"))
-
- return TinygradDynamicShardInferenceEngine(shard_downloader)
- elif inference_engine_name == "dummy":
- from exo.inference.dummy_inference_engine import DummyInferenceEngine
- return DummyInferenceEngine()
- raise ValueError(f"Unsupported inference engine: {inference_engine_name}")
diff --git a/exo/inference/mlx/__init__.py b/exo/inference/mlx/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/inference/mlx/losses.py b/exo/inference/mlx/losses.py
deleted file mode 100644
index 1052caa7..00000000
--- a/exo/inference/mlx/losses.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import mlx.core as mx
-import mlx.nn as nn
-def length_masked_ce_loss(model, inputs, targets, lengths):
- # Run model on inputs
- logits = model(inputs).astype(mx.float32)
-
- # Mask padding tokens
- length_mask = mx.arange(inputs.shape[1])[None, :] < lengths[:, None]
-
- # Calculate the loss
- ce = nn.losses.cross_entropy(logits, targets) * length_mask
- loss = ce.sum() / length_mask.sum()
-# print(f"| {inputs=}\n| ==>{logits=}\n| ~^~{ce=}\n| == {loss=}")
- return loss
-
-#Naive intermediate layer loss, where we replace the targets with gradients and just multiply the output by the gradients to derive the loss. This is naive and may warrant some further iteration, but will do the job for now
-def back_gradient_loss(model, inputs, gradients, lengths):
- out = model(inputs).astype(mx.float32)
- grad = gradients.astype(mx.float32)
-
- # Mask padding tokens
- length_mask = mx.repeat(mx.arange(inputs.shape[1])[None, :] < lengths[:, None], out.shape[-1]).reshape(out.shape)
-
- masked_sum = (out * length_mask).sum(axis=1)
- gradient_lens = mx.abs(grad * masked_sum)
- loss = gradient_lens.sum() / length_mask.sum()
-# print(f"| {inputs=}\n"
-# + f"| ==>{out=}\n"
-# + f"| ~^~{masked_sum=}\n"
-# + f"| <~>{gradient_lens=}\n"
-# + f"| == {loss=}")
- return loss
-
-loss_fns = {
- "back_gradient": back_gradient_loss,
- "length_masked_ce": length_masked_ce_loss,
-}
diff --git a/exo/inference/mlx/models/StableDiffusionPipeline.py b/exo/inference/mlx/models/StableDiffusionPipeline.py
deleted file mode 100644
index f8e7c054..00000000
--- a/exo/inference/mlx/models/StableDiffusionPipeline.py
+++ /dev/null
@@ -1,307 +0,0 @@
-# Adapted from https://github.com/ml-explore/mlx-examples/blob/main/stable_diffusion/stable_diffusion/__init__.py
-
-import time
-from typing import Optional, Tuple
-import inspect
-
-import mlx.core as mx
-import mlx.nn as nn
-from pathlib import Path
-
-from tqdm import tqdm
-
-from .sd_models.vae import ModelArgs as VAEArgs
-from .sd_models.vae import Autoencoder
-from .sd_models.tokenizer import load_tokenizer
-from .sd_models.clip import CLIPTextModel
-from .sd_models.clip import ModelArgs as CLIPArgs
-from .sd_models.unet import UNetConfig, UNetModel
-
-from dataclasses import dataclass, field
-from exo.inference.shard import Shard
-
-@dataclass
-class DiffusionConfig:
- beta_schedule: str = "scaled_linear"
- beta_start: float = 0.00085
- beta_end: float = 0.012
- num_train_steps: int = 1000
-
- @classmethod
- def from_dict(cls, params):
- return cls(**{k: v for k, v in params.items() if k in inspect.signature(cls).parameters})
-
-
-#Sampler
-def _linspace(a, b, num):
- x = mx.arange(0, num) / (num - 1)
- return (b - a) * x + a
-
-
-def _interp(y, x_new):
- """Interpolate the function defined by (arange(0, len(y)), y) at positions x_new."""
- x_low = x_new.astype(mx.int32)
- x_high = mx.minimum(x_low + 1, len(y) - 1)
-
- y_low = y[x_low]
- y_high = y[x_high]
- delta_x = x_new - x_low
- y_new = y_low * (1 - delta_x) + delta_x * y_high
-
- return y_new
-
-class SimpleEulerSampler:
- """A simple Euler integrator that can be used to sample from our diffusion models.
-
- The method ``step()`` performs one Euler step from x_t to x_t_prev.
- """
-
- def __init__(self, config: DiffusionConfig):
- # Compute the noise schedule
- if config.beta_schedule == "linear":
- betas = _linspace(
- config.beta_start, config.beta_end, config.num_train_steps
- )
- elif config.beta_schedule == "scaled_linear":
- betas = _linspace(
- config.beta_start**0.5, config.beta_end**0.5, config.num_train_steps
- ).square()
- else:
- raise NotImplementedError(f"{config.beta_schedule} is not implemented.")
-
- alphas = 1 - betas
- alphas_cumprod = mx.cumprod(alphas)
-
- self._sigmas = mx.concatenate(
- [mx.zeros(1), ((1 - alphas_cumprod) / alphas_cumprod).sqrt()]
- )
-
- @property
- def max_time(self):
- return len(self._sigmas) - 1
-
- def sample_prior(self, shape, dtype=mx.float32, key=None):
- noise = mx.random.normal(shape, key=key)
- return (
- noise * self._sigmas[-1] * (self._sigmas[-1].square() + 1).rsqrt()
- ).astype(dtype)
-
- def add_noise(self, x, t, key=None):
- noise = mx.random.normal(x.shape, key=key)
- s = self.sigmas(t)
- return (x + noise * s) * (s.square() + 1).rsqrt()
-
- def sigmas(self, t):
- return _interp(self._sigmas, t)
-
- def timesteps(self, num_steps: int, start_time=None, dtype=mx.float32):
- start_time = start_time or (len(self._sigmas) - 1)
- assert 0 < start_time <= (len(self._sigmas) - 1)
- steps = _linspace(start_time, 0, num_steps + 1).astype(dtype)
- return list(zip(steps, steps[1:]))
-
- def current_timestep(self, step, total_steps, start_time=None):
- if step < total_steps:
- steps = self.timesteps(total_steps, start_time)
- return steps[step]
- else:
- return mx.array(0),mx.array(0)
-
- def step(self, eps_pred, x_t, t, t_prev):
- sigma = self.sigmas(t).astype(eps_pred.dtype)
- sigma_prev = self.sigmas(t_prev).astype(eps_pred.dtype)
-
- dt = sigma_prev - sigma
- x_t_prev = (sigma.square() + 1).sqrt() * x_t + eps_pred * dt
-
- x_t_prev = x_t_prev * (sigma_prev.square() + 1).rsqrt()
-
- return x_t_prev
-
-@dataclass
-class ShardConfig:
- model_id:str
- start_layer:int
- end_layer:int
- n_layers:int
-
-@dataclass
-class StableDiffusionConfig:
- model_type:str
- vae:VAEArgs
- text_encoder:CLIPArgs
- scheduler:DiffusionConfig
- unet:UNetConfig
- shard:ShardConfig
-
- @classmethod
- def from_dict(cls, params):
- return cls(**{k: v for k, v in params.items() if k in inspect.signature(cls).parameters})
-
-@dataclass
-class ModelArgs(StableDiffusionConfig):
- shard:Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
- def __post_init__(self):
- if isinstance(self.shard, dict):
- self.shard = Shard(**self.shard)
-
- if not isinstance(self.shard, Shard):
- raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
-
-class Model(nn.Module):
- def __init__(self, config):
- super().__init__()
- self.model_type = config.model_type
- self.config = config
- self.model_path = config.vae['path'].split('/vae')[0]
- self.shard = config.shard
- self.shard_clip, self.shard_encoder, self.shard_unet, self.shard_decoder = model_shards(config.shard)
- self.config_clip=CLIPArgs.from_dict(config.text_encoder['config'])
- if self.shard_clip.start_layer != -1:
- self.text_encoder = CLIPTextModel(self.config_clip, shard=self.shard_clip)
- else:
- self.text_encoder = nn.Identity()
- self.tokenizer = load_tokenizer(Path(self.model_path), "vocab.json", "merges.txt")
- self.diffusion_config = DiffusionConfig.from_dict(config.scheduler['config'])
- self.sampler = SimpleEulerSampler(self.diffusion_config)
- if self.shard_unet.start_layer!=-1:
- self.config_unet = UNetConfig.from_dict(config.unet['config'])
- self.unet = UNetModel(self.config_unet, self.shard_unet)
- else:
- self.unet = nn.Identity()
- self.config_vae=VAEArgs.from_dict(config.vae['config'])
- if self.shard_encoder.start_layer != -1:
- self.encoder=Autoencoder(self.config_vae, self.shard_encoder, "vae_encoder")
- else:
- self.encoder = nn.Identity()
- if self.shard_decoder.start_layer != -1:
- self.decoder=Autoencoder(self.config_vae, self.shard_decoder, "vae_decoder")
- else:
- self.decoder = nn.Identity()
-
- def __call__(self,x, step= 0, cfg_weight: float = 7.5,total_steps=50,conditioning=None,mask=None,residual=None,x_t_prev=None,is_finished=False,is_step_finished=False, image=None, strength=0.65, start_step=None):
- t, t_prev = self.sampler.current_timestep(step=step, total_steps=total_steps, start_time=start_step)
- is_finished = False
- is_step_finished = False
- if t.item()==1000:
- if self.shard_clip.start_layer == 0:
- conditioning = x
- if self.shard_clip.start_layer != -1:
- conditioning, mask= self.text_encoder(conditioning,mask)
- seed = int(time.time())
- mx.random.seed(seed)
- if image is None:
- if self.shard_encoder.is_last_layer():
- x = self.sampler.sample_prior((1, *(64, 64), self.config_vae.latent_channels_in), dtype=mx.float32)
- x_t_prev=x
- start_step = self.sampler.max_time
- else:
- if self.shard_encoder.start_layer != -1:
- image= self.encoder.encode(image)
- if self.shard_encoder.is_last_layer():
- start_step = self.sampler.max_time*strength
- total_steps = int(total_steps*strength)
- image = mx.broadcast_to(image, (1,) + image.shape[1:])
- x_t_prev=self.sampler.add_noise(image, mx.array(start_step))
- image = None
- t, t_prev = self.sampler.current_timestep(step=step, total_steps=total_steps, start_time=start_step)
- # Perform the denoising loop
- if self.shard_unet.start_layer != -1:
- with tqdm(total=total_steps,initial=step+1) as pbar:
- if step<total_steps:
- x = x_t_prev
- if self.shard_unet.is_first_layer():
- x_t_unet = mx.concatenate([x] * 2, axis=0) if cfg_weight> 1 else x
- else:
- x_t_unet = x
- t_unet = mx.broadcast_to(t, [len(x_t_unet)])
- x, residual= self.unet(x_t_unet, t_unet, encoder_x=conditioning, residuals=residual)
- if self.shard_unet.is_last_layer():
- if cfg_weight > 1:
- eps_text, eps_neg = x.split(2)
- eps_pred = eps_neg + cfg_weight * (eps_text - eps_neg)
- x = self.sampler.step(eps_pred, x_t_prev, t, t_prev)
- x_t_prev=x
- mx.eval(x)
-
- if self.shard_decoder.is_last_layer():
- is_step_finished=True
- if self.shard_decoder.start_layer != -1:
- x=self.decoder.decode(x)
- if self.shard_decoder.is_last_layer():
- x = mx.clip(x / 2 + 0.5, 0, 1)
- B, H, W, C = x.shape
- x = x.reshape(1, B // 1, H, W, C).transpose(0, 2, 1, 3, 4)
- x = x.reshape(1 * H, B // 1 * W, C)
- x = (x * 255).astype(mx.uint8)
- if t_prev.item() ==0:
- is_finished=True
- mx.eval(x)
-
- return x, {'conditioning':conditioning, 'mask':mask,'residual':residual,'x_t_prev':x_t_prev,'is_finished':is_finished,'is_step_finished':is_step_finished, 'step':step, 'total_steps':total_steps, 'start_step':start_step, 'image':image}
-
-
- def load(self):
- if self.shard_encoder.start_layer != -1:
- vae_weights = mx.load(self.config_vae.weight_files[0])
- vae_weights = self.encoder.sanitize(vae_weights)
- self.encoder.load_weights(list(vae_weights.items()), strict=True)
- if self.shard_decoder.start_layer != -1:
- vae_weights = mx.load(self.config_vae.weight_files[0])
- vae_weights = self.decoder.sanitize(vae_weights)
- self.decoder.load_weights(list(vae_weights.items()), strict=True)
- if self.shard_clip.start_layer != -1:
- clip_weights = mx.load(self.config_clip.weight_files[0])
- clip_weights = self.text_encoder.sanitize(clip_weights)
- self.text_encoder.load_weights(list(clip_weights.items()), strict=True)
- if self.shard_unet.start_layer !=-1:
- unet_weights = mx.load(self.config_unet.weight_files[0])
- unet_weights = self.unet.sanitize(unet_weights)
- self.unet.load_weights(list(unet_weights.items()), strict=True)
-
-def model_shards(shard:ShardConfig):
- def create_shard(shard, model_ranges):
- start_layer = shard.start_layer
- end_layer = shard.end_layer
-
- shards = {}
-
- for model_name, (range_start, range_end) in model_ranges.items():
- if start_layer < range_end and end_layer >= range_start:
- # Calculate the overlap with the model range
- overlap_start = max(start_layer, range_start)
- overlap_end = min(end_layer, range_end - 1)
-
- # Adjust the layers relative to the model's range
- relative_start = overlap_start - range_start
- relative_end = overlap_end - range_start
- shards[model_name] = Shard(model_name, relative_start, relative_end, range_end - range_start)
- else:
- # If no overlap, create a zero-layer shard
- shards[model_name] = Shard(model_name, -1, -1, range_end - range_start)
-
- return shards
-
- # Define the ranges for different models
- model_ranges = {
- 'clip': (0, 12),
- 'vae_encoder':(12,17),
- 'unet':(17,26),
- 'vae_decoder': (26, 31) # Example range for unet
- }
-
- # Call the function and get the shards for all models
- shards = create_shard(shard, model_ranges)
-
- # Access individual shards
- shard_clip = shards['clip']
- shard_encoder = shards['vae_encoder']
- shard_unet = shards['unet']
- shard_decoder = shards['vae_decoder']
-
- return shard_clip, shard_encoder, shard_unet, shard_decoder
-
-
-
diff --git a/exo/inference/mlx/models/__init__.py b/exo/inference/mlx/models/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/inference/mlx/models/base.py b/exo/inference/mlx/models/base.py
deleted file mode 100644
index 5181adfd..00000000
--- a/exo/inference/mlx/models/base.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from typing import Optional
-import mlx.core as mx
-import mlx.nn as nn
-from mlx_lm.models.cache import KVCache
-
-
-class IdentityBlock(nn.Module):
- def __call__(self, x: mx.array, mask: Optional[mx.array] = None, cache: Optional[KVCache] = None) -> mx.array:
- return x
diff --git a/exo/inference/mlx/models/deepseek_v2.py b/exo/inference/mlx/models/deepseek_v2.py
deleted file mode 100644
index 8952e9e6..00000000
--- a/exo/inference/mlx/models/deepseek_v2.py
+++ /dev/null
@@ -1,127 +0,0 @@
-from dataclasses import dataclass, field
-from typing import Optional
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from mlx_lm.models.cache import KVCache
-from mlx_lm.models.deepseek_v2 import ModelArgs, DeepseekV2DecoderLayer
-from .base import IdentityBlock
-from exo.inference.shard import Shard
-
-
-@dataclass
-class ModelArgs(ModelArgs):
- shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
- def __post_init__(self):
- if isinstance(self.shard, Shard):
- return
- if not isinstance(self.shard, dict):
- raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
- self.shard = Shard(**self.shard)
-
-
-class DeepseekV2Model(nn.Module):
- def __init__(self, config: ModelArgs):
- super().__init__()
- self.args = config
- self.num_hidden_layers = config.num_hidden_layers
- self.vocab_size = config.vocab_size
- if self.args.shard.is_first_layer():
- self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
-
- self.layers = []
- for i in range(self.num_hidden_layers):
- if self.args.shard.start_layer <= i <= self.args.shard.end_layer:
- self.layers.append(DeepseekV2DecoderLayer(config, i))
- else:
- self.layers.append(IdentityBlock())
-
- if self.args.shard.is_last_layer():
- self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
-
- def __call__(
- self,
- x: mx.array,
- cache: Optional[KVCache] = None,
- ) -> mx.array:
- if self.args.shard.is_first_layer():
- h = self.embed_tokens(x)
- else:
- h = x
-
- mask = None
- T = h.shape[1]
- if T > 1:
- mask = nn.MultiHeadAttention.create_additive_causal_mask(T)
- mask = mask.astype(h.dtype)
-
- if cache is None:
- cache = [None]*len(self.layers)
-
- for layer, c in zip(self.layers, cache):
- h = layer(h, mask, c)
-
- if self.args.shard.is_last_layer():
- h = self.norm(h)
- return h
-
-
-class Model(nn.Module):
- def __init__(self, config: ModelArgs):
- super().__init__()
- self.args = config
- self.model_type = config.model_type
- self.model = DeepseekV2Model(config)
- if self.args.shard.is_last_layer():
- self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
-
- def __call__(
- self,
- inputs: mx.array,
- cache: Optional[KVCache] = None,
- ):
- out = self.model(inputs, cache)
- if self.args.shard.is_last_layer():
- return self.lm_head(out)
- return out
-
- def sanitize(self, weights):
- shard_state_dict = {}
-
- for key, value in weights.items():
- if key.startswith('model.layers.'):
- layer_num = int(key.split('.')[2])
- if self.args.shard.start_layer <= layer_num <= self.args.shard.end_layer:
- shard_state_dict[key] = value
- elif self.args.shard.is_first_layer() and key.startswith('model.embed_tokens'):
- shard_state_dict[key] = value
- elif self.args.shard.is_last_layer() and (key.startswith('model.norm') or key.startswith('lm_head')):
- shard_state_dict[key] = value
-
- for l in range(self.args.num_hidden_layers):
- prefix = f"model.layers.{l}"
- for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
- for k in ["weight", "scales", "biases"]:
- if f"{prefix}.mlp.experts.0.{m}.{k}" in shard_state_dict:
- to_join = [shard_state_dict.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}") for e in range(self.args.n_routed_experts)]
- shard_state_dict[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
-
- return shard_state_dict
-
- @property
- def layers(self):
- return self.model.layers
-
- @property
- def head_dim(self):
- return (
- self.args.qk_nope_head_dim + self.args.qk_rope_head_dim,
- self.args.v_head_dim,
- )
-
- @property
- def n_kv_heads(self):
- return self.args.num_key_value_heads
diff --git a/exo/inference/mlx/models/deepseek_v3.py b/exo/inference/mlx/models/deepseek_v3.py
deleted file mode 100644
index bf36a86b..00000000
--- a/exo/inference/mlx/models/deepseek_v3.py
+++ /dev/null
@@ -1,134 +0,0 @@
-from dataclasses import dataclass, field
-from typing import Optional
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from mlx_lm.models.cache import KVCache
-from mlx_lm.models.deepseek_v3 import (
- ModelArgs as V3ModelArgs,
- DeepseekV3DecoderLayer,
-)
-from .base import IdentityBlock
-from exo.inference.shard import Shard
-
-
-@dataclass
-class ModelArgs(V3ModelArgs):
- shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
- def __post_init__(self):
- if isinstance(self.shard, Shard):
- return
- if not isinstance(self.shard, dict):
- raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
- self.shard = Shard(**self.shard)
-
-
-class DeepseekV3Model(nn.Module):
- def __init__(self, config: ModelArgs):
- super().__init__()
- self.args = config
- self.num_hidden_layers = config.num_hidden_layers
- self.vocab_size = config.vocab_size
- if self.args.shard.is_first_layer():
- self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
-
- self.layers = []
- for i in range(self.num_hidden_layers):
- if self.args.shard.start_layer <= i <= self.args.shard.end_layer:
- self.layers.append(DeepseekV3DecoderLayer(config, i))
- else:
- self.layers.append(IdentityBlock())
-
- if self.args.shard.is_last_layer():
- self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
-
- def __call__(
- self,
- x: mx.array,
- cache: Optional[KVCache] = None,
- ) -> mx.array:
- if self.args.shard.is_first_layer():
- h = self.embed_tokens(x)
- else:
- h = x
-
- mask = None
- T = h.shape[1]
- if T > 1:
- mask = nn.MultiHeadAttention.create_additive_causal_mask(T)
- mask = mask.astype(h.dtype)
-
- if cache is None:
- cache = [None]*len(self.layers)
-
- for layer, c in zip(self.layers, cache):
- h = layer(h, mask, c)
-
- if self.args.shard.is_last_layer():
- h = self.norm(h)
- return h
-
-
-class Model(nn.Module):
- def __init__(self, config: ModelArgs):
- super().__init__()
- self.args = config
- self.model_type = config.model_type
- self.model = DeepseekV3Model(config)
- if self.args.shard.is_last_layer():
- self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
-
- def __call__(
- self,
- inputs: mx.array,
- cache: Optional[KVCache] = None,
- ):
- out = self.model(inputs, cache)
- if self.args.shard.is_last_layer():
- return self.lm_head(out)
- return out
-
- def sanitize(self, weights):
- shard_state_dict = {}
-
- for key, value in weights.items():
- if key.startswith('model.layers.'):
- layer_num = int(key.split('.')[2])
- if self.args.shard.start_layer <= layer_num <= self.args.shard.end_layer:
- shard_state_dict[key] = value
- elif self.args.shard.is_first_layer() and key.startswith('model.embed_tokens'):
- shard_state_dict[key] = value
- elif self.args.shard.is_last_layer() and (key.startswith('model.norm') or key.startswith('lm_head')):
- shard_state_dict[key] = value
-
- for l in range(self.args.num_hidden_layers):
- prefix = f"model.layers.{l}"
- for n, m in [("w1", "gate_proj"), ("w2", "down_proj"), ("w3", "up_proj")]:
- for k in ["weight", "scales", "biases"]:
- expert_key = f"{prefix}.mlp.experts.0.{m}.{k}"
- if expert_key in shard_state_dict:
- to_join = [
- shard_state_dict.pop(f"{prefix}.mlp.experts.{e}.{m}.{k}")
- for e in range(self.args.n_routed_experts)
- ]
- shard_state_dict[f"{prefix}.mlp.switch_mlp.{m}.{k}"] = mx.stack(to_join)
-
- return shard_state_dict
-
- @property
- def layers(self):
- return self.model.layers
-
- @property
- def head_dim(self):
- return (
- self.args.qk_nope_head_dim + self.args.qk_rope_head_dim,
- self.args.v_head_dim,
- )
-
- @property
- def n_kv_heads(self):
- return self.args.num_key_value_heads
diff --git a/exo/inference/mlx/models/gemma2.py b/exo/inference/mlx/models/gemma2.py
deleted file mode 100644
index 88957fd9..00000000
--- a/exo/inference/mlx/models/gemma2.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from dataclasses import dataclass, field
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from mlx_lm.models.base import create_attention_mask
-from mlx_lm.models.gemma2 import TransformerBlock, ModelArgs, RMSNorm
-
-from ...shard import Shard
-from .base import IdentityBlock
-
-
-@dataclass
-class ModelArgs(ModelArgs):
- shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
- def __post_init__(self):
- if isinstance(self.shard, Shard):
- return
- if not isinstance(self.shard, dict):
- raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
- self.shard = Shard(**self.shard)
-
-
-class GemmaModel(nn.Module):
- def __init__(self, args: ModelArgs):
- super().__init__()
- self.args = args
- self.vocab_size = args.vocab_size
- self.num_hidden_layers = args.num_hidden_layers
- assert self.vocab_size > 0
- if args.shard.is_first_layer() or args.shard.is_last_layer():
- self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
- self.layers = []
- for i in range(self.num_hidden_layers):
- if args.shard.start_layer <= i <= args.shard.end_layer:
- self.layers.append(TransformerBlock(args=args))
- else:
- self.layers.append(IdentityBlock())
- if args.shard.is_last_layer():
- self.norm = RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
-
- def __call__(
- self,
- inputs: mx.array,
- cache=None,
- ):
- if self.args.shard.is_first_layer():
- h = self.embed_tokens(inputs)
- h = h * (self.args.hidden_size**0.5)
- else:
- h = inputs
-
- mask = None
- if h.ndim > 1 and h.shape[1] > 1:
- mask = create_attention_mask(h, cache)
-
- if cache is None:
- cache = [None]*len(self.layers)
-
- for layer, c in zip(self.layers, cache):
- h = layer(h, mask, cache=c)
-
- if self.args.shard.is_last_layer():
- h = self.norm(h)
- return h
-
-
-class Model(nn.Module):
- def __init__(self, args: ModelArgs):
- super().__init__()
- self.args = args
- self.model_type = args.model_type
- self.model = GemmaModel(args)
- if args.shard.is_last_layer():
- self.final_logit_softcapping = args.final_logit_softcapping
-
- def __call__(
- self,
- inputs: mx.array,
- cache=None,
- ):
- out = self.model(inputs, cache)
- if self.args.shard.is_last_layer():
- out = self.model.embed_tokens.as_linear(out)
- out = mx.tanh(out / self.final_logit_softcapping)
- out = out * self.final_logit_softcapping
- return out
-
- def sanitize(self, weights):
- shard_state_dict = {}
-
- for key, value in weights.items():
- if "self_attn.rotary_emb.inv_freq" in key:
- continue
- if key.startswith('model.layers.'):
- layer_num = int(key.split('.')[2])
- if self.args.shard.start_layer <= layer_num <= self.args.shard.end_layer:
- shard_state_dict[key] = value
- elif (self.args.shard.is_first_layer() or self.args.shard.is_last_layer()) and key.startswith('model.embed_tokens'):
- shard_state_dict[key] = value
- elif self.args.shard.is_last_layer() and (key.startswith('model.norm')):
- shard_state_dict[key] = value
-
- return shard_state_dict
-
- @property
- def layers(self):
- return self.model.layers
-
- @property
- def head_dim(self):
- return self.args.head_dim
-
- @property
- def n_kv_heads(self):
- return self.args.num_key_value_heads
diff --git a/exo/inference/mlx/models/llama.py b/exo/inference/mlx/models/llama.py
deleted file mode 100644
index 8b069ecc..00000000
--- a/exo/inference/mlx/models/llama.py
+++ /dev/null
@@ -1,125 +0,0 @@
-from dataclasses import dataclass, field
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from mlx_lm.models.base import create_attention_mask
-from mlx_lm.models.llama import TransformerBlock, ModelArgs
-
-from ...shard import Shard
-from .base import IdentityBlock
-
-
-@dataclass
-class ModelArgs(ModelArgs):
- shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
- def __post_init__(self):
- super().__post_init__() # Ensure parent initializations are respected
-
- if isinstance(self.shard, Shard):
- return
- if not isinstance(self.shard, dict):
- raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
- self.shard = Shard(**self.shard)
-
-
-class LlamaModel(nn.Module):
- def __init__(self, args: ModelArgs):
- super().__init__()
- self.args = args
- self.vocab_size = args.vocab_size
- self.num_hidden_layers = args.num_hidden_layers
- assert self.vocab_size > 0
- if args.shard.is_first_layer() or (args.shard.is_last_layer() and args.tie_word_embeddings):
- self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
- self.layers = []
- for i in range(self.num_hidden_layers):
- if args.shard.start_layer <= i <= args.shard.end_layer:
- self.layers.append(TransformerBlock(args=args))
- else:
- self.layers.append(IdentityBlock())
- if args.shard.is_last_layer():
- self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
-
- def __call__(
- self,
- inputs: mx.array,
- cache=None,
- ):
- if self.args.shard.is_first_layer():
- h = self.embed_tokens(inputs)
- else:
- h = inputs
-
- mask = None
- if h.ndim > 1 and h.shape[1] > 1:
- mask = create_attention_mask(h, cache)
-
- if cache is None:
- cache = [None]*len(self.layers)
-
- for layer, c in zip(self.layers, cache):
- h = layer(h, mask, cache=c)
-
- if self.args.shard.is_last_layer():
- h = self.norm(h)
- return h
-
-
-class Model(nn.Module):
- def __init__(self, args: ModelArgs):
- super().__init__()
- self.args = args
- self.model_type = args.model_type
- self.model = LlamaModel(args)
- if args.shard.is_last_layer():
- if not args.tie_word_embeddings:
- self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
-
- def __call__(
- self,
- inputs: mx.array,
- cache=None,
- ):
- out = self.model(inputs, cache)
- if self.args.shard.is_last_layer():
- if self.args.tie_word_embeddings:
- out = self.model.embed_tokens.as_linear(out)
- else:
- out = self.lm_head(out)
- return out
-
- def sanitize(self, weights):
- shard_state_dict = {}
-
- for key, value in weights.items():
- if "self_attn.rotary_emb.inv_freq" in key:
- continue
- if key.startswith('model.layers.'):
- layer_num = int(key.split('.')[2])
- if self.args.shard.start_layer <= layer_num <= self.args.shard.end_layer:
- shard_state_dict[key] = value
- elif self.args.shard.is_first_layer() and key.startswith('model.embed_tokens'):
- shard_state_dict[key] = value
- elif (self.args.shard.is_last_layer() and self.args.tie_word_embeddings) and key.startswith('model.embed_tokens'):
- shard_state_dict[key] = value
- elif (self.args.shard.is_last_layer() and not self.args.tie_word_embeddings) and key.startswith('lm_head'):
- shard_state_dict[key] = value
- elif self.args.shard.is_last_layer() and (key.startswith('model.norm')):
- shard_state_dict[key] = value
-
- return shard_state_dict
-
- @property
- def layers(self):
- return self.model.layers
-
- @property
- def head_dim(self):
- return (self.args.head_dim or self.args.hidden_size // self.args.num_attention_heads)
-
- @property
- def n_kv_heads(self):
- return self.args.num_key_value_heads
diff --git a/exo/inference/mlx/models/llava.py b/exo/inference/mlx/models/llava.py
deleted file mode 100644
index b734b09b..00000000
--- a/exo/inference/mlx/models/llava.py
+++ /dev/null
@@ -1,585 +0,0 @@
-# Copyright © 2024 Apple Inc.
-
-import math
-import inspect
-from dataclasses import dataclass, field
-from typing import Optional, Dict, Union
-
-import mlx.core as mx
-import mlx.nn as nn
-from mlx_lm.models.base import BaseModelArgs, KVCache
-from exo.inference.shard import Shard
-from .base import IdentityBlock
-import numpy as np
-
-
-@dataclass
-class VisionConfig:
- model_type: str
- num_hidden_layers: int = 24
- hidden_size: int = 1024
- intermediate_size: int = 4096
- num_attention_heads: int = 16
- image_size: int = 336
- patch_size: int = 14
- projection_dim: int = 768
- vocab_size: int = 32000
- num_channels: int = 3
- layer_norm_eps: float = 1e-5
-
- @classmethod
- def from_dict(cls, params):
- return cls(**{k: v for k, v in params.items() if k in inspect.signature(cls).parameters})
-
-
-class VisionAttention(nn.Module):
- def __init__(
- self,
- dims: int,
- num_heads: int,
- query_input_dims: Optional[int] = None,
- key_input_dims: Optional[int] = None,
- value_input_dims: Optional[int] = None,
- value_dims: Optional[int] = None,
- value_output_dims: Optional[int] = None,
- bias: bool = False,
- ):
- super().__init__()
-
- if (dims % num_heads) != 0:
- raise ValueError("The input feature dimensions should be divisible by the "
- f"number of heads ({dims} % {num_heads}) != 0")
-
- query_input_dims = query_input_dims or dims
- key_input_dims = key_input_dims or dims
- value_input_dims = value_input_dims or key_input_dims
- value_dims = value_dims or dims
- value_output_dims = value_output_dims or dims
-
- self.num_heads = num_heads
- self.q_proj = nn.Linear(query_input_dims, dims, bias=bias)
- self.k_proj = nn.Linear(key_input_dims, dims, bias=bias)
- self.v_proj = nn.Linear(value_input_dims, value_dims, bias=bias)
- self.out_proj = nn.Linear(value_dims, value_output_dims, bias=bias)
-
- def __call__(self, queries, keys, values, mask=None):
- queries = self.q_proj(queries)
- keys = self.k_proj(keys)
- values = self.v_proj(values)
-
- num_heads = self.num_heads
- B, L, D = queries.shape
- _, S, _ = keys.shape
- queries = queries.reshape(B, L, num_heads, -1).transpose(0, 2, 1, 3)
- keys = keys.reshape(B, S, num_heads, -1).transpose(0, 2, 3, 1)
- values = values.reshape(B, S, num_heads, -1).transpose(0, 2, 1, 3)
-
- scale = math.sqrt(1/queries.shape[-1])
- scores = (queries*scale) @ keys
- if mask is not None:
- scores = scores + mask.astype(scores.dtype)
- scores = mx.softmax(scores, axis=-1)
- values_hat = (scores @ values).transpose(0, 2, 1, 3).reshape(B, L, -1)
-
- return self.out_proj(values_hat)
-
-
-class VisionMLP(nn.Module):
- def __init__(self, config: VisionConfig):
- super().__init__()
- self.activation_fn = nn.GELU(approx="fast")
- self.fc1 = nn.Linear(config.hidden_size, config.intermediate_size)
- self.fc2 = nn.Linear(config.intermediate_size, config.hidden_size)
-
- def __call__(self, x: mx.array) -> mx.array:
- x = self.activation_fn(self.fc1(x))
- x = self.fc2(x)
- return x
-
-
-class VisionEncoderLayer(nn.Module):
- def __init__(self, config: VisionConfig):
- super().__init__()
- self.embed_dim = config.hidden_size
- self.self_attn = VisionAttention(config.hidden_size, config.num_attention_heads, bias=True)
- self.layer_norm1 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
- self.mlp = VisionMLP(config)
- self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
-
- def __call__(self, x: mx.array, mask: Optional[mx.array] = None) -> mx.array:
- y = self.layer_norm1(x)
- y = self.self_attn(y, y, y, mask)
- x = x + y
- y = self.layer_norm2(x)
- y = self.mlp(y)
- return x + y
-
-
-class VisionEncoder(nn.Module):
- def __init__(self, config: VisionConfig):
- super().__init__()
- self.layers = [VisionEncoderLayer(config) for _ in range(config.num_hidden_layers)]
-
-
-class VisionEmbeddings(nn.Module):
- def __init__(self, config: VisionConfig):
- super().__init__()
- self.config = config
- self.embed_dim = config.hidden_size
- self.image_size = config.image_size
- self.patch_size = config.patch_size
-
- self.class_embedding = mx.zeros((config.hidden_size,))
-
- self.patch_embedding = nn.Conv2d(
- in_channels=config.num_channels,
- out_channels=self.embed_dim,
- kernel_size=self.patch_size,
- stride=self.patch_size,
- bias=False,
- )
-
- self.num_patches = (self.image_size // self.patch_size)**2
- self.num_positions = self.num_patches + 1
- self.position_embedding = nn.Embedding(self.num_positions, self.embed_dim)
-
- def __call__(self, x: mx.array) -> mx.array:
- batch_size = x.shape[0]
- patch_embeddings = self.patch_embedding(x)
- patch_embeddings = mx.flatten(patch_embeddings, start_axis=1, end_axis=2)
- embed_dim = patch_embeddings.shape[-1]
- cls_embeddings = mx.broadcast_to(self.class_embedding, (batch_size, 1, embed_dim))
- embeddings = mx.concatenate((cls_embeddings, patch_embeddings), axis=1)
- embeddings += self.position_embedding.weight
- return embeddings
-
-
-class ClipVisionModel(nn.Module):
- def __init__(self, config: VisionConfig):
- super().__init__()
- self.embeddings = VisionEmbeddings(config)
- self.pre_layrnorm = nn.LayerNorm(config.hidden_size)
- self.encoder = VisionEncoder(config)
- self.post_layernorm = nn.LayerNorm(config.hidden_size)
-
- def __call__(
- self,
- x: mx.array,
- output_hidden_states: Optional[bool] = None,
- ) -> mx.array:
- x = self.embeddings(x)
- x = self.pre_layrnorm(x)
-
- encoder_states = (x,) if output_hidden_states else None
-
- for l in self.encoder.layers:
- x = l(x, mask=None)
- if output_hidden_states:
- encoder_states = encoder_states + (x,)
-
- pooler_output = self.post_layernorm(x[:, 0, :])
- return pooler_output, x, encoder_states
-
-
-class VisionModel(nn.Module):
- def __init__(self, config: VisionConfig):
- super().__init__()
-
- self.model_type = config.model_type
- if self.model_type != "clip_vision_model":
- raise ValueError(f"Unsupported model type: {self.model_type}")
-
- self.vision_model = ClipVisionModel(config)
-
- def __call__(self, x: mx.array, output_hidden_states: Optional[bool] = None) -> mx.array:
- return self.vision_model(x, output_hidden_states)
-
- def sanitize(self, weights):
- sanitized_weights = {}
- for k, v in weights.items():
- if "position_ids" in k:
- # Remove unused position_ids
- continue
- elif "patch_embedding.weight" in k:
- # PyTorch conv2d weight tensors have shape:
- # [out_channels, in_channels, kH, KW]
- # MLX conv2d expects the weight be of shape:
- # [out_channels, kH, KW, in_channels]
- sanitized_weights[k] = v.transpose(0, 2, 3, 1)
- else:
- sanitized_weights[k] = v
-
- return sanitized_weights
-
-
-@dataclass
-class TextConfig:
- model_type: str
- hidden_size: int = 4096
- num_hidden_layers: int = 32
- intermediate_size: int = 11008
- num_attention_heads: int = 32
- head_dim: int = None
- rms_norm_eps: float = 1e-6
- vocab_size: int = 32000
- num_key_value_heads: int = None
- rope_theta: float = 10000
- rope_traditional: bool = False
- rope_scaling: Optional[Dict[str, Union[float, str]]] = None
-
- @classmethod
- def from_dict(cls, params):
- return cls(**{k: v for k, v in params.items() if k in inspect.signature(cls).parameters})
-
- def __post_init__(self):
- if self.num_key_value_heads is None:
- self.num_key_value_heads = self.num_attention_heads
-
- if self.head_dim is None:
- self.head_dim = self.hidden_size // self.num_attention_heads
-
- if self.model_type is None:
- self.model_type = "llama"
-
- if self.rope_scaling:
- required_keys = {"factor", "type"}
- if not all(key in self.rope_scaling for key in required_keys):
- raise ValueError(f"rope_scaling must contain keys {required_keys}")
-
- if self.rope_scaling["type"] != "linear":
- raise ValueError("rope_scaling 'type' currently only supports 'linear'")
-
-
-class TextAttention(nn.Module):
- def __init__(self, config: TextConfig):
- super().__init__()
-
- dim = config.hidden_size
- self.n_heads = n_heads = config.num_attention_heads
- self.n_kv_heads = n_kv_heads = config.num_key_value_heads
-
- self.repeats = n_heads // n_kv_heads
-
- head_dim = config.hidden_size // n_heads
- self.scale = head_dim**-0.5
-
- self.q_proj = nn.Linear(dim, n_heads*head_dim, bias=False)
- self.k_proj = nn.Linear(dim, n_kv_heads*head_dim, bias=False)
- self.v_proj = nn.Linear(dim, n_kv_heads*head_dim, bias=False)
- self.o_proj = nn.Linear(n_heads*head_dim, dim, bias=False)
-
- rope_scale = (1/config.rope_scaling["factor"] if config.rope_scaling is not None and config.rope_scaling["type"] == "linear" else 1)
- self.rope = nn.RoPE(
- head_dim,
- traditional=config.rope_traditional,
- base=config.rope_theta,
- scale=rope_scale,
- )
-
- def __call__(
- self,
- x: mx.array,
- mask: Optional[mx.array] = None,
- cache: Optional[KVCache] = None,
- ) -> mx.array:
- B, L, D = x.shape
-
- queries, keys, values = self.q_proj(x), self.k_proj(x), self.v_proj(x)
-
- # Prepare the queries, keys and values for the attention computation
- queries = queries.reshape(B, L, self.n_heads, -1).transpose(0, 2, 1, 3)
- keys = keys.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
- values = values.reshape(B, L, self.n_kv_heads, -1).transpose(0, 2, 1, 3)
-
- if cache is not None:
- queries = self.rope(queries, offset=cache.offset)
- keys = self.rope(keys, offset=cache.offset)
- keys, values = cache.update_and_fetch(keys, values)
- else:
- queries = self.rope(queries)
- keys = self.rope(keys)
-
- output = mx.fast.scaled_dot_product_attention(queries, keys, values, scale=self.scale, mask=mask)
- output = output.transpose(0, 2, 1, 3).reshape(B, L, -1)
- return self.o_proj(output)
-
-
-class TextMLP(nn.Module):
- def __init__(self, dim, hidden_dim):
- super().__init__()
- self.gate_proj = nn.Linear(dim, hidden_dim, bias=False)
- self.down_proj = nn.Linear(hidden_dim, dim, bias=False)
- self.up_proj = nn.Linear(dim, hidden_dim, bias=False)
-
- def __call__(self, x) -> mx.array:
- return self.down_proj(nn.silu(self.gate_proj(x))*self.up_proj(x))
-
-
-class TransformerBlock(nn.Module):
- def __init__(self, config: TextConfig):
- super().__init__()
- self.num_attention_heads = config.num_attention_heads
- self.hidden_size = config.hidden_size
- self.self_attn = TextAttention(config)
- self.mlp = TextMLP(config.hidden_size, config.intermediate_size)
- self.input_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
- self.post_attention_layernorm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
- self.config = config
-
- def __call__(
- self,
- x: mx.array,
- mask: Optional[mx.array] = None,
- cache: Optional[KVCache] = None,
- ) -> mx.array:
- r = self.self_attn(self.input_layernorm(x), mask, cache)
- h = x + r
- r = self.mlp(self.post_attention_layernorm(h))
- out = h + r
- return out
-
-
-class Llama(nn.Module):
- def __init__(self, config: TextConfig, shard: Shard):
- super().__init__()
- self.config = config
- self.shard = shard
- self.vocab_size = config.vocab_size
- self.model_type = config.model_type
- self.num_hidden_layers = config.num_hidden_layers
- self.num_key_value_heads = config.num_key_value_heads
- self.head_dim = config.head_dim
- assert self.vocab_size > 0
- if self.shard.is_first_layer():
- self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
- self.layers = []
- for i in range(self.num_hidden_layers):
- if self.shard.start_layer <= i <= self.shard.end_layer:
- self.layers.append(TransformerBlock(config=config))
- else:
- self.layers.append(IdentityBlock())
- if self.shard.is_last_layer():
- self.norm = nn.RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
-
- def __call__(
- self,
- inputs: mx.array,
- cache=None,
- inputs_embeds=None,
- ):
- # for passing merged input embeddings
- if inputs_embeds is None:
- if self.shard.is_first_layer():
- h = self.embed_tokens(inputs)
- else:
- h = inputs
- else:
- h = inputs_embeds
-
- mask = None
- if h.shape[1] > 1:
- mask = nn.MultiHeadAttention.create_additive_causal_mask(h.shape[1])
- mask = mask.astype(h.dtype)
-
- if cache is None:
- cache = [None]*len(self.layers)
-
- for layer, c in zip(self.layers, cache):
- h = layer(h, mask, c)
-
- if self.shard.is_last_layer():
- h = self.norm(h)
- return h
-
-
-class LanguageModel(nn.Module):
- def __init__(self, config: TextConfig, shard: Shard):
- super().__init__()
- self.model_type = config.model_type
- if self.model_type != "llama":
- raise ValueError(f"Model type {self.model_type} not supported. Currently only 'llama' is supported")
- self.shard = shard
- self.model = Llama(config, shard)
- if self.shard.is_last_layer():
- self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
-
- def __call__(
- self,
- inputs: mx.array,
- cache=None,
- inputs_embeds=None,
- ):
- out = self.model(inputs, cache, inputs_embeds)
- if self.shard.is_last_layer():
- out = self.lm_head(out)
- return out
-
- def sanitize(self, weights):
- shard_state_dict = {}
- for key, value in weights.items():
- if "self_attn.rotary_emb.inv_freq" in key:
- continue
-
- if key.startswith('language_model.model.layers.'):
- layer_num = int(key.split('.')[3])
- if layer_num < self.shard.start_layer or layer_num > self.shard.end_layer:
- continue
- if not self.shard.is_first_layer() and key.startswith('language_model.model.embed_tokens'):
- continue
- elif not self.shard.is_last_layer() and (key.startswith('language_model.model.norm') or key.startswith('language_model.lm_head')):
- continue
-
- shard_state_dict[key] = value
-
- return shard_state_dict
-
-
-@dataclass
-class LlaVAConfig(BaseModelArgs):
- text_config: TextConfig
- vision_config: VisionConfig = None
- model_type: str = "llava"
- ignore_index: int = -100
- image_token_index: int = 32000
- vision_feature_select_strategy: str = "default"
- vision_feature_layer: int = -2
- vocab_size: int = 32000
-
- @classmethod
- def from_dict(cls, params):
- updated_params = {}
- class_params = inspect.signature(cls).parameters
- for k, v in params.items():
- if k in class_params:
- if k in ["text_config", "vision_config"]:
- v = class_params[k].annotation.from_dict(v)
- updated_params.update({k: v})
-
- return cls(**updated_params)
-
-
-@dataclass
-class ModelArgs(LlaVAConfig):
- shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
- def __post_init__(self):
- if isinstance(self.shard, dict):
- self.shard = Shard(**self.shard)
-
- if not isinstance(self.shard, Shard):
- raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
- if not self.shard.is_first_layer():
- self.vision_config = None
-
-
-class LlavaMultiModalProjector(nn.Module):
- def __init__(self, config: LlaVAConfig):
- super().__init__()
- self.linear_1 = nn.Linear(config.vision_config.hidden_size, config.text_config.hidden_size, bias=True)
- self.gelu = nn.GELU()
- self.linear_2 = nn.Linear(config.text_config.hidden_size, config.text_config.hidden_size, bias=True)
-
- def __call__(self, x: mx.array) -> mx.array:
- x = self.linear_1(x)
- x = self.gelu(x)
- x = self.linear_2(x)
- return x
-
-
-class Model(nn.Module):
- def __init__(self, config: ModelArgs):
- super().__init__()
- self.config = config
- self.model_type = config.model_type
- if config.vision_config:
- self.vision_tower = VisionModel(config.vision_config)
- self.multi_modal_projector = LlavaMultiModalProjector(config)
- self.vision_feature_layer = config.vision_feature_layer
- self.vision_feature_select_strategy = config.vision_feature_select_strategy
- self.language_model = LanguageModel(config.text_config, config.shard)
-
- def get_input_embeddings(
- self,
- input_ids: Optional[mx.array] = None,
- pixel_values: Optional[mx.array] = None,
- ):
- if pixel_values is None:
- return self.language_model(input_ids)
-
- # Get the input embeddings from the language model
- inputs_embeds = self.language_model.model.embed_tokens(input_ids)
-
- # Get the ouptut hidden states from the vision model
- *_, hidden_states = self.vision_tower(pixel_values.transpose(0, 2, 3, 1), output_hidden_states=True)
-
- # Select the hidden states from the desired layer
- selected_image_feature = hidden_states[self.vision_feature_layer]
-
- if self.vision_feature_select_strategy == "default":
- selected_image_feature = selected_image_feature[:, 1:]
- elif self.vision_feature_select_strategy == "full":
- selected_image_feature = selected_image_feature
- else:
- raise ValueError("Unexpected feature selection strategy: "
- f"{self.vision_feature_select_strategy}")
-
- # Pass image features through the multi-modal projector
- image_features = self.multi_modal_projector(selected_image_feature)
-
- # Insert special image tokens in the input_ids
- final_inputs_embeds = self._merge_input_ids_with_image_features(image_features, inputs_embeds, input_ids)
- return final_inputs_embeds
-
- def _merge_input_ids_with_image_features(self, image_features, inputs_embeds, input_ids):
- image_token_index = self.config.image_token_index
- num_images, num_image_patches, embed_dim = image_features.shape
-
- # Positions of <image> tokens in input_ids, assuming batch size is 1
- image_positions = np.where(input_ids[0] == image_token_index)[0].tolist()
-
- if len(image_positions) != num_images:
- raise ValueError(f"The number of image tokens ({len(image_positions)}) does not "
- f" match the number of image inputs ({num_images}).")
-
- text_segments = []
- start_idx = 0
-
- for position in image_positions:
- text_segments.append(inputs_embeds[:, start_idx:position])
- start_idx = position + 1
-
- image_embeddings = mx.split(image_features, image_features.shape[0])
- final_embeddings = [v for p in zip(text_segments, image_embeddings) for v in p]
- final_embeddings += [inputs_embeds[:, start_idx:]]
-
- # Create a final embedding of shape
- # (1, num_image_patches*num_images + sequence_len, embed_dim)
- return mx.concatenate(final_embeddings, axis=1)
-
- def __call__(self, input_ids: mx.array, pixel_values: mx.array = None, cache=None):
- input_embddings = None
- if pixel_values is not None:
- input_embddings = self.get_input_embeddings(input_ids, pixel_values)
- logits = self.language_model(input_ids, cache=cache, inputs_embeds=input_embddings)
- return logits
-
- def sanitize(self, weights):
- if self.config.vision_config:
- weights = self.vision_tower.sanitize(weights)
- else:
- weights = {k: v for k, v in weights.items() if not k.startswith(('vision_tower', 'multi_modal_projector', 'vision_feature_layer', 'vision_feature_select_strategy'))}
- weights = self.language_model.sanitize(weights)
- return weights
-
- @property
- def layers(self):
- return self.language_model.model.layers
-
- @property
- def head_dim(self):
- return (self.language_model.model.head_dim or self.language_model.model.hidden_size // self.language_model.model.num_attention_heads)
-
- @property
- def n_kv_heads(self):
- return self.language_model.model.num_key_value_heads
diff --git a/exo/inference/mlx/models/phi3.py b/exo/inference/mlx/models/phi3.py
deleted file mode 100644
index acd4114a..00000000
--- a/exo/inference/mlx/models/phi3.py
+++ /dev/null
@@ -1,117 +0,0 @@
-from dataclasses import dataclass, field
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from mlx_lm.models.base import create_attention_mask
-from mlx_lm.models.phi3 import TransformerBlock, ModelArgs
-
-from ...shard import Shard
-from .base import IdentityBlock
-
-@dataclass
-class ModelArgs(ModelArgs):
- shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
- def __post_init__(self):
- super().__post_init__()
-
- if isinstance(self.shard, Shard):
- return
- if not isinstance(self.shard, dict):
- raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
- self.shard = Shard(**self.shard)
-
-class Phi3Model(nn.Module):
- def __init__(self, args: ModelArgs):
- super().__init__()
- self.args = args
- self.vocab_size = args.vocab_size
- self.num_hidden_layers = args.num_hidden_layers
- assert self.vocab_size > 0
-
- if self.args.shard.is_first_layer():
- self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
-
- self.layers = []
- for i in range(self.num_hidden_layers):
- if self.args.shard.start_layer <= i <= self.args.shard.end_layer:
- self.layers.append(TransformerBlock(args=args))
- else:
- self.layers.append(IdentityBlock())
-
- if self.args.shard.is_last_layer():
- self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
-
- def __call__(
- self,
- inputs: mx.array,
- cache=None,
- ):
- if self.args.shard.is_first_layer():
- h = self.embed_tokens(inputs)
- else:
- h = inputs
-
- mask = None
- if h.shape[1] > 1:
- mask = create_attention_mask(h, cache)
-
- if cache is None:
- cache = [None] * len(self.layers)
-
- for layer, c in zip(self.layers, cache):
- h = layer(h, mask, c)
-
- if self.args.shard.is_last_layer():
- h = self.norm(h)
- return h
-
-class Model(nn.Module):
- def __init__(self, args: ModelArgs):
- super().__init__()
- self.args = args
- self.model_type = args.model_type
- self.model = Phi3Model(args)
- if self.args.shard.is_last_layer():
- self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
-
- def __call__(
- self,
- inputs: mx.array,
- cache=None,
- ):
- out = self.model(inputs, cache)
- if self.args.shard.is_last_layer():
- out = self.lm_head(out)
- return out
-
- def sanitize(self, weights):
- shard_state_dict = {}
-
- for key, value in weights.items():
- if "self_attn.rope.inv_freq" in key:
- continue
- if key.startswith('model.layers.'):
- layer_num = int(key.split('.')[2])
- if self.args.shard.start_layer <= layer_num <= self.args.shard.end_layer:
- shard_state_dict[key] = value
- elif self.args.shard.is_first_layer() and key.startswith('model.embed_tokens'):
- shard_state_dict[key] = value
- elif self.args.shard.is_last_layer() and (key.startswith('lm_head') or key.startswith('model.norm')):
- shard_state_dict[key] = value
-
- return shard_state_dict
-
- @property
- def layers(self):
- return self.model.layers
-
- @property
- def head_dim(self):
- return self.args.hidden_size // self.args.num_attention_heads
-
- @property
- def n_kv_heads(self):
- return self.args.num_key_value_heads
diff --git a/exo/inference/mlx/models/qwen2.py b/exo/inference/mlx/models/qwen2.py
deleted file mode 100644
index 492fbb27..00000000
--- a/exo/inference/mlx/models/qwen2.py
+++ /dev/null
@@ -1,129 +0,0 @@
-from dataclasses import dataclass, field
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from mlx_lm.models.base import create_attention_mask
-from mlx_lm.models.qwen2 import TransformerBlock, ModelArgs
-
-from ...shard import Shard
-from .base import IdentityBlock
-
-@dataclass
-class ModelArgs(ModelArgs):
- shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
- def __post_init__(self):
- super().__post_init__()
-
- if isinstance(self.shard, Shard):
- return
- if not isinstance(self.shard, dict):
- raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
- self.shard = Shard(**self.shard)
-
-class Qwen2Model(nn.Module):
- def __init__(self, args: ModelArgs):
- super().__init__()
- self.args = args
- self.vocab_size = args.vocab_size
- self.num_hidden_layers = args.num_hidden_layers
- assert self.vocab_size > 0
-
- if self.args.shard.is_first_layer() or (self.args.shard.is_last_layer() and args.tie_word_embeddings):
- self.embed_tokens = nn.Embedding(args.vocab_size, args.hidden_size)
-
- self.layers = []
- for i in range(self.num_hidden_layers):
- if self.args.shard.start_layer <= i <= self.args.shard.end_layer:
- self.layers.append(TransformerBlock(args=args))
- else:
- self.layers.append(IdentityBlock())
-
- if self.args.shard.is_last_layer():
- self.norm = nn.RMSNorm(args.hidden_size, eps=args.rms_norm_eps)
-
- def __call__(
- self,
- inputs: mx.array,
- cache=None,
- ):
- if self.args.shard.is_first_layer():
- h = self.embed_tokens(inputs)
- else:
- h = inputs
-
- mask = None
- if h.shape[1] > 1:
- mask = create_attention_mask(h, cache)
-
- if cache is None:
- cache = [None]*len(self.layers)
-
- for layer, c in zip(self.layers, cache):
- h = layer(h, mask, c)
-
- if self.args.shard.is_last_layer():
- h = self.norm(h)
- return h
-
-
-class Model(nn.Module):
- def __init__(self, args: ModelArgs):
- super().__init__()
- self.args = args
- self.model_type = args.model_type
- self.model = Qwen2Model(args)
- if self.args.shard.is_last_layer():
- if not args.tie_word_embeddings:
- self.lm_head = nn.Linear(args.hidden_size, args.vocab_size, bias=False)
-
- def __call__(
- self,
- inputs: mx.array,
- cache=None,
- ):
- out = self.model(inputs, cache)
- if self.args.shard.is_last_layer():
- if self.args.tie_word_embeddings:
- out = self.model.embed_tokens.as_linear(out)
- else:
- out = self.lm_head(out)
- return out
-
- def sanitize(self, weights):
- shard_state_dict = {}
-
- for key, value in weights.items():
- if "self_attn.rotary_emb.inv_freq" in key:
- continue
- if key.startswith('model.layers.'):
- layer_num = int(key.split('.')[2])
- if self.args.shard.start_layer <= layer_num <= self.args.shard.end_layer:
- shard_state_dict[key] = value
- elif self.args.shard.is_first_layer() and key.startswith('model.embed_tokens'):
- shard_state_dict[key] = value
- elif (self.args.shard.is_last_layer() and self.args.tie_word_embeddings) and key.startswith('model.embed_tokens'):
- shard_state_dict[key] = value
- elif (self.args.shard.is_last_layer() and not self.args.tie_word_embeddings) and key.startswith('lm_head'):
- shard_state_dict[key] = value
- elif self.args.shard.is_last_layer() and (key.startswith('model.norm')):
- shard_state_dict[key] = value
-
- if self.args.tie_word_embeddings:
- shard_state_dict.pop("lm_head.weight", None)
-
- return shard_state_dict
-
- @property
- def layers(self):
- return self.model.layers
-
- @property
- def head_dim(self):
- return self.args.hidden_size // self.args.num_attention_heads
-
- @property
- def n_kv_heads(self):
- return self.args.num_key_value_heads
diff --git a/exo/inference/mlx/models/sd_models/clip.py b/exo/inference/mlx/models/sd_models/clip.py
deleted file mode 100644
index 849460f4..00000000
--- a/exo/inference/mlx/models/sd_models/clip.py
+++ /dev/null
@@ -1,191 +0,0 @@
-# Adapted from https://github.com/ml-explore/mlx-examples/blob/main/stable_diffusion/stable_diffusion/clip.py
-
-import math
-from dataclasses import dataclass
-from typing import List, Optional
-
-import mlx.core as mx
-import mlx.nn as nn
-from dataclasses import field, dataclass
-from exo.inference.shard import Shard
-from exo.inference.mlx.models.base import IdentityBlock
-
-_ACTIVATIONS = {"quick_gelu": nn.gelu_fast_approx, "gelu": nn.gelu}
-
-
-
-@dataclass
-class CLIPTextModelConfig:
- num_layers: int = 23
- model_dims: int = 1024
- num_heads: int = 16
- max_length: int = 77
- vocab_size: int = 49408
- projection_dim: Optional[int] = None
- hidden_act: str = "quick_gelu"
-
- @classmethod
- def from_dict(cls, config):
- return ModelArgs(
- num_layers=config["num_hidden_layers"],
- model_dims=config["hidden_size"],
- num_heads=config["num_attention_heads"],
- max_length=config["max_position_embeddings"],
- vocab_size=config["vocab_size"],
- projection_dim=config["projection_dim"] if "WithProjection" in config['architectures'][0] else None,
- hidden_act=config.get("hidden_act", "quick_gelu"),
- weight_files=config.get("weight_files", [])
- )
-
-@dataclass
-class ModelArgs(CLIPTextModelConfig):
- shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
- weight_files: List[str] = field(default_factory=lambda: [])
- def __post_init__(self):
- if isinstance(self.shard, dict):
- self.shard = Shard(**self.shard)
-
- if not isinstance(self.shard, Shard):
- raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
- if not self.shard.is_first_layer():
- self.vision_config = None
-
-
-@dataclass
-class CLIPOutput:
- pooled_output: Optional[mx.array] = None
- last_hidden_state: Optional[mx.array] = None
- hidden_states: Optional[List[mx.array]] = None
-
-
-class CLIPEncoderLayer(nn.Module):
- """The transformer encoder layer from CLIP."""
-
- def __init__(self, model_dims: int, num_heads: int, activation: str):
- super().__init__()
-
- self.layer_norm1 = nn.LayerNorm(model_dims)
- self.layer_norm2 = nn.LayerNorm(model_dims)
-
- self.attention = nn.MultiHeadAttention(model_dims, num_heads)
- self.attention.query_proj.bias = mx.zeros(model_dims)
- self.attention.key_proj.bias = mx.zeros(model_dims)
- self.attention.value_proj.bias = mx.zeros(model_dims)
- self.attention.out_proj.bias = mx.zeros(model_dims)
-
- self.linear1 = nn.Linear(model_dims, 4 * model_dims)
- self.linear2 = nn.Linear(4 * model_dims, model_dims)
-
- self.act = _ACTIVATIONS[activation]
-
- def __call__(self, x, attn_mask=None):
-
- y = self.layer_norm1(x)
- y = self.attention(y, y, y, attn_mask)
- x = y + x
-
- y = self.layer_norm2(x)
- y = self.linear1(y)
- y = self.act(y)
- y = self.linear2(y)
- x = y + x
- return x
-
-
-class CLIPTextModel(nn.Module):
- """Implements the text encoder transformer from CLIP."""
-
- def __init__(self, config: CLIPTextModelConfig, shard: Shard):
- super().__init__()
-
- self.shard = shard
- self.layers_range = range(self.shard.start_layer*2, self.shard.end_layer*2+2)
- if self.shard.is_first_layer():
- self.token_embedding = nn.Embedding(config.vocab_size, config.model_dims)
- self.position_embedding = nn.Embedding(config.max_length, config.model_dims)
- self.layers = []
- for i in range(math.ceil(config.num_layers/2)):
- if 2*i in self.layers_range:
- self.layers.append(CLIPEncoderLayer(config.model_dims, config.num_heads, config.hidden_act))
- if 2*i+1 in self.layers_range and 2*i+1 < config.num_layers:
- self.layers.append(CLIPEncoderLayer(config.model_dims, config.num_heads, config.hidden_act))
- else:
- self.layers.append(IdentityBlock())
- if self.shard.is_last_layer():
- self.final_layer_norm = nn.LayerNorm(config.model_dims)
-
- if config.projection_dim is not None:
- self.text_projection = nn.Linear(
- config.model_dims, config.projection_dim, bias=False
- )
-
- def _get_mask(self, N, dtype):
- indices = mx.arange(N)
- mask = indices[:, None] < indices[None]
- mask = mask.astype(dtype) * (-6e4 if dtype == mx.float16 else -1e9)
- return mask
-
- def __call__(self, x, mask=None):
- # Extract some shapes
- if self.shard.is_first_layer():
- B, N = x.shape
- eos_tokens = x.argmax(-1)
-
- # Compute the embeddings
- x = self.token_embedding(x)
-
- x = x + self.position_embedding.weight[:N]
- # Compute the features from the transformer
- mask = self._get_mask(N, x.dtype)
-
- for l in self.layers:
- x = l(x, mask)
- # Apply the final layernorm and return
-
- if self.shard.is_last_layer():
- x = self.final_layer_norm(x)
-
-
-
- return x, mask
- def sanitize(self, weights):
- sanitized_weights = {}
- for key, value in weights.items():
- if "position_ids" in key:
- continue
- if key.startswith("text_model."):
- key = key[11:]
- if key.startswith("embeddings."):
- key = key[11:]
- if key.startswith("encoder."):
- key = key[8:]
-
- # Map attention layers
- if "self_attn." in key:
- key = key.replace("self_attn.", "attention.")
- if "q_proj." in key:
- key = key.replace("q_proj.", "query_proj.")
- if "k_proj." in key:
- key = key.replace("k_proj.", "key_proj.")
- if "v_proj." in key:
- key = key.replace("v_proj.", "value_proj.")
-
- # Map ffn layers
- if "mlp.fc1" in key:
- key = key.replace("mlp.fc1", "linear1")
- if "mlp.fc2" in key:
- key = key.replace("mlp.fc2", "linear2")
-
- if key.startswith("layers."):
- layer_num = int(key.split(".")[1])
- if layer_num not in self.layers_range:
- continue
- if not self.shard.is_first_layer() and "embedding" in key:
- continue
- if not self.shard.is_last_layer() and key.startswith("final_layer_norm"):
- continue
- if not self.shard.is_last_layer() and key.startswith("text_projection"):
- continue
- sanitized_weights[key] = value
- return sanitized_weights
diff --git a/exo/inference/mlx/models/sd_models/tokenizer.py b/exo/inference/mlx/models/sd_models/tokenizer.py
deleted file mode 100644
index 4987bb90..00000000
--- a/exo/inference/mlx/models/sd_models/tokenizer.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# adapted from https://github.com/ml-explore/mlx-examples/blob/main/stable_diffusion/stable_diffusion/tokenizer.py
-
-import regex
-import json
-import glob
-
-
-class Tokenizer:
- """A simple port of CLIPTokenizer from https://github.com/huggingface/transformers/ ."""
-
- def __init__(self, bpe_ranks, vocab):
- self.bpe_ranks = bpe_ranks
- self.vocab = vocab
- self.pat = regex.compile(
- r"""<\|startoftext\|>|<\|endoftext\|>|'s|'t|'re|'ve|'m|'ll|'d|[\p{L}]+|[\p{N}]|[^\s\p{L}\p{N}]+""",
- regex.IGNORECASE,
- )
-
- self._cache = {self.bos: self.bos, self.eos: self.eos}
-
- @property
- def bos(self):
- return "<|startoftext|>"
-
- @property
- def bos_token(self):
- return self.vocab[self.bos]
-
- @property
- def eos(self):
- return "<|endoftext|>"
-
- @property
- def eos_token(self):
- return self.vocab[self.eos]
-
- def bpe(self, text):
- if text in self._cache:
- return self._cache[text]
-
- unigrams = list(text[:-1]) + [text[-1] + "</w>"]
- unique_bigrams = set(zip(unigrams, unigrams[1:]))
-
- if not unique_bigrams:
- return unigrams
-
- # In every iteration try to merge the two most likely bigrams. If none
- # was merged we are done.
- #
- # Ported from https://github.com/huggingface/transformers/blob/main/src/transformers/models/clip/tokenization_clip.py
- while unique_bigrams:
- bigram = min(
- unique_bigrams, key=lambda pair: self.bpe_ranks.get(pair, float("inf"))
- )
- if bigram not in self.bpe_ranks:
- break
-
- new_unigrams = []
- skip = False
- for a, b in zip(unigrams, unigrams[1:]):
- if skip:
- skip = False
- continue
-
- if (a, b) == bigram:
- new_unigrams.append(a + b)
- skip = True
-
- else:
- new_unigrams.append(a)
-
- if not skip:
- new_unigrams.append(b)
-
- unigrams = new_unigrams
- unique_bigrams = set(zip(unigrams, unigrams[1:]))
-
- self._cache[text] = unigrams
-
- return unigrams
-
- def tokenize(self, text, prepend_bos=True, append_eos=True):
- if isinstance(text, list):
- return [self.tokenize(t, prepend_bos, append_eos) for t in text]
-
- # Lower case cleanup and split according to self.pat. Hugging Face does
- # a much more thorough job here but this should suffice for 95% of
- # cases.
- clean_text = regex.sub(r"\s+", " ", text.lower())
- tokens = regex.findall(self.pat, clean_text)
-
- # Split the tokens according to the byte-pair merge file
- bpe_tokens = [ti for t in tokens for ti in self.bpe(t)]
-
- # Map to token ids and return
- tokens = [self.vocab[t] for t in bpe_tokens]
- if prepend_bos:
- tokens = [self.bos_token] + tokens
- if append_eos:
- tokens.append(self.eos_token)
-
- return tokens
-
- def encode(self, prompt):
- tokens = [self.tokenize(prompt)]
- negative_text = ""
- if negative_text is not None:
- tokens += [self.tokenize(negative_text)]
- lengths = [len(t) for t in tokens]
- N = max(lengths)
- tokens = [t + [0] * (N - len(t)) for t in tokens]
- return tokens
-
-def load_tokenizer(
- model_path: str,
- vocab_key: str = "tokenizer_vocab",
- merges_key: str = "tokenizer_merges",
-):
-
- vocab_file = glob.glob(str(model_path/"tokenizer"/vocab_key))[0]
- with open(vocab_file, encoding="utf-8") as f:
- vocab = json.load(f)
-
- merges_file = glob.glob(str(model_path/"tokenizer"/merges_key))[0]
- with open(merges_file, encoding="utf-8") as f:
- bpe_merges = f.read().strip().split("\n")[1 : 49152 - 256 - 2 + 1]
- bpe_merges = [tuple(m.split()) for m in bpe_merges]
- bpe_ranks = dict(map(reversed, enumerate(bpe_merges)))
-
- return Tokenizer(bpe_ranks, vocab)
-
diff --git a/exo/inference/mlx/models/sd_models/unet.py b/exo/inference/mlx/models/sd_models/unet.py
deleted file mode 100644
index 3fe44b86..00000000
--- a/exo/inference/mlx/models/sd_models/unet.py
+++ /dev/null
@@ -1,629 +0,0 @@
-# Adapted from https://github.com/ml-explore/mlx-examples/blob/main/stable_diffusion/stable_diffusion/unet.py
-
-import math
-from typing import Optional
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from dataclasses import dataclass, field
-from typing import Tuple, Optional, List
-from exo.inference.shard import Shard
-
-@dataclass
-class UNetConfig:
- in_channels: int = 4
- out_channels: int = 4
- conv_in_kernel: int = 3
- conv_out_kernel: int = 3
- block_out_channels: Tuple[int] = (320, 640, 1280, 1280)
- layers_per_block: Tuple[int] = (2, 2, 2, 2)
- mid_block_layers: int = 2
- transformer_layers_per_block: Tuple[int] = (1, 1, 1, 1)
- num_attention_heads: Tuple[int] = (5, 10, 20, 20)
- cross_attention_dim: Tuple[int] = (1024,) * 4
- norm_num_groups: int = 32
- down_block_types: Tuple[str] = (
- "CrossAttnDownBlock2D",
- "CrossAttnDownBlock2D",
- "CrossAttnDownBlock2D",
- "DownBlock2D",
- )
- up_block_types: Tuple[str] = (
- "UpBlock2D",
- "CrossAttnUpBlock2D",
- "CrossAttnUpBlock2D",
- "CrossAttnUpBlock2D",
- )
- addition_embed_type: Optional[str] = None
- addition_time_embed_dim: Optional[int] = None
- projection_class_embeddings_input_dim: Optional[int] = None
- weight_files: List[str] = field(default_factory=lambda: [])
-
-
-
- @classmethod
- def from_dict(cls,config):
- n_blocks = len(config['block_out_channels'])
- return UNetConfig(
- in_channels=config["in_channels"],
- out_channels=config["out_channels"],
- block_out_channels=config["block_out_channels"],
- layers_per_block=[config["layers_per_block"]] * n_blocks,
- transformer_layers_per_block=config.get(
- "transformer_layers_per_block", (1,) * 4
- ),
- num_attention_heads=(
- [config["attention_head_dim"]] * n_blocks
- if isinstance(config["attention_head_dim"], int)
- else config["attention_head_dim"]
- ),
- cross_attention_dim=[config["cross_attention_dim"]] * n_blocks,
- norm_num_groups=config["norm_num_groups"],
- down_block_types=config["down_block_types"],
- up_block_types=config["up_block_types"][::-1],
- addition_embed_type=config.get("addition_embed_type", None),
- addition_time_embed_dim=config.get("addition_time_embed_dim", None),
- projection_class_embeddings_input_dim=config.get(
- "projection_class_embeddings_input_dim", None
- ),
- weight_files=config.get("weight_files", [])
-
- )
-
-
-def upsample_nearest(x, scale: int = 2):
- B, H, W, C = x.shape
- x = mx.broadcast_to(x[:, :, None, :, None, :], (B, H, scale, W, scale, C))
- x = x.reshape(B, H * scale, W * scale, C)
-
- return x
-
-
-class TimestepEmbedding(nn.Module):
- def __init__(self, in_channels: int, time_embed_dim: int):
- super().__init__()
-
- self.linear_1 = nn.Linear(in_channels, time_embed_dim)
- self.linear_2 = nn.Linear(time_embed_dim, time_embed_dim)
-
- def __call__(self, x):
- x = self.linear_1(x)
- x = nn.silu(x)
- x = self.linear_2(x)
-
- return x
-
-
-class TransformerBlock(nn.Module):
- def __init__(
- self,
- model_dims: int,
- num_heads: int,
- hidden_dims: Optional[int] = None,
- memory_dims: Optional[int] = None,
- ):
- super().__init__()
-
- self.norm1 = nn.LayerNorm(model_dims)
- self.attn1 = nn.MultiHeadAttention(model_dims, num_heads)
- self.attn1.out_proj.bias = mx.zeros(model_dims)
-
- memory_dims = memory_dims or model_dims
- self.norm2 = nn.LayerNorm(model_dims)
- self.attn2 = nn.MultiHeadAttention(
- model_dims, num_heads, key_input_dims=memory_dims
- )
- self.attn2.out_proj.bias = mx.zeros(model_dims)
-
- hidden_dims = hidden_dims or 4 * model_dims
- self.norm3 = nn.LayerNorm(model_dims)
- self.linear1 = nn.Linear(model_dims, hidden_dims)
- self.linear2 = nn.Linear(model_dims, hidden_dims)
- self.linear3 = nn.Linear(hidden_dims, model_dims)
-
- def __call__(self, x, memory, attn_mask, memory_mask):
- # Self attention
- y = self.norm1(x)
- y = self.attn1(y, y, y, attn_mask)
- x = x + y
-
- # Cross attention
- y = self.norm2(x)
- y = self.attn2(y, memory, memory, memory_mask)
- x = x + y
-
- # FFN
- y = self.norm3(x)
- y_a = self.linear1(y)
- y_b = self.linear2(y)
- y = y_a * nn.gelu(y_b)
- y = self.linear3(y)
- x = x + y
-
- return x
-
-
-class Transformer2D(nn.Module):
- """A transformer model for inputs with 2 spatial dimensions."""
-
- def __init__(
- self,
- in_channels: int,
- model_dims: int,
- encoder_dims: int,
- num_heads: int,
- num_layers: int = 1,
- norm_num_groups: int = 32,
- ):
- super().__init__()
-
- self.norm = nn.GroupNorm(norm_num_groups, in_channels, pytorch_compatible=True)
- self.proj_in = nn.Linear(in_channels, model_dims)
- self.transformer_blocks = [
- TransformerBlock(model_dims, num_heads, memory_dims=encoder_dims)
- for i in range(num_layers)
- ]
- self.proj_out = nn.Linear(model_dims, in_channels)
-
- def __call__(self, x, encoder_x, attn_mask, encoder_attn_mask):
- # Save the input to add to the output
- input_x = x
- dtype = x.dtype
-
- # Perform the input norm and projection
- B, H, W, C = x.shape
- x = self.norm(x.astype(mx.float32)).astype(dtype).reshape(B, -1, C)
- x = self.proj_in(x)
-
- # Apply the transformer
- for block in self.transformer_blocks:
- x = block(x, encoder_x, attn_mask, encoder_attn_mask)
-
- # Apply the output projection and reshape
- x = self.proj_out(x)
- x = x.reshape(B, H, W, C)
-
- return x + input_x
-
-
-class ResnetBlock2D(nn.Module):
- def __init__(
- self,
- in_channels: int,
- out_channels: Optional[int] = None,
- groups: int = 32,
- temb_channels: Optional[int] = None,
- ):
- super().__init__()
-
- out_channels = out_channels or in_channels
-
- self.norm1 = nn.GroupNorm(groups, in_channels, pytorch_compatible=True)
- self.conv1 = nn.Conv2d(
- in_channels, out_channels, kernel_size=3, stride=1, padding=1
- )
- if temb_channels is not None:
- self.time_emb_proj = nn.Linear(temb_channels, out_channels)
- self.norm2 = nn.GroupNorm(groups, out_channels, pytorch_compatible=True)
- self.conv2 = nn.Conv2d(
- out_channels, out_channels, kernel_size=3, stride=1, padding=1
- )
-
- if in_channels != out_channels:
- self.conv_shortcut = nn.Linear(in_channels, out_channels)
-
- def __call__(self, x, temb=None):
- dtype = x.dtype
-
- if temb is not None:
- temb = self.time_emb_proj(nn.silu(temb))
- y = self.norm1(x.astype(mx.float32)).astype(dtype)
-
- y = nn.silu(y)
-
- y = self.conv1(y)
-
-
- if temb is not None:
- y = y + temb[:, None, None, :]
- y = self.norm2(y.astype(mx.float32)).astype(dtype)
- y = nn.silu(y)
- y = self.conv2(y)
-
- x = y + (x if "conv_shortcut" not in self else self.conv_shortcut(x))
- return x
-
-
-class UNetBlock2D(nn.Module):
- def __init__(
- self,
- in_channels: int,
- out_channels: int,
- temb_channels: int,
- prev_out_channels: Optional[int] = None,
- num_layers: int = 1,
- transformer_layers_per_block: int = 1,
- num_attention_heads: int = 8,
- cross_attention_dim=1280,
- resnet_groups: int = 32,
- add_downsample=True,
- add_upsample=True,
- add_cross_attention=True,
- ):
- super().__init__()
-
- # Prepare the in channels list for the resnets
- if prev_out_channels is None:
- in_channels_list = [in_channels] + [out_channels] * (num_layers - 1)
- else:
- in_channels_list = [prev_out_channels] + [out_channels] * (num_layers - 1)
- res_channels_list = [out_channels] * (num_layers - 1) + [in_channels]
- in_channels_list = [
- a + b for a, b in zip(in_channels_list, res_channels_list)
- ]
-
- # Add resnet blocks that also process the time embedding
- self.resnets = [
- ResnetBlock2D(
- in_channels=ic,
- out_channels=out_channels,
- temb_channels=temb_channels,
- groups=resnet_groups,
- )
- for ic in in_channels_list
- ]
-
- # Add optional cross attention layers
- if add_cross_attention:
- self.attentions = [
- Transformer2D(
- in_channels=out_channels,
- model_dims=out_channels,
- num_heads=num_attention_heads,
- num_layers=transformer_layers_per_block,
- encoder_dims=cross_attention_dim,
- )
- for i in range(num_layers)
- ]
-
- # Add an optional downsampling layer
- if add_downsample:
- self.downsample = nn.Conv2d(
- out_channels, out_channels, kernel_size=3, stride=2, padding=1
- )
-
- # or upsampling layer
- if add_upsample:
- self.upsample = nn.Conv2d(
- out_channels, out_channels, kernel_size=3, stride=1, padding=1
- )
-
- def __call__(
- self,
- x,
- encoder_x=None,
- temb=None,
- attn_mask=None,
- encoder_attn_mask=None,
- residual_hidden_states=None,
- ):
- output_states = []
-
- for i in range(len(self.resnets)):
- if residual_hidden_states is not None:
- x = mx.concatenate([x, residual_hidden_states.pop()], axis=-1)
-
- x = self.resnets[i](x, temb)
-
- if "attentions" in self:
- x = self.attentions[i](x, encoder_x, attn_mask, encoder_attn_mask)
-
- output_states.append(x)
-
- if "downsample" in self:
- x = self.downsample(x)
- output_states.append(x)
-
- if "upsample" in self:
- x = self.upsample(upsample_nearest(x))
- output_states.append(x)
-
- return x, output_states
-
-
-class UNetModel(nn.Module):
- """The conditional 2D UNet model that actually performs the denoising."""
-
- def __init__(self, config: UNetConfig, shard: Shard):
- super().__init__()
- self.shard = shard
- self.start_layer = shard.start_layer
- self.end_layer = shard.end_layer
- self.layers_range = list(range(self.start_layer, self.end_layer+1))
- if shard.is_first_layer():
- self.conv_in = nn.Conv2d(
- config.in_channels,
- config.block_out_channels[0],
- config.conv_in_kernel,
- padding=(config.conv_in_kernel - 1) // 2,
- )
-
- self.timesteps = nn.SinusoidalPositionalEncoding(
- config.block_out_channels[0],
- max_freq=1,
- min_freq=math.exp(
- -math.log(10000) + 2 * math.log(10000) / config.block_out_channels[0]
- ),
- scale=1.0,
- cos_first=True,
- full_turns=False,
- )
- self.time_embedding = TimestepEmbedding(
- config.block_out_channels[0],
- config.block_out_channels[0] * 4,
- )
-
- if config.addition_embed_type == "text_time":
- self.add_time_proj = nn.SinusoidalPositionalEncoding(
- config.addition_time_embed_dim,
- max_freq=1,
- min_freq=math.exp(
- -math.log(10000)
- + 2 * math.log(10000) / config.addition_time_embed_dim
- ),
- scale=1.0,
- cos_first=True,
- full_turns=False,
- )
- self.add_embedding = TimestepEmbedding(
- config.projection_class_embeddings_input_dim,
- config.block_out_channels[0] * 4,
- )
-
- # Make the downsampling blocks
- block_channels = [config.block_out_channels[0]] + list(
- config.block_out_channels
- )
- self.down_blocks = []
-
- for i, (in_channels, out_channels) in enumerate(zip(block_channels, block_channels[1:])):
- if i in self.layers_range:
- self.down_blocks.append(
- UNetBlock2D(
- in_channels=in_channels,
- out_channels=out_channels,
- temb_channels=config.block_out_channels[0] * 4,
- num_layers=config.layers_per_block[i],
- transformer_layers_per_block=config.transformer_layers_per_block[i],
- num_attention_heads=config.num_attention_heads[i],
- cross_attention_dim=config.cross_attention_dim[i],
- resnet_groups=config.norm_num_groups,
- add_downsample=(i < len(config.block_out_channels) - 1),
- add_upsample=False,
- add_cross_attention="CrossAttn" in config.down_block_types[i],
- )
- )
- else:
- self.down_blocks.append(nn.Identity())
-
-
- # Make the middle block
- if 4 in self.layers_range:
- self.mid_blocks = [
- ResnetBlock2D(
- in_channels=config.block_out_channels[-1],
- out_channels=config.block_out_channels[-1],
- temb_channels=config.block_out_channels[0] * 4,
- groups=config.norm_num_groups,
- ),
- Transformer2D(
- in_channels=config.block_out_channels[-1],
- model_dims=config.block_out_channels[-1],
- num_heads=config.num_attention_heads[-1],
- num_layers=config.transformer_layers_per_block[-1],
- encoder_dims=config.cross_attention_dim[-1],
- ),
- ResnetBlock2D(
- in_channels=config.block_out_channels[-1],
- out_channels=config.block_out_channels[-1],
- temb_channels=config.block_out_channels[0] * 4,
- groups=config.norm_num_groups,
- ),
- ]
-
- # Make the upsampling blocks
- block_channels = (
- [config.block_out_channels[0]]
- + list(config.block_out_channels)
- + [config.block_out_channels[-1]]
- )
-
- total_items = len(block_channels) - 3
- reversed_channels = list(reversed(list(zip(block_channels, block_channels[1:], block_channels[2:]))))
-
- self.up_blocks = []
- for rev_i, (in_channels, out_channels, prev_out_channels) in enumerate(reversed_channels):
- i = total_items - rev_i
- if rev_i+5 in self.layers_range:
- self.up_blocks.append(
- UNetBlock2D(
- in_channels=in_channels,
- out_channels=out_channels,
- temb_channels=config.block_out_channels[0] * 4,
- prev_out_channels=prev_out_channels,
- num_layers=config.layers_per_block[i] + 1,
- transformer_layers_per_block=config.transformer_layers_per_block[i],
- num_attention_heads=config.num_attention_heads[i],
- cross_attention_dim=config.cross_attention_dim[i],
- resnet_groups=config.norm_num_groups,
- add_downsample=False,
- add_upsample=(i > 0),
- add_cross_attention="CrossAttn" in config.up_block_types[i],
- )
- )
- else:
- self.up_blocks.append(nn.Identity())
-
-
- if shard.is_last_layer():
- self.conv_norm_out = nn.GroupNorm(
- config.norm_num_groups,
- config.block_out_channels[0],
- pytorch_compatible=True,
- )
- self.conv_out = nn.Conv2d(
- config.block_out_channels[0],
- config.out_channels,
- config.conv_out_kernel,
- padding=(config.conv_out_kernel - 1) // 2,
- )
-
- def __call__(
- self,
- x,
- timestep,
- encoder_x,
- attn_mask=None,
- encoder_attn_mask=None,
- text_time=None,
- residuals=None,
- ):
- # Compute the time embeddings
-
- temb = self.timesteps(timestep).astype(x.dtype)
- temb = self.time_embedding(temb)
-
- # Add the extra text_time conditioning
- if text_time is not None:
- text_emb, time_ids = text_time
- emb = self.add_time_proj(time_ids).flatten(1).astype(x.dtype)
- emb = mx.concatenate([text_emb, emb], axis=-1)
- emb = self.add_embedding(emb)
- temb = temb + emb
-
- if self.shard.is_first_layer():
- # Preprocess the input
- x = self.conv_in(x)
- residuals = [x]
- # Run the downsampling part of the unet
-
- for i in range(len(self.down_blocks)):
- if i in self.layers_range:
- x, res = self.down_blocks[i](
- x,
- encoder_x=encoder_x,
- temb=temb,
- attn_mask=attn_mask,
- encoder_attn_mask=encoder_attn_mask,
- )
- residuals.extend(res)
- else:
- x= self.down_blocks[i](x)
-
- if 4 in self.layers_range:
- # Run the middle part of the unet
- x = self.mid_blocks[0](x, temb)
- x = self.mid_blocks[1](x, encoder_x, attn_mask, encoder_attn_mask)
- x = self.mid_blocks[2](x, temb)
-
- # Run the upsampling part of the unet
- for i in range(len(self.up_blocks)):
- if i+5 in self.layers_range:
- x, _ = self.up_blocks[i](
- x,
- encoder_x=encoder_x,
- temb=temb,
- attn_mask=attn_mask,
- encoder_attn_mask=encoder_attn_mask,
- residual_hidden_states=residuals,
- )
- else:
- x= self.up_blocks[i](x)
-
- # Postprocess the output
- if self.shard.is_last_layer():
- dtype = x.dtype
- x = self.conv_norm_out(x.astype(mx.float32)).astype(dtype)
- x = nn.silu(x)
- x = self.conv_out(x)
-
- return x, residuals
- def sanitize(self, weights):
- sanitized_weights = {}
- for key, value in weights.items():
- k1=""
- k2=""
- if "downsamplers" in key:
- key = key.replace("downsamplers.0.conv", "downsample")
- if "upsamplers" in key:
- key = key.replace("upsamplers.0.conv", "upsample")
-
- # Map the mid block
- if "mid_block.resnets.0" in key:
- key = key.replace("mid_block.resnets.0", "mid_blocks.0")
- if "mid_block.attentions.0" in key:
- key = key.replace("mid_block.attentions.0", "mid_blocks.1")
- if "mid_block.resnets.1" in key:
- key = key.replace("mid_block.resnets.1", "mid_blocks.2")
-
- # Map attention layers
- if "to_k" in key:
- key = key.replace("to_k", "key_proj")
- if "to_out.0" in key:
- key = key.replace("to_out.0", "out_proj")
- if "to_q" in key:
- key = key.replace("to_q", "query_proj")
- if "to_v" in key:
- key = key.replace("to_v", "value_proj")
-
- # Map transformer ffn
- if "ff.net.2" in key:
- key = key.replace("ff.net.2", "linear3")
- if "ff.net.0" in key:
- k1 = key.replace("ff.net.0.proj", "linear1")
- k2 = key.replace("ff.net.0.proj", "linear2")
- v1, v2 = mx.split(value, 2)
-
-
- if "conv_shortcut.weight" in key:
- value = value.squeeze()
-
- # Transform the weights from 1x1 convs to linear
- if len(value.shape) == 4 and ("proj_in" in key or "proj_out" in key):
- value = value.squeeze()
-
- if len(value.shape) == 4:
- value = value.transpose(0, 2, 3, 1)
- value = value.reshape(-1).reshape(value.shape)
-
- if key.startswith("conv_in") :
- if 0 not in self.layers_range:
- continue
-
- if key.startswith("down_blocks"):
- layer_num = int(key.split(".")[1])
- if layer_num not in self.layers_range:
- continue
-
- if key.startswith("mid_block"):
- if 4 not in self.layers_range:
- continue
-
- if key.startswith("up_blocks"):
- layer_num = int(key.split(".")[1])
- if (layer_num+5) not in self.layers_range:
- continue
-
- if key.startswith("conv_out") or key.startswith("conv_norm_out"):
- if 8 not in self.layers_range:
- continue
-
- if len(k1)>0:
- sanitized_weights[k1] = v1
- sanitized_weights[k2] = v2
- else:
- sanitized_weights[key] = value
-
-
- return sanitized_weights
diff --git a/exo/inference/mlx/models/sd_models/vae.py b/exo/inference/mlx/models/sd_models/vae.py
deleted file mode 100644
index 0f148517..00000000
--- a/exo/inference/mlx/models/sd_models/vae.py
+++ /dev/null
@@ -1,429 +0,0 @@
-# Adapted from https://github.com/ml-explore/mlx-examples/blob/main/stable_diffusion/stable_diffusion/vae.py
-
-import math
-from typing import List
-
-import mlx.core as mx
-import mlx.nn as nn
-
-from .unet import ResnetBlock2D, upsample_nearest
-from dataclasses import dataclass, field
-from exo.inference.shard import Shard
-from typing import Tuple
-import inspect
-from ..base import IdentityBlock
-
-@dataclass
-class AutoencoderConfig:
- in_channels: int = 3
- out_channels: int = 3
- latent_channels_out: int = 8
- latent_channels_in: int = 4
- block_out_channels: Tuple[int] = (128, 256, 512, 512)
- layers_per_block: int = 2
- norm_num_groups: int = 32
- scaling_factor: float = 0.18215
- weight_files: List[str] = field(default_factory=lambda: [])
- @classmethod
- def from_dict(cls, params):
- return cls(**{k: v for k, v in params.items() if k in inspect.signature(cls).parameters})
-
-
-@dataclass
-class ModelArgs(AutoencoderConfig):
- shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
-
- def __post_init__(self):
- if isinstance(self.shard, dict):
- self.shard = Shard(**self.shard)
-
- if not isinstance(self.shard, Shard):
- raise TypeError(f"Expected shard to be a Shard instance or a dict, got {type(self.shard)} instead")
-
- if not self.shard.is_first_layer():
- self.vision_config = None
-
-
-class Attention(nn.Module):
- """A single head unmasked attention for use with the VAE."""
-
- def __init__(self, dims: int, norm_groups: int = 32):
- super().__init__()
-
- self.group_norm = nn.GroupNorm(norm_groups, dims, pytorch_compatible=True)
- self.query_proj = nn.Linear(dims, dims)
- self.key_proj = nn.Linear(dims, dims)
- self.value_proj = nn.Linear(dims, dims)
- self.out_proj = nn.Linear(dims, dims)
-
- def __call__(self, x):
- B, H, W, C = x.shape
-
- y = self.group_norm(x)
-
- queries = self.query_proj(y).reshape(B, H * W, C)
- keys = self.key_proj(y).reshape(B, H * W, C)
- values = self.value_proj(y).reshape(B, H * W, C)
-
- scale = 1 / math.sqrt(queries.shape[-1])
- scores = (queries * scale) @ keys.transpose(0, 2, 1)
- attn = mx.softmax(scores, axis=-1)
- y = (attn @ values).reshape(B, H, W, C)
-
- y = self.out_proj(y)
- x = x + y
-
- return x
-
-
-class EncoderDecoderBlock2D(nn.Module):
- def __init__(
- self,
- in_channels: int,
- out_channels: int,
- num_layers: int = 1,
- resnet_groups: int = 32,
- add_downsample=True,
- add_upsample=True,
- ):
- super().__init__()
-
- # Add the resnet blocks
- self.resnets = [
- ResnetBlock2D(
- in_channels=in_channels if i == 0 else out_channels,
- out_channels=out_channels,
- groups=resnet_groups,
- )
- for i in range(num_layers)
- ]
-
- # Add an optional downsampling layer
- if add_downsample:
- self.downsample = nn.Conv2d(
- out_channels, out_channels, kernel_size=3, stride=2, padding=0
- )
-
- # or upsampling layer
- if add_upsample:
- self.upsample = nn.Conv2d(
- out_channels, out_channels, kernel_size=3, stride=1, padding=1
- )
-
- def __call__(self, x):
- for resnet in self.resnets:
- x = resnet(x)
- if "downsample" in self:
- x = mx.pad(x, [(0, 0), (0, 1), (0, 1), (0, 0)])
- x = self.downsample(x)
-
- if "upsample" in self:
- x = self.upsample(upsample_nearest(x))
- return x
-
-
-class Encoder(nn.Module):
- """Implements the encoder side of the Autoencoder."""
-
- def __init__(
- self,
- in_channels: int,
- latent_channels_out: int,
- block_out_channels: List[int] = [64],
- layers_per_block: int = 2,
- resnet_groups: int = 32,
- layers_range: List[int] = [],
- shard: Shard = field(default_factory=lambda: Shard("", 0, 0, 0))
- ):
- super().__init__()
- self.layers_range = layers_range
- self.shard = shard
- if self.shard.is_first_layer():
- self.conv_in = nn.Conv2d(
- in_channels, block_out_channels[0], kernel_size=3, stride=1, padding=1
- )
-
- channels = [block_out_channels[0]] + list(block_out_channels)
- self.down_blocks = []
- current_layer = 1
- for i, (in_channels, out_channels) in enumerate(zip(channels, channels[1:])):
- if current_layer in self.layers_range:
- self.down_blocks.append(
- EncoderDecoderBlock2D(
- in_channels,
- out_channels,
- num_layers=layers_per_block,
- resnet_groups=resnet_groups,
- add_downsample=i < len(block_out_channels) - 1,
- add_upsample=False,
- )
- )
- else:
- self.down_blocks.append(IdentityBlock())
- current_layer += 1
-
- if self.shard.is_last_layer():
- self.mid_blocks = [
- ResnetBlock2D(
- in_channels=block_out_channels[-1],
- out_channels=block_out_channels[-1],
- groups=resnet_groups,
- ),
- Attention(block_out_channels[-1], resnet_groups),
- ResnetBlock2D(
- in_channels=block_out_channels[-1],
- out_channels=block_out_channels[-1],
- groups=resnet_groups,
- ),
- ]
-
- self.conv_norm_out = nn.GroupNorm(
- resnet_groups, block_out_channels[-1], pytorch_compatible=True
- )
- self.conv_out = nn.Conv2d(block_out_channels[-1], latent_channels_out, 3, padding=1)
-
- def __call__(self, x):
- if self.shard.is_first_layer():
- x = self.conv_in(x)
-
- for l in self.down_blocks:
- x = l(x)
-
- if self.shard.is_last_layer():
- x = self.mid_blocks[0](x)
- x = self.mid_blocks[1](x)
- x = self.mid_blocks[2](x)
-
- x = self.conv_norm_out(x)
- x = nn.silu(x)
- x = self.conv_out(x)
-
- return x
-
-
-class Decoder(nn.Module):
- """Implements the decoder side of the Autoencoder."""
-
- def __init__(
- self,
- in_channels: int,
- out_channels: int,
- shard: Shard,
- layer_range: List[int],
- block_out_channels: List[int] = [64],
- layers_per_block: int = 2,
- resnet_groups: int = 32,
- ):
- super().__init__()
- self.out_channels = out_channels
- self.layers_range = layer_range
- if 0 in layer_range:
- self.conv_in = nn.Conv2d(
- in_channels, block_out_channels[-1], kernel_size=3, stride=1, padding=1
- )
-
- if 0 in layer_range:
- self.mid_blocks = [
- ResnetBlock2D(
- in_channels=block_out_channels[-1],
- out_channels=block_out_channels[-1],
- groups=resnet_groups,
- ),
- Attention(block_out_channels[-1], resnet_groups),
- ResnetBlock2D(
- in_channels=block_out_channels[-1],
- out_channels=block_out_channels[-1],
- groups=resnet_groups,
- ),
- ]
-
- channels = list(reversed(block_out_channels))
- channels = [channels[0]] + channels
-
- self.up_blocks = []
- current_layer = 1
-
- for i, (in_channels, out_channels) in enumerate(zip(channels, channels[1:])):
- if current_layer in layer_range:
- self.up_blocks.append(
- EncoderDecoderBlock2D(
- in_channels,
- out_channels,
- num_layers=layers_per_block,
- resnet_groups=resnet_groups,
- add_downsample=False,
- add_upsample=i < len(block_out_channels) - 1,
- )
- )
- else:
- self.up_blocks.append(IdentityBlock())
- current_layer += 1
- if 4 in layer_range:
- self.conv_norm_out = nn.GroupNorm(
- resnet_groups, block_out_channels[0], pytorch_compatible=True
- )
- self.conv_out = nn.Conv2d(block_out_channels[0], self.out_channels, 3, padding=1)
-
-
- def __call__(self, x):
- if 0 in self.layers_range:
- x = self.conv_in(x)
- x = self.mid_blocks[0](x)
- x = self.mid_blocks[1](x)
- x = self.mid_blocks[2](x)
-
- for l in self.up_blocks:
- x = l(x)
- if 4 in self.layers_range:
- x = self.conv_norm_out(x)
- x = nn.silu(x)
- x = self.conv_out(x)
- return x
-
-
-class Autoencoder(nn.Module):
- """The autoencoder that allows us to perform diffusion in the latent space."""
-
- def __init__(self, config: AutoencoderConfig, shard: Shard, model_shard: str):
- super().__init__()
- self.shard = shard
- self.start_layer = shard.start_layer
- self.end_layer = shard.end_layer
- self.layers_range = list(range(self.start_layer, self.end_layer+1))
- self.latent_channels = config.latent_channels_in
- self.scaling_factor = config.scaling_factor
- self.model_shard = model_shard
- if self.model_shard == "vae_encoder":
- self.encoder = Encoder(
- config.in_channels,
- config.latent_channels_out,
- config.block_out_channels,
- config.layers_per_block,
- resnet_groups=config.norm_num_groups,
- layers_range=self.layers_range,
- shard=shard
- )
- if self.shard.is_last_layer():
- self.quant_proj = nn.Linear(
- config.latent_channels_out, config.latent_channels_out
- )
- if self.model_shard == "vae_decoder":
- self.decoder = Decoder(
- config.latent_channels_in,
- config.out_channels,
- shard,
- self.layers_range,
- config.block_out_channels,
- config.layers_per_block + 1,
- resnet_groups=config.norm_num_groups,
- )
- if self.shard.is_first_layer():
- self.post_quant_proj = nn.Linear(
- config.latent_channels_in, config.latent_channels_in
- )
-
- def decode(self, z):
- if self.shard.is_first_layer():
- z = z / self.scaling_factor
- z=self.post_quant_proj(z)
- return self.decoder(z)
-
- def encode(self, x):
- x = self.encoder(x)
- if self.shard.is_last_layer():
- x = self.quant_proj(x)
- mean, logvar = x.split(2, axis=-1)
- mean = mean * self.scaling_factor
- logvar = logvar + 2 * math.log(self.scaling_factor)
- x = mean
- return x
-
- def __call__(self, x, key=None):
- mean, logvar = self.encode(x)
- z = mx.random.normal(mean.shape, key=key) * mx.exp(0.5 * logvar) + mean
- x_hat = self.decode(z)
-
- return dict(x_hat=x_hat, z=z, mean=mean, logvar=logvar)
-
- def sanitize(self, weights):
- shard = self.shard
- layers = self.layers_range
- sanitized_weights = {}
- for key, value in weights.items():
-
- if "downsamplers" in key:
- key = key.replace("downsamplers.0.conv", "downsample")
- if "upsamplers" in key:
- key = key.replace("upsamplers.0.conv", "upsample")
-
- # Map attention layers
- if "key" in key:
- key = key.replace("key", "key_proj")
- if "proj_attn" in key:
- key = key.replace("proj_attn", "out_proj")
- if "query" in key:
- key = key.replace("query", "query_proj")
- if "value" in key:
- key = key.replace("value", "value_proj")
-
- # Map the mid block
- if "mid_block.resnets.0" in key:
- key = key.replace("mid_block.resnets.0", "mid_blocks.0")
- if "mid_block.attentions.0" in key:
- key = key.replace("mid_block.attentions.0", "mid_blocks.1")
- if "mid_block.resnets.1" in key:
- key = key.replace("mid_block.resnets.1", "mid_blocks.2")
-
- # Map the quant/post_quant layers
- if "quant_conv" in key:
- key = key.replace("quant_conv", "quant_proj")
- value = value.squeeze()
-
- # Map the conv_shortcut to linear
- if "conv_shortcut.weight" in key:
- value = value.squeeze()
-
- if len(value.shape) == 4:
- value = value.transpose(0, 2, 3, 1)
- value = value.reshape(-1).reshape(value.shape)
-
-
- if "post_quant_conv" in key :
- key = key.replace("quant_conv", "quant_proj")
- value = value.squeeze()
-
- if 'decoder' in key and self.model_shard == "vae_decoder":
- if key.startswith("decoder.mid_blocks."):
- if 0 in layers:
- sanitized_weights[key] = value
- if "conv_in" in key and 0 in layers:
- sanitized_weights[key] = value
- if key.startswith("decoder.up_blocks."):
- layer_num = int(key.split(".")[2])+1
- if layer_num in layers:
- sanitized_weights[key] = value
- if key.startswith("decoder.conv_norm_out") and 4 in layers:
- sanitized_weights[key] = value
- if key.startswith("decoder.conv_out") and 4 in layers:
- sanitized_weights[key] = value
- if self.model_shard == "vae_decoder":
- if key.startswith("post_quant_proj") and 0 in layers:
- sanitized_weights[key] = value
- if self.model_shard == "vae_encoder":
- if key.startswith("encoder."):
- if "conv_in" in key and shard.is_first_layer():
- sanitized_weights[key] = value
- if key.startswith("encoder.down_blocks."):
- layer_num = int(key.split(".")[2])+1
- if layer_num in layers:
- sanitized_weights[key] = value
- if key.startswith("encoder.mid_blocks.") and shard.is_last_layer():
- sanitized_weights[key] = value
- if "conv_norm_out" in key and shard.is_last_layer():
- sanitized_weights[key] = value
- if "conv_out" in key and shard.is_last_layer():
- sanitized_weights[key] = value
- if key.startswith("quant_proj") and shard.is_last_layer():
- sanitized_weights[key] = value
- return sanitized_weights
-
diff --git a/exo/inference/mlx/perf_improvements.md b/exo/inference/mlx/perf_improvements.md
deleted file mode 100644
index aa9869c8..00000000
--- a/exo/inference/mlx/perf_improvements.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Perf improvements
-
-Target: 460 tok/sec
-- removing sample goes from 369 -> 402
-- performance degrades as we generate more tokens
-- make mlx inference engien synchronous, removing thread pool executor: 402 -> 413
-- remove self.on_opaque_status.trigger_all: 413 -> 418
diff --git a/exo/inference/mlx/sharded_inference_engine.py b/exo/inference/mlx/sharded_inference_engine.py
deleted file mode 100644
index cb730321..00000000
--- a/exo/inference/mlx/sharded_inference_engine.py
+++ /dev/null
@@ -1,179 +0,0 @@
-import numpy as np
-import mlx.core as mx
-import mlx.nn as nn
-from mlx_lm.sample_utils import make_sampler
-import mlx.optimizers as optim
-from ..inference_engine import InferenceEngine
-from .sharded_utils import load_model_shard, resolve_tokenizer
-from .losses import loss_fns
-from ..shard import Shard
-from typing import Dict, Optional, Tuple
-from exo.download.shard_download import ShardDownloader
-import asyncio
-from collections import OrderedDict
-from mlx_lm.models.cache import make_prompt_cache
-from concurrent.futures import ThreadPoolExecutor
-
-class MLXDynamicShardInferenceEngine(InferenceEngine):
- def __init__(self, shard_downloader: ShardDownloader):
- self.shard = None
- self.shard_downloader = shard_downloader
- self.caches = OrderedDict()
- self.sampler_params: tuple[float, float] = (0.0, 0.0, 0.0, 1)
- self.sampler = make_sampler(*self.sampler_params)
- self._mlx_thread = ThreadPoolExecutor(max_workers=1, thread_name_prefix="mlx")
- self._tokenizer_thread = ThreadPoolExecutor(max_workers=1, thread_name_prefix="tokenizer")
- self.session = {}
- self._shard_lock = asyncio.Lock()
-
- async def _eval_mlx(self, *args):
- await asyncio.get_running_loop().run_in_executor(self._mlx_thread, mx.eval, *args)
-
- async def poll_state(self, request_id: str, max_caches=2):
- if request_id in self.caches:
- self.caches.move_to_end(request_id)
- else:
- newcache = make_prompt_cache(self.model)
- if len(self.caches) > max_caches:
- self.caches.popitem(last=False)
- self.caches[request_id] = newcache
- return {"cache": self.caches[request_id]}
-
- async def sample(self, x: np.ndarray, temp: float = 0.0, top_p: float = 1.0) -> np.ndarray:
- if (temp, top_p, 0.0, 1) != self.sampler_params:
- self.sampler_params = (temp, top_p, 0.0, 1)
- self.sampler = make_sampler(*self.sampler_params)
- logits = mx.array(x)
- logits = logits[:, -1, :]
- logprobs = logits - mx.logsumexp(logits, keepdims=True)
- result = self.sampler(logprobs)
- await self._eval_mlx(result)
- return np.asarray(result, dtype=int)
-
- async def encode(self, shard: Shard, prompt: str) -> np.ndarray:
- await self.ensure_shard(shard)
- return np.asarray(
- await asyncio.get_running_loop().run_in_executor(
- self._tokenizer_thread,
- self.tokenizer.encode,
- prompt
- )
- )
-
- async def decode(self, shard: Shard, tokens) -> str:
- await self.ensure_shard(shard)
- return await asyncio.get_running_loop().run_in_executor(
- self._tokenizer_thread,
- self.tokenizer.decode,
- tokens
- )
-
- async def save_checkpoint(self, shard: Shard, path: str):
- await self.ensure_shard(shard)
- await asyncio.get_running_loop().run_in_executor(self._mlx_thread, lambda: self.model.save_weights(path))
-
- async def load_checkpoint(self, shard: Shard, path: str):
- await self.ensure_shard(shard)
- await asyncio.get_running_loop().run_in_executor(self._mlx_thread, lambda: self.model.load_weights(path))
-
- async def infer_tensor(self, request_id: str, shard: Shard, input_data: np.ndarray, inference_state: Optional[dict] = None) -> tuple[np.ndarray, Optional[dict]]:
- await self.ensure_shard(shard)
- state = await self.poll_state(request_id) if self.model.model_type != 'StableDiffusionPipeline' else {}
- x = mx.array(input_data)
-
- if self.model.model_type != 'StableDiffusionPipeline':
- output_data = await asyncio.get_running_loop().run_in_executor(
- self._mlx_thread,
- lambda: self.model(x, **state, **(inference_state or {}))
- )
- inference_state = None
- else:
- result = await asyncio.get_running_loop().run_in_executor(
- self._mlx_thread,
- lambda: self.model(x, **state, **(inference_state or {}))
- )
- output_data, inference_state = result
-
- await self._eval_mlx(output_data)
- output_data = await asyncio.get_running_loop().run_in_executor(
- self._mlx_thread,
- lambda: np.array(output_data, copy=False)
- )
- return output_data, inference_state
-
- async def evaluate(self, request_id: str, shard: Shard, inputs, targets, lengths, loss: str = "length_masked_ce"):
- await self.ensure_shard(shard)
- await self.save_session('loss', loss_fns[loss])
- x = mx.array(inputs)
- y = mx.array(targets)
- l = mx.array(lengths)
-
- score = await asyncio.get_running_loop().run_in_executor(
- self._mlx_thread,
- lambda: self.session['loss'](self.model, x, y, l)
- )
- return score
-
- async def ensure_train(self, shard: Shard, loss: str, opt=optim.SGD, lr=1e-5, trainable_layers=['input_layernorm', 'gate_proj']):
- await self.ensure_shard(shard)
-
- if 'train_layers' not in self.session or self.session['train_layers'] != trainable_layers:
- await self.save_session('train_layers', trainable_layers)
- def freeze_unfreeze():
- self.model.freeze()
- self.model.apply_to_modules(
- lambda k, v: v.unfreeze() if any(k.endswith(layer_name) for layer_name in trainable_layers) else None
- )
- await asyncio.get_running_loop().run_in_executor(self._mlx_thread, freeze_unfreeze)
-
- if 'lossname' not in self.session or 'LVaG' not in self.session or self.session['lossname'] != loss:
- await self.save_session('lossname', loss)
- await self.save_session('LVaG', nn.value_and_grad(self.model, loss_fns[loss]))
-
- if 'opt' not in self.session:
- await self.save_session('opt', opt(lr))
- return True
-
- async def train(self, request_id: str, shard: Shard, inputs, targets, lengths, loss: str = "length_masked_ce", opt=optim.SGD, lr=1e-5):
- await self.ensure_train(shard, loss, opt, lr)
-
- def train_step(inp, tar, lng):
- lval, grad = self.session['LVaG'](self.model, inp, tar, lng)
- gradlayers = grad['model']['layers']
- self.session['opt'].update(self.model, grad)
- return lval, gradlayers, (self.model.parameters(), self.session['opt'].state, lval)
-
- x = mx.array(inputs)
- y = mx.array(targets)
- l = mx.array(lengths)
- score, gradients, eval_args = await asyncio.get_running_loop().run_in_executor(
- self._mlx_thread,
- lambda: train_step(x, y, l)
- )
- await self._eval_mlx(*eval_args)
-
- layers = [{k: v["weight"] for k, v in layer.items() if 'weight' in v} for layer in gradients if layer]
- first_layer = np.array(layers[0]['input_layernorm'], copy=False)
- await self._eval_mlx(first_layer)
- return score, first_layer
-
- async def ensure_shard(self, shard: Shard):
- async with self._shard_lock:
- if self.shard == shard: return
- model_path = await self.shard_downloader.ensure_shard(shard, self.__class__.__name__)
- if self.shard != shard:
- model_shard = await asyncio.get_running_loop().run_in_executor(
- self._mlx_thread,
- lambda: load_model_shard(model_path, shard, lazy=False)
- )
- if hasattr(model_shard, "tokenizer"):
- self.tokenizer = model_shard.tokenizer
- else:
- self.tokenizer = await resolve_tokenizer(model_path)
- self.shard = shard
- self.model = model_shard
- self.caches = OrderedDict()
- self.session = {}
-
- async def cleanup(self):
- self._mlx_thread.shutdown(wait=True)
diff --git a/exo/inference/mlx/sharded_utils.py b/exo/inference/mlx/sharded_utils.py
deleted file mode 100644
index 34f29604..00000000
--- a/exo/inference/mlx/sharded_utils.py
+++ /dev/null
@@ -1,257 +0,0 @@
-# Adapted from https://github.com/ml-explore/mlx-examples/blob/main/llms/mlx_lm/utils.py
-
-import glob
-import importlib
-import json
-import logging
-import asyncio
-import aiohttp
-from functools import partial
-from pathlib import Path
-from typing import Optional, Tuple, Union, List, Callable
-from PIL import Image
-from io import BytesIO
-import base64
-import traceback
-
-import mlx.core as mx
-import mlx.nn as nn
-from transformers import AutoProcessor
-
-from mlx_lm.tokenizer_utils import load_tokenizer, TokenizerWrapper
-
-from exo import DEBUG
-from exo.inference.tokenizers import resolve_tokenizer
-from ..shard import Shard
-
-
-class ModelNotFoundError(Exception):
- def __init__(self, message):
- self.message = message
- super().__init__(self.message)
-
-
-MODEL_REMAPPING = {
- "mistral": "llama", # mistral is compatible with llama
- "phi-msft": "phixtral",
-}
-
-
-def _get_classes(config: dict):
- """
- Retrieve the model and model args classes based on the configuration.
-
- Args:
- config (dict): The model configuration.
-
- Returns:
- A tuple containing the Model class and the ModelArgs class.
- """
- model_type = config["model_type"]
- model_type = MODEL_REMAPPING.get(model_type, model_type)
- try:
- arch = importlib.import_module(f"exo.inference.mlx.models.{model_type}")
- except ImportError:
- msg = f"Model type {model_type} not supported."
- logging.error(msg)
- traceback.print_exc()
- raise ValueError(msg)
-
- return arch.Model, arch.ModelArgs
-
-
-def load_config(model_path: Path) -> dict:
- try:
- config_path = model_path / "config.json"
- if config_path.exists():
- with open(config_path, "r") as f:
- config = json.load(f)
- return config
-
- model_index_path = model_path / "model_index.json"
- if model_index_path.exists():
- config = load_model_index(model_path, model_index_path)
- return config
- except FileNotFoundError:
- logging.error(f"Config file not found in {model_path}")
- raise
- return config
-
-def load_model_shard(
- model_path: Path,
- shard: Shard,
- lazy: bool = False,
- model_config: dict = {},
-) -> nn.Module:
- """
- Load and initialize the model from a given path.
-
- Args:
- model_path (Path): The path to load the model from.
- lazy (bool): If False eval the model parameters to make sure they are
- loaded in memory before returning, otherwise they will be loaded
- when needed. Default: ``False``
- model_config(dict, optional): Configuration parameters for the model.
- Defaults to an empty dictionary.
-
- Returns:
- nn.Module: The loaded and initialized model.
-
- Raises:
- FileNotFoundError: If the weight files (.safetensors) are not found.
- ValueError: If the model class or args class are not found or cannot be instantiated.
- """
- config = load_config(model_path)
- config.update(model_config)
-
- # TODO hack
- config["shard"] = {
- "model_id": model_path.name,
- "start_layer": shard.start_layer,
- "end_layer": shard.end_layer,
- "n_layers": shard.n_layers,
- }
-
- weight_files = glob.glob(str(model_path/"model*.safetensors"))
-
- if not weight_files:
- # Try weight for back-compat
- weight_files = glob.glob(str(model_path/"weight*.safetensors"))
-
- model_class, model_args_class = _get_classes(config=config)
-
- class ShardedModel(model_class):
- def __init__(self, args):
- super().__init__(args)
- self.shard = Shard(args.shard.model_id, args.shard.start_layer, args.shard.end_layer, args.shard.n_layers)
-
- def __call__(self, x, *args, **kwargs):
- y = super().__call__(x, *args, **kwargs)
- return y
-
- model_args = model_args_class.from_dict(config)
- model = ShardedModel(model_args)
-
- if config.get("model_index", False):
- model.load()
- return model
-
- if not weight_files:
- logging.error(f"No safetensors found in {model_path}")
- raise FileNotFoundError(f"No safetensors found in {model_path}")
-
- weights = {}
- for wf in sorted(weight_files):
- if DEBUG >= 8:
- layer_nums = set()
- for k in mx.load(wf):
- if k.startswith("model.layers."):
- layer_num = int(k.split(".")[2])
- layer_nums.add(layer_num)
- if k.startswith("language_model.model.layers."):
- layer_num = int(k.split(".")[3])
- layer_nums.add(layer_num)
- print(f"\"{wf.split('/')[-1]}\": {sorted(layer_nums)},")
-
- weights.update(mx.load(wf))
-
-
-
- if hasattr(model, "sanitize"):
- weights = model.sanitize(weights)
- if DEBUG >= 8:
- print(f"\n|| {config=} ||\n")
-
- if (quantization := config.get("quantization", None)) is not None:
- # Handle legacy models which may not have everything quantized
- def class_predicate(p, m):
- if not hasattr(m, "to_quantized"):
- return False
- return f"{p}.scales" in weights
-
-
- nn.quantize(
- model,
- **quantization,
- class_predicate=class_predicate,
- )
-
- model.load_weights(list(weights.items()), strict=True)
-
- if not lazy:
- mx.eval(model.parameters())
-
- model.eval()
- return model
-
-async def load_shard(
- model_path: str,
- shard: Shard,
- tokenizer_config={},
- model_config={},
- adapter_path: Optional[str] = None,
- lazy: bool = False,
-) -> Tuple[nn.Module, TokenizerWrapper]:
- model = load_model_shard(model_path, shard, lazy, model_config)
-
- # TODO: figure out a generic solution
- if model.model_type == "llava":
- processor = AutoProcessor.from_pretrained(model_path)
- processor.eos_token_id = processor.tokenizer.eos_token_id
- processor.encode = processor.tokenizer.encode
- return model, processor
- elif hasattr(model, "tokenizer"):
- tokenizer = model.tokenizer
- return model, tokenizer
- else:
- tokenizer = await resolve_tokenizer(model_path)
- return model, tokenizer
-
-
-async def get_image_from_str(_image_str: str):
- image_str = _image_str.strip()
-
- if image_str.startswith("http"):
- async with aiohttp.ClientSession() as session:
- async with session.get(image_str, timeout=10) as response:
- content = await response.read()
- return Image.open(BytesIO(content)).convert("RGB")
- elif image_str.startswith("data:image/"):
- # Extract the image format and base64 data
- format_prefix, base64_data = image_str.split(";base64,")
- image_format = format_prefix.split("/")[1].lower()
- if DEBUG >= 2: print(f"{image_str=} {image_format=}")
- imgdata = base64.b64decode(base64_data)
- img = Image.open(BytesIO(imgdata))
-
- # Convert to RGB if not already
- if img.mode != "RGB":
- img = img.convert("RGB")
-
- return img
- else:
- raise ValueError("Invalid image_str format. Must be a URL or a base64 encoded image.")
-
-# loading a combined config for all models in the index
-def load_model_index(model_path: Path, model_index_path: Path):
- models_config = {}
- with open(model_index_path, "r") as f:
- model_index = json.load(f)
- models_config["model_index"] = True
- models_config["model_type"] = model_index["_class_name"]
- models_config["models"] = {}
- for model in model_index.keys():
- model_config_path = glob.glob(str(model_path / model / "*config.json"))
- if len(model_config_path)>0:
- with open(model_config_path[0], "r") as f:
- model_config = { }
- model_config["model_type"] = model
- model_config["config"] = json.load(f)
- model_config["path"] = model_path / model
- if model_config["path"]/"*model.safetensors":
- model_config["config"].update({"weight_files": list(glob.glob(str(model_config["path"]/"*model.safetensors")))})
- model_config["path"] = str(model_path / model)
- m = {}
- m[model] = model_config
- models_config.update(m)
- return models_config
diff --git a/exo/inference/mlx/test_non_blocking.py b/exo/inference/mlx/test_non_blocking.py
deleted file mode 100644
index 4eb2e9fb..00000000
--- a/exo/inference/mlx/test_non_blocking.py
+++ /dev/null
@@ -1,81 +0,0 @@
-import asyncio
-import time
-import numpy as np
-from exo.inference.mlx.sharded_inference_engine import MLXDynamicShardInferenceEngine
-from exo.download.new_shard_download import NewShardDownloader
-from exo.inference.shard import Shard
-from exo.models import build_base_shard
-from collections import deque
-from statistics import mean, median
-
-async def test_non_blocking():
- # Setup
- shard_downloader = NewShardDownloader()
- engine = MLXDynamicShardInferenceEngine(shard_downloader)
- _shard = build_base_shard("llama-3.1-8b", "MLXDynamicShardInferenceEngine")
- shard = Shard(_shard.model_id, _shard.start_layer, _shard.n_layers - 1, _shard.n_layers)
- await engine.ensure_shard(shard)
-
- queue = asyncio.Queue()
- measurements = deque(maxlen=1000000)
- running = True
-
- async def mlx_worker():
- try:
- start_time = time.time()
- count = 0
- while running and (time.time() - start_time) < 5: # Hard time limit
- start = time.perf_counter_ns()
- await engine.infer_prompt("req1", shard, "test prompt")
- duration = (time.perf_counter_ns() - start) / 1_000_000 # Convert to ms
- count += 1
- print(f"MLX operation {count} took: {duration:.3f}ms")
- except asyncio.CancelledError:
- pass
- finally:
- print(f"\nTotal MLX operations completed: {count}")
- print(f"Average rate: {count/5:.1f} ops/second")
-
- async def latency_producer():
- try:
- start_time = time.perf_counter_ns()
- count = 0
- while running:
- await queue.put(time.perf_counter_ns())
- count += 1
- await asyncio.sleep(0) # Yield to event loop without delay
- duration = (time.perf_counter_ns() - start_time) / 1e9 # Convert to seconds
- print(f"\nProducer iterations: {count}")
- print(f"Producer rate: {count/duration:.1f} iterations/second")
- except asyncio.CancelledError:
- pass
-
- async def latency_consumer():
- try:
- while running:
- timestamp = await queue.get()
- latency = (time.perf_counter_ns() - timestamp) / 1_000_000 # Convert to ms
- measurements.append(latency)
- queue.task_done()
- except asyncio.CancelledError:
- pass
-
- tasks = [
- asyncio.create_task(mlx_worker()),
- asyncio.create_task(latency_producer()),
- asyncio.create_task(latency_consumer())
- ]
-
- try:
- await asyncio.wait_for(asyncio.gather(*tasks), timeout=6)
- except asyncio.TimeoutError:
- print("\nTest timed out")
- finally:
- running = False
- for task in tasks:
- task.cancel()
- await asyncio.gather(*tasks, return_exceptions=True)
- print(f"\nFinal measurement count: {len(measurements)}")
-
-if __name__ == "__main__":
- asyncio.run(test_non_blocking())
diff --git a/exo/inference/mlx/test_sharded_model.py b/exo/inference/mlx/test_sharded_model.py
deleted file mode 100644
index c9743d07..00000000
--- a/exo/inference/mlx/test_sharded_model.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from exo.inference.shard import Shard
-import mlx.core as mx
-import mlx.nn as nn
-from typing import Optional
-import numpy as np
-
-
-class DummyModel(nn.Module):
- def __init__(self, shard: Optional[Shard] = None):
- self.shard = shard
- self.layers = [
- nn.Linear(8, 128),
- nn.Linear(128, 128),
- nn.Linear(128, 128),
- nn.Linear(128, 128),
- nn.Linear(128, 8),
- ]
-
- self.n_kv_heads = 4
- self.head_dim = 4
-
- def __call__(self, x, cache=None):
- if self.shard:
- for layer in self.layers[self.shard.start_layer:self.shard.end_layer + 1]:
- x = layer(x)
- if self.shard.is_last_layer():
- x = x.reshape((1, 2, 4))
- else:
- for layer in self.layers:
- x = layer(x)
- x = x.reshape((1, 2, 4))
-
- return x
-
-
-model = DummyModel()
-model.save_weights("./test_weights.npz")
-n_layers = 5
-shard1 = Shard("test", 0, n_layers // 2, n_layers)
-sharded_model1 = DummyModel(shard1)
-shard2 = Shard("test", n_layers//2 + 1, n_layers - 1, n_layers)
-sharded_model2 = DummyModel(shard2)
-
-model.load_weights("./test_weights.npz")
-sharded_model1.load_weights("./test_weights.npz")
-sharded_model2.load_weights("./test_weights.npz")
-
-fullresp = model(mx.array([1, 2, 3, 4, 5, 6, 7, 8]))
-resp1 = sharded_model1(mx.array([1, 2, 3, 4, 5, 6, 7, 8]))
-resp2 = sharded_model2(resp1)
-
-assert np.all(np.array(fullresp) == np.array(resp2))
diff --git a/exo/inference/shard.py b/exo/inference/shard.py
deleted file mode 100644
index 21b662f6..00000000
--- a/exo/inference/shard.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from dataclasses import dataclass, field
-
-
-@dataclass(frozen=True)
-class Shard:
- model_id: str
- start_layer: int
- end_layer: int
- n_layers: int
-
- def __hash__(self):
- return hash((self.model_id, self.start_layer, self.end_layer, self.n_layers))
-
- def is_first_layer(self) -> bool:
- return self.start_layer == 0
-
- def is_last_layer(self) -> bool:
- return self.end_layer == self.n_layers - 1
-
- def get_layer_count(self) -> int:
- return self.end_layer - self.start_layer + 1
-
- def to_dict(self) -> dict:
- return {
- "model_id": self.model_id,
- "start_layer": self.start_layer,
- "end_layer": self.end_layer,
- "n_layers": self.n_layers,
- }
-
- def from_dict(data: dict) -> 'Shard':
- return Shard(**data)
-
- def overlaps(self, other: 'Shard') -> bool:
- return shards_overlap(self, other)
-
-
-def shards_overlap(shard1: Shard, shard2: Shard) -> bool:
- return (shard1.model_id == shard2.model_id and max(shard1.start_layer, shard2.start_layer) <= min(shard1.end_layer, shard2.end_layer))
diff --git a/exo/inference/test_dummy_inference_engine.py b/exo/inference/test_dummy_inference_engine.py
deleted file mode 100644
index fad5178e..00000000
--- a/exo/inference/test_dummy_inference_engine.py
+++ /dev/null
@@ -1,47 +0,0 @@
-import pytest
-import numpy as np
-from exo.inference.dummy_inference_engine import DummyInferenceEngine
-from exo.inference.shard import Shard
-
-
-@pytest.mark.asyncio
-async def test_dummy_inference_specific():
- engine = DummyInferenceEngine()
- test_shard = Shard(model_id="test_model", start_layer=0, end_layer=1, n_layers=1)
- test_prompt = "This is a test prompt"
-
- result, _ = await engine.infer_prompt("test_request", test_shard, test_prompt)
-
- print(f"Inference result shape: {result.shape}")
-
- assert result.shape[0] == 1, "Result should be a 2D array with first dimension 1"
-
-
-@pytest.mark.asyncio
-async def test_dummy_inference_engine():
- # Initialize the DummyInferenceEngine
- engine = DummyInferenceEngine()
-
- # Create a test shard
- shard = Shard(model_id="test_model", start_layer=0, end_layer=1, n_layers=1)
-
- # Test infer_prompt
- output, _ = await engine.infer_prompt("test_id", shard, "Test prompt")
-
- assert isinstance(output, np.ndarray), "Output should be a numpy array"
- assert output.ndim == 2, "Output should be 2-dimensional"
-
- # Test infer_tensor
- input_tensor = np.array([[1, 2, 3]])
- output, _ = await engine.infer_tensor("test_id", shard, input_tensor)
-
- assert isinstance(output, np.ndarray), "Output should be a numpy array"
- assert output.ndim == 2, "Output should be 2-dimensional"
-
- print("All tests passed!")
-
-
-if __name__ == "__main__":
- import asyncio
- asyncio.run(test_dummy_inference_engine())
- asyncio.run(test_dummy_inference_specific())
diff --git a/exo/inference/test_inference_engine.py b/exo/inference/test_inference_engine.py
deleted file mode 100644
index 956a1162..00000000
--- a/exo/inference/test_inference_engine.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from exo.inference.mlx.sharded_inference_engine import MLXDynamicShardInferenceEngine
-from exo.inference.inference_engine import InferenceEngine
-from exo.download.new_shard_download import NewShardDownloader
-from exo.inference.shard import Shard
-from exo.helpers import DEBUG
-import os
-import asyncio
-import numpy as np
-
-
-# An inference engine should work the same for any number of Shards, as long as the Shards are continuous.
-async def test_inference_engine(inference_engine_1: InferenceEngine, inference_engine_2: InferenceEngine, model_id: str, n_layers: int):
- prompt = "In a single word only, what is the last name of the current president of the USA?"
- resp_full, _ = await inference_engine_1.infer_prompt("A", shard=Shard(model_id=model_id, start_layer=0, end_layer=n_layers - 1, n_layers=n_layers), prompt=prompt)
- token_full = await inference_engine_1.sample(resp_full)
- token_full = token_full.reshape(1, -1)
- next_resp_full, _ = await inference_engine_1.infer_tensor(
- "A",
- shard=Shard(model_id=model_id, start_layer=0, end_layer=n_layers - 1, n_layers=n_layers),
- input_data=token_full,
- )
-
- pp = n_layers // 2
- resp1, _ = await inference_engine_1.infer_prompt("B", shard=Shard(model_id=model_id, start_layer=0, end_layer=pp, n_layers=n_layers), prompt=prompt)
- resp2, _ = await inference_engine_2.infer_tensor(
- "B",
- shard=Shard(model_id=model_id, start_layer=pp + 1, end_layer=n_layers - 1, n_layers=n_layers),
- input_data=resp1,
- )
- tokens2 = await inference_engine_1.sample(resp2)
- tokens2 = tokens2.reshape(1, -1)
- resp3, _ = await inference_engine_1.infer_tensor(
- "B",
- shard=Shard(model_id=model_id, start_layer=0, end_layer=pp, n_layers=n_layers),
- input_data=tokens2,
- )
- resp4, _ = await inference_engine_2.infer_tensor(
- "B",
- shard=Shard(model_id=model_id, start_layer=pp + 1, end_layer=n_layers - 1, n_layers=n_layers),
- input_data=resp3,
- )
-
- assert np.array_equal(resp_full, resp2)
- assert np.array_equal(next_resp_full, resp4)
-
-
-asyncio.run(test_inference_engine(MLXDynamicShardInferenceEngine(NewShardDownloader()), MLXDynamicShardInferenceEngine(NewShardDownloader()), "llama-3.2-1b", 16))
-
-if os.getenv("RUN_TINYGRAD", default="0") == "1":
- import tinygrad
- import os
- from exo.inference.tinygrad.inference import TinygradDynamicShardInferenceEngine
- tinygrad.helpers.DEBUG.value = int(os.getenv("TINYGRAD_DEBUG", default="0"))
- asyncio.run(test_inference_engine(TinygradDynamicShardInferenceEngine(NewShardDownloader()), TinygradDynamicShardInferenceEngine(NewShardDownloader()), "llama-3.2-1b", 32))
diff --git a/exo/inference/tinygrad/__init__.py b/exo/inference/tinygrad/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/inference/tinygrad/inference.py b/exo/inference/tinygrad/inference.py
deleted file mode 100644
index 8e336dce..00000000
--- a/exo/inference/tinygrad/inference.py
+++ /dev/null
@@ -1,157 +0,0 @@
-from pathlib import Path
-import json
-import os
-from exo.inference.tinygrad.models.llama import Transformer, TransformerShard, convert_from_huggingface, fix_bf16, sample_logits
-from exo.inference.shard import Shard
-from exo.inference.tokenizers import resolve_tokenizer
-from tinygrad.nn.state import safe_save, safe_load, get_state_dict, load_state_dict
-from tinygrad import Tensor, nn, Context, TinyJit
-from exo.inference.inference_engine import InferenceEngine
-import numpy as np
-from exo.inference.tinygrad.tinygrad_helpers import concat_weights, load
-from exo.download.shard_download import ShardDownloader
-from concurrent.futures import ThreadPoolExecutor
-from .stateful_model import make_prompt_state
-from .losses import length_masked_ce_loss
-from collections import OrderedDict
-import asyncio
-from typing import Optional
-Tensor.no_grad = True
-# default settings
-TEMPERATURE = int(os.getenv("TEMPERATURE", 0.85))
-TOP_K = 25
-TOP_P = 0.9
-ALPHA_F = 0.1
-ALPHA_P = 0.0
-MODEL_PARAMS = {
- "1B": {
- "args": {
- "dim": 2048, "n_heads": 32, "n_kv_heads": 8, "n_layers": 16, "norm_eps": 1e-5, "rope_theta": 500000, "vocab_size": 128256, "hidden_dim": 8192,
- "rope_scaling": {"factor": 32.0, "high_freq_factor": 4.0, "low_freq_factor": 1.0, "original_max_position_embeddings": 8192, "rope_type": "llama3"}, "tie_word_embeddings": True
- }, "files": 1
- }, "3B": {
- "args": {
- "dim": 3072, "n_heads": 24, "n_kv_heads": 8, "n_layers": 28, "norm_eps": 1e-5, "rope_theta": 500000, "vocab_size": 128256, "hidden_dim": 8192,
- "rope_scaling": {"factor": 32.0, "high_freq_factor": 4.0, "low_freq_factor": 1.0, "original_max_position_embeddings": 8192, "rope_type": "llama3"}, "tie_word_embeddings": True
- }, "files": 1
- }, "8B": {"args": {"dim": 4096, "n_heads": 32, "n_kv_heads": 8, "n_layers": 32, "norm_eps": 1e-5, "rope_theta": 500000, "vocab_size": 128256, "hidden_dim": 14336}, "files": 1},
- "70B": {"args": {"dim": 8192, "n_heads": 64, "n_kv_heads": 8, "n_layers": 80, "norm_eps": 1e-5, "rope_theta": 500000, "vocab_size": 128256, "hidden_dim": 28672}, "files": 8}
-}
-
-
-def build_transformer(model_path: Path, shard: Shard, model_size="8B", device=None):
- # build model
- linear = nn.Linear
- model = Transformer(**MODEL_PARAMS[model_size]["args"], linear=linear, max_context=8192, jit=True, shard=shard)
-
- # load weights
- if model_path.is_dir():
- if (model_path/"model.safetensors.index.json").exists(): weights = load(str(model_path/"model.safetensors.index.json"), shard)
- elif (model_path/"model.safetensors").exists(): weights = load(str(model_path/"model.safetensors"), shard)
- else: weights = concat_weights([load(str(model_path/f"consolidated.{i:02d}.pth"), shard) for i in range(MODEL_PARAMS[model_size]["files"])], device[0] if isinstance(device, tuple) else device)
- else:
- weights = load(str(model_path), shard)
- weights = convert_from_huggingface(weights, model, MODEL_PARAMS[model_size]["args"]["n_heads"], MODEL_PARAMS[model_size]["args"]["n_kv_heads"])
- weights = fix_bf16(weights)
-
- with Context(BEAM=0):
- # replace weights in model
- load_state_dict(model, weights, strict=False, consume=False) # consume=True
- model = TransformerShard(shard, model)
-
- return model
-
-_executor = ThreadPoolExecutor(max_workers=1) # singleton so tinygrad always runs on the same thread
-class TinygradDynamicShardInferenceEngine(InferenceEngine):
- def __init__(self, shard_downloader: ShardDownloader):
- self.shard = None
- self.shard_downloader = shard_downloader
- self.states = OrderedDict()
- self.executor = _executor
-
- def poll_state(self, x, request_id: str, max_states=2):
- if request_id not in self.states:
- if len(self.states) >= max_states:
- self.states.popitem(last=False)
- self.states[request_id] = make_prompt_state(x, self.model)
- else:
- self.states.move_to_end(request_id)
- state = self.states[request_id]
- return {"start_pos": state.start, "cache": state.cache}
-
- async def sample(self, x: np.ndarray, temp=TEMPERATURE, top_p: float = 0.0) -> np.ndarray:
- def sample_wrapper():
- logits = x[:, -1, :]
- return sample_logits(Tensor(logits).flatten(), temp, 0, 0.8, top_p, 0.0).realize().numpy().astype(int)
- return await asyncio.get_running_loop().run_in_executor(self.executor, sample_wrapper)
-
- async def encode(self, shard: Shard, prompt: str) -> np.ndarray:
- await self.ensure_shard(shard)
- tokens = await asyncio.get_running_loop().run_in_executor(self.executor, self.tokenizer.encode, prompt)
- return await asyncio.get_running_loop().run_in_executor(self.executor, np.array, tokens)
-
- async def decode(self, shard: Shard, tokens) -> str:
- await self.ensure_shard(shard)
- tokens = await asyncio.get_running_loop().run_in_executor(self.executor, self.tokenizer.decode, tokens)
- return tokens
-
- async def load_checkpoint(self, shard: Shard, path: str):
- await self.ensure_shard(shard)
- state_dict = safe_load(path)
- await asyncio.get_running_loop().run_in_executor(self.executor, load_state_dict, self.model, state_dict)
-
- async def save_checkpoint(self, shard: Shard, path: str):
- await self.ensure_shard(shard)
- state_dict = await asyncio.get_running_loop().run_in_executor(self.executor, get_state_dict, self.model)
- safe_save(state_dict, path)
-
- async def infer_tensor(self, request_id: str, shard: Shard, input_data: np.ndarray, inference_state: Optional[dict] = None) -> tuple[np.ndarray, Optional[dict]]:
- await self.ensure_shard(shard)
- def wrap_infer():
- x = Tensor(input_data)
- h = self.model.embed(x)
- state = self.poll_state(h, request_id)
- out = self.model.forward(h, **state)
- self.states[request_id].start += x.shape[1]
- return out.numpy()
- output_data = await asyncio.get_running_loop().run_in_executor(self.executor, wrap_infer)
- return output_data, inference_state
-
- async def evaluate(self, request_id: str, shard: Shard, inputs, targets, lengths, loss=length_masked_ce_loss):
- def step(x, y, l):
- Tensor.training = False
- return self.session['loss'](self.model, x, y, l)
- await self.ensure_shard(shard)
- score = await asyncio.get_running_loop().run_in_executor(self.executor, lambda: self.session['jit'](Tensor(inputs), targets, lengths))
- out = score.numpy()
- return out
-
- async def train(self, request_id: str, shard: Shard, inputs, targets, lengths, loss=length_masked_ce_loss, opt=nn.optim.Adam, lr=1e-5):
- def step(x, y, l):
- Tensor.training = True
- score = self.session['loss'](self.model, x, y, l)
- self.session['opt'].zero_grad()
- score.backward()
- self.session['opt'].step()
- return score
- await self.ensure_shard(shard)
-
- score = await asyncio.get_running_loop().run_in_executor(self.executor, lambda: self.session['jit'](Tensor(inputs), targets, lengths).realize())
-
- return loss.numpy(), loss.numpy()
-
- async def ensure_shard(self, shard: Shard):
- if self.shard == shard:
- return
-
- model_path = await self.shard_downloader.ensure_shard(shard, self.__class__.__name__)
-
- if self.shard != shard:
- loop = asyncio.get_running_loop()
- parameters = "1B" if "1b" in shard.model_id.lower() else "3B" if "3b" in shard.model_id.lower() else "8B" if "8b" in shard.model_id.lower() else "70B"
- model_shard = await loop.run_in_executor(self.executor, build_transformer, model_path, shard, parameters)
-
- tokenizer_path = str((model_path if model_path.is_dir() else model_path.parent))
- self.tokenizer = await resolve_tokenizer(tokenizer_path)
- self.shard = shard
- self.model = model_shard
diff --git a/exo/inference/tinygrad/losses.py b/exo/inference/tinygrad/losses.py
deleted file mode 100644
index 02f54231..00000000
--- a/exo/inference/tinygrad/losses.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from tinygrad import Tensor, dtypes
-import numpy as np
-def length_masked_ce_loss(model, inputs, targets, lengths):
- # Run model on inputs
- logits = model(inputs).cast(dtypes.float32).contiguous()
-
- # Mask padding tokens
- length_mask = Tensor(np.arange(inputs.shape[1])[None, :] < lengths[:, None], requires_grad=False)
-
- # Calculate the loss
- ce = logits.sparse_categorical_crossentropy(Tensor(targets, requires_grad=False)).mul(length_mask)
- loss = ce.sum() / length_mask.sum()
- return loss
-
diff --git a/exo/inference/tinygrad/models/__init__.py b/exo/inference/tinygrad/models/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/inference/tinygrad/models/llama.py b/exo/inference/tinygrad/models/llama.py
deleted file mode 100644
index fbe0d69f..00000000
--- a/exo/inference/tinygrad/models/llama.py
+++ /dev/null
@@ -1,327 +0,0 @@
-from typing import Tuple, Union, Optional, Dict, Any, List
-from tinygrad import Tensor, Variable, TinyJit, dtypes, nn, Device
-from tinygrad.helpers import getenv
-from collections import OrderedDict
-
-
-# https://github.com/facebookresearch/llama/blob/1076b9c51c77ad06e9d7ba8a4c6df775741732bd/llama/model.py#L47
-def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0, dtype=dtypes.half, rope_scaling: Optional[Dict[str, float]] = None) -> Tensor:
- freqs = 1.0/(theta**(Tensor.arange(0, dim, 2)[:(dim // 2)]/dim))
-
- if rope_scaling:
- factor = rope_scaling.get('factor', 1.0)
- low_freq_factor = rope_scaling.get('low_freq_factor', 1.0)
- high_freq_factor = rope_scaling.get('high_freq_factor', 1.0)
- original_max_pos_emb = rope_scaling.get('original_max_position_embeddings', end)
-
- freqs[:dim // 4] *= low_freq_factor
- freqs[dim // 4:] = freqs[dim // 4:].contiguous()*high_freq_factor
- freqs *= (original_max_pos_emb/end)**(1.0/factor)
-
- freqs = Tensor.arange(end).unsqueeze(dim=1)*freqs.unsqueeze(dim=0)
- # TODO: move dtype outside this
- return Tensor.stack(freqs.cos().cast(dtype), freqs.sin().cast(dtype), dim=-1).reshape(1, end, 1, dim // 2, 2)
-
-
-# (a+i*b) * (c+i*d) = (ac-bd) + i*(ad+bc)
-def complex_mult(A, c, d):
- a, b = A[..., 0:1], A[..., 1:2]
- ro = a*c - b*d
- co = a*d + b*c
- return ro.cat(co, dim=-1)
-
-
-def apply_rotary_emb(xq: Tensor, xk: Tensor, freqs_cis: Tensor) -> Tuple[Tensor, Tensor]:
- assert freqs_cis.shape[1] == xq.shape[1] == xk.shape[1], f"freqs_cis shape mismatch {freqs_cis.shape} xq:{xq.shape} xk:{xk.shape}"
- xq = xq.reshape(*xq.shape[0:-1], -1, 2)
- xk = xk.reshape(*xk.shape[0:-1], -1, 2)
- assert len(xq.shape) == len(xk.shape) == len(freqs_cis.shape) == 5
- c, d = freqs_cis[..., 0:1], freqs_cis[..., 1:2]
- xq_out = complex_mult(xq, c, d)
- xk_out = complex_mult(xk, c, d)
- return xq_out.flatten(3), xk_out.flatten(3)
-
-
-def repeat_kv(x: Tensor, n_rep: int) -> Tensor:
- bs, seqlen, n_kv_heads, head_dim = x.shape
- if n_rep == 1: return x
- # NOTE: this is different from x.repeat((1, 1, n_rep, 1))
- return x.repeat((1, 1, 1, n_rep)).reshape(bs, seqlen, n_kv_heads*n_rep, head_dim)
-
-class Attention:
- def __init__(self, dim, n_heads, n_kv_heads, max_context, linear=nn.Linear):
- self.n_heads = n_heads
- self.n_kv_heads = n_kv_heads if n_kv_heads is not None else n_heads # n_kv_heads != n_heads implies MQA [arxiv/2307.09288, A.2.1]
- self.head_dim = dim // n_heads
- self.n_rep = self.n_heads // self.n_kv_heads
- self.max_context = max_context
-
- self.wq = linear(dim, self.n_heads*self.head_dim, bias=False)
- self.wk = linear(dim, self.n_kv_heads*self.head_dim, bias=False)
- self.wv = linear(dim, self.n_kv_heads*self.head_dim, bias=False)
- self.wo = linear(self.n_heads*self.head_dim, dim, bias=False)
-
- def __call__(self, x: Tensor, start_pos: Union[Variable, int], freqs_cis: Tensor, mask: Optional[Tensor], cache: Optional[Tensor]=None) -> Tensor:
- if getenv("WQKV"):
- if not hasattr(self, 'wqkv'): self.wqkv = Tensor.cat(self.wq.weight, self.wk.weight, self.wv.weight)
- xqkv = x @ self.wqkv.T
- xq, xk, xv = xqkv.split([self.wq.weight.shape[0], self.wk.weight.shape[0], self.wv.weight.shape[0]], dim=2)
- else:
- xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)
-
- xq = xq.reshape(xq.shape[0], xq.shape[1], self.n_heads, self.head_dim)
- xk = xk.reshape(xk.shape[0], xk.shape[1], self.n_kv_heads, self.head_dim)
- xv = xv.reshape(xv.shape[0], xv.shape[1], self.n_kv_heads, self.head_dim)
-
- xq, xk = apply_rotary_emb(xq, xk, freqs_cis)
- bsz, seqlen, _, _ = xq.shape
-
- if cache is not None:
- # update the cache
- assert xk.dtype == xv.dtype == cache.dtype, f"{xk.dtype=}, {xv.dtype=}, {cache.dtype=}"
- cache.shrink((None, None, (start_pos, start_pos + seqlen), None, None)).assign(Tensor.stack(xk, xv)).realize()
-
- keys = cache[0].shrink((None, (0, start_pos + seqlen), None, None)) if start_pos > 0 else xk
- values = cache[1].shrink((None, (0, start_pos + seqlen), None, None)) if start_pos > 0 else xv
- else:
- keys = xk
- values = xv
-
- keys, values = repeat_kv(keys, self.n_rep), repeat_kv(values, self.n_rep)
- xq, keys, values = xq.transpose(1, 2), keys.transpose(1, 2), values.transpose(1, 2)
- attn = xq.scaled_dot_product_attention(keys, values, mask).transpose(1, 2)
- attn = attn.reshape(bsz, seqlen, -1)
- return self.wo(attn)
-
-
-class FeedForward:
- def __init__(self, dim: int, hidden_dim: int, linear=nn.Linear):
- self.w1 = linear(dim, hidden_dim, bias=False)
- self.w2 = linear(hidden_dim, dim, bias=False)
- self.w3 = linear(dim, hidden_dim, bias=False) # the gate in Gated Linear Unit
-
- def __call__(self, x: Tensor) -> Tensor:
- return self.w2(self.w1(x).silu()*self.w3(x)) # SwiGLU [arxiv/2002.05202, eq (5)]
-
-
-class TransformerBlock:
- def __init__(self, dim: int, hidden_dim: int, n_heads: int, n_kv_heads: int, norm_eps: float, max_context: int, linear=nn.Linear, feed_forward=FeedForward):
- self.attention = Attention(dim, n_heads, n_kv_heads, max_context, linear)
- self.feed_forward = feed_forward(dim, hidden_dim, linear)
- self.attention_norm = nn.RMSNorm(dim, norm_eps)
- self.ffn_norm = nn.RMSNorm(dim, norm_eps)
-
- def __call__(self, x: Tensor, start_pos: Union[Variable, int], freqs_cis: Tensor, mask: Optional[Tensor], cache: Optional[Tensor]=None):
- h = x + self.attention(self.attention_norm(x), start_pos, freqs_cis, mask, cache=cache)
- return (h + self.feed_forward(self.ffn_norm(h))).contiguous()
-
-
-# standard openai sampling
-def sample_logits(logits: Tensor, temp: float, k: int, p: float, af: float, ap: float):
- assert logits.ndim == 1, "only works on 1d tensors"
- assert 0 <= p <= 1, "p must be between 0 and 1"
- assert 0 <= k <= logits.numel(), "k must be between 0 and numel"
-
- # if temperature is very low just use argmax
- if temp < 1e-6: return logits.argmax().reshape(1)
-
- # alpha sampling
- if af or ap:
- if not hasattr(sample, "alpha_counter"):
- setattr(sample, "alpha_counter", Tensor.zeros_like(logits, dtype=dtypes.int32).contiguous())
- logits = logits - (sample.alpha_counter*af + (sample.alpha_counter > 0)*ap)
-
- # replace NaNs with -inf
- logits = (logits != logits).where(-float("inf"), logits)
-
- # softmax
- t = (logits/temp).softmax()
-
- counter, counter2 = Tensor.arange(t.numel(), device=logits.device).contiguous(), Tensor.arange(t.numel() - 1, -1, -1, device=logits.device).contiguous()
- # top k
- if k:
- output, output_indices = Tensor.zeros(k, device=logits.device).contiguous(), Tensor.zeros(k, device=logits.device, dtype=dtypes.int32).contiguous()
- for i in range(k):
- t_argmax = (t.numel() - ((t == (t_max := t.max()))*counter2).max() - 1).cast(dtypes.default_int)
- output = output + t_max.unsqueeze(0).pad(((i, k - i - 1),))
- output_indices = output_indices + t_argmax.unsqueeze(0).pad(((i, k - i - 1),))
- t = (counter == t_argmax).where(0, t)
-
- # approximate top p
- # because we are already limited to top k elements we can do top p "without sorting"
- output_cumsum = output[::-1]._cumsum()[::-1] + t.sum()
- output = (output_cumsum >= (1 - p))*output
- output_indices = (output_cumsum >= (1 - p))*output_indices
-
- # sample
- output_idx = output.multinomial()
- output_token = output_indices[output_idx]
- else:
- output_token = t.multinomial()
-
- # increase alpha counter
- if af or ap:
- sample.alpha_counter = (counter == output_token).where(sample.alpha_counter + 1, sample.alpha_counter)
-
- return output_token
-
-
-from exo.inference.shard import Shard
-
-
-class Transformer:
- def __init__(
- self,
- dim: int,
- hidden_dim: int,
- n_heads: int,
- n_layers: int,
- norm_eps: float,
- vocab_size,
- shard: Shard = None,
- linear=nn.Linear,
- n_kv_heads=None,
- rope_theta=10000,
- max_context=1024,
- jit=True,
- feed_forward=FeedForward,
- rope_scaling: Optional[Dict[str, float]] = None,
- tie_word_embeddings=False,
- ):
- self.layers = [TransformerBlock(dim, hidden_dim, n_heads, n_kv_heads, norm_eps, max_context, linear, feed_forward=feed_forward) for _ in range(n_layers)]
- self.norm = nn.RMSNorm(dim, norm_eps)
- self.tok_embeddings = nn.Embedding(vocab_size, dim)
- self.output = nn.Linear(dim, vocab_size, bias=False)
- if tie_word_embeddings:
- self.output.weight = self.tok_embeddings.weight
- self.max_context = max_context
- self.freqs_cis = precompute_freqs_cis(dim // n_heads, self.max_context*2, rope_theta, rope_scaling=rope_scaling).contiguous()
- self.forward_jit = TinyJit(self.forward_base) if jit else None
- self.shard = shard
-
- def forward_base(self, x: Tensor, start_pos: Union[Variable, int], cache: Optional[List[Tensor]] = None):
- seqlen = x.shape[1]
- freqs_cis = self.freqs_cis.shrink((None, (start_pos, start_pos + seqlen), None, None, None))
- mask = Tensor.full((1, 1, seqlen, start_pos + seqlen), float("-100000000"), dtype=x.dtype, device=x.device).triu(start_pos + 1).realize() if seqlen > 1 else None
-
- h = x
-
- if cache is None:
- cache = [None for _ in range(self.shard.start_layer, self.shard.end_layer + 1)]
- for i, c in zip(range(self.shard.start_layer, self.shard.end_layer + 1), cache):
- layer = self.layers[i]
- h = layer(h, start_pos, freqs_cis, mask, cache=c)
-
- if self.shard.is_last_layer():
- logits = self.output(self.norm(h)).float().realize()
- return logits
- else:
- return h
-
- def embed(self, inputs: Tensor):
- if self.shard.is_first_layer():
- h = self.tok_embeddings(inputs)
- else:
- h = inputs
- return h
-
- def forward(self, x: Tensor, start_pos: int, cache: Optional[List[Tensor]] = None):
- if x.shape[0:2] == (1, 1) and self.forward_jit is not None and start_pos != 0:
- return self.forward_jit(x, Variable("start_pos", 1, self.max_context).bind(start_pos), cache=cache)
- return self.forward_base(x, start_pos, cache=cache)
-
- def __call__(self, x: Tensor, start_pos: Variable, cache: Optional[List[Tensor]] = None):
- # TODO: better way to handle the first call v.s. the rest?
- h = self.embed(x)
- return self.forward(h, start_pos, cache=cache)
-
-class TransformerShard:
- def __init__(
- self,
- shard: Shard,
- base,
- jit: bool = True,
- ):
- shardrange = range(shard.start_layer, shard.end_layer + 1)
- self.layers = [layer for layer, n in zip(base.layers, range(shard.n_layers)) if n in shardrange]
- self.norm = base.norm
- self.tok_embeddings = base.tok_embeddings
- self.embed = (lambda x: self.tok_embeddings(x)) if shard.is_first_layer() else (lambda x: x)
- self.output = base.output
- self.post = (lambda x: self.output(x)) if shard.is_last_layer() else (lambda x: x)
- self.max_context = base.max_context
- self.null_cache = [None for _ in shardrange]
- self.freqs_cis = base.freqs_cis
- self.forward_jit = TinyJit(self.forward_base) if jit else None
-
- def forward_base(self, x: Tensor, start_pos: Union[Variable, int], cache):
- seqlen = x.shape[1]
- freqs_cis = self.freqs_cis.shrink((None, (start_pos, start_pos + seqlen), None, None, None))
- mask = Tensor.full((1, 1, seqlen, start_pos + seqlen), float("-100000000"), dtype=x.dtype, device=x.device).triu(start_pos + 1).realize() if seqlen > 1 else None
-
- for layer, c in zip(self.layers, cache):
- x = layer(x, start_pos, freqs_cis, mask, cache=c)
-
- out = self.post(x)
- return out
-
- def forward(self, x: Tensor, start_pos: int, cache: Optional[List[Tensor]] = None):
- if x.shape[0:2] == (1, 1) and self.forward_jit is not None and start_pos != 0:
- return self.forward_jit(x, Variable("start_pos", 1, self.max_context).bind(start_pos), cache=cache)
- return self.forward_base(x, start_pos, cache=cache)
-
- def __call__(self, x: Tensor, start_pos: Variable, cache: Optional[List[Tensor]] = None):
- # TODO: better way to handle the first call v.s. the rest?
- h = self.embed(x)
- return self.forward(h, start_pos, cache=self.null_cache if cache is None else cache)
-
-# *** helpers ***
-
-
-def convert_from_huggingface(weights: Dict[str, Tensor], model: Transformer, n_heads: int, n_kv_heads: int):
- def permute(v: Tensor, n_heads: int):
- return v.reshape(n_heads, 2, v.shape[0] // n_heads // 2, v.shape[1]).transpose(1, 2).reshape(*v.shape[:2])
-
- keymap = {
- "model.embed_tokens.weight": "tok_embeddings.weight",
- **{f"model.layers.{l}.input_layernorm.weight": f"layers.{l}.attention_norm.weight"
- for l in range(len(model.layers))},
- **{f"model.layers.{l}.self_attn.{x}_proj.weight": f"layers.{l}.attention.w{x}.weight"
- for x in ["q", "k", "v", "o"]
- for l in range(len(model.layers))},
- **{f"model.layers.{l}.post_attention_layernorm.weight": f"layers.{l}.ffn_norm.weight"
- for l in range(len(model.layers))},
- **{f"model.layers.{l}.mlp.{x}_proj.weight": f"layers.{l}.feed_forward.w{y}.weight"
- for x, y in {"gate": "1", "down": "2", "up": "3"}.items()
- for l in range(len(model.layers))},
- "model.norm.weight": "norm.weight",
- "lm_head.weight": "output.weight",
- }
- sd = {}
- for k, v in weights.items():
- if ".rotary_emb." in k: continue
- v = v.to(Device.DEFAULT)
- if "model.layers" in k:
- if "q_proj" in k:
- v = permute(v, n_heads)
- elif "k_proj" in k:
- v = permute(v, n_kv_heads)
- if k in keymap:
- sd[keymap[k]] = v
- else:
- sd[k] = v
- return sd
-
-
-def fix_bf16(weights: Dict[Any, Tensor]):
- if Device.DEFAULT == "CLANG":
- # TODO: without casting to float16, 70B llama OOM on tinybox.
- return {
- k: (v.llvm_bf16_cast(dtypes.float32).to(v.device) if v.dtype == dtypes.bfloat16 else v)
- for k, v in weights.items()
- }
- if getenv("SUPPORT_BF16", 1):
- # TODO: without casting to float16, 70B llama OOM on tinybox.
- return {k: v.cast(dtypes.float32).cast(dtypes.float16) if v.dtype == dtypes.bfloat16 else v for k, v in weights.items()}
- # TODO: check if device supports bf16
- return {k: v.llvm_bf16_cast(dtypes.half).to(v.device) if v.dtype == dtypes.bfloat16 else v for k, v in weights.items()}
diff --git a/exo/inference/tinygrad/stateful_model.py b/exo/inference/tinygrad/stateful_model.py
deleted file mode 100644
index 1d9b605d..00000000
--- a/exo/inference/tinygrad/stateful_model.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from tinygrad import Tensor, Variable
-from collections import OrderedDict
-from typing import List, Optional
-
-def create_kv_cache(x: Tensor, layer):
- cache_kv = Tensor.zeros(2, x.shape[0], layer.max_context, layer.n_kv_heads, layer.head_dim, dtype=x.dtype).contiguous().realize()
- if isinstance(x.device, tuple):
- # TODO: instead of specifying how to shard, it can follow how xk and xv are being sharded
- cache_kv.shard_((x.device), axis=3 if getenv("SHARD_KVCACHE") else None).realize()
- return cache_kv.realize()
-
-class ModelState:
- cache: List[Tensor]
- start: int
- def __init__(self, cache: List[Tensor], start: int = 0):
- self.cache = cache
- self.start = start
-
-def make_prompt_state(x: Tensor, model):
- cache = [create_kv_cache(x, l.attention) for l in model.layers]
-
- return ModelState(cache)
diff --git a/exo/inference/tinygrad/tinygrad_helpers.py b/exo/inference/tinygrad/tinygrad_helpers.py
deleted file mode 100644
index a33b01ae..00000000
--- a/exo/inference/tinygrad/tinygrad_helpers.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from tinygrad.nn.state import safe_load, torch_load
-from tinygrad import Tensor
-from pathlib import Path
-import json
-from typing import List
-from exo.inference.shard import Shard
-from exo.helpers import DEBUG
-from exo.download.hf.hf_helpers import get_allow_patterns
-from fnmatch import fnmatch
-import re
-
-
-# **** helper functions ****
-def concat_weights(models, device=None):
- def convert(name) -> Tensor:
- disk_tensors: List[Tensor] = [model[name] for model in models]
- if len(disk_tensors) == 1 or len(disk_tensors[0].shape) == 1:
- return disk_tensors[0].to(device=device)
- axis = 1 if name.endswith(".attention.wo.weight") or name.endswith(".feed_forward.w2.weight") else 0
- lazy_tensors = [data.to(device=device) for data in disk_tensors]
- return lazy_tensors[0].cat(*lazy_tensors[1:], dim=axis)
-
- return {name: convert(name) for name in {name: None for model in models for name in model}}
-
-
-def load(fn: str, shard: Shard):
- if fn.endswith('.index.json'):
- with open(fn) as fp:
- weight_map = json.load(fp)['weight_map']
- parts = {}
- filtered_weight_map = {}
- allow_patterns = get_allow_patterns(weight_map, shard)
- for k, n in weight_map.items():
- if allow_patterns is not None and not any(fnmatch(n, r) for r in allow_patterns):
- continue
- if k.startswith("model.layers."):
- layer_num = int(k.split('.')[2])
- if layer_num < shard.start_layer or layer_num > shard.end_layer:
- continue
-
- parts[n] = load(str(Path(fn).parent/Path(n).name), shard)
- filtered_weight_map[k] = n
- if DEBUG >= 2: print(f"Excluded model param keys for {shard=}: {sorted(set(weight_map.keys()) - set(filtered_weight_map.keys()))}")
- return {k: parts[n][k] for k, n in filtered_weight_map.items()}
- elif fn.endswith(".safetensors"):
- weight_map = safe_load(fn)
- for k in list(weight_map):
- if (n := re.search(r"\.(\d+)\.", k)) and not (shard.start_layer <= int(n.group(1)) <= shard.end_layer):
- del weight_map[k]
- return weight_map
- else:
- return torch_load(fn)
diff --git a/exo/inference/tokenizers.py b/exo/inference/tokenizers.py
deleted file mode 100644
index 865765d5..00000000
--- a/exo/inference/tokenizers.py
+++ /dev/null
@@ -1,63 +0,0 @@
-import traceback
-from os import PathLike
-from aiofiles import os as aios
-from typing import Union
-from transformers import AutoTokenizer, AutoProcessor
-import numpy as np
-from exo.helpers import DEBUG
-from exo.download.new_shard_download import ensure_downloads_dir
-
-
-class DummyTokenizer:
- def __init__(self):
- self.eos_token_id = 69
- self.vocab_size = 1000
-
- def apply_chat_template(self, conversation, tokenize=True, add_generation_prompt=True, tools=None, **kwargs):
- return "dummy_tokenized_prompt"
-
- def encode(self, text):
- return np.array([1])
-
- def decode(self, tokens):
- return "dummy" * len(tokens)
-
-
-async def resolve_tokenizer(repo_id: Union[str, PathLike]):
- if repo_id == "dummy":
- return DummyTokenizer()
- local_path = await ensure_downloads_dir()/str(repo_id).replace("/", "--")
- if DEBUG >= 2: print(f"Checking if local path exists to load tokenizer from local {local_path=}")
- try:
- if local_path and await aios.path.exists(local_path):
- if DEBUG >= 2: print(f"Resolving tokenizer for {repo_id=} from {local_path=}")
- return await _resolve_tokenizer(local_path)
- except:
- if DEBUG >= 5: print(f"Local check for {local_path=} failed. Resolving tokenizer for {repo_id=} normally...")
- if DEBUG >= 5: traceback.print_exc()
- return await _resolve_tokenizer(repo_id)
-
-
-async def _resolve_tokenizer(repo_id_or_local_path: Union[str, PathLike]):
- try:
- if DEBUG >= 4: print(f"Trying AutoProcessor for {repo_id_or_local_path}")
- processor = AutoProcessor.from_pretrained(repo_id_or_local_path, use_fast=True if "Mistral-Large" in f"{repo_id_or_local_path}" else False, trust_remote_code=True)
- if not hasattr(processor, 'eos_token_id'):
- processor.eos_token_id = getattr(processor, 'tokenizer', getattr(processor, '_tokenizer', processor)).eos_token_id
- if not hasattr(processor, 'encode'):
- processor.encode = getattr(processor, 'tokenizer', getattr(processor, '_tokenizer', processor)).encode
- if not hasattr(processor, 'decode'):
- processor.decode = getattr(processor, 'tokenizer', getattr(processor, '_tokenizer', processor)).decode
- return processor
- except Exception as e:
- if DEBUG >= 4: print(f"Failed to load processor for {repo_id_or_local_path}. Error: {e}")
- if DEBUG >= 4: print(traceback.format_exc())
-
- try:
- if DEBUG >= 4: print(f"Trying AutoTokenizer for {repo_id_or_local_path}")
- return AutoTokenizer.from_pretrained(repo_id_or_local_path, trust_remote_code=True)
- except Exception as e:
- if DEBUG >= 4: print(f"Failed to load tokenizer for {repo_id_or_local_path}. Falling back to tinygrad tokenizer. Error: {e}")
- if DEBUG >= 4: print(traceback.format_exc())
-
- raise ValueError(f"[TODO] Unsupported model: {repo_id_or_local_path}")
diff --git a/exo/main.py b/exo/main.py
deleted file mode 100644
index 1ac5672d..00000000
--- a/exo/main.py
+++ /dev/null
@@ -1,402 +0,0 @@
-import argparse
-import asyncio
-import atexit
-import signal
-import json
-import platform
-import os
-import time
-import traceback
-import uuid
-import numpy as np
-from tqdm import tqdm
-from exo.train.dataset import load_dataset, iterate_batches
-from exo.networking.manual.manual_discovery import ManualDiscovery
-from exo.orchestration.node import Node
-from exo.networking.grpc.grpc_server import GRPCServer
-from exo.networking.udp.udp_discovery import UDPDiscovery
-from exo.networking.tailscale.tailscale_discovery import TailscaleDiscovery
-from exo.networking.grpc.grpc_peer_handle import GRPCPeerHandle
-from exo.topology.ring_memory_weighted_partitioning_strategy import RingMemoryWeightedPartitioningStrategy
-from exo.api import ChatGPTAPI
-from exo.download.shard_download import ShardDownloader, NoopShardDownloader
-from exo.download.download_progress import RepoProgressEvent
-from exo.download.new_shard_download import new_shard_downloader, has_exo_home_read_access, has_exo_home_write_access, ensure_exo_home, seed_models
-from exo.helpers import print_yellow_exo, find_available_port, DEBUG, get_system_info, get_or_create_node_id, get_all_ip_addresses_and_interfaces, terminal_link, shutdown
-from exo.inference.shard import Shard
-from exo.inference.inference_engine import get_inference_engine
-from exo.inference.tokenizers import resolve_tokenizer
-from exo.models import build_base_shard, get_repo
-from exo.viz.topology_viz import TopologyViz
-import uvloop
-import concurrent.futures
-import resource
-import psutil
-
-# TODO: figure out why this is happening
-os.environ["GRPC_VERBOSITY"] = "error"
-os.environ["TRANSFORMERS_VERBOSITY"] = "error"
-os.environ["TOKENIZERS_PARALLELISM"] = "true"
-
-# Configure uvloop for maximum performance
-def configure_uvloop():
- uvloop.install()
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
-
- # Increase file descriptor limits on Unix systems
- if not psutil.WINDOWS:
- soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
- try: resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
- except ValueError:
- try: resource.setrlimit(resource.RLIMIT_NOFILE, (8192, hard))
- except ValueError: pass
-
- loop.set_default_executor(concurrent.futures.ThreadPoolExecutor(max_workers=min(32, (os.cpu_count() or 1) * 4)))
- return loop
-
-# parse args
-parser = argparse.ArgumentParser(description="Initialize GRPC Discovery")
-parser.add_argument("command", nargs="?", choices=["run", "eval", "train"], help="Command to run")
-parser.add_argument("model_name", nargs="?", help="Model name to run")
-parser.add_argument("--default-model", type=str, default=None, help="Default model")
-parser.add_argument("--iters", type=int, default=100, help="Training iterations")
-parser.add_argument("--save-every", type=int, default=5, help="Save the model every N iterations.")
-parser.add_argument("--data", type=str, default="exo/train/data/lora", help="Directory where training data lives")
-parser.add_argument("--batch-size", type=int, default=1, help="Minibatch size.")
-parser.add_argument("--resume-checkpoint", type=str, default=None, help="Path to a custom checkpoint to load")
-parser.add_argument("--save-checkpoint-dir", type=str, default="checkpoints", help="Path to a folder where checkpoints are stored")
-parser.add_argument("--node-id", type=str, default=None, help="Node ID")
-parser.add_argument("--node-host", type=str, default="0.0.0.0", help="Node host")
-parser.add_argument("--node-port", type=int, default=None, help="Node port")
-parser.add_argument("--models-seed-dir", type=str, default=None, help="Model seed directory")
-parser.add_argument("--listen-port", type=int, default=5678, help="Listening port for discovery")
-parser.add_argument("--download-quick-check", action="store_true", help="Quick check local path for model shards download")
-parser.add_argument("--max-parallel-downloads", type=int, default=8, help="Max parallel downloads for model shards download")
-parser.add_argument("--broadcast-port", type=int, default=5678, help="Broadcast port for discovery")
-parser.add_argument("--discovery-module", type=str, choices=["udp", "tailscale", "manual"], default="udp", help="Discovery module to use")
-parser.add_argument("--discovery-timeout", type=int, default=30, help="Discovery timeout in seconds")
-parser.add_argument("--discovery-config-path", type=str, default=None, help="Path to discovery config json file")
-parser.add_argument("--wait-for-peers", type=int, default=0, help="Number of peers to wait to connect to before starting")
-parser.add_argument("--chatgpt-api-port", type=int, default=52415, help="ChatGPT API port")
-parser.add_argument("--chatgpt-api-response-timeout", type=int, default=900, help="ChatGPT API response timeout in seconds")
-parser.add_argument("--max-generate-tokens", type=int, default=10000, help="Max tokens to generate in each request")
-parser.add_argument("--inference-engine", type=str, default=None, help="Inference engine to use (mlx, tinygrad, or dummy)")
-parser.add_argument("--disable-tui", action=argparse.BooleanOptionalAction, help="Disable TUI")
-parser.add_argument("--run-model", type=str, help="Specify a model to run directly")
-parser.add_argument("--prompt", type=str, help="Prompt for the model when using --run-model", default="Who are you?")
-parser.add_argument("--default-temp", type=float, help="Default token sampling temperature", default=0.0)
-parser.add_argument("--tailscale-api-key", type=str, default=None, help="Tailscale API key")
-parser.add_argument("--tailnet-name", type=str, default=None, help="Tailnet name")
-parser.add_argument("--node-id-filter", type=str, default=None, help="Comma separated list of allowed node IDs (only for UDP and Tailscale discovery)")
-parser.add_argument("--interface-type-filter", type=str, default=None, help="Comma separated list of allowed interface types (only for UDP discovery)")
-parser.add_argument("--system-prompt", type=str, default=None, help="System prompt for the ChatGPT API")
-args = parser.parse_args()
-print(f"Selected inference engine: {args.inference_engine}")
-
-print_yellow_exo()
-
-print("\n" + "="*80)
-print("EXO")
-print("="*80)
-print("\nEXO started out of a desire to run research experiments on large language")
-print("models using the hardware we already owned.")
-print("\nWhat began here is becoming part of something much larger.")
-print("\nsoon™")
-print("\n- The EXO Team")
-print("="*80 + "\n")
-
-system_info = get_system_info()
-print(f"Detected system: {system_info}")
-
-shard_downloader: ShardDownloader = new_shard_downloader(args.max_parallel_downloads) if args.inference_engine != "dummy" else NoopShardDownloader()
-inference_engine_name = args.inference_engine or ("mlx" if system_info == "Apple Silicon Mac" else "tinygrad")
-print(f"Inference engine name after selection: {inference_engine_name}")
-
-inference_engine = get_inference_engine(inference_engine_name, shard_downloader)
-print(f"Using inference engine: {inference_engine.__class__.__name__} with shard downloader: {shard_downloader.__class__.__name__}")
-
-if args.node_port is None:
- args.node_port = find_available_port(args.node_host)
- if DEBUG >= 1: print(f"Using available port: {args.node_port}")
-
-args.node_id = args.node_id or get_or_create_node_id()
-chatgpt_api_endpoints = [f"http://{ip}:{args.chatgpt_api_port}/v1/chat/completions" for ip, _ in get_all_ip_addresses_and_interfaces()]
-web_chat_urls = [f"http://{ip}:{args.chatgpt_api_port}" for ip, _ in get_all_ip_addresses_and_interfaces()]
-if DEBUG >= 0:
- print("Chat interface started:")
- for web_chat_url in web_chat_urls:
- print(f" - {terminal_link(web_chat_url)}")
- print("ChatGPT API endpoint served at:")
- for chatgpt_api_endpoint in chatgpt_api_endpoints:
- print(f" - {terminal_link(chatgpt_api_endpoint)}")
-
-# Convert node-id-filter and interface-type-filter to lists if provided
-allowed_node_ids = args.node_id_filter.split(',') if args.node_id_filter else None
-allowed_interface_types = args.interface_type_filter.split(',') if args.interface_type_filter else None
-
-if args.discovery_module == "udp":
- discovery = UDPDiscovery(
- args.node_id,
- args.node_port,
- args.listen_port,
- args.broadcast_port,
- lambda peer_id, address, description, device_capabilities: GRPCPeerHandle(peer_id, address, description, device_capabilities),
- discovery_timeout=args.discovery_timeout,
- allowed_node_ids=allowed_node_ids,
- allowed_interface_types=allowed_interface_types
- )
-elif args.discovery_module == "tailscale":
- discovery = TailscaleDiscovery(
- args.node_id,
- args.node_port,
- lambda peer_id, address, description, device_capabilities: GRPCPeerHandle(peer_id, address, description, device_capabilities),
- discovery_timeout=args.discovery_timeout,
- tailscale_api_key=args.tailscale_api_key,
- tailnet=args.tailnet_name,
- allowed_node_ids=allowed_node_ids
- )
-elif args.discovery_module == "manual":
- if not args.discovery_config_path:
- raise ValueError(f"--discovery-config-path is required when using manual discovery. Please provide a path to a config json file.")
- discovery = ManualDiscovery(args.discovery_config_path, args.node_id, create_peer_handle=lambda peer_id, address, description, device_capabilities: GRPCPeerHandle(peer_id, address, description, device_capabilities))
-topology_viz = TopologyViz(chatgpt_api_endpoints=chatgpt_api_endpoints, web_chat_urls=web_chat_urls) if not args.disable_tui else None
-node = Node(
- args.node_id,
- None,
- inference_engine,
- discovery,
- shard_downloader,
- partitioning_strategy=RingMemoryWeightedPartitioningStrategy(),
- max_generate_tokens=args.max_generate_tokens,
- topology_viz=topology_viz,
- default_sample_temperature=args.default_temp
-)
-server = GRPCServer(node, args.node_host, args.node_port)
-node.server = server
-api = ChatGPTAPI(
- node,
- node.inference_engine.__class__.__name__,
- response_timeout=args.chatgpt_api_response_timeout,
- on_chat_completion_request=lambda req_id, __, prompt: topology_viz.update_prompt(req_id, prompt) if topology_viz else None,
- default_model=args.default_model,
- system_prompt=args.system_prompt
-)
-buffered_token_output = {}
-def update_topology_viz(req_id, tokens, __):
- if not topology_viz: return
- if not node.inference_engine.shard: return
- if node.inference_engine.shard.model_id == 'stable-diffusion-2-1-base': return
- if req_id in buffered_token_output: buffered_token_output[req_id].extend(tokens)
- else: buffered_token_output[req_id] = tokens
- topology_viz.update_prompt_output(req_id, node.inference_engine.tokenizer.decode(buffered_token_output[req_id]))
-node.on_token.register("update_topology_viz").on_next(update_topology_viz)
-def update_prompt_viz(request_id, opaque_status: str):
- if not topology_viz: return
- try:
- status = json.loads(opaque_status)
- if status.get("type") != "node_status" or status.get("status") != "start_process_prompt": return
- topology_viz.update_prompt(request_id, status.get("prompt", "corrupted prompt (this should never happen)"))
- except Exception as e:
- if DEBUG >= 2:
- print(f"Failed to update prompt viz: {e}")
- traceback.print_exc()
-node.on_opaque_status.register("update_prompt_viz").on_next(update_prompt_viz)
-
-def preemptively_load_shard(request_id: str, opaque_status: str):
- try:
- status = json.loads(opaque_status)
- if status.get("type") != "node_status" or status.get("status") != "start_process_prompt": return
- current_shard = node.get_current_shard(Shard.from_dict(status.get("shard")))
- if DEBUG >= 2: print(f"Preemptively starting download for {current_shard}")
- asyncio.create_task(node.inference_engine.ensure_shard(current_shard))
- except Exception as e:
- if DEBUG >= 2:
- print(f"Failed to preemptively start download: {e}")
- traceback.print_exc()
-node.on_opaque_status.register("preemptively_load_shard").on_next(preemptively_load_shard)
-
-last_events: dict[str, tuple[float, RepoProgressEvent]] = {}
-def throttled_broadcast(shard: Shard, event: RepoProgressEvent):
- global last_events
- current_time = time.time()
- if event.status == "not_started": return
- last_event = last_events.get(shard.model_id)
- if last_event and last_event[1].status == "complete" and event.status == "complete": return
- if last_event and last_event[0] == event.status and current_time - last_event[0] < 0.2: return
- last_events[shard.model_id] = (current_time, event)
- asyncio.create_task(node.broadcast_opaque_status("", json.dumps({"type": "download_progress", "node_id": node.id, "progress": event.to_dict()})))
-shard_downloader.on_progress.register("broadcast").on_next(throttled_broadcast)
-
-async def run_model_cli(node: Node, model_name: str, prompt: str):
- inference_class = node.inference_engine.__class__.__name__
- shard = build_base_shard(model_name, inference_class)
- if not shard:
- print(f"Error: Unsupported model '{model_name}' for inference engine {inference_class}")
- return
- tokenizer = await resolve_tokenizer(get_repo(shard.model_id, inference_class))
- request_id = str(uuid.uuid4())
- callback_id = f"cli-wait-response-{request_id}"
- callback = node.on_token.register(callback_id)
- if topology_viz:
- topology_viz.update_prompt(request_id, prompt)
- prompt = tokenizer.apply_chat_template([{"role": "user", "content": prompt}], tokenize=False, add_generation_prompt=True)
-
- try:
- print(f"Processing prompt: {prompt}")
- await node.process_prompt(shard, prompt, request_id=request_id)
-
- tokens = []
- def on_token(_request_id, _tokens, _is_finished):
- tokens.extend(_tokens)
- return _request_id == request_id and _is_finished
- await callback.wait(on_token, timeout=300)
-
- print("\nGenerated response:")
- print(tokenizer.decode(tokens))
- except Exception as e:
- print(f"Error processing prompt: {str(e)}")
- traceback.print_exc()
- finally:
- node.on_token.deregister(callback_id)
-
-def clean_path(path):
- """Clean and resolve path"""
- if path.startswith("Optional("):
- path = path.strip('Optional("').rstrip('")')
- return os.path.expanduser(path)
-
-async def hold_outstanding(node: Node):
- while node.outstanding_requests:
- await asyncio.sleep(.5)
- return
-
-async def run_iter(node: Node, shard: Shard, train: bool, data, batch_size=1):
- losses = []
- tokens = []
- for batch in tqdm(iterate_batches(data, batch_size), total=len(data) // batch_size):
- _, _, lengths = batch
- losses.append(np.sum(lengths * await node.enqueue_example(shard, *batch, train=train)))
- tokens.append(np.sum(lengths))
- total_tokens = np.sum(tokens)
- total_loss = np.sum(losses) / total_tokens
-
- return total_loss, total_tokens
-
-async def eval_model_cli(node: Node, model_name, dataloader, batch_size, num_batches=-1):
- inference_class = node.inference_engine.__class__.__name__
- shard = build_base_shard(model_name, inference_class)
- if not shard:
- print(f"Error: Unsupported model '{model_name}' for inference engine {inference_class}")
- return
- tokenizer = await resolve_tokenizer(get_repo(shard.model_id, inference_class))
- train, val, test = dataloader(tokenizer.encode)
- print(f"Evaluating {len(test)} examples with batch_size {batch_size}")
- loss, tokens = await run_iter(node, shard, False, test, batch_size)
- print(f"total | {loss=}, {tokens=}")
- print("Waiting for outstanding tasks")
- await hold_outstanding(node)
-
-async def train_model_cli(node: Node, model_name, dataloader, batch_size, iters, save_interval=0, checkpoint_dir=None):
- inference_class = node.inference_engine.__class__.__name__
- shard = build_base_shard(model_name, inference_class)
- if not shard:
- print(f"Error: Unsupported model '{model_name}' for inference engine {inference_class}")
- return
- tokenizer = await resolve_tokenizer(get_repo(shard.model_id, inference_class))
- train, val, test = dataloader(tokenizer.encode)
- print(f"Training on {len(train)} examples with batch_size {batch_size} for {iters} epochs")
- for i in tqdm(range(3)):
- await asyncio.sleep(1)
- for epoch in range(iters):
- loss, tokens = await run_iter(node, shard, True, train, batch_size)
- print(f"epoch {epoch + 1}/{iters}\t| loss: {loss}, tokens: {tokens}")
- if save_interval > 0 and epoch > 0 and (epoch % save_interval) == 0 and checkpoint_dir is not None:
- await node.coordinate_save(shard, epoch, checkpoint_dir)
- await hold_outstanding(node)
- await hold_outstanding(node)
-
-async def check_exo_home():
- home, has_read, has_write = await ensure_exo_home(), await has_exo_home_read_access(), await has_exo_home_write_access()
- if DEBUG >= 1: print(f"exo home directory: {home}")
- print(f"{has_read=}, {has_write=}")
- if not has_read or not has_write:
- print(f"""
- WARNING: Limited permissions for exo home directory: {home}.
- This may prevent model downloads from working correctly.
- {"❌ No read access" if not has_read else ""}
- {"❌ No write access" if not has_write else ""}
- """)
-
-async def main():
- loop = asyncio.get_running_loop()
-
- try: await check_exo_home()
- except Exception as e: print(f"Error checking exo home directory: {e}")
-
- if not args.models_seed_dir is None:
- try:
- models_seed_dir = clean_path(args.models_seed_dir)
- await seed_models(models_seed_dir)
- except Exception as e:
- print(f"Error seeding models: {e}")
-
- def restore_cursor():
- if platform.system() != "Windows":
- os.system("tput cnorm") # Show cursor
-
- # Restore the cursor when the program exits
- atexit.register(restore_cursor)
-
- # Use a more direct approach to handle signals
- def handle_exit():
- asyncio.ensure_future(shutdown(signal.SIGTERM, loop, node.server))
-
- if platform.system() != "Windows":
- for s in [signal.SIGINT, signal.SIGTERM]:
- loop.add_signal_handler(s, handle_exit)
-
- await node.start(wait_for_peers=args.wait_for_peers)
-
- if args.command == "run" or args.run_model:
- model_name = args.model_name or args.run_model
- if not model_name:
- print("Error: Model name is required when using 'run' command or --run-model")
- return
- await run_model_cli(node, model_name, args.prompt)
- elif args.command == "eval" or args.command == 'train':
- model_name = args.model_name
- dataloader = lambda tok: load_dataset(args.data, preprocess=lambda item: tok(item)
- , loadline=lambda line: json.loads(line).get("text",""))
- if args.command == 'eval':
- if not model_name:
- print("Error: Much like a human, I can't evaluate anything without a model")
- return
- await eval_model_cli(node, model_name, dataloader, args.batch_size)
- else:
- if not model_name:
- print("Error: This train ain't leaving the station without a model")
- return
- await train_model_cli(node, model_name, dataloader, args.batch_size, args.iters, save_interval=args.save_every, checkpoint_dir=args.save_checkpoint_dir)
-
- else:
- asyncio.create_task(api.run(port=args.chatgpt_api_port)) # Start the API server as a non-blocking task
- await asyncio.Event().wait()
-
- if args.wait_for_peers > 0:
- print("Cooldown to allow peers to exit gracefully")
- for i in tqdm(range(50)):
- await asyncio.sleep(.1)
-
-def run():
- loop = None
- try:
- loop = configure_uvloop()
- loop.run_until_complete(main())
- except KeyboardInterrupt:
- print("\nShutdown requested... exiting")
- finally:
- if loop: loop.close()
-
-if __name__ == "__main__":
- run()
diff --git a/exo/models.py b/exo/models.py
deleted file mode 100644
index b64bcca1..00000000
--- a/exo/models.py
+++ /dev/null
@@ -1,273 +0,0 @@
-from exo.inference.shard import Shard
-from typing import Optional, List
-
-model_cards = {
- ### llama
- "llama-3.3-70b": {
- "layers": 80,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Llama-3.3-70B-Instruct-4bit",
- "TinygradDynamicShardInferenceEngine": "unsloth/Llama-3.3-70B-Instruct",
- },
- },
- "llama-3.2-1b": {
- "layers": 16,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Llama-3.2-1B-Instruct-4bit",
- "TinygradDynamicShardInferenceEngine": "unsloth/Llama-3.2-1B-Instruct",
- },
- },
- "llama-3.2-1b-8bit": {
- "layers": 16,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Llama-3.2-1B-Instruct-8bit",
- "TinygradDynamicShardInferenceEngine": "unsloth/Llama-3.2-1B-Instruct",
- },
- },
- "llama-3.2-3b": {
- "layers": 28,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Llama-3.2-3B-Instruct-4bit",
- "TinygradDynamicShardInferenceEngine": "unsloth/Llama-3.2-3B-Instruct",
- },
- },
- "llama-3.2-3b-8bit": {
- "layers": 28,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Llama-3.2-3B-Instruct-8bit",
- "TinygradDynamicShardInferenceEngine": "unsloth/Llama-3.2-3B-Instruct",
- },
- },
- "llama-3.2-3b-bf16": {
- "layers": 28,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Llama-3.2-3B-Instruct",
- "TinygradDynamicShardInferenceEngine": "unsloth/Llama-3.2-3B-Instruct",
- },
- },
- "llama-3.1-8b": {
- "layers": 32,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3.1-8B-Instruct-4bit",
- "TinygradDynamicShardInferenceEngine": "mlabonne/Meta-Llama-3.1-8B-Instruct-abliterated",
- },
- },
- "llama-3.1-70b": {
- "layers": 80,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3.1-70B-Instruct-4bit",
- "TinygradDynamicShardInferenceEngine": "NousResearch/Meta-Llama-3.1-70B-Instruct",
- },
- },
- "llama-3.1-70b-bf16": {
- "layers": 80,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3.1-70B-Instruct-bf16-CORRECTED",
- "TinygradDynamicShardInferenceEngine": "NousResearch/Meta-Llama-3.1-70B-Instruct",
- },
- },
- "llama-3-8b": {
- "layers": 32,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3-8B-Instruct-4bit",
- "TinygradDynamicShardInferenceEngine": "TriAiExperiments/SFR-Iterative-DPO-LLaMA-3-8B-R",
- },
- },
- "llama-3-70b": {
- "layers": 80,
- "repo": {
- "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3-70B-Instruct-4bit",
- "TinygradDynamicShardInferenceEngine": "TriAiExperiments/SFR-Iterative-DPO-LLaMA-3-70B-R",
- },
- },
- "llama-3.1-405b": { "layers": 126, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3.1-405B-4bit", }, },
- "llama-3.1-405b-8bit": { "layers": 126, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Meta-Llama-3.1-405B-Instruct-8bit", }, },
- ### mistral
- "mistral-nemo": { "layers": 40, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Mistral-Nemo-Instruct-2407-4bit", }, },
- "mistral-large": { "layers": 88, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Mistral-Large-Instruct-2407-4bit", }, },
- ### deepseek
- "deepseek-coder-v2-lite": { "layers": 27, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-Coder-V2-Lite-Instruct-4bit-mlx", }, },
- "deepseek-coder-v2.5": { "layers": 60, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-V2.5-MLX-AQ4_1_64", }, },
- "deepseek-v3": { "layers": 61, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-V3-4bit", }, },
- "deepseek-v3-3bit": { "layers": 61, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-V3-3bit", }, },
- "deepseek-r1": { "layers": 61, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-4bit", }, },
- "deepseek-r1-3bit": { "layers": 61, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-3bit", }, },
- ### deepseek distills
- "deepseek-r1-distill-qwen-1.5b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/deepseek-r1-distill-qwen-1.5b", }, },
- "deepseek-r1-distill-qwen-1.5b-3bit": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-1.5B-3bit", }, },
- "deepseek-r1-distill-qwen-1.5b-6bit": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-1.5B-6bit", }, },
- "deepseek-r1-distill-qwen-1.5b-8bit": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-1.5B-8bit", }, },
- "deepseek-r1-distill-qwen-1.5b-bf16": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-1.5B-bf16", }, },
- "deepseek-r1-distill-qwen-7b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit", }, },
- "deepseek-r1-distill-qwen-7b-3bit": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-7B-3bit", }, },
- "deepseek-r1-distill-qwen-7b-6bit": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-7B-6bit", }, },
- "deepseek-r1-distill-qwen-7b-8bit": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-7B-8bit", }, },
- "deepseek-r1-distill-qwen-7b-bf16": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-7B-bf16", }, },
- "deepseek-r1-distill-qwen-14b": { "layers": 48, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-14B-4bit", }, },
- "deepseek-r1-distill-qwen-14b-3bit": { "layers": 48, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-14B-3bit", }, },
- "deepseek-r1-distill-qwen-14b-6bit": { "layers": 48, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-14B-6bit", }, },
- "deepseek-r1-distill-qwen-14b-8bit": { "layers": 48, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-14B-8bit", }, },
- "deepseek-r1-distill-qwen-14b-bf16": { "layers": 48, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-14B-bf16", }, },
- "deepseek-r1-distill-qwen-32b": { "layers": 64, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-32B-4bit", }, },
- "deepseek-r1-distill-qwen-32b-3bit": { "layers": 64, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-32B-3bit", }, },
- "deepseek-r1-distill-qwen-32b-6bit": { "layers": 64, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-32B-6bit", }, },
- "deepseek-r1-distill-qwen-32b-8bit": { "layers": 64, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-32B-MLX-8Bit", }, },
- "deepseek-r1-distill-qwen-32b-bf16": { "layers": 64, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Qwen-32B-bf16", }, },
- "deepseek-r1-distill-llama-8b": { "layers": 32, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Llama-8B-4bit", }, },
- "deepseek-r1-distill-llama-8b-3bit": { "layers": 32, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Llama-8B-3bit", }, },
- "deepseek-r1-distill-llama-8b-6bit": { "layers": 32, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Llama-8B-6bit", }, },
- "deepseek-r1-distill-llama-8b-8bit": { "layers": 32, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Llama-8B-8bit", }, },
- "deepseek-r1-distill-llama-8b-bf16": { "layers": 32, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Llama-8B-bf16", }, },
- "deepseek-r1-distill-llama-70b": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Llama-70B-4bit", }, },
- "deepseek-r1-distill-llama-70b-3bit": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Llama-70B-3bit", }, },
- "deepseek-r1-distill-llama-70b-6bit": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Llama-70B-6bit", }, },
- "deepseek-r1-distill-llama-70b-8bit": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/DeepSeek-R1-Distill-Llama-70B-8bit", }, },
- ### llava
- "llava-1.5-7b-hf": { "layers": 32, "repo": { "MLXDynamicShardInferenceEngine": "llava-hf/llava-1.5-7b-hf", }, },
- ### qwen
- "qwen-2.5-0.5b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-0.5B-Instruct-4bit", }, },
- "qwen-2.5-1.5b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-1.5B-Instruct-4bit", }, },
- "qwen-2.5-coder-1.5b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Coder-1.5B-Instruct-4bit", }, },
- "qwen-2.5-3b": { "layers": 36, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-3B-Instruct-4bit", }, },
- "qwen-2.5-coder-3b": { "layers": 36, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Coder-3B-Instruct-4bit", }, },
- "qwen-2.5-7b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-7B-Instruct-4bit", }, },
- "qwen-2.5-coder-7b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Coder-7B-Instruct-4bit", }, },
- "qwen-2.5-math-7b": { "layers": 28, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Math-7B-Instruct-4bit", }, },
- "qwen-2.5-14b": { "layers": 48, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-14B-Instruct-4bit", }, },
- "qwen-2.5-coder-14b": { "layers": 48, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Coder-14B-Instruct-4bit", }, },
- "qwen-2.5-32b": { "layers": 64, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-32B-Instruct-4bit", }, },
- "qwen-2.5-coder-32b": { "layers": 64, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Coder-32B-Instruct-4bit", }, },
- "qwen-2.5-72b": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-72B-Instruct-4bit", }, },
- "qwen-2.5-math-72b": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Qwen2.5-Math-72B-Instruct-4bit", }, },
- ### nemotron
- "nemotron-70b": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/nvidia_Llama-3.1-Nemotron-70B-Instruct-HF_4bit", }, },
- "nemotron-70b-bf16": { "layers": 80, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Llama-3.1-Nemotron-70B-Instruct-HF-bf16", }, },
- # gemma
- "gemma2-9b": { "layers": 42, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/gemma-2-9b-it-4bit", }, },
- "gemma2-27b": { "layers": 46, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/gemma-2-27b-it-4bit", }, },
- # stable diffusion
- "stable-diffusion-2-1-base": { "layers": 31, "repo": { "MLXDynamicShardInferenceEngine": "stabilityai/stable-diffusion-2-1-base" } },
- # phi
- "phi-3.5-mini": { "layers": 32, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/Phi-3.5-mini-instruct-4bit", }, },
- "phi-4": { "layers": 40, "repo": { "MLXDynamicShardInferenceEngine": "mlx-community/phi-4-4bit", }, },
- # dummy
- "dummy": { "layers": 8, "repo": { "DummyInferenceEngine": "dummy", }, },
-}
-
-pretty_name = {
- "llama-3.3-70b": "Llama 3.3 70B",
- "llama-3.2-1b": "Llama 3.2 1B",
- "llama-3.2-1b-8bit": "Llama 3.2 1B (8-bit)",
- "llama-3.2-3b": "Llama 3.2 3B",
- "llama-3.2-3b-8bit": "Llama 3.2 3B (8-bit)",
- "llama-3.2-3b-bf16": "Llama 3.2 3B (BF16)",
- "llama-3.1-8b": "Llama 3.1 8B",
- "llama-3.1-70b": "Llama 3.1 70B",
- "llama-3.1-70b-bf16": "Llama 3.1 70B (BF16)",
- "llama-3.1-405b": "Llama 3.1 405B",
- "llama-3.1-405b-8bit": "Llama 3.1 405B (8-bit)",
- "gemma2-9b": "Gemma2 9B",
- "gemma2-27b": "Gemma2 27B",
- "nemotron-70b": "Nemotron 70B",
- "nemotron-70b-bf16": "Nemotron 70B (BF16)",
- "mistral-nemo": "Mistral Nemo",
- "mistral-large": "Mistral Large",
- "deepseek-coder-v2-lite": "Deepseek Coder V2 Lite",
- "deepseek-coder-v2.5": "Deepseek Coder V2.5",
- "deepseek-v3": "Deepseek V3 (4-bit)",
- "deepseek-v3-3bit": "Deepseek V3 (3-bit)",
- "deepseek-r1": "Deepseek R1 (4-bit)",
- "deepseek-r1-3bit": "Deepseek R1 (3-bit)",
- "llava-1.5-7b-hf": "LLaVa 1.5 7B (Vision Model)",
- "qwen-2.5-0.5b": "Qwen 2.5 0.5B",
- "qwen-2.5-1.5b": "Qwen 2.5 1.5B",
- "qwen-2.5-coder-1.5b": "Qwen 2.5 Coder 1.5B",
- "qwen-2.5-3b": "Qwen 2.5 3B",
- "qwen-2.5-coder-3b": "Qwen 2.5 Coder 3B",
- "qwen-2.5-7b": "Qwen 2.5 7B",
- "qwen-2.5-coder-7b": "Qwen 2.5 Coder 7B",
- "qwen-2.5-math-7b": "Qwen 2.5 7B (Math)",
- "qwen-2.5-14b": "Qwen 2.5 14B",
- "qwen-2.5-coder-14b": "Qwen 2.5 Coder 14B",
- "qwen-2.5-32b": "Qwen 2.5 32B",
- "qwen-2.5-coder-32b": "Qwen 2.5 Coder 32B",
- "qwen-2.5-72b": "Qwen 2.5 72B",
- "qwen-2.5-math-72b": "Qwen 2.5 72B (Math)",
- "phi-3.5-mini": "Phi-3.5 Mini",
- "phi-4": "Phi-4",
- "llama-3-8b": "Llama 3 8B",
- "llama-3-70b": "Llama 3 70B",
- "stable-diffusion-2-1-base": "Stable Diffusion 2.1",
- "deepseek-r1-distill-qwen-1.5b": "DeepSeek R1 Distill Qwen 1.5B",
- "deepseek-r1-distill-qwen-1.5b-3bit": "DeepSeek R1 Distill Qwen 1.5B (3-bit)",
- "deepseek-r1-distill-qwen-1.5b-6bit": "DeepSeek R1 Distill Qwen 1.5B (6-bit)",
- "deepseek-r1-distill-qwen-1.5b-8bit": "DeepSeek R1 Distill Qwen 1.5B (8-bit)",
- "deepseek-r1-distill-qwen-1.5b-bf16": "DeepSeek R1 Distill Qwen 1.5B (BF16)",
- "deepseek-r1-distill-qwen-7b": "DeepSeek R1 Distill Qwen 7B",
- "deepseek-r1-distill-qwen-7b-3bit": "DeepSeek R1 Distill Qwen 7B (3-bit)",
- "deepseek-r1-distill-qwen-7b-6bit": "DeepSeek R1 Distill Qwen 7B (6-bit)",
- "deepseek-r1-distill-qwen-7b-8bit": "DeepSeek R1 Distill Qwen 7B (8-bit)",
- "deepseek-r1-distill-qwen-7b-bf16": "DeepSeek R1 Distill Qwen 7B (BF16)",
- "deepseek-r1-distill-qwen-14b": "DeepSeek R1 Distill Qwen 14B",
- "deepseek-r1-distill-qwen-14b-3bit": "DeepSeek R1 Distill Qwen 14B (3-bit)",
- "deepseek-r1-distill-qwen-14b-6bit": "DeepSeek R1 Distill Qwen 14B (6-bit)",
- "deepseek-r1-distill-qwen-14b-8bit": "DeepSeek R1 Distill Qwen 14B (8-bit)",
- "deepseek-r1-distill-qwen-14b-bf16": "DeepSeek R1 Distill Qwen 14B (BF16)",
- "deepseek-r1-distill-qwen-32b": "DeepSeek R1 Distill Qwen 32B",
- "deepseek-r1-distill-qwen-32b-3bit": "DeepSeek R1 Distill Qwen 32B (3-bit)",
- "deepseek-r1-distill-qwen-32b-8bit": "DeepSeek R1 Distill Qwen 32B (8-bit)",
- "deepseek-r1-distill-qwen-32b-bf16": "DeepSeek R1 Distill Qwen 32B (BF16)",
- "deepseek-r1-distill-llama-8b-8bit": "DeepSeek R1 Distill Llama 8B (8-bit)",
- "deepseek-r1-distill-llama-70b-6bit": "DeepSeek R1 Distill Llama 70B (6-bit)",
- "deepseek-r1-distill-llama-70b-8bit": "DeepSeek R1 Distill Llama 70B (8-bit)",
- "deepseek-r1-distill-llama-8b": "DeepSeek R1 Distill Llama 8B",
- "deepseek-r1-distill-llama-8b-3bit": "DeepSeek R1 Distill Llama 8B (3-bit)",
- "deepseek-r1-distill-llama-8b-6bit": "DeepSeek R1 Distill Llama 8B (6-bit)",
- "deepseek-r1-distill-llama-8b-8bit": "DeepSeek R1 Distill Llama 8B (8-bit)",
- "deepseek-r1-distill-llama-8b-bf16": "DeepSeek R1 Distill Llama 8B (BF16)",
- "deepseek-r1-distill-llama-70b": "DeepSeek R1 Distill Llama 70B",
- "deepseek-r1-distill-llama-70b-3bit": "DeepSeek R1 Distill Llama 70B (3-bit)",
- "deepseek-r1-distill-llama-70b-6bit": "DeepSeek R1 Distill Llama 70B (6-bit)",
- "deepseek-r1-distill-llama-70b-8bit": "DeepSeek R1 Distill Llama 70B (8-bit)",
- "deepseek-r1-distill-qwen-32b-6bit": "DeepSeek R1 Distill Qwen 32B (6-bit)",
-}
-
-def get_repo(model_id: str, inference_engine_classname: str) -> Optional[str]:
- return model_cards.get(model_id, {}).get("repo", {}).get(inference_engine_classname, None)
-
-def get_pretty_name(model_id: str) -> Optional[str]:
- return pretty_name.get(model_id, None)
-
-def build_base_shard(model_id: str, inference_engine_classname: str) -> Optional[Shard]:
- repo = get_repo(model_id, inference_engine_classname)
- n_layers = model_cards.get(model_id, {}).get("layers", 0)
- if repo is None or n_layers < 1:
- return None
- return Shard(model_id, 0, 0, n_layers)
-
-def build_full_shard(model_id: str, inference_engine_classname: str) -> Optional[Shard]:
- base_shard = build_base_shard(model_id, inference_engine_classname)
- if base_shard is None: return None
- return Shard(base_shard.model_id, 0, base_shard.n_layers - 1, base_shard.n_layers)
-
-def get_supported_models(supported_inference_engine_lists: Optional[List[List[str]]] = None) -> List[str]:
- if not supported_inference_engine_lists:
- return list(model_cards.keys())
-
- from exo.inference.inference_engine import inference_engine_classes
- supported_inference_engine_lists = [
- [inference_engine_classes[engine] if engine in inference_engine_classes else engine for engine in engine_list]
- for engine_list in supported_inference_engine_lists
- ]
-
- def has_any_engine(model_info: dict, engine_list: List[str]) -> bool:
- return any(engine in model_info.get("repo", {}) for engine in engine_list)
-
- def supports_all_engine_lists(model_info: dict) -> bool:
- return all(has_any_engine(model_info, engine_list)
- for engine_list in supported_inference_engine_lists)
-
- return [
- model_id for model_id, model_info in model_cards.items()
- if supports_all_engine_lists(model_info)
- ]
diff --git a/exo/networking/__init__.py b/exo/networking/__init__.py
deleted file mode 100644
index 44a10a30..00000000
--- a/exo/networking/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .discovery import Discovery
-from .peer_handle import PeerHandle
-from .server import Server
-
-__all__ = ["Discovery", "PeerHandle", "Server"]
diff --git a/exo/networking/discovery.py b/exo/networking/discovery.py
deleted file mode 100644
index cdcbfabc..00000000
--- a/exo/networking/discovery.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import List
-from .peer_handle import PeerHandle
-
-
-class Discovery(ABC):
- @abstractmethod
- async def start(self) -> None:
- pass
-
- @abstractmethod
- async def stop(self) -> None:
- pass
-
- @abstractmethod
- async def discover_peers(self, wait_for_peers: int = 0) -> List[PeerHandle]:
- pass
diff --git a/exo/networking/grpc/__init__.py b/exo/networking/grpc/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/networking/grpc/grpc_peer_handle.py b/exo/networking/grpc/grpc_peer_handle.py
deleted file mode 100644
index 997d43ef..00000000
--- a/exo/networking/grpc/grpc_peer_handle.py
+++ /dev/null
@@ -1,226 +0,0 @@
-import grpc
-import numpy as np
-import asyncio
-from typing import Optional, Tuple, List
-
-from . import node_service_pb2
-from . import node_service_pb2_grpc
-
-from ..peer_handle import PeerHandle
-from exo.inference.shard import Shard
-from exo.topology.topology import Topology
-from exo.topology.device_capabilities import DeviceCapabilities, DeviceFlops
-from exo.helpers import DEBUG
-import json
-import platform
-
-if platform.system().lower() == "darwin" and platform.machine().lower() == "arm64":
- import mlx.core as mx
-else:
- import numpy as mx
-
-
-class GRPCPeerHandle(PeerHandle):
- def __init__(self, _id: str, address: str, desc: str, device_capabilities: DeviceCapabilities):
- self._id = _id
- self.address = address
- self.desc = desc
- self._device_capabilities = device_capabilities
- self.channel = None
- self.stub = None
- self.channel_options = [
- ("grpc.max_metadata_size", 32 * 1024 * 1024),
- ("grpc.max_receive_message_length", 256 * 1024 * 1024),
- ("grpc.max_send_message_length", 256 * 1024 * 1024),
- ("grpc.max_concurrent_streams", 100),
- ("grpc.http2.min_time_between_pings_ms", 10000),
- ("grpc.keepalive_time_ms", 10000),
- ("grpc.keepalive_timeout_ms", 5000),
- ("grpc.keepalive_permit_without_calls", 1),
- ("grpc.http2.max_pings_without_data", 0),
- ("grpc.http2.min_ping_interval_without_data_ms", 5000),
- ("grpc.tcp_nodelay", 1),
- ("grpc.optimization_target", "throughput"),
- ]
-
- def id(self) -> str:
- return self._id
-
- def addr(self) -> str:
- return self.address
-
- def description(self) -> str:
- return self.desc
-
- def device_capabilities(self) -> DeviceCapabilities:
- return self._device_capabilities
-
- async def connect(self):
- self.channel = grpc.aio.insecure_channel(
- self.address,
- options=self.channel_options,
- compression=grpc.Compression.Gzip
- )
- self.stub = node_service_pb2_grpc.NodeServiceStub(self.channel)
- await asyncio.wait_for(self.channel.channel_ready(), timeout=10.0)
-
- async def is_connected(self) -> bool:
- return self.channel is not None and self.channel.get_state() == grpc.ChannelConnectivity.READY
-
- async def disconnect(self):
- if self.channel:
- await self.channel.close()
- self.channel = None
- self.stub = None
-
- async def _ensure_connected(self):
- if not (await self.is_connected()):
- try:
- await asyncio.wait_for(self.connect(), timeout=10.0)
- except asyncio.TimeoutError:
- if DEBUG >= 2: print(f"Connection timeout for {self._id}@{self.address}")
- await self.disconnect()
- raise
-
- async def health_check(self) -> bool:
- try:
- await self._ensure_connected()
- request = node_service_pb2.HealthCheckRequest()
- response = await asyncio.wait_for(self.stub.HealthCheck(request), timeout=5)
- return response.is_healthy
- except asyncio.TimeoutError:
- return False
- except Exception:
- if DEBUG >= 4:
- print(f"Health check failed for {self._id}@{self.address}.")
- import traceback
- traceback.print_exc()
- return False
-
- async def send_prompt(self, shard: Shard, prompt: str, inference_state: Optional[dict] = None, request_id: Optional[str] = None) -> Optional[np.array]:
- await self._ensure_connected()
- request = node_service_pb2.PromptRequest(
- prompt=prompt,
- shard=node_service_pb2.Shard(
- model_id=shard.model_id,
- start_layer=shard.start_layer,
- end_layer=shard.end_layer,
- n_layers=shard.n_layers,
- ),
- request_id=request_id,
- inference_state=None if inference_state is None else self.serialize_inference_state(inference_state)
- )
- await self.stub.SendPrompt(request)
-
- async def send_tensor(self, shard: Shard, tensor: np.ndarray, inference_state: Optional[dict] = None, request_id: Optional[str] = None) -> Optional[np.array]:
- await self._ensure_connected()
- request = node_service_pb2.TensorRequest(
- shard=node_service_pb2.Shard(
- model_id=shard.model_id,
- start_layer=shard.start_layer,
- end_layer=shard.end_layer,
- n_layers=shard.n_layers,
- ),
- tensor=node_service_pb2.Tensor(tensor_data=tensor.tobytes(), shape=tensor.shape, dtype=str(tensor.dtype)),
- request_id=request_id,
- inference_state=None if inference_state is None else self.serialize_inference_state(inference_state)
- )
- response = await self.stub.SendTensor(request)
-
- if not response.tensor_data or not response.shape or not response.dtype:
- return None
-
- return np.frombuffer(response.tensor_data, dtype=np.dtype(response.dtype)).reshape(response.shape)
-
- async def send_example(self, shard: Shard, example: np.ndarray, target: np.ndarray, length: np.ndarray, train: bool, request_id: Optional[str] = None) -> Optional[np.array]:
- await self._ensure_connected()
- request = node_service_pb2.ExampleRequest(
- shard=node_service_pb2.Shard(
- model_id=shard.model_id,
- start_layer=shard.start_layer,
- end_layer=shard.end_layer,
- n_layers=shard.n_layers,
- ),
- example=node_service_pb2.Tensor(tensor_data=example.tobytes(), shape=example.shape, dtype=str(example.dtype)),
- target=node_service_pb2.Tensor(tensor_data=target.tobytes(), shape=target.shape, dtype=str(target.dtype)),
- length=node_service_pb2.Tensor(tensor_data=length.tobytes(), shape=length.shape, dtype=str(length.dtype)),
- train=train,
- request_id=request_id,
- )
- response = await self.stub.SendExample(request)
- loss = response.loss
- if train and not shard.is_first_layer():
- grads = np.frombuffer(response.grads.tensor_data, dtype=np.dtype(response.grads.dtype)).reshape(response.grads.shape)
- return loss, grads
- else:
- return loss
-
- async def send_loss(self, shard: Shard, tensor: np.ndarray, request_id: Optional[str] = None) -> Optional[np.array]:
- await self._ensure_connected()
- request = node_service_pb2.TensorRequest(
- shard=node_service_pb2.Shard(
- model_id=shard.model_id,
- start_layer=shard.start_layer,
- end_layer=shard.end_layer,
- n_layers=shard.n_layers,
- ),
- tensor=node_service_pb2.Tensor(tensor_data=tensor.tobytes(), shape=tensor.shape, dtype=str(tensor.dtype)),
- request_id=request_id,
- )
- response = await self.stub.SendLoss(request)
-
- if not response.tensor_data or not response.shape or not response.dtype:
- return None
-
- return np.frombuffer(response.tensor_data, dtype=np.dtype(response.dtype)).reshape(response.shape)
-
- async def collect_topology(self, visited: set[str], max_depth: int) -> Topology:
- await self._ensure_connected()
- request = node_service_pb2.CollectTopologyRequest(visited=visited, max_depth=max_depth)
- response = await self.stub.CollectTopology(request)
- topology = Topology()
- for node_id, capabilities in response.nodes.items():
- device_capabilities = DeviceCapabilities(
- model=capabilities.model, chip=capabilities.chip, memory=capabilities.memory, flops=DeviceFlops(fp16=capabilities.flops.fp16, fp32=capabilities.flops.fp32, int8=capabilities.flops.int8)
- )
- topology.update_node(node_id, device_capabilities)
- for node_id, peer_connections in response.peer_graph.items():
- for conn in peer_connections.connections:
- topology.add_edge(node_id, conn.to_id, conn.description)
- return topology
-
- async def send_result(self, request_id: str, result: List[int], is_finished: bool) -> None:
- await self._ensure_connected()
- tensor = None
- if isinstance(result, np.ndarray):
- tensor = node_service_pb2.Tensor(tensor_data=result.tobytes(), shape=result.shape, dtype=str(result.dtype))
- result = []
- request = node_service_pb2.SendResultRequest(request_id=request_id, result=result, tensor=tensor, is_finished=is_finished)
- await self.stub.SendResult(request)
-
- async def send_opaque_status(self, request_id: str, status: str) -> None:
- await self._ensure_connected()
- request = node_service_pb2.SendOpaqueStatusRequest(request_id=request_id, status=status)
- await asyncio.wait_for(self.stub.SendOpaqueStatus(request), timeout=10.0)
-
- def serialize_inference_state(self, inference_state: dict) -> node_service_pb2.InferenceState:
- proto_inference_state = node_service_pb2.InferenceState()
- other_data = {}
- for k, v in inference_state.items():
- if isinstance(v, mx.array):
- np_array = np.array(v)
- tensor_data = node_service_pb2.Tensor(tensor_data=np_array.tobytes(), shape=list(np_array.shape), dtype=str(np_array.dtype))
- proto_inference_state.tensor_data[k].CopyFrom(tensor_data)
- elif isinstance(v, list) and all(isinstance(item, mx.array) for item in v):
- tensor_list = node_service_pb2.TensorList()
- for tensor in v:
- np_array = np.array(tensor)
- tensor_data = node_service_pb2.Tensor(tensor_data=np_array.tobytes(), shape=list(np_array.shape), dtype=str(np_array.dtype))
- tensor_list.tensors.append(tensor_data)
- proto_inference_state.tensor_list_data[k].CopyFrom(tensor_list)
- else:
- # For non-tensor data, we'll still use JSON
- other_data[k] = v
- if other_data:
- proto_inference_state.other_data_json = json.dumps(other_data)
- return proto_inference_state
diff --git a/exo/networking/grpc/grpc_server.py b/exo/networking/grpc/grpc_server.py
deleted file mode 100644
index cbc33f94..00000000
--- a/exo/networking/grpc/grpc_server.py
+++ /dev/null
@@ -1,173 +0,0 @@
-import grpc
-from concurrent import futures
-import numpy as np
-from asyncio import CancelledError
-
-import platform
-
-from . import node_service_pb2
-from . import node_service_pb2_grpc
-from exo import DEBUG
-from exo.inference.shard import Shard
-from exo.orchestration import Node
-import json
-
-if platform.system().lower() == "darwin" and platform.machine().lower() == "arm64":
- import mlx.core as mx
-else:
- import numpy as mx
-
-
-class GRPCServer(node_service_pb2_grpc.NodeServiceServicer):
- def __init__(self, node: Node, host: str, port: int):
- self.node = node
- self.host = host
- self.port = port
- self.server = None
-
- async def start(self) -> None:
- self.server = grpc.aio.server(
- futures.ThreadPoolExecutor(max_workers=32),
- options=[
- ("grpc.max_metadata_size", 32*1024*1024),
- ("grpc.max_send_message_length", 256*1024*1024),
- ("grpc.max_receive_message_length", 256*1024*1024),
- ("grpc.keepalive_time_ms", 10000),
- ("grpc.keepalive_timeout_ms", 5000),
- ("grpc.http2.max_pings_without_data", 0),
- ("grpc.http2.min_time_between_pings_ms", 10000),
- ("grpc.http2.min_ping_interval_without_data_ms", 5000),
- ("grpc.max_concurrent_streams", 100),
- ("grpc.tcp_nodelay", 1),
- ("grpc.optimization_target", "throughput"),
- ("grpc.keepalive_permit_without_calls", 1),
- ("grpc.http2.max_concurrent_streams", 0), # Unlimited concurrent streams
- ],
- )
- node_service_pb2_grpc.add_NodeServiceServicer_to_server(self, self.server)
- listen_addr = f"{self.host}:{self.port}"
- self.server.add_insecure_port(listen_addr)
- await self.server.start()
- if DEBUG >= 1: print(f"Server started, listening on {listen_addr}")
-
- async def stop(self) -> None:
- if self.server:
- try:
- await self.server.stop(grace=5)
- await self.server.wait_for_termination()
- except CancelledError:
- pass
- if DEBUG >= 1: print("Server stopped and all connections are closed")
-
- async def SendPrompt(self, request, context):
- shard = Shard(
- model_id=request.shard.model_id,
- start_layer=request.shard.start_layer,
- end_layer=request.shard.end_layer,
- n_layers=request.shard.n_layers,
- )
- prompt = request.prompt
- request_id = request.request_id
- inference_state = None if request.inference_state is None else self.deserialize_inference_state(request.inference_state)
- result = await self.node.process_prompt(shard, prompt, request_id, inference_state)
- if DEBUG >= 5: print(f"SendPrompt {shard=} {prompt=} {request_id=} result: {result}")
- tensor_data = result.tobytes() if result is not None else None
- return node_service_pb2.Tensor(tensor_data=tensor_data, shape=result.shape, dtype=str(result.dtype)) if result is not None else node_service_pb2.Tensor()
-
- async def SendTensor(self, request, context):
- shard = Shard(
- model_id=request.shard.model_id,
- start_layer=request.shard.start_layer,
- end_layer=request.shard.end_layer,
- n_layers=request.shard.n_layers,
- )
- tensor = np.frombuffer(request.tensor.tensor_data, dtype=np.dtype(request.tensor.dtype)).reshape(request.tensor.shape)
- request_id = request.request_id
-
- inference_state = None if request.inference_state is None else self.deserialize_inference_state(request.inference_state)
-
- result = await self.node.process_tensor(shard, tensor, request_id, inference_state)
- if DEBUG >= 5: print(f"SendTensor tensor {shard=} {tensor=} {request_id=} result: {result}")
- tensor_data = result.tobytes() if result is not None else None
- return node_service_pb2.Tensor(tensor_data=tensor_data, shape=result.shape, dtype=str(result.dtype)) if result is not None else node_service_pb2.Tensor()
-
- async def SendExample(self, request, context):
- shard = Shard(
- model_id=request.shard.model_id,
- start_layer=request.shard.start_layer,
- end_layer=request.shard.end_layer,
- n_layers=request.shard.n_layers,
- )
- example = np.frombuffer(request.example.tensor_data, dtype=np.dtype(request.example.dtype)).reshape(request.example.shape)
- target = np.frombuffer(request.target.tensor_data, dtype=np.dtype(request.target.dtype)).reshape(request.target.shape)
- length = np.frombuffer(request.length.tensor_data, dtype=np.dtype(request.length.dtype)).reshape(request.length.shape)
- train = request.train
- request_id = request.request_id
-
- if train and not shard.is_first_layer():
- loss, grad = await self.node.process_example(shard, example, target, length, train, request_id)
- tensor_data = grad.tobytes()
- grad_tensor = node_service_pb2.Tensor(tensor_data=tensor_data, shape=grad.shape, dtype=str(grad.dtype))
- return node_service_pb2.Loss(loss=loss, grads=grad_tensor)
- else:
- loss = await self.node.process_example(shard, example, target, length, train, request_id)
- return node_service_pb2.Loss(loss=loss, grads=None)
-
- async def CollectTopology(self, request, context):
- max_depth = request.max_depth
- visited = set(request.visited)
- topology = self.node.current_topology
- nodes = {
- node_id:
- node_service_pb2.DeviceCapabilities(
- model=cap.model,
- chip=cap.chip,
- memory=cap.memory,
- flops=node_service_pb2.DeviceFlops(fp32=cap.flops.fp32, fp16=cap.flops.fp16, int8=cap.flops.int8),
- )
- for node_id, cap in topology.nodes.items()
- }
- peer_graph = {
- node_id: node_service_pb2.PeerConnections(connections=[node_service_pb2.PeerConnection(to_id=conn.to_id, description=conn.description) for conn in connections])
- for node_id, connections in topology.peer_graph.items()
- }
- if DEBUG >= 5: print(f"CollectTopology {max_depth=} {visited=} {nodes=} {peer_graph=}")
- return node_service_pb2.Topology(nodes=nodes, peer_graph=peer_graph)
-
- async def SendResult(self, request, context):
- request_id = request.request_id
- result = request.result
- is_finished = request.is_finished
- img = request.tensor
- if DEBUG >= 5: print(f"Received SendResult request: {request_id=} {result=} {is_finished=}")
- result = list(result)
- if len(img.tensor_data) > 0:
- result = np.frombuffer(img.tensor_data, dtype=np.dtype(img.dtype)).reshape(img.shape)
- self.node.on_token.trigger_all(request_id, result, is_finished)
- return node_service_pb2.Empty()
-
- async def SendOpaqueStatus(self, request, context):
- request_id = request.request_id
- status = request.status
- if DEBUG >= 8: print(f"Received SendOpaqueStatus request: {request_id=} {status=}")
- self.node.on_opaque_status.trigger_all(request_id, status)
- return node_service_pb2.Empty()
-
- async def HealthCheck(self, request, context):
- return node_service_pb2.HealthCheckResponse(is_healthy=True)
-
- def deserialize_inference_state(self, inference_state_proto: node_service_pb2.InferenceState) -> dict:
- inference_state = {}
-
- for k, tensor_data in inference_state_proto.tensor_data.items():
- np_array = np.frombuffer(tensor_data.tensor_data, dtype=tensor_data.dtype).reshape(tensor_data.shape)
- inference_state[k] = mx.array(np_array)
-
- for k, tensor_list in inference_state_proto.tensor_list_data.items():
- inference_state[k] = [mx.array(np.frombuffer(tensor.tensor_data, dtype=tensor.dtype).reshape(tensor.shape)) for tensor in tensor_list.tensors]
-
- if inference_state_proto.other_data_json:
- other_data = json.loads(inference_state_proto.other_data_json)
- inference_state.update(other_data)
-
- return inference_state
diff --git a/exo/networking/grpc/node_service.proto b/exo/networking/grpc/node_service.proto
deleted file mode 100644
index 882a5247..00000000
--- a/exo/networking/grpc/node_service.proto
+++ /dev/null
@@ -1,116 +0,0 @@
-syntax = "proto3";
-
-package node_service;
-
-service NodeService {
- rpc SendPrompt (PromptRequest) returns (Tensor) {}
- rpc SendTensor (TensorRequest) returns (Tensor) {}
- rpc SendExample (ExampleRequest) returns (Loss) {}
- rpc CollectTopology (CollectTopologyRequest) returns (Topology) {}
- rpc SendResult (SendResultRequest) returns (Empty) {}
- rpc SendOpaqueStatus (SendOpaqueStatusRequest) returns (Empty) {}
- rpc HealthCheck (HealthCheckRequest) returns (HealthCheckResponse) {}
-}
-
-message Shard {
- string model_id = 1;
- int32 start_layer = 2;
- int32 end_layer = 3;
- int32 n_layers = 4;
-}
-
-message PromptRequest {
- Shard shard = 1;
- string prompt = 2;
- optional string request_id = 3;
- optional InferenceState inference_state = 4;
-}
-
-message TensorRequest {
- Shard shard = 1;
- Tensor tensor = 2;
- optional string request_id = 3;
- optional InferenceState inference_state = 4;
-}
-
-message ExampleRequest {
- Shard shard = 1;
- Tensor example = 2;
- Tensor target = 3;
- Tensor length = 4;
- bool train = 5;
- optional string request_id = 6;
-}
-
-message Loss {
- float loss = 1;
- optional Tensor grads = 2;
-}
-
-message Tensor {
- bytes tensor_data = 1;
- repeated int32 shape = 2;
- string dtype = 3;
-}
-
-message TensorList {
- repeated Tensor tensors = 1;
-}
-
-message InferenceState {
- map<string, Tensor> tensor_data = 1;
- map<string, TensorList> tensor_list_data = 2;
- string other_data_json = 3;
-}
-
-message CollectTopologyRequest {
- repeated string visited = 1;
- int32 max_depth = 2;
-}
-
-message Topology {
- map<string, DeviceCapabilities> nodes = 1;
- map<string, PeerConnections> peer_graph = 2;
-}
-
-message PeerConnection {
- string to_id = 1;
- optional string description = 2;
-}
-
-message PeerConnections {
- repeated PeerConnection connections = 1;
-}
-
-message DeviceFlops {
- double fp32 = 1;
- double fp16 = 2;
- double int8 = 3;
-}
-
-message DeviceCapabilities {
- string model = 1;
- string chip = 2;
- int32 memory = 3;
- DeviceFlops flops = 4;
-}
-
-message SendResultRequest {
- string request_id = 1;
- repeated int32 result = 2;
- optional Tensor tensor = 3;
- bool is_finished = 4;
-}
-
-message SendOpaqueStatusRequest {
- string request_id = 1;
- string status = 2;
-}
-
-message HealthCheckRequest {}
-
-message HealthCheckResponse {
- bool is_healthy = 1;
-}
-
-message Empty {}
diff --git a/exo/networking/grpc/node_service_pb2.py b/exo/networking/grpc/node_service_pb2.py
deleted file mode 100644
index 9a83380d..00000000
--- a/exo/networking/grpc/node_service_pb2.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by the protocol buffer compiler. DO NOT EDIT!
-# NO CHECKED-IN PROTOBUF GENCODE
-# source: node_service.proto
-# Protobuf Python Version: 5.27.2
-"""Generated protocol buffer code."""
-from google.protobuf import descriptor as _descriptor
-from google.protobuf import descriptor_pool as _descriptor_pool
-from google.protobuf import runtime_version as _runtime_version
-from google.protobuf import symbol_database as _symbol_database
-from google.protobuf.internal import builder as _builder
-_runtime_version.ValidateProtobufRuntimeVersion(
- _runtime_version.Domain.PUBLIC,
- 5,
- 27,
- 2,
- '',
- 'node_service.proto'
-)
-# @@protoc_insertion_point(imports)
-
-_sym_db = _symbol_database.Default()
-
-
-
-
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12node_service.proto\x12\x0cnode_service\"S\n\x05Shard\x12\x10\n\x08model_id\x18\x01 \x01(\t\x12\x13\n\x0bstart_layer\x18\x02 \x01(\x05\x12\x11\n\tend_layer\x18\x03 \x01(\x05\x12\x10\n\x08n_layers\x18\x04 \x01(\x05\"\xbb\x01\n\rPromptRequest\x12\"\n\x05shard\x18\x01 \x01(\x0b\x32\x13.node_service.Shard\x12\x0e\n\x06prompt\x18\x02 \x01(\t\x12\x17\n\nrequest_id\x18\x03 \x01(\tH\x00\x88\x01\x01\x12:\n\x0finference_state\x18\x04 \x01(\x0b\x32\x1c.node_service.InferenceStateH\x01\x88\x01\x01\x42\r\n\x0b_request_idB\x12\n\x10_inference_state\"\xd1\x01\n\rTensorRequest\x12\"\n\x05shard\x18\x01 \x01(\x0b\x32\x13.node_service.Shard\x12$\n\x06tensor\x18\x02 \x01(\x0b\x32\x14.node_service.Tensor\x12\x17\n\nrequest_id\x18\x03 \x01(\tH\x00\x88\x01\x01\x12:\n\x0finference_state\x18\x04 \x01(\x0b\x32\x1c.node_service.InferenceStateH\x01\x88\x01\x01\x42\r\n\x0b_request_idB\x12\n\x10_inference_state\"\xde\x01\n\x0e\x45xampleRequest\x12\"\n\x05shard\x18\x01 \x01(\x0b\x32\x13.node_service.Shard\x12%\n\x07\x65xample\x18\x02 \x01(\x0b\x32\x14.node_service.Tensor\x12$\n\x06target\x18\x03 \x01(\x0b\x32\x14.node_service.Tensor\x12$\n\x06length\x18\x04 \x01(\x0b\x32\x14.node_service.Tensor\x12\r\n\x05train\x18\x05 \x01(\x08\x12\x17\n\nrequest_id\x18\x06 \x01(\tH\x00\x88\x01\x01\x42\r\n\x0b_request_id\"H\n\x04Loss\x12\x0c\n\x04loss\x18\x01 \x01(\x02\x12(\n\x05grads\x18\x02 \x01(\x0b\x32\x14.node_service.TensorH\x00\x88\x01\x01\x42\x08\n\x06_grads\";\n\x06Tensor\x12\x13\n\x0btensor_data\x18\x01 \x01(\x0c\x12\r\n\x05shape\x18\x02 \x03(\x05\x12\r\n\x05\x64type\x18\x03 \x01(\t\"3\n\nTensorList\x12%\n\x07tensors\x18\x01 \x03(\x0b\x32\x14.node_service.Tensor\"\xd2\x02\n\x0eInferenceState\x12\x41\n\x0btensor_data\x18\x01 \x03(\x0b\x32,.node_service.InferenceState.TensorDataEntry\x12J\n\x10tensor_list_data\x18\x02 \x03(\x0b\x32\x30.node_service.InferenceState.TensorListDataEntry\x12\x17\n\x0fother_data_json\x18\x03 \x01(\t\x1aG\n\x0fTensorDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.node_service.Tensor:\x02\x38\x01\x1aO\n\x13TensorListDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\'\n\x05value\x18\x02 \x01(\x0b\x32\x18.node_service.TensorList:\x02\x38\x01\"<\n\x16\x43ollectTopologyRequest\x12\x0f\n\x07visited\x18\x01 \x03(\t\x12\x11\n\tmax_depth\x18\x02 \x01(\x05\"\x98\x02\n\x08Topology\x12\x30\n\x05nodes\x18\x01 \x03(\x0b\x32!.node_service.Topology.NodesEntry\x12\x39\n\npeer_graph\x18\x02 \x03(\x0b\x32%.node_service.Topology.PeerGraphEntry\x1aN\n\nNodesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12/\n\x05value\x18\x02 \x01(\x0b\x32 .node_service.DeviceCapabilities:\x02\x38\x01\x1aO\n\x0ePeerGraphEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x1d.node_service.PeerConnections:\x02\x38\x01\"I\n\x0ePeerConnection\x12\r\n\x05to_id\x18\x01 \x01(\t\x12\x18\n\x0b\x64\x65scription\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_description\"D\n\x0fPeerConnections\x12\x31\n\x0b\x63onnections\x18\x01 \x03(\x0b\x32\x1c.node_service.PeerConnection\"7\n\x0b\x44\x65viceFlops\x12\x0c\n\x04\x66p32\x18\x01 \x01(\x01\x12\x0c\n\x04\x66p16\x18\x02 \x01(\x01\x12\x0c\n\x04int8\x18\x03 \x01(\x01\"k\n\x12\x44\x65viceCapabilities\x12\r\n\x05model\x18\x01 \x01(\t\x12\x0c\n\x04\x63hip\x18\x02 \x01(\t\x12\x0e\n\x06memory\x18\x03 \x01(\x05\x12(\n\x05\x66lops\x18\x04 \x01(\x0b\x32\x19.node_service.DeviceFlops\"\x82\x01\n\x11SendResultRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x03(\x05\x12)\n\x06tensor\x18\x03 \x01(\x0b\x32\x14.node_service.TensorH\x00\x88\x01\x01\x12\x13\n\x0bis_finished\x18\x04 \x01(\x08\x42\t\n\x07_tensor\"=\n\x17SendOpaqueStatusRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\"\x14\n\x12HealthCheckRequest\")\n\x13HealthCheckResponse\x12\x12\n\nis_healthy\x18\x01 \x01(\x08\"\x07\n\x05\x45mpty2\x97\x04\n\x0bNodeService\x12\x41\n\nSendPrompt\x12\x1b.node_service.PromptRequest\x1a\x14.node_service.Tensor\"\x00\x12\x41\n\nSendTensor\x12\x1b.node_service.TensorRequest\x1a\x14.node_service.Tensor\"\x00\x12\x41\n\x0bSendExample\x12\x1c.node_service.ExampleRequest\x1a\x12.node_service.Loss\"\x00\x12Q\n\x0f\x43ollectTopology\x12$.node_service.CollectTopologyRequest\x1a\x16.node_service.Topology\"\x00\x12\x44\n\nSendResult\x12\x1f.node_service.SendResultRequest\x1a\x13.node_service.Empty\"\x00\x12P\n\x10SendOpaqueStatus\x12%.node_service.SendOpaqueStatusRequest\x1a\x13.node_service.Empty\"\x00\x12T\n\x0bHealthCheck\x12 .node_service.HealthCheckRequest\x1a!.node_service.HealthCheckResponse\"\x00\x62\x06proto3')
-
-_globals = globals()
-_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
-_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'node_service_pb2', _globals)
-if not _descriptor._USE_C_DESCRIPTORS:
- DESCRIPTOR._loaded_options = None
- _globals['_INFERENCESTATE_TENSORDATAENTRY']._loaded_options = None
- _globals['_INFERENCESTATE_TENSORDATAENTRY']._serialized_options = b'8\001'
- _globals['_INFERENCESTATE_TENSORLISTDATAENTRY']._loaded_options = None
- _globals['_INFERENCESTATE_TENSORLISTDATAENTRY']._serialized_options = b'8\001'
- _globals['_TOPOLOGY_NODESENTRY']._loaded_options = None
- _globals['_TOPOLOGY_NODESENTRY']._serialized_options = b'8\001'
- _globals['_TOPOLOGY_PEERGRAPHENTRY']._loaded_options = None
- _globals['_TOPOLOGY_PEERGRAPHENTRY']._serialized_options = b'8\001'
- _globals['_SHARD']._serialized_start=36
- _globals['_SHARD']._serialized_end=119
- _globals['_PROMPTREQUEST']._serialized_start=122
- _globals['_PROMPTREQUEST']._serialized_end=309
- _globals['_TENSORREQUEST']._serialized_start=312
- _globals['_TENSORREQUEST']._serialized_end=521
- _globals['_EXAMPLEREQUEST']._serialized_start=524
- _globals['_EXAMPLEREQUEST']._serialized_end=746
- _globals['_LOSS']._serialized_start=748
- _globals['_LOSS']._serialized_end=820
- _globals['_TENSOR']._serialized_start=822
- _globals['_TENSOR']._serialized_end=881
- _globals['_TENSORLIST']._serialized_start=883
- _globals['_TENSORLIST']._serialized_end=934
- _globals['_INFERENCESTATE']._serialized_start=937
- _globals['_INFERENCESTATE']._serialized_end=1275
- _globals['_INFERENCESTATE_TENSORDATAENTRY']._serialized_start=1123
- _globals['_INFERENCESTATE_TENSORDATAENTRY']._serialized_end=1194
- _globals['_INFERENCESTATE_TENSORLISTDATAENTRY']._serialized_start=1196
- _globals['_INFERENCESTATE_TENSORLISTDATAENTRY']._serialized_end=1275
- _globals['_COLLECTTOPOLOGYREQUEST']._serialized_start=1277
- _globals['_COLLECTTOPOLOGYREQUEST']._serialized_end=1337
- _globals['_TOPOLOGY']._serialized_start=1340
- _globals['_TOPOLOGY']._serialized_end=1620
- _globals['_TOPOLOGY_NODESENTRY']._serialized_start=1461
- _globals['_TOPOLOGY_NODESENTRY']._serialized_end=1539
- _globals['_TOPOLOGY_PEERGRAPHENTRY']._serialized_start=1541
- _globals['_TOPOLOGY_PEERGRAPHENTRY']._serialized_end=1620
- _globals['_PEERCONNECTION']._serialized_start=1622
- _globals['_PEERCONNECTION']._serialized_end=1695
- _globals['_PEERCONNECTIONS']._serialized_start=1697
- _globals['_PEERCONNECTIONS']._serialized_end=1765
- _globals['_DEVICEFLOPS']._serialized_start=1767
- _globals['_DEVICEFLOPS']._serialized_end=1822
- _globals['_DEVICECAPABILITIES']._serialized_start=1824
- _globals['_DEVICECAPABILITIES']._serialized_end=1931
- _globals['_SENDRESULTREQUEST']._serialized_start=1934
- _globals['_SENDRESULTREQUEST']._serialized_end=2064
- _globals['_SENDOPAQUESTATUSREQUEST']._serialized_start=2066
- _globals['_SENDOPAQUESTATUSREQUEST']._serialized_end=2127
- _globals['_HEALTHCHECKREQUEST']._serialized_start=2129
- _globals['_HEALTHCHECKREQUEST']._serialized_end=2149
- _globals['_HEALTHCHECKRESPONSE']._serialized_start=2151
- _globals['_HEALTHCHECKRESPONSE']._serialized_end=2192
- _globals['_EMPTY']._serialized_start=2194
- _globals['_EMPTY']._serialized_end=2201
- _globals['_NODESERVICE']._serialized_start=2204
- _globals['_NODESERVICE']._serialized_end=2739
-# @@protoc_insertion_point(module_scope)
diff --git a/exo/networking/grpc/node_service_pb2_grpc.py b/exo/networking/grpc/node_service_pb2_grpc.py
deleted file mode 100644
index ac12e2f7..00000000
--- a/exo/networking/grpc/node_service_pb2_grpc.py
+++ /dev/null
@@ -1,355 +0,0 @@
-# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
-"""Client and server classes corresponding to protobuf-defined services."""
-import grpc
-import warnings
-
-from . import node_service_pb2 as node__service__pb2
-
-GRPC_GENERATED_VERSION = '1.67.0'
-GRPC_VERSION = grpc.__version__
-_version_not_supported = False
-
-try:
- from grpc._utilities import first_version_is_lower
- _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
-except ImportError:
- _version_not_supported = True
-
-if _version_not_supported:
- raise RuntimeError(
- f'The grpc package installed is at version {GRPC_VERSION},'
- + f' but the generated code in node_service_pb2_grpc.py depends on'
- + f' grpcio>={GRPC_GENERATED_VERSION}.'
- + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
- + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
- )
-
-
-class NodeServiceStub(object):
- """Missing associated documentation comment in .proto file."""
-
- def __init__(self, channel):
- """Constructor.
-
- Args:
- channel: A grpc.Channel.
- """
- self.SendPrompt = channel.unary_unary(
- '/node_service.NodeService/SendPrompt',
- request_serializer=node__service__pb2.PromptRequest.SerializeToString,
- response_deserializer=node__service__pb2.Tensor.FromString,
- _registered_method=True)
- self.SendTensor = channel.unary_unary(
- '/node_service.NodeService/SendTensor',
- request_serializer=node__service__pb2.TensorRequest.SerializeToString,
- response_deserializer=node__service__pb2.Tensor.FromString,
- _registered_method=True)
- self.SendExample = channel.unary_unary(
- '/node_service.NodeService/SendExample',
- request_serializer=node__service__pb2.ExampleRequest.SerializeToString,
- response_deserializer=node__service__pb2.Loss.FromString,
- _registered_method=True)
- self.CollectTopology = channel.unary_unary(
- '/node_service.NodeService/CollectTopology',
- request_serializer=node__service__pb2.CollectTopologyRequest.SerializeToString,
- response_deserializer=node__service__pb2.Topology.FromString,
- _registered_method=True)
- self.SendResult = channel.unary_unary(
- '/node_service.NodeService/SendResult',
- request_serializer=node__service__pb2.SendResultRequest.SerializeToString,
- response_deserializer=node__service__pb2.Empty.FromString,
- _registered_method=True)
- self.SendOpaqueStatus = channel.unary_unary(
- '/node_service.NodeService/SendOpaqueStatus',
- request_serializer=node__service__pb2.SendOpaqueStatusRequest.SerializeToString,
- response_deserializer=node__service__pb2.Empty.FromString,
- _registered_method=True)
- self.HealthCheck = channel.unary_unary(
- '/node_service.NodeService/HealthCheck',
- request_serializer=node__service__pb2.HealthCheckRequest.SerializeToString,
- response_deserializer=node__service__pb2.HealthCheckResponse.FromString,
- _registered_method=True)
-
-
-class NodeServiceServicer(object):
- """Missing associated documentation comment in .proto file."""
-
- def SendPrompt(self, request, context):
- """Missing associated documentation comment in .proto file."""
- context.set_code(grpc.StatusCode.UNIMPLEMENTED)
- context.set_details('Method not implemented!')
- raise NotImplementedError('Method not implemented!')
-
- def SendTensor(self, request, context):
- """Missing associated documentation comment in .proto file."""
- context.set_code(grpc.StatusCode.UNIMPLEMENTED)
- context.set_details('Method not implemented!')
- raise NotImplementedError('Method not implemented!')
-
- def SendExample(self, request, context):
- """Missing associated documentation comment in .proto file."""
- context.set_code(grpc.StatusCode.UNIMPLEMENTED)
- context.set_details('Method not implemented!')
- raise NotImplementedError('Method not implemented!')
-
- def CollectTopology(self, request, context):
- """Missing associated documentation comment in .proto file."""
- context.set_code(grpc.StatusCode.UNIMPLEMENTED)
- context.set_details('Method not implemented!')
- raise NotImplementedError('Method not implemented!')
-
- def SendResult(self, request, context):
- """Missing associated documentation comment in .proto file."""
- context.set_code(grpc.StatusCode.UNIMPLEMENTED)
- context.set_details('Method not implemented!')
- raise NotImplementedError('Method not implemented!')
-
- def SendOpaqueStatus(self, request, context):
- """Missing associated documentation comment in .proto file."""
- context.set_code(grpc.StatusCode.UNIMPLEMENTED)
- context.set_details('Method not implemented!')
- raise NotImplementedError('Method not implemented!')
-
- def HealthCheck(self, request, context):
- """Missing associated documentation comment in .proto file."""
- context.set_code(grpc.StatusCode.UNIMPLEMENTED)
- context.set_details('Method not implemented!')
- raise NotImplementedError('Method not implemented!')
-
-
-def add_NodeServiceServicer_to_server(servicer, server):
- rpc_method_handlers = {
- 'SendPrompt': grpc.unary_unary_rpc_method_handler(
- servicer.SendPrompt,
- request_deserializer=node__service__pb2.PromptRequest.FromString,
- response_serializer=node__service__pb2.Tensor.SerializeToString,
- ),
- 'SendTensor': grpc.unary_unary_rpc_method_handler(
- servicer.SendTensor,
- request_deserializer=node__service__pb2.TensorRequest.FromString,
- response_serializer=node__service__pb2.Tensor.SerializeToString,
- ),
- 'SendExample': grpc.unary_unary_rpc_method_handler(
- servicer.SendExample,
- request_deserializer=node__service__pb2.ExampleRequest.FromString,
- response_serializer=node__service__pb2.Loss.SerializeToString,
- ),
- 'CollectTopology': grpc.unary_unary_rpc_method_handler(
- servicer.CollectTopology,
- request_deserializer=node__service__pb2.CollectTopologyRequest.FromString,
- response_serializer=node__service__pb2.Topology.SerializeToString,
- ),
- 'SendResult': grpc.unary_unary_rpc_method_handler(
- servicer.SendResult,
- request_deserializer=node__service__pb2.SendResultRequest.FromString,
- response_serializer=node__service__pb2.Empty.SerializeToString,
- ),
- 'SendOpaqueStatus': grpc.unary_unary_rpc_method_handler(
- servicer.SendOpaqueStatus,
- request_deserializer=node__service__pb2.SendOpaqueStatusRequest.FromString,
- response_serializer=node__service__pb2.Empty.SerializeToString,
- ),
- 'HealthCheck': grpc.unary_unary_rpc_method_handler(
- servicer.HealthCheck,
- request_deserializer=node__service__pb2.HealthCheckRequest.FromString,
- response_serializer=node__service__pb2.HealthCheckResponse.SerializeToString,
- ),
- }
- generic_handler = grpc.method_handlers_generic_handler(
- 'node_service.NodeService', rpc_method_handlers)
- server.add_generic_rpc_handlers((generic_handler,))
- server.add_registered_method_handlers('node_service.NodeService', rpc_method_handlers)
-
-
- # This class is part of an EXPERIMENTAL API.
-class NodeService(object):
- """Missing associated documentation comment in .proto file."""
-
- @staticmethod
- def SendPrompt(request,
- target,
- options=(),
- channel_credentials=None,
- call_credentials=None,
- insecure=False,
- compression=None,
- wait_for_ready=None,
- timeout=None,
- metadata=None):
- return grpc.experimental.unary_unary(
- request,
- target,
- '/node_service.NodeService/SendPrompt',
- node__service__pb2.PromptRequest.SerializeToString,
- node__service__pb2.Tensor.FromString,
- options,
- channel_credentials,
- insecure,
- call_credentials,
- compression,
- wait_for_ready,
- timeout,
- metadata,
- _registered_method=True)
-
- @staticmethod
- def SendTensor(request,
- target,
- options=(),
- channel_credentials=None,
- call_credentials=None,
- insecure=False,
- compression=None,
- wait_for_ready=None,
- timeout=None,
- metadata=None):
- return grpc.experimental.unary_unary(
- request,
- target,
- '/node_service.NodeService/SendTensor',
- node__service__pb2.TensorRequest.SerializeToString,
- node__service__pb2.Tensor.FromString,
- options,
- channel_credentials,
- insecure,
- call_credentials,
- compression,
- wait_for_ready,
- timeout,
- metadata,
- _registered_method=True)
-
- @staticmethod
- def SendExample(request,
- target,
- options=(),
- channel_credentials=None,
- call_credentials=None,
- insecure=False,
- compression=None,
- wait_for_ready=None,
- timeout=None,
- metadata=None):
- return grpc.experimental.unary_unary(
- request,
- target,
- '/node_service.NodeService/SendExample',
- node__service__pb2.ExampleRequest.SerializeToString,
- node__service__pb2.Loss.FromString,
- options,
- channel_credentials,
- insecure,
- call_credentials,
- compression,
- wait_for_ready,
- timeout,
- metadata,
- _registered_method=True)
-
- @staticmethod
- def CollectTopology(request,
- target,
- options=(),
- channel_credentials=None,
- call_credentials=None,
- insecure=False,
- compression=None,
- wait_for_ready=None,
- timeout=None,
- metadata=None):
- return grpc.experimental.unary_unary(
- request,
- target,
- '/node_service.NodeService/CollectTopology',
- node__service__pb2.CollectTopologyRequest.SerializeToString,
- node__service__pb2.Topology.FromString,
- options,
- channel_credentials,
- insecure,
- call_credentials,
- compression,
- wait_for_ready,
- timeout,
- metadata,
- _registered_method=True)
-
- @staticmethod
- def SendResult(request,
- target,
- options=(),
- channel_credentials=None,
- call_credentials=None,
- insecure=False,
- compression=None,
- wait_for_ready=None,
- timeout=None,
- metadata=None):
- return grpc.experimental.unary_unary(
- request,
- target,
- '/node_service.NodeService/SendResult',
- node__service__pb2.SendResultRequest.SerializeToString,
- node__service__pb2.Empty.FromString,
- options,
- channel_credentials,
- insecure,
- call_credentials,
- compression,
- wait_for_ready,
- timeout,
- metadata,
- _registered_method=True)
-
- @staticmethod
- def SendOpaqueStatus(request,
- target,
- options=(),
- channel_credentials=None,
- call_credentials=None,
- insecure=False,
- compression=None,
- wait_for_ready=None,
- timeout=None,
- metadata=None):
- return grpc.experimental.unary_unary(
- request,
- target,
- '/node_service.NodeService/SendOpaqueStatus',
- node__service__pb2.SendOpaqueStatusRequest.SerializeToString,
- node__service__pb2.Empty.FromString,
- options,
- channel_credentials,
- insecure,
- call_credentials,
- compression,
- wait_for_ready,
- timeout,
- metadata,
- _registered_method=True)
-
- @staticmethod
- def HealthCheck(request,
- target,
- options=(),
- channel_credentials=None,
- call_credentials=None,
- insecure=False,
- compression=None,
- wait_for_ready=None,
- timeout=None,
- metadata=None):
- return grpc.experimental.unary_unary(
- request,
- target,
- '/node_service.NodeService/HealthCheck',
- node__service__pb2.HealthCheckRequest.SerializeToString,
- node__service__pb2.HealthCheckResponse.FromString,
- options,
- channel_credentials,
- insecure,
- call_credentials,
- compression,
- wait_for_ready,
- timeout,
- metadata,
- _registered_method=True)
diff --git a/exo/networking/manual/__init__.py b/exo/networking/manual/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/networking/manual/manual_discovery.py b/exo/networking/manual/manual_discovery.py
deleted file mode 100644
index 041eafa6..00000000
--- a/exo/networking/manual/manual_discovery.py
+++ /dev/null
@@ -1,101 +0,0 @@
-import os
-import asyncio
-from typing import Dict, List, Callable, Optional
-from concurrent.futures import ThreadPoolExecutor
-
-from exo.networking.discovery import Discovery
-from exo.topology.device_capabilities import DeviceCapabilities
-from exo.networking.manual.network_topology_config import NetworkTopology, PeerConfig
-from exo.helpers import DEBUG_DISCOVERY
-from exo.networking.peer_handle import PeerHandle
-
-
-class ManualDiscovery(Discovery):
- def __init__(
- self,
- network_config_path: str,
- node_id: str,
- create_peer_handle: Callable[[str, str, str, DeviceCapabilities], PeerHandle],
- ):
- self.network_config_path = network_config_path
- self.node_id = node_id
- self.create_peer_handle = create_peer_handle
-
- self.listen_task = None
- self.known_peers: Dict[str, PeerHandle] = {}
-
- self._cached_peers: Dict[str, PeerConfig] = {}
- self._last_modified_time: Optional[float] = None
- self._file_executor = ThreadPoolExecutor(max_workers=1)
-
- async def start(self) -> None:
- self.listen_task = asyncio.create_task(self.task_find_peers_from_config())
-
- async def stop(self) -> None:
- if self.listen_task: self.listen_task.cancel()
- self._file_executor.shutdown(wait=True)
-
- async def discover_peers(self, wait_for_peers: int = 0) -> List[PeerHandle]:
- if wait_for_peers > 0:
- while len(self.known_peers) < wait_for_peers:
- if DEBUG_DISCOVERY >= 2: print(f"Current peers: {len(self.known_peers)}/{wait_for_peers}. Waiting for more peers...")
- await asyncio.sleep(0.1)
- if DEBUG_DISCOVERY >= 2: print(f"Discovered peers: {[peer.id() for peer in self.known_peers.values()]}")
- return list(self.known_peers.values())
-
- async def task_find_peers_from_config(self):
- if DEBUG_DISCOVERY >= 2: print("Starting task to find peers from config...")
- while True:
- peers_from_config = await self._get_peers()
- new_known_peers = {}
- for peer_id, peer_config in peers_from_config.items():
- try:
- if DEBUG_DISCOVERY >= 2: print(f"Checking peer {peer_id=} at {peer_config.address}:{peer_config.port}")
- peer = self.known_peers.get(peer_id)
- if not peer:
- if DEBUG_DISCOVERY >= 2: print(f"{peer_id=} not found in known peers. Adding.")
- peer = self.create_peer_handle(peer_id, f"{peer_config.address}:{peer_config.port}", "MAN", peer_config.device_capabilities)
- is_healthy = await peer.health_check()
- if is_healthy:
- if DEBUG_DISCOVERY >= 2: print(f"{peer_id=} at {peer_config.address}:{peer_config.port} is healthy.")
- new_known_peers[peer_id] = peer
- elif DEBUG_DISCOVERY >= 2:
- print(f"{peer_id=} at {peer_config.address}:{peer_config.port} is not healthy. Removing.")
- except Exception as e:
- if DEBUG_DISCOVERY >= 2: print(f"Exception occurred when attempting to add {peer_id=}: {e}")
- self.known_peers = new_known_peers
- await asyncio.sleep(5.0)
-
- if DEBUG_DISCOVERY >= 2: print(f"Current known peers: {[peer.id() for peer in self.known_peers.values()]}")
-
- async def _get_peers(self):
- try:
- loop = asyncio.get_running_loop()
- current_mtime = await loop.run_in_executor(self._file_executor, os.path.getmtime, self.network_config_path)
-
- if (self._cached_peers is not None and self._last_modified_time is not None and current_mtime <= self._last_modified_time):
- return self._cached_peers
-
- topology = await loop.run_in_executor(self._file_executor, NetworkTopology.from_path, self.network_config_path)
-
- if self.node_id not in topology.peers:
- raise ValueError(
- f"Node ID {self.node_id} not found in network config file "
- f"{self.network_config_path}. Please run with `node_id` set to "
- f"one of the keys in the config file: {[k for k, _ in topology.peers]}"
- )
-
- peers_in_network = topology.peers
- peers_in_network.pop(self.node_id)
-
- self._cached_peers = peers_in_network
- self._last_modified_time = current_mtime
-
- return peers_in_network
-
- except Exception as e:
- if DEBUG_DISCOVERY >= 2:
- print(f"Error when loading network config file from {self.network_config_path}. "
- f"Please update the config file in order to successfully discover peers. "
- f"Exception: {e}")
- return self._cached_peers
diff --git a/exo/networking/manual/network_topology_config.py b/exo/networking/manual/network_topology_config.py
deleted file mode 100644
index c0246ec3..00000000
--- a/exo/networking/manual/network_topology_config.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from typing import Dict
-from pydantic import BaseModel, ValidationError
-
-from exo.topology.device_capabilities import DeviceCapabilities
-
-
-class PeerConfig(BaseModel):
- address: str
- port: int
- device_capabilities: DeviceCapabilities
-
-
-class NetworkTopology(BaseModel):
- """Configuration of the network. A collection outlining all nodes in the network, including the node this is running from."""
-
- peers: Dict[str, PeerConfig]
- """
- node_id to PeerConfig. The node_id is used to identify the peer in the discovery process. The node that this is running from should be included in this dict.
- """
- @classmethod
- def from_path(cls, path: str) -> "NetworkTopology":
- try:
- with open(path, "r") as f:
- config_data = f.read()
- except FileNotFoundError as e:
- raise FileNotFoundError(f"Config file not found at {path}") from e
-
- try:
- return cls.model_validate_json(config_data)
- except ValidationError as e:
- raise ValueError(f"Error validating network topology config from {path}: {e}") from e
diff --git a/exo/networking/manual/test_data/invalid_config.json b/exo/networking/manual/test_data/invalid_config.json
deleted file mode 100644
index 283feadf..00000000
--- a/exo/networking/manual/test_data/invalid_config.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "peers": {
- "node1": {
- "address": "localhost",
- "device_capabilities": {
- "model": "Unknown Model",
- "chip": "Unknown Chip",
- "memory": 0,
- "flops": {
- "fp32": 0,
- "fp16": 0,
- "int8": 0
- }
- }
- }
- }
-}
diff --git a/exo/networking/manual/test_data/invalid_json.json b/exo/networking/manual/test_data/invalid_json.json
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/networking/manual/test_data/test_config.json b/exo/networking/manual/test_data/test_config.json
deleted file mode 100644
index 54eced72..00000000
--- a/exo/networking/manual/test_data/test_config.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "peers": {
- "node1": {
- "address": "localhost",
- "port": 50051,
- "device_capabilities": {
- "model": "Unknown Model",
- "chip": "Unknown Chip",
- "memory": 0,
- "flops": {
- "fp32": 0,
- "fp16": 0,
- "int8": 0
- }
- }
- },
- "node2": {
- "address": "localhost",
- "port": 50052,
- "device_capabilities": {
- "model": "Unknown Model",
- "chip": "Unknown Chip",
- "memory": 0,
- "flops": {
- "fp32": 0,
- "fp16": 0,
- "int8": 0
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/exo/networking/manual/test_data/test_config_single_node.json b/exo/networking/manual/test_data/test_config_single_node.json
deleted file mode 100644
index 81a0670f..00000000
--- a/exo/networking/manual/test_data/test_config_single_node.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "peers": {
- "node1": {
- "address": "localhost",
- "port": 50051,
- "device_capabilities": {
- "model": "Unknown Model",
- "chip": "Unknown Chip",
- "memory": 0,
- "flops": {
- "fp32": 0,
- "fp16": 0,
- "int8": 0
- }
- }
- }
- }
-}
diff --git a/exo/networking/manual/test_manual_discovery.py b/exo/networking/manual/test_manual_discovery.py
deleted file mode 100644
index 317fba9d..00000000
--- a/exo/networking/manual/test_manual_discovery.py
+++ /dev/null
@@ -1,181 +0,0 @@
-import json
-import asyncio
-import unittest
-from unittest import mock
-from exo.networking.manual.manual_discovery import ManualDiscovery
-from exo.networking.manual.network_topology_config import NetworkTopology
-from exo.networking.grpc.grpc_peer_handle import GRPCPeerHandle
-from exo.networking.grpc.grpc_server import GRPCServer
-from exo.orchestration.node import Node
-
-root_path = "./exo/networking/manual/test_data/test_config.json"
-
-
-class TestSingleNodeManualDiscovery(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- self.peer1 = mock.AsyncMock()
- self.peer1.connect = mock.AsyncMock()
- self.discovery1 = ManualDiscovery(
- root_path,
- "node1",
- create_peer_handle=lambda peer_id, address, description, device_capabilities: self.peer1,
- )
- await self.discovery1.start()
-
- async def asyncTearDown(self):
- await self.discovery1.stop()
-
- async def test_discovery(self):
- peers1 = await self.discovery1.discover_peers(wait_for_peers=0)
- assert len(peers1) == 0
-
- self.peer1.connect.assert_not_called()
-
-
-class TestManualDiscovery(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- self.peer1 = mock.AsyncMock()
- self.peer2 = mock.AsyncMock()
- self.peer1.connect = mock.AsyncMock()
- self.peer2.connect = mock.AsyncMock()
- self.discovery1 = ManualDiscovery(
- root_path,
- "node1",
- create_peer_handle=lambda peer_id, address, description, device_capabilities: self.peer1,
- )
- self.discovery2 = ManualDiscovery(
- root_path,
- "node2",
- create_peer_handle=lambda peer_id, address, description, device_capabilities: self.peer2,
- )
- await self.discovery1.start()
- await self.discovery2.start()
-
- async def asyncTearDown(self):
- await self.discovery1.stop()
- await self.discovery2.stop()
-
- async def test_discovery(self):
- peers1 = await self.discovery1.discover_peers(wait_for_peers=1)
- assert len(peers1) == 1
- peers2 = await self.discovery2.discover_peers(wait_for_peers=1)
- assert len(peers2) == 1
-
- # connect has to be explicitly called after discovery
- self.peer1.connect.assert_not_called()
- self.peer2.connect.assert_not_called()
-
-
-class TestManualDiscoveryWithGRPCPeerHandle(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- config = NetworkTopology.from_path(root_path)
-
- self.node1 = mock.AsyncMock(spec=Node)
- self.node2 = mock.AsyncMock(spec=Node)
- self.server1 = GRPCServer(self.node1, config.peers["node1"].address, config.peers["node1"].port)
- self.server2 = GRPCServer(self.node2, config.peers["node2"].address, config.peers["node2"].port)
- await self.server1.start()
- await self.server2.start()
- self.discovery1 = ManualDiscovery(
- root_path,
- "node1",
- create_peer_handle=lambda peer_id, address, description, device_capabilities: GRPCPeerHandle(peer_id, address, description, device_capabilities),
- )
- self.discovery2 = ManualDiscovery(
- root_path,
- "node2",
- create_peer_handle=lambda peer_id, address, description, device_capabilities: GRPCPeerHandle(peer_id, address, description, device_capabilities),
- )
- await self.discovery1.start()
- await self.discovery2.start()
-
- async def asyncTearDown(self):
- await self.discovery1.stop()
- await self.discovery2.stop()
- await self.server1.stop()
- await self.server2.stop()
-
- async def test_grpc_discovery(self):
- peers1 = await self.discovery1.discover_peers(wait_for_peers=1)
- assert len(peers1) == 1
- peers2 = await self.discovery2.discover_peers(wait_for_peers=1)
- assert len(peers2) == 1
-
- # Connect
- await peers1[0].connect()
- await peers2[0].connect()
- self.assertTrue(await peers1[0].is_connected())
- self.assertTrue(await peers2[0].is_connected())
-
- # Kill server1
- await self.server1.stop()
-
- self.assertTrue(await peers1[0].is_connected())
- self.assertFalse(await peers2[0].is_connected())
-
- # Kill server2
- await self.server2.stop()
-
- self.assertFalse(await peers1[0].is_connected())
- self.assertFalse(await peers2[0].is_connected())
-
- async def test_dynamic_config_update(self):
- initial_peers = await self.discovery1.discover_peers(wait_for_peers=1)
- self.assertEqual(len(initial_peers), 1)
-
- # Save original config for cleanup
- with open(root_path, "r") as f:
- original_config = json.load(f)
-
- try:
- updated_config = {
- "peers": {
- **original_config["peers"],
- "node3": {
- "address": "localhost",
- "port": 50053,
- "device_capabilities": {
- "model": "Unknown Model",
- "chip": "Unknown Chip",
- "memory": 0,
- "flops": {"fp32": 0, "fp16": 0, "int8": 0},
- },
- },
- }
- }
-
- with open(root_path, "w") as f:
- json.dump(updated_config, f, indent=2)
-
- node3 = mock.AsyncMock(spec=Node)
- server3 = GRPCServer(node3, "localhost", 50053)
- await server3.start()
-
- try:
- # Wait for the config to be reloaded
- await asyncio.sleep(1.5)
-
- updated_peers = await self.discovery1.discover_peers(wait_for_peers=2)
- self.assertEqual(len(updated_peers), 2)
-
- for peer in updated_peers:
- await peer.connect()
- self.assertTrue(await peer.is_connected())
-
- finally:
- await server3.stop()
-
- finally:
- # Restore the original config file
- with open(root_path, "w") as f:
- json.dump(original_config, f, indent=2)
-
- # Wait for the config to be reloaded again
- await asyncio.sleep(1.5)
-
- updated_peers = await self.discovery1.discover_peers(wait_for_peers=1)
- self.assertEqual(len(updated_peers), 1)
-
-
-if __name__ == "__main__":
- asyncio.run(unittest.main())
diff --git a/exo/networking/manual/test_network_topology_config.py b/exo/networking/manual/test_network_topology_config.py
deleted file mode 100644
index b5b2fbd5..00000000
--- a/exo/networking/manual/test_network_topology_config.py
+++ /dev/null
@@ -1,49 +0,0 @@
-import unittest
-
-from exo.networking.manual.network_topology_config import NetworkTopology
-
-root_path = "./exo/networking/manual/test_data/"
-
-
-class TestNetworkTopologyConfig(unittest.TestCase):
- def test_from_path_invalid_path(self):
- with self.assertRaises(FileNotFoundError) as e:
- NetworkTopology.from_path("invalid_path")
- self.assertEqual(str(e.exception), "Config file not found at invalid_path")
-
- def test_from_path_invalid_json(self):
- with self.assertRaises(ValueError) as e:
- NetworkTopology.from_path(root_path + "invalid_json.json")
- self.assertIn("Error validating network topology config from", str(e.exception))
- self.assertIn("1 validation error for NetworkTopology\n Invalid JSON: EOF while parsing a value at line 1 column 0", str(e.exception))
-
- def test_from_path_invalid_config(self):
- with self.assertRaises(ValueError) as e:
- NetworkTopology.from_path(root_path + "invalid_config.json")
- self.assertIn("Error validating network topology config from", str(e.exception))
- self.assertIn("port\n Field required", str(e.exception))
-
- def test_from_path_valid(self):
- config = NetworkTopology.from_path(root_path + "test_config.json")
-
- self.assertEqual(config.peers["node1"].port, 50051)
- self.assertEqual(config.peers["node1"].device_capabilities.model, "Unknown Model")
- self.assertEqual(config.peers["node1"].address, "localhost")
- self.assertEqual(config.peers["node1"].device_capabilities.chip, "Unknown Chip")
- self.assertEqual(config.peers["node1"].device_capabilities.memory, 0)
- self.assertEqual(config.peers["node1"].device_capabilities.flops.fp32, 0)
- self.assertEqual(config.peers["node1"].device_capabilities.flops.fp16, 0)
- self.assertEqual(config.peers["node1"].device_capabilities.flops.int8, 0)
-
- self.assertEqual(config.peers["node2"].port, 50052)
- self.assertEqual(config.peers["node2"].device_capabilities.model, "Unknown Model")
- self.assertEqual(config.peers["node2"].address, "localhost")
- self.assertEqual(config.peers["node2"].device_capabilities.chip, "Unknown Chip")
- self.assertEqual(config.peers["node2"].device_capabilities.memory, 0)
- self.assertEqual(config.peers["node2"].device_capabilities.flops.fp32, 0)
- self.assertEqual(config.peers["node2"].device_capabilities.flops.fp16, 0)
- self.assertEqual(config.peers["node2"].device_capabilities.flops.int8, 0)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/exo/networking/peer_handle.py b/exo/networking/peer_handle.py
deleted file mode 100644
index d75318ef..00000000
--- a/exo/networking/peer_handle.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import Optional, Tuple, List
-import numpy as np
-from exo.inference.shard import Shard
-from exo.topology.device_capabilities import DeviceCapabilities
-from exo.topology.topology import Topology
-
-
-class PeerHandle(ABC):
- @abstractmethod
- def id(self) -> str:
- pass
-
- @abstractmethod
- def addr(self) -> str:
- pass
-
- @abstractmethod
- def description(self) -> str:
- pass
-
- @abstractmethod
- def device_capabilities(self) -> DeviceCapabilities:
- pass
-
- @abstractmethod
- async def connect(self) -> None:
- pass
-
- @abstractmethod
- async def is_connected(self) -> bool:
- pass
-
- @abstractmethod
- async def disconnect(self) -> None:
- pass
-
- @abstractmethod
- async def health_check(self) -> bool:
- pass
-
- @abstractmethod
- async def send_prompt(self, shard: Shard, prompt: str, request_id: Optional[str] = None) -> Optional[np.array]:
- pass
-
- @abstractmethod
- async def send_tensor(self, shard: Shard, tensor: np.array, request_id: Optional[str] = None) -> Optional[np.array]:
- pass
-
- @abstractmethod
- async def send_result(self, request_id: str, result: List[int], is_finished: bool) -> None:
- pass
-
- @abstractmethod
- async def collect_topology(self, visited: set[str], max_depth: int) -> Topology:
- pass
diff --git a/exo/networking/server.py b/exo/networking/server.py
deleted file mode 100644
index 8e7f9812..00000000
--- a/exo/networking/server.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from abc import ABC, abstractmethod
-
-
-class Server(ABC):
- @abstractmethod
- async def start(self) -> None:
- pass
-
- @abstractmethod
- async def stop(self) -> None:
- pass
diff --git a/exo/networking/tailscale/__init__.py b/exo/networking/tailscale/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/networking/tailscale/tailscale_discovery.py b/exo/networking/tailscale/tailscale_discovery.py
deleted file mode 100644
index 61a13507..00000000
--- a/exo/networking/tailscale/tailscale_discovery.py
+++ /dev/null
@@ -1,178 +0,0 @@
-import asyncio
-import time
-import traceback
-from typing import List, Dict, Callable, Tuple
-from exo.networking.discovery import Discovery
-from exo.networking.peer_handle import PeerHandle
-from exo.topology.device_capabilities import DeviceCapabilities, device_capabilities, UNKNOWN_DEVICE_CAPABILITIES
-from exo.helpers import DEBUG, DEBUG_DISCOVERY
-from .tailscale_helpers import get_device_id, update_device_attributes, get_device_attributes, get_tailscale_devices, Device
-
-
-class TailscaleDiscovery(Discovery):
- def __init__(
- self,
- node_id: str,
- node_port: int,
- create_peer_handle: Callable[[str, str, str, DeviceCapabilities], PeerHandle],
- discovery_interval: int = 5,
- discovery_timeout: int = 30,
- update_interval: int = 15,
- device_capabilities: DeviceCapabilities = UNKNOWN_DEVICE_CAPABILITIES,
- tailscale_api_key: str = None,
- tailnet: str = None,
- allowed_node_ids: List[str] = None,
- ):
- self.node_id = node_id
- self.node_port = node_port
- self.create_peer_handle = create_peer_handle
- self.discovery_interval = discovery_interval
- self.discovery_timeout = discovery_timeout
- self.update_interval = update_interval
- self.device_capabilities = device_capabilities
- self.known_peers: Dict[str, Tuple[PeerHandle, float, float]] = {}
- self.discovery_task = None
- self.cleanup_task = None
- self.tailscale_api_key = tailscale_api_key
- self.tailnet = tailnet
- self.allowed_node_ids = allowed_node_ids
- self._device_id = None
- self.update_task = None
-
- async def start(self):
- self.device_capabilities = await device_capabilities()
- self.discovery_task = asyncio.create_task(self.task_discover_peers())
- self.cleanup_task = asyncio.create_task(self.task_cleanup_peers())
- self.update_task = asyncio.create_task(self.task_update_device_posture_attributes())
-
- async def task_update_device_posture_attributes(self):
- while True:
- try:
- await self.update_device_posture_attributes()
- if DEBUG_DISCOVERY >= 2:
- print(f"Updated device posture attributes")
- except Exception as e:
- print(f"Error updating device posture attributes: {e}")
- print(traceback.format_exc())
- finally:
- await asyncio.sleep(self.update_interval)
-
- async def get_device_id(self):
- if self._device_id:
- return self._device_id
- self._device_id = await get_device_id()
- return self._device_id
-
- async def update_device_posture_attributes(self):
- await update_device_attributes(await self.get_device_id(), self.tailscale_api_key, self.node_id, self.node_port, self.device_capabilities)
-
- async def task_discover_peers(self):
- while True:
- try:
- devices: dict[str, Device] = await get_tailscale_devices(self.tailscale_api_key, self.tailnet)
- current_time = time.time()
-
- active_devices = {name: device for name, device in devices.items() if device.last_seen is not None and (current_time - device.last_seen.timestamp()) < 30}
-
- if DEBUG_DISCOVERY >= 4: print(f"Found tailscale devices: {devices}")
- if DEBUG_DISCOVERY >= 2: print(f"Active tailscale devices: {len(active_devices)}/{len(devices)}")
- if DEBUG_DISCOVERY >= 2: print("Time since last seen tailscale devices", [(current_time - device.last_seen.timestamp()) for device in devices.values()])
-
- for device in active_devices.values():
- if device.name == self.node_id: continue
- peer_host = device.addresses[0]
- peer_id, peer_port, device_capabilities = await get_device_attributes(device.device_id, self.tailscale_api_key)
- if not peer_id:
- if DEBUG_DISCOVERY >= 4: print(f"{device.device_id} does not have exo node attributes. skipping.")
- continue
-
- if self.allowed_node_ids and peer_id not in self.allowed_node_ids:
- if DEBUG_DISCOVERY >= 2: print(f"Ignoring peer {peer_id} as it's not in the allowed node IDs list")
- continue
-
- if peer_id not in self.known_peers or self.known_peers[peer_id][0].addr() != f"{peer_host}:{peer_port}":
- new_peer_handle = self.create_peer_handle(peer_id, f"{peer_host}:{peer_port}", "TS", device_capabilities)
- if not await new_peer_handle.health_check():
- if DEBUG >= 1: print(f"Peer {peer_id} at {peer_host}:{peer_port} is not healthy. Skipping.")
- continue
-
- if DEBUG >= 1: print(f"Adding {peer_id=} at {peer_host}:{peer_port}. Replace existing peer_id: {peer_id in self.known_peers}")
- self.known_peers[peer_id] = (
- new_peer_handle,
- current_time,
- current_time,
- )
- else:
- if not await self.known_peers[peer_id][0].health_check():
- if DEBUG >= 1: print(f"Peer {peer_id} at {peer_host}:{peer_port} is not healthy. Removing.")
- if peer_id in self.known_peers: del self.known_peers[peer_id]
- continue
- self.known_peers[peer_id] = (self.known_peers[peer_id][0], self.known_peers[peer_id][1], current_time)
-
- except Exception as e:
- print(f"Error in discover peers: {e}")
- print(traceback.format_exc())
- finally:
- await asyncio.sleep(self.discovery_interval)
-
- async def stop(self):
- if self.discovery_task:
- self.discovery_task.cancel()
- if self.cleanup_task:
- self.cleanup_task.cancel()
- if self.update_task:
- self.update_task.cancel()
- if self.discovery_task or self.cleanup_task or self.update_task:
- await asyncio.gather(self.discovery_task, self.cleanup_task, self.update_task, return_exceptions=True)
-
- async def discover_peers(self, wait_for_peers: int = 0) -> List[PeerHandle]:
- if wait_for_peers > 0:
- while len(self.known_peers) < wait_for_peers:
- if DEBUG_DISCOVERY >= 2:
- print(f"Current peers: {len(self.known_peers)}/{wait_for_peers}. Waiting for more peers...")
- await asyncio.sleep(0.1)
- return [peer_handle for peer_handle, _, _ in self.known_peers.values()]
-
- async def task_cleanup_peers(self):
- while True:
- try:
- current_time = time.time()
- peers_to_remove = []
-
- peer_ids = list(self.known_peers.keys())
- results = await asyncio.gather(*[self.check_peer(peer_id, current_time) for peer_id in peer_ids], return_exceptions=True)
-
- for peer_id, should_remove in zip(peer_ids, results):
- if should_remove: peers_to_remove.append(peer_id)
-
- if DEBUG_DISCOVERY >= 2:
- print(
- "Peer statuses:", {
- peer_handle.id(): f"is_connected={await peer_handle.is_connected()}, health_check={await peer_handle.health_check()}, connected_at={connected_at}, last_seen={last_seen}"
- for peer_handle, connected_at, last_seen in self.known_peers.values()
- }
- )
-
- for peer_id in peers_to_remove:
- if peer_id in self.known_peers:
- del self.known_peers[peer_id]
- if DEBUG_DISCOVERY >= 2: print(f"Removed peer {peer_id} due to inactivity or failed health check.")
- except Exception as e:
- print(f"Error in cleanup peers: {e}")
- print(traceback.format_exc())
- finally:
- await asyncio.sleep(self.discovery_interval)
-
- async def check_peer(self, peer_id: str, current_time: float) -> bool:
- peer_handle, connected_at, last_seen = self.known_peers.get(peer_id, (None, None, None))
- if peer_handle is None: return False
-
- try:
- is_connected = await peer_handle.is_connected()
- health_ok = await peer_handle.health_check()
- except Exception as e:
- if DEBUG_DISCOVERY >= 2: print(f"Error checking peer {peer_id}: {e}")
- return True
-
- should_remove = ((not is_connected and current_time - connected_at > self.discovery_timeout) or (current_time - last_seen > self.discovery_timeout) or (not health_ok))
- return should_remove
diff --git a/exo/networking/tailscale/tailscale_helpers.py b/exo/networking/tailscale/tailscale_helpers.py
deleted file mode 100644
index 969a72da..00000000
--- a/exo/networking/tailscale/tailscale_helpers.py
+++ /dev/null
@@ -1,125 +0,0 @@
-import json
-import asyncio
-import aiohttp
-import re
-from typing import Dict, Any, Tuple, List, Optional
-from exo.helpers import DEBUG_DISCOVERY
-from exo.topology.device_capabilities import DeviceCapabilities, DeviceFlops
-from datetime import datetime, timezone
-
-
-class Device:
- def __init__(self, device_id: str, name: str, addresses: List[str], last_seen: Optional[datetime] = None):
- self.device_id = device_id
- self.name = name
- self.addresses = addresses
- self.last_seen = last_seen
-
- @classmethod
- def from_dict(cls, data: Dict[str, Any]) -> 'Device':
- return cls(device_id=data.get('id', ''), name=data.get('name', ''), addresses=data.get('addresses', []), last_seen=cls.parse_datetime(data.get('lastSeen')))
-
- @staticmethod
- def parse_datetime(date_string: Optional[str]) -> Optional[datetime]:
- if not date_string:
- return None
- return datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
-
-
-async def get_device_id() -> str:
- try:
- process = await asyncio.create_subprocess_exec('tailscale', 'status', '--json', stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
- stdout, stderr = await process.communicate()
- if process.returncode != 0:
- raise Exception(f"Command failed with exit code {process.returncode}: {stderr.decode().strip()}.")
- if DEBUG_DISCOVERY >= 4: print(f"tailscale status: {stdout.decode()}")
- data = json.loads(stdout.decode())
- return data['Self']['ID']
- except Exception as e:
- raise Exception(f"{str(e)} Do you have the tailscale cli installed? See: https://tailscale.com/kb/1080/cli")
-
-
-async def update_device_attributes(device_id: str, api_key: str, node_id: str, node_port: int, device_capabilities: DeviceCapabilities):
- async with aiohttp.ClientSession() as session:
- base_url = f"https://api.tailscale.com/api/v2/device/{device_id}/attributes"
- headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}
-
- attributes = {
- "custom:exo_node_id": node_id.replace('-', '_'), "custom:exo_node_port": node_port, "custom:exo_device_capability_chip": sanitize_attribute(device_capabilities.chip),
- "custom:exo_device_capability_model": sanitize_attribute(device_capabilities.model), "custom:exo_device_capability_memory": str(device_capabilities.memory),
- "custom:exo_device_capability_flops_fp16": str(device_capabilities.flops.fp16), "custom:exo_device_capability_flops_fp32": str(device_capabilities.flops.fp32),
- "custom:exo_device_capability_flops_int8": str(device_capabilities.flops.int8)
- }
-
- for attr_name, attr_value in attributes.items():
- url = f"{base_url}/{attr_name}"
- data = {"value": str(attr_value).replace(' ', '_')} # Ensure all values are strings for JSON
- async with session.post(url, headers=headers, json=data) as response:
- if response.status == 200:
- if DEBUG_DISCOVERY >= 1: print(f"Updated device posture attribute {attr_name} for device {device_id}")
- else:
- print(f"Failed to update device posture attribute {attr_name}: {response.status} {await response.text()}")
-
-
-async def get_device_attributes(device_id: str, api_key: str) -> Tuple[str, int, DeviceCapabilities]:
- async with aiohttp.ClientSession() as session:
- url = f"https://api.tailscale.com/api/v2/device/{device_id}/attributes"
- headers = {'Authorization': f'Bearer {api_key}'}
- async with session.get(url, headers=headers) as response:
- if response.status == 200:
- data = await response.json()
- attributes = data.get("attributes", {})
- node_id = attributes.get("custom:exo_node_id", "").replace('_', '-')
- node_port = int(attributes.get("custom:exo_node_port", 0))
- device_capabilities = DeviceCapabilities(
- model=attributes.get("custom:exo_device_capability_model", "").replace('_', ' '),
- chip=attributes.get("custom:exo_device_capability_chip", "").replace('_', ' '),
- memory=int(attributes.get("custom:exo_device_capability_memory", 0)),
- flops=DeviceFlops(
- fp16=float(attributes.get("custom:exo_device_capability_flops_fp16", 0)),
- fp32=float(attributes.get("custom:exo_device_capability_flops_fp32", 0)),
- int8=float(attributes.get("custom:exo_device_capability_flops_int8", 0))
- )
- )
- return node_id, node_port, device_capabilities
- else:
- print(f"Failed to fetch posture attributes for {device_id}: {response.status}")
- return "", 0, DeviceCapabilities(model="", chip="", memory=0, flops=DeviceFlops(fp16=0, fp32=0, int8=0))
-
-
-def parse_device_attributes(data: Dict[str, str]) -> Dict[str, Any]:
- result = {}
- prefix = "custom:exo_"
- for key, value in data.items():
- if key.startswith(prefix):
- attr_name = key.replace(prefix, "")
- if attr_name in ["node_id", "node_port", "device_capability_chip", "device_capability_model"]:
- result[attr_name] = value.replace('_', ' ')
- elif attr_name in ["device_capability_memory", "device_capability_flops_fp16", "device_capability_flops_fp32", "device_capability_flops_int8"]:
- result[attr_name] = float(value)
- return result
-
-
-def sanitize_attribute(value: str) -> str:
- # Replace invalid characters with underscores
- sanitized_value = re.sub(r'[^a-zA-Z0-9_.]', '_', value)
- # Truncate to 50 characters
- return sanitized_value[:50]
-
-
-async def get_tailscale_devices(api_key: str, tailnet: str) -> Dict[str, Device]:
- async with aiohttp.ClientSession() as session:
- url = f"https://api.tailscale.com/api/v2/tailnet/{tailnet}/devices"
- headers = {"Authorization": f"Bearer {api_key}"}
-
- async with session.get(url, headers=headers) as response:
- response.raise_for_status()
- data = await response.json()
-
- devices = {}
- for device_data in data.get("devices", []):
- print("Device data: ", device_data)
- device = Device.from_dict(device_data)
- devices[device.name] = device
-
- return devices
diff --git a/exo/networking/tailscale/test_tailscale_discovery.py b/exo/networking/tailscale/test_tailscale_discovery.py
deleted file mode 100644
index 63cc9d04..00000000
--- a/exo/networking/tailscale/test_tailscale_discovery.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import os
-import asyncio
-import unittest
-from unittest import mock
-from exo.networking.tailscale.tailscale_discovery import TailscaleDiscovery
-from exo.networking.peer_handle import PeerHandle
-
-
-class TestTailscaleDiscovery(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- self.tailscale_api_key = os.environ.get("TAILSCALE_API_KEY", "")
- self.tailnet = os.environ.get("TAILSCALE_TAILNET", "")
- self.discovery = TailscaleDiscovery(
- node_id="test_node",
- node_port=50051,
- create_peer_handle=lambda peer_id, address, description, device_capabilities: unittest.mock.Mock(spec=PeerHandle, id=lambda: peer_id),
- tailscale_api_key=self.tailscale_api_key,
- tailnet=self.tailnet
- )
- await self.discovery.start()
-
- async def asyncTearDown(self):
- await self.discovery.stop()
-
- async def test_discovery(self):
- # Wait for a short period to allow discovery to happen
- await asyncio.sleep(15)
-
- # Get discovered peers
- peers = await self.discovery.discover_peers()
-
- # Check if any peers were discovered
- self.assertGreater(len(peers), 0, "No peers were discovered")
-
- # Print discovered peers for debugging
- print(f"Discovered peers: {[peer.id() for peer in peers]}")
-
- # Check if discovered peers are instances of GRPCPeerHandle
- print(peers)
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/exo/networking/udp/__init__.py b/exo/networking/udp/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/networking/udp/test_udp_discovery.py b/exo/networking/udp/test_udp_discovery.py
deleted file mode 100644
index ff1dc5c2..00000000
--- a/exo/networking/udp/test_udp_discovery.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import asyncio
-import unittest
-from unittest import mock
-from exo.networking.udp.udp_discovery import UDPDiscovery
-from exo.networking.grpc.grpc_peer_handle import GRPCPeerHandle
-from exo.networking.grpc.grpc_server import GRPCServer
-from exo.orchestration.node import Node
-
-
-class TestUDPDiscovery(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- self.peer1 = mock.AsyncMock()
- self.peer2 = mock.AsyncMock()
- self.peer1.connect = mock.AsyncMock()
- self.peer2.connect = mock.AsyncMock()
- self.discovery1 = UDPDiscovery("discovery1", 50051, 5678, 5679, create_peer_handle=lambda peer_id, address, description, device_capabilities: self.peer1)
- self.discovery2 = UDPDiscovery("discovery2", 50052, 5679, 5678, create_peer_handle=lambda peer_id, address, description, device_capabilities: self.peer2)
- await self.discovery1.start()
- await self.discovery2.start()
-
- async def asyncTearDown(self):
- await self.discovery1.stop()
- await self.discovery2.stop()
-
- async def test_discovery(self):
- peers1 = await self.discovery1.discover_peers(wait_for_peers=1)
- assert len(peers1) == 1
- peers2 = await self.discovery2.discover_peers(wait_for_peers=1)
- assert len(peers2) == 1
-
- # connect has to be explicitly called after discovery
- self.peer1.connect.assert_not_called()
- self.peer2.connect.assert_not_called()
-
-
-class TestUDPDiscoveryWithGRPCPeerHandle(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- self.node1 = mock.AsyncMock(spec=Node)
- self.node2 = mock.AsyncMock(spec=Node)
- self.server1 = GRPCServer(self.node1, "localhost", 50053)
- self.server2 = GRPCServer(self.node2, "localhost", 50054)
- await self.server1.start()
- await self.server2.start()
- self.discovery1 = UDPDiscovery("discovery1", 50053, 5678, 5679, lambda peer_id, address, description, device_capabilities: GRPCPeerHandle(peer_id, address, description, device_capabilities))
- self.discovery2 = UDPDiscovery("discovery2", 50054, 5679, 5678, lambda peer_id, address, description, device_capabilities: GRPCPeerHandle(peer_id, address, description, device_capabilities))
- await self.discovery1.start()
- await self.discovery2.start()
-
- async def asyncTearDown(self):
- await self.discovery1.stop()
- await self.discovery2.stop()
- await self.server1.stop()
- await self.server2.stop()
-
- async def test_grpc_discovery(self):
- peers1 = await self.discovery1.discover_peers(wait_for_peers=1)
- assert len(peers1) == 1
- peers2 = await self.discovery2.discover_peers(wait_for_peers=1)
- assert len(peers2) == 1
- assert not await peers1[0].is_connected()
- assert not await peers2[0].is_connected()
-
- # Connect
- await peers1[0].connect()
- await peers2[0].connect()
- assert await peers1[0].is_connected()
- assert await peers2[0].is_connected()
-
- # Kill server1
- await self.server1.stop()
-
- assert await peers1[0].is_connected()
- assert not await peers2[0].is_connected()
-
-
-if __name__ == "__main__":
- asyncio.run(unittest.main())
diff --git a/exo/networking/udp/udp_discovery.py b/exo/networking/udp/udp_discovery.py
deleted file mode 100644
index 7117c7cd..00000000
--- a/exo/networking/udp/udp_discovery.py
+++ /dev/null
@@ -1,246 +0,0 @@
-import asyncio
-import json
-import socket
-import time
-import traceback
-from typing import List, Dict, Callable, Tuple, Coroutine, Optional
-from exo.networking.discovery import Discovery
-from exo.networking.peer_handle import PeerHandle
-from exo.topology.device_capabilities import DeviceCapabilities, device_capabilities, UNKNOWN_DEVICE_CAPABILITIES
-from exo.helpers import DEBUG, DEBUG_DISCOVERY, get_all_ip_addresses_and_interfaces, get_interface_priority_and_type
-
-
-class ListenProtocol(asyncio.DatagramProtocol):
- def __init__(self, on_message: Callable[[bytes, Tuple[str, int]], Coroutine]):
- super().__init__()
- self.on_message = on_message
- self.loop = asyncio.get_event_loop()
-
- def connection_made(self, transport):
- self.transport = transport
-
- def datagram_received(self, data, addr):
- asyncio.create_task(self.on_message(data, addr))
-
-
-def get_broadcast_address(ip_addr: str) -> str:
- try:
- # Split IP into octets and create broadcast address for the subnet
- ip_parts = ip_addr.split('.')
- return f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.255"
- except:
- return "255.255.255.255"
-
-
-class BroadcastProtocol(asyncio.DatagramProtocol):
- def __init__(self, message: str, broadcast_port: int, source_ip: str):
- self.message = message
- self.broadcast_port = broadcast_port
- self.source_ip = source_ip
-
- def connection_made(self, transport):
- sock = transport.get_extra_info("socket")
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- # Try both subnet-specific and global broadcast
- broadcast_addr = get_broadcast_address(self.source_ip)
- transport.sendto(self.message.encode("utf-8"), (broadcast_addr, self.broadcast_port))
- if broadcast_addr != "255.255.255.255":
- transport.sendto(self.message.encode("utf-8"), ("255.255.255.255", self.broadcast_port))
-
-
-class UDPDiscovery(Discovery):
- def __init__(
- self,
- node_id: str,
- node_port: int,
- listen_port: int,
- broadcast_port: int,
- create_peer_handle: Callable[[str, str, str, DeviceCapabilities], PeerHandle],
- broadcast_interval: int = 2.5,
- discovery_timeout: int = 30,
- device_capabilities: DeviceCapabilities = UNKNOWN_DEVICE_CAPABILITIES,
- allowed_node_ids: Optional[List[str]] = None,
- allowed_interface_types: Optional[List[str]] = None,
- ):
- self.node_id = node_id
- self.node_port = node_port
- self.listen_port = listen_port
- self.broadcast_port = broadcast_port
- self.create_peer_handle = create_peer_handle
- self.broadcast_interval = broadcast_interval
- self.discovery_timeout = discovery_timeout
- self.device_capabilities = device_capabilities
- self.allowed_node_ids = allowed_node_ids
- self.allowed_interface_types = allowed_interface_types
- self.known_peers: Dict[str, Tuple[PeerHandle, float, float, int]] = {}
- self.broadcast_task = None
- self.listen_task = None
- self.cleanup_task = None
-
- async def start(self):
- self.device_capabilities = await device_capabilities()
- self.broadcast_task = asyncio.create_task(self.task_broadcast_presence())
- self.listen_task = asyncio.create_task(self.task_listen_for_peers())
- self.cleanup_task = asyncio.create_task(self.task_cleanup_peers())
-
- async def stop(self):
- if self.broadcast_task: self.broadcast_task.cancel()
- if self.listen_task: self.listen_task.cancel()
- if self.cleanup_task: self.cleanup_task.cancel()
- if self.broadcast_task or self.listen_task or self.cleanup_task:
- await asyncio.gather(self.broadcast_task, self.listen_task, self.cleanup_task, return_exceptions=True)
-
- async def discover_peers(self, wait_for_peers: int = 0) -> List[PeerHandle]:
- if wait_for_peers > 0:
- while len(self.known_peers) < wait_for_peers:
- if DEBUG_DISCOVERY >= 2: print(f"Current peers: {len(self.known_peers)}/{wait_for_peers}. Waiting for more peers...")
- await asyncio.sleep(0.1)
- return [peer_handle for peer_handle, _, _, _ in self.known_peers.values()]
-
- async def task_broadcast_presence(self):
- while True:
- for addr, interface_name in get_all_ip_addresses_and_interfaces():
- interface_priority, interface_type = await get_interface_priority_and_type(interface_name)
- message = json.dumps({
- "type": "discovery",
- "node_id": self.node_id,
- "grpc_port": self.node_port,
- "device_capabilities": self.device_capabilities.to_dict(),
- "priority": interface_priority,
- "interface_name": interface_name,
- "interface_type": interface_type,
- })
-
- transport = None
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- try:
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
- except AttributeError:
- pass
- sock.bind((addr, 0))
-
- transport, _ = await asyncio.get_event_loop().create_datagram_endpoint(
- lambda: BroadcastProtocol(message, self.broadcast_port, addr),
- sock=sock
- )
- except Exception as e:
- print(f"Error in broadcast presence ({addr} - {interface_name} - {interface_priority}): {e}")
- finally:
- if transport:
- try: transport.close()
- except Exception as e:
- if DEBUG_DISCOVERY >= 2: print(f"Error closing transport: {e}")
-
- await asyncio.sleep(self.broadcast_interval)
-
- async def on_listen_message(self, data, addr):
- if not data:
- return
-
- decoded_data = data.decode("utf-8", errors="ignore")
-
- # Check if the decoded data starts with a valid JSON character
- if not (decoded_data.strip() and decoded_data.strip()[0] in "{["):
- if DEBUG_DISCOVERY >= 2: print(f"Received invalid JSON data from {addr}: {decoded_data[:100]}")
- return
-
- try:
- decoder = json.JSONDecoder(strict=False)
- message = decoder.decode(decoded_data)
- except json.JSONDecodeError as e:
- if DEBUG_DISCOVERY >= 2: print(f"Error decoding JSON data from {addr}: {e}")
- return
-
- if DEBUG_DISCOVERY >= 2: print(f"received from peer {addr}: {message}")
-
- if message["type"] == "discovery" and message["node_id"] != self.node_id:
- peer_id = message["node_id"]
-
- # Skip if peer_id is not in allowed list
- if self.allowed_node_ids and peer_id not in self.allowed_node_ids:
- if DEBUG_DISCOVERY >= 2: print(f"Ignoring peer {peer_id} as it's not in the allowed node IDs list")
- return
-
- peer_host = addr[0]
- peer_port = message["grpc_port"]
- peer_prio = message["priority"]
- peer_interface_name = message["interface_name"]
- peer_interface_type = message["interface_type"]
-
- # Skip if interface type is not in allowed list
- if self.allowed_interface_types and peer_interface_type not in self.allowed_interface_types:
- if DEBUG_DISCOVERY >= 2: print(f"Ignoring peer {peer_id} as its interface type {peer_interface_type} is not in the allowed interface types list")
- return
-
- device_capabilities = DeviceCapabilities(**message["device_capabilities"])
-
- if peer_id not in self.known_peers or self.known_peers[peer_id][0].addr() != f"{peer_host}:{peer_port}":
- if peer_id in self.known_peers:
- existing_peer_prio = self.known_peers[peer_id][3]
- if existing_peer_prio >= peer_prio:
- if DEBUG >= 1:
- print(f"Ignoring peer {peer_id} at {peer_host}:{peer_port} with priority {peer_prio} because we already know about a peer with higher or equal priority: {existing_peer_prio}")
- return
- new_peer_handle = self.create_peer_handle(peer_id, f"{peer_host}:{peer_port}", f"{peer_interface_type} ({peer_interface_name})", device_capabilities)
- if not await new_peer_handle.health_check():
- if DEBUG >= 1: print(f"Peer {peer_id} at {peer_host}:{peer_port} is not healthy. Skipping.")
- return
- if DEBUG >= 1: print(f"Adding {peer_id=} at {peer_host}:{peer_port}. Replace existing peer_id: {peer_id in self.known_peers}")
- self.known_peers[peer_id] = (new_peer_handle, time.time(), time.time(), peer_prio)
- else:
- if not await self.known_peers[peer_id][0].health_check():
- if DEBUG >= 1: print(f"Peer {peer_id} at {peer_host}:{peer_port} is not healthy. Removing.")
- if peer_id in self.known_peers: del self.known_peers[peer_id]
- return
- if peer_id in self.known_peers: self.known_peers[peer_id] = (self.known_peers[peer_id][0], self.known_peers[peer_id][1], time.time(), peer_prio)
-
- async def task_listen_for_peers(self):
- await asyncio.get_event_loop().create_datagram_endpoint(lambda: ListenProtocol(self.on_listen_message), local_addr=("0.0.0.0", self.listen_port))
- if DEBUG_DISCOVERY >= 2: print("Started listen task")
-
- async def task_cleanup_peers(self):
- while True:
- try:
- current_time = time.time()
- peers_to_remove = []
-
- peer_ids = list(self.known_peers.keys())
- results = await asyncio.gather(*[self.check_peer(peer_id, current_time) for peer_id in peer_ids], return_exceptions=True)
-
- for peer_id, should_remove in zip(peer_ids, results):
- if should_remove: peers_to_remove.append(peer_id)
-
- if DEBUG_DISCOVERY >= 2:
- print(
- "Peer statuses:", {
- peer_handle.id(): f"is_connected={await peer_handle.is_connected()}, health_check={await peer_handle.health_check()}, connected_at={connected_at}, last_seen={last_seen}, prio={prio}"
- for peer_handle, connected_at, last_seen, prio in self.known_peers.values()
- }
- )
-
- for peer_id in peers_to_remove:
- if peer_id in self.known_peers:
- del self.known_peers[peer_id]
- if DEBUG_DISCOVERY >= 2: print(f"Removed peer {peer_id} due to inactivity or failed health check.")
- except Exception as e:
- print(f"Error in cleanup peers: {e}")
- print(traceback.format_exc())
- finally:
- await asyncio.sleep(self.broadcast_interval)
-
- async def check_peer(self, peer_id: str, current_time: float) -> bool:
- peer_handle, connected_at, last_seen, prio = self.known_peers.get(peer_id, (None, None, None, None))
- if peer_handle is None: return False
-
- try:
- is_connected = await peer_handle.is_connected()
- health_ok = await peer_handle.health_check()
- except Exception as e:
- if DEBUG_DISCOVERY >= 2: print(f"Error checking peer {peer_id}: {e}")
- return True
-
- should_remove = ((not is_connected and current_time - connected_at > self.discovery_timeout) or (current_time - last_seen > self.discovery_timeout) or (not health_ok))
- return should_remove
diff --git a/exo/orchestration/__init__.py b/exo/orchestration/__init__.py
deleted file mode 100644
index 022a9160..00000000
--- a/exo/orchestration/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .node import Node
-
-__all__ = ["Node"]
diff --git a/exo/orchestration/node.py b/exo/orchestration/node.py
deleted file mode 100644
index 1281aa8a..00000000
--- a/exo/orchestration/node.py
+++ /dev/null
@@ -1,628 +0,0 @@
-import numpy as np
-import json
-import asyncio
-import uuid
-import time
-import traceback
-from typing import List, Dict, Optional, Tuple, Union, Set
-from exo.networking import Discovery, PeerHandle, Server
-from exo.inference.inference_engine import InferenceEngine, Shard
-from exo.topology.topology import Topology
-from exo.topology.device_capabilities import device_capabilities, UNKNOWN_DEVICE_CAPABILITIES
-from exo.topology.partitioning_strategy import Partition, PartitioningStrategy, map_partitions_to_shards
-from exo import DEBUG
-from exo.helpers import AsyncCallbackSystem
-from exo.viz.topology_viz import TopologyViz
-from exo.download.download_progress import RepoProgressEvent
-from exo.inference.inference_engine import get_inference_engine, InferenceEngine
-from exo.download.shard_download import ShardDownloader
-
-class Node:
- def __init__(
- self,
- _id: str,
- server: Server,
- inference_engine: InferenceEngine,
- discovery: Discovery,
- shard_downloader: ShardDownloader,
- partitioning_strategy: PartitioningStrategy = None,
- max_generate_tokens: int = 1024,
- default_sample_temperature: float = 0.0,
- topology_viz: Optional[TopologyViz] = None,
- ):
- self.id = _id
- self.inference_engine = inference_engine
- self.server = server
- self.discovery = discovery
- self.shard_downloader = shard_downloader
- self.partitioning_strategy = partitioning_strategy
- self.peers: List[PeerHandle] = {}
- self.topology: Topology = Topology()
- self.device_capabilities = UNKNOWN_DEVICE_CAPABILITIES
- self.buffered_token_output: Dict[str, Tuple[List[int], bool]] = {}
- self.buffered_logits: Dict[str, List[np.ndarray]] = {}
- self.buffered_inputs: Dict[str, List[np.ndarray]] = {}
- self.buffered_partials: Dict[str, List[np.ndarray]] = {}
- self.checkpoints: Dict[str, Dict[str, int]] = {}
-
- self.max_generate_tokens = max_generate_tokens
- self.topology_viz = topology_viz
- self.default_sample_temperature = default_sample_temperature
- self._on_token = AsyncCallbackSystem[str, Tuple[str, List[int], bool]]()
- self._on_opaque_status = AsyncCallbackSystem[str, Tuple[str, str]]()
- self._on_opaque_status.register("node_status").on_next(self.on_node_status)
- self.node_download_progress: Dict[str, RepoProgressEvent] = {}
- self.topology_inference_engines_pool: List[List[str]] = []
- self.outstanding_requests = {}
-
- async def start(self, wait_for_peers: int = 0) -> None:
- self.device_capabilities = await device_capabilities()
- await self.server.start()
- await self.discovery.start()
- await self.update_peers(wait_for_peers)
- await self.collect_topology(set())
- if DEBUG >= 2: print(f"Collected topology: {self.topology}")
- asyncio.create_task(self.periodic_topology_collection(2.0))
-
- async def stop(self) -> None:
- await self.discovery.stop()
- await self.server.stop()
-
- def on_node_status(self, request_id, opaque_status):
- try:
- status_data = json.loads(opaque_status)
- status_type = status_data.get("type", "")
- if status_type == "supported_inference_engines":
- node_id = status_data.get("node_id")
- engines = status_data.get("engines", [])
- self.topology_inference_engines_pool.append(engines)
- elif status_type == "node_status":
- if status_data.get("status", "").startswith("start_"):
- self.current_topology.active_node_id = status_data.get("node_id")
- elif status_data.get("status", "").startswith("end_"):
- if status_data.get("node_id") == self.current_topology.active_node_id:
- self.current_topology.active_node_id = None
-
- download_progress = None
- if status_type == "download_progress":
- if DEBUG >= 8: print(f"Download progress from {status_data.get('node_id')}: {status_data.get('progress')}")
- download_progress = RepoProgressEvent.from_dict(status_data.get('progress'))
- self.node_download_progress[status_data.get('node_id')] = download_progress
-
- if self.topology_viz:
- self.topology_viz.update_visualization(self.topology, self.partitioning_strategy.partition(self.topology), self.id, self.node_download_progress)
- except Exception as e:
- if DEBUG >= 1: print(f"Error on_node_status: {e}")
- if DEBUG >= 1: traceback.print_exc()
-
- def get_supported_inference_engines(self):
- supported_engine_names = []
- if self.inference_engine.__class__.__name__ == 'MLXDynamicShardInferenceEngine':
- supported_engine_names.append('mlx')
- supported_engine_names.append('tinygrad')
- else:
- supported_engine_names.append('tinygrad')
- return supported_engine_names
-
- async def broadcast_supported_engines(self, supported_engines_names: List[str]):
- status_message = json.dumps({"type": "supported_inference_engines", "node_id": self.id, "engines": supported_engines_names})
- await self.broadcast_opaque_status("", status_message)
-
- def get_topology_inference_engines(self) -> List[List[str]]:
- return self.topology_inference_engines_pool
-
- token_count = 0
- first_token_time = 0
- async def process_inference_result(
- self,
- shard,
- result: np.ndarray,
- request_id: Optional[str] = None,
- inference_state: Optional[dict] = None,
- ):
- if shard.model_id != 'stable-diffusion-2-1-base':
- if request_id not in self.buffered_token_output:
- self.buffered_token_output[request_id] = ([], False)
- is_finished = len(self.buffered_token_output[request_id][0]) >= self.max_generate_tokens
- if shard.is_last_layer() and not is_finished:
- token = await self.inference_engine.sample(result, temp=self.default_sample_temperature)
- await self.inference_engine.ensure_shard(shard)
- self.buffered_token_output[request_id][0].append(token.item())
- is_finished = token.item() == self.inference_engine.tokenizer.eos_token_id or is_finished or len(self.buffered_token_output[request_id][0]) >= self.max_generate_tokens
- if DEBUG >= 2: print(f"[{request_id}] result size: {result.size}, is finished: {is_finished}, buffered tokens: {len(self.buffered_token_output[request_id][0])}")
- forward = token.reshape(1, -1)
- intermediate_result = [self.buffered_token_output[request_id][0][-1]]
- else:
- forward = result
- else:
- await self.inference_engine.ensure_shard(shard)
- is_finished = inference_state.get("is_finished", False)
- intermediate_result, inference_state = self.handle_stable_diffusion(inference_state, result)
- forward = result
- if shard.is_last_layer():
- self.trigger_on_token_callbacks(request_id, intermediate_result, is_finished)
- asyncio.create_task(self.broadcast_result(request_id, intermediate_result, is_finished))
-
- if is_finished:
- if shard.model_id != 'stable-diffusion-2-1-base':
- self.buffered_token_output[request_id] = (self.buffered_token_output[request_id][0], True)
- self.outstanding_requests.pop(request_id)
- else:
- self.outstanding_requests[request_id] = "waiting"
- asyncio.create_task(self.forward_tensor(shard, forward, request_id, self.get_partition_index(offset = 1), inference_state))
-
- return np.array(self.buffered_token_output[request_id][0]) if shard.model_id != 'stable-diffusion-2-1-base' else intermediate_result
-
-
- async def process_prompt(
- self,
- base_shard: Shard,
- prompt: str,
- request_id: Optional[str] = None,
- inference_state: Optional[dict] = {},
- ) -> Optional[np.ndarray]:
- shard = self.get_current_shard(base_shard)
- start_time = time.perf_counter_ns()
- asyncio.create_task(
- self.broadcast_opaque_status(
- request_id,
- json.dumps({
- "type": "node_status",
- "node_id": self.id,
- "status": "start_process_prompt",
- "base_shard": base_shard.to_dict(),
- "shard": shard.to_dict(),
- "prompt": prompt,
- "request_id": request_id,
- }),
- )
- )
- start_time = time.perf_counter_ns()
- resp = await self._process_prompt(base_shard, prompt, request_id, inference_state)
- end_time = time.perf_counter_ns()
- elapsed_time_ns = end_time - start_time
- asyncio.create_task(
- self.broadcast_opaque_status(
- request_id,
- json.dumps({
- "type": "node_status",
- "node_id": self.id,
- "status": "end_process_prompt",
- "base_shard": base_shard.to_dict(),
- "shard": shard.to_dict(),
- "prompt": prompt,
- "request_id": request_id,
- "elapsed_time_ns": elapsed_time_ns,
- }),
- )
- )
- if DEBUG >= 2: print(f"[{request_id}] process prompt: {base_shard=} {shard=} {prompt=} {elapsed_time_ns=}")
-
- async def _process_prompt(self, base_shard: Shard, prompt: str, request_id: Optional[str] = None, inference_state: Optional[dict] = None) -> Optional[np.ndarray]:
- if request_id is None:
- request_id = str(uuid.uuid4())
- shard = self.get_current_shard(base_shard)
- if DEBUG >= 2: print(f"[{request_id}] process prompt: {base_shard=} {shard=} {prompt=}")
-
- if not shard.is_first_layer():
- if DEBUG >= 2: print(f"[{request_id}] forwarding to next shard: {base_shard=} {shard=} {prompt=}")
- self.outstanding_requests[request_id] = "waiting"
- resp = await self.forward_prompt(shard, prompt, request_id, 0, inference_state)
- return None
- else:
- self.outstanding_requests[request_id] = "processing"
- result, inference_state = await self.inference_engine.infer_prompt(request_id, shard, prompt, inference_state)
- ret = await self.process_inference_result(shard, result, request_id, inference_state)
- return result
-
- async def enqueue_example(
- self,
- base_shard: Shard,
- example: np.ndarray,
- target: np.ndarray,
- length: np.ndarray,
- request_id: Optional[str] = None,
- train: bool = False,
- ):
- shard = self.get_current_shard(base_shard)
- if shard.is_first_layer():
- loss = await self.process_example(shard, example, target, length, train, request_id)
- return loss
- else:
- if request_id is None:
- request_id = str(uuid.uuid4())
- self.outstanding_requests[request_id] = "waiting"
- loss = await self.forward_example(shard, example, target, length, train, request_id, 0)
- return loss
-
- async def coordinate_save(
- self,
- base_shard: Shard,
- iteration: int,
- destination: str,
- ):
- shard = self.get_current_shard(base_shard)
- model = shard.model_id
- sid = shard.__hash__()
- path = f"{destination}/{model}/{sid}-{iteration}.safetensors"
- self.outstanding_requests[f"{sid}::{iteration}"] = "Checking"
- if model not in self.checkpoints:
- self.checkpoints[model] = {}
- if sid not in self.checkpoints[model]:
- self.checkpoints[model][sid] = []
- if len(self.checkpoints[model][sid]) < 1 or self.checkpoints[model][sid][-1] < iteration:
- print(f"Saving checkpoint to {path}")
- self.outstanding_requests[f"{sid}::{iteration}"] = "Saving"
- import os
- os.makedirs("/".join(path.split("/")[:-1]), exist_ok=True)
- await self.inference_engine.save_checkpoint(shard, path)
- self.checkpoints[model][sid] = sorted(self.checkpoints[model][sid] + [iteration])
- self.outstanding_requests.pop(f"{sid}::{iteration}")
-
- async def process_example(
- self,
- base_shard: Shard,
- example: np.ndarray,
- target: np.ndarray,
- length: np.ndarray,
- train: bool = False,
- request_id: Optional[str] = None,
- ):
- shard = self.get_current_shard(base_shard)
- asyncio.create_task(
- self.broadcast_opaque_status(
- request_id,
- json.dumps({
- "type": "node_status",
- "node_id": self.id,
- "status": f"start_{'train' if train else 'eval'}_example",
- "base_shard": base_shard.to_dict(),
- "shard": shard.to_dict(),
- "example_size": example.size,
- "example_shape": example.shape,
- "request_id": request_id,
- }),
- )
- )
- start_time = time.perf_counter_ns()
- resp = await self._process_example(shard, example, target, length, train, request_id)
- end_time = time.perf_counter_ns()
- elapsed_time_ns = end_time - start_time
- asyncio.create_task(
- self.broadcast_opaque_status(
- request_id,
- json.dumps({
- "type": "node_status",
- "node_id": self.id,
- "status": f"end_{'train' if train else 'eval'}_example",
- "base_shard": base_shard.to_dict(),
- "shard": shard.to_dict(),
- "request_id": request_id,
- "elapsed_time_ns": elapsed_time_ns,
- }),
- )
- )
- return resp
-
- async def _process_example(
- self,
- base_shard: Shard,
- example: np.ndarray,
- target: np.ndarray,
- length: np.ndarray,
- train: bool = False,
- request_id: Optional[str] = None,
- ) -> Optional[np.ndarray]:
- if request_id is None:
- request_id = str(uuid.uuid4())
- shard = self.get_current_shard(base_shard)
- if DEBUG >= 1: print(f"[{request_id}] process_example: {example.shape=}")
- try:
- target = target.astype(int)
- if train:
- if shard.is_last_layer():
- self.outstanding_requests[request_id] = "training"
- loss, grad = await self.inference_engine.train(request_id, shard, example, target, length)
- else:
- self.outstanding_requests[request_id] = "preprocessing"
- step, _ = await self.inference_engine.infer_tensor(request_id, shard, example)
- self.outstanding_requests[request_id] = "waiting"
- loss, backgrad = await self.forward_example(shard, step, target, length, train, request_id, self.get_partition_index(offset = 1))
- self.outstanding_requests[request_id] = "training"
- partial_loss, grad = await self.inference_engine.train(request_id, shard, example, backgrad, length, loss="back_gradient")
- self.outstanding_requests.pop(request_id)
- if shard.is_first_layer():
- return loss
- else:
- return loss, grad
- else:
- if shard.is_last_layer():
- self.outstanding_requests[request_id] = "evaluating"
- loss = await self.inference_engine.evaluate(request_id, shard, example, target, length)
- else:
- self.outstanding_requests[request_id] = "preprocessing"
- step, _ = await self.inference_engine.infer_tensor(request_id, shard, example)
- self.outstanding_requests[request_id] = "waiting"
- loss = await self.forward_example(shard, step, target, length, train, request_id, self.get_partition_index(offset = 1))
- self.outstanding_requests.pop(request_id)
- return loss
- except Exception as e:
- self.outstanding_requests.pop(request_id)
- print(f"Error processing example for shard {shard}: {e}")
- traceback.print_exc()
- return None
-
- async def process_tensor(
- self,
- base_shard: Shard,
- tensor: np.ndarray,
- request_id: Optional[str] = None,
- inference_state: Optional[dict] = None,
- ) -> Optional[np.ndarray]:
- shard = self.get_current_shard(base_shard)
- start_time = time.perf_counter_ns()
- resp = await self._process_tensor(shard, tensor, request_id, inference_state)
- end_time = time.perf_counter_ns()
- elapsed_time_ns = end_time - start_time
- if DEBUG >= 2: print(f"[{request_id}] process_tensor: {base_shard=} {shard=} {tensor.size=} {tensor.shape=} {elapsed_time_ns=}")
-
- async def _process_tensor(
- self,
- base_shard: Shard,
- tensor: np.ndarray,
- request_id: Optional[str] = None,
- inference_state: Optional[dict] = None,
- ) -> Optional[np.ndarray]:
- if request_id is None:
- request_id = str(uuid.uuid4())
- shard = self.get_current_shard(base_shard)
-
- try:
- self.outstanding_requests[request_id] = "processing"
- result, inference_state = await self.inference_engine.infer_tensor(request_id, shard, tensor, inference_state)
- ret = await self.process_inference_result(shard, result, request_id, inference_state)
- return ret
- except Exception as e:
- self.outstanding_requests.pop(request_id)
- print(f"Error processing tensor for shard {shard}: {e}")
- traceback.print_exc()
-
- async def forward_example(
- self,
- base_shard: Shard,
- step: np.ndarray,
- target: np.ndarray,
- length: np.ndarray,
- train: bool,
- request_id: str,
- target_index: int,
- ) -> None:
- if DEBUG >= 1: print(f"target partition index: {target_index}")
- target_id = self.partitioning_strategy.partition(self.topology)[target_index].node_id
- target_shard = self.get_current_shard(base_shard, target_index)
- if DEBUG >= 2: print(f"computed target from: {base_shard} {target_index}, {self.topology}. target shard: {target_shard}")
- target_peer = next((p for p in self.peers if p.id() == target_id), None)
- if not target_peer:
- raise ValueError(f"peer for {target_index} not found")
- if DEBUG >= 1: print(f"sending example to {target_peer.id()}: {step} => {target} ({length})")
- resp = await target_peer.send_example(target_shard, step, target, length, request_id=request_id, train=train)
- return resp
-
- async def forward_prompt(
- self,
- base_shard: Shard,
- prompt: str,
- request_id: str,
- target_index: int,
- inference_state: Optional[dict] = None,
- ) -> None:
- if DEBUG >= 1: print(f"target partition index: {target_index}")
- target_id = self.partitioning_strategy.partition(self.topology)[target_index].node_id
- next_shard = self.get_current_shard(base_shard, target_index)
- if DEBUG >= 2: print(f"Computed target from: {base_shard} {target_index}, {self.topology}. next shard: {next_shard}")
- if target_id == self.id:
- await self.process_prompt(next_shard, prompt, request_id, inference_state)
- else:
- target_peer = next((p for p in self.peers if p.id() == target_id), None)
- if not target_peer:
- raise ValueError(f"Peer for {target_index} not found")
- if DEBUG >= 1: print(f"Sending prompt to {target_peer.id()}: {prompt}")
- await target_peer.send_prompt(next_shard, prompt, request_id=request_id, inference_state=inference_state)
-
- async def forward_tensor(
- self,
- base_shard: Shard,
- tensor: np.ndarray,
- request_id: str,
- target_index: int,
- inference_state: Optional[dict] = None,
- ) -> None:
- if DEBUG >= 1: print(f"target partition index: {target_index}")
- target_id = self.partitioning_strategy.partition(self.topology)[target_index].node_id
- next_shard = self.get_current_shard(base_shard, target_index)
- if DEBUG >= 2: print(f"Computed target from: {base_shard} {target_index}, {self.topology}. target shard: {next_shard}")
- if target_id == self.id:
- await self.process_tensor(next_shard, tensor, request_id, inference_state)
- else:
- target_peer = next((p for p in self.peers if p.id() == target_id), None)
- if not target_peer:
- raise ValueError(f"Peer for {target_index} not found")
- if DEBUG >= 1: print(f"Sending tensor to {target_peer.id()}: {tensor}")
- await target_peer.send_tensor(next_shard, tensor, request_id=request_id, inference_state=inference_state)
-
- def get_partition_index(self, offset: int = 0):
- if not self.partitioning_strategy:
- if DEBUG >= 1: print("No partitioning strategy found. Skipping forward.")
- return None
- partitions = self.partitioning_strategy.partition(self.topology)
- current_partition_index = next((i for i, p in enumerate(partitions) if p.node_id == self.id), None)
- if current_partition_index is None:
- raise ValueError(f"No current partition found for node: {self.id}")
- return (current_partition_index + offset) % len(partitions)
-
- def get_current_shard(self, base_shard: Shard, index: Optional[int] = None) -> Shard:
- if index is None:
- index = self.get_partition_index()
- partitions = self.partitioning_strategy.partition(self.topology)
- shards = map_partitions_to_shards(partitions, base_shard.n_layers, base_shard.model_id)
- return shards[index]
-
- async def update_peers(self, wait_for_peers: int = 0) -> bool:
- next_peers = await self.discovery.discover_peers(wait_for_peers)
- current_peer_ids = {peer.id() for peer in self.peers}
- next_peer_ids = {peer.id() for peer in next_peers}
- peers_added = [peer for peer in next_peers if peer.id() not in current_peer_ids]
- peers_removed = [peer for peer in self.peers if peer.id() not in next_peer_ids]
- peers_updated = [peer for peer in next_peers if peer.id() in current_peer_ids and any(p.addr() != peer.addr() for p in self.peers if p.id() == peer.id())]
- peers_unchanged = [peer for peer in next_peers if peer.id() in current_peer_ids and all(p.addr() == peer.addr() for p in self.peers if p.id() == peer.id())]
- peers_to_disconnect = [peer for peer in peers_removed if await peer.is_connected()]
- peers_to_connect = [peer for peer in peers_added + peers_updated + peers_unchanged if not await peer.is_connected()]
-
- def _pretty(peers: List[PeerHandle]) -> List[str]:
- return [f"{peer.id()}@{peer.addr()}" for peer in peers]
-
- if DEBUG >= 2:
- print(f"update_peers: added={peers_added} removed={peers_removed} updated={peers_updated} unchanged={peers_unchanged} to_disconnect={peers_to_disconnect} to_connect={peers_to_connect}")
-
- async def disconnect_with_timeout(peer, timeout=5):
- try:
- await asyncio.wait_for(peer.disconnect(), timeout)
- return True
- except Exception as e:
- print(f"Error disconnecting peer {peer.id()}@{peer.addr()}: {e}")
- traceback.print_exc()
- return False
-
- async def connect_with_timeout(peer, timeout=5):
- try:
- await asyncio.wait_for(peer.connect(), timeout)
- return True
- except Exception as e:
- print(f"Error connecting peer {peer.id()}@{peer.addr()}: {e}")
- traceback.print_exc()
- return False
-
- disconnect_results = await asyncio.gather(*(disconnect_with_timeout(peer) for peer in peers_to_disconnect), return_exceptions=True)
- connect_results = await asyncio.gather(*(connect_with_timeout(peer) for peer in peers_to_connect), return_exceptions=True)
-
- successful_disconnects = [peer for peer, result in zip(peers_to_disconnect, disconnect_results) if result is True]
- failed_disconnects = [peer for peer, result in zip(peers_to_disconnect, disconnect_results) if result is False]
- successful_connects = [peer for peer, result in zip(peers_to_connect, connect_results) if result is True]
- failed_connects = [peer for peer, result in zip(peers_to_connect, connect_results) if result is False]
- if DEBUG >= 1:
- if successful_disconnects: print(f"Successfully disconnected peers: {_pretty(successful_disconnects)}")
- if failed_disconnects: print(f"Failed to disconnect peers: {_pretty(failed_disconnects)}")
- if successful_connects: print(f"Successfully connected peers: {_pretty(successful_connects)}")
- if failed_connects: print(f"Failed to connect peers: {_pretty(failed_connects)}")
-
- self.peers = next_peers
- return len(peers_added) > 0 or len(peers_removed) > 0 or len(peers_updated) > 0
-
- async def select_best_inference_engine(self):
- if self.inference_engine.__class__.__name__ == 'DummyInferenceEngine': return
- supported_engines = self.get_supported_inference_engines()
- await self.broadcast_supported_engines(supported_engines)
- if len(self.get_topology_inference_engines()):
- self.inference_engine = get_inference_engine(supported_engines[0], self.shard_downloader)
-
- async def periodic_topology_collection(self, interval: int):
- while True:
- await asyncio.sleep(interval)
- try:
- did_peers_change = await self.update_peers()
- if DEBUG >= 2: print(f"{did_peers_change=}")
- await self.collect_topology(set())
- if did_peers_change:
- await self.select_best_inference_engine()
- except Exception as e:
- print(f"Error collecting topology: {e}")
- traceback.print_exc()
-
- async def collect_topology(self, visited: set[str], max_depth: int = 4) -> Topology:
- next_topology = Topology()
- next_topology.update_node(self.id, self.device_capabilities)
-
- if DEBUG >= 2: print(f"Collecting topology {max_depth=} {visited=}")
-
- prev_visited = visited.copy()
- visited.add(self.id)
- visited.update(p.id() for p in self.peers)
-
- for peer in self.peers:
- next_topology.update_node(peer.id(), peer.device_capabilities())
- next_topology.add_edge(self.id, peer.id(), peer.description())
-
- if peer.id() in prev_visited:
- continue
-
- if max_depth <= 0:
- if DEBUG >= 2: print("Max depth reached. Skipping...")
- continue
-
- try:
- other_topology = await asyncio.wait_for(peer.collect_topology(visited, max_depth=max_depth - 1), timeout=5.0)
- if DEBUG >= 2: print(f"Collected topology from: {peer.id()}: {other_topology}")
- next_topology.merge(peer.id(), other_topology)
- except Exception as e:
- print(f"Error collecting topology from {peer.id()}: {e}")
- traceback.print_exc()
-
- next_topology.active_node_id = self.topology.active_node_id
- self.topology = next_topology
- if self.topology_viz:
- self.topology_viz.update_visualization(self.topology, self.partitioning_strategy.partition(self.topology), self.id)
- return self.topology
-
- @property
- def on_token(self) -> AsyncCallbackSystem[str, Tuple[str, List[int], bool]]:
- return self._on_token
-
- @property
- def on_opaque_status(self) -> AsyncCallbackSystem[str, Tuple[str, str]]:
- return self._on_opaque_status
-
- def trigger_on_token_callbacks(self, request_id: str, tokens: List[int], is_finished: bool) -> None:
- if DEBUG >= 2: print(f"Triggering all on_token callbacks with {request_id=} {tokens=} {is_finished=}")
- self.on_token.trigger_all(request_id, tokens, is_finished)
-
- async def broadcast_result(self, request_id: str, result: List[int], is_finished: bool) -> None:
- if DEBUG >= 2: print(f"Broadcasting result: {request_id=} {result=} {is_finished=}")
- async def send_result_to_peer(peer):
- try:
- await asyncio.wait_for(peer.send_result(request_id, result, is_finished), timeout=15.0)
- except asyncio.TimeoutError:
- print(f"Timeout broadcasting result to {peer.id()}")
- except Exception as e:
- print(f"Error broadcasting result to {peer.id()}: {e}")
- traceback.print_exc()
-
- await asyncio.gather(*[send_result_to_peer(peer) for peer in self.peers], return_exceptions=True)
-
- async def broadcast_opaque_status(self, request_id: str, status: str) -> None:
- if DEBUG >= 8: print(f"Broadcasting opaque status: {request_id=} {status=}")
-
- async def send_status_to_peer(peer):
- try:
- await asyncio.wait_for(peer.send_opaque_status(request_id, status), timeout=15.0)
- except asyncio.TimeoutError:
- print(f"Timeout sending opaque status to {peer.id()}")
- except Exception as e:
- print(f"Error sending opaque status to {peer.id()}: {e}")
- traceback.print_exc()
-
- await asyncio.gather(*[send_status_to_peer(peer) for peer in self.peers], return_exceptions=True)
- # in the case of opaque status, we also want to receive our own opaque statuses
- self.on_opaque_status.trigger_all(request_id, status)
-
- @property
- def current_topology(self) -> Topology:
- return self.topology
-
- def handle_stable_diffusion(self, inference_state, result):
- if inference_state['is_step_finished']:
- inference_state['step']+=1
- progress = [inference_state['step'],inference_state['total_steps']]
- intermediate_result = result
- if progress[0] == progress[1]:
- intermediate_result = result
- return intermediate_result, inference_state
diff --git a/exo/orchestration/test_node.py b/exo/orchestration/test_node.py
deleted file mode 100644
index 6f992bbe..00000000
--- a/exo/orchestration/test_node.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import unittest
-from unittest.mock import Mock, AsyncMock
-import numpy as np
-import pytest
-
-from .node import Node
-from exo.networking.peer_handle import PeerHandle
-from exo.download.shard_download import NoopShardDownloader
-
-class TestNode(unittest.IsolatedAsyncioTestCase):
- def setUp(self):
- self.mock_inference_engine = AsyncMock()
- self.mock_server = AsyncMock()
- self.mock_server.start = AsyncMock()
- self.mock_server.stop = AsyncMock()
- self.mock_discovery = AsyncMock()
- self.mock_discovery.start = AsyncMock()
- self.mock_discovery.stop = AsyncMock()
- mock_peer1 = Mock(spec=PeerHandle)
- mock_peer1.id.return_value = "peer1"
- mock_peer2 = Mock(spec=PeerHandle)
- mock_peer2.id.return_value = "peer2"
- self.mock_discovery.discover_peers = AsyncMock(return_value=[mock_peer1, mock_peer2])
-
- self.node = Node("test_node", self.mock_server, self.mock_inference_engine, "localhost", 50051, self.mock_discovery, NoopShardDownloader())
-
- async def asyncSetUp(self):
- await self.node.start()
-
- async def asyncTearDown(self):
- await self.node.stop()
-
- async def test_node_initialization(self):
- self.assertEqual(self.node.node_id, "test_node")
- self.assertEqual(self.node.host, "localhost")
- self.assertEqual(self.node.port, 50051)
-
- async def test_node_start(self):
- self.mock_server.start.assert_called_once_with("localhost", 50051)
-
- async def test_node_stop(self):
- await self.node.stop()
- self.mock_server.stop.assert_called_once()
-
- async def test_discover_and_connect_to_peers(self):
- await self.node.discover_and_connect_to_peers()
- self.assertEqual(len(self.node.peers), 2)
- self.assertIn("peer1", map(lambda p: p.id(), self.node.peers))
- self.assertIn("peer2", map(lambda p: p.id(), self.node.peers))
-
- async def test_process_tensor_calls_inference_engine(self):
- mock_peer = Mock()
- self.node.peers = [mock_peer]
-
- input_tensor = np.array([69, 1, 2])
- await self.node.process_tensor(input_tensor, None)
-
- self.node.inference_engine.process_shard.assert_called_once_with(input_tensor)
-
- @pytest.mark.asyncio
- async def test_node_capabilities():
- node = Node()
- await node.initialize()
- caps = await node.get_device_capabilities()
- assert caps is not None
- assert caps.model != ""
diff --git a/exo/orchestration/tracing.py b/exo/orchestration/tracing.py
deleted file mode 100644
index 4466fc7d..00000000
--- a/exo/orchestration/tracing.py
+++ /dev/null
@@ -1,166 +0,0 @@
-from dataclasses import dataclass
-from typing import Dict, Optional, Any
-from opentelemetry import trace, context
-from opentelemetry.trace import Status, StatusCode, SpanContext
-from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
-from contextlib import contextmanager
-import time
-from threading import Lock
-
-@dataclass
-class TraceContext:
- request_id: str
- sequence_number: int
- current_span: Optional[trace.Span] = None
- trace_parent: Optional[str] = None
- token_group_span: Optional[trace.Span] = None
- token_count: int = 0
- token_group_size: int = 10 # Default group size
- request_span: Optional[trace.Span] = None # Track the main request span
-
-class Tracer:
- def __init__(self):
- self.tracer = trace.get_tracer("exo")
- self.contexts: Dict[str, TraceContext] = {}
- self._lock = Lock()
- self.propagator = TraceContextTextMapPropagator()
-
- def get_context(self, request_id: str) -> Optional[TraceContext]:
- with self._lock:
- return self.contexts.get(request_id)
-
- def set_context(self, request_id: str, context: TraceContext):
- with self._lock:
- self.contexts[request_id] = context
-
- def inject_context(self, span: trace.Span) -> str:
- """Inject current span context into carrier for propagation"""
- carrier = {}
- ctx = trace.set_span_in_context(span)
- self.propagator.inject(carrier, context=ctx)
- return carrier.get("traceparent", "")
-
- def extract_context(self, trace_parent: str) -> Optional[context.Context]:
- """Extract span context from carrier"""
- if not trace_parent:
- return None
- carrier = {"traceparent": trace_parent}
- return self.propagator.extract(carrier)
-
- def create_context_from_parent(self, request_id: str, trace_parent: str, sequence_number: int = 0) -> TraceContext:
- """Create a new context with the given trace parent"""
- parent_ctx = self.extract_context(trace_parent)
- if parent_ctx:
- # Create a new request span that links to the parent context
- request_span = self.tracer.start_span(
- "request",
- context=parent_ctx,
- attributes={
- "request_id": request_id,
- "sequence_number": sequence_number
- }
- )
- return TraceContext(
- request_id=request_id,
- sequence_number=sequence_number,
- request_span=request_span,
- current_span=request_span,
- trace_parent=trace_parent
- )
- return TraceContext(request_id=request_id, sequence_number=sequence_number)
-
- def handle_token(self, context: TraceContext, token: int, is_finished: bool = False):
- """Handle token generation and manage token group spans"""
- context.token_count += 1
-
- # Start a new token group span if needed
- if not context.token_group_span and context.request_span:
- group_number = (context.token_count - 1) // context.token_group_size + 1
-
- # Create token group span as child of request span
- parent_ctx = trace.set_span_in_context(context.request_span)
- context.token_group_span = self.tracer.start_span(
- f"token_group_{group_number}",
- context=parent_ctx,
- attributes={
- "request_id": context.request_id,
- "group.number": group_number,
- "group.start_token": context.token_count,
- "group.max_tokens": context.token_group_size
- }
- )
-
- # Add token to current group span
- if context.token_group_span:
- relative_pos = ((context.token_count - 1) % context.token_group_size) + 1
- context.token_group_span.set_attribute(f"token.{relative_pos}", token)
- context.token_group_span.set_attribute("token.count", relative_pos)
-
- # End current group span if we've reached the group size or if generation is finished
- if context.token_count % context.token_group_size == 0 or is_finished:
- context.token_group_span.set_attribute("token.final_count", relative_pos)
- context.token_group_span.end()
- context.token_group_span = None
-
- @contextmanager
- def start_span(self, name: str, context: TraceContext, extra_attributes: Optional[Dict[str, Any]] = None):
- """Start a new span with proper parent context"""
- attributes = {
- "request_id": context.request_id,
- "sequence_number": context.sequence_number
- }
- if extra_attributes:
- attributes.update(extra_attributes)
-
- # Use request span as parent if available
- parent_ctx = None
- if context.request_span:
- parent_ctx = trace.set_span_in_context(context.request_span)
- elif context.trace_parent:
- parent_ctx = self.extract_context(context.trace_parent)
- if parent_ctx and not context.request_span:
- # Create a new request span that links to the parent context
- context.request_span = self.tracer.start_span(
- "request",
- context=parent_ctx,
- attributes={
- "request_id": context.request_id,
- "sequence_number": context.sequence_number
- }
- )
- parent_ctx = trace.set_span_in_context(context.request_span)
- elif context.current_span:
- parent_ctx = trace.set_span_in_context(context.current_span)
-
- # Create span with parent context if it exists
- if parent_ctx:
- span = self.tracer.start_span(
- name,
- context=parent_ctx,
- attributes=attributes
- )
- else:
- span = self.tracer.start_span(
- name,
- attributes=attributes
- )
-
- # Update context with current span
- prev_span = context.current_span
- context.current_span = span
-
- try:
- start_time = time.perf_counter()
- yield span
- duration = time.perf_counter() - start_time
- span.set_attribute("duration_s", duration)
- span.set_status(Status(StatusCode.OK))
- except Exception as e:
- span.set_status(Status(StatusCode.ERROR, str(e)))
- raise
- finally:
- span.end()
- context.current_span = prev_span
-
-# Global tracer instance
-tracer = Tracer()
\ No newline at end of file
diff --git a/exo/test_callbacks.py b/exo/test_callbacks.py
deleted file mode 100644
index c10083d6..00000000
--- a/exo/test_callbacks.py
+++ /dev/null
@@ -1,50 +0,0 @@
-import asyncio
-from typing import Any, Callable
-from exo.helpers import AsyncCallbackSystem, AsyncCallback
-
-
-# Usage example
-async def main() -> None:
- callback_system = AsyncCallbackSystem[str, Any]()
-
- # Register callbacks
- callback1 = callback_system.register("callback1")
- callback2 = callback_system.register("callback2")
-
- def on_next_callback(name: str) -> Callable[..., None]:
- def callback(*args: Any) -> None:
- print(f"{name} received values: {args}")
-
- return callback
-
- callback1.on_next(on_next_callback("Callback1"))
- callback2.on_next(on_next_callback("Callback2"))
-
- async def wait_for_callback(name: str, callback: AsyncCallback[Any], condition: Callable[..., bool]) -> None:
- try:
- result = await callback.wait(condition, timeout=2)
- print(f"{name} wait completed with result: {result}")
- except asyncio.TimeoutError:
- print(f"{name} wait timed out")
-
- # Trigger all callbacks at once
- callback_system.trigger_all("Hello", 42, True)
-
- # Wait for all callbacks with different conditions
- await asyncio.gather(
- wait_for_callback("Callback1", callback1, lambda msg, num, flag: isinstance(msg, str) and num > 0),
- wait_for_callback("Callback2", callback2, lambda msg, num, flag: flag is True),
- )
-
- # Trigger individual callback
- callback_system.trigger("callback2", "World", -10, False)
-
- # Demonstrate timeout
- new_callback = callback_system.register("new_callback")
- new_callback.on_next(on_next_callback("NewCallback"))
- await wait_for_callback("NewCallback", new_callback, lambda msg, num, flag: num > 100)
-
- callback_system.trigger("callback2", "World", 200, False)
-
-
-asyncio.run(main())
diff --git a/exo/tinychat/common.css b/exo/tinychat/common.css
deleted file mode 100644
index e654d6a1..00000000
--- a/exo/tinychat/common.css
+++ /dev/null
@@ -1,130 +0,0 @@
-/* make it responsive */
-@media(min-width: 852px) {
- body {
- font-size: 14px;
- }
-}
-@media(max-width: 852px) {
- body {
- font-size: 12px;
- }
-}
-
-/* resets */
-html, body {
- width: 100%;
- height: 100%;
-}
-
-*::-webkit-scrollbar {
- display: none;
-}
-
-* {
- -ms-overflow-style: none;
- scrollbar-width: none;
-}
-
-* {
- -moz-box-sizing: border-box;
- -webkit-box-sizing: border-box;
- box-sizing: border-box;
-}
-
-/* default */
-body {
- margin: 0;
- background-color: var(--primary-bg-color);
- color: var(--foreground-color);
-}
-
-h1, h2, h3, h4, h5, h6 {
- margin: 0em;
-}
-
-hr {
- width: 92%;
-}
-
-button {
- cursor: pointer;
- border: none;
- background-color: transparent;
-}
-button:hover {
-}
-button:active {
-}
-
-/* components */
-.container {
- margin: 0 auto;
- padding: 1rem;
-}
-
-.centered {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
-}
-
-.centered-w-only {
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
-}
-
-.centered-h-only {
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
-}
-
-.card {
- padding: 0;
-}
-
-.card-header {
- padding: 0.5rem 1rem;
-}
-
-.card-container {
- width: 96vw;
- height: 100%;
- gap: 1rem;
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- justify-content: center;
- align-items: center;
-}
-
-.clean-a {
- text-decoration: underline;
- text-decoration-color: #006fc1;
- text-decoration-thickness: 2px;
- color: inherit;
-}
-
-.hover-underline {
- text-decoration: underline;
- text-decoration-color: #228039;
- text-decoration-thickness: 2px;
- color: inherit;
-}
-
-.flex-horizontal {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
-}
-
-.vertical-separator {
- padding: 0 0.5rem;
-}
-
-[x-cloak] {
- display: none !important;
-}
diff --git a/exo/tinychat/favicon.svg b/exo/tinychat/favicon.svg
deleted file mode 100644
index 420a33e0..00000000
--- a/exo/tinychat/favicon.svg
+++ /dev/null
@@ -1,25 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 150 70" shape-rendering="crispEdges">
- <g id="logo">
- <!-- t -->
- <polygon points="10,40 10,20 0,20 0,10 10,10 10,0 20,0 20,10 30,10 30,20 20,20 20,30 30,30 30,40" />
- <!-- i -->
- <polygon points="40,40 40,20 50,20 50,40" />
- <polygon points="40,10 40,0 50,0 50,10" />
- <!-- n -->
- <polygon points="60,40 60,10 80,10 80,40 90,40 90,20 70,20 70,40" />
- <!-- y -->
- <polygon points="100,50 100,40 130,40 130,10 120,10 120,20 110,20 110,10 100,10 100,30 120,30 120,50" />
- </g>
- <style>
- @media (prefers-color-scheme: dark) {
- #logo {
- fill: #fff;
- }
- }
- @media (prefers-color-scheme: light) {
- #logo {
- fill: #000;
- }
- }
- </style>
-</svg>
diff --git a/exo/tinychat/index.css b/exo/tinychat/index.css
deleted file mode 100644
index 1971795a..00000000
--- a/exo/tinychat/index.css
+++ /dev/null
@@ -1,846 +0,0 @@
-/* define colors */
-:root {
- --primary-color: #fff;
- --secondary-color: #2a2a2a;
- --secondary-color-transparent: #ffffff66;
- --primary-bg-color: #1a1a1a;
- --foreground-color: #f0f0f0;
- --red-color: #a52e4d;
-}
-
-main {
- width: 100%;
- height: 100%;
-
- display: flex;
- flex-direction: column;
-
- place-items: center;
-}
-
-.home {
- width: 100%;
- height: 90%;
- margin-bottom: 10rem;
- padding-top: 2rem;
-}
-
-.title {
- font-size: 3rem;
- margin: 1rem 0;
- margin-top: 3rem;
-}
-
-.histories-container-container {
- width: 100%;
- max-height: 75%;
-
- position: relative;
-}
-
-.histories-container {
- overflow-y: auto;
- overflow-x: hidden;
- width: 100%;
- height: 100%;
-
- display: flex;
- flex-direction: column;
- gap: 1rem;
- align-items: center;
-
- margin: 0;
- padding: 3rem 1rem;
-}
-
-.histories-start {
- height: 3rem;
- width: 100%;
-
- z-index: 999;
- top: 0;
- position: absolute;
-
- background: linear-gradient(
- 180deg,
- var(--primary-bg-color) 0%,
- transparent 100%
- );
-}
-.histories-end {
- height: 3rem;
- width: 100%;
-
- z-index: 999;
- bottom: 0;
- position: absolute;
-
- background: linear-gradient(
- 0deg,
- var(--primary-bg-color) 0%,
- transparent 100%
- );
-}
-
-.history {
- padding: 1rem;
- width: 100%;
- max-width: 40rem;
-
- background-color: var(--secondary-color);
- border-radius: 10px;
- border-left: 2px solid var(--primary-color);
-
- cursor: pointer;
-
- transform: translateX(calc(1px * var(--tx, 0)));
- opacity: var(--opacity, 1);
-}
-.history:hover {
- background-color: var(--secondary-color);
-}
-
-.history-delete-button {
- position: absolute;
- top: 0;
- right: 0;
- padding: 0.5rem;
- margin: 0;
- outline: none;
- border: none;
- background-color: var(--secondary-color);
- color: var(--foreground-color);
- border-radius: 0 0 0 10px;
- cursor: pointer;
- transition: 0.2s;
-}
-.history-delete-button:hover {
- background-color: var(--secondary-color);
- padding: 0.75rem;
-}
-
-.messages {
- overflow-y: auto;
- height: 100%;
- width: 100%;
- max-width: 1200px;
-
- display: flex;
- flex-direction: column;
- gap: 1rem;
- align-items: center;
- padding-top: 4rem;
- padding-bottom: 11rem;
- margin: 0 auto;
-}
-
-.message {
- max-width: 75%;
- padding: 0.5rem 1rem;
- border-radius: 20px;
-}
-
-@media(max-width: 1482px) {
- .messages {
- padding-left: 16px;
- padding-right: 16px;
- }
-}
-
-.message-role-assistant {
- background-color: var(--secondary-color);
- margin-right: auto;
- color: #fff;
-}
-.message-role-user {
- margin-left: auto;
- background-color: var(--primary-color);
- color: #000;
-}
-
-.message-role-user p {
- white-space: pre-wrap;
- word-wrap: break-word;
-}
-
-.download-progress {
- position: fixed;
- bottom: 11rem;
- left: 50%;
- transform: translateX(-50%);
- margin-left: 125px;
- width: 100%;
- max-width: 1200px;
- overflow-y: auto;
- min-height: 350px;
- padding: 2rem;
- z-index: 998;
-}
-.message > pre {
- white-space: pre-wrap;
-}
-
-.progress-bar-container {
- width: 100%;
- background-color: #e0e0e0;
- border-radius: 4px;
- margin: 10px 0;
-}
-.progress-bar {
- height: 20px;
- border-radius: 4px;
- transition: width 0.5s ease-in-out;
-}
-.progress-bar.complete {
- background-color: #4CAF50;
-}
-.progress-bar.in-progress {
- background-color: #2196F3;
-}
-
-.toast {
- width: 100%;
- background-color: #fc2a2a;
- color: #fff;
- text-align: left;
- border-radius: 2px;
- padding: 16px;
- position: fixed;
- z-index: 9999;
- top: 0;
- left: 0;
- right: 0;
- display: flex;
- flex-direction: column;
- white-space: pre-wrap;
- font-family: monospace;
-}
-
-.toast-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- width: 100%;
-}
-
-.toast-error-message {
- flex-grow: 1;
-}
-
-.toast-header-buttons {
- display: flex;
- align-items: center;
- gap: 16px;
- margin-left: 24px;
-}
-
-.toast-expand-button {
- background: none;
- border: none;
- color: white;
- padding: 4px;
- cursor: pointer;
- font-size: 1em;
-}
-
-.toast-close-button {
- background: none;
- border: none;
- color: white;
- padding: 4px;
- cursor: pointer;
- font-size: 1.2em;
- line-height: 1;
-}
-
-.toast-expand-button:hover,
-.toast-close-button:hover {
- opacity: 0.8;
-}
-
-.toast-content {
- margin-top: 10px;
- padding: 10px;
- background-color: rgba(0, 0, 0, 0.2);
- border-radius: 4px;
-}
-
-.hljs {
- width: 100%;
- position: relative;
- border-radius: 10px;
- /* wrap code blocks */
- white-space: pre-wrap;
-}
-/* put clipboard button in the top right corner of the code block */
-.clipboard-button {
- position: absolute;
- top: 0;
- right: 0;
- padding: 0.5rem;
- margin: 0;
- outline: none;
- border: none;
- background-color: var(--secondary-color);
- color: var(--foreground-color);
- border-radius: 0 0 0 10px;
- cursor: pointer;
- transition: 0.2s;
-}
-.clipboard-button:hover {
- background-color: var(--secondary-color);
- padding: 0.75rem;
-}
-
-.input-container {
- position: fixed;
- bottom: 0;
- left: 250px;
- width: calc(100% - 250px);
- max-width: 1200px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- z-index: 999;
- background: linear-gradient(
- 0deg,
- var(--primary-bg-color) 55%,
- transparent 100%
- );
- left: 50%;
- transform: translateX(-50%);
- margin-left: 125px;
-}
-
-.input-performance {
- margin-top: 4rem;
-
- display: flex;
- flex-direction: row;
- gap: 1rem;
-}
-
-.input-performance-point {
- display: flex;
- flex-direction: row;
- place-items: center;
- gap: 0.5rem;
-}
-.input-performance-point > p {
- height: 1rem;
- line-height: normal;
-}
-
-.input {
- width: 90%;
- min-height: 3rem;
- flex-shrink: 0;
-
- display: flex;
- flex-direction: row;
- justify-content: center;
- gap: 0.5rem;
-
- align-items: flex-end;
- margin-bottom: 2rem;
-}
-
-.input-form {
- width: 100%;
- padding: 1rem;
- min-height: 3rem;
- max-height: 8rem;
-
- background-color: var(--secondary-color);
- color: var(--foreground-color);
- border-radius: 10px;
- border: none;
- resize: none;
- outline: none;
-}
-
-.input-button {
- height: 3rem;
- width: 4rem;
-
- background-color: var(--primary-color);
- color: var(--secondary-color);
- border-radius: 10px;
- padding: 0.5rem;
- cursor: pointer;
-}
-.input-button:hover {
- background-color: var(--secondary-color-transparent);
-}
-.input-button:disabled {
- background-color: var(--secondary-color);
- cursor: not-allowed;
-}
-
-/* wrap text */
-p {
- white-space: pre-wrap;
-}
-
-/* fonts */
-.megrim-regular {
- font-family: "Megrim", system-ui;
- font-weight: 400;
- font-style: normal;
-}
-
-.monospace {
- font-family: monospace;
-}
-
-.model-selector {
- display: none;
-}
-
-/* Image upload button styles */
-.image-input-button {
- background-color: var(--secondary-color);
- color: var(--foreground-color);
- border: none;
- border-radius: 50%;
- width: 40px;
- height: 40px;
- font-size: 18px;
- cursor: pointer;
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 10px;
-}
-
-.image-input-button:hover {
- background-color: var(--secondary-color-transparent);
- transform: scale(1.1);
-}
-
-.image-input-button:focus {
- outline: none;
- box-shadow: 0 0 0 3px rgba(var(--secondary-color-rgb), 0.5);
-}
-
-.image-input-button i {
- transition: all 0.3s ease;
-}
-
-.image-input-button:hover i {
- transform: scale(1.2);
-}
-
-/* Hidden file input styles */
-#image-upload {
- display: none;
-}
-
-.image-preview-container {
- position: relative;
- display: inline-block;
- margin-right: 10px;
-}
-
-.image-preview {
- max-width: 100px;
- max-height: 100px;
- object-fit: cover;
- border-radius: 5px;
-}
-
-.remove-image-button {
- position: absolute;
- top: -5px;
- right: -5px;
- background-color: rgba(255, 255, 255, 0.8);
- border: none;
- border-radius: 50%;
- padding: 2px 5px;
- cursor: pointer;
-}
-
-.message > p > img {
- max-width: 100%;
- max-height: 100%;
- object-fit: contain;
-}
-
-.clear-history-button {
- background-color: var(--red-color);
- color: white;
- padding: 10px 20px;
- border-radius: 5px;
- display: flex;
- align-items: center;
- gap: 8px;
- transition: all 0.3s ease;
- margin: 1rem auto;
- border: none;
- cursor: pointer;
-}
-
-.clear-history-button:hover {
- opacity: 0.8;
- transform: scale(1.05);
-}
-
-.clear-history-button i {
- font-size: 14px;
-}
-
-/* Add new sidebar styles */
-.sidebar {
- position: fixed;
- left: 0;
- top: 0;
- bottom: 0;
- width: 250px;
- background-color: var(--secondary-color);
- padding: 20px;
- overflow-y: auto;
- z-index: 1000;
-}
-
-.model-option {
- padding: 12px;
- margin: 8px 0;
- border-radius: 8px;
- background-color: var(--primary-bg-color);
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-.model-option:hover {
- transform: translateX(5px);
-}
-
-.model-option.selected {
- border-left: 3px solid var(--primary-color);
- background-color: var(--secondary-color-transparent);
-}
-
-.model-name {
- font-weight: bold;
- margin-bottom: 4px;
-}
-
-.model-progress {
- font-size: 0.9em;
- color: var(--secondary-color-transparent);
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.model-progress-info {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.model-progress i {
- font-size: 0.9em;
- color: var(--primary-color);
-}
-
-/* Adjust main content to accommodate sidebar */
-main {
- margin-left: 250px;
- width: calc(100% - 250px);
-}
-
-/* Add styles for the back button */
-.back-button {
- position: fixed;
- top: 1rem;
- left: calc(250px + 1rem); /* Sidebar width + padding */
- background-color: var(--secondary-color);
- color: var(--foreground-color);
- padding: 0.5rem 1rem;
- border-radius: 8px;
- border: none;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- z-index: 1000;
- transition: all 0.2s ease;
-}
-
-.back-button:hover {
- transform: translateX(-5px);
- background-color: var(--secondary-color-transparent);
-}
-
-.model-info {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.model-size {
- font-size: 0.8em;
- color: var(--secondary-color-transparent);
- opacity: 0.8;
-}
-
-.model-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 4px;
-}
-
-.model-delete-button {
- background: none;
- border: none;
- color: var(--red-color);
- padding: 4px 8px;
- cursor: pointer;
- transition: all 0.2s ease;
- opacity: 0.7;
-}
-
-.model-delete-button:hover {
- opacity: 1;
- transform: scale(1.1);
-}
-
-.model-option:hover .model-delete-button {
- opacity: 1;
-}
-
-.loading-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
- padding: 20px;
- color: var(--secondary-color-transparent);
-}
-
-.loading-container i {
- font-size: 24px;
-}
-
-.loading-container span {
- font-size: 14px;
-}
-
-/* Add this to your CSS */
-.fa-spin {
- animation: fa-spin 2s infinite linear;
-}
-
-@keyframes fa-spin {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
-}
-
-.model-download-button {
- background: none;
- border: none;
- color: var(--primary-color);
- padding: 4px 8px;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.2s ease;
- display: inline-flex;
- align-items: center;
- gap: 6px;
- background-color: var(--primary-bg-color);
- font-size: 0.9em;
- width: fit-content;
- align-self: flex-start;
-}
-
-.model-download-button:hover {
- transform: scale(1.05);
- background-color: var(--secondary-color-transparent);
-}
-
-.model-download-button i {
- font-size: 0.9em;
-}
-
-.topology-section {
- margin-bottom: 30px;
- padding: 15px;
- background: rgba(255, 255, 255, 0.05);
- border-radius: 8px;
-}
-
-.topology-visualization {
- min-height: 150px;
- position: relative;
- margin-top: 10px;
-}
-
-.topology-loading {
- display: flex;
- align-items: center;
- gap: 10px;
- color: #666;
- font-size: 0.9em;
-}
-
-.topology-node {
- padding: 8px;
- background: rgba(255, 255, 255, 0.05);
- border-radius: 4px;
- margin: 4px 0;
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.node-info {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.9em;
-}
-
-.topology-node .status {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- flex-shrink: 0;
-}
-
-.topology-node .status.active {
- background: #4CAF50;
-}
-
-.topology-node .status.inactive {
- background: #666;
-}
-
-.node-details {
- padding-left: 12px;
- display: flex;
- flex-direction: column;
- gap: 2px;
- font-size: 0.8em;
- opacity: 0.6;
-}
-
-.node-details span {
- display: flex;
- align-items: center;
-}
-
-.peer-connections {
- margin-top: 8px;
- padding-left: 12px;
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.peer-connection {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 0.85em;
- color: #a0a0a0;
-}
-
-.peer-connection i {
- font-size: 0.8em;
- color: #666;
-}
-
-.thinking-block {
- background-color: rgba(255, 255, 255, 0.05);
- border-radius: 8px;
- margin: 8px 0;
- overflow: hidden;
-}
-
-.thinking-header {
- background-color: rgba(255, 255, 255, 0.1);
- padding: 8px 12px;
- font-size: 0.9em;
- color: #a0a0a0;
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.thinking-content {
- padding: 12px;
- white-space: pre-wrap;
-}
-
-@keyframes thinking-spin {
- to { transform: rotate(360deg); }
-}
-
-.thinking-header.thinking::before {
- content: '';
- width: 12px;
- height: 12px;
- border: 2px solid #a0a0a0;
- border-top-color: transparent;
- border-radius: 50%;
- animation: thinking-spin 1s linear infinite;
-}
-
-.model-group {
- margin-bottom: 12px;
-}
-
-.model-group-header,
-.model-subgroup-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 12px;
- background-color: var(--primary-bg-color);
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.2s ease;
- margin-bottom: 8px;
-}
-
-.model-group-header:hover,
-.model-subgroup-header:hover {
- background-color: var(--secondary-color-transparent);
-}
-
-.model-group-content {
- padding-left: 12px;
-}
-
-.model-subgroup {
- margin-bottom: 8px;
-}
-
-.model-subgroup-header {
- font-size: 0.9em;
- background-color: rgba(255, 255, 255, 0.05);
-}
-
-.model-subgroup-content {
- padding-left: 12px;
-}
-
-.group-header-content {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.model-count {
- font-size: 0.8em;
- color: var(--secondary-color-transparent);
- font-family: monospace;
-}
diff --git a/exo/tinychat/index.html b/exo/tinychat/index.html
deleted file mode 100644
index 63e37c8a..00000000
--- a/exo/tinychat/index.html
+++ /dev/null
@@ -1,454 +0,0 @@
-<!DOCTYPE html>
-
-<head>
-<title>tinychat</title>
-<meta content="width=device-width, initial-scale=1" name="viewport"/>
-<link href="favicon.svg" rel="icon" type="image/svg+xml"/>
-<script defer="" src="/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js"></script>
-<script defer="" src="/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
-<script defer="" src="/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
-<script defer="" src="/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js"></script>
-<script defer="" src="/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
-<script src="/static/unpkg.com/dompurify@3.1.5/dist/purify.min.js"></script>
-<script src="/static/unpkg.com/marked@13.0.0/marked.min.js"></script>
-<script src="/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js"></script>
-<script src="/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"></script>
-<script src="/index.js"></script>
-<link href="/static/fonts.googleapis.com" rel="preconnect"/>
-<link crossorigin="" href="/static/fonts.gstatic.com" rel="preconnect"/>
-<link href="/static/fonts.googleapis.com/css2" rel="stylesheet"/>
-<link href="/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css" rel="stylesheet"/>
-<link crossorigin="anonymous" href="/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" referrerpolicy="no-referrer" rel="stylesheet">
-<link href="/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css" rel="stylesheet"/>
-<link href="/index.css" rel="stylesheet"/>
-<link href="/common.css" rel="stylesheet"/>
-<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
-</head>
-<body>
-<main x-data="state" x-init="console.log(endpoint)">
- <div class="sidebar">
- <!-- Add topology section -->
- <div class="topology-section">
- <h2 class="megrim-regular">Network Topology</h2>
- <div class="topology-visualization"
- x-init="initTopology()"
- x-ref="topologyViz">
- <!-- Loading indicator for topology -->
- <div class="topology-loading" x-show="!topology">
- <i class="fas fa-spinner fa-spin"></i>
- <span>Loading topology...</span>
- </div>
- <!-- Topology visualization will be rendered here -->
- </div>
- </div>
-
- <h2 class="megrim-regular" style="margin-bottom: 20px;">Models</h2>
- <div style="display: flex; align-items: center; margin-bottom: 10px;">
- <label style="margin-right: 5px;">
- <input type="checkbox" x-model="showDownloadedOnly" style="margin-right: 5px;">
- Downloaded only
- </label>
- </div>
-
- <!-- Loading indicator -->
- <div class="loading-container" x-show="Object.keys(models).length === 0">
- <i class="fas fa-spinner fa-spin"></i>
- <span>Loading models...</span>
- </div>
-
- <!-- Group models by prefix -->
- <template x-for="[mainPrefix, subGroups] in Object.entries(groupModelsByPrefix(models))" :key="mainPrefix">
- <div class="model-group">
- <div class="model-group-header" @click="toggleGroup(mainPrefix)">
- <div class="group-header-content">
- <span x-text="mainPrefix"></span>
- <span class="model-count" x-text="getGroupCounts(Object.values(subGroups).flatMap(group => Object.values(group)))"></span>
- </div>
- <i class="fas" :class="isGroupExpanded(mainPrefix) ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
- </div>
-
- <div class="model-group-content" x-show="isGroupExpanded(mainPrefix)" x-transition>
- <template x-for="[subPrefix, groupModels] in Object.entries(subGroups)" :key="subPrefix">
- <div class="model-subgroup">
- <div class="model-subgroup-header" @click.stop="toggleGroup(mainPrefix, subPrefix)">
- <div class="group-header-content">
- <span x-text="subPrefix"></span>
- <span class="model-count" x-text="getGroupCounts(groupModels)"></span>
- </div>
- <i class="fas" :class="isGroupExpanded(mainPrefix, subPrefix) ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
- </div>
-
- <div class="model-subgroup-content" x-show="isGroupExpanded(mainPrefix, subPrefix)" x-transition>
- <template x-for="(model, key) in groupModels" :key="key">
- <div class="model-option"
- :class="{ 'selected': cstate.selectedModel === key }"
- @click="cstate.selectedModel = key">
- <div class="model-header">
- <div class="model-name" x-text="model.name"></div>
- <button
- @click.stop="deleteModel(key, model)"
- class="model-delete-button"
- x-show="model.download_percentage > 0">
- <i class="fas fa-trash"></i>
- </button>
- </div>
- <div class="model-info">
- <div class="model-progress">
- <template x-if="model.loading">
- <span><i class="fas fa-spinner fa-spin"></i> Checking download status...</span>
- </template>
- <div class="model-progress-info">
- <template x-if="!model.loading && model.download_percentage != null">
- <span>
- <template x-if="downloadProgress?.some(p =>
- p.repo_id && p.repo_id.toLowerCase().includes(key.toLowerCase()) && !p.isComplete
- )">
- <i class="fas fa-circle-notch fa-spin"></i>
- </template>
- <span x-text="model.downloaded ? 'Downloaded' : `${Math.round(model.download_percentage)}% downloaded`"></span>
- </span>
- </template>
- <template x-if="!model.loading && (model.download_percentage === null || model.download_percentage < 100) && !downloadProgress?.some(p => !p.isComplete)">
- <button
- @click.stop="handleDownload(key)"
- class="model-download-button">
- <i class="fas fa-download"></i>
- <span x-text="(model.download_percentage > 0 && model.download_percentage < 100) ? 'Continue Downloading' : 'Download'"></span>
- </button>
- </template>
- </div>
- </div>
- <template x-if="model.total_size">
- <div class="model-size" x-text="model.total_downloaded ?
- `${formatBytes(model.total_downloaded)} / ${formatBytes(model.total_size)}` :
- formatBytes(model.total_size)">
- </div>
- </template>
- </div>
- </div>
- </template>
- </div>
- </div>
- </template>
- </div>
- </div>
- </template>
- </div>
- <!-- Error Toast -->
- <div x-show="errorMessage !== null" x-transition.opacity class="toast">
- <div class="toast-header">
- <span class="toast-error-message" x-text="errorMessage?.basic || ''"></span>
- <div class="toast-header-buttons">
- <button @click="errorExpanded = !errorExpanded; if (errorTimeout) { clearTimeout(errorTimeout); errorTimeout = null; }"
- class="toast-expand-button"
- x-show="errorMessage?.stack">
- <span x-text="errorExpanded ? 'Hide Details' : 'Show Details'"></span>
- </button>
- <button @click="errorMessage = null; errorExpanded = false;" class="toast-close-button">
- <i class="fas fa-times"></i>
- </button>
- </div>
- </div>
- <div class="toast-content" x-show="errorExpanded" x-transition>
- <span x-text="errorMessage?.stack || ''"></span>
- </div>
- </div>
-
-<div @popstate.window="
- if (home === 2) {
- home = -1;
- cstate = { time: null, messages: [], selectedModel: 'llama-3.1-8b' };
- time_till_first = 0;
- tokens_per_second = 0;
- total_tokens = 0;
- }
- " class="home centered" x-effect="
- $refs.inputForm.focus();
- if (home === 1) setTimeout(() => home = 2, 100);
- if (home === -1) setTimeout(() => home = 0, 100);
- " x-show="home === 0" x-transition="">
-<h1 class="title megrim-regular">tinychat</h1>
-<template x-if="histories.length">
- <button
- @click="if(confirm('Are you sure you want to clear all history?')) clearAllHistory();"
- class="clear-history-button">
- <i class="fas fa-trash"></i> Clear All History
- </button>
-</template>
-<div class="histories-container-container">
-<template x-if="histories.length">
-<div class="histories-start"></div>
-</template>
-<div class="histories-container" x-intersect="
- $el.scrollTo({ top: 0, behavior: 'smooth' });
- ">
-<template x-for="_state in histories.toSorted((a, b) => b.time - a.time)">
-<div @click="
- cstate = _state;
- if (!cstate.selectedModel) cstate.selectedModel = 'llama-3.2-1b';
- home = 2;
- window.history.pushState({}, '', '/');
- " @touchend="
- if (Math.abs($event.changedTouches[0].clientX - otx) > trigger) removeHistory(_state);
- $el.style.setProperty('--tx', 0);
- $el.style.setProperty('--opacity', 1);
- " @touchmove="
- $el.style.setProperty('--tx', $event.changedTouches[0].clientX - otx);
- $el.style.setProperty('--opacity', 1 - (Math.abs($event.changedTouches[0].clientX - otx) / trigger));
- " @touchstart="
- otx = $event.changedTouches[0].clientX;
- " class="history" x-data="{ otx: 0, trigger: 75 }">
-<h3 x-text="new Date(_state.time).toLocaleString()"></h3>
-<p x-text="$truncate(_state.messages[0].content, 76)"></p>
-<!-- delete button -->
-<button @click.stop="removeHistory(_state);" class="history-delete-button">
-<i class="fas fa-trash"></i>
-</button>
-</div>
-</template>
-</div>
-<template x-if="histories.length">
-<div class="histories-end"></div>
-</template>
-</div>
-</div>
-</div>
-<button
- @click="
- home = 0;
- cstate = { time: null, messages: [], selectedModel: cstate.selectedModel };
- time_till_first = 0;
- tokens_per_second = 0;
- total_tokens = 0;
- "
- class="back-button"
- x-show="home === 2">
- <i class="fas fa-arrow-left"></i>
- Back to Chats
-</button>
-<div class="messages"
- x-init="
- $watch('cstate', (value) => {
- $el.innerHTML = '';
-
- value.messages.forEach((msg) => {
- const div = document.createElement('div');
- div.className = `message message-role-${msg.role}`;
-
- try {
- // If there's an embedded generated image
- if (msg.content.includes('![Generated Image]')) {
- const imageUrlMatch = msg.content.match(/\((.*?)\)/);
- if (imageUrlMatch) {
- const imageUrl = imageUrlMatch[1];
- const img = document.createElement('img');
- img.src = imageUrl;
- img.alt = 'Generated Image';
-
- img.onclick = async () => {
- try {
- const response = await fetch(img.src);
- const blob = await response.blob();
- const file = new File([blob], 'image.png', { type: 'image/png' });
- handleImageUpload({ target: { files: [file] } });
- } catch (error) {
- console.error('Error fetching image:', error);
- }
- };
- div.appendChild(img);
- } else {
- // fallback if markdown is malformed
- div.textContent = msg.content;
- }
- } else {
- // Otherwise, transform message text (including streamed think blocks).
- div.innerHTML = transformMessageContent(msg);
- // Render math after content is inserted
- MathJax.typesetPromise([div]);
- }
- } catch (e) {
- console.error('Error rendering message:', e);
- div.textContent = msg.content; // fallback
- }
-
- // Add a clipboard button to code blocks
- const codeBlocks = div.querySelectorAll('.hljs');
- codeBlocks.forEach((codeBlock) => {
- const button = document.createElement('button');
- button.className = 'clipboard-button';
- button.innerHTML = '<i class=\'fas fa-clipboard\'></i>';
-
- button.onclick = () => {
- const range = document.createRange();
- range.setStartBefore(codeBlock);
- range.setEndAfter(codeBlock);
- window.getSelection()?.removeAllRanges();
- window.getSelection()?.addRange(range);
- document.execCommand('copy');
- window.getSelection()?.removeAllRanges();
-
- button.innerHTML = '<i class=\'fas fa-check\'></i>';
- setTimeout(() => {
- button.innerHTML = '<i class=\'fas fa-clipboard\'></i>';
- }, 1000);
- };
-
- codeBlock.appendChild(button);
- });
-
- $el.appendChild(div);
- });
-
- // Scroll to bottom after rendering
- $el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
- });
- "
- x-ref="messages"
- x-show="home === 2"
- x-transition=""
->
-</div>
-
-<!-- Download Progress Section -->
-<template x-if="downloadProgress && downloadProgress.length > 0">
- <div class="download-progress message message-role-assistant">
- <h2>Download Progress</h2>
- <br>
- <template x-for="(progress, index) in downloadProgress" :key="index">
- <div class="download-progress-node">
- <br>
- <h3 x-text="`Download ${index + 1}`"></h3>
- <p><strong>Model:</strong> <span x-text="progress.repo_id + '@' + progress.repo_revision"></span></p>
- <p><strong>Status:</strong> <span x-text="progress.status"></span></p>
- <div class="progress-bar-container">
- <div class="progress-bar"
- :class="progress.isComplete ? 'complete' : 'in-progress'"
- :style="`width: ${progress.percentage}%;`">
- </div>
- </div>
- <template x-if="!progress.isComplete">
- <div>
- <p><strong>Progress:</strong> <span x-text="`${progress.downloaded_bytes_display} / ${progress.total_bytes_display} (${progress.percentage}%)`"></span></p>
- <p><strong>Speed:</strong> <span x-text="progress.overall_speed_display || 'N/A'"></span></p>
- <p><strong>ETA:</strong> <span x-text="progress.overall_eta_display || 'N/A'"></span></p>
- </div>
- </template>
- </div>
- </template>
- </div>
-</template>
-
-
-<div class="input-container">
-<div class="input-performance">
-<span class="input-performance-point">
-<p class="monospace" x-text="models[cstate.selectedModel]?.name || cstate.selectedModel"></p>
-<p class="megrim-regular">-</p>
-</span>
-<span class="input-performance-point">
-<p class="monospace" x-text="(time_till_first / 1000).toFixed(2)"></p>
-<p class="megrim-regular">SEC TO FIRST TOKEN</p>
-</span>
-<span class="input-performance-point">
-<p class="monospace" x-text="tokens_per_second.toFixed(1)"></p>
-<p class="megrim-regular">TOKENS/SEC</p>
-</span>
-<span class="input-performance-point">
-<p class="monospace" x-text="total_tokens"></p>
-<p class="megrim-regular">TOKENS</p>
-</span>
-</div>
-<div class="input">
-<button @click="$refs.imageUpload.click()" class="image-input-button" x-show="cstate.selectedModel === 'llava-1.5-7b-hf' || cstate.selectedModel === 'stable-diffusion-2-1-base'">
-<i class="fas fa-image"></i>
-</button>
-<input @change="$data.handleImageUpload($event)" accept="image/*" id="image-upload" style="display: none;" type="file" x-ref="imageUpload"/>
-<div class="image-preview-container" x-show="imagePreview">
-<img :src="imagePreview" alt="Uploaded Image" class="image-preview"/>
-<button @click="imagePreview = null; imageUrl = null;" class="remove-image-button">
-<i class="fas fa-times"></i>
-</button>
-</div>
-<textarea
- :disabled="generating || (downloadProgress?.length > 0 && downloadProgress.some(p => !p.isComplete))"
- :placeholder="
- generating ? 'Generating...' :
- (downloadProgress?.length > 0 && downloadProgress.some(p => !p.isComplete)) ? 'Download in progress...' :
- 'Say something'
- "
- @input="
- home = (home === 0) ? 1 : home
- if (cstate.messages.length === 0 && $el.value === '') home = -1;
-
- if ($el.value !== '') {
- const messages = [...cstate.messages];
- messages.push({ role: 'user', content: $el.value });
- // updateTotalTokens(messages);
- } else {
- if (cstate.messages.length === 0) total_tokens = 0;
- // else updateTotalTokens(cstate.messages);
- }
- "
- @keydown.enter="await handleEnter($event)"
- @keydown.escape.window="$focus.focus($el)"
- autofocus=""
- class="input-form"
- id="input-form"
- rows="1"
- x-autosize=""
- x-effect="
- console.log(generating);
- if (!generating) $nextTick(() => {
- $el.focus();
- setTimeout(() => $refs.messages.scrollTo({ top: $refs.messages.scrollHeight, behavior: 'smooth' }), 100);
- });
- "
- x-ref="inputForm"></textarea>
-<button
- :disabled="generating || (downloadProgress?.length > 0 && downloadProgress.some(p => !p.isComplete))"
- @click="await handleSend()"
- class="input-button">
- <i :class="generating ? 'fa-spinner fa-spin' : 'fa-paper-plane'" class="fas"></i>
-</button>
-</div>
-</div>
-</main>
-
-<script>
- /**
- * Transform a single message's content into HTML, preserving <think> blocks.
- * Ensure LaTeX expressions are properly delimited for MathJax.
- */
- function transformMessageContent(message) {
- let text = message.content;
- console.log('Processing message content:', text);
-
- // First replace think blocks
- text = text.replace(
- /<think>([\s\S]*?)(?:<\/think>|$)/g,
- (match, body) => {
- console.log('Found think block with content:', body);
- const isComplete = match.includes('</think>');
- const spinnerClass = isComplete ? '' : ' thinking';
- const parsedBody = DOMPurify.sanitize(marked.parse(body));
- return `
-<div class='thinking-block'>
- <div class='thinking-header${spinnerClass}'>Thinking...</div>
- <div class='thinking-content'>${parsedBody}</div>
-</div>`;
- }
- );
-
- // Add backslashes to parentheses and brackets for LaTeX
- text = text
- .replace(/\((?=\s*[\d\\])/g, '\\(') // Add backslash before opening parentheses
- .replace(/\)(?!\w)/g, '\\)') // Add backslash before closing parentheses
- .replace(/\[(?=\s*[\d\\])/g, '\\[') // Add backslash before opening brackets
- .replace(/\](?!\w)/g, '\\]') // Add backslash before closing brackets
- .replace(/\[[\s\n]*\\boxed/g, '\\[\\boxed') // Ensure boxed expressions are properly delimited
- .replace(/\\!/g, '\\\\!'); // Preserve LaTeX spacing commands
-
- return DOMPurify.sanitize(marked.parse(text));
- }
-</script>
-</body>
diff --git a/exo/tinychat/index.js b/exo/tinychat/index.js
deleted file mode 100644
index 5b39e64a..00000000
--- a/exo/tinychat/index.js
+++ /dev/null
@@ -1,889 +0,0 @@
-document.addEventListener("alpine:init", () => {
- Alpine.data("state", () => ({
- // current state
- cstate: {
- time: null,
- messages: [],
- selectedModel: 'llama-3.2-1b',
- },
-
- // historical state
- histories: JSON.parse(localStorage.getItem("histories")) || [],
-
- home: 0,
- generating: false,
- endpoint: `${window.location.origin}/v1`,
-
- // Initialize error message structure
- errorMessage: null,
- errorExpanded: false,
- errorTimeout: null,
-
- // performance tracking
- time_till_first: 0,
- tokens_per_second: 0,
- total_tokens: 0,
-
- // image handling
- imagePreview: null,
-
- // download progress
- downloadProgress: null,
- downloadProgressInterval: null, // To keep track of the polling interval
-
- // Pending message storage
- pendingMessage: null,
-
- modelPoolInterval: null,
-
- // Add models state alongside existing state
- models: {},
-
- // Show only models available locally
- showDownloadedOnly: false,
-
- topology: null,
- topologyInterval: null,
-
- // Add these new properties
- expandedGroups: {},
-
- init() {
- // Clean up any pending messages
- localStorage.removeItem("pendingMessage");
-
- // Get initial model list
- this.fetchInitialModels();
-
- // Start polling for download progress
- this.startDownloadProgressPolling();
-
- // Start model polling with the new pattern
- this.startModelPolling();
- },
-
- async fetchInitialModels() {
- try {
- const response = await fetch(`${window.location.origin}/initial_models`);
- if (response.ok) {
- const initialModels = await response.json();
- this.models = initialModels;
- }
- } catch (error) {
- console.error('Error fetching initial models:', error);
- }
- },
-
- async startModelPolling() {
- while (true) {
- try {
- await this.populateSelector();
- // Wait 15 seconds before next poll
- await new Promise(resolve => setTimeout(resolve, 15000));
- } catch (error) {
- console.error('Model polling error:', error);
- // If there's an error, wait before retrying
- await new Promise(resolve => setTimeout(resolve, 15000));
- }
- }
- },
-
- async populateSelector() {
- return new Promise((resolve, reject) => {
- const evtSource = new EventSource(`${window.location.origin}/modelpool`);
-
- evtSource.onmessage = (event) => {
- if (event.data === "[DONE]") {
- evtSource.close();
- resolve();
- return;
- }
-
- const modelData = JSON.parse(event.data);
- // Update existing model data while preserving other properties
- Object.entries(modelData).forEach(([modelName, data]) => {
- if (this.models[modelName]) {
- this.models[modelName] = {
- ...this.models[modelName],
- ...data,
- loading: false
- };
- }
- });
- };
-
- evtSource.onerror = (error) => {
- console.error('EventSource failed:', error);
- evtSource.close();
- reject(error);
- };
- });
- },
-
- removeHistory(cstate) {
- const index = this.histories.findIndex((state) => {
- return state.time === cstate.time;
- });
- if (index !== -1) {
- this.histories.splice(index, 1);
- localStorage.setItem("histories", JSON.stringify(this.histories));
- }
- },
-
- clearAllHistory() {
- this.histories = [];
- localStorage.setItem("histories", JSON.stringify([]));
- },
-
- // Utility functions
- formatBytes(bytes) {
- if (bytes === 0) return '0 B';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- },
-
- formatDuration(seconds) {
- if (seconds === null || seconds === undefined || isNaN(seconds)) return '';
- const h = Math.floor(seconds / 3600);
- const m = Math.floor((seconds % 3600) / 60);
- const s = Math.floor(seconds % 60);
- if (h > 0) return `${h}h ${m}m ${s}s`;
- if (m > 0) return `${m}m ${s}s`;
- return `${s}s`;
- },
-
- async handleImageUpload(event) {
- const file = event.target.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = (e) => {
- this.imagePreview = e.target.result;
- this.imageUrl = e.target.result; // Store the image URL
- // Add image preview to the chat
- this.cstate.messages.push({
- role: "user",
- content: ``,
- });
- };
- reader.readAsDataURL(file);
- }
- },
-
-
- async handleSend() {
- try {
- const el = document.getElementById("input-form");
- const value = el.value.trim();
- if (!value && !this.imagePreview) return;
-
- if (this.generating) return;
- this.generating = true;
- if (this.home === 0) this.home = 1;
-
- // ensure that going back in history will go back to home
- window.history.pushState({}, "", "/");
-
- // add message to list
- if (value) {
- this.cstate.messages.push({ role: "user", content: value });
- }
-
- // clear textarea
- el.value = "";
- el.style.height = "auto";
- el.style.height = el.scrollHeight + "px";
-
- localStorage.setItem("pendingMessage", value);
- this.processMessage(value);
- } catch (error) {
- console.error('error', error);
- this.setError(error);
- this.generating = false;
- }
- },
-
- async processMessage(value) {
- try {
- // reset performance tracking
- const prefill_start = Date.now();
- let start_time = 0;
- let tokens = 0;
- this.tokens_per_second = 0;
-
- // prepare messages for API request
- let apiMessages = this.cstate.messages.map(msg => {
- if (msg.content.startsWith('![Uploaded Image]')) {
- return {
- role: "user",
- content: [
- {
- type: "image_url",
- image_url: {
- url: this.imageUrl
- }
- },
- {
- type: "text",
- text: value // Use the actual text the user typed
- }
- ]
- };
- } else {
- return {
- role: msg.role,
- content: msg.content
- };
- }
- });
-
- if (this.cstate.selectedModel === "stable-diffusion-2-1-base") {
- // Send a request to the image generation endpoint
- console.log(apiMessages[apiMessages.length - 1].content)
- console.log(this.cstate.selectedModel)
- console.log(this.endpoint)
- const response = await fetch(`${this.endpoint}/image/generations`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- "model": 'stable-diffusion-2-1-base',
- "prompt": apiMessages[apiMessages.length - 1].content,
- "image_url": this.imageUrl
- }),
- });
-
- if (!response.ok) {
- throw new Error("Failed to fetch");
- }
- const reader = response.body.getReader();
- let done = false;
- let gottenFirstChunk = false;
-
- while (!done) {
- const { value, done: readerDone } = await reader.read();
- done = readerDone;
- const decoder = new TextDecoder();
-
- if (value) {
- // Assume non-binary data (text) comes first
- const chunk = decoder.decode(value, { stream: true });
- const parsed = JSON.parse(chunk);
- console.log(parsed)
-
- if (parsed.progress) {
- if (!gottenFirstChunk) {
- this.cstate.messages.push({ role: "assistant", content: "" });
- gottenFirstChunk = true;
- }
- this.cstate.messages[this.cstate.messages.length - 1].content = parsed.progress;
- }
- else if (parsed.images) {
- if (!gottenFirstChunk) {
- this.cstate.messages.push({ role: "assistant", content: "" });
- gottenFirstChunk = true;
- }
- const imageUrl = parsed.images[0].url;
- console.log(imageUrl)
- this.cstate.messages[this.cstate.messages.length - 1].content = `})`;
- }
- }
- }
- }
-
- else{
- const containsImage = apiMessages.some(msg => Array.isArray(msg.content) && msg.content.some(item => item.type === 'image_url'));
- if (containsImage) {
- // Map all messages with string content to object with type text
- apiMessages = apiMessages.map(msg => {
- if (typeof msg.content === 'string') {
- return {
- ...msg,
- content: [
- {
- type: "text",
- text: msg.content
- }
- ]
- };
- }
- return msg;
- });
- }
-
- console.log(apiMessages)
- //start receiving server sent events
- let gottenFirstChunk = false;
- for await (
- const chunk of this.openaiChatCompletion(this.cstate.selectedModel, apiMessages)
- ) {
- if (!gottenFirstChunk) {
- this.cstate.messages.push({ role: "assistant", content: "" });
- gottenFirstChunk = true;
- }
-
- // add chunk to the last message
- this.cstate.messages[this.cstate.messages.length - 1].content += chunk;
-
- // calculate performance tracking
- tokens += 1;
- this.total_tokens += 1;
- if (start_time === 0) {
- start_time = Date.now();
- this.time_till_first = start_time - prefill_start;
- } else {
- const diff = Date.now() - start_time;
- if (diff > 0) {
- this.tokens_per_second = tokens / (diff / 1000);
- }
- }
- }
- }
- // Clean the cstate before adding it to histories
- const cleanedCstate = JSON.parse(JSON.stringify(this.cstate));
- cleanedCstate.messages = cleanedCstate.messages.map(msg => {
- if (Array.isArray(msg.content)) {
- return {
- ...msg,
- content: msg.content.map(item =>
- item.type === 'image_url' ? { type: 'image_url', image_url: { url: '[IMAGE_PLACEHOLDER]' } } : item
- )
- };
- }
- return msg;
- });
-
- // Update the state in histories or add it if it doesn't exist
- const index = this.histories.findIndex((cstate) => cstate.time === cleanedCstate.time);
- cleanedCstate.time = Date.now();
- if (index !== -1) {
- // Update the existing entry
- this.histories[index] = cleanedCstate;
- } else {
- // Add a new entry
- this.histories.push(cleanedCstate);
- }
- console.log(this.histories)
- // update in local storage
- try {
- localStorage.setItem("histories", JSON.stringify(this.histories));
- } catch (error) {
- console.error("Failed to save histories to localStorage:", error);
- }
- } catch (error) {
- console.error('error', error);
- this.setError(error);
- } finally {
- this.generating = false;
- }
- },
-
- async handleEnter(event) {
- // if shift is not pressed
- if (!event.shiftKey) {
- event.preventDefault();
- await this.handleSend();
- }
- },
-
- updateTotalTokens(messages) {
- fetch(`${this.endpoint}/chat/token/encode`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ messages }),
- }).then((response) => response.json()).then((data) => {
- this.total_tokens = data.length;
- }).catch(console.error);
- },
-
- async *openaiChatCompletion(model, messages) {
- const response = await fetch(`${this.endpoint}/chat/completions`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- "model": model,
- "messages": messages,
- "stream": true,
- }),
- });
- if (!response.ok) {
- const errorResBody = await response.json()
- if (errorResBody?.detail) {
- throw new Error(`Failed to fetch completions: ${errorResBody.detail}`);
- } else {
- throw new Error("Failed to fetch completions: Unknown error");
- }
- }
-
- const reader = response.body.pipeThrough(new TextDecoderStream())
- .pipeThrough(new EventSourceParserStream()).getReader();
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- if (value.type === "event") {
- const json = JSON.parse(value.data);
- if (json.choices) {
- const choice = json.choices[0];
- if (choice.finish_reason === "stop") break;
- if (choice.delta.content) yield choice.delta.content;
- }
- }
- }
- },
-
- async fetchDownloadProgress() {
- try {
- const response = await fetch(`${this.endpoint}/download/progress`);
- if (response.ok) {
- const data = await response.json();
- const progressArray = Object.values(data);
- if (progressArray.length > 0) {
- this.downloadProgress = progressArray.map(progress => {
- // Check if download is complete
- if (progress.status === "complete") {
- return {
- ...progress,
- isComplete: true,
- percentage: 100
- };
- } else if (progress.status === "failed") {
- return {
- ...progress,
- isComplete: false,
- errorMessage: "Download failed"
- };
- } else {
- return {
- ...progress,
- isComplete: false,
- downloaded_bytes_display: this.formatBytes(progress.downloaded_bytes),
- total_bytes_display: this.formatBytes(progress.total_bytes),
- overall_speed_display: progress.overall_speed ? this.formatBytes(progress.overall_speed) + '/s' : '',
- overall_eta_display: progress.overall_eta ? this.formatDuration(progress.overall_eta) : '',
- percentage: ((progress.downloaded_bytes / progress.total_bytes) * 100).toFixed(2)
- };
- }
- });
- const allComplete = this.downloadProgress.every(progress => progress.isComplete);
- if (allComplete) {
- // Check for pendingMessage
- const savedMessage = localStorage.getItem("pendingMessage");
- if (savedMessage) {
- // Clear pendingMessage
- localStorage.removeItem("pendingMessage");
- // Call processMessage() with savedMessage
- if (this.lastErrorMessage) {
- await this.processMessage(savedMessage);
- }
- }
- this.lastErrorMessage = null;
- this.downloadProgress = null;
- }
- } else {
- // No ongoing download
- this.downloadProgress = null;
- }
- }
- } catch (error) {
- console.error("Error fetching download progress:", error);
- this.downloadProgress = null;
- }
- },
-
- startDownloadProgressPolling() {
- if (this.downloadProgressInterval) {
- // Already polling
- return;
- }
- this.fetchDownloadProgress(); // Fetch immediately
- this.downloadProgressInterval = setInterval(() => {
- this.fetchDownloadProgress();
- }, 1000); // Poll every second
- },
-
- // Add a helper method to set errors consistently
- setError(error) {
- this.errorMessage = {
- basic: error.message || "An unknown error occurred",
- stack: error.stack || ""
- };
- this.errorExpanded = false;
-
- if (this.errorTimeout) {
- clearTimeout(this.errorTimeout);
- }
-
- if (!this.errorExpanded) {
- this.errorTimeout = setTimeout(() => {
- this.errorMessage = null;
- this.errorExpanded = false;
- }, 30 * 1000);
- }
- },
-
- async deleteModel(modelName, model) {
- const downloadedSize = model.total_downloaded || 0;
- const sizeMessage = downloadedSize > 0 ?
- `This will free up ${this.formatBytes(downloadedSize)} of space.` :
- 'This will remove any partially downloaded files.';
-
- if (!confirm(`Are you sure you want to delete ${model.name}? ${sizeMessage}`)) {
- return;
- }
-
- try {
- const response = await fetch(`${window.location.origin}/models/${modelName}`, {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json'
- }
- });
-
- const data = await response.json();
-
- if (!response.ok) {
- throw new Error(data.detail || 'Failed to delete model');
- }
-
- // Update the model status in the UI
- if (this.models[modelName]) {
- this.models[modelName].downloaded = false;
- this.models[modelName].download_percentage = 0;
- this.models[modelName].total_downloaded = 0;
- }
-
- // If this was the selected model, switch to a different one
- if (this.cstate.selectedModel === modelName) {
- const availableModel = Object.keys(this.models).find(key => this.models[key].downloaded);
- this.cstate.selectedModel = availableModel || 'llama-3.2-1b';
- }
-
- // Show success message
- console.log(`Model deleted successfully from: ${data.path}`);
-
- // Refresh the model list
- await this.populateSelector();
- } catch (error) {
- console.error('Error deleting model:', error);
- this.setError(error.message || 'Failed to delete model');
- }
- },
-
- async handleDownload(modelName) {
- try {
- const response = await fetch(`${window.location.origin}/download`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- model: modelName
- })
- });
-
- const data = await response.json();
-
- if (!response.ok) {
- throw new Error(data.error || 'Failed to start download');
- }
-
- // Update the model's status immediately when download starts
- if (this.models[modelName]) {
- this.models[modelName] = {
- ...this.models[modelName],
- loading: true
- };
- }
-
- } catch (error) {
- console.error('Error starting download:', error);
- this.setError(error);
- }
- },
-
- async fetchTopology() {
- try {
- const response = await fetch(`${this.endpoint}/topology`);
- if (!response.ok) throw new Error('Failed to fetch topology');
- return await response.json();
- } catch (error) {
- console.error('Topology fetch error:', error);
- return null;
- }
- },
-
- initTopology() {
- // Initial fetch
- this.updateTopology();
-
- // Set up periodic updates
- this.topologyInterval = setInterval(() => this.updateTopology(), 5000);
-
- // Cleanup on page unload
- window.addEventListener('beforeunload', () => {
- if (this.topologyInterval) {
- clearInterval(this.topologyInterval);
- }
- });
- },
-
- async updateTopology() {
- const topologyData = await this.fetchTopology();
- if (!topologyData) return;
-
- const vizElement = this.$refs.topologyViz;
- vizElement.innerHTML = ''; // Clear existing visualization
-
- // Helper function to truncate node ID
- const truncateNodeId = (id) => id.substring(0, 8);
-
- // Create nodes from object
- Object.entries(topologyData.nodes).forEach(([nodeId, node]) => {
- const nodeElement = document.createElement('div');
- nodeElement.className = 'topology-node';
-
- // Get peer connections for this node
- const peerConnections = topologyData.peer_graph[nodeId] || [];
- const peerConnectionsHtml = peerConnections.map(peer => `
- <div class="peer-connection">
- <i class="fas fa-arrow-right"></i>
- <span>To ${truncateNodeId(peer.to_id)}: ${peer.description}</span>
- </div>
- `).join('');
-
- nodeElement.innerHTML = `
- <div class="node-info">
- <span class="status ${nodeId === topologyData.active_node_id ? 'active' : 'inactive'}"></span>
- <span>${node.model} [${truncateNodeId(nodeId)}]</span>
- </div>
- <div class="node-details">
- <span>${node.chip}</span>
- <span>${(node.memory / 1024).toFixed(1)}GB RAM</span>
- <span>${node.flops.fp32.toFixed(1)} TF</span>
- </div>
- <div class="peer-connections">
- ${peerConnectionsHtml}
- </div>
- `;
- vizElement.appendChild(nodeElement);
- });
- },
-
- // Add these helper methods
- countDownloadedModels(models) {
- return Object.values(models).filter(model => model.downloaded).length;
- },
-
- getGroupCounts(groupModels) {
- const total = Object.keys(groupModels).length;
- const downloaded = this.countDownloadedModels(groupModels);
- return `[${downloaded}/${total}]`;
- },
-
- // Update the existing groupModelsByPrefix method to include counts
- groupModelsByPrefix(models) {
- const groups = {};
- const filteredModels = this.showDownloadedOnly ?
- Object.fromEntries(Object.entries(models).filter(([, model]) => model.downloaded)) :
- models;
-
- Object.entries(filteredModels).forEach(([key, model]) => {
- const parts = key.split('-');
- const mainPrefix = parts[0].toUpperCase();
-
- let subPrefix;
- if (parts.length === 2) {
- subPrefix = parts[1].toUpperCase();
- } else if (parts.length > 2) {
- subPrefix = parts[1].toUpperCase();
- } else {
- subPrefix = 'OTHER';
- }
-
- if (!groups[mainPrefix]) {
- groups[mainPrefix] = {};
- }
- if (!groups[mainPrefix][subPrefix]) {
- groups[mainPrefix][subPrefix] = {};
- }
- groups[mainPrefix][subPrefix][key] = model;
- });
- return groups;
- },
-
- toggleGroup(prefix, subPrefix = null) {
- const key = subPrefix ? `${prefix}-${subPrefix}` : prefix;
- this.expandedGroups[key] = !this.expandedGroups[key];
- },
-
- isGroupExpanded(prefix, subPrefix = null) {
- const key = subPrefix ? `${prefix}-${subPrefix}` : prefix;
- return this.expandedGroups[key] || false;
- },
- }));
-});
-
-const { markedHighlight } = globalThis.markedHighlight;
-marked.use(markedHighlight({
- langPrefix: "hljs language-",
- highlight(code, lang, _info) {
- const language = hljs.getLanguage(lang) ? lang : "plaintext";
- return hljs.highlight(code, { language }).value;
- },
-}));
-
-// **** eventsource-parser ****
-class EventSourceParserStream extends TransformStream {
- constructor() {
- let parser;
-
- super({
- start(controller) {
- parser = createParser((event) => {
- if (event.type === "event") {
- controller.enqueue(event);
- }
- });
- },
-
- transform(chunk) {
- parser.feed(chunk);
- },
- });
- }
-}
-
-function createParser(onParse) {
- let isFirstChunk;
- let buffer;
- let startingPosition;
- let startingFieldLength;
- let eventId;
- let eventName;
- let data;
- reset();
- return {
- feed,
- reset,
- };
- function reset() {
- isFirstChunk = true;
- buffer = "";
- startingPosition = 0;
- startingFieldLength = -1;
- eventId = void 0;
- eventName = void 0;
- data = "";
- }
- function feed(chunk) {
- buffer = buffer ? buffer + chunk : chunk;
- if (isFirstChunk && hasBom(buffer)) {
- buffer = buffer.slice(BOM.length);
- }
- isFirstChunk = false;
- const length = buffer.length;
- let position = 0;
- let discardTrailingNewline = false;
- while (position < length) {
- if (discardTrailingNewline) {
- if (buffer[position] === "\n") {
- ++position;
- }
- discardTrailingNewline = false;
- }
- let lineLength = -1;
- let fieldLength = startingFieldLength;
- let character;
- for (
- let index = startingPosition;
- lineLength < 0 && index < length;
- ++index
- ) {
- character = buffer[index];
- if (character === ":" && fieldLength < 0) {
- fieldLength = index - position;
- } else if (character === "\r") {
- discardTrailingNewline = true;
- lineLength = index - position;
- } else if (character === "\n") {
- lineLength = index - position;
- }
- }
- if (lineLength < 0) {
- startingPosition = length - position;
- startingFieldLength = fieldLength;
- break;
- } else {
- startingPosition = 0;
- startingFieldLength = -1;
- }
- parseEventStreamLine(buffer, position, fieldLength, lineLength);
- position += lineLength + 1;
- }
- if (position === length) {
- buffer = "";
- } else if (position > 0) {
- buffer = buffer.slice(position);
- }
- }
- function parseEventStreamLine(lineBuffer, index, fieldLength, lineLength) {
- if (lineLength === 0) {
- if (data.length > 0) {
- onParse({
- type: "event",
- id: eventId,
- event: eventName || void 0,
- data: data.slice(0, -1),
- // remove trailing newline
- });
-
- data = "";
- eventId = void 0;
- }
- eventName = void 0;
- return;
- }
- const noValue = fieldLength < 0;
- const field = lineBuffer.slice(
- index,
- index + (noValue ? lineLength : fieldLength),
- );
- let step = 0;
- if (noValue) {
- step = lineLength;
- } else if (lineBuffer[index + fieldLength + 1] === " ") {
- step = fieldLength + 2;
- } else {
- step = fieldLength + 1;
- }
- const position = index + step;
- const valueLength = lineLength - step;
- const value = lineBuffer.slice(position, position + valueLength).toString();
- if (field === "data") {
- data += value ? "".concat(value, "\n") : "\n";
- } else if (field === "event") {
- eventName = value;
- } else if (field === "id" && !value.includes("\0")) {
- eventId = value;
- } else if (field === "retry") {
- const retry = parseInt(value, 10);
- if (!Number.isNaN(retry)) {
- onParse({
- type: "reconnect-interval",
- value: retry,
- });
- }
- }
- }
-}
-
-const BOM = [239, 187, 191];
-function hasBom(buffer) {
- return BOM.every((charCode, index) => buffer.charCodeAt(index) === charCode);
-}
diff --git a/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js b/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js
deleted file mode 100644
index a363545c..00000000
--- a/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{var H=Object.create,v=Object.defineProperty,N=Object.getPrototypeOf,V=Object.prototype.hasOwnProperty,z=Object.getOwnPropertyNames,q=Object.getOwnPropertyDescriptor;var W=n=>v(n,"__esModule",{value:!0});var D=(n,e)=>()=>(e||(e={exports:{}},n(e.exports,e)),e.exports);var F=(n,e,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of z(e))!V.call(n,r)&&r!=="default"&&v(n,r,{get:()=>e[r],enumerable:!(o=q(e,r))||o.enumerable});return n},U=n=>F(W(v(n!=null?H(N(n)):{},"default",n&&n.__esModule&&"default"in n?{get:()=>n.default,enumerable:!0}:{value:n,enumerable:!0})),n);var I=D((E,w)=>{(function(){"use strict";function n(){var e=window,o=document;if("scrollBehavior"in o.documentElement.style&&e.__forceSmoothScrollPolyfill__!==!0)return;var r=e.HTMLElement||e.Element,i=468,f={scroll:e.scroll||e.scrollTo,scrollBy:e.scrollBy,elementScroll:r.prototype.scroll||b,scrollIntoView:r.prototype.scrollIntoView},u=e.performance&&e.performance.now?e.performance.now.bind(e.performance):Date.now;function c(t){var l=["MSIE ","Trident/","Edge/"];return new RegExp(l.join("|")).test(t)}var g=c(e.navigator.userAgent)?1:0;function b(t,l){this.scrollLeft=t,this.scrollTop=l}function M(t){return .5*(1-Math.cos(Math.PI*t))}function m(t){if(t===null||typeof t!="object"||t.behavior===void 0||t.behavior==="auto"||t.behavior==="instant")return!0;if(typeof t=="object"&&t.behavior==="smooth")return!1;throw new TypeError("behavior member of ScrollOptions "+t.behavior+" is not a valid value for enumeration ScrollBehavior.")}function O(t,l){if(l==="Y")return t.clientHeight+g<t.scrollHeight;if(l==="X")return t.clientWidth+g<t.scrollWidth}function T(t,l){var s=e.getComputedStyle(t,null)["overflow"+l];return s==="auto"||s==="scroll"}function R(t){var l=O(t,"Y")&&T(t,"Y"),s=O(t,"X")&&T(t,"X");return l||s}function $(t){for(;t!==o.body&&R(t)===!1;)t=t.parentNode||t.host;return t}function S(t){var l=u(),s,d,p,a=(l-t.startTime)/i;a=a>1?1:a,s=M(a),d=t.startX+(t.x-t.startX)*s,p=t.startY+(t.y-t.startY)*s,t.method.call(t.scrollable,d,p),(d!==t.x||p!==t.y)&&e.requestAnimationFrame(S.bind(e,t))}function h(t,l,s){var d,p,a,y,_=u();t===o.body?(d=e,p=e.scrollX||e.pageXOffset,a=e.scrollY||e.pageYOffset,y=f.scroll):(d=t,p=t.scrollLeft,a=t.scrollTop,y=b),S({scrollable:d,method:y,startTime:_,startX:p,startY:a,x:l,y:s})}e.scroll=e.scrollTo=function(){if(arguments[0]!==void 0){if(m(arguments[0])===!0){f.scroll.call(e,arguments[0].left!==void 0?arguments[0].left:typeof arguments[0]!="object"?arguments[0]:e.scrollX||e.pageXOffset,arguments[0].top!==void 0?arguments[0].top:arguments[1]!==void 0?arguments[1]:e.scrollY||e.pageYOffset);return}h.call(e,o.body,arguments[0].left!==void 0?~~arguments[0].left:e.scrollX||e.pageXOffset,arguments[0].top!==void 0?~~arguments[0].top:e.scrollY||e.pageYOffset)}},e.scrollBy=function(){if(arguments[0]!==void 0){if(m(arguments[0])){f.scrollBy.call(e,arguments[0].left!==void 0?arguments[0].left:typeof arguments[0]!="object"?arguments[0]:0,arguments[0].top!==void 0?arguments[0].top:arguments[1]!==void 0?arguments[1]:0);return}h.call(e,o.body,~~arguments[0].left+(e.scrollX||e.pageXOffset),~~arguments[0].top+(e.scrollY||e.pageYOffset))}},r.prototype.scroll=r.prototype.scrollTo=function(){if(arguments[0]!==void 0){if(m(arguments[0])===!0){if(typeof arguments[0]=="number"&&arguments[1]===void 0)throw new SyntaxError("Value could not be converted");f.elementScroll.call(this,arguments[0].left!==void 0?~~arguments[0].left:typeof arguments[0]!="object"?~~arguments[0]:this.scrollLeft,arguments[0].top!==void 0?~~arguments[0].top:arguments[1]!==void 0?~~arguments[1]:this.scrollTop);return}var t=arguments[0].left,l=arguments[0].top;h.call(this,this,typeof t=="undefined"?this.scrollLeft:~~t,typeof l=="undefined"?this.scrollTop:~~l)}},r.prototype.scrollBy=function(){if(arguments[0]!==void 0){if(m(arguments[0])===!0){f.elementScroll.call(this,arguments[0].left!==void 0?~~arguments[0].left+this.scrollLeft:~~arguments[0]+this.scrollLeft,arguments[0].top!==void 0?~~arguments[0].top+this.scrollTop:~~arguments[1]+this.scrollTop);return}this.scroll({left:~~arguments[0].left+this.scrollLeft,top:~~arguments[0].top+this.scrollTop,behavior:arguments[0].behavior})}},r.prototype.scrollIntoView=function(){if(m(arguments[0])===!0){f.scrollIntoView.call(this,arguments[0]===void 0?!0:arguments[0]);return}var t=$(this),l=t.getBoundingClientRect(),s=this.getBoundingClientRect();t!==o.body?(h.call(this,t,t.scrollLeft+s.left-l.left,t.scrollTop+s.top-l.top),e.getComputedStyle(t).position!=="fixed"&&e.scrollBy({left:l.left,top:l.top,behavior:"smooth"})):e.scrollBy({left:s.left,top:s.top,behavior:"smooth"})}}typeof E=="object"&&typeof w!="undefined"?w.exports={polyfill:n}:n()})()});function j(n){n.magic("range",()=>function(e,o,r=1){typeof o=="undefined"&&(o=e,e=e?1:0);let i=e>o;i&&([e,o]=[o,e]);let f=Array.from({length:(o-e)/r+1},(u,c)=>e+c*r);return i?f.reverse():f})}var Y=U(I());function X(n){Y.default.polyfill(),n.magic("scroll",()=>function(e,o={}){let r=e,i=o.offset?parseInt(o.offset,10):0;if(delete o.offset,typeof e=="string"&&/^[0-9]+?/g.test(e)&&(e=parseInt(e,10)),typeof e=="string"&&(e=document.querySelector(e)),e instanceof Element&&(e=Math.floor(e.getBoundingClientRect().top+window.pageYOffset)),Number.isInteger(e)&&(e={top:e-i,behavior:"smooth"}),typeof e!="object")throw Error("Unsupported $scroll target: ",r);Object.assign(e,o),window.scroll(e)})}function B(n){let e=(o,r)=>{if(r[0].length<=o.length)return o;let i="\u2026";return typeof r[2]!="undefined"&&(i=r[2]),Object.prototype.hasOwnProperty.call(r[1],"ellipsis")&&(i=r[1].ellipsis),o+i};n.magic("truncate",()=>function(...o){return typeof o[0]!="string"||!o[1]?o[0]:typeof o[1]!="object"?e(o[0].slice(0,o[1]),o):Object.prototype.hasOwnProperty.call(o[1],"words")&&o[1].words?e(o[0].split(" ").splice(0,o[1].words).join(" "),o):Object.prototype.hasOwnProperty.call(o[1],"characters")&&o[1].characters?e(o[0].slice(0,o[1].characters),o):o[0]})}function L(n){n.magic("dbg",e=>function(...o){let r=o.map(i=>n.raw(i));console.log(...r)})}function x(n){let e=n.reactive({screensize:window.innerWidth}),o={xs:0,sm:640,md:768,lg:1024,xl:1280,"2xl":1536},r=window.AlpineMagicHelpersConfig&&window.AlpineMagicHelpersConfig.breakpoints?window.AlpineMagicHelpersConfig.breakpoints:o,i;window.addEventListener("resize",()=>{clearTimeout(i),i=setTimeout(()=>{e.screensize=window.innerWidth},150)}),n.magic("screen",f=>u=>{let c=e.screensize;if(Number.isInteger(u))return u<=c;if(r[u]===void 0)throw Error("Undefined $screen property: "+u+". Supported properties: "+Object.keys(r).join(", "));return r[u]<=c})}function P(n){n.magic("interval",()=>function(...e){if(typeof e[0]!="function")return e[0];let o=e[1],r=0,i=!1;typeof e[1]=="object"&&(Object.prototype.hasOwnProperty.call(e[1],"timer")&&(o=e[1].timer),Object.prototype.hasOwnProperty.call(e[1],"delay")&&(r=e[1].delay),Object.prototype.hasOwnProperty.call(e[1],"forceInterval")&&(i=e[1].forceInterval));let f=null,u=!0,c=()=>{let g=u?o+r:o;u=!1,f=setTimeout(()=>{e[0].call(this),i?c():requestAnimationFrame(c)},g)};n.effect(()=>{this.autoIntervalTest==null||this.autoIntervalTest?i?c():requestAnimationFrame(c):clearTimeout(f)})})}function C(n){j(n),X(n),B(n),L(n),x(n),P(n)}document.addEventListener("alpine:initializing",()=>{C(window.Alpine)});})();
diff --git a/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js b/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js
deleted file mode 100644
index 5a2d18be..00000000
--- a/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js
+++ /dev/null
@@ -1,15 +0,0 @@
-(()=>{var _=["input","select","textarea","a[href]","button","[tabindex]:not(slot)","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])',"details>summary:first-of-type","details"],k=_.join(","),K=typeof Element>"u",N=K?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector,B=!K&&Element.prototype.getRootNode?function(i){return i.getRootNode()}:function(i){return i.ownerDocument},V=function(e,t,a){var n=Array.prototype.slice.apply(e.querySelectorAll(k));return t&&N.call(e,k)&&n.unshift(e),n=n.filter(a),n},$=function i(e,t,a){for(var n=[],r=Array.from(e);r.length;){var s=r.shift();if(s.tagName==="SLOT"){var l=s.assignedElements(),m=l.length?l:s.children,p=i(m,!0,a);a.flatten?n.push.apply(n,p):n.push({scope:s,candidates:p})}else{var v=N.call(s,k);v&&a.filter(s)&&(t||!e.includes(s))&&n.push(s);var h=s.shadowRoot||typeof a.getShadowRoot=="function"&&a.getShadowRoot(s),y=!a.shadowRootFilter||a.shadowRootFilter(s);if(h&&y){var w=i(h===!0?s.children:h.children,!0,a);a.flatten?n.push.apply(n,w):n.push({scope:s,candidates:w})}else r.unshift.apply(r,s.children)}}return n},Y=function(e,t){return e.tabIndex<0&&(t||/^(AUDIO|VIDEO|DETAILS)$/.test(e.tagName)||e.isContentEditable)&&isNaN(parseInt(e.getAttribute("tabindex"),10))?0:e.tabIndex},se=function(e,t){return e.tabIndex===t.tabIndex?e.documentOrder-t.documentOrder:e.tabIndex-t.tabIndex},Z=function(e){return e.tagName==="INPUT"},ce=function(e){return Z(e)&&e.type==="hidden"},le=function(e){var t=e.tagName==="DETAILS"&&Array.prototype.slice.apply(e.children).some(function(a){return a.tagName==="SUMMARY"});return t},fe=function(e,t){for(var a=0;a<e.length;a++)if(e[a].checked&&e[a].form===t)return e[a]},de=function(e){if(!e.name)return!0;var t=e.form||B(e),a=function(l){return t.querySelectorAll('input[type="radio"][name="'+l+'"]')},n;if(typeof window<"u"&&typeof window.CSS<"u"&&typeof window.CSS.escape=="function")n=a(window.CSS.escape(e.name));else try{n=a(e.name)}catch(s){return console.error("Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s",s.message),!1}var r=fe(n,e.form);return!r||r===e},be=function(e){return Z(e)&&e.type==="radio"},ve=function(e){return be(e)&&!de(e)},W=function(e){var t=e.getBoundingClientRect(),a=t.width,n=t.height;return a===0&&n===0},he=function(e,t){var a=t.displayCheck,n=t.getShadowRoot;if(getComputedStyle(e).visibility==="hidden")return!0;var r=N.call(e,"details>summary:first-of-type"),s=r?e.parentElement:e;if(N.call(s,"details:not([open]) *"))return!0;var l=B(e).host,m=l?.ownerDocument.contains(l)||e.ownerDocument.contains(e);if(!a||a==="full"){if(typeof n=="function"){for(var p=e;e;){var v=e.parentElement,h=B(e);if(v&&!v.shadowRoot&&n(v)===!0)return W(e);e.assignedSlot?e=e.assignedSlot:!v&&h!==e.ownerDocument?e=h.host:e=v}e=p}if(m)return!e.getClientRects().length}else if(a==="non-zero-area")return W(e);return!1},pe=function(e){if(/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(e.tagName))for(var t=e.parentElement;t;){if(t.tagName==="FIELDSET"&&t.disabled){for(var a=0;a<t.children.length;a++){var n=t.children.item(a);if(n.tagName==="LEGEND")return N.call(t,"fieldset[disabled] *")?!0:!n.contains(e)}return!0}t=t.parentElement}return!1},I=function(e,t){return!(t.disabled||ce(t)||he(t,e)||le(t)||pe(t))},G=function(e,t){return!(ve(t)||Y(t)<0||!I(e,t))},ge=function(e){var t=parseInt(e.getAttribute("tabindex"),10);return!!(isNaN(t)||t>=0)},me=function i(e){var t=[],a=[];return e.forEach(function(n,r){var s=!!n.scope,l=s?n.scope:n,m=Y(l,s),p=s?i(n.candidates):l;m===0?s?t.push.apply(t,p):t.push(l):a.push({documentOrder:r,tabIndex:m,item:n,isScope:s,content:p})}),a.sort(se).reduce(function(n,r){return r.isScope?n.push.apply(n,r.content):n.push(r.content),n},[]).concat(t)},z=function(e,t){t=t||{};var a;return t.getShadowRoot?a=$([e],t.includeContainer,{filter:G.bind(null,t),flatten:!1,getShadowRoot:t.getShadowRoot,shadowRootFilter:ge}):a=V(e,t.includeContainer,G.bind(null,t)),me(a)},x=function(e,t){t=t||{};var a;return t.getShadowRoot?a=$([e],t.includeContainer,{filter:I.bind(null,t),flatten:!0,getShadowRoot:t.getShadowRoot}):a=V(e,t.includeContainer,I.bind(null,t)),a},A=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return N.call(e,k)===!1?!1:G(t,e)},ye=_.concat("iframe").join(","),R=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return N.call(e,ye)===!1?!1:I(t,e)};function Q(i,e){var t=Object.keys(i);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(i);e&&(a=a.filter(function(n){return Object.getOwnPropertyDescriptor(i,n).enumerable})),t.push.apply(t,a)}return t}function X(i){for(var e=1;e<arguments.length;e++){var t=arguments[e]!=null?arguments[e]:{};e%2?Q(Object(t),!0).forEach(function(a){we(i,a,t[a])}):Object.getOwnPropertyDescriptors?Object.defineProperties(i,Object.getOwnPropertyDescriptors(t)):Q(Object(t)).forEach(function(a){Object.defineProperty(i,a,Object.getOwnPropertyDescriptor(t,a))})}return i}function we(i,e,t){return e in i?Object.defineProperty(i,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):i[e]=t,i}var J=function(){var i=[];return{activateTrap:function(t){if(i.length>0){var a=i[i.length-1];a!==t&&a.pause()}var n=i.indexOf(t);n===-1||i.splice(n,1),i.push(t)},deactivateTrap:function(t){var a=i.indexOf(t);a!==-1&&i.splice(a,1),i.length>0&&i[i.length-1].unpause()}}}(),Te=function(e){return e.tagName&&e.tagName.toLowerCase()==="input"&&typeof e.select=="function"},Fe=function(e){return e.key==="Escape"||e.key==="Esc"||e.keyCode===27},Se=function(e){return e.key==="Tab"||e.keyCode===9},ee=function(e){return setTimeout(e,0)},te=function(e,t){var a=-1;return e.every(function(n,r){return t(n)?(a=r,!1):!0}),a},O=function(e){for(var t=arguments.length,a=new Array(t>1?t-1:0),n=1;n<t;n++)a[n-1]=arguments[n];return typeof e=="function"?e.apply(void 0,a):e},L=function(e){return e.target.shadowRoot&&typeof e.composedPath=="function"?e.composedPath()[0]:e.target},re=function(e,t){var a=t?.document||document,n=X({returnFocusOnDeactivate:!0,escapeDeactivates:!0,delayInitialFocus:!0},t),r={containers:[],containerGroups:[],tabbableGroups:[],nodeFocusedBeforeActivation:null,mostRecentlyFocusedNode:null,active:!1,paused:!1,delayInitialFocusTimer:void 0},s,l=function(o,u,c){return o&&o[u]!==void 0?o[u]:n[c||u]},m=function(o){return r.containerGroups.findIndex(function(u){var c=u.container,b=u.tabbableNodes;return c.contains(o)||b.find(function(f){return f===o})})},p=function(o){var u=n[o];if(typeof u=="function"){for(var c=arguments.length,b=new Array(c>1?c-1:0),f=1;f<c;f++)b[f-1]=arguments[f];u=u.apply(void 0,b)}if(u===!0&&(u=void 0),!u){if(u===void 0||u===!1)return u;throw new Error("`".concat(o,"` was specified but was not a node, or did not return a node"))}var g=u;if(typeof u=="string"&&(g=a.querySelector(u),!g))throw new Error("`".concat(o,"` as selector refers to no known node"));return g},v=function(){var o=p("initialFocus");if(o===!1)return!1;if(o===void 0)if(m(a.activeElement)>=0)o=a.activeElement;else{var u=r.tabbableGroups[0],c=u&&u.firstTabbableNode;o=c||p("fallbackFocus")}if(!o)throw new Error("Your focus-trap needs to have at least one focusable element");return o},h=function(){if(r.containerGroups=r.containers.map(function(o){var u=z(o,n.tabbableOptions),c=x(o,n.tabbableOptions);return{container:o,tabbableNodes:u,focusableNodes:c,firstTabbableNode:u.length>0?u[0]:null,lastTabbableNode:u.length>0?u[u.length-1]:null,nextTabbableNode:function(f){var g=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,F=c.findIndex(function(E){return E===f});if(!(F<0))return g?c.slice(F+1).find(function(E){return A(E,n.tabbableOptions)}):c.slice(0,F).reverse().find(function(E){return A(E,n.tabbableOptions)})}}}),r.tabbableGroups=r.containerGroups.filter(function(o){return o.tabbableNodes.length>0}),r.tabbableGroups.length<=0&&!p("fallbackFocus"))throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times")},y=function d(o){if(o!==!1&&o!==a.activeElement){if(!o||!o.focus){d(v());return}o.focus({preventScroll:!!n.preventScroll}),r.mostRecentlyFocusedNode=o,Te(o)&&o.select()}},w=function(o){var u=p("setReturnFocus",o);return u||(u===!1?!1:o)},S=function(o){var u=L(o);if(!(m(u)>=0)){if(O(n.clickOutsideDeactivates,o)){s.deactivate({returnFocus:n.returnFocusOnDeactivate&&!R(u,n.tabbableOptions)});return}O(n.allowOutsideClick,o)||o.preventDefault()}},D=function(o){var u=L(o),c=m(u)>=0;c||u instanceof Document?c&&(r.mostRecentlyFocusedNode=u):(o.stopImmediatePropagation(),y(r.mostRecentlyFocusedNode||v()))},T=function(o){var u=L(o);h();var c=null;if(r.tabbableGroups.length>0){var b=m(u),f=b>=0?r.containerGroups[b]:void 0;if(b<0)o.shiftKey?c=r.tabbableGroups[r.tabbableGroups.length-1].lastTabbableNode:c=r.tabbableGroups[0].firstTabbableNode;else if(o.shiftKey){var g=te(r.tabbableGroups,function(P){var j=P.firstTabbableNode;return u===j});if(g<0&&(f.container===u||R(u,n.tabbableOptions)&&!A(u,n.tabbableOptions)&&!f.nextTabbableNode(u,!1))&&(g=b),g>=0){var F=g===0?r.tabbableGroups.length-1:g-1,E=r.tabbableGroups[F];c=E.lastTabbableNode}}else{var C=te(r.tabbableGroups,function(P){var j=P.lastTabbableNode;return u===j});if(C<0&&(f.container===u||R(u,n.tabbableOptions)&&!A(u,n.tabbableOptions)&&!f.nextTabbableNode(u))&&(C=b),C>=0){var oe=C===r.tabbableGroups.length-1?0:C+1,ue=r.tabbableGroups[oe];c=ue.firstTabbableNode}}}else c=p("fallbackFocus");c&&(o.preventDefault(),y(c))},M=function(o){if(Fe(o)&&O(n.escapeDeactivates,o)!==!1){o.preventDefault(),s.deactivate();return}if(Se(o)){T(o);return}},q=function(o){var u=L(o);m(u)>=0||O(n.clickOutsideDeactivates,o)||O(n.allowOutsideClick,o)||(o.preventDefault(),o.stopImmediatePropagation())},H=function(){if(r.active)return J.activateTrap(s),r.delayInitialFocusTimer=n.delayInitialFocus?ee(function(){y(v())}):y(v()),a.addEventListener("focusin",D,!0),a.addEventListener("mousedown",S,{capture:!0,passive:!1}),a.addEventListener("touchstart",S,{capture:!0,passive:!1}),a.addEventListener("click",q,{capture:!0,passive:!1}),a.addEventListener("keydown",M,{capture:!0,passive:!1}),s},U=function(){if(r.active)return a.removeEventListener("focusin",D,!0),a.removeEventListener("mousedown",S,!0),a.removeEventListener("touchstart",S,!0),a.removeEventListener("click",q,!0),a.removeEventListener("keydown",M,!0),s};return s={get active(){return r.active},get paused(){return r.paused},activate:function(o){if(r.active)return this;var u=l(o,"onActivate"),c=l(o,"onPostActivate"),b=l(o,"checkCanFocusTrap");b||h(),r.active=!0,r.paused=!1,r.nodeFocusedBeforeActivation=a.activeElement,u&&u();var f=function(){b&&h(),H(),c&&c()};return b?(b(r.containers.concat()).then(f,f),this):(f(),this)},deactivate:function(o){if(!r.active)return this;var u=X({onDeactivate:n.onDeactivate,onPostDeactivate:n.onPostDeactivate,checkCanReturnFocus:n.checkCanReturnFocus},o);clearTimeout(r.delayInitialFocusTimer),r.delayInitialFocusTimer=void 0,U(),r.active=!1,r.paused=!1,J.deactivateTrap(s);var c=l(u,"onDeactivate"),b=l(u,"onPostDeactivate"),f=l(u,"checkCanReturnFocus"),g=l(u,"returnFocus","returnFocusOnDeactivate");c&&c();var F=function(){ee(function(){g&&y(w(r.nodeFocusedBeforeActivation)),b&&b()})};return g&&f?(f(w(r.nodeFocusedBeforeActivation)).then(F,F),this):(F(),this)},pause:function(){return r.paused||!r.active?this:(r.paused=!0,U(),this)},unpause:function(){return!r.paused||!r.active?this:(r.paused=!1,h(),H(),this)},updateContainerElements:function(o){var u=[].concat(o).filter(Boolean);return r.containers=u.map(function(c){return typeof c=="string"?a.querySelector(c):c}),r.active&&h(),this}},s.updateContainerElements(e),s};function ne(i){let e,t;window.addEventListener("focusin",()=>{e=t,t=document.activeElement}),i.magic("focus",a=>{let n=a;return{__noscroll:!1,__wrapAround:!1,within(r){return n=r,this},withoutScrolling(){return this.__noscroll=!0,this},noscroll(){return this.__noscroll=!0,this},withWrapAround(){return this.__wrapAround=!0,this},wrap(){return this.withWrapAround()},focusable(r){return R(r)},previouslyFocused(){return e},lastFocused(){return e},focused(){return t},focusables(){return Array.isArray(n)?n:x(n,{displayCheck:"none"})},all(){return this.focusables()},isFirst(r){let s=this.all();return s[0]&&s[0].isSameNode(r)},isLast(r){let s=this.all();return s.length&&s.slice(-1)[0].isSameNode(r)},getFirst(){return this.all()[0]},getLast(){return this.all().slice(-1)[0]},getNext(){let r=this.all(),s=document.activeElement;if(r.indexOf(s)!==-1)return this.__wrapAround&&r.indexOf(s)===r.length-1?r[0]:r[r.indexOf(s)+1]},getPrevious(){let r=this.all(),s=document.activeElement;if(r.indexOf(s)!==-1)return this.__wrapAround&&r.indexOf(s)===0?r.slice(-1)[0]:r[r.indexOf(s)-1]},first(){this.focus(this.getFirst())},last(){this.focus(this.getLast())},next(){this.focus(this.getNext())},previous(){this.focus(this.getPrevious())},prev(){return this.previous()},focus(r){r&&setTimeout(()=>{r.hasAttribute("tabindex")||r.setAttribute("tabindex","0"),r.focus({preventScroll:this.__noscroll})})}}}),i.directive("trap",i.skipDuringClone((a,{expression:n,modifiers:r},{effect:s,evaluateLater:l,cleanup:m})=>{let p=l(n),v=!1,h={escapeDeactivates:!1,allowOutsideClick:!0,fallbackFocus:()=>a};if(r.includes("noautofocus"))h.initialFocus=!1;else{let T=a.querySelector("[autofocus]");T&&(h.initialFocus=T)}let y=re(a,h),w=()=>{},S=()=>{},D=()=>{w(),w=()=>{},S(),S=()=>{},y.deactivate({returnFocus:!r.includes("noreturn")})};s(()=>p(T=>{v!==T&&(T&&!v&&(r.includes("noscroll")&&(S=Ee()),r.includes("inert")&&(w=ae(a)),setTimeout(()=>{y.activate()},15)),!T&&v&&D(),v=!!T)})),m(D)},(a,{expression:n,modifiers:r},{evaluate:s})=>{r.includes("inert")&&s(n)&&ae(a)}))}function ae(i){let e=[];return ie(i,t=>{let a=t.hasAttribute("aria-hidden");t.setAttribute("aria-hidden","true"),e.push(()=>a||t.removeAttribute("aria-hidden"))}),()=>{for(;e.length;)e.pop()()}}function ie(i,e){i.isSameNode(document.body)||!i.parentNode||Array.from(i.parentNode.children).forEach(t=>{t.isSameNode(i)?ie(i.parentNode,e):e(t)})}function Ee(){let i=document.documentElement.style.overflow,e=document.documentElement.style.paddingRight,t=window.innerWidth-document.documentElement.clientWidth;return document.documentElement.style.overflow="hidden",document.documentElement.style.paddingRight=`${t}px`,()=>{document.documentElement.style.overflow=i,document.documentElement.style.paddingRight=e}}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(ne)});})();
-/*! Bundled license information:
-
-tabbable/dist/index.esm.js:
- (*!
- * tabbable 5.3.3
- * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
- *)
-
-focus-trap/dist/focus-trap.esm.js:
- (*!
- * focus-trap 6.9.4
- * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
- *)
-*/
diff --git a/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js b/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js
deleted file mode 100644
index 2342257f..00000000
--- a/exo/tinychat/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(()=>{function o(e){e.directive("intersect",e.skipDuringClone((t,{value:i,expression:l,modifiers:n},{evaluateLater:r,cleanup:c})=>{let s=r(l),a={rootMargin:x(n),threshold:f(n)},u=new IntersectionObserver(d=>{d.forEach(h=>{h.isIntersecting!==(i==="leave")&&(s(),n.includes("once")&&u.disconnect())})},a);u.observe(t),c(()=>{u.disconnect()})}))}function f(e){if(e.includes("full"))return .99;if(e.includes("half"))return .5;if(!e.includes("threshold"))return 0;let t=e[e.indexOf("threshold")+1];return t==="100"?1:t==="0"?0:Number(`.${t}`)}function p(e){let t=e.match(/^(-?[0-9]+)(px|%)?$/);return t?t[1]+(t[2]||"px"):void 0}function x(e){let t="margin",i="0px 0px 0px 0px",l=e.indexOf(t);if(l===-1)return i;let n=[];for(let r=1;r<5;r++)n.push(p(e[l+r]||""));return n=n.filter(r=>r!==void 0),n.length?n.join(" ").trim():i}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(o)});})();
diff --git a/exo/tinychat/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css b/exo/tinychat/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css
deleted file mode 100644
index df4fbc05..00000000
--- a/exo/tinychat/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css
+++ /dev/null
@@ -1,11 +0,0 @@
-/*!
-Pure v3.0.0
-Copyright 2013 Yahoo!
-Licensed under the BSD License.
-https://github.com/pure-css/pure/blob/master/LICENSE
-*/
-/*!
-normalize.css v | MIT License | https://necolas.github.io/normalize.css/
-Copyright (c) Nicolas Gallagher and Jonathan Neal
-*/
-/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}
\ No newline at end of file
diff --git a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css b/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css
deleted file mode 100644
index 45072b30..00000000
--- a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css
+++ /dev/null
@@ -1,9 +0,0 @@
-/*!
- * Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
- * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- * Copyright 2024 Fonticons, Inc.
- */
-.fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-classic,.fa-regular,.fa-sharp,.fa-solid,.fab,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-classic,.fa-regular,.fa-solid,.far,.fas{font-family:"Font Awesome 6 Free"}.fa-brands,.fab{font-family:"Font Awesome 6 Brands"}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-transition-delay:0s;transition-delay:0s;-webkit-transition-duration:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@-webkit-keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,0));transform:rotate(var(--fa-rotate-angle,0))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)}
-
-.fa-0:before{content:"\30"}.fa-1:before{content:"\31"}.fa-2:before{content:"\32"}.fa-3:before{content:"\33"}.fa-4:before{content:"\34"}.fa-5:before{content:"\35"}.fa-6:before{content:"\36"}.fa-7:before{content:"\37"}.fa-8:before{content:"\38"}.fa-9:before{content:"\39"}.fa-fill-drip:before{content:"\f576"}.fa-arrows-to-circle:before{content:"\e4bd"}.fa-chevron-circle-right:before,.fa-circle-chevron-right:before{content:"\f138"}.fa-at:before{content:"\40"}.fa-trash-alt:before,.fa-trash-can:before{content:"\f2ed"}.fa-text-height:before{content:"\f034"}.fa-user-times:before,.fa-user-xmark:before{content:"\f235"}.fa-stethoscope:before{content:"\f0f1"}.fa-comment-alt:before,.fa-message:before{content:"\f27a"}.fa-info:before{content:"\f129"}.fa-compress-alt:before,.fa-down-left-and-up-right-to-center:before{content:"\f422"}.fa-explosion:before{content:"\e4e9"}.fa-file-alt:before,.fa-file-lines:before,.fa-file-text:before{content:"\f15c"}.fa-wave-square:before{content:"\f83e"}.fa-ring:before{content:"\f70b"}.fa-building-un:before{content:"\e4d9"}.fa-dice-three:before{content:"\f527"}.fa-calendar-alt:before,.fa-calendar-days:before{content:"\f073"}.fa-anchor-circle-check:before{content:"\e4aa"}.fa-building-circle-arrow-right:before{content:"\e4d1"}.fa-volleyball-ball:before,.fa-volleyball:before{content:"\f45f"}.fa-arrows-up-to-line:before{content:"\e4c2"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-circle-minus:before,.fa-minus-circle:before{content:"\f056"}.fa-door-open:before{content:"\f52b"}.fa-right-from-bracket:before,.fa-sign-out-alt:before{content:"\f2f5"}.fa-atom:before{content:"\f5d2"}.fa-soap:before{content:"\e06e"}.fa-heart-music-camera-bolt:before,.fa-icons:before{content:"\f86d"}.fa-microphone-alt-slash:before,.fa-microphone-lines-slash:before{content:"\f539"}.fa-bridge-circle-check:before{content:"\e4c9"}.fa-pump-medical:before{content:"\e06a"}.fa-fingerprint:before{content:"\f577"}.fa-hand-point-right:before{content:"\f0a4"}.fa-magnifying-glass-location:before,.fa-search-location:before{content:"\f689"}.fa-forward-step:before,.fa-step-forward:before{content:"\f051"}.fa-face-smile-beam:before,.fa-smile-beam:before{content:"\f5b8"}.fa-flag-checkered:before{content:"\f11e"}.fa-football-ball:before,.fa-football:before{content:"\f44e"}.fa-school-circle-exclamation:before{content:"\e56c"}.fa-crop:before{content:"\f125"}.fa-angle-double-down:before,.fa-angles-down:before{content:"\f103"}.fa-users-rectangle:before{content:"\e594"}.fa-people-roof:before{content:"\e537"}.fa-people-line:before{content:"\e534"}.fa-beer-mug-empty:before,.fa-beer:before{content:"\f0fc"}.fa-diagram-predecessor:before{content:"\e477"}.fa-arrow-up-long:before,.fa-long-arrow-up:before{content:"\f176"}.fa-burn:before,.fa-fire-flame-simple:before{content:"\f46a"}.fa-male:before,.fa-person:before{content:"\f183"}.fa-laptop:before{content:"\f109"}.fa-file-csv:before{content:"\f6dd"}.fa-menorah:before{content:"\f676"}.fa-truck-plane:before{content:"\e58f"}.fa-record-vinyl:before{content:"\f8d9"}.fa-face-grin-stars:before,.fa-grin-stars:before{content:"\f587"}.fa-bong:before{content:"\f55c"}.fa-pastafarianism:before,.fa-spaghetti-monster-flying:before{content:"\f67b"}.fa-arrow-down-up-across-line:before{content:"\e4af"}.fa-spoon:before,.fa-utensil-spoon:before{content:"\f2e5"}.fa-jar-wheat:before{content:"\e517"}.fa-envelopes-bulk:before,.fa-mail-bulk:before{content:"\f674"}.fa-file-circle-exclamation:before{content:"\e4eb"}.fa-circle-h:before,.fa-hospital-symbol:before{content:"\f47e"}.fa-pager:before{content:"\f815"}.fa-address-book:before,.fa-contact-book:before{content:"\f2b9"}.fa-strikethrough:before{content:"\f0cc"}.fa-k:before{content:"\4b"}.fa-landmark-flag:before{content:"\e51c"}.fa-pencil-alt:before,.fa-pencil:before{content:"\f303"}.fa-backward:before{content:"\f04a"}.fa-caret-right:before{content:"\f0da"}.fa-comments:before{content:"\f086"}.fa-file-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-code-pull-request:before{content:"\e13c"}.fa-clipboard-list:before{content:"\f46d"}.fa-truck-loading:before,.fa-truck-ramp-box:before{content:"\f4de"}.fa-user-check:before{content:"\f4fc"}.fa-vial-virus:before{content:"\e597"}.fa-sheet-plastic:before{content:"\e571"}.fa-blog:before{content:"\f781"}.fa-user-ninja:before{content:"\f504"}.fa-person-arrow-up-from-line:before{content:"\e539"}.fa-scroll-torah:before,.fa-torah:before{content:"\f6a0"}.fa-broom-ball:before,.fa-quidditch-broom-ball:before,.fa-quidditch:before{content:"\f458"}.fa-toggle-off:before{content:"\f204"}.fa-archive:before,.fa-box-archive:before{content:"\f187"}.fa-person-drowning:before{content:"\e545"}.fa-arrow-down-9-1:before,.fa-sort-numeric-desc:before,.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-face-grin-tongue-squint:before,.fa-grin-tongue-squint:before{content:"\f58a"}.fa-spray-can:before{content:"\f5bd"}.fa-truck-monster:before{content:"\f63b"}.fa-w:before{content:"\57"}.fa-earth-africa:before,.fa-globe-africa:before{content:"\f57c"}.fa-rainbow:before{content:"\f75b"}.fa-circle-notch:before{content:"\f1ce"}.fa-tablet-alt:before,.fa-tablet-screen-button:before{content:"\f3fa"}.fa-paw:before{content:"\f1b0"}.fa-cloud:before{content:"\f0c2"}.fa-trowel-bricks:before{content:"\e58a"}.fa-face-flushed:before,.fa-flushed:before{content:"\f579"}.fa-hospital-user:before{content:"\f80d"}.fa-tent-arrow-left-right:before{content:"\e57f"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-binoculars:before{content:"\f1e5"}.fa-microphone-slash:before{content:"\f131"}.fa-box-tissue:before{content:"\e05b"}.fa-motorcycle:before{content:"\f21c"}.fa-bell-concierge:before,.fa-concierge-bell:before{content:"\f562"}.fa-pen-ruler:before,.fa-pencil-ruler:before{content:"\f5ae"}.fa-people-arrows-left-right:before,.fa-people-arrows:before{content:"\e068"}.fa-mars-and-venus-burst:before{content:"\e523"}.fa-caret-square-right:before,.fa-square-caret-right:before{content:"\f152"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-sun-plant-wilt:before{content:"\e57a"}.fa-toilets-portable:before{content:"\e584"}.fa-hockey-puck:before{content:"\f453"}.fa-table:before{content:"\f0ce"}.fa-magnifying-glass-arrow-right:before{content:"\e521"}.fa-digital-tachograph:before,.fa-tachograph-digital:before{content:"\f566"}.fa-users-slash:before{content:"\e073"}.fa-clover:before{content:"\e139"}.fa-mail-reply:before,.fa-reply:before{content:"\f3e5"}.fa-star-and-crescent:before{content:"\f699"}.fa-house-fire:before{content:"\e50c"}.fa-minus-square:before,.fa-square-minus:before{content:"\f146"}.fa-helicopter:before{content:"\f533"}.fa-compass:before{content:"\f14e"}.fa-caret-square-down:before,.fa-square-caret-down:before{content:"\f150"}.fa-file-circle-question:before{content:"\e4ef"}.fa-laptop-code:before{content:"\f5fc"}.fa-swatchbook:before{content:"\f5c3"}.fa-prescription-bottle:before{content:"\f485"}.fa-bars:before,.fa-navicon:before{content:"\f0c9"}.fa-people-group:before{content:"\e533"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-heart-broken:before,.fa-heart-crack:before{content:"\f7a9"}.fa-external-link-square-alt:before,.fa-square-up-right:before{content:"\f360"}.fa-face-kiss-beam:before,.fa-kiss-beam:before{content:"\f597"}.fa-film:before{content:"\f008"}.fa-ruler-horizontal:before{content:"\f547"}.fa-people-robbery:before{content:"\e536"}.fa-lightbulb:before{content:"\f0eb"}.fa-caret-left:before{content:"\f0d9"}.fa-circle-exclamation:before,.fa-exclamation-circle:before{content:"\f06a"}.fa-school-circle-xmark:before{content:"\e56d"}.fa-arrow-right-from-bracket:before,.fa-sign-out:before{content:"\f08b"}.fa-chevron-circle-down:before,.fa-circle-chevron-down:before{content:"\f13a"}.fa-unlock-alt:before,.fa-unlock-keyhole:before{content:"\f13e"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-headphones-alt:before,.fa-headphones-simple:before{content:"\f58f"}.fa-sitemap:before{content:"\f0e8"}.fa-circle-dollar-to-slot:before,.fa-donate:before{content:"\f4b9"}.fa-memory:before{content:"\f538"}.fa-road-spikes:before{content:"\e568"}.fa-fire-burner:before{content:"\e4f1"}.fa-flag:before{content:"\f024"}.fa-hanukiah:before{content:"\f6e6"}.fa-feather:before{content:"\f52d"}.fa-volume-down:before,.fa-volume-low:before{content:"\f027"}.fa-comment-slash:before{content:"\f4b3"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-compress:before{content:"\f066"}.fa-wheat-alt:before,.fa-wheat-awn:before{content:"\e2cd"}.fa-ankh:before{content:"\f644"}.fa-hands-holding-child:before{content:"\e4fa"}.fa-asterisk:before{content:"\2a"}.fa-check-square:before,.fa-square-check:before{content:"\f14a"}.fa-peseta-sign:before{content:"\e221"}.fa-header:before,.fa-heading:before{content:"\f1dc"}.fa-ghost:before{content:"\f6e2"}.fa-list-squares:before,.fa-list:before{content:"\f03a"}.fa-phone-square-alt:before,.fa-square-phone-flip:before{content:"\f87b"}.fa-cart-plus:before{content:"\f217"}.fa-gamepad:before{content:"\f11b"}.fa-circle-dot:before,.fa-dot-circle:before{content:"\f192"}.fa-dizzy:before,.fa-face-dizzy:before{content:"\f567"}.fa-egg:before{content:"\f7fb"}.fa-house-medical-circle-xmark:before{content:"\e513"}.fa-campground:before{content:"\f6bb"}.fa-folder-plus:before{content:"\f65e"}.fa-futbol-ball:before,.fa-futbol:before,.fa-soccer-ball:before{content:"\f1e3"}.fa-paint-brush:before,.fa-paintbrush:before{content:"\f1fc"}.fa-lock:before{content:"\f023"}.fa-gas-pump:before{content:"\f52f"}.fa-hot-tub-person:before,.fa-hot-tub:before{content:"\f593"}.fa-map-location:before,.fa-map-marked:before{content:"\f59f"}.fa-house-flood-water:before{content:"\e50e"}.fa-tree:before{content:"\f1bb"}.fa-bridge-lock:before{content:"\e4cc"}.fa-sack-dollar:before{content:"\f81d"}.fa-edit:before,.fa-pen-to-square:before{content:"\f044"}.fa-car-side:before{content:"\f5e4"}.fa-share-alt:before,.fa-share-nodes:before{content:"\f1e0"}.fa-heart-circle-minus:before{content:"\e4ff"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-microscope:before{content:"\f610"}.fa-sink:before{content:"\e06d"}.fa-bag-shopping:before,.fa-shopping-bag:before{content:"\f290"}.fa-arrow-down-z-a:before,.fa-sort-alpha-desc:before,.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-mitten:before{content:"\f7b5"}.fa-person-rays:before{content:"\e54d"}.fa-users:before{content:"\f0c0"}.fa-eye-slash:before{content:"\f070"}.fa-flask-vial:before{content:"\e4f3"}.fa-hand-paper:before,.fa-hand:before{content:"\f256"}.fa-om:before{content:"\f679"}.fa-worm:before{content:"\e599"}.fa-house-circle-xmark:before{content:"\e50b"}.fa-plug:before{content:"\f1e6"}.fa-chevron-up:before{content:"\f077"}.fa-hand-spock:before{content:"\f259"}.fa-stopwatch:before{content:"\f2f2"}.fa-face-kiss:before,.fa-kiss:before{content:"\f596"}.fa-bridge-circle-xmark:before{content:"\e4cb"}.fa-face-grin-tongue:before,.fa-grin-tongue:before{content:"\f589"}.fa-chess-bishop:before{content:"\f43a"}.fa-face-grin-wink:before,.fa-grin-wink:before{content:"\f58c"}.fa-deaf:before,.fa-deafness:before,.fa-ear-deaf:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-road-circle-check:before{content:"\e564"}.fa-dice-five:before{content:"\f523"}.fa-rss-square:before,.fa-square-rss:before{content:"\f143"}.fa-land-mine-on:before{content:"\e51b"}.fa-i-cursor:before{content:"\f246"}.fa-stamp:before{content:"\f5bf"}.fa-stairs:before{content:"\e289"}.fa-i:before{content:"\49"}.fa-hryvnia-sign:before,.fa-hryvnia:before{content:"\f6f2"}.fa-pills:before{content:"\f484"}.fa-face-grin-wide:before,.fa-grin-alt:before{content:"\f581"}.fa-tooth:before{content:"\f5c9"}.fa-v:before{content:"\56"}.fa-bangladeshi-taka-sign:before{content:"\e2e6"}.fa-bicycle:before{content:"\f206"}.fa-rod-asclepius:before,.fa-rod-snake:before,.fa-staff-aesculapius:before,.fa-staff-snake:before{content:"\e579"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-ambulance:before,.fa-truck-medical:before{content:"\f0f9"}.fa-wheat-awn-circle-exclamation:before{content:"\e598"}.fa-snowman:before{content:"\f7d0"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-road-barrier:before{content:"\e562"}.fa-school:before{content:"\f549"}.fa-igloo:before{content:"\f7ae"}.fa-joint:before{content:"\f595"}.fa-angle-right:before{content:"\f105"}.fa-horse:before{content:"\f6f0"}.fa-q:before{content:"\51"}.fa-g:before{content:"\47"}.fa-notes-medical:before{content:"\f481"}.fa-temperature-2:before,.fa-temperature-half:before,.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-dong-sign:before{content:"\e169"}.fa-capsules:before{content:"\f46b"}.fa-poo-bolt:before,.fa-poo-storm:before{content:"\f75a"}.fa-face-frown-open:before,.fa-frown-open:before{content:"\f57a"}.fa-hand-point-up:before{content:"\f0a6"}.fa-money-bill:before{content:"\f0d6"}.fa-bookmark:before{content:"\f02e"}.fa-align-justify:before{content:"\f039"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-helmet-un:before{content:"\e503"}.fa-bullseye:before{content:"\f140"}.fa-bacon:before{content:"\f7e5"}.fa-hand-point-down:before{content:"\f0a7"}.fa-arrow-up-from-bracket:before{content:"\e09a"}.fa-folder-blank:before,.fa-folder:before{content:"\f07b"}.fa-file-medical-alt:before,.fa-file-waveform:before{content:"\f478"}.fa-radiation:before{content:"\f7b9"}.fa-chart-simple:before{content:"\e473"}.fa-mars-stroke:before{content:"\f229"}.fa-vial:before{content:"\f492"}.fa-dashboard:before,.fa-gauge-med:before,.fa-gauge:before,.fa-tachometer-alt-average:before{content:"\f624"}.fa-magic-wand-sparkles:before,.fa-wand-magic-sparkles:before{content:"\e2ca"}.fa-e:before{content:"\45"}.fa-pen-alt:before,.fa-pen-clip:before{content:"\f305"}.fa-bridge-circle-exclamation:before{content:"\e4ca"}.fa-user:before{content:"\f007"}.fa-school-circle-check:before{content:"\e56b"}.fa-dumpster:before{content:"\f793"}.fa-shuttle-van:before,.fa-van-shuttle:before{content:"\f5b6"}.fa-building-user:before{content:"\e4da"}.fa-caret-square-left:before,.fa-square-caret-left:before{content:"\f191"}.fa-highlighter:before{content:"\f591"}.fa-key:before{content:"\f084"}.fa-bullhorn:before{content:"\f0a1"}.fa-globe:before{content:"\f0ac"}.fa-synagogue:before{content:"\f69b"}.fa-person-half-dress:before{content:"\e548"}.fa-road-bridge:before{content:"\e563"}.fa-location-arrow:before{content:"\f124"}.fa-c:before{content:"\43"}.fa-tablet-button:before{content:"\f10a"}.fa-building-lock:before{content:"\e4d6"}.fa-pizza-slice:before{content:"\f818"}.fa-money-bill-wave:before{content:"\f53a"}.fa-area-chart:before,.fa-chart-area:before{content:"\f1fe"}.fa-house-flag:before{content:"\e50d"}.fa-person-circle-minus:before{content:"\e540"}.fa-ban:before,.fa-cancel:before{content:"\f05e"}.fa-camera-rotate:before{content:"\e0d8"}.fa-air-freshener:before,.fa-spray-can-sparkles:before{content:"\f5d0"}.fa-star:before{content:"\f005"}.fa-repeat:before{content:"\f363"}.fa-cross:before{content:"\f654"}.fa-box:before{content:"\f466"}.fa-venus-mars:before{content:"\f228"}.fa-arrow-pointer:before,.fa-mouse-pointer:before{content:"\f245"}.fa-expand-arrows-alt:before,.fa-maximize:before{content:"\f31e"}.fa-charging-station:before{content:"\f5e7"}.fa-shapes:before,.fa-triangle-circle-square:before{content:"\f61f"}.fa-random:before,.fa-shuffle:before{content:"\f074"}.fa-person-running:before,.fa-running:before{content:"\f70c"}.fa-mobile-retro:before{content:"\e527"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-spider:before{content:"\f717"}.fa-hands-bound:before{content:"\e4f9"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-plane-circle-exclamation:before{content:"\e556"}.fa-x-ray:before{content:"\f497"}.fa-spell-check:before{content:"\f891"}.fa-slash:before{content:"\f715"}.fa-computer-mouse:before,.fa-mouse:before{content:"\f8cc"}.fa-arrow-right-to-bracket:before,.fa-sign-in:before{content:"\f090"}.fa-shop-slash:before,.fa-store-alt-slash:before{content:"\e070"}.fa-server:before{content:"\f233"}.fa-virus-covid-slash:before{content:"\e4a9"}.fa-shop-lock:before{content:"\e4a5"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-blender-phone:before{content:"\f6b6"}.fa-building-wheat:before{content:"\e4db"}.fa-person-breastfeeding:before{content:"\e53a"}.fa-right-to-bracket:before,.fa-sign-in-alt:before{content:"\f2f6"}.fa-venus:before{content:"\f221"}.fa-passport:before{content:"\f5ab"}.fa-heart-pulse:before,.fa-heartbeat:before{content:"\f21e"}.fa-people-carry-box:before,.fa-people-carry:before{content:"\f4ce"}.fa-temperature-high:before{content:"\f769"}.fa-microchip:before{content:"\f2db"}.fa-crown:before{content:"\f521"}.fa-weight-hanging:before{content:"\f5cd"}.fa-xmarks-lines:before{content:"\e59a"}.fa-file-prescription:before{content:"\f572"}.fa-weight-scale:before,.fa-weight:before{content:"\f496"}.fa-user-friends:before,.fa-user-group:before{content:"\f500"}.fa-arrow-up-a-z:before,.fa-sort-alpha-up:before{content:"\f15e"}.fa-chess-knight:before{content:"\f441"}.fa-face-laugh-squint:before,.fa-laugh-squint:before{content:"\f59b"}.fa-wheelchair:before{content:"\f193"}.fa-arrow-circle-up:before,.fa-circle-arrow-up:before{content:"\f0aa"}.fa-toggle-on:before{content:"\f205"}.fa-person-walking:before,.fa-walking:before{content:"\f554"}.fa-l:before{content:"\4c"}.fa-fire:before{content:"\f06d"}.fa-bed-pulse:before,.fa-procedures:before{content:"\f487"}.fa-shuttle-space:before,.fa-space-shuttle:before{content:"\f197"}.fa-face-laugh:before,.fa-laugh:before{content:"\f599"}.fa-folder-open:before{content:"\f07c"}.fa-heart-circle-plus:before{content:"\e500"}.fa-code-fork:before{content:"\e13b"}.fa-city:before{content:"\f64f"}.fa-microphone-alt:before,.fa-microphone-lines:before{content:"\f3c9"}.fa-pepper-hot:before{content:"\f816"}.fa-unlock:before{content:"\f09c"}.fa-colon-sign:before{content:"\e140"}.fa-headset:before{content:"\f590"}.fa-store-slash:before{content:"\e071"}.fa-road-circle-xmark:before{content:"\e566"}.fa-user-minus:before{content:"\f503"}.fa-mars-stroke-up:before,.fa-mars-stroke-v:before{content:"\f22a"}.fa-champagne-glasses:before,.fa-glass-cheers:before{content:"\f79f"}.fa-clipboard:before{content:"\f328"}.fa-house-circle-exclamation:before{content:"\e50a"}.fa-file-arrow-up:before,.fa-file-upload:before{content:"\f574"}.fa-wifi-3:before,.fa-wifi-strong:before,.fa-wifi:before{content:"\f1eb"}.fa-bath:before,.fa-bathtub:before{content:"\f2cd"}.fa-underline:before{content:"\f0cd"}.fa-user-edit:before,.fa-user-pen:before{content:"\f4ff"}.fa-signature:before{content:"\f5b7"}.fa-stroopwafel:before{content:"\f551"}.fa-bold:before{content:"\f032"}.fa-anchor-lock:before{content:"\e4ad"}.fa-building-ngo:before{content:"\e4d7"}.fa-manat-sign:before{content:"\e1d5"}.fa-not-equal:before{content:"\f53e"}.fa-border-style:before,.fa-border-top-left:before{content:"\f853"}.fa-map-location-dot:before,.fa-map-marked-alt:before{content:"\f5a0"}.fa-jedi:before{content:"\f669"}.fa-poll:before,.fa-square-poll-vertical:before{content:"\f681"}.fa-mug-hot:before{content:"\f7b6"}.fa-battery-car:before,.fa-car-battery:before{content:"\f5df"}.fa-gift:before{content:"\f06b"}.fa-dice-two:before{content:"\f528"}.fa-chess-queen:before{content:"\f445"}.fa-glasses:before{content:"\f530"}.fa-chess-board:before{content:"\f43c"}.fa-building-circle-check:before{content:"\e4d2"}.fa-person-chalkboard:before{content:"\e53d"}.fa-mars-stroke-h:before,.fa-mars-stroke-right:before{content:"\f22b"}.fa-hand-back-fist:before,.fa-hand-rock:before{content:"\f255"}.fa-caret-square-up:before,.fa-square-caret-up:before{content:"\f151"}.fa-cloud-showers-water:before{content:"\e4e4"}.fa-bar-chart:before,.fa-chart-bar:before{content:"\f080"}.fa-hands-bubbles:before,.fa-hands-wash:before{content:"\e05e"}.fa-less-than-equal:before{content:"\f537"}.fa-train:before{content:"\f238"}.fa-eye-low-vision:before,.fa-low-vision:before{content:"\f2a8"}.fa-crow:before{content:"\f520"}.fa-sailboat:before{content:"\e445"}.fa-window-restore:before{content:"\f2d2"}.fa-plus-square:before,.fa-square-plus:before{content:"\f0fe"}.fa-torii-gate:before{content:"\f6a1"}.fa-frog:before{content:"\f52e"}.fa-bucket:before{content:"\e4cf"}.fa-image:before{content:"\f03e"}.fa-microphone:before{content:"\f130"}.fa-cow:before{content:"\f6c8"}.fa-caret-up:before{content:"\f0d8"}.fa-screwdriver:before{content:"\f54a"}.fa-folder-closed:before{content:"\e185"}.fa-house-tsunami:before{content:"\e515"}.fa-square-nfi:before{content:"\e576"}.fa-arrow-up-from-ground-water:before{content:"\e4b5"}.fa-glass-martini-alt:before,.fa-martini-glass:before{content:"\f57b"}.fa-rotate-back:before,.fa-rotate-backward:before,.fa-rotate-left:before,.fa-undo-alt:before{content:"\f2ea"}.fa-columns:before,.fa-table-columns:before{content:"\f0db"}.fa-lemon:before{content:"\f094"}.fa-head-side-mask:before{content:"\e063"}.fa-handshake:before{content:"\f2b5"}.fa-gem:before{content:"\f3a5"}.fa-dolly-box:before,.fa-dolly:before{content:"\f472"}.fa-smoking:before{content:"\f48d"}.fa-compress-arrows-alt:before,.fa-minimize:before{content:"\f78c"}.fa-monument:before{content:"\f5a6"}.fa-snowplow:before{content:"\f7d2"}.fa-angle-double-right:before,.fa-angles-right:before{content:"\f101"}.fa-cannabis:before{content:"\f55f"}.fa-circle-play:before,.fa-play-circle:before{content:"\f144"}.fa-tablets:before{content:"\f490"}.fa-ethernet:before{content:"\f796"}.fa-eur:before,.fa-euro-sign:before,.fa-euro:before{content:"\f153"}.fa-chair:before{content:"\f6c0"}.fa-check-circle:before,.fa-circle-check:before{content:"\f058"}.fa-circle-stop:before,.fa-stop-circle:before{content:"\f28d"}.fa-compass-drafting:before,.fa-drafting-compass:before{content:"\f568"}.fa-plate-wheat:before{content:"\e55a"}.fa-icicles:before{content:"\f7ad"}.fa-person-shelter:before{content:"\e54f"}.fa-neuter:before{content:"\f22c"}.fa-id-badge:before{content:"\f2c1"}.fa-marker:before{content:"\f5a1"}.fa-face-laugh-beam:before,.fa-laugh-beam:before{content:"\f59a"}.fa-helicopter-symbol:before{content:"\e502"}.fa-universal-access:before{content:"\f29a"}.fa-chevron-circle-up:before,.fa-circle-chevron-up:before{content:"\f139"}.fa-lari-sign:before{content:"\e1c8"}.fa-volcano:before{content:"\f770"}.fa-person-walking-dashed-line-arrow-right:before{content:"\e553"}.fa-gbp:before,.fa-pound-sign:before,.fa-sterling-sign:before{content:"\f154"}.fa-viruses:before{content:"\e076"}.fa-square-person-confined:before{content:"\e577"}.fa-user-tie:before{content:"\f508"}.fa-arrow-down-long:before,.fa-long-arrow-down:before{content:"\f175"}.fa-tent-arrow-down-to-line:before{content:"\e57e"}.fa-certificate:before{content:"\f0a3"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-suitcase:before{content:"\f0f2"}.fa-person-skating:before,.fa-skating:before{content:"\f7c5"}.fa-filter-circle-dollar:before,.fa-funnel-dollar:before{content:"\f662"}.fa-camera-retro:before{content:"\f083"}.fa-arrow-circle-down:before,.fa-circle-arrow-down:before{content:"\f0ab"}.fa-arrow-right-to-file:before,.fa-file-import:before{content:"\f56f"}.fa-external-link-square:before,.fa-square-arrow-up-right:before{content:"\f14c"}.fa-box-open:before{content:"\f49e"}.fa-scroll:before{content:"\f70e"}.fa-spa:before{content:"\f5bb"}.fa-location-pin-lock:before{content:"\e51f"}.fa-pause:before{content:"\f04c"}.fa-hill-avalanche:before{content:"\e507"}.fa-temperature-0:before,.fa-temperature-empty:before,.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-bomb:before{content:"\f1e2"}.fa-registered:before{content:"\f25d"}.fa-address-card:before,.fa-contact-card:before,.fa-vcard:before{content:"\f2bb"}.fa-balance-scale-right:before,.fa-scale-unbalanced-flip:before{content:"\f516"}.fa-subscript:before{content:"\f12c"}.fa-diamond-turn-right:before,.fa-directions:before{content:"\f5eb"}.fa-burst:before{content:"\e4dc"}.fa-house-laptop:before,.fa-laptop-house:before{content:"\e066"}.fa-face-tired:before,.fa-tired:before{content:"\f5c8"}.fa-money-bills:before{content:"\e1f3"}.fa-smog:before{content:"\f75f"}.fa-crutch:before{content:"\f7f7"}.fa-cloud-arrow-up:before,.fa-cloud-upload-alt:before,.fa-cloud-upload:before{content:"\f0ee"}.fa-palette:before{content:"\f53f"}.fa-arrows-turn-right:before{content:"\e4c0"}.fa-vest:before{content:"\e085"}.fa-ferry:before{content:"\e4ea"}.fa-arrows-down-to-people:before{content:"\e4b9"}.fa-seedling:before,.fa-sprout:before{content:"\f4d8"}.fa-arrows-alt-h:before,.fa-left-right:before{content:"\f337"}.fa-boxes-packing:before{content:"\e4c7"}.fa-arrow-circle-left:before,.fa-circle-arrow-left:before{content:"\f0a8"}.fa-group-arrows-rotate:before{content:"\e4f6"}.fa-bowl-food:before{content:"\e4c6"}.fa-candy-cane:before{content:"\f786"}.fa-arrow-down-wide-short:before,.fa-sort-amount-asc:before,.fa-sort-amount-down:before{content:"\f160"}.fa-cloud-bolt:before,.fa-thunderstorm:before{content:"\f76c"}.fa-remove-format:before,.fa-text-slash:before{content:"\f87d"}.fa-face-smile-wink:before,.fa-smile-wink:before{content:"\f4da"}.fa-file-word:before{content:"\f1c2"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-arrows-h:before,.fa-arrows-left-right:before{content:"\f07e"}.fa-house-lock:before{content:"\e510"}.fa-cloud-arrow-down:before,.fa-cloud-download-alt:before,.fa-cloud-download:before{content:"\f0ed"}.fa-children:before{content:"\e4e1"}.fa-blackboard:before,.fa-chalkboard:before{content:"\f51b"}.fa-user-alt-slash:before,.fa-user-large-slash:before{content:"\f4fa"}.fa-envelope-open:before{content:"\f2b6"}.fa-handshake-alt-slash:before,.fa-handshake-simple-slash:before{content:"\e05f"}.fa-mattress-pillow:before{content:"\e525"}.fa-guarani-sign:before{content:"\e19a"}.fa-arrows-rotate:before,.fa-refresh:before,.fa-sync:before{content:"\f021"}.fa-fire-extinguisher:before{content:"\f134"}.fa-cruzeiro-sign:before{content:"\e152"}.fa-greater-than-equal:before{content:"\f532"}.fa-shield-alt:before,.fa-shield-halved:before{content:"\f3ed"}.fa-atlas:before,.fa-book-atlas:before{content:"\f558"}.fa-virus:before{content:"\e074"}.fa-envelope-circle-check:before{content:"\e4e8"}.fa-layer-group:before{content:"\f5fd"}.fa-arrows-to-dot:before{content:"\e4be"}.fa-archway:before{content:"\f557"}.fa-heart-circle-check:before{content:"\e4fd"}.fa-house-chimney-crack:before,.fa-house-damage:before{content:"\f6f1"}.fa-file-archive:before,.fa-file-zipper:before{content:"\f1c6"}.fa-square:before{content:"\f0c8"}.fa-glass-martini:before,.fa-martini-glass-empty:before{content:"\f000"}.fa-couch:before{content:"\f4b8"}.fa-cedi-sign:before{content:"\e0df"}.fa-italic:before{content:"\f033"}.fa-table-cells-column-lock:before{content:"\e678"}.fa-church:before{content:"\f51d"}.fa-comments-dollar:before{content:"\f653"}.fa-democrat:before{content:"\f747"}.fa-z:before{content:"\5a"}.fa-person-skiing:before,.fa-skiing:before{content:"\f7c9"}.fa-road-lock:before{content:"\e567"}.fa-a:before{content:"\41"}.fa-temperature-arrow-down:before,.fa-temperature-down:before{content:"\e03f"}.fa-feather-alt:before,.fa-feather-pointed:before{content:"\f56b"}.fa-p:before{content:"\50"}.fa-snowflake:before{content:"\f2dc"}.fa-newspaper:before{content:"\f1ea"}.fa-ad:before,.fa-rectangle-ad:before{content:"\f641"}.fa-arrow-circle-right:before,.fa-circle-arrow-right:before{content:"\f0a9"}.fa-filter-circle-xmark:before{content:"\e17b"}.fa-locust:before{content:"\e520"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-list-1-2:before,.fa-list-numeric:before,.fa-list-ol:before{content:"\f0cb"}.fa-person-dress-burst:before{content:"\e544"}.fa-money-check-alt:before,.fa-money-check-dollar:before{content:"\f53d"}.fa-vector-square:before{content:"\f5cb"}.fa-bread-slice:before{content:"\f7ec"}.fa-language:before{content:"\f1ab"}.fa-face-kiss-wink-heart:before,.fa-kiss-wink-heart:before{content:"\f598"}.fa-filter:before{content:"\f0b0"}.fa-question:before{content:"\3f"}.fa-file-signature:before{content:"\f573"}.fa-arrows-alt:before,.fa-up-down-left-right:before{content:"\f0b2"}.fa-house-chimney-user:before{content:"\e065"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-puzzle-piece:before{content:"\f12e"}.fa-money-check:before{content:"\f53c"}.fa-star-half-alt:before,.fa-star-half-stroke:before{content:"\f5c0"}.fa-code:before{content:"\f121"}.fa-glass-whiskey:before,.fa-whiskey-glass:before{content:"\f7a0"}.fa-building-circle-exclamation:before{content:"\e4d3"}.fa-magnifying-glass-chart:before{content:"\e522"}.fa-arrow-up-right-from-square:before,.fa-external-link:before{content:"\f08e"}.fa-cubes-stacked:before{content:"\e4e6"}.fa-krw:before,.fa-won-sign:before,.fa-won:before{content:"\f159"}.fa-virus-covid:before{content:"\e4a8"}.fa-austral-sign:before{content:"\e0a9"}.fa-f:before{content:"\46"}.fa-leaf:before{content:"\f06c"}.fa-road:before{content:"\f018"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-person-circle-plus:before{content:"\e541"}.fa-chart-pie:before,.fa-pie-chart:before{content:"\f200"}.fa-bolt-lightning:before{content:"\e0b7"}.fa-sack-xmark:before{content:"\e56a"}.fa-file-excel:before{content:"\f1c3"}.fa-file-contract:before{content:"\f56c"}.fa-fish-fins:before{content:"\e4f2"}.fa-building-flag:before{content:"\e4d5"}.fa-face-grin-beam:before,.fa-grin-beam:before{content:"\f582"}.fa-object-ungroup:before{content:"\f248"}.fa-poop:before{content:"\f619"}.fa-location-pin:before,.fa-map-marker:before{content:"\f041"}.fa-kaaba:before{content:"\f66b"}.fa-toilet-paper:before{content:"\f71e"}.fa-hard-hat:before,.fa-hat-hard:before,.fa-helmet-safety:before{content:"\f807"}.fa-eject:before{content:"\f052"}.fa-arrow-alt-circle-right:before,.fa-circle-right:before{content:"\f35a"}.fa-plane-circle-check:before{content:"\e555"}.fa-face-rolling-eyes:before,.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-object-group:before{content:"\f247"}.fa-chart-line:before,.fa-line-chart:before{content:"\f201"}.fa-mask-ventilator:before{content:"\e524"}.fa-arrow-right:before{content:"\f061"}.fa-map-signs:before,.fa-signs-post:before{content:"\f277"}.fa-cash-register:before{content:"\f788"}.fa-person-circle-question:before{content:"\e542"}.fa-h:before{content:"\48"}.fa-tarp:before{content:"\e57b"}.fa-screwdriver-wrench:before,.fa-tools:before{content:"\f7d9"}.fa-arrows-to-eye:before{content:"\e4bf"}.fa-plug-circle-bolt:before{content:"\e55b"}.fa-heart:before{content:"\f004"}.fa-mars-and-venus:before{content:"\f224"}.fa-home-user:before,.fa-house-user:before{content:"\e1b0"}.fa-dumpster-fire:before{content:"\f794"}.fa-house-crack:before{content:"\e3b1"}.fa-cocktail:before,.fa-martini-glass-citrus:before{content:"\f561"}.fa-face-surprise:before,.fa-surprise:before{content:"\f5c2"}.fa-bottle-water:before{content:"\e4c5"}.fa-circle-pause:before,.fa-pause-circle:before{content:"\f28b"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-apple-alt:before,.fa-apple-whole:before{content:"\f5d1"}.fa-kitchen-set:before{content:"\e51a"}.fa-r:before{content:"\52"}.fa-temperature-1:before,.fa-temperature-quarter:before,.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-cube:before{content:"\f1b2"}.fa-bitcoin-sign:before{content:"\e0b4"}.fa-shield-dog:before{content:"\e573"}.fa-solar-panel:before{content:"\f5ba"}.fa-lock-open:before{content:"\f3c1"}.fa-elevator:before{content:"\e16d"}.fa-money-bill-transfer:before{content:"\e528"}.fa-money-bill-trend-up:before{content:"\e529"}.fa-house-flood-water-circle-arrow-right:before{content:"\e50f"}.fa-poll-h:before,.fa-square-poll-horizontal:before{content:"\f682"}.fa-circle:before{content:"\f111"}.fa-backward-fast:before,.fa-fast-backward:before{content:"\f049"}.fa-recycle:before{content:"\f1b8"}.fa-user-astronaut:before{content:"\f4fb"}.fa-plane-slash:before{content:"\e069"}.fa-trademark:before{content:"\f25c"}.fa-basketball-ball:before,.fa-basketball:before{content:"\f434"}.fa-satellite-dish:before{content:"\f7c0"}.fa-arrow-alt-circle-up:before,.fa-circle-up:before{content:"\f35b"}.fa-mobile-alt:before,.fa-mobile-screen-button:before{content:"\f3cd"}.fa-volume-high:before,.fa-volume-up:before{content:"\f028"}.fa-users-rays:before{content:"\e593"}.fa-wallet:before{content:"\f555"}.fa-clipboard-check:before{content:"\f46c"}.fa-file-audio:before{content:"\f1c7"}.fa-burger:before,.fa-hamburger:before{content:"\f805"}.fa-wrench:before{content:"\f0ad"}.fa-bugs:before{content:"\e4d0"}.fa-rupee-sign:before,.fa-rupee:before{content:"\f156"}.fa-file-image:before{content:"\f1c5"}.fa-circle-question:before,.fa-question-circle:before{content:"\f059"}.fa-plane-departure:before{content:"\f5b0"}.fa-handshake-slash:before{content:"\e060"}.fa-book-bookmark:before{content:"\e0bb"}.fa-code-branch:before{content:"\f126"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-bridge:before{content:"\e4c8"}.fa-phone-alt:before,.fa-phone-flip:before{content:"\f879"}.fa-truck-front:before{content:"\e2b7"}.fa-cat:before{content:"\f6be"}.fa-anchor-circle-exclamation:before{content:"\e4ab"}.fa-truck-field:before{content:"\e58d"}.fa-route:before{content:"\f4d7"}.fa-clipboard-question:before{content:"\e4e3"}.fa-panorama:before{content:"\e209"}.fa-comment-medical:before{content:"\f7f5"}.fa-teeth-open:before{content:"\f62f"}.fa-file-circle-minus:before{content:"\e4ed"}.fa-tags:before{content:"\f02c"}.fa-wine-glass:before{content:"\f4e3"}.fa-fast-forward:before,.fa-forward-fast:before{content:"\f050"}.fa-face-meh-blank:before,.fa-meh-blank:before{content:"\f5a4"}.fa-parking:before,.fa-square-parking:before{content:"\f540"}.fa-house-signal:before{content:"\e012"}.fa-bars-progress:before,.fa-tasks-alt:before{content:"\f828"}.fa-faucet-drip:before{content:"\e006"}.fa-cart-flatbed:before,.fa-dolly-flatbed:before{content:"\f474"}.fa-ban-smoking:before,.fa-smoking-ban:before{content:"\f54d"}.fa-terminal:before{content:"\f120"}.fa-mobile-button:before{content:"\f10b"}.fa-house-medical-flag:before{content:"\e514"}.fa-basket-shopping:before,.fa-shopping-basket:before{content:"\f291"}.fa-tape:before{content:"\f4db"}.fa-bus-alt:before,.fa-bus-simple:before{content:"\f55e"}.fa-eye:before{content:"\f06e"}.fa-face-sad-cry:before,.fa-sad-cry:before{content:"\f5b3"}.fa-audio-description:before{content:"\f29e"}.fa-person-military-to-person:before{content:"\e54c"}.fa-file-shield:before{content:"\e4f0"}.fa-user-slash:before{content:"\f506"}.fa-pen:before{content:"\f304"}.fa-tower-observation:before{content:"\e586"}.fa-file-code:before{content:"\f1c9"}.fa-signal-5:before,.fa-signal-perfect:before,.fa-signal:before{content:"\f012"}.fa-bus:before{content:"\f207"}.fa-heart-circle-xmark:before{content:"\e501"}.fa-home-lg:before,.fa-house-chimney:before{content:"\e3af"}.fa-window-maximize:before{content:"\f2d0"}.fa-face-frown:before,.fa-frown:before{content:"\f119"}.fa-prescription:before{content:"\f5b1"}.fa-shop:before,.fa-store-alt:before{content:"\f54f"}.fa-floppy-disk:before,.fa-save:before{content:"\f0c7"}.fa-vihara:before{content:"\f6a7"}.fa-balance-scale-left:before,.fa-scale-unbalanced:before{content:"\f515"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-comment-dots:before,.fa-commenting:before{content:"\f4ad"}.fa-plant-wilt:before{content:"\e5aa"}.fa-diamond:before{content:"\f219"}.fa-face-grin-squint:before,.fa-grin-squint:before{content:"\f585"}.fa-hand-holding-dollar:before,.fa-hand-holding-usd:before{content:"\f4c0"}.fa-bacterium:before{content:"\e05a"}.fa-hand-pointer:before{content:"\f25a"}.fa-drum-steelpan:before{content:"\f56a"}.fa-hand-scissors:before{content:"\f257"}.fa-hands-praying:before,.fa-praying-hands:before{content:"\f684"}.fa-arrow-right-rotate:before,.fa-arrow-rotate-forward:before,.fa-arrow-rotate-right:before,.fa-redo:before{content:"\f01e"}.fa-biohazard:before{content:"\f780"}.fa-location-crosshairs:before,.fa-location:before{content:"\f601"}.fa-mars-double:before{content:"\f227"}.fa-child-dress:before{content:"\e59c"}.fa-users-between-lines:before{content:"\e591"}.fa-lungs-virus:before{content:"\e067"}.fa-face-grin-tears:before,.fa-grin-tears:before{content:"\f588"}.fa-phone:before{content:"\f095"}.fa-calendar-times:before,.fa-calendar-xmark:before{content:"\f273"}.fa-child-reaching:before{content:"\e59d"}.fa-head-side-virus:before{content:"\e064"}.fa-user-cog:before,.fa-user-gear:before{content:"\f4fe"}.fa-arrow-up-1-9:before,.fa-sort-numeric-up:before{content:"\f163"}.fa-door-closed:before{content:"\f52a"}.fa-shield-virus:before{content:"\e06c"}.fa-dice-six:before{content:"\f526"}.fa-mosquito-net:before{content:"\e52c"}.fa-bridge-water:before{content:"\e4ce"}.fa-person-booth:before{content:"\f756"}.fa-text-width:before{content:"\f035"}.fa-hat-wizard:before{content:"\f6e8"}.fa-pen-fancy:before{content:"\f5ac"}.fa-digging:before,.fa-person-digging:before{content:"\f85e"}.fa-trash:before{content:"\f1f8"}.fa-gauge-simple-med:before,.fa-gauge-simple:before,.fa-tachometer-average:before{content:"\f629"}.fa-book-medical:before{content:"\f7e6"}.fa-poo:before{content:"\f2fe"}.fa-quote-right-alt:before,.fa-quote-right:before{content:"\f10e"}.fa-shirt:before,.fa-t-shirt:before,.fa-tshirt:before{content:"\f553"}.fa-cubes:before{content:"\f1b3"}.fa-divide:before{content:"\f529"}.fa-tenge-sign:before,.fa-tenge:before{content:"\f7d7"}.fa-headphones:before{content:"\f025"}.fa-hands-holding:before{content:"\f4c2"}.fa-hands-clapping:before{content:"\e1a8"}.fa-republican:before{content:"\f75e"}.fa-arrow-left:before{content:"\f060"}.fa-person-circle-xmark:before{content:"\e543"}.fa-ruler:before{content:"\f545"}.fa-align-left:before{content:"\f036"}.fa-dice-d6:before{content:"\f6d1"}.fa-restroom:before{content:"\f7bd"}.fa-j:before{content:"\4a"}.fa-users-viewfinder:before{content:"\e595"}.fa-file-video:before{content:"\f1c8"}.fa-external-link-alt:before,.fa-up-right-from-square:before{content:"\f35d"}.fa-table-cells:before,.fa-th:before{content:"\f00a"}.fa-file-pdf:before{content:"\f1c1"}.fa-bible:before,.fa-book-bible:before{content:"\f647"}.fa-o:before{content:"\4f"}.fa-medkit:before,.fa-suitcase-medical:before{content:"\f0fa"}.fa-user-secret:before{content:"\f21b"}.fa-otter:before{content:"\f700"}.fa-female:before,.fa-person-dress:before{content:"\f182"}.fa-comment-dollar:before{content:"\f651"}.fa-briefcase-clock:before,.fa-business-time:before{content:"\f64a"}.fa-table-cells-large:before,.fa-th-large:before{content:"\f009"}.fa-book-tanakh:before,.fa-tanakh:before{content:"\f827"}.fa-phone-volume:before,.fa-volume-control-phone:before{content:"\f2a0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-clipboard-user:before{content:"\f7f3"}.fa-child:before{content:"\f1ae"}.fa-lira-sign:before{content:"\f195"}.fa-satellite:before{content:"\f7bf"}.fa-plane-lock:before{content:"\e558"}.fa-tag:before{content:"\f02b"}.fa-comment:before{content:"\f075"}.fa-birthday-cake:before,.fa-cake-candles:before,.fa-cake:before{content:"\f1fd"}.fa-envelope:before{content:"\f0e0"}.fa-angle-double-up:before,.fa-angles-up:before{content:"\f102"}.fa-paperclip:before{content:"\f0c6"}.fa-arrow-right-to-city:before{content:"\e4b3"}.fa-ribbon:before{content:"\f4d6"}.fa-lungs:before{content:"\f604"}.fa-arrow-up-9-1:before,.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-litecoin-sign:before{content:"\e1d3"}.fa-border-none:before{content:"\f850"}.fa-circle-nodes:before{content:"\e4e2"}.fa-parachute-box:before{content:"\f4cd"}.fa-indent:before{content:"\f03c"}.fa-truck-field-un:before{content:"\e58e"}.fa-hourglass-empty:before,.fa-hourglass:before{content:"\f254"}.fa-mountain:before{content:"\f6fc"}.fa-user-doctor:before,.fa-user-md:before{content:"\f0f0"}.fa-circle-info:before,.fa-info-circle:before{content:"\f05a"}.fa-cloud-meatball:before{content:"\f73b"}.fa-camera-alt:before,.fa-camera:before{content:"\f030"}.fa-square-virus:before{content:"\e578"}.fa-meteor:before{content:"\f753"}.fa-car-on:before{content:"\e4dd"}.fa-sleigh:before{content:"\f7cc"}.fa-arrow-down-1-9:before,.fa-sort-numeric-asc:before,.fa-sort-numeric-down:before{content:"\f162"}.fa-hand-holding-droplet:before,.fa-hand-holding-water:before{content:"\f4c1"}.fa-water:before{content:"\f773"}.fa-calendar-check:before{content:"\f274"}.fa-braille:before{content:"\f2a1"}.fa-prescription-bottle-alt:before,.fa-prescription-bottle-medical:before{content:"\f486"}.fa-landmark:before{content:"\f66f"}.fa-truck:before{content:"\f0d1"}.fa-crosshairs:before{content:"\f05b"}.fa-person-cane:before{content:"\e53c"}.fa-tent:before{content:"\e57d"}.fa-vest-patches:before{content:"\e086"}.fa-check-double:before{content:"\f560"}.fa-arrow-down-a-z:before,.fa-sort-alpha-asc:before,.fa-sort-alpha-down:before{content:"\f15d"}.fa-money-bill-wheat:before{content:"\e52a"}.fa-cookie:before{content:"\f563"}.fa-arrow-left-rotate:before,.fa-arrow-rotate-back:before,.fa-arrow-rotate-backward:before,.fa-arrow-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-hard-drive:before,.fa-hdd:before{content:"\f0a0"}.fa-face-grin-squint-tears:before,.fa-grin-squint-tears:before{content:"\f586"}.fa-dumbbell:before{content:"\f44b"}.fa-list-alt:before,.fa-rectangle-list:before{content:"\f022"}.fa-tarp-droplet:before{content:"\e57c"}.fa-house-medical-circle-check:before{content:"\e511"}.fa-person-skiing-nordic:before,.fa-skiing-nordic:before{content:"\f7ca"}.fa-calendar-plus:before{content:"\f271"}.fa-plane-arrival:before{content:"\f5af"}.fa-arrow-alt-circle-left:before,.fa-circle-left:before{content:"\f359"}.fa-subway:before,.fa-train-subway:before{content:"\f239"}.fa-chart-gantt:before{content:"\e0e4"}.fa-indian-rupee-sign:before,.fa-indian-rupee:before,.fa-inr:before{content:"\e1bc"}.fa-crop-alt:before,.fa-crop-simple:before{content:"\f565"}.fa-money-bill-1:before,.fa-money-bill-alt:before{content:"\f3d1"}.fa-left-long:before,.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-dna:before{content:"\f471"}.fa-virus-slash:before{content:"\e075"}.fa-minus:before,.fa-subtract:before{content:"\f068"}.fa-chess:before{content:"\f439"}.fa-arrow-left-long:before,.fa-long-arrow-left:before{content:"\f177"}.fa-plug-circle-check:before{content:"\e55c"}.fa-street-view:before{content:"\f21d"}.fa-franc-sign:before{content:"\e18f"}.fa-volume-off:before{content:"\f026"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before,.fa-hands-american-sign-language-interpreting:before,.fa-hands-asl-interpreting:before{content:"\f2a3"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-droplet-slash:before,.fa-tint-slash:before{content:"\f5c7"}.fa-mosque:before{content:"\f678"}.fa-mosquito:before{content:"\e52b"}.fa-star-of-david:before{content:"\f69a"}.fa-person-military-rifle:before{content:"\e54b"}.fa-cart-shopping:before,.fa-shopping-cart:before{content:"\f07a"}.fa-vials:before{content:"\f493"}.fa-plug-circle-plus:before{content:"\e55f"}.fa-place-of-worship:before{content:"\f67f"}.fa-grip-vertical:before{content:"\f58e"}.fa-arrow-turn-up:before,.fa-level-up:before{content:"\f148"}.fa-u:before{content:"\55"}.fa-square-root-alt:before,.fa-square-root-variable:before{content:"\f698"}.fa-clock-four:before,.fa-clock:before{content:"\f017"}.fa-backward-step:before,.fa-step-backward:before{content:"\f048"}.fa-pallet:before{content:"\f482"}.fa-faucet:before{content:"\e005"}.fa-baseball-bat-ball:before{content:"\f432"}.fa-s:before{content:"\53"}.fa-timeline:before{content:"\e29c"}.fa-keyboard:before{content:"\f11c"}.fa-caret-down:before{content:"\f0d7"}.fa-clinic-medical:before,.fa-house-chimney-medical:before{content:"\f7f2"}.fa-temperature-3:before,.fa-temperature-three-quarters:before,.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-mobile-android-alt:before,.fa-mobile-screen:before{content:"\f3cf"}.fa-plane-up:before{content:"\e22d"}.fa-piggy-bank:before{content:"\f4d3"}.fa-battery-3:before,.fa-battery-half:before{content:"\f242"}.fa-mountain-city:before{content:"\e52e"}.fa-coins:before{content:"\f51e"}.fa-khanda:before{content:"\f66d"}.fa-sliders-h:before,.fa-sliders:before{content:"\f1de"}.fa-folder-tree:before{content:"\f802"}.fa-network-wired:before{content:"\f6ff"}.fa-map-pin:before{content:"\f276"}.fa-hamsa:before{content:"\f665"}.fa-cent-sign:before{content:"\e3f5"}.fa-flask:before{content:"\f0c3"}.fa-person-pregnant:before{content:"\e31e"}.fa-wand-sparkles:before{content:"\f72b"}.fa-ellipsis-v:before,.fa-ellipsis-vertical:before{content:"\f142"}.fa-ticket:before{content:"\f145"}.fa-power-off:before{content:"\f011"}.fa-long-arrow-alt-right:before,.fa-right-long:before{content:"\f30b"}.fa-flag-usa:before{content:"\f74d"}.fa-laptop-file:before{content:"\e51d"}.fa-teletype:before,.fa-tty:before{content:"\f1e4"}.fa-diagram-next:before{content:"\e476"}.fa-person-rifle:before{content:"\e54e"}.fa-house-medical-circle-exclamation:before{content:"\e512"}.fa-closed-captioning:before{content:"\f20a"}.fa-hiking:before,.fa-person-hiking:before{content:"\f6ec"}.fa-venus-double:before{content:"\f226"}.fa-images:before{content:"\f302"}.fa-calculator:before{content:"\f1ec"}.fa-people-pulling:before{content:"\e535"}.fa-n:before{content:"\4e"}.fa-cable-car:before,.fa-tram:before{content:"\f7da"}.fa-cloud-rain:before{content:"\f73d"}.fa-building-circle-xmark:before{content:"\e4d4"}.fa-ship:before{content:"\f21a"}.fa-arrows-down-to-line:before{content:"\e4b8"}.fa-download:before{content:"\f019"}.fa-face-grin:before,.fa-grin:before{content:"\f580"}.fa-backspace:before,.fa-delete-left:before{content:"\f55a"}.fa-eye-dropper-empty:before,.fa-eye-dropper:before,.fa-eyedropper:before{content:"\f1fb"}.fa-file-circle-check:before{content:"\e5a0"}.fa-forward:before{content:"\f04e"}.fa-mobile-android:before,.fa-mobile-phone:before,.fa-mobile:before{content:"\f3ce"}.fa-face-meh:before,.fa-meh:before{content:"\f11a"}.fa-align-center:before{content:"\f037"}.fa-book-dead:before,.fa-book-skull:before{content:"\f6b7"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-heart-circle-exclamation:before{content:"\e4fe"}.fa-home-alt:before,.fa-home-lg-alt:before,.fa-home:before,.fa-house:before{content:"\f015"}.fa-calendar-week:before{content:"\f784"}.fa-laptop-medical:before{content:"\f812"}.fa-b:before{content:"\42"}.fa-file-medical:before{content:"\f477"}.fa-dice-one:before{content:"\f525"}.fa-kiwi-bird:before{content:"\f535"}.fa-arrow-right-arrow-left:before,.fa-exchange:before{content:"\f0ec"}.fa-redo-alt:before,.fa-rotate-forward:before,.fa-rotate-right:before{content:"\f2f9"}.fa-cutlery:before,.fa-utensils:before{content:"\f2e7"}.fa-arrow-up-wide-short:before,.fa-sort-amount-up:before{content:"\f161"}.fa-mill-sign:before{content:"\e1ed"}.fa-bowl-rice:before{content:"\e2eb"}.fa-skull:before{content:"\f54c"}.fa-broadcast-tower:before,.fa-tower-broadcast:before{content:"\f519"}.fa-truck-pickup:before{content:"\f63c"}.fa-long-arrow-alt-up:before,.fa-up-long:before{content:"\f30c"}.fa-stop:before{content:"\f04d"}.fa-code-merge:before{content:"\f387"}.fa-upload:before{content:"\f093"}.fa-hurricane:before{content:"\f751"}.fa-mound:before{content:"\e52d"}.fa-toilet-portable:before{content:"\e583"}.fa-compact-disc:before{content:"\f51f"}.fa-file-arrow-down:before,.fa-file-download:before{content:"\f56d"}.fa-caravan:before{content:"\f8ff"}.fa-shield-cat:before{content:"\e572"}.fa-bolt:before,.fa-zap:before{content:"\f0e7"}.fa-glass-water:before{content:"\e4f4"}.fa-oil-well:before{content:"\e532"}.fa-vault:before{content:"\e2c5"}.fa-mars:before{content:"\f222"}.fa-toilet:before{content:"\f7d8"}.fa-plane-circle-xmark:before{content:"\e557"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen-sign:before,.fa-yen:before{content:"\f157"}.fa-rouble:before,.fa-rub:before,.fa-ruble-sign:before,.fa-ruble:before{content:"\f158"}.fa-sun:before{content:"\f185"}.fa-guitar:before{content:"\f7a6"}.fa-face-laugh-wink:before,.fa-laugh-wink:before{content:"\f59c"}.fa-horse-head:before{content:"\f7ab"}.fa-bore-hole:before{content:"\e4c3"}.fa-industry:before{content:"\f275"}.fa-arrow-alt-circle-down:before,.fa-circle-down:before{content:"\f358"}.fa-arrows-turn-to-dots:before{content:"\e4c1"}.fa-florin-sign:before{content:"\e184"}.fa-arrow-down-short-wide:before,.fa-sort-amount-desc:before,.fa-sort-amount-down-alt:before{content:"\f884"}.fa-less-than:before{content:"\3c"}.fa-angle-down:before{content:"\f107"}.fa-car-tunnel:before{content:"\e4de"}.fa-head-side-cough:before{content:"\e061"}.fa-grip-lines:before{content:"\f7a4"}.fa-thumbs-down:before{content:"\f165"}.fa-user-lock:before{content:"\f502"}.fa-arrow-right-long:before,.fa-long-arrow-right:before{content:"\f178"}.fa-anchor-circle-xmark:before{content:"\e4ac"}.fa-ellipsis-h:before,.fa-ellipsis:before{content:"\f141"}.fa-chess-pawn:before{content:"\f443"}.fa-first-aid:before,.fa-kit-medical:before{content:"\f479"}.fa-person-through-window:before{content:"\e5a9"}.fa-toolbox:before{content:"\f552"}.fa-hands-holding-circle:before{content:"\e4fb"}.fa-bug:before{content:"\f188"}.fa-credit-card-alt:before,.fa-credit-card:before{content:"\f09d"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-hand-holding-hand:before{content:"\e4f7"}.fa-book-open-reader:before,.fa-book-reader:before{content:"\f5da"}.fa-mountain-sun:before{content:"\e52f"}.fa-arrows-left-right-to-line:before{content:"\e4ba"}.fa-dice-d20:before{content:"\f6cf"}.fa-truck-droplet:before{content:"\e58c"}.fa-file-circle-xmark:before{content:"\e5a1"}.fa-temperature-arrow-up:before,.fa-temperature-up:before{content:"\e040"}.fa-medal:before{content:"\f5a2"}.fa-bed:before{content:"\f236"}.fa-h-square:before,.fa-square-h:before{content:"\f0fd"}.fa-podcast:before{content:"\f2ce"}.fa-temperature-4:before,.fa-temperature-full:before,.fa-thermometer-4:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-bell:before{content:"\f0f3"}.fa-superscript:before{content:"\f12b"}.fa-plug-circle-xmark:before{content:"\e560"}.fa-star-of-life:before{content:"\f621"}.fa-phone-slash:before{content:"\f3dd"}.fa-paint-roller:before{content:"\f5aa"}.fa-hands-helping:before,.fa-handshake-angle:before{content:"\f4c4"}.fa-location-dot:before,.fa-map-marker-alt:before{content:"\f3c5"}.fa-file:before{content:"\f15b"}.fa-greater-than:before{content:"\3e"}.fa-person-swimming:before,.fa-swimmer:before{content:"\f5c4"}.fa-arrow-down:before{content:"\f063"}.fa-droplet:before,.fa-tint:before{content:"\f043"}.fa-eraser:before{content:"\f12d"}.fa-earth-america:before,.fa-earth-americas:before,.fa-earth:before,.fa-globe-americas:before{content:"\f57d"}.fa-person-burst:before{content:"\e53b"}.fa-dove:before{content:"\f4ba"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-socks:before{content:"\f696"}.fa-inbox:before{content:"\f01c"}.fa-section:before{content:"\e447"}.fa-gauge-high:before,.fa-tachometer-alt-fast:before,.fa-tachometer-alt:before{content:"\f625"}.fa-envelope-open-text:before{content:"\f658"}.fa-hospital-alt:before,.fa-hospital-wide:before,.fa-hospital:before{content:"\f0f8"}.fa-wine-bottle:before{content:"\f72f"}.fa-chess-rook:before{content:"\f447"}.fa-bars-staggered:before,.fa-reorder:before,.fa-stream:before{content:"\f550"}.fa-dharmachakra:before{content:"\f655"}.fa-hotdog:before{content:"\f80f"}.fa-blind:before,.fa-person-walking-with-cane:before{content:"\f29d"}.fa-drum:before{content:"\f569"}.fa-ice-cream:before{content:"\f810"}.fa-heart-circle-bolt:before{content:"\e4fc"}.fa-fax:before{content:"\f1ac"}.fa-paragraph:before{content:"\f1dd"}.fa-check-to-slot:before,.fa-vote-yea:before{content:"\f772"}.fa-star-half:before{content:"\f089"}.fa-boxes-alt:before,.fa-boxes-stacked:before,.fa-boxes:before{content:"\f468"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-assistive-listening-systems:before,.fa-ear-listen:before{content:"\f2a2"}.fa-tree-city:before{content:"\e587"}.fa-play:before{content:"\f04b"}.fa-font:before{content:"\f031"}.fa-table-cells-row-lock:before{content:"\e67a"}.fa-rupiah-sign:before{content:"\e23d"}.fa-magnifying-glass:before,.fa-search:before{content:"\f002"}.fa-ping-pong-paddle-ball:before,.fa-table-tennis-paddle-ball:before,.fa-table-tennis:before{content:"\f45d"}.fa-diagnoses:before,.fa-person-dots-from-line:before{content:"\f470"}.fa-trash-can-arrow-up:before,.fa-trash-restore-alt:before{content:"\f82a"}.fa-naira-sign:before{content:"\e1f6"}.fa-cart-arrow-down:before{content:"\f218"}.fa-walkie-talkie:before{content:"\f8ef"}.fa-file-edit:before,.fa-file-pen:before{content:"\f31c"}.fa-receipt:before{content:"\f543"}.fa-pen-square:before,.fa-pencil-square:before,.fa-square-pen:before{content:"\f14b"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-person-circle-exclamation:before{content:"\e53f"}.fa-chevron-down:before{content:"\f078"}.fa-battery-5:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-skull-crossbones:before{content:"\f714"}.fa-code-compare:before{content:"\e13a"}.fa-list-dots:before,.fa-list-ul:before{content:"\f0ca"}.fa-school-lock:before{content:"\e56f"}.fa-tower-cell:before{content:"\e585"}.fa-down-long:before,.fa-long-arrow-alt-down:before{content:"\f309"}.fa-ranking-star:before{content:"\e561"}.fa-chess-king:before{content:"\f43f"}.fa-person-harassing:before{content:"\e549"}.fa-brazilian-real-sign:before{content:"\e46c"}.fa-landmark-alt:before,.fa-landmark-dome:before{content:"\f752"}.fa-arrow-up:before{content:"\f062"}.fa-television:before,.fa-tv-alt:before,.fa-tv:before{content:"\f26c"}.fa-shrimp:before{content:"\e448"}.fa-list-check:before,.fa-tasks:before{content:"\f0ae"}.fa-jug-detergent:before{content:"\e519"}.fa-circle-user:before,.fa-user-circle:before{content:"\f2bd"}.fa-user-shield:before{content:"\f505"}.fa-wind:before{content:"\f72e"}.fa-car-burst:before,.fa-car-crash:before{content:"\f5e1"}.fa-y:before{content:"\59"}.fa-person-snowboarding:before,.fa-snowboarding:before{content:"\f7ce"}.fa-shipping-fast:before,.fa-truck-fast:before{content:"\f48b"}.fa-fish:before{content:"\f578"}.fa-user-graduate:before{content:"\f501"}.fa-adjust:before,.fa-circle-half-stroke:before{content:"\f042"}.fa-clapperboard:before{content:"\e131"}.fa-circle-radiation:before,.fa-radiation-alt:before{content:"\f7ba"}.fa-baseball-ball:before,.fa-baseball:before{content:"\f433"}.fa-jet-fighter-up:before{content:"\e518"}.fa-diagram-project:before,.fa-project-diagram:before{content:"\f542"}.fa-copy:before{content:"\f0c5"}.fa-volume-mute:before,.fa-volume-times:before,.fa-volume-xmark:before{content:"\f6a9"}.fa-hand-sparkles:before{content:"\e05d"}.fa-grip-horizontal:before,.fa-grip:before{content:"\f58d"}.fa-share-from-square:before,.fa-share-square:before{content:"\f14d"}.fa-child-combatant:before,.fa-child-rifle:before{content:"\e4e0"}.fa-gun:before{content:"\e19b"}.fa-phone-square:before,.fa-square-phone:before{content:"\f098"}.fa-add:before,.fa-plus:before{content:"\2b"}.fa-expand:before{content:"\f065"}.fa-computer:before{content:"\e4e5"}.fa-close:before,.fa-multiply:before,.fa-remove:before,.fa-times:before,.fa-xmark:before{content:"\f00d"}.fa-arrows-up-down-left-right:before,.fa-arrows:before{content:"\f047"}.fa-chalkboard-teacher:before,.fa-chalkboard-user:before{content:"\f51c"}.fa-peso-sign:before{content:"\e222"}.fa-building-shield:before{content:"\e4d8"}.fa-baby:before{content:"\f77c"}.fa-users-line:before{content:"\e592"}.fa-quote-left-alt:before,.fa-quote-left:before{content:"\f10d"}.fa-tractor:before{content:"\f722"}.fa-trash-arrow-up:before,.fa-trash-restore:before{content:"\f829"}.fa-arrow-down-up-lock:before{content:"\e4b0"}.fa-lines-leaning:before{content:"\e51e"}.fa-ruler-combined:before{content:"\f546"}.fa-copyright:before{content:"\f1f9"}.fa-equals:before{content:"\3d"}.fa-blender:before{content:"\f517"}.fa-teeth:before{content:"\f62e"}.fa-ils:before,.fa-shekel-sign:before,.fa-shekel:before,.fa-sheqel-sign:before,.fa-sheqel:before{content:"\f20b"}.fa-map:before{content:"\f279"}.fa-rocket:before{content:"\f135"}.fa-photo-film:before,.fa-photo-video:before{content:"\f87c"}.fa-folder-minus:before{content:"\f65d"}.fa-store:before{content:"\f54e"}.fa-arrow-trend-up:before{content:"\e098"}.fa-plug-circle-minus:before{content:"\e55e"}.fa-sign-hanging:before,.fa-sign:before{content:"\f4d9"}.fa-bezier-curve:before{content:"\f55b"}.fa-bell-slash:before{content:"\f1f6"}.fa-tablet-android:before,.fa-tablet:before{content:"\f3fb"}.fa-school-flag:before{content:"\e56e"}.fa-fill:before{content:"\f575"}.fa-angle-up:before{content:"\f106"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-holly-berry:before{content:"\f7aa"}.fa-chevron-left:before{content:"\f053"}.fa-bacteria:before{content:"\e059"}.fa-hand-lizard:before{content:"\f258"}.fa-notdef:before{content:"\e1fe"}.fa-disease:before{content:"\f7fa"}.fa-briefcase-medical:before{content:"\f469"}.fa-genderless:before{content:"\f22d"}.fa-chevron-right:before{content:"\f054"}.fa-retweet:before{content:"\f079"}.fa-car-alt:before,.fa-car-rear:before{content:"\f5de"}.fa-pump-soap:before{content:"\e06b"}.fa-video-slash:before{content:"\f4e2"}.fa-battery-2:before,.fa-battery-quarter:before{content:"\f243"}.fa-radio:before{content:"\f8d7"}.fa-baby-carriage:before,.fa-carriage-baby:before{content:"\f77d"}.fa-traffic-light:before{content:"\f637"}.fa-thermometer:before{content:"\f491"}.fa-vr-cardboard:before{content:"\f729"}.fa-hand-middle-finger:before{content:"\f806"}.fa-percent:before,.fa-percentage:before{content:"\25"}.fa-truck-moving:before{content:"\f4df"}.fa-glass-water-droplet:before{content:"\e4f5"}.fa-display:before{content:"\e163"}.fa-face-smile:before,.fa-smile:before{content:"\f118"}.fa-thumb-tack:before,.fa-thumbtack:before{content:"\f08d"}.fa-trophy:before{content:"\f091"}.fa-person-praying:before,.fa-pray:before{content:"\f683"}.fa-hammer:before{content:"\f6e3"}.fa-hand-peace:before{content:"\f25b"}.fa-rotate:before,.fa-sync-alt:before{content:"\f2f1"}.fa-spinner:before{content:"\f110"}.fa-robot:before{content:"\f544"}.fa-peace:before{content:"\f67c"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-warehouse:before{content:"\f494"}.fa-arrow-up-right-dots:before{content:"\e4b7"}.fa-splotch:before{content:"\f5bc"}.fa-face-grin-hearts:before,.fa-grin-hearts:before{content:"\f584"}.fa-dice-four:before{content:"\f524"}.fa-sim-card:before{content:"\f7c4"}.fa-transgender-alt:before,.fa-transgender:before{content:"\f225"}.fa-mercury:before{content:"\f223"}.fa-arrow-turn-down:before,.fa-level-down:before{content:"\f149"}.fa-person-falling-burst:before{content:"\e547"}.fa-award:before{content:"\f559"}.fa-ticket-alt:before,.fa-ticket-simple:before{content:"\f3ff"}.fa-building:before{content:"\f1ad"}.fa-angle-double-left:before,.fa-angles-left:before{content:"\f100"}.fa-qrcode:before{content:"\f029"}.fa-clock-rotate-left:before,.fa-history:before{content:"\f1da"}.fa-face-grin-beam-sweat:before,.fa-grin-beam-sweat:before{content:"\f583"}.fa-arrow-right-from-file:before,.fa-file-export:before{content:"\f56e"}.fa-shield-blank:before,.fa-shield:before{content:"\f132"}.fa-arrow-up-short-wide:before,.fa-sort-amount-up-alt:before{content:"\f885"}.fa-house-medical:before{content:"\e3b2"}.fa-golf-ball-tee:before,.fa-golf-ball:before{content:"\f450"}.fa-chevron-circle-left:before,.fa-circle-chevron-left:before{content:"\f137"}.fa-house-chimney-window:before{content:"\e00d"}.fa-pen-nib:before{content:"\f5ad"}.fa-tent-arrow-turn-left:before{content:"\e580"}.fa-tents:before{content:"\e582"}.fa-magic:before,.fa-wand-magic:before{content:"\f0d0"}.fa-dog:before{content:"\f6d3"}.fa-carrot:before{content:"\f787"}.fa-moon:before{content:"\f186"}.fa-wine-glass-alt:before,.fa-wine-glass-empty:before{content:"\f5ce"}.fa-cheese:before{content:"\f7ef"}.fa-yin-yang:before{content:"\f6ad"}.fa-music:before{content:"\f001"}.fa-code-commit:before{content:"\f386"}.fa-temperature-low:before{content:"\f76b"}.fa-biking:before,.fa-person-biking:before{content:"\f84a"}.fa-broom:before{content:"\f51a"}.fa-shield-heart:before{content:"\e574"}.fa-gopuram:before{content:"\f664"}.fa-earth-oceania:before,.fa-globe-oceania:before{content:"\e47b"}.fa-square-xmark:before,.fa-times-square:before,.fa-xmark-square:before{content:"\f2d3"}.fa-hashtag:before{content:"\23"}.fa-expand-alt:before,.fa-up-right-and-down-left-from-center:before{content:"\f424"}.fa-oil-can:before{content:"\f613"}.fa-t:before{content:"\54"}.fa-hippo:before{content:"\f6ed"}.fa-chart-column:before{content:"\e0e3"}.fa-infinity:before{content:"\f534"}.fa-vial-circle-check:before{content:"\e596"}.fa-person-arrow-down-to-line:before{content:"\e538"}.fa-voicemail:before{content:"\f897"}.fa-fan:before{content:"\f863"}.fa-person-walking-luggage:before{content:"\e554"}.fa-arrows-alt-v:before,.fa-up-down:before{content:"\f338"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-calendar:before{content:"\f133"}.fa-trailer:before{content:"\e041"}.fa-bahai:before,.fa-haykal:before{content:"\f666"}.fa-sd-card:before{content:"\f7c2"}.fa-dragon:before{content:"\f6d5"}.fa-shoe-prints:before{content:"\f54b"}.fa-circle-plus:before,.fa-plus-circle:before{content:"\f055"}.fa-face-grin-tongue-wink:before,.fa-grin-tongue-wink:before{content:"\f58b"}.fa-hand-holding:before{content:"\f4bd"}.fa-plug-circle-exclamation:before{content:"\e55d"}.fa-chain-broken:before,.fa-chain-slash:before,.fa-link-slash:before,.fa-unlink:before{content:"\f127"}.fa-clone:before{content:"\f24d"}.fa-person-walking-arrow-loop-left:before{content:"\e551"}.fa-arrow-up-z-a:before,.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-fire-alt:before,.fa-fire-flame-curved:before{content:"\f7e4"}.fa-tornado:before{content:"\f76f"}.fa-file-circle-plus:before{content:"\e494"}.fa-book-quran:before,.fa-quran:before{content:"\f687"}.fa-anchor:before{content:"\f13d"}.fa-border-all:before{content:"\f84c"}.fa-angry:before,.fa-face-angry:before{content:"\f556"}.fa-cookie-bite:before{content:"\f564"}.fa-arrow-trend-down:before{content:"\e097"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-draw-polygon:before{content:"\f5ee"}.fa-balance-scale:before,.fa-scale-balanced:before{content:"\f24e"}.fa-gauge-simple-high:before,.fa-tachometer-fast:before,.fa-tachometer:before{content:"\f62a"}.fa-shower:before{content:"\f2cc"}.fa-desktop-alt:before,.fa-desktop:before{content:"\f390"}.fa-m:before{content:"\4d"}.fa-table-list:before,.fa-th-list:before{content:"\f00b"}.fa-comment-sms:before,.fa-sms:before{content:"\f7cd"}.fa-book:before{content:"\f02d"}.fa-user-plus:before{content:"\f234"}.fa-check:before{content:"\f00c"}.fa-battery-4:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-house-circle-check:before{content:"\e509"}.fa-angle-left:before{content:"\f104"}.fa-diagram-successor:before{content:"\e47a"}.fa-truck-arrow-right:before{content:"\e58b"}.fa-arrows-split-up-and-left:before{content:"\e4bc"}.fa-fist-raised:before,.fa-hand-fist:before{content:"\f6de"}.fa-cloud-moon:before{content:"\f6c3"}.fa-briefcase:before{content:"\f0b1"}.fa-person-falling:before{content:"\e546"}.fa-image-portrait:before,.fa-portrait:before{content:"\f3e0"}.fa-user-tag:before{content:"\f507"}.fa-rug:before{content:"\e569"}.fa-earth-europe:before,.fa-globe-europe:before{content:"\f7a2"}.fa-cart-flatbed-suitcase:before,.fa-luggage-cart:before{content:"\f59d"}.fa-rectangle-times:before,.fa-rectangle-xmark:before,.fa-times-rectangle:before,.fa-window-close:before{content:"\f410"}.fa-baht-sign:before{content:"\e0ac"}.fa-book-open:before{content:"\f518"}.fa-book-journal-whills:before,.fa-journal-whills:before{content:"\f66a"}.fa-handcuffs:before{content:"\e4f8"}.fa-exclamation-triangle:before,.fa-triangle-exclamation:before,.fa-warning:before{content:"\f071"}.fa-database:before{content:"\f1c0"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-bottle-droplet:before{content:"\e4c4"}.fa-mask-face:before{content:"\e1d7"}.fa-hill-rockslide:before{content:"\e508"}.fa-exchange-alt:before,.fa-right-left:before{content:"\f362"}.fa-paper-plane:before{content:"\f1d8"}.fa-road-circle-exclamation:before{content:"\e565"}.fa-dungeon:before{content:"\f6d9"}.fa-align-right:before{content:"\f038"}.fa-money-bill-1-wave:before,.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-life-ring:before{content:"\f1cd"}.fa-hands:before,.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-calendar-day:before{content:"\f783"}.fa-ladder-water:before,.fa-swimming-pool:before,.fa-water-ladder:before{content:"\f5c5"}.fa-arrows-up-down:before,.fa-arrows-v:before{content:"\f07d"}.fa-face-grimace:before,.fa-grimace:before{content:"\f57f"}.fa-wheelchair-alt:before,.fa-wheelchair-move:before{content:"\e2ce"}.fa-level-down-alt:before,.fa-turn-down:before{content:"\f3be"}.fa-person-walking-arrow-right:before{content:"\e552"}.fa-envelope-square:before,.fa-square-envelope:before{content:"\f199"}.fa-dice:before{content:"\f522"}.fa-bowling-ball:before{content:"\f436"}.fa-brain:before{content:"\f5dc"}.fa-band-aid:before,.fa-bandage:before{content:"\f462"}.fa-calendar-minus:before{content:"\f272"}.fa-circle-xmark:before,.fa-times-circle:before,.fa-xmark-circle:before{content:"\f057"}.fa-gifts:before{content:"\f79c"}.fa-hotel:before{content:"\f594"}.fa-earth-asia:before,.fa-globe-asia:before{content:"\f57e"}.fa-id-card-alt:before,.fa-id-card-clip:before{content:"\f47f"}.fa-magnifying-glass-plus:before,.fa-search-plus:before{content:"\f00e"}.fa-thumbs-up:before{content:"\f164"}.fa-user-clock:before{content:"\f4fd"}.fa-allergies:before,.fa-hand-dots:before{content:"\f461"}.fa-file-invoice:before{content:"\f570"}.fa-window-minimize:before{content:"\f2d1"}.fa-coffee:before,.fa-mug-saucer:before{content:"\f0f4"}.fa-brush:before{content:"\f55d"}.fa-mask:before{content:"\f6fa"}.fa-magnifying-glass-minus:before,.fa-search-minus:before{content:"\f010"}.fa-ruler-vertical:before{content:"\f548"}.fa-user-alt:before,.fa-user-large:before{content:"\f406"}.fa-train-tram:before{content:"\e5b4"}.fa-user-nurse:before{content:"\f82f"}.fa-syringe:before{content:"\f48e"}.fa-cloud-sun:before{content:"\f6c4"}.fa-stopwatch-20:before{content:"\e06f"}.fa-square-full:before{content:"\f45c"}.fa-magnet:before{content:"\f076"}.fa-jar:before{content:"\e516"}.fa-note-sticky:before,.fa-sticky-note:before{content:"\f249"}.fa-bug-slash:before{content:"\e490"}.fa-arrow-up-from-water-pump:before{content:"\e4b6"}.fa-bone:before{content:"\f5d7"}.fa-user-injured:before{content:"\f728"}.fa-face-sad-tear:before,.fa-sad-tear:before{content:"\f5b4"}.fa-plane:before{content:"\f072"}.fa-tent-arrows-down:before{content:"\e581"}.fa-exclamation:before{content:"\21"}.fa-arrows-spin:before{content:"\e4bb"}.fa-print:before{content:"\f02f"}.fa-try:before,.fa-turkish-lira-sign:before,.fa-turkish-lira:before{content:"\e2bb"}.fa-dollar-sign:before,.fa-dollar:before,.fa-usd:before{content:"\24"}.fa-x:before{content:"\58"}.fa-magnifying-glass-dollar:before,.fa-search-dollar:before{content:"\f688"}.fa-users-cog:before,.fa-users-gear:before{content:"\f509"}.fa-person-military-pointing:before{content:"\e54a"}.fa-bank:before,.fa-building-columns:before,.fa-institution:before,.fa-museum:before,.fa-university:before{content:"\f19c"}.fa-umbrella:before{content:"\f0e9"}.fa-trowel:before{content:"\e589"}.fa-d:before{content:"\44"}.fa-stapler:before{content:"\e5af"}.fa-masks-theater:before,.fa-theater-masks:before{content:"\f630"}.fa-kip-sign:before{content:"\e1c4"}.fa-hand-point-left:before{content:"\f0a5"}.fa-handshake-alt:before,.fa-handshake-simple:before{content:"\f4c6"}.fa-fighter-jet:before,.fa-jet-fighter:before{content:"\f0fb"}.fa-share-alt-square:before,.fa-square-share-nodes:before{content:"\f1e1"}.fa-barcode:before{content:"\f02a"}.fa-plus-minus:before{content:"\e43c"}.fa-video-camera:before,.fa-video:before{content:"\f03d"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-person-circle-check:before{content:"\e53e"}.fa-level-up-alt:before,.fa-turn-up:before{content:"\f3bf"}
-.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero:before{content:"\f3d0"}.fa-hooli:before{content:"\f427"}.fa-yelp:before{content:"\f1e9"}.fa-cc-visa:before{content:"\f1f0"}.fa-lastfm:before{content:"\f202"}.fa-shopware:before{content:"\f5b5"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-aws:before{content:"\f375"}.fa-redhat:before{content:"\f7bc"}.fa-yoast:before{content:"\f2b1"}.fa-cloudflare:before{content:"\e07d"}.fa-ups:before{content:"\f7e0"}.fa-pixiv:before{content:"\e640"}.fa-wpexplorer:before{content:"\f2de"}.fa-dyalog:before{content:"\f399"}.fa-bity:before{content:"\f37a"}.fa-stackpath:before{content:"\f842"}.fa-buysellads:before{content:"\f20d"}.fa-first-order:before{content:"\f2b0"}.fa-modx:before{content:"\f285"}.fa-guilded:before{content:"\e07e"}.fa-vnv:before{content:"\f40b"}.fa-js-square:before,.fa-square-js:before{content:"\f3b9"}.fa-microsoft:before{content:"\f3ca"}.fa-qq:before{content:"\f1d6"}.fa-orcid:before{content:"\f8d2"}.fa-java:before{content:"\f4e4"}.fa-invision:before{content:"\f7b0"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-centercode:before{content:"\f380"}.fa-glide-g:before{content:"\f2a6"}.fa-drupal:before{content:"\f1a9"}.fa-jxl:before{content:"\e67b"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-unity:before{content:"\e049"}.fa-whmcs:before{content:"\f40d"}.fa-rocketchat:before{content:"\f3e8"}.fa-vk:before{content:"\f189"}.fa-untappd:before{content:"\f405"}.fa-mailchimp:before{content:"\f59e"}.fa-css3-alt:before{content:"\f38b"}.fa-reddit-square:before,.fa-square-reddit:before{content:"\f1a2"}.fa-vimeo-v:before{content:"\f27d"}.fa-contao:before{content:"\f26d"}.fa-square-font-awesome:before{content:"\e5ad"}.fa-deskpro:before{content:"\f38f"}.fa-brave:before{content:"\e63c"}.fa-sistrix:before{content:"\f3ee"}.fa-instagram-square:before,.fa-square-instagram:before{content:"\e055"}.fa-battle-net:before{content:"\f835"}.fa-the-red-yeti:before{content:"\f69d"}.fa-hacker-news-square:before,.fa-square-hacker-news:before{content:"\f3af"}.fa-edge:before{content:"\f282"}.fa-threads:before{content:"\e618"}.fa-napster:before{content:"\f3d2"}.fa-snapchat-square:before,.fa-square-snapchat:before{content:"\f2ad"}.fa-google-plus-g:before{content:"\f0d5"}.fa-artstation:before{content:"\f77a"}.fa-markdown:before{content:"\f60f"}.fa-sourcetree:before{content:"\f7d3"}.fa-google-plus:before{content:"\f2b3"}.fa-diaspora:before{content:"\f791"}.fa-foursquare:before{content:"\f180"}.fa-stack-overflow:before{content:"\f16c"}.fa-github-alt:before{content:"\f113"}.fa-phoenix-squadron:before{content:"\f511"}.fa-pagelines:before{content:"\f18c"}.fa-algolia:before{content:"\f36c"}.fa-red-river:before{content:"\f3e3"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-safari:before{content:"\f267"}.fa-google:before{content:"\f1a0"}.fa-font-awesome-alt:before,.fa-square-font-awesome-stroke:before{content:"\f35c"}.fa-atlassian:before{content:"\f77b"}.fa-linkedin-in:before{content:"\f0e1"}.fa-digital-ocean:before{content:"\f391"}.fa-nimblr:before{content:"\f5a8"}.fa-chromecast:before{content:"\f838"}.fa-evernote:before{content:"\f839"}.fa-hacker-news:before{content:"\f1d4"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-adversal:before{content:"\f36a"}.fa-creative-commons:before{content:"\f25e"}.fa-watchman-monitoring:before{content:"\e087"}.fa-fonticons:before{content:"\f280"}.fa-weixin:before{content:"\f1d7"}.fa-shirtsinbulk:before{content:"\f214"}.fa-codepen:before{content:"\f1cb"}.fa-git-alt:before{content:"\f841"}.fa-lyft:before{content:"\f3c3"}.fa-rev:before{content:"\f5b2"}.fa-windows:before{content:"\f17a"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-square-viadeo:before,.fa-viadeo-square:before{content:"\f2aa"}.fa-meetup:before{content:"\f2e0"}.fa-centos:before{content:"\f789"}.fa-adn:before{content:"\f170"}.fa-cloudsmith:before{content:"\f384"}.fa-opensuse:before{content:"\e62b"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-dribbble-square:before,.fa-square-dribbble:before{content:"\f397"}.fa-codiepie:before{content:"\f284"}.fa-node:before{content:"\f419"}.fa-mix:before{content:"\f3cb"}.fa-steam:before{content:"\f1b6"}.fa-cc-apple-pay:before{content:"\f416"}.fa-scribd:before{content:"\f28a"}.fa-debian:before{content:"\e60b"}.fa-openid:before{content:"\f19b"}.fa-instalod:before{content:"\e081"}.fa-expeditedssl:before{content:"\f23e"}.fa-sellcast:before{content:"\f2da"}.fa-square-twitter:before,.fa-twitter-square:before{content:"\f081"}.fa-r-project:before{content:"\f4f7"}.fa-delicious:before{content:"\f1a5"}.fa-freebsd:before{content:"\f3a4"}.fa-vuejs:before{content:"\f41f"}.fa-accusoft:before{content:"\f369"}.fa-ioxhost:before{content:"\f208"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-app-store:before{content:"\f36f"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-itunes-note:before{content:"\f3b5"}.fa-golang:before{content:"\e40f"}.fa-kickstarter:before,.fa-square-kickstarter:before{content:"\f3bb"}.fa-grav:before{content:"\f2d6"}.fa-weibo:before{content:"\f18a"}.fa-uncharted:before{content:"\e084"}.fa-firstdraft:before{content:"\f3a1"}.fa-square-youtube:before,.fa-youtube-square:before{content:"\f431"}.fa-wikipedia-w:before{content:"\f266"}.fa-rendact:before,.fa-wpressr:before{content:"\f3e4"}.fa-angellist:before{content:"\f209"}.fa-galactic-republic:before{content:"\f50c"}.fa-nfc-directional:before{content:"\e530"}.fa-skype:before{content:"\f17e"}.fa-joget:before{content:"\f3b7"}.fa-fedora:before{content:"\f798"}.fa-stripe-s:before{content:"\f42a"}.fa-meta:before{content:"\e49b"}.fa-laravel:before{content:"\f3bd"}.fa-hotjar:before{content:"\f3b1"}.fa-bluetooth-b:before{content:"\f294"}.fa-square-letterboxd:before{content:"\e62e"}.fa-sticker-mule:before{content:"\f3f7"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-hips:before{content:"\f452"}.fa-behance:before{content:"\f1b4"}.fa-reddit:before{content:"\f1a1"}.fa-discord:before{content:"\f392"}.fa-chrome:before{content:"\f268"}.fa-app-store-ios:before{content:"\f370"}.fa-cc-discover:before{content:"\f1f2"}.fa-wpbeginner:before{content:"\f297"}.fa-confluence:before{content:"\f78d"}.fa-shoelace:before{content:"\e60c"}.fa-mdb:before{content:"\f8ca"}.fa-dochub:before{content:"\f394"}.fa-accessible-icon:before{content:"\f368"}.fa-ebay:before{content:"\f4f4"}.fa-amazon:before{content:"\f270"}.fa-unsplash:before{content:"\e07c"}.fa-yarn:before{content:"\f7e3"}.fa-square-steam:before,.fa-steam-square:before{content:"\f1b7"}.fa-500px:before{content:"\f26e"}.fa-square-vimeo:before,.fa-vimeo-square:before{content:"\f194"}.fa-asymmetrik:before{content:"\f372"}.fa-font-awesome-flag:before,.fa-font-awesome-logo-full:before,.fa-font-awesome:before{content:"\f2b4"}.fa-gratipay:before{content:"\f184"}.fa-apple:before{content:"\f179"}.fa-hive:before{content:"\e07f"}.fa-gitkraken:before{content:"\f3a6"}.fa-keybase:before{content:"\f4f5"}.fa-apple-pay:before{content:"\f415"}.fa-padlet:before{content:"\e4a0"}.fa-amazon-pay:before{content:"\f42c"}.fa-github-square:before,.fa-square-github:before{content:"\f092"}.fa-stumbleupon:before{content:"\f1a4"}.fa-fedex:before{content:"\f797"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-shopify:before{content:"\e057"}.fa-neos:before{content:"\f612"}.fa-square-threads:before{content:"\e619"}.fa-hackerrank:before{content:"\f5f7"}.fa-researchgate:before{content:"\f4f8"}.fa-swift:before{content:"\f8e1"}.fa-angular:before{content:"\f420"}.fa-speakap:before{content:"\f3f3"}.fa-angrycreative:before{content:"\f36e"}.fa-y-combinator:before{content:"\f23b"}.fa-empire:before{content:"\f1d1"}.fa-envira:before{content:"\f299"}.fa-google-scholar:before{content:"\e63b"}.fa-gitlab-square:before,.fa-square-gitlab:before{content:"\e5ae"}.fa-studiovinari:before{content:"\f3f8"}.fa-pied-piper:before{content:"\f2ae"}.fa-wordpress:before{content:"\f19a"}.fa-product-hunt:before{content:"\f288"}.fa-firefox:before{content:"\f269"}.fa-linode:before{content:"\f2b8"}.fa-goodreads:before{content:"\f3a8"}.fa-odnoklassniki-square:before,.fa-square-odnoklassniki:before{content:"\f264"}.fa-jsfiddle:before{content:"\f1cc"}.fa-sith:before{content:"\f512"}.fa-themeisle:before{content:"\f2b2"}.fa-page4:before{content:"\f3d7"}.fa-hashnode:before{content:"\e499"}.fa-react:before{content:"\f41b"}.fa-cc-paypal:before{content:"\f1f4"}.fa-squarespace:before{content:"\f5be"}.fa-cc-stripe:before{content:"\f1f5"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-bitcoin:before{content:"\f379"}.fa-keycdn:before{content:"\f3ba"}.fa-opera:before{content:"\f26a"}.fa-itch-io:before{content:"\f83a"}.fa-umbraco:before{content:"\f8e8"}.fa-galactic-senate:before{content:"\f50d"}.fa-ubuntu:before{content:"\f7df"}.fa-draft2digital:before{content:"\f396"}.fa-stripe:before{content:"\f429"}.fa-houzz:before{content:"\f27c"}.fa-gg:before{content:"\f260"}.fa-dhl:before{content:"\f790"}.fa-pinterest-square:before,.fa-square-pinterest:before{content:"\f0d3"}.fa-xing:before{content:"\f168"}.fa-blackberry:before{content:"\f37b"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-playstation:before{content:"\f3df"}.fa-quinscape:before{content:"\f459"}.fa-less:before{content:"\f41d"}.fa-blogger-b:before{content:"\f37d"}.fa-opencart:before{content:"\f23d"}.fa-vine:before{content:"\f1ca"}.fa-signal-messenger:before{content:"\e663"}.fa-paypal:before{content:"\f1ed"}.fa-gitlab:before{content:"\f296"}.fa-typo3:before{content:"\f42b"}.fa-reddit-alien:before{content:"\f281"}.fa-yahoo:before{content:"\f19e"}.fa-dailymotion:before{content:"\e052"}.fa-affiliatetheme:before{content:"\f36b"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-bootstrap:before{content:"\f836"}.fa-odnoklassniki:before{content:"\f263"}.fa-nfc-symbol:before{content:"\e531"}.fa-mintbit:before{content:"\e62f"}.fa-ethereum:before{content:"\f42e"}.fa-speaker-deck:before{content:"\f83c"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-patreon:before{content:"\f3d9"}.fa-avianex:before{content:"\f374"}.fa-ello:before{content:"\f5f1"}.fa-gofore:before{content:"\f3a7"}.fa-bimobject:before{content:"\f378"}.fa-brave-reverse:before{content:"\e63d"}.fa-facebook-f:before{content:"\f39e"}.fa-google-plus-square:before,.fa-square-google-plus:before{content:"\f0d4"}.fa-web-awesome:before{content:"\e682"}.fa-mandalorian:before{content:"\f50f"}.fa-first-order-alt:before{content:"\f50a"}.fa-osi:before{content:"\f41a"}.fa-google-wallet:before{content:"\f1ee"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-periscope:before{content:"\f3da"}.fa-fulcrum:before{content:"\f50b"}.fa-cloudscale:before{content:"\f383"}.fa-forumbee:before{content:"\f211"}.fa-mizuni:before{content:"\f3cc"}.fa-schlix:before{content:"\f3ea"}.fa-square-xing:before,.fa-xing-square:before{content:"\f169"}.fa-bandcamp:before{content:"\f2d5"}.fa-wpforms:before{content:"\f298"}.fa-cloudversify:before{content:"\f385"}.fa-usps:before{content:"\f7e1"}.fa-megaport:before{content:"\f5a3"}.fa-magento:before{content:"\f3c4"}.fa-spotify:before{content:"\f1bc"}.fa-optin-monster:before{content:"\f23c"}.fa-fly:before{content:"\f417"}.fa-aviato:before{content:"\f421"}.fa-itunes:before{content:"\f3b4"}.fa-cuttlefish:before{content:"\f38c"}.fa-blogger:before{content:"\f37c"}.fa-flickr:before{content:"\f16e"}.fa-viber:before{content:"\f409"}.fa-soundcloud:before{content:"\f1be"}.fa-digg:before{content:"\f1a6"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-letterboxd:before{content:"\e62d"}.fa-symfony:before{content:"\f83d"}.fa-maxcdn:before{content:"\f136"}.fa-etsy:before{content:"\f2d7"}.fa-facebook-messenger:before{content:"\f39f"}.fa-audible:before{content:"\f373"}.fa-think-peaks:before{content:"\f731"}.fa-bilibili:before{content:"\e3d9"}.fa-erlang:before{content:"\f39d"}.fa-x-twitter:before{content:"\e61b"}.fa-cotton-bureau:before{content:"\f89e"}.fa-dashcube:before{content:"\f210"}.fa-42-group:before,.fa-innosoft:before{content:"\e080"}.fa-stack-exchange:before{content:"\f18d"}.fa-elementor:before{content:"\f430"}.fa-pied-piper-square:before,.fa-square-pied-piper:before{content:"\e01e"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-palfed:before{content:"\f3d8"}.fa-superpowers:before{content:"\f2dd"}.fa-resolving:before{content:"\f3e7"}.fa-xbox:before{content:"\f412"}.fa-square-web-awesome-stroke:before{content:"\e684"}.fa-searchengin:before{content:"\f3eb"}.fa-tiktok:before{content:"\e07b"}.fa-facebook-square:before,.fa-square-facebook:before{content:"\f082"}.fa-renren:before{content:"\f18b"}.fa-linux:before{content:"\f17c"}.fa-glide:before{content:"\f2a5"}.fa-linkedin:before{content:"\f08c"}.fa-hubspot:before{content:"\f3b2"}.fa-deploydog:before{content:"\f38e"}.fa-twitch:before{content:"\f1e8"}.fa-ravelry:before{content:"\f2d9"}.fa-mixer:before{content:"\e056"}.fa-lastfm-square:before,.fa-square-lastfm:before{content:"\f203"}.fa-vimeo:before{content:"\f40a"}.fa-mendeley:before{content:"\f7b3"}.fa-uniregistry:before{content:"\f404"}.fa-figma:before{content:"\f799"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-dropbox:before{content:"\f16b"}.fa-instagram:before{content:"\f16d"}.fa-cmplid:before{content:"\e360"}.fa-upwork:before{content:"\e641"}.fa-facebook:before{content:"\f09a"}.fa-gripfire:before{content:"\f3ac"}.fa-jedi-order:before{content:"\f50e"}.fa-uikit:before{content:"\f403"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-phabricator:before{content:"\f3db"}.fa-ussunnah:before{content:"\f407"}.fa-earlybirds:before{content:"\f39a"}.fa-trade-federation:before{content:"\f513"}.fa-autoprefixer:before{content:"\f41c"}.fa-whatsapp:before{content:"\f232"}.fa-square-upwork:before{content:"\e67c"}.fa-slideshare:before{content:"\f1e7"}.fa-google-play:before{content:"\f3ab"}.fa-viadeo:before{content:"\f2a9"}.fa-line:before{content:"\f3c0"}.fa-google-drive:before{content:"\f3aa"}.fa-servicestack:before{content:"\f3ec"}.fa-simplybuilt:before{content:"\f215"}.fa-bitbucket:before{content:"\f171"}.fa-imdb:before{content:"\f2d8"}.fa-deezer:before{content:"\e077"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-jira:before{content:"\f7b1"}.fa-docker:before{content:"\f395"}.fa-screenpal:before{content:"\e570"}.fa-bluetooth:before{content:"\f293"}.fa-gitter:before{content:"\f426"}.fa-d-and-d:before{content:"\f38d"}.fa-microblog:before{content:"\e01a"}.fa-cc-diners-club:before{content:"\f24c"}.fa-gg-circle:before{content:"\f261"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-yandex:before{content:"\f413"}.fa-readme:before{content:"\f4d5"}.fa-html5:before{content:"\f13b"}.fa-sellsy:before{content:"\f213"}.fa-square-web-awesome:before{content:"\e683"}.fa-sass:before{content:"\f41e"}.fa-wirsindhandwerk:before,.fa-wsh:before{content:"\e2d0"}.fa-buromobelexperte:before{content:"\f37f"}.fa-salesforce:before{content:"\f83b"}.fa-octopus-deploy:before{content:"\e082"}.fa-medapps:before{content:"\f3c6"}.fa-ns8:before{content:"\f3d5"}.fa-pinterest-p:before{content:"\f231"}.fa-apper:before{content:"\f371"}.fa-fort-awesome:before{content:"\f286"}.fa-waze:before{content:"\f83f"}.fa-bluesky:before{content:"\e671"}.fa-cc-jcb:before{content:"\f24b"}.fa-snapchat-ghost:before,.fa-snapchat:before{content:"\f2ab"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-rust:before{content:"\e07a"}.fa-wix:before{content:"\f5cf"}.fa-behance-square:before,.fa-square-behance:before{content:"\f1b5"}.fa-supple:before{content:"\f3f9"}.fa-webflow:before{content:"\e65c"}.fa-rebel:before{content:"\f1d0"}.fa-css3:before{content:"\f13c"}.fa-staylinked:before{content:"\f3f5"}.fa-kaggle:before{content:"\f5fa"}.fa-space-awesome:before{content:"\e5ac"}.fa-deviantart:before{content:"\f1bd"}.fa-cpanel:before{content:"\f388"}.fa-goodreads-g:before{content:"\f3a9"}.fa-git-square:before,.fa-square-git:before{content:"\f1d2"}.fa-square-tumblr:before,.fa-tumblr-square:before{content:"\f174"}.fa-trello:before{content:"\f181"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-get-pocket:before{content:"\f265"}.fa-perbyte:before{content:"\e083"}.fa-grunt:before{content:"\f3ad"}.fa-weebly:before{content:"\f5cc"}.fa-connectdevelop:before{content:"\f20e"}.fa-leanpub:before{content:"\f212"}.fa-black-tie:before{content:"\f27e"}.fa-themeco:before{content:"\f5c6"}.fa-python:before{content:"\f3e2"}.fa-android:before{content:"\f17b"}.fa-bots:before{content:"\e340"}.fa-free-code-camp:before{content:"\f2c5"}.fa-hornbill:before{content:"\f592"}.fa-js:before{content:"\f3b8"}.fa-ideal:before{content:"\e013"}.fa-git:before{content:"\f1d3"}.fa-dev:before{content:"\f6cc"}.fa-sketch:before{content:"\f7c6"}.fa-yandex-international:before{content:"\f414"}.fa-cc-amex:before{content:"\f1f3"}.fa-uber:before{content:"\f402"}.fa-github:before{content:"\f09b"}.fa-php:before{content:"\f457"}.fa-alipay:before{content:"\f642"}.fa-youtube:before{content:"\f167"}.fa-skyatlas:before{content:"\f216"}.fa-firefox-browser:before{content:"\e007"}.fa-replyd:before{content:"\f3e6"}.fa-suse:before{content:"\f7d6"}.fa-jenkins:before{content:"\f3b6"}.fa-twitter:before{content:"\f099"}.fa-rockrms:before{content:"\f3e9"}.fa-pinterest:before{content:"\f0d2"}.fa-buffer:before{content:"\f837"}.fa-npm:before{content:"\f3d4"}.fa-yammer:before{content:"\f840"}.fa-btc:before{content:"\f15a"}.fa-dribbble:before{content:"\f17d"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-internet-explorer:before{content:"\f26b"}.fa-stubber:before{content:"\e5c7"}.fa-telegram-plane:before,.fa-telegram:before{content:"\f2c6"}.fa-old-republic:before{content:"\f510"}.fa-odysee:before{content:"\e5c6"}.fa-square-whatsapp:before,.fa-whatsapp-square:before{content:"\f40c"}.fa-node-js:before{content:"\f3d3"}.fa-edge-legacy:before{content:"\e078"}.fa-slack-hash:before,.fa-slack:before{content:"\f198"}.fa-medrt:before{content:"\f3c8"}.fa-usb:before{content:"\f287"}.fa-tumblr:before{content:"\f173"}.fa-vaadin:before{content:"\f408"}.fa-quora:before{content:"\f2c4"}.fa-square-x-twitter:before{content:"\e61a"}.fa-reacteurope:before{content:"\f75d"}.fa-medium-m:before,.fa-medium:before{content:"\f23a"}.fa-amilia:before{content:"\f36d"}.fa-mixcloud:before{content:"\f289"}.fa-flipboard:before{content:"\f44d"}.fa-viacoin:before{content:"\f237"}.fa-critical-role:before{content:"\f6c9"}.fa-sitrox:before{content:"\e44a"}.fa-discourse:before{content:"\f393"}.fa-joomla:before{content:"\f1aa"}.fa-mastodon:before{content:"\f4f6"}.fa-airbnb:before{content:"\f834"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-buy-n-large:before{content:"\f8a6"}.fa-gulp:before{content:"\f3ae"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-strava:before{content:"\f428"}.fa-ember:before{content:"\f423"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-teamspeak:before{content:"\f4f9"}.fa-pushed:before{content:"\f3e1"}.fa-wordpress-simple:before{content:"\f411"}.fa-nutritionix:before{content:"\f3d6"}.fa-wodu:before{content:"\e088"}.fa-google-pay:before{content:"\e079"}.fa-intercom:before{content:"\f7af"}.fa-zhihu:before{content:"\f63f"}.fa-korvue:before{content:"\f42f"}.fa-pix:before{content:"\e43a"}.fa-steam-symbol:before{content:"\f3f6"}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
\ No newline at end of file
diff --git a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.ttf b/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.ttf
deleted file mode 100644
index 1fbb1f7c..00000000
Binary files a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.ttf and /dev/null differ
diff --git a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.woff2 b/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.woff2
deleted file mode 100644
index 5d280216..00000000
Binary files a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-brands-400.woff2 and /dev/null differ
diff --git a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.ttf b/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.ttf
deleted file mode 100644
index 549d68dc..00000000
Binary files a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.ttf and /dev/null differ
diff --git a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.woff2 b/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.woff2
deleted file mode 100644
index 18400d7f..00000000
Binary files a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-regular-400.woff2 and /dev/null differ
diff --git a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.ttf b/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.ttf
deleted file mode 100644
index bb2a8695..00000000
Binary files a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.ttf and /dev/null differ
diff --git a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.woff2 b/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.woff2
deleted file mode 100644
index 758dd4f6..00000000
Binary files a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-solid-900.woff2 and /dev/null differ
diff --git a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.ttf b/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.ttf
deleted file mode 100644
index 8c5864c4..00000000
Binary files a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.ttf and /dev/null differ
diff --git a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.woff2 b/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.woff2
deleted file mode 100644
index f94bec22..00000000
Binary files a/exo/tinychat/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/webfonts/fa-v4compatibility.woff2 and /dev/null differ
diff --git a/exo/tinychat/static/fonts.googleapis.com/css2 b/exo/tinychat/static/fonts.googleapis.com/css2
deleted file mode 100644
index 82f55e83..00000000
--- a/exo/tinychat/static/fonts.googleapis.com/css2
+++ /dev/null
@@ -1,7 +0,0 @@
-@font-face {
- font-family: 'Megrim';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: url(https://fonts.gstatic.com/s/megrim/v16/46kulbz5WjvLqJZlbQ.ttf) format('truetype');
-}
diff --git a/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js b/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js
deleted file mode 100644
index 5d699ae6..00000000
--- a/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js
+++ /dev/null
@@ -1,1213 +0,0 @@
-/*!
- Highlight.js v11.9.0 (git: f47103d4f1)
- (c) 2006-2023 undefined and other contributors
- License: BSD-3-Clause
- */
-var hljs=function(){"use strict";function e(n){
-return n instanceof Map?n.clear=n.delete=n.set=()=>{
-throw Error("map is read-only")}:n instanceof Set&&(n.add=n.clear=n.delete=()=>{
-throw Error("set is read-only")
-}),Object.freeze(n),Object.getOwnPropertyNames(n).forEach((t=>{
-const a=n[t],i=typeof a;"object"!==i&&"function"!==i||Object.isFrozen(a)||e(a)
-})),n}class n{constructor(e){
-void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}
-ignoreMatch(){this.isMatchIgnored=!0}}function t(e){
-return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")
-}function a(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n]
-;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const i=e=>!!e.scope
-;class r{constructor(e,n){
-this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){
-this.buffer+=t(e)}openNode(e){if(!i(e))return;const n=((e,{prefix:n})=>{
-if(e.startsWith("language:"))return e.replace("language:","language-")
-;if(e.includes(".")){const t=e.split(".")
-;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ")
-}return`${n}${e}`})(e.scope,{prefix:this.classPrefix});this.span(n)}
-closeNode(e){i(e)&&(this.buffer+="</span>")}value(){return this.buffer}span(e){
-this.buffer+=`<span class="${e}">`}}const s=(e={})=>{const n={children:[]}
-;return Object.assign(n,e),n};class o{constructor(){
-this.rootNode=s(),this.stack=[this.rootNode]}get top(){
-return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
-this.top.children.push(e)}openNode(e){const n=s({scope:e})
-;this.add(n),this.stack.push(n)}closeNode(){
-if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
-for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
-walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){
-return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),
-n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){
-"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
-o._collapse(e)})))}}class l extends o{constructor(e){super(),this.options=e}
-addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){
-this.closeNode()}__addSublanguage(e,n){const t=e.root
-;n&&(t.scope="language:"+n),this.add(t)}toHTML(){
-return new r(this,this.options).value()}finalize(){
-return this.closeAllNodes(),!0}}function c(e){
-return e?"string"==typeof e?e:e.source:null}function d(e){return b("(?=",e,")")}
-function g(e){return b("(?:",e,")*")}function u(e){return b("(?:",e,")?")}
-function b(...e){return e.map((e=>c(e))).join("")}function m(...e){const n=(e=>{
-const n=e[e.length-1]
-;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{}
-})(e);return"("+(n.capture?"":"?:")+e.map((e=>c(e))).join("|")+")"}
-function p(e){return RegExp(e.toString()+"|").exec("").length-1}
-const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./
-;function h(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t
-;let a=c(e),i="";for(;a.length>0;){const e=_.exec(a);if(!e){i+=a;break}
-i+=a.substring(0,e.index),
-a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0],
-"("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)}
-const f="[a-zA-Z]\\w*",E="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",v={
-begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'",
-illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n",
-contains:[v]},x=(e,n,t={})=>{const i=a({scope:"comment",begin:e,end:n,
-contains:[]},t);i.contains.push({scope:"doctag",
-begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
-end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
-;const r=m("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/)
-;return i.contains.push({begin:b(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i
-},M=x("//","$"),S=x("/\\*","\\*/"),A=x("#","$");var C=Object.freeze({
-__proto__:null,APOS_STRING_MODE:O,BACKSLASH_ESCAPE:v,BINARY_NUMBER_MODE:{
-scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:x,
-C_BLOCK_COMMENT_MODE:S,C_LINE_COMMENT_MODE:M,C_NUMBER_MODE:{scope:"number",
-begin:N,relevance:0},C_NUMBER_RE:N,END_SAME_AS_BEGIN:e=>Object.assign(e,{
-"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{
-n.data._beginMatch!==e[1]&&n.ignoreMatch()}}),HASH_COMMENT_MODE:A,IDENT_RE:f,
-MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+E,relevance:0},
-NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y,
-PHRASAL_WORDS_MODE:{
-begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
-},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/,
-end:/\/[gimuy]*/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]},
-RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
-SHEBANG:(e={})=>{const n=/^#![ ]*\//
-;return e.binary&&(e.begin=b(n,/.*\b/,e.binary,/\b.*/)),a({scope:"meta",begin:n,
-end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)},
-TITLE_MODE:{scope:"title",begin:f,relevance:0},UNDERSCORE_IDENT_RE:E,
-UNDERSCORE_TITLE_MODE:{scope:"title",begin:E,relevance:0}});function T(e,n){
-"."===e.input[e.index-1]&&n.ignoreMatch()}function R(e,n){
-void 0!==e.className&&(e.scope=e.className,delete e.className)}function D(e,n){
-n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
-e.__beforeBegin=T,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
-void 0===e.relevance&&(e.relevance=0))}function I(e,n){
-Array.isArray(e.illegal)&&(e.illegal=m(...e.illegal))}function L(e,n){
-if(e.match){
-if(e.begin||e.end)throw Error("begin & end are not supported with match")
-;e.begin=e.match,delete e.match}}function B(e,n){
-void 0===e.relevance&&(e.relevance=1)}const $=(e,n)=>{if(!e.beforeMatch)return
-;if(e.starts)throw Error("beforeMatch cannot be used with starts")
-;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n]
-})),e.keywords=t.keywords,e.begin=b(t.beforeMatch,d(t.begin)),e.starts={
-relevance:0,contains:[Object.assign(t,{endsParent:!0})]
-},e.relevance=0,delete t.beforeMatch
-},z=["of","and","for","in","not","or","if","then","parent","list","value"],F="keyword"
-;function U(e,n,t=F){const a=Object.create(null)
-;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{
-Object.assign(a,U(e[t],n,t))})),a;function i(e,t){
-n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|")
-;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){
-return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{
-console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{
-P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0)
-},G=Error();function Z(e,n,{key:t}){let a=0;const i=e[t],r={},s={}
-;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=p(n[e-1])
-;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{
-e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,
-delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={
-_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope
-}),(e=>{if(Array.isArray(e.begin)){
-if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),
-G
-;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"),
-G;Z(e,e.begin,{key:"beginScope"}),e.begin=h(e.begin,{joinWith:""})}})(e),(e=>{
-if(Array.isArray(e.end)){
-if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"),
-G
-;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"),
-G;Z(e,e.end,{key:"endScope"}),e.end=h(e.end,{joinWith:""})}})(e)}function Q(e){
-function n(n,t){
-return RegExp(c(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":""))
-}class t{constructor(){
-this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
-addRule(e,n){
-n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),
-this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null)
-;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(h(e,{joinWith:"|"
-}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex
-;const n=this.matcherRe.exec(e);if(!n)return null
-;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t]
-;return n.splice(0,t),Object.assign(n,a)}}class i{constructor(){
-this.rules=[],this.multiRegexes=[],
-this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
-if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t
-;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))),
-n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){
-return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){
-this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){
-const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex
-;let t=n.exec(e)
-;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{
-const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)}
-return t&&(this.regexIndex+=t.position+1,
-this.regexIndex===this.count&&this.considerAll()),t}}
-if(e.compilerExtensions||(e.compilerExtensions=[]),
-e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.")
-;return e.classNameAliases=a(e.classNameAliases||{}),function t(r,s){const o=r
-;if(r.isCompiled)return o
-;[R,L,W,$].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))),
-r.__beforeBegin=null,[D,I,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null
-;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords),
-l=r.keywords.$pattern,
-delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)),
-o.keywordPatternRe=n(l,!0),
-s&&(r.begin||(r.begin=/\B|\b/),o.beginRe=n(o.begin),r.end||r.endsWithParent||(r.end=/\B|\b/),
-r.end&&(o.endRe=n(o.end)),
-o.terminatorEnd=c(o.end)||"",r.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)),
-r.illegal&&(o.illegalRe=n(r.illegal)),
-r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>a(e,{
-variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?a(e,{
-starts:e.starts?a(e.starts):null
-}):Object.isFrozen(e)?a(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{t(e,o)
-})),r.starts&&t(r.starts,s),o.matcher=(e=>{const n=new i
-;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin"
-}))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end"
-}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){
-return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{
-constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}}
-const J=t,Y=a,ee=Symbol("nomatch"),ne=t=>{
-const a=Object.create(null),i=Object.create(null),r=[];let s=!0
-;const o="Could not find the language '{}', did you forget to load/include a language module?",c={
-disableAutodetect:!0,name:"Plain text",contains:[]};let p={
-ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,
-languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
-cssSelector:"pre code",languages:null,__emitter:l};function _(e){
-return p.noHighlightRe.test(e)}function h(e,n,t){let a="",i=""
-;"object"==typeof n?(a=e,
-t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."),
-q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),
-i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r)
-;const s=r.result?r.result:f(r.language,r.code,t)
-;return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){
-const l=Object.create(null);function c(){if(!x.keywords)return void S.addText(A)
-;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(A),t=""
-;for(;n;){t+=A.substring(e,n.index)
-;const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){
-const[e,a]=r
-;if(S.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(C+=a),e.startsWith("_"))t+=n[0];else{
-const t=w.classNameAliases[e]||e;g(n[0],t)}}else t+=n[0]
-;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(A)}var a
-;t+=A.substring(e),S.addText(t)}function d(){null!=x.subLanguage?(()=>{
-if(""===A)return;let e=null;if("string"==typeof x.subLanguage){
-if(!a[x.subLanguage])return void S.addText(A)
-;e=f(x.subLanguage,A,!0,M[x.subLanguage]),M[x.subLanguage]=e._top
-}else e=E(A,x.subLanguage.length?x.subLanguage:null)
-;x.relevance>0&&(C+=e.relevance),S.__addSublanguage(e._emitter,e.language)
-})():c(),A=""}function g(e,n){
-""!==e&&(S.startScope(n),S.addText(e),S.endScope())}function u(e,n){let t=1
-;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue}
-const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?g(i,a):(A=i,c(),A=""),t++}}
-function b(e,n){
-return e.scope&&"string"==typeof e.scope&&S.openNode(w.classNameAliases[e.scope]||e.scope),
-e.beginScope&&(e.beginScope._wrap?(g(A,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),
-A=""):e.beginScope._multi&&(u(e.beginScope,n),A="")),x=Object.create(e,{parent:{
-value:x}}),x}function m(e,t,a){let i=((e,n)=>{const t=e&&e.exec(n)
-;return t&&0===t.index})(e.endRe,a);if(i){if(e["on:end"]){const a=new n(e)
-;e["on:end"](t,a),a.isMatchIgnored&&(i=!1)}if(i){
-for(;e.endsParent&&e.parent;)e=e.parent;return e}}
-if(e.endsWithParent)return m(e.parent,t,a)}function _(e){
-return 0===x.matcher.regexIndex?(A+=e[0],1):(D=!0,0)}function h(e){
-const n=e[0],a=t.substring(e.index),i=m(x,e,a);if(!i)return ee;const r=x
-;x.endScope&&x.endScope._wrap?(d(),
-g(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(d(),
-u(x.endScope,e)):r.skip?A+=n:(r.returnEnd||r.excludeEnd||(A+=n),
-d(),r.excludeEnd&&(A=n));do{
-x.scope&&S.closeNode(),x.skip||x.subLanguage||(C+=x.relevance),x=x.parent
-}while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length}
-let y={};function N(a,r){const o=r&&r[0];if(A+=a,null==o)return d(),0
-;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===o){
-if(A+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`)
-;throw n.languageName=e,n.badRule=y.rule,n}return 1}
-if(y=r,"begin"===r.type)return(e=>{
-const t=e[0],a=e.rule,i=new n(a),r=[a.__beforeBegin,a["on:begin"]]
-;for(const n of r)if(n&&(n(e,i),i.isMatchIgnored))return _(t)
-;return a.skip?A+=t:(a.excludeBegin&&(A+=t),
-d(),a.returnBegin||a.excludeBegin||(A=t)),b(a,e),a.returnBegin?0:t.length})(r)
-;if("illegal"===r.type&&!i){
-const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"<unnamed>")+'"')
-;throw e.mode=x,e}if("end"===r.type){const e=h(r);if(e!==ee)return e}
-if("illegal"===r.type&&""===o)return 1
-;if(R>1e5&&R>3*r.index)throw Error("potential infinite loop, way more iterations than matches")
-;return A+=o,o.length}const w=v(e)
-;if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"')
-;const O=Q(w);let k="",x=r||O;const M={},S=new p.__emitter(p);(()=>{const e=[]
-;for(let n=x;n!==w;n=n.parent)n.scope&&e.unshift(n.scope)
-;e.forEach((e=>S.openNode(e)))})();let A="",C=0,T=0,R=0,D=!1;try{
-if(w.__emitTokens)w.__emitTokens(t,S);else{for(x.matcher.considerAll();;){
-R++,D?D=!1:x.matcher.considerAll(),x.matcher.lastIndex=T
-;const e=x.matcher.exec(t);if(!e)break;const n=N(t.substring(T,e.index),e)
-;T=e.index+n}N(t.substring(T))}return S.finalize(),k=S.toHTML(),{language:e,
-value:k,relevance:C,illegal:!1,_emitter:S,_top:x}}catch(n){
-if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t),
-illegal:!0,relevance:0,_illegalBy:{message:n.message,index:T,
-context:t.slice(T-100,T+100),mode:n.mode,resultSoFar:k},_emitter:S};if(s)return{
-language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:S,_top:x}
-;throw n}}function E(e,n){n=n||p.languages||Object.keys(a);const t=(e=>{
-const n={value:J(e),illegal:!1,relevance:0,_top:c,_emitter:new p.__emitter(p)}
-;return n._emitter.addText(e),n})(e),i=n.filter(v).filter(k).map((n=>f(n,e,!1)))
-;i.unshift(t);const r=i.sort(((e,n)=>{
-if(e.relevance!==n.relevance)return n.relevance-e.relevance
-;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1
-;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,l=s
-;return l.secondBest=o,l}function y(e){let n=null;const t=(e=>{
-let n=e.className+" ";n+=e.parentNode?e.parentNode.className:""
-;const t=p.languageDetectRe.exec(n);if(t){const n=v(t[1])
-;return n||(H(o.replace("{}",t[1])),
-H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"}
-return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return
-;if(x("before:highlightElement",{el:e,language:t
-}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e)
-;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),
-console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),
-console.warn("The element with unescaped HTML:"),
-console.warn(e)),p.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML)
-;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a)
-;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,n,t)=>{const a=n&&i[n]||t
-;e.classList.add("hljs"),e.classList.add("language-"+a)
-})(e,t,r.language),e.result={language:r.language,re:r.relevance,
-relevance:r.relevance},r.secondBest&&(e.secondBest={
-language:r.secondBest.language,relevance:r.secondBest.relevance
-}),x("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){
-"loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(y):N=!0
-}function v(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]}
-function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{
-i[e.toLowerCase()]=n}))}function k(e){const n=v(e)
-;return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach((e=>{
-e[t]&&e[t](n)}))}
-"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{
-N&&w()}),!1),Object.assign(t,{highlight:h,highlightAuto:E,highlightAll:w,
-highlightElement:y,
-highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"),
-q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)},
-initHighlighting:()=>{
-w(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},
-initHighlightingOnLoad:()=>{
-w(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")
-},registerLanguage:(e,n)=>{let i=null;try{i=n(t)}catch(n){
-if(K("Language definition for '{}' could not be registered.".replace("{}",e)),
-!s)throw n;K(n),i=c}
-i.name||(i.name=e),a[e]=i,i.rawDefinition=n.bind(null,t),i.aliases&&O(i.aliases,{
-languageName:e})},unregisterLanguage:e=>{delete a[e]
-;for(const n of Object.keys(i))i[n]===e&&delete i[n]},
-listLanguages:()=>Object.keys(a),getLanguage:v,registerAliases:O,
-autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{
-e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{
-e["before:highlightBlock"](Object.assign({block:n.el},n))
-}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{
-e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)},
-removePlugin:e=>{const n=r.indexOf(e);-1!==n&&r.splice(n,1)}}),t.debugMode=()=>{
-s=!1},t.safeMode=()=>{s=!0},t.versionString="11.9.0",t.regex={concat:b,
-lookahead:d,either:m,optional:u,anyNumberOfTimes:g}
-;for(const n in C)"object"==typeof C[n]&&e(C[n]);return Object.assign(t,C),t
-},te=ne({});te.newInstance=()=>ne({});var ae=te;const ie=e=>({IMPORTANT:{
-scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{
-scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},
-FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/},
-ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",
-contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{
-scope:"number",
-begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",
-relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/}
-}),re=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],se=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],oe=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],le=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],ce=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),de=oe.concat(le)
-;var ge="[0-9](_*[0-9])*",ue=`\\.(${ge})`,be="[0-9a-fA-F](_*[0-9a-fA-F])*",me={
-className:"number",variants:[{
-begin:`(\\b(${ge})((${ue})|\\.)?|(${ue}))[eE][+-]?(${ge})[fFdD]?\\b`},{
-begin:`\\b(${ge})((${ue})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{
-begin:`(${ue})[fFdD]?\\b`},{begin:`\\b(${ge})[fFdD]\\b`},{
-begin:`\\b0[xX]((${be})\\.?|(${be})?\\.(${be}))[pP][+-]?(${ge})[fFdD]?\\b`},{
-begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${be})[lL]?\\b`},{
-begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}],
-relevance:0};function pe(e,n,t){return-1===t?"":e.replace(n,(a=>pe(e,n,t-1)))}
-const _e="[A-Za-z$_][0-9A-Za-z$_]*",he=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],fe=["true","false","null","undefined","NaN","Infinity"],Ee=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],ye=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ne=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],we=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],ve=[].concat(Ne,Ee,ye)
-;function Oe(e){const n=e.regex,t=_e,a={begin:/<[A-Za-z0-9\\._:-]+/,
-end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{
-const t=e[0].length+e.index,a=e.input[t]
-;if("<"===a||","===a)return void n.ignoreMatch();let i
-;">"===a&&(((e,{after:n})=>{const t="</"+e[0].slice(1)
-;return-1!==e.input.indexOf(t,n)})(e,{after:t})||n.ignoreMatch())
-;const r=e.input.substring(t)
-;((i=r.match(/^\s*=/))||(i=r.match(/^\s+extends\s+/))&&0===i.index)&&n.ignoreMatch()
-}},i={$pattern:_e,keyword:he,literal:fe,built_in:ve,"variable.language":we
-},r="[0-9](_?[0-9])*",s=`\\.(${r})`,o="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",l={
-className:"number",variants:[{
-begin:`(\\b(${o})((${s})|\\.)?|(${s}))[eE][+-]?(${r})\\b`},{
-begin:`\\b(${o})\\b((${s})\\b|\\.)?|(${s})\\b`},{
-begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{
-begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{
-begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{
-begin:"\\b0[0-7]+n?\\b"}],relevance:0},c={className:"subst",begin:"\\$\\{",
-end:"\\}",keywords:i,contains:[]},d={begin:"html`",end:"",starts:{end:"`",
-returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,c],subLanguage:"xml"}},g={
-begin:"css`",end:"",starts:{end:"`",returnEnd:!1,
-contains:[e.BACKSLASH_ESCAPE,c],subLanguage:"css"}},u={begin:"gql`",end:"",
-starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,c],
-subLanguage:"graphql"}},b={className:"string",begin:"`",end:"`",
-contains:[e.BACKSLASH_ESCAPE,c]},m={className:"comment",
-variants:[e.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{
-begin:"(?=@[A-Za-z]+)",relevance:0,contains:[{className:"doctag",
-begin:"@[A-Za-z]+"},{className:"type",begin:"\\{",end:"\\}",excludeEnd:!0,
-excludeBegin:!0,relevance:0},{className:"variable",begin:t+"(?=\\s*(-)|$)",
-endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]
-}),e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE]
-},p=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,{match:/\$\d+/},l]
-;c.contains=p.concat({begin:/\{/,end:/\}/,keywords:i,contains:["self"].concat(p)
-});const _=[].concat(m,c.contains),h=_.concat([{begin:/\(/,end:/\)/,keywords:i,
-contains:["self"].concat(_)}]),f={className:"params",begin:/\(/,end:/\)/,
-excludeBegin:!0,excludeEnd:!0,keywords:i,contains:h},E={variants:[{
-match:[/class/,/\s+/,t,/\s+/,/extends/,/\s+/,n.concat(t,"(",n.concat(/\./,t),")*")],
-scope:{1:"keyword",3:"title.class",5:"keyword",7:"title.class.inherited"}},{
-match:[/class/,/\s+/,t],scope:{1:"keyword",3:"title.class"}}]},y={relevance:0,
-match:n.either(/\bJSON/,/\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/,/\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/,/\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/),
-className:"title.class",keywords:{_:[...Ee,...ye]}},N={variants:[{
-match:[/function/,/\s+/,t,/(?=\s*\()/]},{match:[/function/,/\s*(?=\()/]}],
-className:{1:"keyword",3:"title.function"},label:"func.def",contains:[f],
-illegal:/%/},w={
-match:n.concat(/\b/,(v=[...Ne,"super","import"],n.concat("(?!",v.join("|"),")")),t,n.lookahead(/\(/)),
-className:"title.function",relevance:0};var v;const O={
-begin:n.concat(/\./,n.lookahead(n.concat(t,/(?![0-9A-Za-z$_(])/))),end:t,
-excludeBegin:!0,keywords:"prototype",className:"property",relevance:0},k={
-match:[/get|set/,/\s+/,t,/(?=\()/],className:{1:"keyword",3:"title.function"},
-contains:[{begin:/\(\)/},f]
-},x="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+e.UNDERSCORE_IDENT_RE+")\\s*=>",M={
-match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(x)],
-keywords:"async",className:{1:"keyword",3:"title.function"},contains:[f]}
-;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{
-PARAMS_CONTAINS:h,CLASS_REFERENCE:y},illegal:/#(?![$_A-z])/,
-contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{
-label:"use_strict",className:"meta",relevance:10,
-begin:/^\s*['"]use (strict|asm)['"]/
-},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,m,{match:/\$\d+/},l,y,{
-className:"attr",begin:t+n.lookahead(":"),relevance:0},M,{
-begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",
-keywords:"return throw case",relevance:0,contains:[m,e.REGEXP_MODE,{
-className:"function",begin:x,returnBegin:!0,end:"\\s*=>",contains:[{
-className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{
-className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,
-excludeEnd:!0,keywords:i,contains:h}]}]},{begin:/,/,relevance:0},{match:/\s+/,
-relevance:0},{variants:[{begin:"<>",end:"</>"},{
-match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin,
-"on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{
-begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},N,{
-beginKeywords:"while if switch catch for"},{
-begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",
-returnBegin:!0,label:"func.def",contains:[f,e.inherit(e.TITLE_MODE,{begin:t,
-className:"title.function"})]},{match:/\.\.\./,relevance:0},O,{match:"\\$"+t,
-relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"},
-contains:[f]},w,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,
-className:"variable.constant"},E,k,{match:/\$[(.]/}]}}
-const ke=e=>b(/\b/,e,/\w$/.test(e)?/\b/:/\B/),xe=["Protocol","Type"].map(ke),Me=["init","self"].map(ke),Se=["Any","Self"],Ae=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Ce=["false","nil","true"],Te=["assignment","associativity","higherThan","left","lowerThan","none","right"],Re=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],De=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Ie=m(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Le=m(Ie,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),Be=b(Ie,Le,"*"),$e=m(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),ze=m($e,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Fe=b($e,ze,"*"),Ue=b(/[A-Z]/,ze,"*"),je=["attached","autoclosure",b(/convention\(/,m("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",b(/objc\(/,Fe,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],Pe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"]
-;var Ke=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={
-begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]}
-;Object.assign(t,{className:"variable",variants:[{
-begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={
-className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={
-begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,
-end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/,
-contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$?\(\(/,
-end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t]
-},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10
-}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,
-contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{
-name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/,
-keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"],
-literal:["true","false"],
-built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"]
-},contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{
-match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}},
-grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}]
-}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={
-className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{
-match:/\batomic_[a-z]{3,6}\b/}]},o={className:"string",variants:[{
-begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{
-begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
-end:"'",illegal:"."},e.END_SAME_AS_BEGIN({
-begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={
-className:"number",variants:[{begin:"\\b(0b[01']+)"},{
-begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"
-},{
-begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
-}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
-keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
-},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{
-className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={
-className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0
-},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={
-keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"],
-type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"],
-literal:"true false NULL",
-built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr"
-},b=[c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],m={variants:[{begin:/=/,end:/;/},{
-begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],
-keywords:u,contains:b.concat([{begin:/\(/,end:/\)/,keywords:u,
-contains:b.concat(["self"]),relevance:0}]),relevance:0},p={
-begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,
-keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{
-begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})],
-relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/,
-keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,
-end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]
-}]},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u,
-disableAutodetect:!0,illegal:"</",contains:[].concat(m,p,b,[c,{
-begin:e.IDENT_RE+"::",keywords:u},{className:"class",
-beginKeywords:"enum class struct union",end:/[{;:<>=]/,contains:[{
-beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c,
-strings:o,keywords:u}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{
-contains:[{begin:/\\\n/}]
-}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="(?!struct)("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={
-className:"type",begin:"\\b[a-z\\d_]*_t\\b"},o={className:"string",variants:[{
-begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{
-begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
-end:"'",illegal:"."},e.END_SAME_AS_BEGIN({
-begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={
-className:"number",variants:[{begin:"\\b(0b[01']+)"},{
-begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"
-},{
-begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
-}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
-keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
-},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{
-className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={
-className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0
-},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={
-type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"],
-keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"],
-literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"],
-_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"]
-},b={className:"function.dispatch",relevance:0,keywords:{
-_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"]
-},
-begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/))
-},m=[b,c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],p={variants:[{begin:/=/,end:/;/},{
-begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],
-keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u,
-contains:m.concat(["self"]),relevance:0}]),relevance:0},_={className:"function",
-begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,
-keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{
-begin:g,returnBegin:!0,contains:[d],relevance:0},{begin:/::/,relevance:0},{
-begin:/:/,endsWithParent:!0,contains:[o,l]},{relevance:0,match:/,/},{
-className:"params",begin:/\(/,end:/\)/,keywords:u,relevance:0,
-contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,end:/\)/,keywords:u,
-relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]}]
-},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C++",
-aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"</",
-classNameAliases:{"function.dispatch":"built_in"},
-contains:[].concat(p,_,b,m,[c,{
-begin:"\\b(deque|list|queue|priority_queue|pair|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array|tuple|optional|variant|function)\\s*<(?!<)",
-end:">",keywords:u,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:u},{
-match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/],
-className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={
-keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]),
-built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"],
-literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{
-begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{
-begin:"\\b(0b[01']+)"},{
-begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{
-begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
-}],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]
-},r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/,
-keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/,
-end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/
-},e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{
-begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/,
-contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]})
-;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE],
-o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{
-illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
-},u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t]
-},b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={
-begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],
-keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,
-contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{
-begin:"\x3c!--|--\x3e"},{begin:"</?",end:">"}]}]
-}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",
-end:"$",keywords:{
-keyword:"if else elif endif define undef warning error line region endregion pragma checksum"
-}},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/,
-illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"
-},t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",
-relevance:0,end:/[{;=]/,illegal:/[^\s:]/,
-contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{
-beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/,
-contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",
-begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{
-className:"string",begin:/"/,end:/"/}]},{
-beginKeywords:"new return throw await else",relevance:0},{className:"function",
-begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0,
-end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{
-beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial",
-relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0,
-contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params",
-begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,
-contains:[g,a,e.C_BLOCK_COMMENT_MODE]
-},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{
-const n=e.regex,t=ie(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{
-name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{
-keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"},
-contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/
-},t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0
-},{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0
-},t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{
-begin:":("+oe.join("|")+")"},{begin:":(:)?("+le.join("|")+")"}]
-},t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b"},{
-begin:/:/,end:/[;}{]/,
-contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{
-begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri"
-},contains:[...a,{className:"string",begin:/[^)]/,endsWithParent:!0,
-excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]",
-relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/
-},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{
-$pattern:/[a-z-]+/,keyword:"and or not only",attribute:se.join(" ")},contains:[{
-begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{
-className:"selector-tag",begin:"\\b("+re.join("|")+")\\b"}]}},grmr_diff:e=>{
-const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{
-className:"meta",relevance:10,
-match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/)
-},{className:"comment",variants:[{
-begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/),
-end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{
-className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/,
-end:/$/}]}},grmr_go:e=>{const n={
-keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"],
-type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"],
-literal:["true","false","iota","nil"],
-built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"]
-};return{name:"Go",aliases:["golang"],keywords:n,illegal:"</",
-contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"string",
-variants:[e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{begin:"`",end:"`"}]},{
-className:"number",variants:[{begin:e.C_NUMBER_RE+"[i]",relevance:1
-},e.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",
-end:"\\s*(\\{|$)",excludeEnd:!0,contains:[e.TITLE_MODE,{className:"params",
-begin:/\(/,end:/\)/,endsParent:!0,keywords:n,illegal:/["']/}]}]}},
-grmr_graphql:e=>{const n=e.regex;return{name:"GraphQL",aliases:["gql"],
-case_insensitive:!0,disableAutodetect:!1,keywords:{
-keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"],
-literal:["true","false","null"]},
-contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{
-scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation",
-begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/,
-end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{
-scope:"symbol",begin:n.concat(/[_A-Za-z][_0-9A-Za-z]*/,n.lookahead(/\s*:/)),
-relevance:0}],illegal:[/[;<']/,/BEGIN/]}},grmr_ini:e=>{const n=e.regex,t={
-className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{
-begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/,
-end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{
-begin:/\$\{(.*?)\}/}]},r={className:"literal",
-begin:/\bon|off|true|false|yes|no\b/},s={className:"string",
-contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{
-begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]
-},o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0
-},l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{
-name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,
-contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{
-begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)),
-className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{
-const n=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",a=t+pe("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={
-keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"],
-literal:["false","true","null"],
-type:["char","boolean","long","float","int","byte","short","double"],
-built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{
-begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/,
-end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0}
-;return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/,
-contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,
-relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{
-begin:/import java\.[a-z]+\./,keywords:"import",relevance:2
-},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/,
-className:"string",contains:[e.BACKSLASH_ESCAPE]
-},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{
-match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{
-1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{
-begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=(?!=)/],className:{1:"type",
-3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword",
-3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{
-beginKeywords:"new throw return else",relevance:0},{
-begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{
-2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/,
-end:/\)/,keywords:i,relevance:0,
-contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,me,e.C_BLOCK_COMMENT_MODE]
-},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},me,r]}},grmr_javascript:Oe,
-grmr_json:e=>{const n=["true","false","null"],t={scope:"literal",
-beginKeywords:n.join(" ")};return{name:"JSON",keywords:{literal:n},contains:[{
-className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{
-match:/[{}[\],:]/,className:"punctuation",relevance:0
-},e.QUOTE_STRING_MODE,t,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],
-illegal:"\\S"}},grmr_kotlin:e=>{const n={
-keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual",
-built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",
-literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@"
-},a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={
-className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string",
-variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'",
-illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,
-contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={
-className:"meta",
-begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?"
-},o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,
-end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}]
-},l=me,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={
-variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,
-contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g],
-{name:"Kotlin",aliases:["kt","kts"],keywords:n,
-contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",
-begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword",
-begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",
-begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$",
-returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{
-begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,
-contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin:/</,end:/>/,
-keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,
-endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,
-endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0
-},e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{
-begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{
-3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,
-illegal:"extends implements",contains:[{
-beginKeywords:"public protected internal private constructor"
-},e.UNDERSCORE_TITLE_MODE,{className:"type",begin:/</,end:/>/,excludeBegin:!0,
-excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/,
-excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env",
-end:"$",illegal:"\n"},l]}},grmr_less:e=>{
-const n=ie(e),t=de,a="[\\w-]+",i="("+a+"|@\\{"+a+"\\})",r=[],s=[],o=e=>({
-className:"string",begin:"~?"+e+".*?"+e}),l=(e,n,t)=>({className:e,begin:n,
-relevance:t}),c={$pattern:/[a-z-]+/,keyword:"and or not only",
-attribute:se.join(" ")},d={begin:"\\(",end:"\\)",contains:s,keywords:c,
-relevance:0}
-;s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o("'"),o('"'),n.CSS_NUMBER_MODE,{
-begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",
-excludeEnd:!0}
-},n.HEXCOLOR,d,l("variable","@@?"+a,10),l("variable","@\\{"+a+"\\}"),l("built_in","~?`[^`]*?`"),{
-className:"attribute",begin:a+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0
-},n.IMPORTANT,{beginKeywords:"and not"},n.FUNCTION_DISPATCH);const g=s.concat({
-begin:/\{/,end:/\}/,contains:r}),u={beginKeywords:"when",endsWithParent:!0,
-contains:[{beginKeywords:"and not"}].concat(s)},b={begin:i+"\\s*:",
-returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/
-},n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b",
-end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}]
-},m={className:"keyword",
-begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",
-starts:{end:"[;{}]",keywords:c,returnEnd:!0,contains:s,relevance:0}},p={
-className:"variable",variants:[{begin:"@"+a+"\\s*:",relevance:15},{begin:"@"+a
-}],starts:{end:"[;}]",returnEnd:!0,contains:g}},_={variants:[{
-begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:i,end:/\{/}],returnBegin:!0,
-returnEnd:!0,illegal:"[<='$\"]",relevance:0,
-contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,u,l("keyword","all\\b"),l("variable","@\\{"+a+"\\}"),{
-begin:"\\b("+re.join("|")+")\\b",className:"selector-tag"
-},n.CSS_NUMBER_MODE,l("selector-tag",i,0),l("selector-id","#"+i),l("selector-class","\\."+i,0),l("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{
-className:"selector-pseudo",begin:":("+oe.join("|")+")"},{
-className:"selector-pseudo",begin:":(:)?("+le.join("|")+")"},{begin:/\(/,
-end:/\)/,relevance:0,contains:g},{begin:"!important"},n.FUNCTION_DISPATCH]},h={
-begin:a+":(:)?"+`(${t.join("|")})`,returnBegin:!0,contains:[_]}
-;return r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,m,p,h,b,_,u,n.FUNCTION_DISPATCH),
-{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:r}},
-grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"]
-},i=[e.COMMENT("--(?!"+n+")","$"),e.COMMENT("--"+n,t,{contains:[a],relevance:10
-})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE,
-literal:"true false nil",
-keyword:"and break do else elseif end for goto if in local not or repeat return then until while",
-built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"
-},contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)",
-contains:[e.inherit(e.TITLE_MODE,{
-begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",
-begin:"\\(",endsWithParent:!0,contains:i}].concat(i)
-},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",
-begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={
-className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)",
-contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%<?\^\+\*]/}]},t={className:"string",
-begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,n]},a={className:"variable",
-begin:/\$\([\w-]+\s/,end:/\)/,keywords:{
-built_in:"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value"
-},contains:[n]},i={begin:"^"+e.UNDERSCORE_IDENT_RE+"\\s*(?=[:+?]?=)"},r={
-className:"section",begin:/^[^\s]+:/,end:/$/,contains:[n]};return{
-name:"Makefile",aliases:["mk","mak","make"],keywords:{$pattern:/[\w-]+/,
-keyword:"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath"
-},contains:[e.HASH_COMMENT_MODE,n,t,a,i,{className:"meta",begin:/^\.PHONY:/,
-end:/$/,keywords:{$pattern:/[\.\w]+/,keyword:".PHONY"}},r]}},grmr_markdown:e=>{
-const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={
-variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{
-begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/,
-relevance:2},{
-begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/),
-relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{
-begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/
-},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0,
-returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)",
-excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[",
-end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[],
-variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}]
-},i={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{
-begin:/_(?![_\s])/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[]
-}),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r)
-;let o=[n,t];return[a,i,r,s].forEach((e=>{e.contains=e.contains.concat(o)
-})),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{
-className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{
-begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",
-contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",
-end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o,
-end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{
-begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{
-begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",
-contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{
-begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{
-className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{
-className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{
-const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n,
-keyword:["@interface","@class","@protocol","@implementation"]};return{
-name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"],
-keywords:{"variable.language":["this","super"],$pattern:n,
-keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"],
-literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"],
-built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"],
-type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"]
-},illegal:"</",contains:[{className:"built_in",
-begin:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"
-},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.C_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{
-className:"string",variants:[{begin:'@"',end:'"',illegal:"\\n",
-contains:[e.BACKSLASH_ESCAPE]}]},{className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,
-keywords:{
-keyword:"if else elif endif define undef warning error line pragma ifdef ifndef include"
-},contains:[{begin:/\\\n/,relevance:0},e.inherit(e.QUOTE_STRING_MODE,{
-className:"string"}),{className:"string",begin:/<.*?>/,end:/$/,illegal:"\\n"
-},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class",
-begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t,
-contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE,
-relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={
-$pattern:/[\w.]+/,
-keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0"
-},i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/,
-end:/\}/},s={variants:[{begin:/\$\d/},{
-begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])")
-},{begin:/[$%@][^\s\w{]/,relevance:0}]
-},o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{
-const r="\\1"===i?i:n.concat(i,a)
-;return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t)
-},d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{
-endsWithParent:!0}),r,{className:"string",contains:o,variants:[{
-begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",
-end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{
-begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">",
-relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",
-contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",
-contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{
-begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number",
-begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",
-relevance:0},{
-begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",
-keywords:"split return print reverse grep",relevance:0,
-contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{
-begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{
-begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{
-className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{
-begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0
-}),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{
-begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub",
-end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{
-begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",
-subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]
-}];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a,
-contains:g}},grmr_php:e=>{
-const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={
-scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{
-begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null
-}),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{
-illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,{
-begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/,
-contains:e.QUOTE_STRING_MODE.contains.concat(s),"on:begin":(e,n)=>{
-n.data._beginMatch=e[1]||e[2]},"on:end":(e,n)=>{
-n.data._beginMatch!==e[1]&&n.ignoreMatch()}},e.END_SAME_AS_BEGIN({
-begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{
-begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{
-begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{
-begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?"
-}],relevance:0
-},g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={
-keyword:u,literal:(e=>{const n=[];return e.forEach((e=>{
-n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase())
-})),n})(g),built_in:b},p=e=>e.map((e=>e.replace(/\|\d+$/,""))),_={variants:[{
-match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{
-1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{
-match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant"
-}},{match:[/::/,/class/],scope:{2:"variable.language"}},{
-match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class",
-3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))],
-scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class",
-3:"variable.language"}}]},E={scope:"attr",
-match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0,
-begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_]
-},N={relevance:0,
-match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)],
-scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(N)
-;const w=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1,
-keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/,
-endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{
-begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]},
-contains:["self",...w]},...w,{scope:"meta",match:i}]
-},e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{
-scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/,
-keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE,
-contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{
-begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{
-begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,N,f,{
-match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{
-scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/,
-excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use"
-},e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params",
-begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m,
-contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{
-beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait",
-illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{
-beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{
-beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/,
-contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{
-beginKeywords:"use",relevance:0,end:";",contains:[{
-match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]}
-},grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{
-begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",
-end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0
-},e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null,
-skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null,
-contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text",
-aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{
-const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={
-$pattern:/[A-Za-z]\w+|__\w+__/,keyword:a,
-built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"],
-literal:["__debug__","Ellipsis","False","None","NotImplemented","True"],
-type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"]
-},r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/,
-end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={
-className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{
-begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/,
-contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{
-begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/,
-contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{
-begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/,
-contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/,
-end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/,
-relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{
-begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/,
-end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/,
-contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/,
-contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
-},c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={
-className:"number",relevance:0,variants:[{
-begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{
-begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{
-begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})`
-},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})`
-}]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i,
-contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={
-className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/,
-end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i,
-contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{
-name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i,
-illegal:/(<\/|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if",
-relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{
-1:"keyword",3:"title.function"},contains:[m]},{variants:[{
-match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}],
-scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{
-className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}},
-grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt",
-starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{
-begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{
-const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/)
-;return{name:"R",keywords:{$pattern:t,
-keyword:"function if in break next repeat else for while",
-literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10",
-built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm"
-},contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/,
-starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)),
-endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{
-scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0
-}]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}]
-}),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE],
-variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/
-}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/
-}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/
-}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/
-}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/
-}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"',
-relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{
-1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"},
-match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{
-2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"},
-match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{
-match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`",
-contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{
-const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={
-"variable.constant":["__FILE__","__LINE__","__ENCODING__"],
-"variable.language":["self","super"],
-keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"],
-built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"],
-literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={
-begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s]
-}),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10
-}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/,
-end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c],
-variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{
-begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{
-begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?</,end:/>/},{begin:/%[qQwWx]?\//,
-end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{
-begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{
-begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{
-begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{
-begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{
-begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)),
-contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,
-contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number",
-relevance:0,variants:[{
-begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{
-begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b"
-},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{
-begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{
-begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{
-className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0,
-keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{
-match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class",
-4:"title.class.inherited"},keywords:r},{match:[/(include|extend)\s+/,i],scope:{
-2:"title.class"},keywords:r},{relevance:0,match:[i,/\.new[. (]/],scope:{
-1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,
-className:"variable.constant"},{relevance:0,match:a,scope:"title.class"},{
-match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{
-begin:e.IDENT_RE+"::"},{className:"symbol",
-begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol",
-begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable",
-begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{
-className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0,
-relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*",
-keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c],
-illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{
-begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",
-end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l)
-;c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m}
-},{className:"meta.prompt",
-begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])",
-starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby",
-aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/,
-contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}},
-grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0,
-begin:n.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,n.lookahead(/\s*\(/))
-},a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],r=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"]
-;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:r,
-keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"],
-literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:"</",
-contains:[e.C_LINE_COMMENT_MODE,e.COMMENT("/\\*","\\*/",{contains:["self"]
-}),e.inherit(e.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{
-className:"string",variants:[{begin:/b?r(#*)"(.|\n)*?"\1(?!#)/},{
-begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",
-begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{
-begin:"\\b0b([01_]+)"+a},{begin:"\\b0o([0-7_]+)"+a},{
-begin:"\\b0x([A-Fa-f0-9_]+)"+a},{
-begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)"+a}],relevance:0},{
-begin:[/fn/,/\s+/,e.UNDERSCORE_IDENT_RE],className:{1:"keyword",
-3:"title.function"}},{className:"meta",begin:"#!?\\[",end:"\\]",contains:[{
-className:"string",begin:/"/,end:/"/}]},{
-begin:[/let/,/\s+/,/(?:mut\s+)?/,e.UNDERSCORE_IDENT_RE],className:{1:"keyword",
-3:"keyword",4:"variable"}},{
-begin:[/for/,/\s+/,e.UNDERSCORE_IDENT_RE,/\s+/,/in/],className:{1:"keyword",
-3:"variable",5:"keyword"}},{begin:[/type/,/\s+/,e.UNDERSCORE_IDENT_RE],
-className:{1:"keyword",3:"title.class"}},{
-begin:[/(?:trait|enum|struct|union|impl|for)/,/\s+/,e.UNDERSCORE_IDENT_RE],
-className:{1:"keyword",3:"title.class"}},{begin:e.IDENT_RE+"::",keywords:{
-keyword:"Self",built_in:i,type:r}},{className:"punctuation",begin:"->"},t]}},
-grmr_scss:e=>{const n=ie(e),t=le,a=oe,i="@[a-z-]+",r={className:"variable",
-begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS",
-case_insensitive:!0,illegal:"[=/|']",
-contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{
-className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{
-className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0
-},n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag",
-begin:"\\b("+re.join("|")+")\\b",relevance:0},{className:"selector-pseudo",
-begin:":("+a.join("|")+")"},{className:"selector-pseudo",
-begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/,
-contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute",
-begin:"\\b("+ce.join("|")+")\\b"},{
-begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"
-},{begin:/:/,end:/[;}{]/,relevance:0,
-contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT,n.FUNCTION_DISPATCH]
-},{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{
-begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/,
-keyword:"and or not only",attribute:se.join(" ")},contains:[{begin:i,
-className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute"
-},r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE]
-},n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session",
-aliases:["console","shellsession"],contains:[{className:"meta.prompt",
-begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/,
-subLanguage:"bash"}}]}),grmr_sql:e=>{
-const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!r.includes(e))),c={
-begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}}
-;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{
-$pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t
-;return n=n||[],e.map((e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e))
-})(l,{when:e=>e.length<3}),literal:a,type:i,
-built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"]
-},contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/,
-keyword:l.concat(s),literal:a,type:i}},{className:"type",
-begin:n.either("double precision","large object","with timezone","without timezone")
-},c,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string",
-variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/,
-contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{
-className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/,
-relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0
-},t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={
-match:[/\./,m(...xe,...Me)],className:{2:"keyword"}},r={match:b(/\./,m(...Ae)),
-relevance:0},s=Ae.filter((e=>"string"==typeof e)).concat(["_|0"]),o={variants:[{
-className:"keyword",
-match:m(...Ae.filter((e=>"string"!=typeof e)).concat(Se).map(ke),...Me)}]},l={
-$pattern:m(/\b\w+/,/#\w+/),keyword:s.concat(Re),literal:Ce},c=[i,r,o],g=[{
-match:b(/\./,m(...De)),relevance:0},{className:"built_in",
-match:b(/\b/,m(...De),/(?=\()/)}],u={match:/->/,relevance:0},p=[u,{
-className:"operator",relevance:0,variants:[{match:Be},{match:`\\.(\\.|${Le})+`}]
-}],_="([0-9]_*)+",h="([0-9a-fA-F]_*)+",f={className:"number",relevance:0,
-variants:[{match:`\\b(${_})(\\.(${_}))?([eE][+-]?(${_}))?\\b`},{
-match:`\\b0x(${h})(\\.(${h}))?([pP][+-]?(${_}))?\\b`},{match:/\b0o([0-7]_*)+\b/
-},{match:/\b0b([01]_*)+\b/}]},E=(e="")=>({className:"subst",variants:[{
-match:b(/\\/,e,/[0\\tnr"']/)},{match:b(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}]
-}),y=(e="")=>({className:"subst",match:b(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/)
-}),N=(e="")=>({className:"subst",label:"interpol",begin:b(/\\/,e,/\(/),end:/\)/
-}),w=(e="")=>({begin:b(e,/"""/),end:b(/"""/,e),contains:[E(e),y(e),N(e)]
-}),v=(e="")=>({begin:b(e,/"/),end:b(/"/,e),contains:[E(e),N(e)]}),O={
-className:"string",
-variants:[w(),w("#"),w("##"),w("###"),v(),v("#"),v("##"),v("###")]
-},k=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,
-contains:[e.BACKSLASH_ESCAPE]}],x={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//,
-contains:k},M=e=>{const n=b(e,/\//),t=b(/\//,e);return{begin:n,end:t,
-contains:[...k,{scope:"comment",begin:`#(?!.*${t})`,end:/$/}]}},S={
-scope:"regexp",variants:[M("###"),M("##"),M("#"),x]},A={match:b(/`/,Fe,/`/)
-},C=[A,{className:"variable",match:/\$\d+/},{className:"variable",
-match:`\\$${ze}+`}],T=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{
-contains:[{begin:/\(/,end:/\)/,keywords:Pe,contains:[...p,f,O]}]}},{
-scope:"keyword",match:b(/@/,m(...je))},{scope:"meta",match:b(/@/,Fe)}],R={
-match:d(/\b[A-Z]/),relevance:0,contains:[{className:"type",
-match:b(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,ze,"+")
-},{className:"type",match:Ue,relevance:0},{match:/[?!]+/,relevance:0},{
-match:/\.\.\./,relevance:0},{match:b(/\s+&\s+/,d(Ue)),relevance:0}]},D={
-begin:/</,end:/>/,keywords:l,contains:[...a,...c,...T,u,R]};R.contains.push(D)
-;const I={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{
-match:b(Fe,/\s*:/),keywords:"_|0",relevance:0
-},...a,S,...c,...g,...p,f,O,...C,...T,R]},L={begin:/</,end:/>/,
-keywords:"repeat each",contains:[...a,R]},B={begin:/\(/,end:/\)/,keywords:l,
-contains:[{begin:m(d(b(Fe,/\s*:/)),d(b(Fe,/\s+/,Fe,/\s*:/))),end:/:/,
-relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params",
-match:Fe}]},...a,...c,...p,f,O,...T,R,I],endsParent:!0,illegal:/["']/},$={
-match:[/(func|macro)/,/\s+/,m(A.match,Fe,Be)],className:{1:"keyword",
-3:"title.function"},contains:[L,B,n],illegal:[/\[/,/%/]},z={
-match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"},
-contains:[L,B,n],illegal:/\[|%/},F={match:[/operator/,/\s+/,Be],className:{
-1:"keyword",3:"title"}},U={begin:[/precedencegroup/,/\s+/,Ue],className:{
-1:"keyword",3:"title"},contains:[R],keywords:[...Te,...Ce],end:/}/}
-;for(const e of O.variants){const n=e.contains.find((e=>"interpol"===e.label))
-;n.keywords=l;const t=[...c,...g,...p,f,O,...C];n.contains=[...t,{begin:/\(/,
-end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l,
-contains:[...a,$,z,{beginKeywords:"struct protocol class extension enum actor",
-end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{
-className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c]
-},F,U,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0
-},S,...c,...g,...p,f,O,...C,...T,R,I]}},grmr_typescript:e=>{
-const n=Oe(e),t=_e,a=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],i={
-beginKeywords:"namespace",end:/\{/,excludeEnd:!0,
-contains:[n.exports.CLASS_REFERENCE]},r={beginKeywords:"interface",end:/\{/,
-excludeEnd:!0,keywords:{keyword:"interface extends",built_in:a},
-contains:[n.exports.CLASS_REFERENCE]},s={$pattern:_e,
-keyword:he.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]),
-literal:fe,built_in:ve.concat(a),"variable.language":we},o={className:"meta",
-begin:"@"+t},l=(e,n,t)=>{const a=e.contains.findIndex((e=>e.label===n))
-;if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)}
-;return Object.assign(n.keywords,s),
-n.exports.PARAMS_CONTAINS.push(o),n.contains=n.contains.concat([o,i,r]),
-l(n,"shebang",e.SHEBANG()),l(n,"use_strict",{className:"meta",relevance:10,
-begin:/^\s*['"]use strict['"]/
-}),n.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(n,{
-name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n},grmr_vbnet:e=>{
-const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={
-className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{
-begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{
-begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}]
-},o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}]
-}),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]})
-;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0,
-classNameAliases:{label:"symbol"},keywords:{
-keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield",
-built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort",
-type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort",
-literal:"true false nothing"},
-illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{
-className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/,
-end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0,
-variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/
-},{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{
-begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{
-className:"label",begin:/^\w+:/},o,l,{className:"meta",
-begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/,
-end:/$/,keywords:{
-keyword:"const disable else elseif enable end externalsource if region then"},
-contains:[l]}]}},grmr_wasm:e=>{e.regex;const n=e.COMMENT(/\(;/,/;\)/)
-;return n.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/,
-keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"]
-},contains:[e.COMMENT(/;;/,/$/),n,{match:[/(?:offset|align)/,/\s*/,/=/],
-className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{
-match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{
-begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword",
-3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/,
-className:"type"},{className:"keyword",
-match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/
-},{className:"number",relevance:0,
-match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/
-}]}},grmr_xml:e=>{
-const n=e.regex,t=n.concat(/[\p{L}_]/u,n.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),a={
-className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/,
-contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]
-},r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{
-className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={
-endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",
-begin:/[\p{L}0-9._:-]+/u,relevance:0},{begin:/=\s*/,relevance:0,contains:[{
-className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[a]},{
-begin:/'/,end:/'/,contains:[a]},{begin:/[^\s"'=<>`]+/}]}]}]};return{
-name:"HTML, XML",
-aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],
-case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin:/<![a-z]/,
-end:/>/,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{
-className:"meta",begin:/<![a-z]/,end:/>/,contains:[i,r,o,s]}]}]
-},e.COMMENT(/<!--/,/-->/,{relevance:10}),{begin:/<!\[CDATA\[/,end:/\]\]>/,
-relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/,
-relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag",
-begin:/<style(?=\s|>)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{
-end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",
-begin:/<script(?=\s|>)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{
-end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{
-className:"tag",begin:/<>|<\/>/},{className:"tag",
-begin:n.concat(/</,n.lookahead(n.concat(t,n.either(/\/>/,/>/,/\s/)))),
-end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{
-className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{
-className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}
-},grmr_yaml:e=>{
-const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={
-className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/
-},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",
-variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{
-variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={
-end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/,
-end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]",
-contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{
-begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{
-begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",
-relevance:10},{className:"string",
-begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{
-begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,
-relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type",
-begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t
-},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",
-begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",
-relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{
-className:"number",
-begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"
-},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l]
-;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0,
-aliases:["yml"],contains:l}}});const He=ae;for(const e of Object.keys(Ke)){
-const n=e.replace("grmr_","").replace("_","-");He.registerLanguage(n,Ke[e])}
-return He}()
-;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);
\ No newline at end of file
diff --git a/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css b/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css
deleted file mode 100644
index 7f6fe11c..00000000
--- a/exo/tinychat/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css
+++ /dev/null
@@ -1 +0,0 @@
-pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#1e1e1e;color:#dcdcdc}.hljs-keyword,.hljs-literal,.hljs-name,.hljs-symbol{color:#569cd6}.hljs-link{color:#569cd6;text-decoration:underline}.hljs-built_in,.hljs-type{color:#4ec9b0}.hljs-class,.hljs-number{color:#b8d7a3}.hljs-meta .hljs-string,.hljs-string{color:#d69d85}.hljs-regexp,.hljs-template-tag{color:#9a5334}.hljs-formula,.hljs-function,.hljs-params,.hljs-subst,.hljs-title{color:#dcdcdc}.hljs-comment,.hljs-quote{color:#57a64a;font-style:italic}.hljs-doctag{color:#608b4e}.hljs-meta,.hljs-meta .hljs-keyword,.hljs-tag{color:#9b9b9b}.hljs-template-variable,.hljs-variable{color:#bd63c5}.hljs-attr,.hljs-attribute{color:#9cdcfe}.hljs-section{color:gold}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-bullet,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-selector-tag{color:#d7ba7d}.hljs-addition{background-color:#144212;display:inline-block;width:100%}.hljs-deletion{background-color:#600;display:inline-block;width:100%}
\ No newline at end of file
diff --git a/exo/tinychat/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js b/exo/tinychat/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js
deleted file mode 100644
index dd119d59..00000000
--- a/exo/tinychat/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-!function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";var e=new Map;function t(t){var o=e.get(t);o&&o.destroy()}function o(t){var o=e.get(t);o&&o.update()}var r=null;"undefined"==typeof window?((r=function(e){return e}).destroy=function(e){return e},r.update=function(e){return e}):((r=function(t,o){return t&&Array.prototype.forEach.call(t.length?t:[t],(function(t){return function(t){if(t&&t.nodeName&&"TEXTAREA"===t.nodeName&&!e.has(t)){var o,r=null,n=window.getComputedStyle(t),i=(o=t.value,function(){s({testForHeightReduction:""===o||!t.value.startsWith(o),restoreTextAlign:null}),o=t.value}),l=function(o){t.removeEventListener("autosize:destroy",l),t.removeEventListener("autosize:update",a),t.removeEventListener("input",i),window.removeEventListener("resize",a),Object.keys(o).forEach((function(e){return t.style[e]=o[e]})),e.delete(t)}.bind(t,{height:t.style.height,resize:t.style.resize,textAlign:t.style.textAlign,overflowY:t.style.overflowY,overflowX:t.style.overflowX,wordWrap:t.style.wordWrap});t.addEventListener("autosize:destroy",l),t.addEventListener("autosize:update",a),t.addEventListener("input",i),window.addEventListener("resize",a),t.style.overflowX="hidden",t.style.wordWrap="break-word",e.set(t,{destroy:l,update:a}),a()}function s(e){var o,i,l=e.restoreTextAlign,a=void 0===l?null:l,d=e.testForHeightReduction,u=void 0===d||d,c=n.overflowY;if(0!==t.scrollHeight&&("vertical"===n.resize?t.style.resize="none":"both"===n.resize&&(t.style.resize="horizontal"),u&&(o=function(e){for(var t=[];e&&e.parentNode&&e.parentNode instanceof Element;)e.parentNode.scrollTop&&t.push([e.parentNode,e.parentNode.scrollTop]),e=e.parentNode;return function(){return t.forEach((function(e){var t=e[0],o=e[1];t.style.scrollBehavior="auto",t.scrollTop=o,t.style.scrollBehavior=null}))}}(t),t.style.height=""),i="content-box"===n.boxSizing?t.scrollHeight-(parseFloat(n.paddingTop)+parseFloat(n.paddingBottom)):t.scrollHeight+parseFloat(n.borderTopWidth)+parseFloat(n.borderBottomWidth),"none"!==n.maxHeight&&i>parseFloat(n.maxHeight)?("hidden"===n.overflowY&&(t.style.overflow="scroll"),i=parseFloat(n.maxHeight)):"hidden"!==n.overflowY&&(t.style.overflow="hidden"),t.style.height=i+"px",a&&(t.style.textAlign=a),o&&o(),r!==i&&(t.dispatchEvent(new Event("autosize:resized",{bubbles:!0})),r=i),c!==n.overflow&&!a)){var f=n.textAlign;"hidden"===n.overflow&&(t.style.textAlign="start"===f?"end":"start"),s({restoreTextAlign:f,testForHeightReduction:!0})}}function a(){s({testForHeightReduction:!0,restoreTextAlign:null})}}(t)})),t}).destroy=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],t),e},r.update=function(e){return e&&Array.prototype.forEach.call(e.length?e:[e],o),e});var n=r;document.addEventListener("alpine:init",(()=>{var e;(e=window.Alpine).directive("autosize",((e,{modifiers:t},{cleanup:o})=>{n(e);const r=Array.from(e.attributes);let i=!1;for(let{nodeName:e}of r)if("wire:model"===e||e.startsWith("wire:model.")){i=!0;break}!e.hasAttribute("wire:ignore")&&i&&e.setAttribute("wire:ignore","");const l=()=>{n.update(e)};e.addEventListener("autosize",l),o((()=>{n.destroy(e),e.removeEventListener("autosize",l)}))})),e.magic("autosize",(e=>t=>{(t||e).dispatchEvent(new Event("autosize"))}))}))}));
-//# sourceMappingURL=alpine-autosize.min.js.map
diff --git a/exo/tinychat/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js b/exo/tinychat/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js
deleted file mode 100644
index 2ca48278..00000000
--- a/exo/tinychat/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js
+++ /dev/null
@@ -1,5 +0,0 @@
-(()=>{var rt=!1,nt=!1,U=[],it=-1;function qt(e){Cn(e)}function Cn(e){U.includes(e)||U.push(e),Tn()}function Ee(e){let t=U.indexOf(e);t!==-1&&t>it&&U.splice(t,1)}function Tn(){!nt&&!rt&&(rt=!0,queueMicrotask(Rn))}function Rn(){rt=!1,nt=!0;for(let e=0;e<U.length;e++)U[e](),it=e;U.length=0,it=-1,nt=!1}var R,D,L,st,ot=!0;function Ut(e){ot=!1,e(),ot=!0}function Wt(e){R=e.reactive,L=e.release,D=t=>e.effect(t,{scheduler:r=>{ot?qt(r):r()}}),st=e.raw}function at(e){D=e}function Gt(e){let t=()=>{};return[n=>{let i=D(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),L(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=D(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>L(i)}var Jt=[],Yt=[],Xt=[];function Zt(e){Xt.push(e)}function ee(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Yt.push(t))}function Ae(e){Jt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function ct(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function Qt(e){if(e._x_cleanups)for(;e._x_cleanups.length;)e._x_cleanups.pop()()}var lt=new MutationObserver(pt),ut=!1;function le(){lt.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ut=!0}function ft(){Mn(),lt.disconnect(),ut=!1}var ce=[];function Mn(){let e=lt.takeRecords();ce.push(()=>e.length>0&&pt(e));let t=ce.length;queueMicrotask(()=>{if(ce.length===t)for(;ce.length>0;)ce.shift()()})}function _(e){if(!ut)return e();ft();let t=e();return le(),t}var dt=!1,Se=[];function er(){dt=!0}function tr(){dt=!1,pt(Se),Se=[]}function pt(e){if(dt){Se=Se.concat(e);return}let t=new Set,r=new Set,n=new Map,i=new Map;for(let o=0;o<e.length;o++)if(!e[o].target._x_ignoreMutationObserver&&(e[o].type==="childList"&&(e[o].addedNodes.forEach(s=>s.nodeType===1&&t.add(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.add(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ct(s,o)}),n.forEach((o,s)=>{Jt.forEach(a=>a(s,o))});for(let o of r)t.has(o)||Yt.forEach(s=>s(o));t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.has(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,Xt.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function Ce(e){return F(j(e))}function P(e,t,r){return e._x_dataStack=[t,...j(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function j(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?j(e.host):e.parentNode?j(e.parentNode):[]}function F(e){return new Proxy({objects:e},Nn)}var Nn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Dn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Dn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>Pn(n,i),s=>mt(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function Pn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function mt(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),mt(e[t[0]],t.slice(1),r)}}var rr={};function y(e,t){rr[e]=t}function ue(e,t){return Object.entries(rr).forEach(([r,n])=>{let i=null;function o(){if(i)return i;{let[s,a]=_t(t);return i={interceptor:Re,...s},ee(t,a),i}}Object.defineProperty(e,`$${r}`,{get(){return n(t,o())},enumerable:!1})}),e}function nr(e,t,r,...n){try{return r(...n)}catch(i){te(i,e,t)}}function te(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message}
-
-${r?'Expression: "'+r+`"
-
-`:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function De(e){let t=Me;Me=!1;let r=e();return Me=t,r}function M(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return ir(...e)}var ir=gt;function or(e){ir=e}function gt(e,t){let r={};ue(r,e);let n=[r,...j(e)],i=typeof t=="function"?In(n,t):Ln(n,t,e);return nr.bind(null,e,t,i)}function In(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(F([n,...e]),i);Ne(r,o)}}var ht={};function kn(e,t){if(ht[e])return ht[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return te(s,t,e),Promise.resolve()}})();return ht[e]=o,o}function Ln(e,t,r){let n=kn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=F([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>te(l,r,t));n.finished?(Ne(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Ne(i,l,a,s,r)}).catch(l=>te(l,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>te(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var bt="x-";function C(e=""){return bt+e}function sr(e){bt=e}var Pe={};function d(e,t){return Pe[e]=t,{before(r){if(!Pe[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=W.indexOf(r);W.splice(n>=0?n:W.indexOf("DEFAULT"),0,e)}}}function ar(e){return Object.keys(Pe).includes(e)}function de(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=wt(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(ur((o,s)=>n[o]=s)).filter(dr).map(jn(n,r)).sort(Fn).map(o=>$n(e,o))}function wt(e){return Array.from(e).map(ur()).filter(t=>!dr(t))}var xt=!1,fe=new Map,cr=Symbol();function lr(e){xt=!0;let t=Symbol();cr=t,fe.set(t,[]);let r=()=>{for(;fe.get(t).length;)fe.get(t).shift()();fe.delete(t)},n=()=>{xt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Gt(e);return t.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:M.bind(M,e)},()=>t.forEach(a=>a())]}function $n(e,t){let r=()=>{},n=Pe[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),xt?fe.get(cr).push(n):n())};return s.runCleanups=o,s}var Ie=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),ke=e=>e;function ur(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=fr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var fr=[];function re(e){fr.push(e)}function dr({name:e}){return pr().test(e)}var pr=()=>new RegExp(`^${bt}([^:^.]+)\\b`);function jn(e,t){return({name:r,value:n})=>{let i=r.match(pr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var yt="DEFAULT",W=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",yt,"teleport"];function Fn(e,t){let r=W.indexOf(e.type)===-1?yt:e.type,n=W.indexOf(t.type)===-1?yt:t.type;return W.indexOf(r)-W.indexOf(n)}function G(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function T(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>T(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)T(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var mr=!1;function _r(){mr&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),mr=!0,document.body||E("Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine's `<script>` tag?"),G(document,"alpine:init"),G(document,"alpine:initializing"),le(),Zt(t=>S(t,T)),ee(t=>vt(t)),Ae((t,r)=>{de(t,r).forEach(n=>n())});let e=t=>!J(t.parentElement,!0);Array.from(document.querySelectorAll(xr().join(","))).filter(e).forEach(t=>{S(t)}),G(document,"alpine:initialized"),setTimeout(()=>{Bn()})}var Et=[],hr=[];function gr(){return Et.map(e=>e())}function xr(){return Et.concat(hr).map(e=>e())}function Le(e){Et.push(e)}function $e(e){hr.push(e)}function J(e,t=!1){return z(e,r=>{if((t?xr():gr()).some(i=>r.matches(i)))return!0})}function z(e,t){if(e){if(t(e))return e;if(e._x_teleportBack&&(e=e._x_teleportBack),!!e.parentElement)return z(e.parentElement,t)}}function yr(e){return gr().some(t=>e.matches(t))}var br=[];function wr(e){br.push(e)}function S(e,t=T,r=()=>{}){lr(()=>{t(e,(n,i)=>{r(n,i),br.forEach(o=>o(n,i)),de(n,n.attributes).forEach(o=>o()),n._x_ignore&&i()})})}function vt(e,t=T){t(e,r=>{ct(r),Qt(r)})}function Bn(){[["ui","dialog",["[x-dialog], [x-popover]"]],["anchor","anchor",["[x-anchor]"]],["sort","sort",["[x-sort]"]]].forEach(([t,r,n])=>{ar(r)||n.some(i=>{if(document.querySelector(i))return E(`found "${i}", but missing ${t} plugin`),!0})})}var St=[],At=!1;function ne(e=()=>{}){return queueMicrotask(()=>{At||setTimeout(()=>{je()})}),new Promise(t=>{St.push(()=>{e(),t()})})}function je(){for(At=!1;St.length;)St.shift()()}function Er(){At=!0}function pe(e,t){return Array.isArray(t)?vr(e,t.join(" ")):typeof t=="object"&&t!==null?zn(e,t):typeof t=="function"?pe(e,t()):vr(e,t)}function vr(e,t){let r=o=>o.split(" ").filter(Boolean),n=o=>o.split(" ").filter(s=>!e.classList.contains(s)).filter(Boolean),i=o=>(e.classList.add(...o),()=>{e.classList.remove(...o)});return t=t===!0?t="":t||"",i(n(t))}function zn(e,t){let r=a=>a.split(" ").filter(Boolean),n=Object.entries(t).flatMap(([a,c])=>c?r(a):!1).filter(Boolean),i=Object.entries(t).flatMap(([a,c])=>c?!1:r(a)).filter(Boolean),o=[],s=[];return i.forEach(a=>{e.classList.contains(a)&&(e.classList.remove(a),s.push(a))}),n.forEach(a=>{e.classList.contains(a)||(e.classList.add(a),o.push(a))}),()=>{s.forEach(a=>e.classList.add(a)),o.forEach(a=>e.classList.remove(a))}}function Y(e,t){return typeof t=="object"&&t!==null?Kn(e,t):Hn(e,t)}function Kn(e,t){let r={};return Object.entries(t).forEach(([n,i])=>{r[n]=e.style[n],n.startsWith("--")||(n=Vn(n)),e.style.setProperty(n,i)}),setTimeout(()=>{e.style.length===0&&e.removeAttribute("style")}),()=>{Y(e,r)}}function Hn(e,t){let r=e.getAttribute("style",t);return e.setAttribute("style",t),()=>{e.setAttribute("style",r||"")}}function Vn(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function me(e,t=()=>{}){let r=!1;return function(){r?t.apply(this,arguments):(r=!0,e.apply(this,arguments))}}d("transition",(e,{value:t,modifiers:r,expression:n},{evaluate:i})=>{typeof n=="function"&&(n=i(n)),n!==!1&&(!n||typeof n=="boolean"?Un(e,r,t):qn(e,n,t))});function qn(e,t,r){Sr(e,pe,""),{enter:i=>{e._x_transition.enter.during=i},"enter-start":i=>{e._x_transition.enter.start=i},"enter-end":i=>{e._x_transition.enter.end=i},leave:i=>{e._x_transition.leave.during=i},"leave-start":i=>{e._x_transition.leave.start=i},"leave-end":i=>{e._x_transition.leave.end=i}}[r](t)}function Un(e,t,r){Sr(e,Y);let n=!t.includes("in")&&!t.includes("out")&&!r,i=n||t.includes("in")||["enter"].includes(r),o=n||t.includes("out")||["leave"].includes(r);t.includes("in")&&!n&&(t=t.filter((g,b)=>b<t.indexOf("out"))),t.includes("out")&&!n&&(t=t.filter((g,b)=>b>t.indexOf("out")));let s=!t.includes("opacity")&&!t.includes("scale"),a=s||t.includes("opacity"),c=s||t.includes("scale"),l=a?0:1,u=c?_e(t,"scale",95)/100:1,p=_e(t,"delay",0)/1e3,m=_e(t,"origin","center"),w="opacity, transform",$=_e(t,"duration",150)/1e3,we=_e(t,"duration",75)/1e3,f="cubic-bezier(0.4, 0.0, 0.2, 1)";i&&(e._x_transition.enter.during={transformOrigin:m,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${$}s`,transitionTimingFunction:f},e._x_transition.enter.start={opacity:l,transform:`scale(${u})`},e._x_transition.enter.end={opacity:1,transform:"scale(1)"}),o&&(e._x_transition.leave.during={transformOrigin:m,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${we}s`,transitionTimingFunction:f},e._x_transition.leave.start={opacity:1,transform:"scale(1)"},e._x_transition.leave.end={opacity:l,transform:`scale(${u})`})}function Sr(e,t,r={}){e._x_transition||(e._x_transition={enter:{during:r,start:r,end:r},leave:{during:r,start:r,end:r},in(n=()=>{},i=()=>{}){Fe(e,t,{during:this.enter.during,start:this.enter.start,end:this.enter.end},n,i)},out(n=()=>{},i=()=>{}){Fe(e,t,{during:this.leave.during,start:this.leave.start,end:this.leave.end},n,i)}})}window.Element.prototype._x_toggleAndCascadeWithTransitions=function(e,t,r,n){let i=document.visibilityState==="visible"?requestAnimationFrame:setTimeout,o=()=>i(r);if(t){e._x_transition&&(e._x_transition.enter||e._x_transition.leave)?e._x_transition.enter&&(Object.entries(e._x_transition.enter.during).length||Object.entries(e._x_transition.enter.start).length||Object.entries(e._x_transition.enter.end).length)?e._x_transition.in(r):o():e._x_transition?e._x_transition.in(r):o();return}e._x_hidePromise=e._x_transition?new Promise((s,a)=>{e._x_transition.out(()=>{},()=>s(n)),e._x_transitioning&&e._x_transitioning.beforeCancel(()=>a({isFromCancelledTransition:!0}))}):Promise.resolve(n),queueMicrotask(()=>{let s=Ar(e);s?(s._x_hideChildren||(s._x_hideChildren=[]),s._x_hideChildren.push(e)):i(()=>{let a=c=>{let l=Promise.all([c._x_hidePromise,...(c._x_hideChildren||[]).map(a)]).then(([u])=>u?.());return delete c._x_hidePromise,delete c._x_hideChildren,l};a(e).catch(c=>{if(!c.isFromCancelledTransition)throw c})})})};function Ar(e){let t=e.parentNode;if(t)return t._x_hidePromise?t:Ar(t)}function Fe(e,t,{during:r,start:n,end:i}={},o=()=>{},s=()=>{}){if(e._x_transitioning&&e._x_transitioning.cancel(),Object.keys(r).length===0&&Object.keys(n).length===0&&Object.keys(i).length===0){o(),s();return}let a,c,l;Wn(e,{start(){a=t(e,n)},during(){c=t(e,r)},before:o,end(){a(),l=t(e,i)},after:s,cleanup(){c(),l()}})}function Wn(e,t){let r,n,i,o=me(()=>{_(()=>{r=!0,n||t.before(),i||(t.end(),je()),t.after(),e.isConnected&&t.cleanup(),delete e._x_transitioning})});e._x_transitioning={beforeCancels:[],beforeCancel(s){this.beforeCancels.push(s)},cancel:me(function(){for(;this.beforeCancels.length;)this.beforeCancels.shift()();o()}),finish:o},_(()=>{t.start(),t.during()}),Er(),requestAnimationFrame(()=>{if(r)return;let s=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,a=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;s===0&&(s=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),_(()=>{t.before()}),n=!0,requestAnimationFrame(()=>{r||(_(()=>{t.end()}),je(),setTimeout(e._x_transitioning.finish,s+a),i=!0)})})}function _e(e,t,r){if(e.indexOf(t)===-1)return r;let n=e[e.indexOf(t)+1];if(!n||t==="scale"&&isNaN(n))return r;if(t==="duration"||t==="delay"){let i=n.match(/([0-9]+)ms/);if(i)return i[1]}return t==="origin"&&["top","right","left","center","bottom"].includes(e[e.indexOf(t)+2])?[n,e[e.indexOf(t)+2]].join(" "):n}var I=!1;function A(e,t=()=>{}){return(...r)=>I?t(...r):e(...r)}function Or(e){return(...t)=>I&&e(...t)}var Cr=[];function K(e){Cr.push(e)}function Tr(e,t){Cr.forEach(r=>r(e,t)),I=!0,Mr(()=>{S(t,(r,n)=>{n(r,()=>{})})}),I=!1}var Be=!1;function Rr(e,t){t._x_dataStack||(t._x_dataStack=e._x_dataStack),I=!0,Be=!0,Mr(()=>{Gn(t)}),I=!1,Be=!1}function Gn(e){let t=!1;S(e,(n,i)=>{T(n,(o,s)=>{if(t&&yr(o))return s();t=!0,i(o,s)})})}function Mr(e){let t=D;at((r,n)=>{let i=t(r);return L(i),()=>{}}),e(),at(t)}function he(e,t,r,n=[]){switch(e._x_bindings||(e._x_bindings=R({})),e._x_bindings[t]=r,t=n.includes("camel")?ri(t):t,t){case"value":Jn(e,r);break;case"style":Xn(e,r);break;case"class":Yn(e,r);break;case"selected":case"checked":Zn(e,t,r);break;default:Dr(e,t,r);break}}function Jn(e,t){if(e.type==="radio")e.attributes.value===void 0&&(e.value=t),window.fromModel&&(typeof t=="boolean"?e.checked=ge(e.value)===t:e.checked=Nr(e.value,t));else if(e.type==="checkbox")Number.isInteger(t)?e.value=t:!Array.isArray(t)&&typeof t!="boolean"&&![null,void 0].includes(t)?e.value=String(t):Array.isArray(t)?e.checked=t.some(r=>Nr(r,e.value)):e.checked=!!t;else if(e.tagName==="SELECT")ti(e,t);else{if(e.value===t)return;e.value=t===void 0?"":t}}function Yn(e,t){e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedClasses=pe(e,t)}function Xn(e,t){e._x_undoAddedStyles&&e._x_undoAddedStyles(),e._x_undoAddedStyles=Y(e,t)}function Zn(e,t,r){Dr(e,t,r),ei(e,t,r)}function Dr(e,t,r){[null,void 0,!1].includes(r)&&ni(t)?e.removeAttribute(t):(Pr(t)&&(r=t),Qn(e,t,r))}function Qn(e,t,r){e.getAttribute(t)!=r&&e.setAttribute(t,r)}function ei(e,t,r){e[t]!==r&&(e[t]=r)}function ti(e,t){let r=[].concat(t).map(n=>n+"");Array.from(e.options).forEach(n=>{n.selected=r.includes(n.value)})}function ri(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function Nr(e,t){return e==t}function ge(e){return[1,"1","true","on","yes",!0].includes(e)?!0:[0,"0","false","off","no",!1].includes(e)?!1:e?Boolean(e):null}function Pr(e){return["disabled","checked","required","readonly","open","selected","autofocus","itemscope","multiple","novalidate","allowfullscreen","allowpaymentrequest","formnovalidate","autoplay","controls","loop","muted","playsinline","default","ismap","reversed","async","defer","nomodule"].includes(e)}function ni(e){return!["aria-pressed","aria-checked","aria-expanded","aria-selected"].includes(e)}function Ir(e,t,r){return e._x_bindings&&e._x_bindings[t]!==void 0?e._x_bindings[t]:Lr(e,t,r)}function kr(e,t,r,n=!0){if(e._x_bindings&&e._x_bindings[t]!==void 0)return e._x_bindings[t];if(e._x_inlineBindings&&e._x_inlineBindings[t]!==void 0){let i=e._x_inlineBindings[t];return i.extract=n,De(()=>M(e,i.expression))}return Lr(e,t,r)}function Lr(e,t,r){let n=e.getAttribute(t);return n===null?typeof r=="function"?r():r:n===""?!0:Pr(t)?!![t,"true"].includes(n):n}function ze(e,t){var r;return function(){var n=this,i=arguments,o=function(){r=null,e.apply(n,i)};clearTimeout(r),r=setTimeout(o,t)}}function Ke(e,t){let r;return function(){let n=this,i=arguments;r||(e.apply(n,i),r=!0,setTimeout(()=>r=!1,t))}}function He({get:e,set:t},{get:r,set:n}){let i=!0,o,s,a=D(()=>{let c=e(),l=r();if(i)n(Ot(c)),i=!1;else{let u=JSON.stringify(c),p=JSON.stringify(l);u!==o?n(Ot(c)):u!==p&&t(Ot(l))}o=JSON.stringify(e()),s=JSON.stringify(r())});return()=>{L(a)}}function Ot(e){return typeof e=="object"?JSON.parse(JSON.stringify(e)):e}function $r(e){(Array.isArray(e)?e:[e]).forEach(r=>r(B))}var X={},jr=!1;function Fr(e,t){if(jr||(X=R(X),jr=!0),t===void 0)return X[e];X[e]=t,typeof t=="object"&&t!==null&&t.hasOwnProperty("init")&&typeof t.init=="function"&&X[e].init(),Te(X[e])}function Br(){return X}var zr={};function Kr(e,t){let r=typeof t!="function"?()=>t:t;return e instanceof Element?Ct(e,r()):(zr[e]=r,()=>{})}function Hr(e){return Object.entries(zr).forEach(([t,r])=>{Object.defineProperty(e,t,{get(){return(...n)=>r(...n)}})}),e}function Ct(e,t,r){let n=[];for(;n.length;)n.pop()();let i=Object.entries(t).map(([s,a])=>({name:s,value:a})),o=wt(i);return i=i.map(s=>o.find(a=>a.name===s.name)?{name:`x-bind:${s.name}`,value:`"${s.value}"`}:s),de(e,i,r).map(s=>{n.push(s.runCleanups),s()}),()=>{for(;n.length;)n.pop()()}}var Vr={};function qr(e,t){Vr[e]=t}function Ur(e,t){return Object.entries(Vr).forEach(([r,n])=>{Object.defineProperty(e,r,{get(){return(...i)=>n.bind(t)(...i)},enumerable:!1})}),e}var ii={get reactive(){return R},get release(){return L},get effect(){return D},get raw(){return st},version:"3.14.1",flushAndStopDeferringMutations:tr,dontAutoEvaluateFunctions:De,disableEffectScheduling:Ut,startObservingMutations:le,stopObservingMutations:ft,setReactivityEngine:Wt,onAttributeRemoved:Oe,onAttributesAdded:Ae,closestDataStack:j,skipDuringClone:A,onlyDuringClone:Or,addRootSelector:Le,addInitSelector:$e,interceptClone:K,addScopeToNode:P,deferMutations:er,mapAttributes:re,evaluateLater:x,interceptInit:wr,setEvaluator:or,mergeProxies:F,extractProp:kr,findClosest:z,onElRemoved:ee,closestRoot:J,destroyTree:vt,interceptor:Re,transition:Fe,setStyles:Y,mutateDom:_,directive:d,entangle:He,throttle:Ke,debounce:ze,evaluate:M,initTree:S,nextTick:ne,prefixed:C,prefix:sr,plugin:$r,magic:y,store:Fr,start:_r,clone:Rr,cloneNode:Tr,bound:Ir,$data:Ce,watch:ve,walk:T,data:qr,bind:Kr},B=ii;function Tt(e,t){let r=Object.create(null),n=e.split(",");for(let i=0;i<n.length;i++)r[n[i]]=!0;return t?i=>!!r[i.toLowerCase()]:i=>!!r[i]}var oi="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly";var Ms=Tt(oi+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");var Wr=Object.freeze({}),Ns=Object.freeze([]);var si=Object.prototype.hasOwnProperty,xe=(e,t)=>si.call(e,t),H=Array.isArray,ie=e=>Gr(e)==="[object Map]";var ai=e=>typeof e=="string",Ve=e=>typeof e=="symbol",ye=e=>e!==null&&typeof e=="object";var ci=Object.prototype.toString,Gr=e=>ci.call(e),Rt=e=>Gr(e).slice(8,-1);var qe=e=>ai(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e;var Ue=e=>{let t=Object.create(null);return r=>t[r]||(t[r]=e(r))},li=/-(\w)/g,Ds=Ue(e=>e.replace(li,(t,r)=>r?r.toUpperCase():"")),ui=/\B([A-Z])/g,Ps=Ue(e=>e.replace(ui,"-$1").toLowerCase()),Mt=Ue(e=>e.charAt(0).toUpperCase()+e.slice(1)),Is=Ue(e=>e?`on${Mt(e)}`:""),Nt=(e,t)=>e!==t&&(e===e||t===t);var Dt=new WeakMap,be=[],k,Z=Symbol("iterate"),Pt=Symbol("Map key iterate");function fi(e){return e&&e._isEffect===!0}function en(e,t=Wr){fi(e)&&(e=e.raw);let r=pi(e,t);return t.lazy||r(),r}function tn(e){e.active&&(rn(e),e.options.onStop&&e.options.onStop(),e.active=!1)}var di=0;function pi(e,t){let r=function(){if(!r.active)return e();if(!be.includes(r)){rn(r);try{return _i(),be.push(r),k=r,e()}finally{be.pop(),nn(),k=be[be.length-1]}}};return r.id=di++,r.allowRecurse=!!t.allowRecurse,r._isEffect=!0,r.active=!0,r.raw=e,r.deps=[],r.options=t,r}function rn(e){let{deps:t}=e;if(t.length){for(let r=0;r<t.length;r++)t[r].delete(e);t.length=0}}var oe=!0,kt=[];function mi(){kt.push(oe),oe=!1}function _i(){kt.push(oe),oe=!0}function nn(){let e=kt.pop();oe=e===void 0?!0:e}function N(e,t,r){if(!oe||k===void 0)return;let n=Dt.get(e);n||Dt.set(e,n=new Map);let i=n.get(r);i||n.set(r,i=new Set),i.has(k)||(i.add(k),k.deps.push(i),k.options.onTrack&&k.options.onTrack({effect:k,target:e,type:t,key:r}))}function q(e,t,r,n,i,o){let s=Dt.get(e);if(!s)return;let a=new Set,c=u=>{u&&u.forEach(p=>{(p!==k||p.allowRecurse)&&a.add(p)})};if(t==="clear")s.forEach(c);else if(r==="length"&&H(e))s.forEach((u,p)=>{(p==="length"||p>=n)&&c(u)});else switch(r!==void 0&&c(s.get(r)),t){case"add":H(e)?qe(r)&&c(s.get("length")):(c(s.get(Z)),ie(e)&&c(s.get(Pt)));break;case"delete":H(e)||(c(s.get(Z)),ie(e)&&c(s.get(Pt)));break;case"set":ie(e)&&c(s.get(Z));break}let l=u=>{u.options.onTrigger&&u.options.onTrigger({effect:u,target:e,key:r,type:t,newValue:n,oldValue:i,oldTarget:o}),u.options.scheduler?u.options.scheduler(u):u()};a.forEach(l)}var hi=Tt("__proto__,__v_isRef,__isVue"),on=new Set(Object.getOwnPropertyNames(Symbol).map(e=>Symbol[e]).filter(Ve)),gi=sn();var xi=sn(!0);var Jr=yi();function yi(){let e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...r){let n=h(this);for(let o=0,s=this.length;o<s;o++)N(n,"get",o+"");let i=n[t](...r);return i===-1||i===!1?n[t](...r.map(h)):i}}),["push","pop","shift","unshift","splice"].forEach(t=>{e[t]=function(...r){mi();let n=h(this)[t].apply(this,r);return nn(),n}}),e}function sn(e=!1,t=!1){return function(n,i,o){if(i==="__v_isReactive")return!e;if(i==="__v_isReadonly")return e;if(i==="__v_raw"&&o===(e?t?ki:un:t?Ii:ln).get(n))return n;let s=H(n);if(!e&&s&&xe(Jr,i))return Reflect.get(Jr,i,o);let a=Reflect.get(n,i,o);return(Ve(i)?on.has(i):hi(i))||(e||N(n,"get",i),t)?a:It(a)?!s||!qe(i)?a.value:a:ye(a)?e?fn(a):Qe(a):a}}var bi=wi();function wi(e=!1){return function(r,n,i,o){let s=r[n];if(!e&&(i=h(i),s=h(s),!H(r)&&It(s)&&!It(i)))return s.value=i,!0;let a=H(r)&&qe(n)?Number(n)<r.length:xe(r,n),c=Reflect.set(r,n,i,o);return r===h(o)&&(a?Nt(i,s)&&q(r,"set",n,i,s):q(r,"add",n,i)),c}}function Ei(e,t){let r=xe(e,t),n=e[t],i=Reflect.deleteProperty(e,t);return i&&r&&q(e,"delete",t,void 0,n),i}function vi(e,t){let r=Reflect.has(e,t);return(!Ve(t)||!on.has(t))&&N(e,"has",t),r}function Si(e){return N(e,"iterate",H(e)?"length":Z),Reflect.ownKeys(e)}var Ai={get:gi,set:bi,deleteProperty:Ei,has:vi,ownKeys:Si},Oi={get:xi,set(e,t){return console.warn(`Set operation on key "${String(t)}" failed: target is readonly.`,e),!0},deleteProperty(e,t){return console.warn(`Delete operation on key "${String(t)}" failed: target is readonly.`,e),!0}};var Lt=e=>ye(e)?Qe(e):e,$t=e=>ye(e)?fn(e):e,jt=e=>e,Ze=e=>Reflect.getPrototypeOf(e);function We(e,t,r=!1,n=!1){e=e.__v_raw;let i=h(e),o=h(t);t!==o&&!r&&N(i,"get",t),!r&&N(i,"get",o);let{has:s}=Ze(i),a=n?jt:r?$t:Lt;if(s.call(i,t))return a(e.get(t));if(s.call(i,o))return a(e.get(o));e!==i&&e.get(t)}function Ge(e,t=!1){let r=this.__v_raw,n=h(r),i=h(e);return e!==i&&!t&&N(n,"has",e),!t&&N(n,"has",i),e===i?r.has(e):r.has(e)||r.has(i)}function Je(e,t=!1){return e=e.__v_raw,!t&&N(h(e),"iterate",Z),Reflect.get(e,"size",e)}function Yr(e){e=h(e);let t=h(this);return Ze(t).has.call(t,e)||(t.add(e),q(t,"add",e,e)),this}function Xr(e,t){t=h(t);let r=h(this),{has:n,get:i}=Ze(r),o=n.call(r,e);o?cn(r,n,e):(e=h(e),o=n.call(r,e));let s=i.call(r,e);return r.set(e,t),o?Nt(t,s)&&q(r,"set",e,t,s):q(r,"add",e,t),this}function Zr(e){let t=h(this),{has:r,get:n}=Ze(t),i=r.call(t,e);i?cn(t,r,e):(e=h(e),i=r.call(t,e));let o=n?n.call(t,e):void 0,s=t.delete(e);return i&&q(t,"delete",e,void 0,o),s}function Qr(){let e=h(this),t=e.size!==0,r=ie(e)?new Map(e):new Set(e),n=e.clear();return t&&q(e,"clear",void 0,void 0,r),n}function Ye(e,t){return function(n,i){let o=this,s=o.__v_raw,a=h(s),c=t?jt:e?$t:Lt;return!e&&N(a,"iterate",Z),s.forEach((l,u)=>n.call(i,c(l),c(u),o))}}function Xe(e,t,r){return function(...n){let i=this.__v_raw,o=h(i),s=ie(o),a=e==="entries"||e===Symbol.iterator&&s,c=e==="keys"&&s,l=i[e](...n),u=r?jt:t?$t:Lt;return!t&&N(o,"iterate",c?Pt:Z),{next(){let{value:p,done:m}=l.next();return m?{value:p,done:m}:{value:a?[u(p[0]),u(p[1])]:u(p),done:m}},[Symbol.iterator](){return this}}}}function V(e){return function(...t){{let r=t[0]?`on key "${t[0]}" `:"";console.warn(`${Mt(e)} operation ${r}failed: target is readonly.`,h(this))}return e==="delete"?!1:this}}function Ci(){let e={get(o){return We(this,o)},get size(){return Je(this)},has:Ge,add:Yr,set:Xr,delete:Zr,clear:Qr,forEach:Ye(!1,!1)},t={get(o){return We(this,o,!1,!0)},get size(){return Je(this)},has:Ge,add:Yr,set:Xr,delete:Zr,clear:Qr,forEach:Ye(!1,!0)},r={get(o){return We(this,o,!0)},get size(){return Je(this,!0)},has(o){return Ge.call(this,o,!0)},add:V("add"),set:V("set"),delete:V("delete"),clear:V("clear"),forEach:Ye(!0,!1)},n={get(o){return We(this,o,!0,!0)},get size(){return Je(this,!0)},has(o){return Ge.call(this,o,!0)},add:V("add"),set:V("set"),delete:V("delete"),clear:V("clear"),forEach:Ye(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(o=>{e[o]=Xe(o,!1,!1),r[o]=Xe(o,!0,!1),t[o]=Xe(o,!1,!0),n[o]=Xe(o,!0,!0)}),[e,r,t,n]}var[Ti,Ri,Mi,Ni]=Ci();function an(e,t){let r=t?e?Ni:Mi:e?Ri:Ti;return(n,i,o)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?n:Reflect.get(xe(r,i)&&i in n?r:n,i,o)}var Di={get:an(!1,!1)};var Pi={get:an(!0,!1)};function cn(e,t,r){let n=h(r);if(n!==r&&t.call(e,n)){let i=Rt(e);console.warn(`Reactive ${i} contains both the raw and reactive versions of the same object${i==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var ln=new WeakMap,Ii=new WeakMap,un=new WeakMap,ki=new WeakMap;function Li(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function $i(e){return e.__v_skip||!Object.isExtensible(e)?0:Li(Rt(e))}function Qe(e){return e&&e.__v_isReadonly?e:dn(e,!1,Ai,Di,ln)}function fn(e){return dn(e,!0,Oi,Pi,un)}function dn(e,t,r,n,i){if(!ye(e))return console.warn(`value cannot be made reactive: ${String(e)}`),e;if(e.__v_raw&&!(t&&e.__v_isReactive))return e;let o=i.get(e);if(o)return o;let s=$i(e);if(s===0)return e;let a=new Proxy(e,s===2?n:r);return i.set(e,a),a}function h(e){return e&&h(e.__v_raw)||e}function It(e){return Boolean(e&&e.__v_isRef===!0)}y("nextTick",()=>ne);y("dispatch",e=>G.bind(G,e));y("watch",(e,{evaluateLater:t,cleanup:r})=>(n,i)=>{let o=t(n),a=ve(()=>{let c;return o(l=>c=l),c},i);r(a)});y("store",Br);y("data",e=>Ce(e));y("root",e=>J(e));y("refs",e=>(e._x_refs_proxy||(e._x_refs_proxy=F(ji(e))),e._x_refs_proxy));function ji(e){let t=[];return z(e,r=>{r._x_refs&&t.push(r._x_refs)}),t}var Ft={};function Bt(e){return Ft[e]||(Ft[e]=0),++Ft[e]}function pn(e,t){return z(e,r=>{if(r._x_ids&&r._x_ids[t])return!0})}function mn(e,t){e._x_ids||(e._x_ids={}),e._x_ids[t]||(e._x_ids[t]=Bt(t))}y("id",(e,{cleanup:t})=>(r,n=null)=>{let i=`${r}${n?`-${n}`:""}`;return Fi(e,i,t,()=>{let o=pn(e,r),s=o?o._x_ids[r]:Bt(r);return n?`${r}-${s}-${n}`:`${r}-${s}`})});K((e,t)=>{e._x_id&&(t._x_id=e._x_id)});function Fi(e,t,r,n){if(e._x_id||(e._x_id={}),e._x_id[t])return e._x_id[t];let i=n();return e._x_id[t]=i,r(()=>{delete e._x_id[t]}),i}y("el",e=>e);_n("Focus","focus","focus");_n("Persist","persist","persist");function _n(e,t,r){y(t,n=>E(`You can't use [$${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}d("modelable",(e,{expression:t},{effect:r,evaluateLater:n,cleanup:i})=>{let o=n(t),s=()=>{let u;return o(p=>u=p),u},a=n(`${t} = __placeholder`),c=u=>a(()=>{},{scope:{__placeholder:u}}),l=s();c(l),queueMicrotask(()=>{if(!e._x_model)return;e._x_removeModelListeners.default();let u=e._x_model.get,p=e._x_model.set,m=He({get(){return u()},set(w){p(w)}},{get(){return s()},set(w){c(w)}});i(m)})});d("teleport",(e,{modifiers:t,expression:r},{cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&E("x-teleport can only be used on a <template> tag",e);let i=hn(r),o=e.content.cloneNode(!0).firstElementChild;e._x_teleport=o,o._x_teleportBack=e,e.setAttribute("data-teleport-template",!0),o.setAttribute("data-teleport-target",!0),e._x_forwardEvents&&e._x_forwardEvents.forEach(a=>{o.addEventListener(a,c=>{c.stopPropagation(),e.dispatchEvent(new c.constructor(c.type,c))})}),P(o,{},e);let s=(a,c,l)=>{l.includes("prepend")?c.parentNode.insertBefore(a,c):l.includes("append")?c.parentNode.insertBefore(a,c.nextSibling):c.appendChild(a)};_(()=>{s(o,i,t),A(()=>{S(o),o._x_ignore=!0})()}),e._x_teleportPutBack=()=>{let a=hn(r);_(()=>{s(e._x_teleport,a,t)})},n(()=>o.remove())});var Bi=document.createElement("div");function hn(e){let t=A(()=>document.querySelector(e),()=>Bi)();return t||E(`Cannot find x-teleport element for selector: "${e}"`),t}var gn=()=>{};gn.inline=(e,{modifiers:t},{cleanup:r})=>{t.includes("self")?e._x_ignoreSelf=!0:e._x_ignore=!0,r(()=>{t.includes("self")?delete e._x_ignoreSelf:delete e._x_ignore})};d("ignore",gn);d("effect",A((e,{expression:t},{effect:r})=>{r(x(e,t))}));function se(e,t,r,n){let i=e,o=c=>n(c),s={},a=(c,l)=>u=>l(c,u);if(r.includes("dot")&&(t=zi(t)),r.includes("camel")&&(t=Ki(t)),r.includes("passive")&&(s.passive=!0),r.includes("capture")&&(s.capture=!0),r.includes("window")&&(i=window),r.includes("document")&&(i=document),r.includes("debounce")){let c=r[r.indexOf("debounce")+1]||"invalid-wait",l=et(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=ze(o,l)}if(r.includes("throttle")){let c=r[r.indexOf("throttle")+1]||"invalid-wait",l=et(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=Ke(o,l)}return r.includes("prevent")&&(o=a(o,(c,l)=>{l.preventDefault(),c(l)})),r.includes("stop")&&(o=a(o,(c,l)=>{l.stopPropagation(),c(l)})),r.includes("once")&&(o=a(o,(c,l)=>{c(l),i.removeEventListener(t,o,s)})),(r.includes("away")||r.includes("outside"))&&(i=document,o=a(o,(c,l)=>{e.contains(l.target)||l.target.isConnected!==!1&&(e.offsetWidth<1&&e.offsetHeight<1||e._x_isShown!==!1&&c(l))})),r.includes("self")&&(o=a(o,(c,l)=>{l.target===e&&c(l)})),(Vi(t)||yn(t))&&(o=a(o,(c,l)=>{qi(l,r)||c(l)})),i.addEventListener(t,o,s),()=>{i.removeEventListener(t,o,s)}}function zi(e){return e.replace(/-/g,".")}function Ki(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function et(e){return!Array.isArray(e)&&!isNaN(e)}function Hi(e){return[" ","_"].includes(e)?e:e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[_\s]/,"-").toLowerCase()}function Vi(e){return["keydown","keyup"].includes(e)}function yn(e){return["contextmenu","click","mouse"].some(t=>e.includes(t))}function qi(e,t){let r=t.filter(o=>!["window","document","prevent","stop","once","capture","self","away","outside","passive"].includes(o));if(r.includes("debounce")){let o=r.indexOf("debounce");r.splice(o,et((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.includes("throttle")){let o=r.indexOf("throttle");r.splice(o,et((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.length===0||r.length===1&&xn(e.key).includes(r[0]))return!1;let i=["ctrl","shift","alt","meta","cmd","super"].filter(o=>r.includes(o));return r=r.filter(o=>!i.includes(o)),!(i.length>0&&i.filter(s=>((s==="cmd"||s==="super")&&(s="meta"),e[`${s}Key`])).length===i.length&&(yn(e.type)||xn(e.key).includes(r[0])))}function xn(e){if(!e)return[];e=Hi(e);let t={ctrl:"control",slash:"/",space:" ",spacebar:" ",cmd:"meta",esc:"escape",up:"arrow-up",down:"arrow-down",left:"arrow-left",right:"arrow-right",period:".",comma:",",equal:"=",minus:"-",underscore:"_"};return t[e]=e,Object.keys(t).map(r=>{if(t[r]===e)return r}).filter(r=>r)}d("model",(e,{modifiers:t,expression:r},{effect:n,cleanup:i})=>{let o=e;t.includes("parent")&&(o=e.parentNode);let s=x(o,r),a;typeof r=="string"?a=x(o,`${r} = __placeholder`):typeof r=="function"&&typeof r()=="string"?a=x(o,`${r()} = __placeholder`):a=()=>{};let c=()=>{let m;return s(w=>m=w),bn(m)?m.get():m},l=m=>{let w;s($=>w=$),bn(w)?w.set(m):a(()=>{},{scope:{__placeholder:m}})};typeof r=="string"&&e.type==="radio"&&_(()=>{e.hasAttribute("name")||e.setAttribute("name",r)});var u=e.tagName.toLowerCase()==="select"||["checkbox","radio"].includes(e.type)||t.includes("lazy")?"change":"input";let p=I?()=>{}:se(e,u,t,m=>{l(zt(e,t,m,c()))});if(t.includes("fill")&&([void 0,null,""].includes(c())||e.type==="checkbox"&&Array.isArray(c())||e.tagName.toLowerCase()==="select"&&e.multiple)&&l(zt(e,t,{target:e},c())),e._x_removeModelListeners||(e._x_removeModelListeners={}),e._x_removeModelListeners.default=p,i(()=>e._x_removeModelListeners.default()),e.form){let m=se(e.form,"reset",[],w=>{ne(()=>e._x_model&&e._x_model.set(zt(e,t,{target:e},c())))});i(()=>m())}e._x_model={get(){return c()},set(m){l(m)}},e._x_forceModelUpdate=m=>{m===void 0&&typeof r=="string"&&r.match(/\./)&&(m=""),window.fromModel=!0,_(()=>he(e,"value",m)),delete window.fromModel},n(()=>{let m=c();t.includes("unintrusive")&&document.activeElement.isSameNode(e)||e._x_forceModelUpdate(m)})});function zt(e,t,r,n){return _(()=>{if(r instanceof CustomEvent&&r.detail!==void 0)return r.detail!==null&&r.detail!==void 0?r.detail:r.target.value;if(e.type==="checkbox")if(Array.isArray(n)){let i=null;return t.includes("number")?i=Kt(r.target.value):t.includes("boolean")?i=ge(r.target.value):i=r.target.value,r.target.checked?n.includes(i)?n:n.concat([i]):n.filter(o=>!Ui(o,i))}else return r.target.checked;else{if(e.tagName.toLowerCase()==="select"&&e.multiple)return t.includes("number")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return Kt(o)}):t.includes("boolean")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return ge(o)}):Array.from(r.target.selectedOptions).map(i=>i.value||i.text);{let i;return e.type==="radio"?r.target.checked?i=r.target.value:i=n:i=r.target.value,t.includes("number")?Kt(i):t.includes("boolean")?ge(i):t.includes("trim")?i.trim():i}}})}function Kt(e){let t=e?parseFloat(e):null;return Wi(t)?t:e}function Ui(e,t){return e==t}function Wi(e){return!Array.isArray(e)&&!isNaN(e)}function bn(e){return e!==null&&typeof e=="object"&&typeof e.get=="function"&&typeof e.set=="function"}d("cloak",e=>queueMicrotask(()=>_(()=>e.removeAttribute(C("cloak")))));$e(()=>`[${C("init")}]`);d("init",A((e,{expression:t},{evaluate:r})=>typeof t=="string"?!!t.trim()&&r(t,{},!1):r(t,{},!1)));d("text",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{_(()=>{e.textContent=o})})})});d("html",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{_(()=>{e.innerHTML=o,e._x_ignoreSelf=!0,S(e),delete e._x_ignoreSelf})})})});re(Ie(":",ke(C("bind:"))));var wn=(e,{value:t,modifiers:r,expression:n,original:i},{effect:o,cleanup:s})=>{if(!t){let c={};Hr(c),x(e,n)(u=>{Ct(e,u,i)},{scope:c});return}if(t==="key")return Gi(e,n);if(e._x_inlineBindings&&e._x_inlineBindings[t]&&e._x_inlineBindings[t].extract)return;let a=x(e,n);o(()=>a(c=>{c===void 0&&typeof n=="string"&&n.match(/\./)&&(c=""),_(()=>he(e,t,c,r))})),s(()=>{e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedStyles&&e._x_undoAddedStyles()})};wn.inline=(e,{value:t,modifiers:r,expression:n})=>{t&&(e._x_inlineBindings||(e._x_inlineBindings={}),e._x_inlineBindings[t]={expression:n,extract:!1})};d("bind",wn);function Gi(e,t){e._x_keyExpression=t}Le(()=>`[${C("data")}]`);d("data",(e,{expression:t},{cleanup:r})=>{if(Ji(e))return;t=t===""?"{}":t;let n={};ue(n,e);let i={};Ur(i,n);let o=M(e,t,{scope:i});(o===void 0||o===!0)&&(o={}),ue(o,e);let s=R(o);Te(s);let a=P(e,s);s.init&&M(e,s.init),r(()=>{s.destroy&&M(e,s.destroy),a()})});K((e,t)=>{e._x_dataStack&&(t._x_dataStack=e._x_dataStack,t.setAttribute("data-has-alpine-state",!0))});function Ji(e){return I?Be?!0:e.hasAttribute("data-has-alpine-state"):!1}d("show",(e,{modifiers:t,expression:r},{effect:n})=>{let i=x(e,r);e._x_doHide||(e._x_doHide=()=>{_(()=>{e.style.setProperty("display","none",t.includes("important")?"important":void 0)})}),e._x_doShow||(e._x_doShow=()=>{_(()=>{e.style.length===1&&e.style.display==="none"?e.removeAttribute("style"):e.style.removeProperty("display")})});let o=()=>{e._x_doHide(),e._x_isShown=!1},s=()=>{e._x_doShow(),e._x_isShown=!0},a=()=>setTimeout(s),c=me(p=>p?s():o(),p=>{typeof e._x_toggleAndCascadeWithTransitions=="function"?e._x_toggleAndCascadeWithTransitions(e,p,s,o):p?a():o()}),l,u=!0;n(()=>i(p=>{!u&&p===l||(t.includes("immediate")&&(p?a():o()),c(p),l=p,u=!1)}))});d("for",(e,{expression:t},{effect:r,cleanup:n})=>{let i=Xi(t),o=x(e,i.items),s=x(e,e._x_keyExpression||"index");e._x_prevKeys=[],e._x_lookup={},r(()=>Yi(e,i,o,s)),n(()=>{Object.values(e._x_lookup).forEach(a=>a.remove()),delete e._x_prevKeys,delete e._x_lookup})});function Yi(e,t,r,n){let i=s=>typeof s=="object"&&!Array.isArray(s),o=e;r(s=>{Zi(s)&&s>=0&&(s=Array.from(Array(s).keys(),f=>f+1)),s===void 0&&(s=[]);let a=e._x_lookup,c=e._x_prevKeys,l=[],u=[];if(i(s))s=Object.entries(s).map(([f,g])=>{let b=En(t,g,f,s);n(v=>{u.includes(v)&&E("Duplicate key on x-for",e),u.push(v)},{scope:{index:f,...b}}),l.push(b)});else for(let f=0;f<s.length;f++){let g=En(t,s[f],f,s);n(b=>{u.includes(b)&&E("Duplicate key on x-for",e),u.push(b)},{scope:{index:f,...g}}),l.push(g)}let p=[],m=[],w=[],$=[];for(let f=0;f<c.length;f++){let g=c[f];u.indexOf(g)===-1&&w.push(g)}c=c.filter(f=>!w.includes(f));let we="template";for(let f=0;f<u.length;f++){let g=u[f],b=c.indexOf(g);if(b===-1)c.splice(f,0,g),p.push([we,f]);else if(b!==f){let v=c.splice(f,1)[0],O=c.splice(b-1,1)[0];c.splice(f,0,O),c.splice(b,0,v),m.push([v,O])}else $.push(g);we=g}for(let f=0;f<w.length;f++){let g=w[f];a[g]._x_effects&&a[g]._x_effects.forEach(Ee),a[g].remove(),a[g]=null,delete a[g]}for(let f=0;f<m.length;f++){let[g,b]=m[f],v=a[g],O=a[b],Q=document.createElement("div");_(()=>{O||E('x-for ":key" is undefined or invalid',o,b,a),O.after(Q),v.after(O),O._x_currentIfEl&&O.after(O._x_currentIfEl),Q.before(v),v._x_currentIfEl&&v.after(v._x_currentIfEl),Q.remove()}),O._x_refreshXForScope(l[u.indexOf(b)])}for(let f=0;f<p.length;f++){let[g,b]=p[f],v=g==="template"?o:a[g];v._x_currentIfEl&&(v=v._x_currentIfEl);let O=l[b],Q=u[b],ae=document.importNode(o.content,!0).firstElementChild,Vt=R(O);P(ae,Vt,o),ae._x_refreshXForScope=Sn=>{Object.entries(Sn).forEach(([An,On])=>{Vt[An]=On})},_(()=>{v.after(ae),A(()=>S(ae))()}),typeof Q=="object"&&E("x-for key cannot be an object, it must be a string or an integer",o),a[Q]=ae}for(let f=0;f<$.length;f++)a[$[f]]._x_refreshXForScope(l[u.indexOf($[f])]);o._x_prevKeys=u})}function Xi(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=/^\s*\(|\)\s*$/g,n=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,i=e.match(n);if(!i)return;let o={};o.items=i[2].trim();let s=i[1].replace(r,"").trim(),a=s.match(t);return a?(o.item=s.replace(t,"").trim(),o.index=a[1].trim(),a[2]&&(o.collection=a[2].trim())):o.item=s,o}function En(e,t,r,n){let i={};return/^\[.*\]$/.test(e.item)&&Array.isArray(t)?e.item.replace("[","").replace("]","").split(",").map(s=>s.trim()).forEach((s,a)=>{i[s]=t[a]}):/^\{.*\}$/.test(e.item)&&!Array.isArray(t)&&typeof t=="object"?e.item.replace("{","").replace("}","").split(",").map(s=>s.trim()).forEach(s=>{i[s]=t[s]}):i[e.item]=t,e.index&&(i[e.index]=r),e.collection&&(i[e.collection]=n),i}function Zi(e){return!Array.isArray(e)&&!isNaN(e)}function vn(){}vn.inline=(e,{expression:t},{cleanup:r})=>{let n=J(e);n._x_refs||(n._x_refs={}),n._x_refs[t]=e,r(()=>delete n._x_refs[t])};d("ref",vn);d("if",(e,{expression:t},{effect:r,cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&E("x-if can only be used on a <template> tag",e);let i=x(e,t),o=()=>{if(e._x_currentIfEl)return e._x_currentIfEl;let a=e.content.cloneNode(!0).firstElementChild;return P(a,{},e),_(()=>{e.after(a),A(()=>S(a))()}),e._x_currentIfEl=a,e._x_undoIf=()=>{T(a,c=>{c._x_effects&&c._x_effects.forEach(Ee)}),a.remove(),delete e._x_currentIfEl},a},s=()=>{e._x_undoIf&&(e._x_undoIf(),delete e._x_undoIf)};r(()=>i(a=>{a?o():s()})),n(()=>e._x_undoIf&&e._x_undoIf())});d("id",(e,{expression:t},{evaluate:r})=>{r(t).forEach(i=>mn(e,i))});K((e,t)=>{e._x_ids&&(t._x_ids=e._x_ids)});re(Ie("@",ke(C("on:"))));d("on",A((e,{value:t,modifiers:r,expression:n},{cleanup:i})=>{let o=n?x(e,n):()=>{};e.tagName.toLowerCase()==="template"&&(e._x_forwardEvents||(e._x_forwardEvents=[]),e._x_forwardEvents.includes(t)||e._x_forwardEvents.push(t));let s=se(e,t,r,a=>{o(()=>{},{scope:{$event:a},params:[a]})});i(()=>s())}));tt("Collapse","collapse","collapse");tt("Intersect","intersect","intersect");tt("Focus","trap","focus");tt("Mask","mask","mask");function tt(e,t,r){d(t,n=>E(`You can't use [x-${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}B.setEvaluator(gt);B.setReactivityEngine({reactive:Qe,effect:en,release:tn,raw:h});var Ht=B;window.Alpine=Ht;queueMicrotask(()=>{Ht.start()});})();
diff --git a/exo/tinychat/static/unpkg.com/dompurify@3.1.5/dist/purify.min.js b/exo/tinychat/static/unpkg.com/dompurify@3.1.5/dist/purify.min.js
deleted file mode 100644
index ae1c0836..00000000
--- a/exo/tinychat/static/unpkg.com/dompurify@3.1.5/dist/purify.min.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/*! @license DOMPurify 3.1.5 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.5/LICENSE */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),p=b(Array.prototype.push),f=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(Object.prototype.hasOwnProperty),_=b(RegExp.prototype.test),A=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return s(N,t)});var N;function b(e){return function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return c(e,t,o)}}function S(e,o){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t<e.length;t++){E(e,t)||(e[t]=null)}return e}function w(t){const n=l(null);for(const[o,r]of e(t)){E(t,o)&&(Array.isArray(r)?n[o]=R(r):r&&"object"==typeof r&&r.constructor===Object?n[o]=w(r):n[o]=r)}return n}function C(e,t){for(;null!==e;){const n=r(e,t);if(n){if(n.get)return b(n.get);if("function"==typeof n.value)return b(n.value)}e=o(e)}return function(){return null}}const L=i(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),D=i(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),v=i(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),O=i(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),x=i(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),k=i(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),M=i(["#text"]),I=i(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),U=i(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),P=i(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),F=i(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),H=a(/\{\{[\w\W]*|[\w\W]*\}\}/gm),z=a(/<%[\w\W]*|[\w\W]*%>/gm),B=a(/\${[\w\W]*}/gm),W=a(/^data-[\-\w.\u00B7-\uFFFF]/),G=a(/^aria-[\-\w]+$/),Y=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),X=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q=a(/^html$/i),$=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,MUSTACHE_EXPR:H,ERB_EXPR:z,TMPLIT_EXPR:B,DATA_ATTR:W,ARIA_ATTR:G,IS_ALLOWED_URI:Y,IS_SCRIPT_OR_DATA:j,ATTR_WHITESPACE:X,DOCTYPE_NAME:q,CUSTOM_ELEMENT:$});const V=1,Z=3,J=7,Q=8,ee=9,te=function(){return"undefined"==typeof window?null:window},ne=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};var oe=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:te();const o=e=>t(e);if(o.version="3.1.5",o.removed=[],!n||!n.document||n.document.nodeType!==ee)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:b,Element:R,NodeFilter:H,NamedNodeMap:z=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:B,DOMParser:W,trustedTypes:G}=n,j=R.prototype,X=C(j,"cloneNode"),$=C(j,"nextSibling"),oe=C(j,"childNodes"),re=C(j,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ie,ae="";const{implementation:le,createNodeIterator:ce,createDocumentFragment:se,getElementsByTagName:ue}=r,{importNode:me}=a;let pe={};o.isSupported="function"==typeof e&&"function"==typeof re&&le&&void 0!==le.createHTMLDocument;const{MUSTACHE_EXPR:fe,ERB_EXPR:de,TMPLIT_EXPR:he,DATA_ATTR:ge,ARIA_ATTR:Te,IS_SCRIPT_OR_DATA:ye,ATTR_WHITESPACE:Ee,CUSTOM_ELEMENT:_e}=K;let{IS_ALLOWED_URI:Ae}=K,Ne=null;const be=S({},[...L,...D,...v,...x,...M]);let Se=null;const Re=S({},[...I,...U,...P,...F]);let we=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ce=null,Le=null,De=!0,ve=!0,Oe=!1,xe=!0,ke=!1,Me=!0,Ie=!1,Ue=!1,Pe=!1,Fe=!1,He=!1,ze=!1,Be=!0,We=!1;const Ge="user-content-";let Ye=!0,je=!1,Xe={},qe=null;const $e=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ke=null;const Ve=S({},["audio","video","img","source","image","track"]);let Ze=null;const Je=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Qe="http://www.w3.org/1998/Math/MathML",et="http://www.w3.org/2000/svg",tt="http://www.w3.org/1999/xhtml";let nt=tt,ot=!1,rt=null;const it=S({},[Qe,et,tt],d);let at=null;const lt=["application/xhtml+xml","text/html"],ct="text/html";let st=null,ut=null;const mt=r.createElement("form"),pt=function(e){return e instanceof RegExp||e instanceof Function},ft=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ut||ut!==e){if(e&&"object"==typeof e||(e={}),e=w(e),at=-1===lt.indexOf(e.PARSER_MEDIA_TYPE)?ct:e.PARSER_MEDIA_TYPE,st="application/xhtml+xml"===at?d:f,Ne=E(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,st):be,Se=E(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,st):Re,rt=E(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,d):it,Ze=E(e,"ADD_URI_SAFE_ATTR")?S(w(Je),e.ADD_URI_SAFE_ATTR,st):Je,Ke=E(e,"ADD_DATA_URI_TAGS")?S(w(Ve),e.ADD_DATA_URI_TAGS,st):Ve,qe=E(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,st):$e,Ce=E(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,st):{},Le=E(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,st):{},Xe=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,De=!1!==e.ALLOW_ARIA_ATTR,ve=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,xe=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ke=e.SAFE_FOR_TEMPLATES||!1,Me=!1!==e.SAFE_FOR_XML,Ie=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,He=e.RETURN_DOM_FRAGMENT||!1,ze=e.RETURN_TRUSTED_TYPE||!1,Pe=e.FORCE_BODY||!1,Be=!1!==e.SANITIZE_DOM,We=e.SANITIZE_NAMED_PROPS||!1,Ye=!1!==e.KEEP_CONTENT,je=e.IN_PLACE||!1,Ae=e.ALLOWED_URI_REGEXP||Y,nt=e.NAMESPACE||tt,we=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&pt(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(we.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&pt(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(we.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(we.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),ke&&(ve=!1),He&&(Fe=!0),Xe&&(Ne=S({},M),Se=[],!0===Xe.html&&(S(Ne,L),S(Se,I)),!0===Xe.svg&&(S(Ne,D),S(Se,U),S(Se,F)),!0===Xe.svgFilters&&(S(Ne,v),S(Se,U),S(Se,F)),!0===Xe.mathMl&&(S(Ne,x),S(Se,P),S(Se,F))),e.ADD_TAGS&&(Ne===be&&(Ne=w(Ne)),S(Ne,e.ADD_TAGS,st)),e.ADD_ATTR&&(Se===Re&&(Se=w(Se)),S(Se,e.ADD_ATTR,st)),e.ADD_URI_SAFE_ATTR&&S(Ze,e.ADD_URI_SAFE_ATTR,st),e.FORBID_CONTENTS&&(qe===$e&&(qe=w(qe)),S(qe,e.FORBID_CONTENTS,st)),Ye&&(Ne["#text"]=!0),Ie&&S(Ne,["html","head","body"]),Ne.table&&(S(Ne,["tbody"]),delete Ce.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ie=e.TRUSTED_TYPES_POLICY,ae=ie.createHTML("")}else void 0===ie&&(ie=ne(G,c)),null!==ie&&"string"==typeof ae&&(ae=ie.createHTML(""));i&&i(e),ut=e}},dt=S({},["mi","mo","mn","ms","mtext"]),ht=S({},["foreignobject","annotation-xml"]),gt=S({},["title","style","font","a","script"]),Tt=S({},[...D,...v,...O]),yt=S({},[...x,...k]),Et=function(e){let t=re(e);t&&t.tagName||(t={namespaceURI:nt,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!rt[e.namespaceURI]&&(e.namespaceURI===et?t.namespaceURI===tt?"svg"===n:t.namespaceURI===Qe?"svg"===n&&("annotation-xml"===o||dt[o]):Boolean(Tt[n]):e.namespaceURI===Qe?t.namespaceURI===tt?"math"===n:t.namespaceURI===et?"math"===n&&ht[o]:Boolean(yt[n]):e.namespaceURI===tt?!(t.namespaceURI===et&&!ht[o])&&(!(t.namespaceURI===Qe&&!dt[o])&&(!yt[n]&&(gt[n]||!Tt[n]))):!("application/xhtml+xml"!==at||!rt[e.namespaceURI]))},_t=function(e){p(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},At=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Se[e])if(Fe||He)try{_t(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Nt=function(e){let t=null,n=null;if(Pe)e="<remove></remove>"+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===at&&nt===tt&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const o=ie?ie.createHTML(e):e;if(nt===tt)try{t=(new W).parseFromString(o,at)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(nt,"template",null);try{t.documentElement.innerHTML=ot?ae:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),nt===tt?ue.call(t,Ie?"html":"body")[0]:Ie?t.documentElement:i},bt=function(e){return ce.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT|H.SHOW_PROCESSING_INSTRUCTION|H.SHOW_CDATA_SECTION,null)},St=function(e){return e instanceof B&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof z)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Rt=function(e){return"function"==typeof b&&e instanceof b},wt=function(e,t,n){pe[e]&&u(pe[e],(e=>{e.call(o,t,n,ut)}))},Ct=function(e){let t=null;if(wt("beforeSanitizeElements",e,null),St(e))return _t(e),!0;const n=st(e.nodeName);if(wt("uponSanitizeElement",e,{tagName:n,allowedTags:Ne}),e.hasChildNodes()&&!Rt(e.firstElementChild)&&_(/<[/\w]/g,e.innerHTML)&&_(/<[/\w]/g,e.textContent))return _t(e),!0;if(e.nodeType===J)return _t(e),!0;if(Me&&e.nodeType===Q&&_(/<[/\w]/g,e.data))return _t(e),!0;if(!Ne[n]||Ce[n]){if(!Ce[n]&&Dt(n)){if(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,n))return!1;if(we.tagNameCheck instanceof Function&&we.tagNameCheck(n))return!1}if(Ye&&!qe[n]){const t=re(e)||e.parentNode,n=oe(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=X(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,$(e))}}}return _t(e),!0}return e instanceof R&&!Et(e)?(_t(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!_(/<\/no(script|embed|frames)/i,e.innerHTML)?(ke&&e.nodeType===Z&&(t=e.textContent,u([fe,de,he],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),wt("afterSanitizeElements",e,null),!1):(_t(e),!0)},Lt=function(e,t,n){if(Be&&("id"===t||"name"===t)&&(n in r||n in mt))return!1;if(ve&&!Le[t]&&_(ge,t));else if(De&&_(Te,t));else if(!Se[t]||Le[t]){if(!(Dt(e)&&(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,e)||we.tagNameCheck instanceof Function&&we.tagNameCheck(e))&&(we.attributeNameCheck instanceof RegExp&&_(we.attributeNameCheck,t)||we.attributeNameCheck instanceof Function&&we.attributeNameCheck(t))||"is"===t&&we.allowCustomizedBuiltInElements&&(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,n)||we.tagNameCheck instanceof Function&&we.tagNameCheck(n))))return!1}else if(Ze[t]);else if(_(Ae,g(n,Ee,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!Ke[e]){if(Oe&&!_(ye,g(n,Ee,"")));else if(n)return!1}else;return!0},Dt=function(e){return"annotation-xml"!==e&&h(e,_e)},vt=function(e){wt("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Se};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=st(a);let p="value"===a?c:y(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,wt("uponSanitizeAttribute",e,n),p=n.attrValue,n.forceKeepAttr)continue;if(At(a,e),!n.keepAttr)continue;if(!xe&&_(/\/>/i,p)){At(a,e);continue}if(Me&&_(/((--!?|])>)|<\/(style|title)/i,p)){At(a,e);continue}ke&&u([fe,de,he],(e=>{p=g(p,e," ")}));const f=st(e.nodeName);if(Lt(f,s,p)){if(!We||"id"!==s&&"name"!==s||(At(a,e),p=Ge+p),ie&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(f,s)){case"TrustedHTML":p=ie.createHTML(p);break;case"TrustedScriptURL":p=ie.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),St(e)?_t(e):m(o.removed)}catch(e){}}}wt("afterSanitizeAttributes",e,null)},Ot=function e(t){let n=null;const o=bt(t);for(wt("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)wt("uponSanitizeShadowNode",n,null),Ct(n)||(n.content instanceof s&&e(n.content),vt(n));wt("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(ot=!e,ot&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Rt(e)){if("function"!=typeof e.toString)throw A("toString is not a function");if("string"!=typeof(e=e.toString()))throw A("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Ue||ft(t),o.removed=[],"string"==typeof e&&(je=!1),je){if(e.nodeName){const t=st(e.nodeName);if(!Ne[t]||Ce[t])throw A("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof b)n=Nt("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===V&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Fe&&!ke&&!Ie&&-1===e.indexOf("<"))return ie&&ze?ie.createHTML(e):e;if(n=Nt(e),!n)return Fe?null:ze?ae:""}n&&Pe&&_t(n.firstChild);const c=bt(je?e:n);for(;i=c.nextNode();)Ct(i)||(i.content instanceof s&&Ot(i.content),vt(i));if(je)return e;if(Fe){if(He)for(l=se.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Se.shadowroot||Se.shadowrootmode)&&(l=me.call(a,l,!0)),l}let m=Ie?n.outerHTML:n.innerHTML;return Ie&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&_(q,n.ownerDocument.doctype.name)&&(m="<!DOCTYPE "+n.ownerDocument.doctype.name+">\n"+m),ke&&u([fe,de,he],(e=>{m=g(m,e," ")})),ie&&ze?ie.createHTML(m):m},o.setConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};ft(e),Ue=!0},o.clearConfig=function(){ut=null,Ue=!1},o.isValidAttribute=function(e,t,n){ut||ft({});const o=st(e),r=st(t);return Lt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(pe[e]=pe[e]||[],p(pe[e],t))},o.removeHook=function(e){if(pe[e])return m(pe[e])},o.removeHooks=function(e){pe[e]&&(pe[e]=[])},o.removeAllHooks=function(){pe={}},o}();return oe}));
-//# sourceMappingURL=purify.min.js.map
diff --git a/exo/tinychat/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js b/exo/tinychat/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js
deleted file mode 100644
index d7af7950..00000000
--- a/exo/tinychat/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js
+++ /dev/null
@@ -1,97 +0,0 @@
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.markedHighlight = {}));
-})(this, (function (exports) { 'use strict';
-
- function markedHighlight(options) {
- if (typeof options === 'function') {
- options = {
- highlight: options
- };
- }
-
- if (!options || typeof options.highlight !== 'function') {
- throw new Error('Must provide highlight function');
- }
-
- if (typeof options.langPrefix !== 'string') {
- options.langPrefix = 'language-';
- }
-
- return {
- async: !!options.async,
- walkTokens(token) {
- if (token.type !== 'code') {
- return;
- }
-
- const lang = getLang(token.lang);
-
- if (options.async) {
- return Promise.resolve(options.highlight(token.text, lang, token.lang || '')).then(updateToken(token));
- }
-
- const code = options.highlight(token.text, lang, token.lang || '');
- if (code instanceof Promise) {
- throw new Error('markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.');
- }
- updateToken(token)(code);
- },
- useNewRenderer: true,
- renderer: {
- code({ text, lang, escaped }) {
- const language = getLang(lang);
- const classAttr = language
- ? ` class="${options.langPrefix}${escape(language)}"`
- : '';
- text = text.replace(/\n$/, '');
- return `<pre><code${classAttr}>${escaped ? text : escape(text, true)}\n</code></pre>`;
- }
- }
- };
- }
-
- function getLang(lang) {
- return (lang || '').match(/\S*/)[0];
- }
-
- function updateToken(token) {
- return (code) => {
- if (typeof code === 'string' && code !== token.text) {
- token.escaped = true;
- token.text = code;
- }
- };
- }
-
- // copied from marked helpers
- const escapeTest = /[&<>"']/;
- const escapeReplace = new RegExp(escapeTest.source, 'g');
- const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
- const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
- const escapeReplacements = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": '''
- };
- const getEscapeReplacement = (ch) => escapeReplacements[ch];
- function escape(html, encode) {
- if (encode) {
- if (escapeTest.test(html)) {
- return html.replace(escapeReplace, getEscapeReplacement);
- }
- } else {
- if (escapeTestNoEncode.test(html)) {
- return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
- }
- }
-
- return html;
- }
-
- exports.markedHighlight = markedHighlight;
-
-}));
diff --git a/exo/tinychat/static/unpkg.com/marked@13.0.0/marked.min.js b/exo/tinychat/static/unpkg.com/marked@13.0.0/marked.min.js
deleted file mode 100644
index 774998f7..00000000
--- a/exo/tinychat/static/unpkg.com/marked@13.0.0/marked.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * marked v13.0.0 - a markdown parser
- * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed)
- * https://github.com/markedjs/marked
- */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;const p=/(^|[^\[])\^/g;function u(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(p,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function k(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const g={exec:()=>null};function f(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length<t;)n.push("");for(;s<n.length;s++)n[s]=n[s].trim().replace(/\\\|/g,"|");return n}function d(e,t,n){const s=e.length;if(0===s)return"";let r=0;for(;r<s;){const i=e.charAt(s-r-1);if(i!==t||n){if(i===t||!n)break;r++}else r++}return e.slice(0,s-r)}function x(e,t,n,s){const r=t.href,i=t.title?c(t.title):null,l=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){s.state.inLink=!0;const e={type:"link",raw:n,href:r,title:i,text:l,tokens:s.inlineTokens(l)};return s.state.inLink=!1,e}return{type:"image",raw:n,href:r,title:i,text:c(l)}}class b{options;rules;lexer;constructor(t){this.options=t||e.defaults}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:d(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=d(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:d(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=d(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l<e.length;l++)if(/^ {0,3}>/.test(e[l]))i.push(e[l]),t=!0;else{if(t)break;i.push(e[l])}e=e.slice(l);const o=i.join("\n"),a=o.replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1").replace(/^ {0,3}>[ \t]?/gm,"");n=n?`${n}\n${o}`:o,s=s?`${s}\n${a}`:a;const c=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(a,r,!0),this.lexer.state.top=c,0===e.length)break;const h=r[r.length-1];if("code"===h?.type)break;if("blockquote"===h?.type){const t=h,i=t.raw+"\n"+e.join("\n"),l=this.blockquote(i);r[r.length-1]=l,n=n.substring(0,n.length-t.raw.length)+l.raw,s=s.substring(0,s.length-t.text.length)+l.text;break}if("list"!==h?.type);else{const t=h,i=t.raw+"\n"+e.join("\n"),l=this.list(i);r[r.length-1]=l,n=n.substring(0,n.length-h.raw.length)+l.raw,s=s.substring(0,s.length-t.raw.length)+l.raw,e=i.substring(r[r.length-1].raw.length).split("\n")}}return{type:"blockquote",raw:n,tokens:r,text:s}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e<r.items.length;e++)if(this.lexer.state.top=!1,r.items[e].tokens=this.lexer.blockTokens(r.items[e].text,[]),!r.loose){const t=r.items[e].tokens.filter((e=>"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e<r.items.length;e++)r.items[e].loose=!0;return r}}html(e){const t=this.rules.block.html.exec(e);if(t){return{type:"html",block:!0,raw:t[0],pre:"pre"===t[1]||"script"===t[1]||"style"===t[1],text:t[0]}}}def(e){const t=this.rules.block.def.exec(e);if(t){const e=t[1].toLowerCase().replace(/\s+/g," "),n=t[2]?t[2].replace(/^<(.*)>$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=f(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(let e=0;e<n.length;e++)i.header.push({text:n[e],tokens:this.lexer.inline(n[e]),header:!0,align:i.align[e]});for(const e of r)i.rows.push(f(e,i.header.length).map(((e,t)=>({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^<a /i.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&/^<\/a>/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^</.test(e)){if(!/>$/.test(e))return;const t=d(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s<e.length;s++)if("\\"===e[s])s++;else if(e[s]===t[0])n++;else if(e[s]===t[1]&&(n--,n<0))return s;return-1}(t[2],"()");if(e>-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^</.test(n)&&(n=this.options.pedantic&&!/>$/.test(e)?n.slice(1):n.slice(1,-1)),x(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return x(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const w=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,m=/(?:[*+-]|\d{1,9}[.)])/,y=u(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,m).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),$=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,z=/(?!\s*\])(?:\\.|[^\[\]\\])+/,T=u(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",z).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),R=u(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,m).getRegex(),_="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",A=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,S=u("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",A).replace("tag",_).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),I=u($).replace("hr",w).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex(),E={blockquote:u(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",I).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:T,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:w,html:S,lheading:y,list:R,newline:/^(?: *(?:\n|$))+/,paragraph:I,table:g,text:/^[^\n]+/},q=u("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",w).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex(),Z={...E,table:q,paragraph:u($).replace("hr",w).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",q).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",_).getRegex()},L={...E,html:u("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:\"[^\"]*\"|'[^']*'|\\s[^'\"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",A).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:g,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:u($).replace("hr",w).replace("heading"," *#{1,6} *[^\n]").replace("lheading",y).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},P=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,Q="\\p{P}\\p{S}",B=u(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,Q).getRegex(),C=u(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,Q).getRegex(),M=u("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,Q).getRegex(),O=u("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,Q).getRegex(),j=u(/\\([punct])/,"gu").replace(/punct/g,Q).getRegex(),D=u(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),H=u(A).replace("(?:--\x3e|$)","--\x3e").getRegex(),F=u("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment",H).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),U=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,X=u(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",U).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),N=u(/^!?\[(label)\]\[(ref)\]/).replace("label",U).replace("ref",z).getRegex(),G=u(/^!?\[(ref)\](?:\[\])?/).replace("ref",z).getRegex(),J={_backpedal:g,anyPunctuation:j,autolink:D,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:g,emStrongLDelim:C,emStrongRDelimAst:M,emStrongRDelimUnd:O,escape:P,link:X,nolink:G,punctuation:B,reflink:N,reflinkSearch:u("reflink|nolink(?!\\()","g").replace("reflink",N).replace("nolink",G).getRegex(),tag:F,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,url:g},K={...J,link:u(/^!?\[(label)\]\((.*?)\)/).replace("label",U).getRegex(),reflink:u(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",U).getRegex()},V={...J,escape:u(P).replace("])","~|])").getRegex(),url:u(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/},W={...V,br:u(v).replace("{2,}","*").getRegex(),text:u(V.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},Y={normal:E,gfm:Z,pedantic:L},ee={normal:J,gfm:V,breaks:W,pedantic:K};class te{tokens;options;state;tokenizer;inlineQueue;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||e.defaults,this.options.tokenizer=this.options.tokenizer||new b,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};const n={block:Y.normal,inline:ee.normal};this.options.pedantic?(n.block=Y.pedantic,n.inline=ee.pedantic):this.options.gfm&&(n.block=Y.gfm,this.options.breaks?n.inline=ee.breaks:n.inline=ee.gfm),this.tokenizer.rules=n}static get rules(){return{block:Y,inline:ee}}static lex(e,t){return new te(t).lex(e)}static lexInline(e,t){return new te(t).inlineTokens(e)}lex(e){e=e.replace(/\r\n|\r/g,"\n"),this.blockTokens(e,this.tokens);for(let e=0;e<this.inlineQueue.length;e++){const t=this.inlineQueue[e];this.inlineTokens(t.src,t.tokens)}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){let s,r,i;for(e=this.options.pedantic?e.replace(/\t/g," ").replace(/^ +$/gm,""):e.replace(/^( *)(\t+)/gm,((e,t,n)=>t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((n=>!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0)))))if(s=this.tokenizer.space(e))e=e.substring(s.raw.length),1===s.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(s);else if(s=this.tokenizer.code(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?t.push(s):(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.fences(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.heading(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.hr(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.blockquote(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.list(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.html(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.def(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title}):(r.raw+="\n"+s.raw,r.text+="\n"+s.raw,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.table(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.lheading(e))e=e.substring(s.raw.length),t.push(s);else{if(i=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(i=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i)))r=t[t.length-1],n&&"paragraph"===r?.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);else if(s=this.tokenizer.text(e))e=e.substring(s.raw.length),r=t[t.length-1],r&&"text"===r.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class ne{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(/^\S*/)?.[0],r=e.replace(/\n$/,"")+"\n";return s?'<pre><code class="language-'+c(s)+'">'+(n?r:c(r,!0))+"</code></pre>\n":"<pre><code>"+(n?r:c(r,!0))+"</code></pre>\n"}blockquote({tokens:e}){return`<blockquote>\n${this.parser.parse(e)}</blockquote>\n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`<h${t}>${this.parser.parseInline(e)}</h${t}>\n`}hr(e){return"<hr>\n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t<e.items.length;t++){const n=e.items[t];s+=this.listitem(n)}const r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+s+"</"+r+">\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?e.tokens.length>0&&"paragraph"===e.tokens[0].type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+e.tokens[0].tokens[0].text)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" "}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`<li>${t}</li>\n`}checkbox({checked:e}){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox">'}paragraph({tokens:e}){return`<p>${this.parser.parseInline(e)}</p>\n`}table(e){let t="",n="";for(let t=0;t<e.header.length;t++)n+=this.tablecell(e.header[t]);t+=this.tablerow({text:n});let s="";for(let t=0;t<e.rows.length;t++){const r=e.rows[t];n="";for(let e=0;e<r.length;e++)n+=this.tablecell(r[e]);s+=this.tablerow({text:n})}return s&&(s=`<tbody>${s}</tbody>`),"<table>\n<thead>\n"+t+"</thead>\n"+s+"</table>\n"}tablerow({text:e}){return`<tr>\n${e}</tr>\n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>\n`}strong({tokens:e}){return`<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return`<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return`<code>${e}</code>`}br(e){return"<br>"}del({tokens:e}){return`<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=k(e);if(null===r)return s;let i='<a href="'+(e=r)+'"';return t&&(i+=' title="'+t+'"'),i+=">"+s+"</a>",i}image({href:e,title:t,text:n}){const s=k(e);if(null===s)return n;let r=`<img src="${e=s}" alt="${n}"`;return t&&(r+=` title="${t}"`),r+=">",r}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):e.text}}class se{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}}class re{options;renderer;textRenderer;constructor(t){this.options=t||e.defaults,this.options.renderer=this.options.renderer||new ne,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new se}static parse(e,t){return new re(t).parse(e)}static parseInline(e,t){return new re(t).parseInline(e)}parse(e,t=!0){let n="";for(let s=0;s<e.length;s++){const r=e[s];if(this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[r.type]){const e=r,t=this.options.extensions.renderers[e.type].call({parser:this},e);if(!1!==t||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(e.type)){n+=t||"";continue}}const i=r;switch(i.type){case"space":n+=this.renderer.space(i);continue;case"hr":n+=this.renderer.hr(i);continue;case"heading":n+=this.renderer.heading(i);continue;case"code":n+=this.renderer.code(i);continue;case"table":n+=this.renderer.table(i);continue;case"blockquote":n+=this.renderer.blockquote(i);continue;case"list":n+=this.renderer.list(i);continue;case"html":n+=this.renderer.html(i);continue;case"paragraph":n+=this.renderer.paragraph(i);continue;case"text":{let r=i,l=this.renderer.text(r);for(;s+1<e.length&&"text"===e[s+1].type;)r=e[++s],l+="\n"+this.renderer.text(r);n+=t?this.renderer.paragraph({type:"paragraph",raw:l,text:l,tokens:[{type:"text",raw:l,text:l}]}):l;continue}default:{const e='Token with "'+i.type+'" type was not found.';if(this.options.silent)return console.error(e),"";throw new Error(e)}}}return n}parseInline(e,t){t=t||this.renderer;let n="";for(let s=0;s<e.length;s++){const r=e[s];if(this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[r.type]){const e=this.options.extensions.renderers[r.type].call({parser:this},r);if(!1!==e||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(r.type)){n+=e||"";continue}}const i=r;switch(i.type){case"escape":case"text":n+=t.text(i);break;case"html":n+=t.html(i);break;case"link":n+=t.link(i);break;case"image":n+=t.image(i);break;case"strong":n+=t.strong(i);break;case"em":n+=t.em(i);break;case"codespan":n+=t.codespan(i);break;case"br":n+=t.br(i);break;case"del":n+=t.del(i);break;default:{const e='Token with "'+i.type+'" type was not found.';if(this.options.silent)return console.error(e),"";throw new Error(e)}}}return n}}class ie{options;constructor(t){this.options=t||e.defaults}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}}class le{defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};options=this.setOptions;parse=this.#e(te.lex,re.parse);parseInline=this.#e(te.lexInline,re.parseInline);Parser=re;Renderer=ne;TextRenderer=se;Lexer=te;Tokenizer=b;Hooks=ie;constructor(...e){this.use(...e)}walkTokens(e,t){let n=[];for(const s of e)switch(n=n.concat(t.call(this,s)),s.type){case"table":{const e=s;for(const s of e.header)n=n.concat(this.walkTokens(s.tokens,t));for(const s of e.rows)for(const e of s)n=n.concat(this.walkTokens(e.tokens,t));break}case"list":{const e=s;n=n.concat(this.walkTokens(e.items,t));break}default:{const e=s;this.defaults.extensions?.childTokens?.[e.type]?this.defaults.extensions.childTokens[e.type].forEach((s=>{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new ne(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n;let r=e.renderer[s];const i=t[s];t[s]=(...n)=>{e.useNewRenderer||(r=this.#t(r,s,t));let l=r.apply(t,n);return!1===l&&(l=i.apply(t,n)),l||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new b(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new ie;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];ie.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}#t(e,t,n){switch(t){case"heading":return function(s){return s.type&&s.type===t?e(n.parser.parseInline(s.tokens),s.depth,function(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}(n.parser.parseInline(s.tokens,n.parser.textRenderer))):e.apply(this,arguments)};case"code":return function(n){return n.type&&n.type===t?e(n.text,n.lang,!!n.escaped):e.apply(this,arguments)};case"table":return function(n){if(!n.type||n.type!==t)return e.apply(this,arguments);let s="",r="";for(let e=0;e<n.header.length;e++)r+=this.tablecell({text:n.header[e].text,tokens:n.header[e].tokens,header:!0,align:n.align[e]});s+=this.tablerow({text:r});let i="";for(let e=0;e<n.rows.length;e++){const t=n.rows[e];r="";for(let e=0;e<t.length;e++)r+=this.tablecell({text:t[e].text,tokens:t[e].tokens,header:!1,align:n.align[e]});i+=this.tablerow({text:r})}return e(s,i)};case"blockquote":return function(n){if(!n.type||n.type!==t)return e.apply(this,arguments);const s=this.parser.parse(n.tokens);return e(s)};case"list":return function(n){if(!n.type||n.type!==t)return e.apply(this,arguments);const s=n.ordered,r=n.start,i=n.loose;let l="";for(let e=0;e<n.items.length;e++){const t=n.items[e],s=t.checked,r=t.task;let o="";if(t.task){const e=this.checkbox({checked:!!s});i?t.tokens.length>0&&"paragraph"===t.tokens[0].type?(t.tokens[0].text=e+" "+t.tokens[0].text,t.tokens[0].tokens&&t.tokens[0].tokens.length>0&&"text"===t.tokens[0].tokens[0].type&&(t.tokens[0].tokens[0].text=e+" "+t.tokens[0].tokens[0].text)):t.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parser.parse(t.tokens,i),l+=this.listitem({type:"list_item",raw:o,text:o,task:r,checked:!!s,loose:i,tokens:t.tokens})}return e(l,s,r)};case"html":return function(n){return n.type&&n.type===t?e(n.text,n.block):e.apply(this,arguments)};case"paragraph":case"strong":case"em":case"del":return function(n){return n.type&&n.type===t?e(this.parser.parseInline(n.tokens)):e.apply(this,arguments)};case"escape":case"codespan":case"text":return function(n){return n.type&&n.type===t?e(n.text):e.apply(this,arguments)};case"link":return function(n){return n.type&&n.type===t?e(n.href,n.title,this.parser.parseInline(n.tokens)):e.apply(this,arguments)};case"image":return function(n){return n.type&&n.type===t?e(n.href,n.title,n.text):e.apply(this,arguments)}}return e}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return te.lex(e,t??this.defaults)}parser(e,t){return re.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#n(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#n(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="<p>An error occurred:</p><pre>"+c(n.message+"",!0)+"</pre>";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const oe=new le;function ae(e,t){return oe.parse(e,t)}ae.options=ae.setOptions=function(e){return oe.setOptions(e),ae.defaults=oe.defaults,n(ae.defaults),ae},ae.getDefaults=t,ae.defaults=e.defaults,ae.use=function(...e){return oe.use(...e),ae.defaults=oe.defaults,n(ae.defaults),ae},ae.walkTokens=function(e,t){return oe.walkTokens(e,t)},ae.parseInline=oe.parseInline,ae.Parser=re,ae.parser=re.parse,ae.Renderer=ne,ae.TextRenderer=se,ae.Lexer=te,ae.lexer=te.lex,ae.Tokenizer=b,ae.Hooks=ie,ae.parse=ae;const ce=ae.options,he=ae.setOptions,pe=ae.use,ue=ae.walkTokens,ke=ae.parseInline,ge=ae,fe=re.parse,de=te.lex;e.Hooks=ie,e.Lexer=te,e.Marked=le,e.Parser=re,e.Renderer=ne,e.TextRenderer=se,e.Tokenizer=b,e.getDefaults=t,e.lexer=de,e.marked=ae,e.options=ce,e.parse=ge,e.parseInline=ke,e.parser=fe,e.setOptions=he,e.use=pe,e.walkTokens=ue}));
diff --git a/exo/tinychat/update_deps.py b/exo/tinychat/update_deps.py
deleted file mode 100644
index dfacc513..00000000
--- a/exo/tinychat/update_deps.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import os
-import requests
-from bs4 import BeautifulSoup
-from urllib.parse import urljoin, urlparse
-import re
-
-
-def download_file(url, local_path):
- response = requests.get(url)
- if response.status_code == 200:
- os.makedirs(os.path.dirname(local_path), exist_ok=True)
- with open(local_path, 'wb') as f:
- f.write(response.content)
- print(f"Downloaded: {local_path}")
- else:
- print(response.status_code)
- print(f"Failed to download: {url}")
-
-
-def update_html(html_content, base_url):
- soup = BeautifulSoup(html_content, 'html.parser')
-
- for tag in soup.find_all(['script', 'link']):
- if tag.has_attr('src'):
- url = tag['src']
- elif tag.has_attr('href'):
- url = tag['href']
- else:
- continue
-
- if url.startswith(('http://', 'https://')):
- full_url = url
- else:
- full_url = urljoin(base_url, url)
-
- parsed_url = urlparse(full_url)
- local_path = os.path.join('static', parsed_url.netloc, parsed_url.path.lstrip('/'))
-
- download_file(full_url, local_path)
-
- relative_path = os.path.relpath(local_path, '.')
- if tag.name == 'script':
- tag['src'] = "/" + relative_path
- elif tag.name == 'link':
- tag['href'] = "/" + relative_path
-
- return str(soup)
-
-
-# Read the HTML file
-with open('./index.html', 'r') as f:
- html_content = f.read()
-
-# Update HTML and download files
-# updated_html = update_html(html_content, 'https://example.com')
-
-# # Write the updated HTML
-# with open('./index.html', 'w') as f:
-# f.write(updated_html)
-
-print("HTML file updated with local paths.")
-
-# Download Font Awesome CSS and font files
-base_url = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/"
-css_url = urljoin(base_url, "css/all.min.css")
-output_dir = "static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2"
-
-# Download CSS file
-css_output_path = os.path.join(output_dir, "css", "all.min.css")
-download_file(css_url, css_output_path)
-
-# Parse CSS file for font URLs
-with open(css_output_path, 'r', encoding='utf-8') as f:
- css_content = f.read()
-
-# Extract font URLs from the CSS content
-font_urls = re.findall(r'url\((.*?\.(?:woff2|ttf))\)', css_content)
-
-print(f"Found {len(font_urls)} font URLs")
-
-# Download font files
-for font_url in font_urls:
- font_url = font_url.strip('"\'')
- if font_url.startswith('../'):
- font_url = font_url[3:]
-
- # Use base_url instead of urljoin to keep the version number
- full_url = base_url + font_url
- relative_path = font_url
- output_path = os.path.join(output_dir, relative_path)
- download_file(full_url, output_path)
-
-print("Download complete!")
diff --git a/exo/topology/__init__.py b/exo/topology/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/topology/device_capabilities.py b/exo/topology/device_capabilities.py
deleted file mode 100644
index 4794c43a..00000000
--- a/exo/topology/device_capabilities.py
+++ /dev/null
@@ -1,294 +0,0 @@
-from typing import Any
-from pydantic import BaseModel
-from exo import DEBUG
-import subprocess
-import psutil
-import asyncio
-from exo.helpers import get_mac_system_info, subprocess_pool
-
-TFLOPS = 1.00
-
-
-class DeviceFlops(BaseModel):
- # units of TFLOPS
- fp32: float
- fp16: float
- int8: float
-
- def __str__(self):
- return f"fp32: {self.fp32 / TFLOPS:.2f} TFLOPS, fp16: {self.fp16 / TFLOPS:.2f} TFLOPS, int8: {self.int8 / TFLOPS:.2f} TFLOPS"
-
- def to_dict(self):
- return self.model_dump()
-
-
-class DeviceCapabilities(BaseModel):
- model: str
- chip: str
- memory: int
- flops: DeviceFlops
-
- def __str__(self):
- return f"Model: {self.model}. Chip: {self.chip}. Memory: {self.memory}MB. Flops: {self.flops}"
-
- def model_post_init(self, __context: Any) -> None:
- if isinstance(self.flops, dict):
- self.flops = DeviceFlops(**self.flops)
-
- def to_dict(self):
- return {"model": self.model, "chip": self.chip, "memory": self.memory, "flops": self.flops.to_dict()}
-
-
-UNKNOWN_DEVICE_CAPABILITIES = DeviceCapabilities(model="Unknown Model", chip="Unknown Chip", memory=0, flops=DeviceFlops(fp32=0, fp16=0, int8=0))
-
-CHIP_FLOPS = {
- # Source: https://www.cpu-monkey.com
- # Note: currently no distinction between variants of M3 Max and M3 Pro, we pick the lower one to be conservative
- ### M chips
- "Apple M1": DeviceFlops(fp32=2.29*TFLOPS, fp16=4.58*TFLOPS, int8=9.16*TFLOPS),
- "Apple M1 Pro": DeviceFlops(fp32=5.30*TFLOPS, fp16=10.60*TFLOPS, int8=21.20*TFLOPS),
- "Apple M1 Max": DeviceFlops(fp32=10.60*TFLOPS, fp16=21.20*TFLOPS, int8=42.40*TFLOPS),
- "Apple M1 Ultra": DeviceFlops(fp32=21.20*TFLOPS, fp16=42.40*TFLOPS, int8=84.80*TFLOPS),
- "Apple M2": DeviceFlops(fp32=3.55*TFLOPS, fp16=7.10*TFLOPS, int8=14.20*TFLOPS),
- "Apple M2 Pro": DeviceFlops(fp32=5.68*TFLOPS, fp16=11.36*TFLOPS, int8=22.72*TFLOPS),
- "Apple M2 Max": DeviceFlops(fp32=13.49*TFLOPS, fp16=26.98*TFLOPS, int8=53.96*TFLOPS),
- "Apple M2 Ultra": DeviceFlops(fp32=26.98*TFLOPS, fp16=53.96*TFLOPS, int8=107.92*TFLOPS),
- "Apple M3": DeviceFlops(fp32=3.55*TFLOPS, fp16=7.10*TFLOPS, int8=14.20*TFLOPS),
- "Apple M3 Pro": DeviceFlops(fp32=4.97*TFLOPS, fp16=9.94*TFLOPS, int8=19.88*TFLOPS),
- "Apple M3 Max": DeviceFlops(fp32=14.20*TFLOPS, fp16=28.40*TFLOPS, int8=56.80*TFLOPS),
- "Apple M3 Ultra": DeviceFlops(fp32=54.26*TFLOPS, fp16=108.52*TFLOPS, int8=217.04*TFLOPS),
- "Apple M4": DeviceFlops(fp32=4.26*TFLOPS, fp16=8.52*TFLOPS, int8=17.04*TFLOPS),
- "Apple M4 Pro": DeviceFlops(fp32=5.72*TFLOPS, fp16=11.44*TFLOPS, int8=22.88*TFLOPS),
- "Apple M4 Max": DeviceFlops(fp32=18.03*TFLOPS, fp16=36.07*TFLOPS, int8=72.14*TFLOPS),
- ### A chips
- "Apple A13 Bionic": DeviceFlops(fp32=0.69*TFLOPS, fp16=1.38*TFLOPS, int8=2.76*TFLOPS),
- "Apple A14 Bionic": DeviceFlops(fp32=0.75*TFLOPS, fp16=1.50*TFLOPS, int8=3.00*TFLOPS),
- "Apple A15 Bionic": DeviceFlops(fp32=1.37*TFLOPS, fp16=2.74*TFLOPS, int8=5.48*TFLOPS),
- "Apple A16 Bionic": DeviceFlops(fp32=1.79*TFLOPS, fp16=3.58*TFLOPS, int8=7.16*TFLOPS),
- "Apple A17 Pro": DeviceFlops(fp32=2.15*TFLOPS, fp16=4.30*TFLOPS, int8=8.60*TFLOPS),
- ### NVIDIA GPUs
- # RTX 40 series
- "NVIDIA GEFORCE RTX 4090": DeviceFlops(fp32=82.58*TFLOPS, fp16=165.16*TFLOPS, int8=330.32*TFLOPS),
- "NVIDIA GEFORCE RTX 4080": DeviceFlops(fp32=48.74*TFLOPS, fp16=97.48*TFLOPS, int8=194.96*TFLOPS),
- "NVIDIA GEFORCE RTX 4080 SUPER": DeviceFlops(fp32=52.0*TFLOPS, fp16=104.0*TFLOPS, int8=208.0*TFLOPS),
- "NVIDIA GEFORCE RTX 4070 TI SUPER": DeviceFlops(fp32=40.0*TFLOPS, fp16=80.0*TFLOPS, int8=160.0*TFLOPS),
- "NVIDIA GEFORCE RTX 4070 TI": DeviceFlops(fp32=39.43*TFLOPS, fp16=78.86*TFLOPS, int8=157.72*TFLOPS),
- "NVIDIA GEFORCE RTX 4070 SUPER": DeviceFlops(fp32=30.0*TFLOPS, fp16=60.0*TFLOPS, int8=120.0*TFLOPS),
- "NVIDIA GEFORCE RTX 4070": DeviceFlops(fp32=29.0*TFLOPS, fp16=58.0*TFLOPS, int8=116.0*TFLOPS),
- "NVIDIA GEFORCE RTX 4060 TI 16GB": DeviceFlops(fp32=22.0*TFLOPS, fp16=44.0*TFLOPS, int8=88.0*TFLOPS),
- "NVIDIA GEFORCE RTX 4060 TI": DeviceFlops(fp32=22.0*TFLOPS, fp16=44.0*TFLOPS, int8=88.0*TFLOPS),
- # RTX 30 series
- "NVIDIA GEFORCE RTX 3050": DeviceFlops(fp32=9.11*TFLOPS, fp16=18.22*TFLOPS, int8=36.44*TFLOPS),
- "NVIDIA GEFORCE RTX 3060": DeviceFlops(fp32=13.0*TFLOPS, fp16=26.0*TFLOPS, int8=52.0*TFLOPS),
- "NVIDIA GEFORCE RTX 3060 TI": DeviceFlops(fp32=16.2*TFLOPS, fp16=32.4*TFLOPS, int8=64.8*TFLOPS),
- "NVIDIA GEFORCE RTX 3070": DeviceFlops(fp32=20.3*TFLOPS, fp16=40.6*TFLOPS, int8=81.2*TFLOPS),
- "NVIDIA GEFORCE RTX 3070 TI": DeviceFlops(fp32=21.8*TFLOPS, fp16=43.6*TFLOPS, int8=87.2*TFLOPS),
- "NVIDIA GEFORCE RTX 3080 (10 GB)": DeviceFlops(fp32=29.8*TFLOPS, fp16=59.6*TFLOPS, int8=119.2*TFLOPS),
- "NVIDIA GEFORCE RTX 3080 (12 GB)": DeviceFlops(fp32=30.6*TFLOPS, fp16=61.2*TFLOPS, int8=122.4*TFLOPS),
- "NVIDIA GEFORCE RTX 3080 TI": DeviceFlops(fp32=34.1*TFLOPS, fp16=68.2*TFLOPS, int8=136.4*TFLOPS),
- "NVIDIA GEFORCE RTX 3090": DeviceFlops(fp32=35.6*TFLOPS, fp16=71.2*TFLOPS, int8=142.4*TFLOPS),
- "NVIDIA GEFORCE RTX 3090 TI": DeviceFlops(fp32=40.0*TFLOPS, fp16=80.0*TFLOPS, int8=160.0*TFLOPS),
- # RTX 20 series
- "NVIDIA GEFORCE RTX 2060": DeviceFlops(fp32=6.45*TFLOPS, fp16=12.9*TFLOPS, int8=25.8*TFLOPS),
- "NVIDIA GEFORCE RTX 2060 SUPER": DeviceFlops(fp32=7.2*TFLOPS, fp16=14.4*TFLOPS, int8=28.8*TFLOPS),
- "NVIDIA GEFORCE RTX 2070": DeviceFlops(fp32=7.46*TFLOPS, fp16=14.93*TFLOPS, int8=29.86*TFLOPS),
- "NVIDIA GEFORCE RTX 2070 SUPER": DeviceFlops(fp32=9.06*TFLOPS, fp16=18.12*TFLOPS, int8=36.24*TFLOPS),
- "NVIDIA GEFORCE RTX 2080": DeviceFlops(fp32=10.07*TFLOPS, fp16=20.14*TFLOPS, int8=40.28*TFLOPS),
- "NVIDIA GEFORCE RTX 2080 TI": DeviceFlops(fp32=13.45*TFLOPS, fp16=26.9*TFLOPS, int8=40.28*TFLOPS),
- "NVIDIA GEFORCE RTX 2080 SUPER": DeviceFlops(fp32=11.15*TFLOPS, fp16=22.30*TFLOPS, int8=44.60*TFLOPS),
- "NVIDIA TITAN RTX": DeviceFlops(fp32=16.31*TFLOPS, fp16=32.62*TFLOPS, int8=65.24*TFLOPS),
- # GTX 10 series
- "NVIDIA GEFORCE GTX 1050 TI": DeviceFlops(fp32=2.0*TFLOPS, fp16=4.0*TFLOPS, int8=8.0*TFLOPS),
- "NVIDIA GEFORCE GTX 1070": DeviceFlops(fp32=6.463*TFLOPS, fp16=0.101*TFLOPS, int8=25.852*TFLOPS),
- "NVIDIA GEFORCE GTX 1080": DeviceFlops(fp32=8.873*TFLOPS, fp16=0.138*TFLOPS, int8=35.492*TFLOPS),
- "NVIDIA GEFORCE GTX 1080 TI": DeviceFlops(fp32=11.34*TFLOPS, fp16=0.177*TFLOPS, int8=45.36*TFLOPS),
- # GTX 16 series
- "NVIDIA GeForce GTX 1660 TI": DeviceFlops(fp32=4.8*TFLOPS, fp16=9.6*TFLOPS, int8=19.2*TFLOPS),
- # QUADRO RTX Ampere series
- "NVIDIA RTX A2000": DeviceFlops(fp32=7.99*TFLOPS, fp16=7.99*TFLOPS, int8=31.91*TFLOPS),
- "NVIDIA RTX A4000": DeviceFlops(fp32=19.17*TFLOPS, fp16=19.17*TFLOPS, int8=76.68*TFLOPS),
- "NVIDIA RTX A4500": DeviceFlops(fp32=23.65*TFLOPS, fp16=23.65*TFLOPS, int8=94.6*TFLOPS),
- "NVIDIA RTX A5000": DeviceFlops(fp32=27.8*TFLOPS, fp16=27.8*TFLOPS, int8=111.2*TFLOPS),
- "NVIDIA RTX A6000": DeviceFlops(fp32=38.71*TFLOPS, fp16=38.71*TFLOPS, int8=154.84*TFLOPS),
- # NVIDIA Ada Lovelace Architecture-Based
- "NVIDIA RTX 4000 ADA GENERATION": DeviceFlops(fp32=26.7*TFLOPS, fp16=26.7*TFLOPS, int8=258.0*TFLOPS),
- # Common Server GPUs
- "NVIDIA A40 48GB PCIE": DeviceFlops(fp32=37.4*TFLOPS, fp16=149.7*TFLOPS, int8=299.3*TFLOPS),
- "NVIDIA A100 40GB PCIE": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
- "NVIDIA A800 40GB PCIE": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
- "NVIDIA A100 80GB PCIE": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
- "NVIDIA A800 80GB PCIE": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
- "NVIDIA A100 80GB SXM": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
- "NVIDIA A800 80GB SXM": DeviceFlops(fp32=19.5*TFLOPS, fp16=312.0*TFLOPS, int8=624.0*TFLOPS),
- # ... add more devices if needed ...
- ### AMD GPUs
- # RX 6000 series
- "AMD Radeon RX 6900 XT": DeviceFlops(fp32=23.04*TFLOPS, fp16=46.08*TFLOPS, int8=92.16*TFLOPS),
- "AMD Radeon RX 6800 XT": DeviceFlops(fp32=20.74*TFLOPS, fp16=41.48*TFLOPS, int8=82.96*TFLOPS),
- "AMD Radeon RX 6800": DeviceFlops(fp32=16.17*TFLOPS, fp16=32.34*TFLOPS, int8=64.68*TFLOPS),
- "AMD Radeon RX 6700 XT": DeviceFlops(fp32=13.21*TFLOPS, fp16=26.42*TFLOPS, int8=52.84*TFLOPS),
- "AMD Radeon RX 6700": DeviceFlops(fp32=11.4*TFLOPS, fp16=22.8*TFLOPS, int8=45.6*TFLOPS),
- "AMD Radeon RX 6600 XT": DeviceFlops(fp32=10.6*TFLOPS, fp16=21.2*TFLOPS, int8=42.4*TFLOPS),
- "AMD Radeon RX 6600": DeviceFlops(fp32=8.93*TFLOPS, fp16=17.86*TFLOPS, int8=35.72*TFLOPS),
- "AMD Radeon RX 6500 XT": DeviceFlops(fp32=5.77*TFLOPS, fp16=11.54*TFLOPS, int8=23.08*TFLOPS),
- "AMD Radeon RX 6400": DeviceFlops(fp32=3.57*TFLOPS, fp16=7.14*TFLOPS, int8=14.28*TFLOPS),
- # RX 7000 series
- "AMD Radeon RX 7900 XTX": DeviceFlops(fp32=61.4*TFLOPS, fp16=122.8*TFLOPS, int8=245.6*TFLOPS),
- "AMD Radeon RX 7900 XT": DeviceFlops(fp32=53.4*TFLOPS, fp16=106.8*TFLOPS, int8=213.6*TFLOPS),
- "AMD Radeon RX 7800 XT": DeviceFlops(fp32=42.6*TFLOPS, fp16=85.2*TFLOPS, int8=170.4*TFLOPS),
- "AMD Radeon RX 7700 XT": DeviceFlops(fp32=34.2*TFLOPS, fp16=68.4*TFLOPS, int8=136.8*TFLOPS),
- "AMD Radeon RX 7600": DeviceFlops(fp32=21.5*TFLOPS, fp16=43.0*TFLOPS, int8=86.0*TFLOPS),
- "AMD Radeon RX 7500": DeviceFlops(fp32=16.2*TFLOPS, fp16=32.4*TFLOPS, int8=64.8*TFLOPS),
- ### Qualcomm embedded chips: TODO
-}
-CHIP_FLOPS.update({f"LAPTOP GPU {key}": value for key, value in CHIP_FLOPS.items()})
-CHIP_FLOPS.update({f"Laptop GPU {key}": value for key, value in CHIP_FLOPS.items()})
-CHIP_FLOPS.update({f"{key} LAPTOP GPU": value for key, value in CHIP_FLOPS.items()})
-CHIP_FLOPS.update({f"{key} Laptop GPU": value for key, value in CHIP_FLOPS.items()})
-
-
-async def device_capabilities() -> DeviceCapabilities:
- if psutil.MACOS:
- return await mac_device_capabilities()
- elif psutil.LINUX:
- return await linux_device_capabilities()
- elif psutil.WINDOWS:
- return await windows_device_capabilities()
- else:
- return DeviceCapabilities(
- model="Unknown Device",
- chip="Unknown Chip",
- memory=psutil.virtual_memory().total // 2**20,
- flops=DeviceFlops(fp32=0, fp16=0, int8=0),
- )
-
-
-async def mac_device_capabilities() -> DeviceCapabilities:
- model_id, chip_id, memory = await get_mac_system_info()
-
- return DeviceCapabilities(
- model=model_id,
- chip=chip_id,
- memory=memory,
- flops=CHIP_FLOPS.get(chip_id, DeviceFlops(fp32=0, fp16=0, int8=0))
- )
-
-
-async def linux_device_capabilities() -> DeviceCapabilities:
- import psutil
- from tinygrad import Device
-
- if DEBUG >= 2: print(f"tinygrad {Device.DEFAULT=}")
- if Device.DEFAULT == "CUDA" or Device.DEFAULT == "NV" or Device.DEFAULT == "GPU":
- import pynvml
-
- pynvml.nvmlInit()
- handle = pynvml.nvmlDeviceGetHandleByIndex(0)
- gpu_raw_name = pynvml.nvmlDeviceGetName(handle).upper()
- gpu_name = gpu_raw_name.rsplit(" ", 1)[0] if gpu_raw_name.endswith("GB") else gpu_raw_name
- gpu_memory_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
-
- if DEBUG >= 2: print(f"NVIDIA device {gpu_name=} {gpu_memory_info=}")
-
- pynvml.nvmlShutdown()
-
- return DeviceCapabilities(
- model=f"Linux Box ({gpu_name})",
- chip=gpu_name,
- memory=gpu_memory_info.total // 2**20,
- flops=CHIP_FLOPS.get(gpu_name, DeviceFlops(fp32=0, fp16=0, int8=0)),
- )
- elif Device.DEFAULT == "AMD":
- import pyamdgpuinfo
-
- gpu_raw_info = pyamdgpuinfo.get_gpu(0)
- gpu_name = gpu_raw_info.name
- gpu_memory_info = gpu_raw_info.memory_info["vram_size"]
-
- if DEBUG >= 2: print(f"AMD device {gpu_name=} {gpu_memory_info=}")
-
- return DeviceCapabilities(
- model="Linux Box (" + gpu_name + ")",
- chip=gpu_name,
- memory=gpu_memory_info // 2**20,
- flops=CHIP_FLOPS.get(gpu_name, DeviceFlops(fp32=0, fp16=0, int8=0)),
- )
-
- else:
- return DeviceCapabilities(
- model=f"Linux Box (Device: {Device.DEFAULT})",
- chip=f"Unknown Chip (Device: {Device.DEFAULT})",
- memory=psutil.virtual_memory().total // 2**20,
- flops=DeviceFlops(fp32=0, fp16=0, int8=0),
- )
-
-
-def windows_device_capabilities() -> DeviceCapabilities:
- import psutil
-
- def get_gpu_info():
- import win32com.client # install pywin32
-
- wmiObj = win32com.client.GetObject("winmgmts:\\\\.\\root\\cimv2")
- gpus = wmiObj.ExecQuery("SELECT * FROM Win32_VideoController")
-
- gpu_info = []
- for gpu in gpus:
- info = {
- "Name": gpu.Name,
- "AdapterRAM": gpu.AdapterRAM, # Bug in this property, returns -ve for VRAM > 4GB (uint32 overflow)
- "DriverVersion": gpu.DriverVersion,
- "VideoProcessor": gpu.VideoProcessor
- }
- gpu_info.append(info)
-
- return gpu_info
-
- gpus_info = get_gpu_info()
- gpu_names = [gpu['Name'] for gpu in gpus_info]
-
- contains_nvidia = any('nvidia' in gpu_name.lower() for gpu_name in gpu_names)
- contains_amd = any('amd' in gpu_name.lower() for gpu_name in gpu_names)
-
- if contains_nvidia:
- import pynvml
-
- pynvml.nvmlInit()
- handle = pynvml.nvmlDeviceGetHandleByIndex(0)
- gpu_raw_name = pynvml.nvmlDeviceGetName(handle).upper()
- gpu_name = gpu_raw_name.rsplit(" ", 1)[0] if gpu_raw_name.endswith("GB") else gpu_raw_name
- gpu_memory_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
-
- if DEBUG >= 2: print(f"NVIDIA device {gpu_name=} {gpu_memory_info=}")
-
- return DeviceCapabilities(
- model=f"Windows Box ({gpu_name})",
- chip=gpu_name,
- memory=gpu_memory_info.total // 2**20,
- flops=CHIP_FLOPS.get(gpu_name, DeviceFlops(fp32=0, fp16=0, int8=0)),
- )
- elif contains_amd:
- # For AMD GPUs, pyrsmi is the way (Official python package for rocm-smi)
- from pyrsmi import rocml
-
- rocml.smi_initialize()
- gpu_name = rocml.smi_get_device_name(0).upper()
- gpu_memory_info = rocml.smi_get_device_memory_total(0)
-
- if DEBUG >= 2: print(f"AMD device {gpu_name=} {gpu_memory_info=}")
-
- rocml.smi_shutdown()
-
- return DeviceCapabilities(
- model="Windows Box ({gpu_name})",
- chip={gpu_name},
- memory=gpu_memory_info.total // 2**20,
- flops=DeviceFlops(fp32=0, fp16=0, int8=0),
- )
- else:
- return DeviceCapabilities(
- model=f"Windows Box (Device: Unknown)",
- chip=f"Unknown Chip (Device(s): {gpu_names})",
- memory=psutil.virtual_memory().total // 2**20,
- flops=DeviceFlops(fp32=0, fp16=0, int8=0),
- )
diff --git a/exo/topology/partitioning_strategy.py b/exo/topology/partitioning_strategy.py
deleted file mode 100644
index 4924eeba..00000000
--- a/exo/topology/partitioning_strategy.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import List, Dict
-from dataclasses import dataclass
-from .topology import Topology
-from exo.inference.shard import Shard
-from exo.topology.device_capabilities import device_capabilities
-import asyncio
-
-
-# Partitions shard-space into pieces of contiguous shards, represented by floating point range [start, end) between 0 and 1
-@dataclass
-class Partition:
- node_id: str
- start: float
- end: float
-
-
-class PartitioningStrategy(ABC):
- @abstractmethod
- def partition(self, topology: Topology) -> List[Partition]:
- pass
-
-
-def map_partitions_to_shards(partitions: List[Partition], num_layers: int, model_id: str) -> List[Shard]:
- shards = []
- for i, partition in enumerate(partitions):
- start_layer = int(partition.start*num_layers)
- end_layer = int(partition.end*num_layers) - 1
-
- # Ensure the last partition covers up to num_layers - 1
- if i == len(partitions) - 1:
- end_layer = num_layers - 1
-
- # Ensure no empty shards
- if start_layer <= end_layer:
- shards.append(Shard(model_id, start_layer, end_layer, num_layers))
-
- # Ensure full coverage
- if shards and shards[-1].end_layer < num_layers - 1:
- shards[-1] = Shard(model_id, shards[-1].start_layer, num_layers - 1, num_layers)
-
- return shards
diff --git a/exo/topology/ring_memory_weighted_partitioning_strategy.py b/exo/topology/ring_memory_weighted_partitioning_strategy.py
deleted file mode 100644
index 6550aeb1..00000000
--- a/exo/topology/ring_memory_weighted_partitioning_strategy.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from typing import List
-from .partitioning_strategy import PartitioningStrategy
-from .topology import Topology
-from .partitioning_strategy import Partition
-
-
-class RingMemoryWeightedPartitioningStrategy(PartitioningStrategy):
- def partition(self, topology: Topology) -> List[Partition]:
- nodes = list(topology.all_nodes())
- nodes.sort(key=lambda x: (x[1].memory, x[0]), reverse=True)
- total_memory = sum(node[1].memory for node in nodes)
- partitions = []
- start = 0
- for node in nodes:
- end = round(start + (node[1].memory/total_memory), 5)
- partitions.append(Partition(node[0], start, end))
- start = end
- return partitions
diff --git a/exo/topology/test_device_capabilities.py b/exo/topology/test_device_capabilities.py
deleted file mode 100644
index 86eb516d..00000000
--- a/exo/topology/test_device_capabilities.py
+++ /dev/null
@@ -1,94 +0,0 @@
-import pytest
-from unittest.mock import patch
-from exo.topology.device_capabilities import mac_device_capabilities, DeviceCapabilities, DeviceFlops, TFLOPS, device_capabilities
-
-
-@pytest.mark.asyncio
-@patch("subprocess.check_output")
-async def test_mac_device_capabilities_pro(mock_check_output):
- # Mock the subprocess output
- mock_check_output.return_value = b"""
-Hardware:
-
-Hardware Overview:
-
-Model Name: MacBook Pro
-Model Identifier: Mac15,9
-Model Number: Z1CM000EFB/A
-Chip: Apple M3 Max
-Total Number of Cores: 16 (12 performance and 4 efficiency)
-Memory: 128 GB
-System Firmware Version: 10000.000.0
-OS Loader Version: 10000.000.0
-Serial Number (system): XXXXXXXXXX
-Hardware UUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
-Provisioning UDID: XXXXXXXX-XXXXXXXXXXXXXXXX
-Activation Lock Status: Enabled
-"""
-
- # Call the function
- result = await mac_device_capabilities()
-
- # Check the results
- assert isinstance(result, DeviceCapabilities)
- assert result.model == "MacBook Pro"
- assert result.chip == "Apple M3 Max"
- assert result.memory == 131072 # 128 GB in MB
- assert str(result) == "Model: MacBook Pro. Chip: Apple M3 Max. Memory: 131072MB. Flops: 14.20 TFLOPS, fp16: 28.40 TFLOPS, int8: 56.80 TFLOPS"
-
-
-@pytest.mark.asyncio
-@patch("subprocess.check_output")
-async def test_mac_device_capabilities_air(mock_check_output):
- # Mock the subprocess output
- mock_check_output.return_value = b"""
-Hardware:
-
-Hardware Overview:
-
-Model Name: MacBook Air
-Model Identifier: Mac14,2
-Model Number: MLY33B/A
-Chip: Apple M2
-Total Number of Cores: 8 (4 performance and 4 efficiency)
-Memory: 8 GB
-System Firmware Version: 10000.00.0
-OS Loader Version: 10000.00.0
-Serial Number (system): XXXXXXXXXX
-Hardware UUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
-Provisioning UDID: XXXXXXXX-XXXXXXXXXXXXXXXX
-Activation Lock Status: Disabled
-"""
-
- # Call the function
- result = await mac_device_capabilities()
-
- # Check the results
- assert isinstance(result, DeviceCapabilities)
- assert result.model == "MacBook Air"
- assert result.chip == "Apple M2"
- assert result.memory == 8192 # 8 GB in MB
-
-
-@pytest.mark.skip(reason="Unskip this test when running on a MacBook Pro, Apple M3 Max, 128GB")
-@pytest.mark.asyncio
-async def test_mac_device_capabilities_real():
- # Call the function without mocking
- result = await mac_device_capabilities()
-
- # Check the results
- assert isinstance(result, DeviceCapabilities)
- assert result.model == "MacBook Pro"
- assert result.chip == "Apple M3 Max"
- assert result.memory == 131072 # 128 GB in MB
- assert result.flops == DeviceFlops(fp32=14.20*TFLOPS, fp16=28.40*TFLOPS, int8=56.80*TFLOPS)
- assert str(result) == "Model: MacBook Pro. Chip: Apple M3 Max. Memory: 131072MB. Flops: 14.20 TFLOPS, fp16: 28.40 TFLOPS, int8: 56.80 TFLOPS"
-
-
-@pytest.mark.asyncio
-async def test_device_capabilities():
- caps = await device_capabilities()
- assert caps.model != ""
- assert caps.chip != ""
- assert caps.memory > 0
- assert caps.flops is not None
diff --git a/exo/topology/test_map_partitions.py b/exo/topology/test_map_partitions.py
deleted file mode 100644
index 5254915e..00000000
--- a/exo/topology/test_map_partitions.py
+++ /dev/null
@@ -1,81 +0,0 @@
-import unittest
-from typing import List
-from exo.topology.partitioning_strategy import Partition, map_partitions_to_shards
-from exo.inference.shard import Shard
-
-
-class TestRingMemoryWeightedPartitioningStrategy(unittest.TestCase):
- def test_map_partitions_to_shards(self):
- partitions = [
- Partition("node1", 0.0, 0.42857),
- Partition("node2", 0.42857, 0.71428),
- Partition("node3", 0.71428, 0.99999),
- ]
- shards = map_partitions_to_shards(partitions, 32, "model")
- self.assertEqual(
- shards,
- [
- Shard("model", 0, 12, 32),
- Shard("model", 13, 21, 32),
- Shard("model", 22, 31, 32),
- ],
- )
-
- partitions = [
- Partition("node1", 0.0, 0.1),
- Partition("node2", 0.1, 0.2),
- Partition("node3", 0.2, 1.0),
- ]
- shards = map_partitions_to_shards(partitions, 32, "model")
- self.assertEqual(
- shards,
- [
- Shard("model", 0, 2, 32),
- Shard("model", 3, 5, 32),
- Shard("model", 6, 31, 32),
- ],
- )
-
- partitions = [
- Partition("node1", 0.0, 1.0),
- ]
- shards = map_partitions_to_shards(partitions, 32, "model")
- self.assertEqual(
- shards,
- [
- Shard("model", 0, 31, 32),
- ],
- )
-
- partitions = []
- shards = map_partitions_to_shards(partitions, 32, "model")
- self.assertEqual(shards, [])
-
- def test_broken_map_partitions_to_shards(self):
- # this was an old broken implementation that sometimes had rounding errors!
- def _broken_map_partitions_to_shards(partitions: List[Partition], num_layers, model_id: str):
- shards = []
- for i, partition in enumerate(partitions):
- start_layer = int(partition.start*num_layers)
- end_layer = int(partition.end*num_layers) - 1
- shards.append(Shard(model_id, start_layer, end_layer, num_layers))
- return shards
-
- partitions = [
- Partition("node1", 0.0, 0.42857),
- Partition("node2", 0.42857, 0.71428),
- Partition("node3", 0.71428, 0.99999),
- ]
- shards = _broken_map_partitions_to_shards(partitions, 32, "model")
- self.assertEqual(
- shards,
- [
- Shard("model", 0, 12, 32),
- Shard("model", 13, 21, 32),
- Shard("model", 22, 30, 32),
- ],
- )
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/exo/topology/test_ring_memory_weighted_partitioning_strategy.py b/exo/topology/test_ring_memory_weighted_partitioning_strategy.py
deleted file mode 100644
index fd466f36..00000000
--- a/exo/topology/test_ring_memory_weighted_partitioning_strategy.py
+++ /dev/null
@@ -1,90 +0,0 @@
-import unittest
-from exo.topology.ring_memory_weighted_partitioning_strategy import RingMemoryWeightedPartitioningStrategy
-from exo.topology.topology import Topology
-from exo.topology.device_capabilities import DeviceCapabilities, DeviceFlops
-from exo.topology.partitioning_strategy import Partition
-
-
-class TestRingMemoryWeightedPartitioningStrategy(unittest.TestCase):
- def test_partition(self):
- # triangle
- # node1 -> node2 -> node3 -> node1
- topology = Topology()
- topology.update_node(
- "node1",
- DeviceCapabilities(model="test1", chip="test1", memory=3000, flops=DeviceFlops(fp32=0, fp16=0, int8=0)),
- )
- topology.update_node(
- "node2",
- DeviceCapabilities(model="test2", chip="test2", memory=1000, flops=DeviceFlops(fp32=0, fp16=0, int8=0)),
- )
- topology.update_node(
- "node3",
- DeviceCapabilities(model="test3", chip="test3", memory=6000, flops=DeviceFlops(fp32=0, fp16=0, int8=0)),
- )
- topology.add_edge("node1", "node2")
- topology.add_edge("node2", "node3")
- topology.add_edge("node3", "node1")
- topology.add_edge("node1", "node3")
-
- strategy = RingMemoryWeightedPartitioningStrategy()
- partitions = strategy.partition(topology)
-
- self.assertEqual(len(partitions), 3)
- self.assertEqual(
- partitions,
- [
- Partition("node3", 0.0, 0.6),
- Partition("node1", 0.6, 0.9),
- Partition("node2", 0.9, 1.0),
- ],
- )
-
- def test_partition_rounding(self):
- # triangle
- # node1 -> node2 -> node3 -> node1
- topology = Topology()
- topology.update_node(
- "node1",
- DeviceCapabilities(
- model="MacBook Pro",
- chip="test1",
- memory=128*1024*1024*1024,
- flops=DeviceFlops(fp32=0, fp16=0, int8=0),
- ),
- )
- topology.update_node(
- "node2",
- DeviceCapabilities(
- model="Mac Studio",
- chip="test2",
- memory=192*1024*1024*1024,
- flops=DeviceFlops(fp32=0, fp16=0, int8=0),
- ),
- )
- topology.update_node(
- "node3",
- DeviceCapabilities(
- model="MacBook Pro",
- chip="test3",
- memory=128*1024*1024*1024,
- flops=DeviceFlops(fp32=0, fp16=0, int8=0),
- ),
- )
-
- strategy = RingMemoryWeightedPartitioningStrategy()
- partitions = strategy.partition(topology)
-
- self.assertEqual(len(partitions), 3)
- self.assertEqual(
- partitions,
- [
- Partition("node3", 0.0, 0.42857),
- Partition("node1", 0.6, 0.9),
- Partition("node2", 0.9, 1.0),
- ],
- )
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/exo/topology/topology.py b/exo/topology/topology.py
deleted file mode 100644
index be45f9dd..00000000
--- a/exo/topology/topology.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from .device_capabilities import DeviceCapabilities
-from typing import Dict, Set, Optional
-from dataclasses import dataclass
-
-@dataclass
-class PeerConnection:
- from_id: str
- to_id: str
- description: Optional[str] = None
-
- def __hash__(self):
- # Use both from_id and to_id for uniqueness in sets
- return hash((self.from_id, self.to_id))
-
- def __eq__(self, other):
- if not isinstance(other, PeerConnection):
- return False
- # Compare both from_id and to_id for equality
- return self.from_id == other.from_id and self.to_id == other.to_id
-
-class Topology:
- def __init__(self):
- self.nodes: Dict[str, DeviceCapabilities] = {}
- self.peer_graph: Dict[str, Set[PeerConnection]] = {}
- self.active_node_id: Optional[str] = None
-
- def update_node(self, node_id: str, device_capabilities: DeviceCapabilities):
- self.nodes[node_id] = device_capabilities
-
- def get_node(self, node_id: str) -> DeviceCapabilities:
- return self.nodes.get(node_id)
-
- def all_nodes(self):
- return self.nodes.items()
-
- def add_edge(self, from_id: str, to_id: str, description: Optional[str] = None):
- if from_id not in self.peer_graph:
- self.peer_graph[from_id] = set()
- conn = PeerConnection(from_id, to_id, description)
- self.peer_graph[from_id].add(conn)
-
- def merge(self, peer_node_id: str, other: "Topology"):
- for node_id, capabilities in other.nodes.items():
- if node_id != peer_node_id: continue
- self.update_node(node_id, capabilities)
- for node_id, connections in other.peer_graph.items():
- for conn in connections:
- if conn.from_id != peer_node_id: continue
- self.add_edge(conn.from_id, conn.to_id, conn.description)
-
- def __str__(self):
- nodes_str = ", ".join(f"{node_id}: {cap}" for node_id, cap in self.nodes.items())
- edges_str = ", ".join(f"{node}: {[f'{c.to_id}({c.description})' for c in conns]}"
- for node, conns in self.peer_graph.items())
- return f"Topology(Nodes: {{{nodes_str}}}, Edges: {{{edges_str}}})"
-
- def to_json(self):
- return {
- "nodes": {
- node_id: capabilities.to_dict()
- for node_id, capabilities in self.nodes.items()
- },
- "peer_graph": {
- node_id: [
- {
- "from_id": conn.from_id,
- "to_id": conn.to_id,
- "description": conn.description
- }
- for conn in connections
- ]
- for node_id, connections in self.peer_graph.items()
- },
- "active_node_id": self.active_node_id
- }
diff --git a/exo/train/__init__.py b/exo/train/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/train/data/lora/test.jsonl b/exo/train/data/lora/test.jsonl
deleted file mode 100644
index 4f1e7709..00000000
--- a/exo/train/data/lora/test.jsonl
+++ /dev/null
@@ -1,100 +0,0 @@
-{"text": "table: 1-10015132-16\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What is terrence ross' nationality\nA: SELECT Nationality FROM 1-10015132-16 WHERE Player = 'Terrence Ross'"}
-{"text": "table: 1-10015132-16\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What clu was in toronto 1995-96\nA: SELECT School/Club Team FROM 1-10015132-16 WHERE Years in Toronto = '1995-96'"}
-{"text": "table: 1-10015132-16\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: which club was in toronto 2003-06\nA: SELECT School/Club Team FROM 1-10015132-16 WHERE Years in Toronto = '2003-06'"}
-{"text": "table: 1-10015132-16\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: how many schools or teams had jalen rose\nA: SELECT COUNT School/Club Team FROM 1-10015132-16 WHERE Player = 'Jalen Rose'"}
-{"text": "table: 1-10083598-1\ncolumns: No, Date, Round, Circuit, Pole Position, Fastest Lap, Race winner, Report\nQ: Where was Assen held?\nA: SELECT Round FROM 1-10083598-1 WHERE Circuit = 'Assen'"}
-{"text": "table: 1-10083598-1\ncolumns: No, Date, Round, Circuit, Pole Position, Fastest Lap, Race winner, Report\nQ: What was the number of race that Kevin Curtain won?\nA: SELECT COUNT No FROM 1-10083598-1 WHERE Pole Position = 'Kevin Curtain'"}
-{"text": "table: 1-10083598-1\ncolumns: No, Date, Round, Circuit, Pole Position, Fastest Lap, Race winner, Report\nQ: What was the date of the race in Misano?\nA: SELECT Date FROM 1-10083598-1 WHERE Circuit = 'Misano'"}
-{"text": "table: 1-1013129-2\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: How many different positions did Sherbrooke Faucons (qmjhl) provide in the draft?\nA: SELECT COUNT Position FROM 1-1013129-2 WHERE College/junior/club team = 'Sherbrooke Faucons (QMJHL)'"}
-{"text": "table: 1-1013129-2\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What are the nationalities of the player picked from Thunder Bay Flyers (ushl)\nA: SELECT Nationality FROM 1-1013129-2 WHERE College/junior/club team = 'Thunder Bay Flyers (USHL)'"}
-{"text": "table: 1-1013129-2\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: How many different college/junior/club teams provided a player to the Washington Capitals NHL Team?\nA: SELECT COUNT College/junior/club team FROM 1-1013129-2 WHERE NHL team = 'Washington Capitals'"}
-{"text": "table: 1-1013129-3\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: How many different nationalities do the players of New Jersey Devils come from?\nA: SELECT COUNT Nationality FROM 1-1013129-3 WHERE NHL team = 'New Jersey Devils'"}
-{"text": "table: 1-1013129-3\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What's Dorain Anneck's pick number?\nA: SELECT Pick FROM 1-1013129-3 WHERE Player = 'Dorain Anneck'"}
-{"text": "table: 1-1013129-3\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What is the nationality of the player from Vancouver Canucks?\nA: SELECT Nationality FROM 1-1013129-3 WHERE NHL team = 'Vancouver Canucks'"}
-{"text": "table: 1-1013129-3\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What's the pick number of the player from Springfield Olympics (Nejhl)?\nA: SELECT Pick FROM 1-1013129-3 WHERE College/junior/club team = 'Springfield Olympics (NEJHL)'"}
-{"text": "table: 1-1014206-2\ncolumns: #, Shipyard, Laid down, Launched, Commissioned, Fleet, Status\nQ: When were the ships launched that were laid down on september 1, 1964?\nA: SELECT Launched FROM 1-1014206-2 WHERE Laid down = 'September 1, 1964'"}
-{"text": "table: 1-1014206-2\ncolumns: #, Shipyard, Laid down, Launched, Commissioned, Fleet, Status\nQ: List the # for ships commissioned on december 18, 1965.\nA: SELECT # FROM 1-1014206-2 WHERE Commissioned = 'December 18, 1965'"}
-{"text": "table: 1-1014206-2\ncolumns: #, Shipyard, Laid down, Launched, Commissioned, Fleet, Status\nQ: List the # for ships commissioned on september 30, 1967.\nA: SELECT # FROM 1-1014206-2 WHERE Commissioned = 'September 30, 1967'"}
-{"text": "table: 1-1014206-2\ncolumns: #, Shipyard, Laid down, Launched, Commissioned, Fleet, Status\nQ: When were ships laid down that were commissioned on october 29, 1965?\nA: SELECT Laid down FROM 1-1014206-2 WHERE Commissioned = 'October 29, 1965'"}
-{"text": "table: 1-1015521-2\ncolumns: Equivalent NATO Rank Code, Rank in Spanish, Rank in English, Commonwealth equivalent, US Air Force equivalent\nQ: What could a spanish coronel be addressed as in the commonwealth military?\nA: SELECT Commonwealth equivalent FROM 1-1015521-2 WHERE Rank in Spanish = 'Coronel'"}
-{"text": "table: 1-1015521-2\ncolumns: Equivalent NATO Rank Code, Rank in Spanish, Rank in English, Commonwealth equivalent, US Air Force equivalent\nQ: Give me a list of all spanish officer titles that could receive recognition as group captain in english\nA: SELECT Rank in English FROM 1-1015521-2 WHERE Commonwealth equivalent = 'Group Captain'"}
-{"text": "table: 1-1015521-2\ncolumns: Equivalent NATO Rank Code, Rank in Spanish, Rank in English, Commonwealth equivalent, US Air Force equivalent\nQ: If you are a pilot officer in the commonwealth then what will you called as in the US air force?\nA: SELECT US Air Force equivalent FROM 1-1015521-2 WHERE Commonwealth equivalent = 'Pilot Officer'"}
-{"text": "table: 1-1015521-2\ncolumns: Equivalent NATO Rank Code, Rank in Spanish, Rank in English, Commonwealth equivalent, US Air Force equivalent\nQ: If you're a major general in the US air force then what ranking will you receive in the commonwealth's air force?\nA: SELECT Commonwealth equivalent FROM 1-1015521-2 WHERE US Air Force equivalent = 'Major General'"}
-{"text": "table: 1-1015521-2\ncolumns: Equivalent NATO Rank Code, Rank in Spanish, Rank in English, Commonwealth equivalent, US Air Force equivalent\nQ: If you get a ranking as major in the english military then what would the spanish military address you as? \nA: SELECT Rank in Spanish FROM 1-1015521-2 WHERE Rank in English = 'Major'"}
-{"text": "table: 1-10182508-5\ncolumns: Rank Each wrestlers total number of days as champion are ranked highest to lowest; wrestlers with the same number mean that they are tied for that certain rank., Wrestler, # of reigns, Combined defenses, Combined days\nQ: Which wrestlers have had 2 reigns?\nA: SELECT Wrestler FROM 1-10182508-5 WHERE # of reigns = 2"}
-{"text": "table: 1-10182508-5\ncolumns: Rank Each wrestlers total number of days as champion are ranked highest to lowest; wrestlers with the same number mean that they are tied for that certain rank., Wrestler, # of reigns, Combined defenses, Combined days\nQ: In terms of reigns, what is the lowest number listed?\nA: SELECT MIN # of reigns FROM 1-10182508-5"}
-{"text": "table: 1-10182508-5\ncolumns: Rank Each wrestlers total number of days as champion are ranked highest to lowest; wrestlers with the same number mean that they are tied for that certain rank., Wrestler, # of reigns, Combined defenses, Combined days\nQ: What rank was Bryan Danielson in this chart?\nA: SELECT Rank Each wrestlers total number of days as champion are ranked highest to lowest; wrestlers with the same number mean that they are tied for that certain rank. FROM 1-10182508-5 WHERE Wrestler = 'Bryan Danielson'"}
-{"text": "table: 1-10182508-5\ncolumns: Rank Each wrestlers total number of days as champion are ranked highest to lowest; wrestlers with the same number mean that they are tied for that certain rank., Wrestler, # of reigns, Combined defenses, Combined days\nQ: How many combined days did Go Shiozaki have?\nA: SELECT Combined days FROM 1-10182508-5 WHERE Wrestler = 'Go Shiozaki'"}
-{"text": "table: 1-10182508-5\ncolumns: Rank Each wrestlers total number of days as champion are ranked highest to lowest; wrestlers with the same number mean that they are tied for that certain rank., Wrestler, # of reigns, Combined defenses, Combined days\nQ: What was Go Shiozaki's rank?\nA: SELECT MIN Rank Each wrestlers total number of days as champion are ranked highest to lowest; wrestlers with the same number mean that they are tied for that certain rank. FROM 1-10182508-5 WHERE Wrestler = 'Go Shiozaki'"}
-{"text": "table: 1-1024710-2\ncolumns: Member, Electorate, Province, MPs term, Election date\nQ: Which province is grey and bell electorate in\nA: SELECT Province FROM 1-1024710-2 WHERE Electorate = 'Grey and Bell'"}
-{"text": "table: 1-1024710-2\ncolumns: Member, Electorate, Province, MPs term, Election date\nQ: Which province is bay of islands in\nA: SELECT Province FROM 1-1024710-2 WHERE Electorate = 'Bay of Islands'"}
-{"text": "table: 1-10294071-1\ncolumns: Player, Total W\u2013L, Singles W\u2013L, Doubles W\u2013L, Ties played, Debut, Years played\nQ: what is the total number of\u00a0total w\u2013l\u00a0where\u00a0doubles w\u2013l\u00a0is 11\u201311\nA: SELECT COUNT Total W\u2013L FROM 1-10294071-1 WHERE Doubles W\u2013L = '11\u201311'"}
-{"text": "table: 1-10294071-1\ncolumns: Player, Total W\u2013L, Singles W\u2013L, Doubles W\u2013L, Ties played, Debut, Years played\nQ: what is the total number of\u00a0singles w\u2013l\u00a0where\u00a0doubles w\u2013l\u00a0is 11\u201314\nA: SELECT COUNT Singles W\u2013L FROM 1-10294071-1 WHERE Doubles W\u2013L = '11\u201314'"}
-{"text": "table: 1-10294071-1\ncolumns: Player, Total W\u2013L, Singles W\u2013L, Doubles W\u2013L, Ties played, Debut, Years played\nQ: what's the\u00a0total w\u2013l\u00a0where\u00a0player\u00a0is boro jovanovi\u0107 category:articles with hcards\nA: SELECT Total W\u2013L FROM 1-10294071-1 WHERE Player = 'Boro Jovanovi\u0107 Category:Articles with hCards'"}
-{"text": "table: 1-10294071-1\ncolumns: Player, Total W\u2013L, Singles W\u2013L, Doubles W\u2013L, Ties played, Debut, Years played\nQ: what is the maximum\u00a0ties played\u00a0where\u00a0player\u00a0is josip palada category:articles with hcards\nA: SELECT MAX Ties played FROM 1-10294071-1 WHERE Player = 'Josip Palada Category:Articles with hCards'"}
-{"text": "table: 1-10294071-1\ncolumns: Player, Total W\u2013L, Singles W\u2013L, Doubles W\u2013L, Ties played, Debut, Years played\nQ: what is the total number of\u00a0ties played\u00a0where\u00a0total w\u2013l\u00a0is 38\u201324\nA: SELECT COUNT Ties played FROM 1-10294071-1 WHERE Total W\u2013L = '38\u201324'"}
-{"text": "table: 1-10333757-1\ncolumns: Calls, Frequency, Branding, Format, Market/Rank, Timeslot, Group owner\nQ: What is the Frequency at the Market/Rank of Burlington - Plattsburgh , Vermont - New York /143?\nA: SELECT COUNT Frequency FROM 1-10333757-1 WHERE Market/Rank = 'Burlington - Plattsburgh , Vermont - New York /143'"}
-{"text": "table: 1-10333757-1\ncolumns: Calls, Frequency, Branding, Format, Market/Rank, Timeslot, Group owner\nQ: What is the Branding for Group Owner Qantam of Cape Cod, LLC?\nA: SELECT Branding FROM 1-10333757-1 WHERE Group owner = 'Qantam of Cape Cod, LLC'"}
-{"text": "table: 1-10333757-1\ncolumns: Calls, Frequency, Branding, Format, Market/Rank, Timeslot, Group owner\nQ: What Branding does WRKO calls use?\nA: SELECT Branding FROM 1-10333757-1 WHERE Calls = 'WRKO'"}
-{"text": "table: 1-10333757-1\ncolumns: Calls, Frequency, Branding, Format, Market/Rank, Timeslot, Group owner\nQ: What is the Format for Branding of 1290 wkbk w281au 104.1?\nA: SELECT Format FROM 1-10333757-1 WHERE Branding = '1290 WKBK W281AU 104.1'"}
-{"text": "table: 1-10333757-1\ncolumns: Calls, Frequency, Branding, Format, Market/Rank, Timeslot, Group owner\nQ: Which Market/Rank is associated with WCRN calls?\nA: SELECT Market/Rank FROM 1-10333757-1 WHERE Calls = 'WCRN'"}
-{"text": "table: 1-10333757-1\ncolumns: Calls, Frequency, Branding, Format, Market/Rank, Timeslot, Group owner\nQ: Which Frequency is used for WEGP calls?\nA: SELECT Frequency FROM 1-10333757-1 WHERE Calls = 'WEGP'"}
-{"text": "table: 1-10408617-5\ncolumns: Scheme, Tariff code, BTs retail price (regulated), Approx premium, Prefixes\nQ: What is the regulated retail price for the tariff code ff0 prs?\nA: SELECT BTs retail price (regulated) FROM 1-10408617-5 WHERE Tariff code = 'ff0 PRS'"}
-{"text": "table: 1-10408617-5\ncolumns: Scheme, Tariff code, BTs retail price (regulated), Approx premium, Prefixes\nQ: What is the premium associated with tariff code g9?\nA: SELECT Approx premium FROM 1-10408617-5 WHERE Tariff code = 'g9'"}
-{"text": "table: 1-10408617-5\ncolumns: Scheme, Tariff code, BTs retail price (regulated), Approx premium, Prefixes\nQ: How many tariff codes have a bts retail price of 2p/min or inclusive?\nA: SELECT COUNT Tariff code FROM 1-10408617-5 WHERE BTs retail price (regulated) = '2p/min or inclusive'"}
-{"text": "table: 1-10408617-5\ncolumns: Scheme, Tariff code, BTs retail price (regulated), Approx premium, Prefixes\nQ: How many tariff codes have a bts retail price of 2.553p/min?\nA: SELECT COUNT Tariff code FROM 1-10408617-5 WHERE BTs retail price (regulated) = '2.553p/min'"}
-{"text": "table: 1-10408617-5\ncolumns: Scheme, Tariff code, BTs retail price (regulated), Approx premium, Prefixes\nQ: What prefixes are priced at pence per minute, fixed at all times with a premium of 3p/min?\nA: SELECT Prefixes FROM 1-10408617-5 WHERE Scheme = 'Pence per minute, fixed at all times' AND Approx premium = '3p/min'"}
-{"text": "table: 1-10408617-5\ncolumns: Scheme, Tariff code, BTs retail price (regulated), Approx premium, Prefixes\nQ: What is the bts retail price (regulated) for tariff code g10?\nA: SELECT BTs retail price (regulated) FROM 1-10408617-5 WHERE Tariff code = 'g10'"}
-{"text": "table: 1-10409754-5\ncolumns: Nominative, Old orthography, New orthography, /e/ or /\u00e6/ (IPA), Tone (Latvian notation: /~/ - level, /^/ - broken), Translation\nQ: What is the tone for gen.sing. plague?\nA: SELECT Tone (Latvian notation: /~/ - level, /^/ - broken) FROM 1-10409754-5 WHERE Translation = 'Gen.Sing. plague'"}
-{"text": "table: 1-10432351-1\ncolumns: Star (Pismis24-#), Spectral type, Magnitude (M bol ), Temperature (K), Radius (R \u2609 ), Mass (M \u2609 )\nQ: What is the smallest possible radius?\nA: SELECT MIN Radius (R \u2609 ) FROM 1-10432351-1"}
-{"text": "table: 1-10432351-1\ncolumns: Star (Pismis24-#), Spectral type, Magnitude (M bol ), Temperature (K), Radius (R \u2609 ), Mass (M \u2609 )\nQ: What are all the spectral types for star mismis24-# is 1sw?\nA: SELECT Spectral type FROM 1-10432351-1 WHERE Star (Pismis24-#) = '1SW'"}
-{"text": "table: 1-10432351-1\ncolumns: Star (Pismis24-#), Spectral type, Magnitude (M bol ), Temperature (K), Radius (R \u2609 ), Mass (M \u2609 )\nQ: If a radius is 10, what is the lowest possible mass?\nA: SELECT MIN Mass (M \u2609 ) FROM 1-10432351-1 WHERE Radius (R \u2609 ) = 10"}
-{"text": "table: 1-105344-2\ncolumns: Year, Aircraft kilometers, Departures, Flying hours, Passengers, Seat factor, Employees, Profit/loss\nQ: What percentage of seats were filled in 2006?\nA: SELECT Seat factor FROM 1-105344-2 WHERE Year = 2006"}
-{"text": "table: 1-105344-2\ncolumns: Year, Aircraft kilometers, Departures, Flying hours, Passengers, Seat factor, Employees, Profit/loss\nQ: How many hours were flown in each of the years where more than 64379058.0 kilometers were flown?\nA: SELECT Flying hours FROM 1-105344-2 WHERE Aircraft kilometers > 64379058.0"}
-{"text": "table: 1-105344-2\ncolumns: Year, Aircraft kilometers, Departures, Flying hours, Passengers, Seat factor, Employees, Profit/loss\nQ: Of the years that had exactly 17096 departures, what is the greatest number of aircraft kilometers flown?\nA: SELECT MAX Aircraft kilometers FROM 1-105344-2 WHERE Departures = 17096"}
-{"text": "table: 1-10548224-1\ncolumns: Year, Game or event, Date contested, League or governing body, Sport, Winning team, Losing team, Final score\nQ: Which winning team beat the New York Yankees?\nA: SELECT Winning team FROM 1-10548224-1 WHERE Losing team = 'New York Yankees'"}
-{"text": "table: 1-10548224-1\ncolumns: Year, Game or event, Date contested, League or governing body, Sport, Winning team, Losing team, Final score\nQ: What was the final score for the game that was contested on February 1, 2009?\nA: SELECT Final score FROM 1-10548224-1 WHERE Date contested = 'February 1, 2009'"}
-{"text": "table: 1-10548224-1\ncolumns: Year, Game or event, Date contested, League or governing body, Sport, Winning team, Losing team, Final score\nQ: What sport had a final score of 3-2?\nA: SELECT Sport FROM 1-10548224-1 WHERE Final score = '3-2'"}
-{"text": "table: 1-10548224-1\ncolumns: Year, Game or event, Date contested, League or governing body, Sport, Winning team, Losing team, Final score\nQ: Who was the winning team of the game that was contested on February 1, 2009?\nA: SELECT Winning team FROM 1-10548224-1 WHERE Date contested = 'February 1, 2009'"}
-{"text": "table: 1-10548224-1\ncolumns: Year, Game or event, Date contested, League or governing body, Sport, Winning team, Losing team, Final score\nQ: Who was the losing team of the game that was contested on February 1, 2004?\nA: SELECT Losing team FROM 1-10548224-1 WHERE Date contested = 'February 1, 2004'"}
-{"text": "table: 1-1057262-2\ncolumns: Crop (kilotonnes), New South Wales, Victoria, Queensland, Western Australia, South Australia, Tasmania, Total\nQ: what's the minimum\u00a0total\u00a0with\u00a0crop (kilotonnes)\u00a0being s lupin\nA: SELECT MIN Total FROM 1-1057262-2 WHERE Crop (kilotonnes) = 's Lupin'"}
-{"text": "table: 1-1057262-2\ncolumns: Crop (kilotonnes), New South Wales, Victoria, Queensland, Western Australia, South Australia, Tasmania, Total\nQ: what's the\u00a0new south wales\u00a0with\u00a0crop (kilotonnes)\u00a0being canola\nA: SELECT New South Wales FROM 1-1057262-2 WHERE Crop (kilotonnes) = 'Canola'"}
-{"text": "table: 1-1057262-2\ncolumns: Crop (kilotonnes), New South Wales, Victoria, Queensland, Western Australia, South Australia, Tasmania, Total\nQ: what's the total number of\u00a0south australia\u00a0with\u00a0victoria\u00a0value of 2173\nA: SELECT COUNT South Australia FROM 1-1057262-2 WHERE Victoria = 2173"}
-{"text": "table: 1-1057262-2\ncolumns: Crop (kilotonnes), New South Wales, Victoria, Queensland, Western Australia, South Australia, Tasmania, Total\nQ: what's the minimum\u00a0tasmania value\nA: SELECT MIN Tasmania FROM 1-1057262-2"}
-{"text": "table: 1-1057262-2\ncolumns: Crop (kilotonnes), New South Wales, Victoria, Queensland, Western Australia, South Australia, Tasmania, Total\nQ: what's the total number of\u00a0tasmania\u00a0with\u00a0new south wales\u00a0crop of 190 kilotonnes\nA: SELECT COUNT Tasmania FROM 1-1057262-2 WHERE New South Wales = 190"}
-{"text": "table: 1-1058787-1\ncolumns: Approximate Age, Virtues, Psycho Social Crisis, Significant Relationship, Existential Question [ not in citation given ], Examples\nQ: How many significant relationships list Will as a virtue?\nA: SELECT COUNT Significant Relationship FROM 1-1058787-1 WHERE Virtues = 'Will'"}
-{"text": "table: 1-1058787-1\ncolumns: Approximate Age, Virtues, Psycho Social Crisis, Significant Relationship, Existential Question [ not in citation given ], Examples\nQ: Which examples ask the existential question \"Can I Love?\"\nA: SELECT Examples FROM 1-1058787-1 WHERE Existential Question [ not in citation given ] = 'Can I Love?'"}
-{"text": "table: 1-1059743-2\ncolumns: Rank, Member Association, Points, Group stage, Play-off, AFC Cup\nQ: How many countries got 796.7 points?\nA: SELECT COUNT Rank FROM 1-1059743-2 WHERE Points = '796.7'"}
-{"text": "table: 1-1059743-2\ncolumns: Rank, Member Association, Points, Group stage, Play-off, AFC Cup\nQ: In what group stage were 177.2 points awarded?\nA: SELECT COUNT Group stage FROM 1-1059743-2 WHERE Points = '177.2'"}
-{"text": "table: 1-1059743-2\ncolumns: Rank, Member Association, Points, Group stage, Play-off, AFC Cup\nQ: What is the lowest group to earn 886.6 points?\nA: SELECT MIN Group stage FROM 1-1059743-2 WHERE Points = '886.6'"}
-{"text": "table: 1-1059743-2\ncolumns: Rank, Member Association, Points, Group stage, Play-off, AFC Cup\nQ: How many countries earned 177.2 points?\nA: SELECT COUNT Member Association FROM 1-1059743-2 WHERE Points = '177.2'"}
-{"text": "table: 1-10586064-2\ncolumns: County, Precincts, Lunsford, % Lunsford, McConnell, % McConnell, Total\nQ: If % lunsford is 51.82% what is the % mcconnell in Letcher?\nA: SELECT % McConnell FROM 1-10586064-2 WHERE % Lunsford = '51.82%'"}
-{"text": "table: 1-10586064-2\ncolumns: County, Precincts, Lunsford, % Lunsford, McConnell, % McConnell, Total\nQ: What country had the total 18,900 (r)?\nA: SELECT County FROM 1-10586064-2 WHERE Total = '18,900 (R)'"}
-{"text": "table: 1-10586064-2\ncolumns: County, Precincts, Lunsford, % Lunsford, McConnell, % McConnell, Total\nQ: When % mcconnell is 44.54% what are the total number of counties?\nA: SELECT COUNT County FROM 1-10586064-2 WHERE % McConnell = '44.54%'"}
-{"text": "table: 1-10586064-2\ncolumns: County, Precincts, Lunsford, % Lunsford, McConnell, % McConnell, Total\nQ: If % mcconnell is 47.17% what is the total number of mcconnell ?\nA: SELECT COUNT McConnell FROM 1-10586064-2 WHERE % McConnell = '47.17%'"}
-{"text": "table: 1-10586064-2\ncolumns: County, Precincts, Lunsford, % Lunsford, McConnell, % McConnell, Total\nQ: What is the county of precints 515?\nA: SELECT County FROM 1-10586064-2 WHERE Precincts = 515"}
-{"text": "table: 1-10601843-2\ncolumns: Stadium, Capacity, City, Country, Tenant, Opening\nQ: Which city has a capacity of 41903?\nA: SELECT City FROM 1-10601843-2 WHERE Capacity = 41903"}
-{"text": "table: 1-10601843-2\ncolumns: Stadium, Capacity, City, Country, Tenant, Opening\nQ: What is the maximum capacity of the Otkrytie Arena stadium?\nA: SELECT MAX Capacity FROM 1-10601843-2 WHERE Stadium = 'Otkrytie Arena'"}
-{"text": "table: 1-10601843-2\ncolumns: Stadium, Capacity, City, Country, Tenant, Opening\nQ: When did the stadium where Bursaspor is the tenant open?\nA: SELECT MIN Opening FROM 1-10601843-2 WHERE Tenant = 'Bursaspor'"}
-{"text": "table: 1-10601843-2\ncolumns: Stadium, Capacity, City, Country, Tenant, Opening\nQ: How many tenants are there in the city of Samsun?\nA: SELECT COUNT Tenant FROM 1-10601843-2 WHERE City = 'Samsun'"}
-{"text": "table: 1-10610087-5\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date\nQ: what's the\u00a0original air date\u00a0with\u00a0title\u00a0 \"hell\"\nA: SELECT Original air date FROM 1-10610087-5 WHERE Title = '\"Hell\"'"}
-{"text": "table: 1-10638523-1\ncolumns: Particulars and Characteristics, Shivalik Zone, Mid-Hill Zone, High hill zone, Trance- n Himalaya Zone\nQ: What is the percentage of the Shivalik Zone where the percentage of the Mid-Hill Zone is 10%?\nA: SELECT Shivalik Zone FROM 1-10638523-1 WHERE Mid-Hill Zone = '10%'"}
-{"text": "table: 1-10638523-1\ncolumns: Particulars and Characteristics, Shivalik Zone, Mid-Hill Zone, High hill zone, Trance- n Himalaya Zone\nQ: For mid-hill zone what is the altitude?\nA: SELECT Mid-Hill Zone FROM 1-10638523-1 WHERE Particulars and Characteristics = 'Altitude'"}
-{"text": "table: 1-10638523-1\ncolumns: Particulars and Characteristics, Shivalik Zone, Mid-Hill Zone, High hill zone, Trance- n Himalaya Zone\nQ: What are the climatic conditions for the trance- n himalaya zone?\nA: SELECT Trance- n Himalaya Zone FROM 1-10638523-1 WHERE Particulars and Characteristics = 'Climatic conditions'"}
-{"text": "table: 1-10638523-1\ncolumns: Particulars and Characteristics, Shivalik Zone, Mid-Hill Zone, High hill zone, Trance- n Himalaya Zone\nQ: What is the percentage of the trance- n himalaya zone that corresponds with the high hill zone is 25%?\nA: SELECT Trance- n Himalaya Zone FROM 1-10638523-1 WHERE High hill zone = '25%'"}
-{"text": "table: 1-10644188-3\ncolumns: Total tenure rank, Uninterrupted rank, Name, State represented, Dates of service, Total tenure time, Uninterrupted time\nQ: What is the state of Ted Stevens?\nA: SELECT State represented FROM 1-10644188-3 WHERE Name = 'Ted Stevens'"}
-{"text": "table: 1-10682862-68\ncolumns: Country, Players, Standard, Minor, First title, Last title\nQ: What's the standard of the country who won its first title in 1992?\nA: SELECT MAX Standard FROM 1-10682862-68 WHERE First title = 1992"}
-{"text": "table: 1-10682862-68\ncolumns: Country, Players, Standard, Minor, First title, Last title\nQ: What's the smallest number of players?\nA: SELECT MIN Players FROM 1-10682862-68"}
-{"text": "table: 1-10682862-68\ncolumns: Country, Players, Standard, Minor, First title, Last title\nQ: In what year was the last last title received, by any of the countries?\nA: SELECT MAX Last title FROM 1-10682862-68"}
-{"text": "table: 1-10710364-1\ncolumns: Religious group, Population % 1961, Population % 1971, Population % 1981, Population % 1991, Population % 2001\nQ: What religious groups made up 0.72% of the Indian population in 2001?\nA: SELECT Religious group FROM 1-10710364-1 WHERE Population % 2001 = '0.72%'"}
-{"text": "table: 1-10718868-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: What is the original air date for episode 15 of season 6?\nA: SELECT Original air date FROM 1-10718868-2 WHERE No. in season = 15"}
-{"text": "table: 1-10718868-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many episodes in season 6 titles \"Poppin' Tags\"?\nA: SELECT COUNT No. in season FROM 1-10718868-2 WHERE Title = '\"Poppin' Tags\"'"}
-{"text": "table: 1-10753917-1\ncolumns: Season, Driver, Team, Engine, Poles, Wins, Podiums, Points, Margin of defeat\nQ: Which podiums did the Williams team have with a margin of defeat of 2?\nA: SELECT Podiums FROM 1-10753917-1 WHERE Team = 'Williams' AND Margin of defeat = '2'"}
-{"text": "table: 1-10753917-1\ncolumns: Season, Driver, Team, Engine, Poles, Wins, Podiums, Points, Margin of defeat\nQ: How many drivers on the williams team had a margin of defeat of 2?\nA: SELECT COUNT Driver FROM 1-10753917-1 WHERE Team = 'Williams' AND Margin of defeat = '2'"}
-{"text": "table: 1-10753917-1\ncolumns: Season, Driver, Team, Engine, Poles, Wins, Podiums, Points, Margin of defeat\nQ: How many seasons was clay regazzoni the driver?\nA: SELECT COUNT Season FROM 1-10753917-1 WHERE Driver = 'Clay Regazzoni'"}
-{"text": "table: 1-10753917-1\ncolumns: Season, Driver, Team, Engine, Poles, Wins, Podiums, Points, Margin of defeat\nQ: Which margin of defeats had points of 30?\nA: SELECT Margin of defeat FROM 1-10753917-1 WHERE Points = '30'"}
-{"text": "table: 1-10753917-1\ncolumns: Season, Driver, Team, Engine, Poles, Wins, Podiums, Points, Margin of defeat\nQ: Which podiums did the alfa romeo team have?\nA: SELECT Podiums FROM 1-10753917-1 WHERE Team = 'Alfa Romeo'"}
-{"text": "table: 1-10797636-1\ncolumns: Village (German), Village (Slovene), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: What was the percent of slovenes 1951 for bach?\nA: SELECT Percent of Slovenes 1951 FROM 1-10797636-1 WHERE Village (German) = 'Bach'"}
-{"text": "table: 1-10812403-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What college's team is the Saskatchewan Roughriders?\nA: SELECT College FROM 1-10812403-4 WHERE CFL Team = 'Saskatchewan Roughriders'"}
-{"text": "table: 1-10812403-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What position did Calvin Mccarty play?\nA: SELECT Position FROM 1-10812403-4 WHERE Player = 'Calvin McCarty'"}
-{"text": "table: 1-10812403-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: How many people were pick #30?\nA: SELECT COUNT Position FROM 1-10812403-4 WHERE Pick # = 30"}
diff --git a/exo/train/data/lora/train.jsonl b/exo/train/data/lora/train.jsonl
deleted file mode 100644
index 949261fd..00000000
--- a/exo/train/data/lora/train.jsonl
+++ /dev/null
@@ -1,1000 +0,0 @@
-{"text": "table: 1-1000181-1\ncolumns: State/territory, Text/background colour, Format, Current slogan, Current series, Notes\nQ: Tell me what the notes are for South Australia \nA: SELECT Notes FROM 1-1000181-1 WHERE Current slogan = 'SOUTH AUSTRALIA'"}
-{"text": "table: 1-1000181-1\ncolumns: State/territory, Text/background colour, Format, Current slogan, Current series, Notes\nQ: What is the current series where the new series began in June 2011?\nA: SELECT Current series FROM 1-1000181-1 WHERE Notes = 'New series began in June 2011'"}
-{"text": "table: 1-1000181-1\ncolumns: State/territory, Text/background colour, Format, Current slogan, Current series, Notes\nQ: What is the format for South Australia?\nA: SELECT Format FROM 1-1000181-1 WHERE State/territory = 'South Australia'"}
-{"text": "table: 1-1000181-1\ncolumns: State/territory, Text/background colour, Format, Current slogan, Current series, Notes\nQ: Name the background colour for the Australian Capital Territory\nA: SELECT Text/background colour FROM 1-1000181-1 WHERE State/territory = 'Australian Capital Territory'"}
-{"text": "table: 1-10007452-3\ncolumns: Order Year, Manufacturer, Model, Fleet Series (Quantity), Powertrain (Engine/Transmission), Fuel Propulsion\nQ: how many times is the fuel propulsion is cng?\nA: SELECT COUNT Fleet Series (Quantity) FROM 1-10007452-3 WHERE Fuel Propulsion = 'CNG'"}
-{"text": "table: 1-10007452-3\ncolumns: Order Year, Manufacturer, Model, Fleet Series (Quantity), Powertrain (Engine/Transmission), Fuel Propulsion\nQ: what is the fuel propulsion where the fleet series (quantity) is 310-329 (20)?\nA: SELECT Fuel Propulsion FROM 1-10007452-3 WHERE Fleet Series (Quantity) = '310-329 (20)'"}
-{"text": "table: 1-10007452-3\ncolumns: Order Year, Manufacturer, Model, Fleet Series (Quantity), Powertrain (Engine/Transmission), Fuel Propulsion\nQ: who is the manufacturer for the order year 1998?\nA: SELECT Manufacturer FROM 1-10007452-3 WHERE Order Year = '1998'"}
-{"text": "table: 1-10007452-3\ncolumns: Order Year, Manufacturer, Model, Fleet Series (Quantity), Powertrain (Engine/Transmission), Fuel Propulsion\nQ: how many times is the model ge40lfr?\nA: SELECT COUNT Manufacturer FROM 1-10007452-3 WHERE Model = 'GE40LFR'"}
-{"text": "table: 1-10007452-3\ncolumns: Order Year, Manufacturer, Model, Fleet Series (Quantity), Powertrain (Engine/Transmission), Fuel Propulsion\nQ: how many times is the fleet series (quantity) is 468-473 (6)?\nA: SELECT COUNT Order Year FROM 1-10007452-3 WHERE Fleet Series (Quantity) = '468-473 (6)'"}
-{"text": "table: 1-10007452-3\ncolumns: Order Year, Manufacturer, Model, Fleet Series (Quantity), Powertrain (Engine/Transmission), Fuel Propulsion\nQ: what is the powertrain (engine/transmission) when the order year is 2000?\nA: SELECT Powertrain (Engine/Transmission) FROM 1-10007452-3 WHERE Order Year = '2000'"}
-{"text": "table: 1-10006830-1\ncolumns: Aircraft, Description, Max Gross Weight, Total disk area, Max disk Loading\nQ: What if the description of a ch-47d chinook?\nA: SELECT Description FROM 1-10006830-1 WHERE Aircraft = 'CH-47D Chinook'"}
-{"text": "table: 1-10006830-1\ncolumns: Aircraft, Description, Max Gross Weight, Total disk area, Max disk Loading\nQ: What is the max gross weight of the Robinson R-22?\nA: SELECT Max Gross Weight FROM 1-10006830-1 WHERE Aircraft = 'Robinson R-22'"}
-{"text": "table: 1-10015132-1\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What school did player number 6 come from?\nA: SELECT School/Club Team FROM 1-10015132-1 WHERE No. = '6'"}
-{"text": "table: 1-10015132-1\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What school did the player that has been in Toronto from 2012-present come from?\nA: SELECT School/Club Team FROM 1-10015132-1 WHERE Years in Toronto = '2012-present'"}
-{"text": "table: 1-10015132-1\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What school did the player that has been in Toronto from 2010-2012 go to?\nA: SELECT School/Club Team FROM 1-10015132-1 WHERE Years in Toronto = '2010-2012'"}
-{"text": "table: 1-10015132-1\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What position did the player from Baylor play?\nA: SELECT Position FROM 1-10015132-1 WHERE School/Club Team = 'Baylor'"}
-{"text": "table: 1-10015132-14\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: Who played in the Toronto Raptors from 1995-96?\nA: SELECT Player FROM 1-10015132-14 WHERE Years in Toronto = '1995-96'"}
-{"text": "table: 1-10015132-14\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: Which number was Patrick O'Bryant?\nA: SELECT No. FROM 1-10015132-14 WHERE Player = 'Patrick O'Bryant'"}
-{"text": "table: 1-10015132-14\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What school did Patrick O'Bryant play for?\nA: SELECT School/Club Team FROM 1-10015132-14 WHERE Player = 'Patrick O'Bryant'"}
-{"text": "table: 1-10015132-14\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: How many number does Fordham school have?\nA: SELECT COUNT No. FROM 1-10015132-14 WHERE School/Club Team = 'Fordham'"}
-{"text": "table: 1-10015132-14\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: Which school was in Toronto in 2001-02?\nA: SELECT School/Club Team FROM 1-10015132-14 WHERE Years in Toronto = '2001-02'"}
-{"text": "table: 1-10015132-21\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: Which school did the player that played 2004-05 attend?\nA: SELECT School/Club Team FROM 1-10015132-21 WHERE Years in Toronto = '2004-05'"}
-{"text": "table: 1-10015132-21\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: Which position does Loren Woods play?\nA: SELECT Position FROM 1-10015132-21 WHERE Player = 'Loren Woods'"}
-{"text": "table: 1-10015132-21\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What number is the player that played 1998-2001\nA: SELECT MIN No. FROM 1-10015132-21 WHERE Years in Toronto = '1998-2001'"}
-{"text": "table: 1-10015132-21\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: Which country is the player that went to Georgetown from?\nA: SELECT Nationality FROM 1-10015132-21 WHERE School/Club Team = 'Georgetown'"}
-{"text": "table: 1-10015132-21\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: Which school did Herb Williams go to?\nA: SELECT School/Club Team FROM 1-10015132-21 WHERE Player = 'Herb Williams'"}
-{"text": "table: 1-10015132-3\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: When did the player from Hawaii play for Toronto?\nA: SELECT Years in Toronto FROM 1-10015132-3 WHERE School/Club Team = 'Hawaii'"}
-{"text": "table: 1-10015132-3\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: During what period did Dell Curry play for Toronto?\nA: SELECT Years in Toronto FROM 1-10015132-3 WHERE Player = 'Dell Curry'"}
-{"text": "table: 1-10015132-3\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What's the number of the player from Boise State?\nA: SELECT No. FROM 1-10015132-3 WHERE School/Club Team = 'Boise State'"}
-{"text": "table: 1-10015132-3\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What's Dell Curry nationality?\nA: SELECT Nationality FROM 1-10015132-3 WHERE Player = 'Dell Curry'"}
-{"text": "table: 1-10015132-7\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: which player is from georgia\nA: SELECT Player FROM 1-10015132-7 WHERE School/Club Team = 'Georgia'"}
-{"text": "table: 1-10015132-7\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: what school is rudy gay from\nA: SELECT School/Club Team FROM 1-10015132-7 WHERE Player = 'Rudy Gay'"}
-{"text": "table: 1-10015132-7\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: what nationality is the player who played from 1997-98\nA: SELECT Nationality FROM 1-10015132-7 WHERE Years in Toronto = '1997-98'"}
-{"text": "table: 1-10015132-7\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: what position did the player from connecticut play\nA: SELECT Position FROM 1-10015132-7 WHERE School/Club Team = 'Connecticut'"}
-{"text": "table: 1-10015132-2\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: During which years was Marcus Banks in Toronto?\nA: SELECT Years in Toronto FROM 1-10015132-2 WHERE Player = 'Marcus Banks'"}
-{"text": "table: 1-10015132-2\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: Which positions were in Toronto in 2004?\nA: SELECT Position FROM 1-10015132-2 WHERE Years in Toronto = '2004'"}
-{"text": "table: 1-10015132-2\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What nationality is the player Muggsy Bogues?\nA: SELECT Nationality FROM 1-10015132-2 WHERE Player = 'Muggsy Bogues'"}
-{"text": "table: 1-10015132-2\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What years was the player Lonny Baxter in Toronto?\nA: SELECT Years in Toronto FROM 1-10015132-2 WHERE Player = 'Lonny Baxter'"}
-{"text": "table: 1-10015132-2\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: How many players were with the school or club team La Salle?\nA: SELECT COUNT Player FROM 1-10015132-2 WHERE School/Club Team = 'La Salle'"}
-{"text": "table: 1-10021158-3\ncolumns: Year, Tournaments played, Cuts made*, Wins, 2nd, Top 10s, Best finish, Earnings ($), Money list rank, Scoring average, Scoring rank\nQ: When the scoring rank was 117, what was the best finish?\nA: SELECT Best finish FROM 1-10021158-3 WHERE Scoring rank = '117'"}
-{"text": "table: 1-10021158-3\ncolumns: Year, Tournaments played, Cuts made*, Wins, 2nd, Top 10s, Best finish, Earnings ($), Money list rank, Scoring average, Scoring rank\nQ: When the best finish was T69, how many people came in 2nd?\nA: SELECT 2nd FROM 1-10021158-3 WHERE Best finish = 'T69'"}
-{"text": "table: 1-10021158-3\ncolumns: Year, Tournaments played, Cuts made*, Wins, 2nd, Top 10s, Best finish, Earnings ($), Money list rank, Scoring average, Scoring rank\nQ: How many wins were there when the money list rank was 183?\nA: SELECT COUNT Wins FROM 1-10021158-3 WHERE Money list rank = '183'"}
-{"text": "table: 1-10021158-3\ncolumns: Year, Tournaments played, Cuts made*, Wins, 2nd, Top 10s, Best finish, Earnings ($), Money list rank, Scoring average, Scoring rank\nQ: When the money list rank was n/a, what was the scoring average?\nA: SELECT Scoring average FROM 1-10021158-3 WHERE Money list rank = 'n/a'"}
-{"text": "table: 1-10021158-3\ncolumns: Year, Tournaments played, Cuts made*, Wins, 2nd, Top 10s, Best finish, Earnings ($), Money list rank, Scoring average, Scoring rank\nQ: What time was the highest for 2nd finishers?\nA: SELECT MAX 2nd FROM 1-10021158-3"}
-{"text": "table: 1-1004033-1\ncolumns: Season, Player, Position, Nationality, Team, Draft Pick #, Draft Class, College\nQ: When did the Metrostars have their first Rookie of the Year winner?\nA: SELECT MIN Season FROM 1-1004033-1 WHERE Team = 'MetroStars'"}
-{"text": "table: 1-1004033-1\ncolumns: Season, Player, Position, Nationality, Team, Draft Pick #, Draft Class, College\nQ: What college did the Rookie of the Year from the Columbus Crew attend?\nA: SELECT College FROM 1-1004033-1 WHERE Team = 'Columbus Crew'"}
-{"text": "table: 1-1004033-1\ncolumns: Season, Player, Position, Nationality, Team, Draft Pick #, Draft Class, College\nQ: How many teams had a #1 draft pick that won the Rookie of the Year Award?\nA: SELECT COUNT Team FROM 1-1004033-1 WHERE Draft Pick # = '1'"}
-{"text": "table: 1-1004033-1\ncolumns: Season, Player, Position, Nationality, Team, Draft Pick #, Draft Class, College\nQ: What position did the #10 draft pick play?\nA: SELECT Position FROM 1-1004033-1 WHERE Draft Pick # = '10'"}
-{"text": "table: 1-10023387-1\ncolumns: Player, Years Played, Total W-L, Singles W-L, Doubles W-L\nQ: what's the\u00a0years played\u00a0with\u00a0singles w-l\u00a0of 3\u20132\nA: SELECT Years Played FROM 1-10023387-1 WHERE Singles W-L = '3\u20132'"}
-{"text": "table: 1-10023387-1\ncolumns: Player, Years Played, Total W-L, Singles W-L, Doubles W-L\nQ: what's the\u00a0doubles w-l\u00a0for player\u00a0seol jae-min (none)\nA: SELECT Doubles W-L FROM 1-10023387-1 WHERE Player = 'Seol Jae-Min (none)'"}
-{"text": "table: 1-10023387-1\ncolumns: Player, Years Played, Total W-L, Singles W-L, Doubles W-L\nQ: what's the\u00a0singles w-l\u00a0for kim doo-hwan\nA: SELECT Singles W-L FROM 1-10023387-1 WHERE Player = 'Kim Doo-Hwan'"}
-{"text": "table: 1-10023387-1\ncolumns: Player, Years Played, Total W-L, Singles W-L, Doubles W-L\nQ: what's the total number of\u00a0singles w-l\u00a0with\u00a0doubles w-l\u00a0of 0\u20130 and\u00a0total w-l\u00a0of 3\u20131\nA: SELECT COUNT Singles W-L FROM 1-10023387-1 WHERE Doubles W-L = '0\u20130' AND Total W-L = '3\u20131'"}
-{"text": "table: 1-10023387-1\ncolumns: Player, Years Played, Total W-L, Singles W-L, Doubles W-L\nQ: what's the\u00a0doubles w-l\u00a0with\u00a0years played\u00a0value of 1 (1968)\nA: SELECT Doubles W-L FROM 1-10023387-1 WHERE Years Played = '1 (1968)'"}
-{"text": "table: 1-10023387-1\ncolumns: Player, Years Played, Total W-L, Singles W-L, Doubles W-L\nQ: what\u00a0years are played\u00a0for player\u00a0 im chung-yang\nA: SELECT Years Played FROM 1-10023387-1 WHERE Player = 'Im Chung-Yang'"}
-{"text": "table: 1-10020178-1\ncolumns: Name, Canton, Height (meters), Crest length (meters), Type, Year of construction, Name of the Lake\nQ: What is the name of the 375 crest length?\nA: SELECT Name FROM 1-10020178-1 WHERE Crest length (meters) = 375"}
-{"text": "table: 1-10020178-1\ncolumns: Name, Canton, Height (meters), Crest length (meters), Type, Year of construction, Name of the Lake\nQ: What is year of construction of spitallamm?\nA: SELECT MIN Year of construction FROM 1-10020178-1 WHERE Name = 'Spitallamm'"}
-{"text": "table: 1-10020178-1\ncolumns: Name, Canton, Height (meters), Crest length (meters), Type, Year of construction, Name of the Lake\nQ: What is the canton of grande dixence?\nA: SELECT Canton FROM 1-10020178-1 WHERE Name = 'Grande Dixence'"}
-{"text": "table: 1-10020178-1\ncolumns: Name, Canton, Height (meters), Crest length (meters), Type, Year of construction, Name of the Lake\nQ: What is the name where lago di luzzone is?\nA: SELECT Name FROM 1-10020178-1 WHERE Name of the Lake = 'Lago di Luzzone'"}
-{"text": "table: 1-100518-1\ncolumns: Name, Direction, Mantra, Weapon, Consort, Graha (Planet), Guardian M\u0101t\u1e5bk\u0101\nQ: What is the guardian m\u0101t\u1e5bk\u0101 for the guardian whose consort is Sv\u0101h\u0101?\nA: SELECT Guardian M\u0101t\u1e5bk\u0101 FROM 1-100518-1 WHERE Consort = 'Sv\u0101h\u0101'"}
-{"text": "table: 1-100518-1\ncolumns: Name, Direction, Mantra, Weapon, Consort, Graha (Planet), Guardian M\u0101t\u1e5bk\u0101\nQ: Where the mantra is \"o\u1e43 ya\u1e43 v\u0101yuve nama\u1e25\", what is the direction of the guardian?\nA: SELECT Direction FROM 1-100518-1 WHERE Mantra = 'O\u1e43 Ya\u1e43 V\u0101yuve Nama\u1e25'"}
-{"text": "table: 1-100518-1\ncolumns: Name, Direction, Mantra, Weapon, Consort, Graha (Planet), Guardian M\u0101t\u1e5bk\u0101\nQ: What weapon is used by the guardian whose consort is \u015bac\u012b?\nA: SELECT Weapon FROM 1-100518-1 WHERE Consort = '\u015aac\u012b'"}
-{"text": "table: 1-100518-1\ncolumns: Name, Direction, Mantra, Weapon, Consort, Graha (Planet), Guardian M\u0101t\u1e5bk\u0101\nQ: What are the directions for the guardian whose weapon is kha\u1e0dga (sword)?\nA: SELECT Direction FROM 1-100518-1 WHERE Weapon = 'Kha\u1e0dga (sword)'"}
-{"text": "table: 1-100518-1\ncolumns: Name, Direction, Mantra, Weapon, Consort, Graha (Planet), Guardian M\u0101t\u1e5bk\u0101\nQ: What are the weapons used by guardians for the direction East?\nA: SELECT Weapon FROM 1-100518-1 WHERE Direction = 'East'"}
-{"text": "table: 1-100518-1\ncolumns: Name, Direction, Mantra, Weapon, Consort, Graha (Planet), Guardian M\u0101t\u1e5bk\u0101\nQ: What are the directions for the guardian whose graha (planet) is b\u1e5bhaspati (Jupiter)?\nA: SELECT Direction FROM 1-100518-1 WHERE Graha (Planet) = 'B\u1e5bhaspati (Jupiter)'"}
-{"text": "table: 1-10054296-1\ncolumns: Member, Headquarters, Classification, Chapters, Founded, UCCFS\nQ: What is the number of chapters listed for the fraternity with a headquarters in Austin, Texas?\nA: SELECT MAX Chapters FROM 1-10054296-1 WHERE Classification = 'Fraternity' AND Headquarters = 'Austin, Texas'"}
-{"text": "table: 1-10054296-1\ncolumns: Member, Headquarters, Classification, Chapters, Founded, UCCFS\nQ: What are the members listed with the sorority classification\nA: SELECT Member FROM 1-10054296-1 WHERE Classification = 'Sorority'"}
-{"text": "table: 1-10054296-1\ncolumns: Member, Headquarters, Classification, Chapters, Founded, UCCFS\nQ: Name the member that has 12 chapters\nA: SELECT Member FROM 1-10054296-1 WHERE Chapters = 12"}
-{"text": "table: 1-10054296-1\ncolumns: Member, Headquarters, Classification, Chapters, Founded, UCCFS\nQ: Where is the headquarters of Alpha Nu Omega\nA: SELECT Headquarters FROM 1-10054296-1 WHERE Member = 'Alpha Nu Omega'"}
-{"text": "table: 1-1007688-1\ncolumns: Year, Typhus, Typhoid fever, Relapsing fever, Smallpox, Malaria\nQ: what is the number of relapsing fever when malaria is 3000\nA: SELECT MIN Relapsing fever FROM 1-1007688-1 WHERE Malaria = '3000'"}
-{"text": "table: 1-1007688-1\ncolumns: Year, Typhus, Typhoid fever, Relapsing fever, Smallpox, Malaria\nQ: what is the typhoid fever number for the year 1934\nA: SELECT Typhoid fever FROM 1-1007688-1 WHERE Year = '1934'"}
-{"text": "table: 1-1007688-1\ncolumns: Year, Typhus, Typhoid fever, Relapsing fever, Smallpox, Malaria\nQ: What are all the typhus number when smallpox is 4\nA: SELECT Typhus FROM 1-1007688-1 WHERE Smallpox = 4"}
-{"text": "table: 1-1007688-1\ncolumns: Year, Typhus, Typhoid fever, Relapsing fever, Smallpox, Malaria\nQ: what is the number of smallpox when typhoid fever is 293\nA: SELECT MAX Smallpox FROM 1-1007688-1 WHERE Typhoid fever = 293"}
-{"text": "table: 1-1007688-1\ncolumns: Year, Typhus, Typhoid fever, Relapsing fever, Smallpox, Malaria\nQ: what is the typhoid fever number for the year 1929\nA: SELECT Typhoid fever FROM 1-1007688-1 WHERE Year = '1929'"}
-{"text": "table: 1-10082596-1\ncolumns: School, Location, Founded, Affiliation, Enrollment, Team Nickname, Primary conference\nQ: How many schools are in Bloomington, IN?\nA: SELECT COUNT Founded FROM 1-10082596-1 WHERE Location = 'Bloomington, IN'"}
-{"text": "table: 1-10082596-1\ncolumns: School, Location, Founded, Affiliation, Enrollment, Team Nickname, Primary conference\nQ: How many of the schools are designated private/Presbyterian?\nA: SELECT COUNT Location FROM 1-10082596-1 WHERE Affiliation = 'Private/Presbyterian'"}
-{"text": "table: 1-10082596-1\ncolumns: School, Location, Founded, Affiliation, Enrollment, Team Nickname, Primary conference\nQ: In what year was Lindenwood University founded?\nA: SELECT MIN Founded FROM 1-10082596-1 WHERE School = 'Lindenwood University'"}
-{"text": "table: 1-10082596-1\ncolumns: School, Location, Founded, Affiliation, Enrollment, Team Nickname, Primary conference\nQ: How many of the schools listed are in Ames, IA?\nA: SELECT COUNT Primary conference FROM 1-10082596-1 WHERE Location = 'Ames, IA'"}
-{"text": "table: 1-1008653-9\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: What is the capital (endonym) where Douglas is the Capital (exonym)?\nA: SELECT Capital ( endonym ) FROM 1-1008653-9 WHERE Capital ( exonym ) = 'Douglas'"}
-{"text": "table: 1-1008653-9\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: How many countries (endonym) has the capital (endonym) of Jakarta?\nA: SELECT COUNT Country ( endonym ) FROM 1-1008653-9 WHERE Capital ( endonym ) = 'Jakarta'"}
-{"text": "table: 1-1008653-9\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: What is the country (exonym) where the official or native language(s) (alphabet/script) is Icelandic?\nA: SELECT Country ( exonym ) FROM 1-1008653-9 WHERE Official or native language(s) (alphabet/script) = 'Icelandic'"}
-{"text": "table: 1-1008653-9\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: In which country (endonym) is Irish English the official or native language(s) (alphabet/script)?\nA: SELECT Country ( endonym ) FROM 1-1008653-9 WHERE Official or native language(s) (alphabet/script) = 'Irish English'"}
-{"text": "table: 1-1008653-9\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: Which country (exonym) is the country (endonym) isle of man ellan vannin?\nA: SELECT Country ( exonym ) FROM 1-1008653-9 WHERE Country ( endonym ) = 'Isle of Man Ellan Vannin'"}
-{"text": "table: 1-1009087-1\ncolumns: Season, Network, Season premiere, Season finale, TV season, Ranking, Viewers (in millions)\nQ: The season premiere aired on September 11, 2000 aired on how many networks? \nA: SELECT COUNT Network FROM 1-1009087-1 WHERE Season premiere = 'September 11, 2000'"}
-{"text": "table: 1-1009087-1\ncolumns: Season, Network, Season premiere, Season finale, TV season, Ranking, Viewers (in millions)\nQ: What was the ranking of the season finale aired on May 8, 2006? \nA: SELECT Ranking FROM 1-1009087-1 WHERE Season finale = 'May 8, 2006'"}
-{"text": "table: 1-1011906-1\ncolumns: Regional County Municipality (RCM), Population Canada 2011 Census, Land Area, Density (pop. per km2), Seat of RCM\nQ: what is the minimum\u00a0population canada 2011 census\u00a0with\u00a0seat of rcm\u00a0being cowansville\nA: SELECT MIN Population Canada 2011 Census FROM 1-1011906-1 WHERE Seat of RCM = 'Cowansville'"}
-{"text": "table: 1-1011906-1\ncolumns: Regional County Municipality (RCM), Population Canada 2011 Census, Land Area, Density (pop. per km2), Seat of RCM\nQ: what's the\u00a0land area\u00a0with\u00a0seat of rcm\u00a0being granby\nA: SELECT Land Area FROM 1-1011906-1 WHERE Seat of RCM = 'Granby'"}
-{"text": "table: 1-101196-1\ncolumns: County, English name, Irish name, Population, Irish speakers\nQ: What is the population for County Mayo with the English Name Carrowteige?\nA: SELECT Population FROM 1-101196-1 WHERE County = 'County Mayo' AND English name = 'Carrowteige'"}
-{"text": "table: 1-101196-1\ncolumns: County, English name, Irish name, Population, Irish speakers\nQ: What is the Irish name listed with 62% Irish speakers?\nA: SELECT Irish name FROM 1-101196-1 WHERE Irish speakers = '62%'"}
-{"text": "table: 1-101196-1\ncolumns: County, English name, Irish name, Population, Irish speakers\nQ: What is the population for the Irish Name Leitir meall\u00e1in?\nA: SELECT Population FROM 1-101196-1 WHERE Irish name = 'Leitir Meall\u00e1in'"}
-{"text": "table: 1-101196-1\ncolumns: County, English name, Irish name, Population, Irish speakers\nQ: What is the county for the Irish name Carna?\nA: SELECT County FROM 1-101196-1 WHERE Irish name = 'Carna'"}
-{"text": "table: 1-101196-1\ncolumns: County, English name, Irish name, Population, Irish speakers\nQ: How many County Kerry have 53% Irish speakers?\nA: SELECT COUNT English name FROM 1-101196-1 WHERE Irish speakers = '53%' AND County = 'County Kerry'"}
-{"text": "table: 1-101196-1\ncolumns: County, English name, Irish name, Population, Irish speakers\nQ: What is the population for the English name Spiddal?\nA: SELECT Population FROM 1-101196-1 WHERE English name = 'Spiddal'"}
-{"text": "table: 1-10118412-6\ncolumns: State/Territory, Asian American Population (2010 Census), Chinese, Filipino, Indian, Japanese, Korean, Vietnamese, Other Asian\nQ: What is the the Chinese population for the state that has a Filipino population of 1474707?\nA: SELECT MIN Chinese FROM 1-10118412-6 WHERE Filipino = 1474707"}
-{"text": "table: 1-10118412-6\ncolumns: State/Territory, Asian American Population (2010 Census), Chinese, Filipino, Indian, Japanese, Korean, Vietnamese, Other Asian\nQ: How many States have an Indian population of 30947?\nA: SELECT COUNT Filipino FROM 1-10118412-6 WHERE Indian = 30947"}
-{"text": "table: 1-10118412-6\ncolumns: State/Territory, Asian American Population (2010 Census), Chinese, Filipino, Indian, Japanese, Korean, Vietnamese, Other Asian\nQ: What is the highest Indian population?\nA: SELECT MAX Indian FROM 1-10118412-6"}
-{"text": "table: 1-10121127-1\ncolumns: UN Operation name, UN Operation title, Location, Dates of Australian involvement, Number of Australians involved, Australian role\nQ: What is Australia's role in the UN operation Unama?\nA: SELECT Australian role FROM 1-10121127-1 WHERE UN Operation name = 'UNAMA'"}
-{"text": "table: 1-10121127-1\ncolumns: UN Operation name, UN Operation title, Location, Dates of Australian involvement, Number of Australians involved, Australian role\nQ: What is the UN operation title with the UN operation name, Uncok?\nA: SELECT UN Operation title FROM 1-10121127-1 WHERE UN Operation name = 'UNCOK'"}
-{"text": "table: 1-10121127-1\ncolumns: UN Operation name, UN Operation title, Location, Dates of Australian involvement, Number of Australians involved, Australian role\nQ: How many Australians were in the UN commission on Korea?\nA: SELECT COUNT Number of Australians involved FROM 1-10121127-1 WHERE UN Operation title = 'UN Commission on Korea'"}
-{"text": "table: 1-10121127-1\ncolumns: UN Operation name, UN Operation title, Location, Dates of Australian involvement, Number of Australians involved, Australian role\nQ: When was it where 65 Australians were involved in the UN?\nA: SELECT Dates of Australian involvement FROM 1-10121127-1 WHERE Number of Australians involved = '65'"}
-{"text": "table: 1-10120207-8\ncolumns: Season, Timeslot ( ET ), Season premiere, Season finale, TV season, Rank, Viewers (millions)\nQ: What year is the season with the 10.73 million views?\nA: SELECT TV season FROM 1-10120207-8 WHERE Viewers (millions) = '10.73'"}
-{"text": "table: 1-10120207-8\ncolumns: Season, Timeslot ( ET ), Season premiere, Season finale, TV season, Rank, Viewers (millions)\nQ: What is the season year where the rank is 39?\nA: SELECT TV season FROM 1-10120207-8 WHERE Rank = '39'"}
-{"text": "table: 1-10120207-8\ncolumns: Season, Timeslot ( ET ), Season premiere, Season finale, TV season, Rank, Viewers (millions)\nQ: What is the number of season premieres were 10.17 people watched?\nA: SELECT COUNT Season premiere FROM 1-10120207-8 WHERE Viewers (millions) = '10.17'"}
-{"text": "table: 1-10120207-8\ncolumns: Season, Timeslot ( ET ), Season premiere, Season finale, TV season, Rank, Viewers (millions)\nQ: What is the year of the season that was 12?\nA: SELECT TV season FROM 1-10120207-8 WHERE Season = 12"}
-{"text": "table: 1-1012730-1\ncolumns: Year, Starts, Wins, Top 5, Top 10, Poles, Avg. Start, Avg. Finish, Winnings, Position, Team(s)\nQ: In 2012 what was the average finish?\nA: SELECT Avg. Finish FROM 1-1012730-1 WHERE Year = 2012"}
-{"text": "table: 1-1012730-1\ncolumns: Year, Starts, Wins, Top 5, Top 10, Poles, Avg. Start, Avg. Finish, Winnings, Position, Team(s)\nQ: How many wins happened in 1983?\nA: SELECT MIN Wins FROM 1-1012730-1 WHERE Year = 1983"}
-{"text": "table: 1-1012730-1\ncolumns: Year, Starts, Wins, Top 5, Top 10, Poles, Avg. Start, Avg. Finish, Winnings, Position, Team(s)\nQ: How many top tens had an average start of 29.4?\nA: SELECT COUNT Top 10 FROM 1-1012730-1 WHERE Avg. Start = '29.4'"}
-{"text": "table: 1-1012730-1\ncolumns: Year, Starts, Wins, Top 5, Top 10, Poles, Avg. Start, Avg. Finish, Winnings, Position, Team(s)\nQ: How many poles had an average finish of 19.1?\nA: SELECT MAX Poles FROM 1-1012730-1 WHERE Avg. Finish = '19.1'"}
-{"text": "table: 1-1012730-1\ncolumns: Year, Starts, Wins, Top 5, Top 10, Poles, Avg. Start, Avg. Finish, Winnings, Position, Team(s)\nQ: How many starts did Hendrick motorsports have?\nA: SELECT MIN Starts FROM 1-1012730-1 WHERE Team(s) = 'Hendrick Motorsports'"}
-{"text": "table: 1-1013129-10\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: NHL players are all centre in Florida panthers.\nA: SELECT Player FROM 1-1013129-10 WHERE Position = 'Centre' AND NHL team = 'Florida Panthers'"}
-{"text": "table: 1-1013129-10\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: NHL team player San Jose Sharks is United States nationally.\nA: SELECT Player FROM 1-1013129-10 WHERE NHL team = 'San Jose Sharks' AND Nationality = 'United States'"}
-{"text": "table: 1-1013129-10\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: All players are position mark polak.\nA: SELECT Position FROM 1-1013129-10 WHERE Player = 'Mark Polak'"}
-{"text": "table: 1-1013129-10\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: Position in nhl team centre are all smaller pick than 243.0\nA: SELECT NHL team FROM 1-1013129-10 WHERE Position = 'Centre' AND Pick < 243.0"}
-{"text": "table: 1-1013129-11\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What college/junior/club teams do the players from the St. Louis Blues come from?\nA: SELECT College/junior/club team FROM 1-1013129-11 WHERE NHL team = 'St. Louis Blues'"}
-{"text": "table: 1-1013129-11\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What teams do the players from TPS (Finland) play for?\nA: SELECT NHL team FROM 1-1013129-11 WHERE College/junior/club team = 'TPS (Finland)'"}
-{"text": "table: 1-1013129-11\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What high school team did Doug Nolan play for?\nA: SELECT College/junior/club team FROM 1-1013129-11 WHERE Player = 'Doug Nolan'"}
-{"text": "table: 1-1013129-11\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What club team is Per Gustafsson play for?\nA: SELECT College/junior/club team FROM 1-1013129-11 WHERE Player = 'Per Gustafsson'"}
-{"text": "table: 1-1013129-11\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What is the nationality of Shayne Wright?\nA: SELECT Nationality FROM 1-1013129-11 WHERE Player = 'Shayne Wright'"}
-{"text": "table: 1-10128185-2\ncolumns: Song, Mobiles, Northern Ireland, Northern England, Scotland, Southern England, Wales, Total\nQ: How many votes did Southern England cast whilst Northern Ireland cast 3?\nA: SELECT Southern England FROM 1-10128185-2 WHERE Northern Ireland = 3"}
-{"text": "table: 1-10128185-2\ncolumns: Song, Mobiles, Northern Ireland, Northern England, Scotland, Southern England, Wales, Total\nQ: What was the lowest number of votes Scotland cast?\nA: SELECT MIN Scotland FROM 1-10128185-2"}
-{"text": "table: 1-10128185-2\ncolumns: Song, Mobiles, Northern Ireland, Northern England, Scotland, Southern England, Wales, Total\nQ: What is the total number of votes if Scotland cast 35?\nA: SELECT COUNT Scotland FROM 1-10128185-2 WHERE Total = 35"}
-{"text": "table: 1-10128185-2\ncolumns: Song, Mobiles, Northern Ireland, Northern England, Scotland, Southern England, Wales, Total\nQ: How many votes did Northern Ireland cast if the total was 35?\nA: SELECT Northern Ireland FROM 1-10128185-2 WHERE Total = 35"}
-{"text": "table: 1-10128185-2\ncolumns: Song, Mobiles, Northern Ireland, Northern England, Scotland, Southern England, Wales, Total\nQ: How many votes did Wales cast when Northern England cast 6?\nA: SELECT MIN Wales FROM 1-10128185-2 WHERE Northern England = 6"}
-{"text": "table: 1-1012730-2\ncolumns: Year, Starts, Wins, Top 5, Top 10, Poles, Avg. Start, Avg. Finish, Winnings, Position, Team(s)\nQ: What teams had 9 in the top 5 and 1 wins?\nA: SELECT Team(s) FROM 1-1012730-2 WHERE Top 5 = 9 AND Wins = 1"}
-{"text": "table: 1-1013129-1\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What teams did the player vadim sharifijanov play for?\nA: SELECT College/junior/club team FROM 1-1013129-1 WHERE Player = 'Vadim Sharifijanov'"}
-{"text": "table: 1-1013129-1\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What positions do the hartford whalers nhl team have?\nA: SELECT Position FROM 1-1013129-1 WHERE NHL team = 'Hartford Whalers'"}
-{"text": "table: 1-1013129-1\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What is the smallest pick for the player, brett lindros?\nA: SELECT MIN Pick FROM 1-1013129-1 WHERE Player = 'Brett Lindros'"}
-{"text": "table: 1-1013129-1\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What positions does the college/junior/club team, molot perm (russia) have?\nA: SELECT Position FROM 1-1013129-1 WHERE College/junior/club team = 'Molot Perm (Russia)'"}
-{"text": "table: 1-1013129-1\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: The nhl team new york islanders is what nationality?\nA: SELECT Nationality FROM 1-1013129-1 WHERE NHL team = 'New York Islanders'"}
-{"text": "table: 1-1013168-3\ncolumns: District, Vacator, Reason for change, Successor, Date successor seated\nQ: What is the name of the vacator for district Louisiana 1st?\nA: SELECT Vacator FROM 1-1013168-3 WHERE District = 'Louisiana 1st'"}
-{"text": "table: 1-101336-1\ncolumns: Formula, Notation, T c (K), No. of Cu-O planes in unit cell, Crystal structure\nQ: What is the notion when the crystal structure is tetragonal and the formula is bi 2 sr 2 cacu 2 o 8\nA: SELECT Notation FROM 1-101336-1 WHERE Crystal structure = 'Tetragonal' AND Formula = 'Bi 2 Sr 2 CaCu 2 O 8'"}
-{"text": "table: 1-101336-1\ncolumns: Formula, Notation, T c (K), No. of Cu-O planes in unit cell, Crystal structure\nQ: How many times is the formula tl 2 ba 2 cuo 6?\nA: SELECT No. of Cu-O planes in unit cell FROM 1-101336-1 WHERE Formula = 'Tl 2 Ba 2 CuO 6'"}
-{"text": "table: 1-101336-1\ncolumns: Formula, Notation, T c (K), No. of Cu-O planes in unit cell, Crystal structure\nQ: What is the crystal structure for the formula yba 2 cu 3 o 7?\nA: SELECT Crystal structure FROM 1-101336-1 WHERE Formula = 'YBa 2 Cu 3 O 7'"}
-{"text": "table: 1-101336-1\ncolumns: Formula, Notation, T c (K), No. of Cu-O planes in unit cell, Crystal structure\nQ: What is the number for t c (k) when the notation is tl-2212?\nA: SELECT COUNT T c (K) FROM 1-101336-1 WHERE Notation = 'Tl-2212'"}
-{"text": "table: 1-10138926-1\ncolumns: #, City, 1981 Census, 1991 Census, 2001 Census, 2010 Est., Region\nQ: How many 2010 estimations have been done in the city of Cremona?\nA: SELECT COUNT 2010 Est. FROM 1-10138926-1 WHERE City = 'Cremona'"}
-{"text": "table: 1-10138926-1\ncolumns: #, City, 1981 Census, 1991 Census, 2001 Census, 2010 Est., Region\nQ: What's the 2001 census of the region of Abruzzo where the 1871 census is bigger than 51092.0?\nA: SELECT MIN 2001 Census FROM 1-10138926-1 WHERE Region = 'Abruzzo' AND 1981 Census > 51092.0"}
-{"text": "table: 1-10138926-1\ncolumns: #, City, 1981 Census, 1991 Census, 2001 Census, 2010 Est., Region\nQ: What's the 1991 census of the city of Carpi?\nA: SELECT MAX 1991 Census FROM 1-10138926-1 WHERE City = 'Carpi'"}
-{"text": "table: 1-10138926-1\ncolumns: #, City, 1981 Census, 1991 Census, 2001 Census, 2010 Est., Region\nQ: How many 2001 censuses are there on number 13?\nA: SELECT COUNT 2001 Census FROM 1-10138926-1 WHERE # = 13"}
-{"text": "table: 1-10138926-1\ncolumns: #, City, 1981 Census, 1991 Census, 2001 Census, 2010 Est., Region\nQ: What's the 1981 census of Livorno?\nA: SELECT 1981 Census FROM 1-10138926-1 WHERE City = 'Livorno'"}
-{"text": "table: 1-1013129-8\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: Which NHL team has player Mike Loach?\nA: SELECT NHL team FROM 1-1013129-8 WHERE Player = 'Mike Loach'"}
-{"text": "table: 1-1013129-8\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What is the NHL team that has Peter Strom?\nA: SELECT NHL team FROM 1-1013129-8 WHERE Player = 'Peter Strom'"}
-{"text": "table: 1-1013129-8\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: What team is Keith Mccambridge on?\nA: SELECT College/junior/club team FROM 1-1013129-8 WHERE Player = 'Keith McCambridge'"}
-{"text": "table: 1-1013129-8\ncolumns: Pick, Player, Position, Nationality, NHL team, College/junior/club team\nQ: How many nationalities are the pick 193?\nA: SELECT COUNT Nationality FROM 1-1013129-8 WHERE Pick = 193"}
-{"text": "table: 1-1013168-2\ncolumns: State (class), Vacator, Reason for change, Successor, Date of successors formal installation\nQ: Who was the succesor that was formally installed on November 8, 1978?\nA: SELECT Successor FROM 1-1013168-2 WHERE Date of successors formal installation = 'November 8, 1978'"}
-{"text": "table: 1-1014319-1\ncolumns: Week, Dance/song, Horwood, Goodman, Dixon, Tonioli, Total, Result\nQ: How many songs received a 10 from Goodman and were rated by Tonioli?\nA: SELECT COUNT Tonioli FROM 1-1014319-1 WHERE Goodman = '10'"}
-{"text": "table: 1-1014319-1\ncolumns: Week, Dance/song, Horwood, Goodman, Dixon, Tonioli, Total, Result\nQ: What score did Goodman give to all songs with safe results, which received a 7 from Horwood and have a total score of 31?\nA: SELECT Goodman FROM 1-1014319-1 WHERE Total = '31' AND Horwood = '7' AND Result = 'Safe'"}
-{"text": "table: 1-1014319-1\ncolumns: Week, Dance/song, Horwood, Goodman, Dixon, Tonioli, Total, Result\nQ: What score did Dixon give to the song \"samba / young hearts run free\", which was in second place?\nA: SELECT Dixon FROM 1-1014319-1 WHERE Dance/song = 'Samba / Young Hearts Run Free' AND Result = 'Second place'"}
-{"text": "table: 1-1014319-1\ncolumns: Week, Dance/song, Horwood, Goodman, Dixon, Tonioli, Total, Result\nQ: How many scores did Goodman give to \"samba / young hearts run free\", which was in second place?\nA: SELECT COUNT Goodman FROM 1-1014319-1 WHERE Result = 'Second place' AND Dance/song = 'Samba / Young Hearts Run Free'"}
-{"text": "table: 1-1015421-1\ncolumns: Class, Operator, No. Built, Year Built, Cars per Set, Unit nos.\nQ: What year was number 7 built?\nA: SELECT Year Built FROM 1-1015421-1 WHERE No. Built = 7"}
-{"text": "table: 1-1015914-24\ncolumns: Case/Suffix, we two, you and I, you two, them two (the two), who-two\nQ: What is we two when the case/suffix is loc.?\nA: SELECT we two FROM 1-1015914-24 WHERE Case/Suffix = 'loc.'"}
-{"text": "table: 1-1015914-24\ncolumns: Case/Suffix, we two, you and I, you two, them two (the two), who-two\nQ: What is them two (the two) when we two is ngalbelpa?\nA: SELECT them two (the two) FROM 1-1015914-24 WHERE we two = 'ngalbelpa'"}
-{"text": "table: 1-1015914-24\ncolumns: Case/Suffix, we two, you and I, you two, them two (the two), who-two\nQ: What is them two (the two) when you and i is ng\u0153balngu?\nA: SELECT them two (the two) FROM 1-1015914-24 WHERE you and I = 'ng\u0153balngu'"}
-{"text": "table: 1-1015914-24\ncolumns: Case/Suffix, we two, you and I, you two, them two (the two), who-two\nQ: What is who-two where you and i is ng\u0153ban?\nA: SELECT who-two FROM 1-1015914-24 WHERE you and I = 'ng\u0153ban'"}
-{"text": "table: 1-1015914-24\ncolumns: Case/Suffix, we two, you and I, you two, them two (the two), who-two\nQ: What is we two where you two is ngipen?\nA: SELECT we two FROM 1-1015914-24 WHERE you two = 'ngipen'"}
-{"text": "table: 1-1015914-24\ncolumns: Case/Suffix, we two, you and I, you two, them two (the two), who-two\nQ: What is who-two when you two is ngipelngu?\nA: SELECT who-two FROM 1-1015914-24 WHERE you two = 'ngipelngu'"}
-{"text": "table: 1-10160447-1\ncolumns: Position, Driver, Points, Winnings, Series\nQ: what's the\u00a0points\u00a0with\u00a0driver\u00a0 mark martin\nA: SELECT Points FROM 1-10160447-1 WHERE Driver = 'Mark Martin'"}
-{"text": "table: 1-10160447-1\ncolumns: Position, Driver, Points, Winnings, Series\nQ: what's the\u00a0points\u00a0with\u00a0driver\u00a0 rusty wallace\nA: SELECT Points FROM 1-10160447-1 WHERE Driver = 'Rusty Wallace'"}
-{"text": "table: 1-10160447-1\ncolumns: Position, Driver, Points, Winnings, Series\nQ: what's the total number of\u00a0position\u00a0with\u00a0driver\u00a0 robby gordon\nA: SELECT COUNT Position FROM 1-10160447-1 WHERE Driver = 'Robby Gordon'"}
-{"text": "table: 1-10160447-1\ncolumns: Position, Driver, Points, Winnings, Series\nQ: what's the maximum\u00a0position\u00a0with\u00a0winnings\u00a0 $50,000\nA: SELECT MAX Position FROM 1-10160447-1 WHERE Winnings = '$50,000'"}
-{"text": "table: 1-10236830-6\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: What actor was nominted for an award in the film Anastasiya Slutskaya?\nA: SELECT Actors Name FROM 1-10236830-6 WHERE Film Name = 'Anastasiya Slutskaya'"}
-{"text": "table: 1-10236830-6\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: What was the film Falling up nominated for?\nA: SELECT Nomination FROM 1-10236830-6 WHERE Film Name = 'Falling Up'"}
-{"text": "table: 1-10236830-6\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: What is the name of the actress that was nominated for best actress in a leading role in the film Chopin: Desire for love?\nA: SELECT Actors Name FROM 1-10236830-6 WHERE Film Name = 'Chopin: Desire for Love' AND Nomination = 'Best Actress in a Leading Role'"}
-{"text": "table: 1-10236830-6\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: What is the name of the actress that was nominated for best actress in a leading role in the film Chopin: Desire for love?\nA: SELECT Actors Name FROM 1-10236830-6 WHERE Film Name = 'Chopin: Desire for Love' AND Nomination = 'Best Actress in a Leading Role'"}
-{"text": "table: 1-10236830-6\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: Which films does the actor Alla Sergiyko star in?\nA: SELECT Film Name FROM 1-10236830-6 WHERE Actors Name = 'Alla Sergiyko'"}
-{"text": "table: 1-10236830-6\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: Which nominations was the film 27 Stolen Kisses nominated for?\nA: SELECT Nomination FROM 1-10236830-6 WHERE Film Name = '27 Stolen Kisses'"}
-{"text": "table: 1-10236830-4\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: Which actor from Serbia was nominated for best actor in a supporting role?\nA: SELECT Actors Name FROM 1-10236830-4 WHERE Nomination = 'Best Actor in a Supporting Role' AND Country = 'Serbia'"}
-{"text": "table: 1-10236830-4\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: Vsevolod Shilovskiy is from what country?\nA: SELECT Country FROM 1-10236830-4 WHERE Actors Name = 'Vsevolod Shilovskiy'"}
-{"text": "table: 1-10236830-4\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: Which nominations are connected to the film Totalitarian Romance?\nA: SELECT Nomination FROM 1-10236830-4 WHERE Film Name = 'Totalitarian Romance'"}
-{"text": "table: 1-10236830-4\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: Srdjan Dragojevic worked on a film which earned what nomination?\nA: SELECT Nomination FROM 1-10236830-4 WHERE Director = 'Srdjan Dragojevic'"}
-{"text": "table: 1-10236830-4\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: Which actors are from Ukraine?\nA: SELECT Actors Name FROM 1-10236830-4 WHERE Country = 'Ukraine'"}
-{"text": "table: 1-10236830-1\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: What was the film that vadim ilyenko directed?\nA: SELECT Film Name FROM 1-10236830-1 WHERE Director = 'Vadim Ilyenko'"}
-{"text": "table: 1-10236830-1\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: What was the actors name that vadim ilyenko directed?\nA: SELECT Actors Name FROM 1-10236830-1 WHERE Director = 'Vadim Ilyenko'"}
-{"text": "table: 1-10236830-1\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: What was the actors name for fuchzhou and nomination was best non-professional actor?\nA: SELECT Actors Name FROM 1-10236830-1 WHERE Film Name = 'Fuchzhou' AND Nomination = 'Best Non-Professional Actor'"}
-{"text": "table: 1-10236830-1\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: What film did michaylo ilyenko make with best actor in a supporting role?\nA: SELECT Film Name FROM 1-10236830-1 WHERE Director = 'Michaylo Ilyenko' AND Nomination = 'Best Actor in a Supporting Role'"}
-{"text": "table: 1-10236830-1\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: What was the actor's name for best debut?\nA: SELECT Actors Name FROM 1-10236830-1 WHERE Nomination = 'Best Debut'"}
-{"text": "table: 1-10236830-1\ncolumns: Nomination, Actors Name, Film Name, Director, Country\nQ: What was the number of nominations for natalia raskokoha?\nA: SELECT COUNT Nomination FROM 1-10236830-1 WHERE Actors Name = 'Natalia Raskokoha'"}
-{"text": "table: 1-10240125-1\ncolumns: Season, Division, League Apps, League Goals, FA Cup Apps, FA Cup Goals, Total Apps, Total Goals\nQ: What is the highest value of Total Goals?\nA: SELECT MAX Total Goals FROM 1-10240125-1"}
-{"text": "table: 1-10240125-1\ncolumns: Season, Division, League Apps, League Goals, FA Cup Apps, FA Cup Goals, Total Apps, Total Goals\nQ: When FA Cup Apps is 9 what is the smallest number of FA Cup Goals?\nA: SELECT MIN FA Cup Goals FROM 1-10240125-1 WHERE FA Cup Apps = 9"}
-{"text": "table: 1-10240125-1\ncolumns: Season, Division, League Apps, League Goals, FA Cup Apps, FA Cup Goals, Total Apps, Total Goals\nQ: What is the smallest number of Total Goals?\nA: SELECT MIN Total Goals FROM 1-10240125-1"}
-{"text": "table: 1-10264179-2\ncolumns: Round, Circuit, Date, Pole Position, Fastest Lap, Winning Driver, Winning Team\nQ: What circuit was the race where Hideki Mutoh had the fastest lap?\nA: SELECT Circuit FROM 1-10264179-2 WHERE Fastest Lap = 'Hideki Mutoh'"}
-{"text": "table: 1-10269427-3\ncolumns: Episode #, Production code, Title, Directed by, Written by, Airdate\nQ: what is the minimum production code with title \"foreign exchange problem / turn about\"\nA: SELECT MIN Production code FROM 1-10269427-3 WHERE Title = '\"Foreign Exchange Problem / Turn About\"'"}
-{"text": "table: 1-10269427-3\ncolumns: Episode #, Production code, Title, Directed by, Written by, Airdate\nQ: what is the episode # for title \"the yindianapolis 500 / personality problem\"\nA: SELECT Episode # FROM 1-10269427-3 WHERE Title = '\"The Yindianapolis 500 / Personality Problem\"'"}
-{"text": "table: 1-10269427-3\ncolumns: Episode #, Production code, Title, Directed by, Written by, Airdate\nQ: what is the episode # for production code 227\nA: SELECT Episode # FROM 1-10269427-3 WHERE Production code = 227"}
-{"text": "table: 1-10269427-3\ncolumns: Episode #, Production code, Title, Directed by, Written by, Airdate\nQ: who directed the movie written by is sib ventress / aydrea ten bosch\nA: SELECT Directed by FROM 1-10269427-3 WHERE Written by = 'Sib Ventress / Aydrea ten Bosch'"}
-{"text": "table: 1-10269427-3\ncolumns: Episode #, Production code, Title, Directed by, Written by, Airdate\nQ: what is the production code with title \"skirting the issue / moon over my yinnie\"\nA: SELECT Production code FROM 1-10269427-3 WHERE Title = '\"Skirting the Issue / Moon Over my Yinnie\"'"}
-{"text": "table: 1-10240125-2\ncolumns: Season, Division, League Apps, League Goals, FA Cup Apps, FA Cup Goals, Total Apps, Total Goals\nQ: Whatis the number of total goals maximum?\nA: SELECT MAX Total Goals FROM 1-10240125-2"}
-{"text": "table: 1-10262329-1\ncolumns: Assembly Type, Adhesive Type, Time(Sec), Temp (\u00b0C), Pressure\nQ: HOW MANY TEMPERATURE INTERVALS ARE POSSIBLE TO USE WITH ACRYL? \nA: SELECT COUNT Temp (\u00b0C) FROM 1-10262329-1 WHERE Adhesive Type = 'Acryl'"}
-{"text": "table: 1-1028356-3\ncolumns: Outcome, Year, Championship, Surface, Partner, Opponents, Score\nQ: How many matches where played with Jim Pugh?\nA: SELECT COUNT Opponents FROM 1-1028356-3 WHERE Partner = 'Jim Pugh'"}
-{"text": "table: 1-1028356-3\ncolumns: Outcome, Year, Championship, Surface, Partner, Opponents, Score\nQ: What is the score with partner Jim Pugh?\nA: SELECT Score FROM 1-1028356-3 WHERE Partner = 'Jim Pugh'"}
-{"text": "table: 1-1028356-3\ncolumns: Outcome, Year, Championship, Surface, Partner, Opponents, Score\nQ: How many matched scored 3\u20136, 7\u20136(5), 6\u20133?\nA: SELECT COUNT Surface FROM 1-1028356-3 WHERE Score = '3\u20136, 7\u20136(5), 6\u20133'"}
-{"text": "table: 1-1028356-3\ncolumns: Outcome, Year, Championship, Surface, Partner, Opponents, Score\nQ: What is the score of the match with partner Jim Pugh?\nA: SELECT Score FROM 1-1028356-3 WHERE Partner = 'Jim Pugh'"}
-{"text": "table: 1-1028356-3\ncolumns: Outcome, Year, Championship, Surface, Partner, Opponents, Score\nQ: What year was the championship in Wimbledon (2)?\nA: SELECT Year FROM 1-1028356-3 WHERE Championship = 'Wimbledon (2)'"}
-{"text": "table: 1-1028356-3\ncolumns: Outcome, Year, Championship, Surface, Partner, Opponents, Score\nQ: What is the score of the match with opponents Gretchen Magers Kelly Jones?\nA: SELECT Score FROM 1-1028356-3 WHERE Opponents = 'Gretchen Magers Kelly Jones'"}
-{"text": "table: 1-10284385-1\ncolumns: Begin Date, End Date, Representative, Date of birth, House term, State served, Party, Age (years, days)\nQ: How many birthdays does Earl Hanley Beshlin have?\nA: SELECT COUNT Date of birth FROM 1-10284385-1 WHERE Representative = 'Earl Hanley Beshlin'"}
-{"text": "table: 1-10284385-1\ncolumns: Begin Date, End Date, Representative, Date of birth, House term, State served, Party, Age (years, days)\nQ: Which politican party has a birthday of November 10, 1880\nA: SELECT Party FROM 1-10284385-1 WHERE Date of birth = 'November 10, 1880'"}
-{"text": "table: 1-10284385-1\ncolumns: Begin Date, End Date, Representative, Date of birth, House term, State served, Party, Age (years, days)\nQ: Which representative has a birthday of January 31, 1866?\nA: SELECT Representative FROM 1-10284385-1 WHERE Date of birth = 'January 31, 1866'"}
-{"text": "table: 1-10295819-1\ncolumns: Player, Current singles ranking, Current doubles ranking, First year played, Ties played, Total W\u2013L, Singles W\u2013L, Doubles W\u2013L\nQ: What is the Singles W-L for the players named Laurynas Grigelis?\nA: SELECT Singles W\u2013L FROM 1-10295819-1 WHERE Player = 'Laurynas Grigelis'"}
-{"text": "table: 1-10295819-1\ncolumns: Player, Current singles ranking, Current doubles ranking, First year played, Ties played, Total W\u2013L, Singles W\u2013L, Doubles W\u2013L\nQ: What is the Current singles ranking for the player named Mantas Bugaili\u0161kis?\nA: SELECT Current singles ranking FROM 1-10295819-1 WHERE Player = 'Mantas Bugaili\u0161kis'"}
-{"text": "table: 1-10312547-1\ncolumns: Character, 1954 Broadway, 1955 broadcast, 1960 broadcast, 1979 Broadway, 1990 Broadway, 1991 Broadway, 1998 Broadway, 1999 Broadway\nQ: How many playerd Mrs. Darling in the 1999 Broadway?\nA: SELECT COUNT 1999 Broadway FROM 1-10312547-1 WHERE Character = 'Mrs. Darling'"}
-{"text": "table: 1-10312547-1\ncolumns: Character, 1954 Broadway, 1955 broadcast, 1960 broadcast, 1979 Broadway, 1990 Broadway, 1991 Broadway, 1998 Broadway, 1999 Broadway\nQ: Who played Peter Pan in the 1990 Broadway?\nA: SELECT 1990 Broadway FROM 1-10312547-1 WHERE Character = 'Peter Pan'"}
-{"text": "table: 1-10312547-1\ncolumns: Character, 1954 Broadway, 1955 broadcast, 1960 broadcast, 1979 Broadway, 1990 Broadway, 1991 Broadway, 1998 Broadway, 1999 Broadway\nQ: Who played in the 1991 Broadway before Barbara McCulloh played the part in the 1999 Broadway?\nA: SELECT 1991 Broadway FROM 1-10312547-1 WHERE 1999 Broadway = 'Barbara McCulloh'"}
-{"text": "table: 1-10312547-1\ncolumns: Character, 1954 Broadway, 1955 broadcast, 1960 broadcast, 1979 Broadway, 1990 Broadway, 1991 Broadway, 1998 Broadway, 1999 Broadway\nQ: Who played in the 1990 Broadway after Tom Halloran played the character in the 1955 Broadcast?\nA: SELECT 1990 Broadway FROM 1-10312547-1 WHERE 1955 broadcast = 'Tom Halloran'"}
-{"text": "table: 1-10312547-1\ncolumns: Character, 1954 Broadway, 1955 broadcast, 1960 broadcast, 1979 Broadway, 1990 Broadway, 1991 Broadway, 1998 Broadway, 1999 Broadway\nQ: What character did Drake English play in the 1998 Broadway?\nA: SELECT Character FROM 1-10312547-1 WHERE 1998 Broadway = 'Drake English'"}
-{"text": "table: 1-103084-4\ncolumns: Year, Broadcast date, BBC One total viewing, BBC One Rank, BBC Two total viewing, BBC Two Rank\nQ: What date was BBC One total viewing greater then 11616996.338225884?\nA: SELECT Broadcast date FROM 1-103084-4 WHERE BBC One total viewing > 11616996.338225884"}
-{"text": "table: 1-103084-4\ncolumns: Year, Broadcast date, BBC One total viewing, BBC One Rank, BBC Two total viewing, BBC Two Rank\nQ: How many years did BBC One rank 20th?\nA: SELECT COUNT Year FROM 1-103084-4 WHERE BBC One Rank = '20th'"}
-{"text": "table: 1-103084-4\ncolumns: Year, Broadcast date, BBC One total viewing, BBC One Rank, BBC Two total viewing, BBC Two Rank\nQ: What year was the BBC two total viewing 7,530,000?\nA: SELECT MIN Year FROM 1-103084-4 WHERE BBC Two total viewing = '7,530,000'"}
-{"text": "table: 1-10321124-1\ncolumns: \u2193 Function / Genus \u2192, Shigella, Salmonella, Yersinia, Escherichia\nQ: how many\u00a0\u2193 function / genus \u2192\u00a0with\u00a0escherichia\u00a0being espd\nA: SELECT COUNT \u2193 Function / Genus \u2192 FROM 1-10321124-1 WHERE Escherichia = 'EspD'"}
-{"text": "table: 1-10321124-1\ncolumns: \u2193 Function / Genus \u2192, Shigella, Salmonella, Yersinia, Escherichia\nQ: what's the\u00a0salmonella\u00a0with\u00a0escherichia\u00a0being espd\nA: SELECT Salmonella FROM 1-10321124-1 WHERE Escherichia = 'EspD'"}
-{"text": "table: 1-10321124-1\ncolumns: \u2193 Function / Genus \u2192, Shigella, Salmonella, Yersinia, Escherichia\nQ: what's the\u00a0\u2193 function / genus \u2192\u00a0with\u00a0shigella\u00a0being spa32\nA: SELECT \u2193 Function / Genus \u2192 FROM 1-10321124-1 WHERE Shigella = 'Spa32'"}
-{"text": "table: 1-10321124-1\ncolumns: \u2193 Function / Genus \u2192, Shigella, Salmonella, Yersinia, Escherichia\nQ: what's the\u00a0salmonella\u00a0with\u00a0shigella\u00a0being ipgc\nA: SELECT Salmonella FROM 1-10321124-1 WHERE Shigella = 'IpgC'"}
-{"text": "table: 1-10321124-1\ncolumns: \u2193 Function / Genus \u2192, Shigella, Salmonella, Yersinia, Escherichia\nQ: what's the\u00a0salmonella\u00a0with\u00a0escherichia\u00a0being sepb (escn)\nA: SELECT Salmonella FROM 1-10321124-1 WHERE Escherichia = 'SepB (EscN)'"}
-{"text": "table: 1-10321124-1\ncolumns: \u2193 Function / Genus \u2192, Shigella, Salmonella, Yersinia, Escherichia\nQ: what's the\u00a0shigella\u00a0with\u00a0yersinia\u00a0being yscp\nA: SELECT Shigella FROM 1-10321124-1 WHERE Yersinia = 'YscP'"}
-{"text": "table: 1-10321805-1\ncolumns: Year (Ceremony), Film title used in nomination, Original title, Director, Result\nQ: How many original titles did Marriage Italian-Style have? \nA: SELECT COUNT Original title FROM 1-10321805-1 WHERE Film title used in nomination = 'Marriage Italian-Style'"}
-{"text": "table: 1-10321805-1\ncolumns: Year (Ceremony), Film title used in nomination, Original title, Director, Result\nQ: What year was a movie with the original title La Leggenda del Santo Bevitore submitted?\nA: SELECT Year (Ceremony) FROM 1-10321805-1 WHERE Original title = 'La leggenda del santo bevitore'"}
-{"text": "table: 1-10335-1\ncolumns: Camp, Estimated deaths, Operational, Occupied territory, Current country of location, Primary means for mass killings\nQ: what's the\u00a0camp\u00a0with\u00a0estimated deaths\u00a0of 600,000\nA: SELECT Camp FROM 1-10335-1 WHERE Estimated deaths = '600,000'"}
-{"text": "table: 1-10335-1\ncolumns: Camp, Estimated deaths, Operational, Occupied territory, Current country of location, Primary means for mass killings\nQ: what's the\u00a0operational period\u00a0with\u00a0camp\u00a0 sajmi\u0161te\nA: SELECT Operational FROM 1-10335-1 WHERE Camp = 'Sajmi\u0161te'"}
-{"text": "table: 1-10335-1\ncolumns: Camp, Estimated deaths, Operational, Occupied territory, Current country of location, Primary means for mass killings\nQ: what's the\u00a0estimated deaths\u00a0with\u00a0operational period\u00a0of 17 march 1942 \u2013 end of june 1943\nA: SELECT Estimated deaths FROM 1-10335-1 WHERE Operational = '17 March 1942 \u2013 end of June 1943'"}
-{"text": "table: 1-10335-1\ncolumns: Camp, Estimated deaths, Operational, Occupied territory, Current country of location, Primary means for mass killings\nQ: what's the\u00a0current country of location\u00a0with\u00a0operational period\u00a0\u00a0of summer of 1941 to 28 june 1944\nA: SELECT Current country of location FROM 1-10335-1 WHERE Operational = 'Summer of 1941 to 28 June 1944'"}
-{"text": "table: 1-10335-1\ncolumns: Camp, Estimated deaths, Operational, Occupied territory, Current country of location, Primary means for mass killings\nQ: what's the\u00a0occupied territory\u00a0with\u00a0estimated deaths\u00a0of 600,000\nA: SELECT Occupied territory FROM 1-10335-1 WHERE Estimated deaths = '600,000'"}
-{"text": "table: 1-10335-1\ncolumns: Camp, Estimated deaths, Operational, Occupied territory, Current country of location, Primary means for mass killings\nQ: what's the\u00a0occupied territory\u00a0with\u00a0operational\u00a0period of may 1940 \u2013 january 1945\nA: SELECT Occupied territory FROM 1-10335-1 WHERE Operational = 'May 1940 \u2013 January 1945'"}
-{"text": "table: 1-10360823-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: Which overall pick was traded to the Cleveland Browns?\nA: SELECT Overall FROM 1-10360823-1 WHERE College = 'Traded to the Cleveland Browns'"}
-{"text": "table: 1-10360823-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: Overall pick 240 was a pick in which round?\nA: SELECT Round FROM 1-10360823-1 WHERE Overall = 240"}
-{"text": "table: 1-10360823-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: Which overall pick number went to college at Youngstown State?\nA: SELECT MIN Overall FROM 1-10360823-1 WHERE College = 'Youngstown State'"}
-{"text": "table: 1-10360823-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: What position is played by pick 255 overall?\nA: SELECT Position FROM 1-10360823-1 WHERE Overall = 255"}
-{"text": "table: 1-10360823-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: Which player was chosen in round 17?\nA: SELECT Player name FROM 1-10360823-1 WHERE Round = 17"}
-{"text": "table: 1-10361453-2\ncolumns: Game, Date, Opponent, Result, Vikings points, Opponents, Record, Attendance\nQ: The record of 7-3 had the largest attendance of what?\nA: SELECT MAX Attendance FROM 1-10361453-2 WHERE Record = '7-3'"}
-{"text": "table: 1-10361453-2\ncolumns: Game, Date, Opponent, Result, Vikings points, Opponents, Record, Attendance\nQ: The record of 9-4 was against which opponent?\nA: SELECT Opponent FROM 1-10361453-2 WHERE Record = '9-4'"}
-{"text": "table: 1-10361453-2\ncolumns: Game, Date, Opponent, Result, Vikings points, Opponents, Record, Attendance\nQ: The game number of 8 had a record of what?\nA: SELECT Record FROM 1-10361453-2 WHERE Game = 8"}
-{"text": "table: 1-10360656-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: What round was Steve Stonebreaker drafted?\nA: SELECT MAX Round FROM 1-10360656-1 WHERE Player name = 'Steve Stonebreaker'"}
-{"text": "table: 1-10360656-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: Who was the top picki n the draft?\nA: SELECT MIN Choice FROM 1-10360656-1"}
-{"text": "table: 1-10360656-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: What round was Bill Hill drafted?\nA: SELECT Choice FROM 1-10360656-1 WHERE Player name = 'Bill Hill'"}
-{"text": "table: 1-10360656-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: What was the name of the quarterback drafted?\nA: SELECT Player name FROM 1-10360656-1 WHERE Position = 'Quarterback'"}
-{"text": "table: 1-10361625-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: Where is the college where Keith Hartwig plays?\nA: SELECT College FROM 1-10361625-1 WHERE Player name = 'Keith Hartwig'"}
-{"text": "table: 1-10361625-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: What is the name of the linebacker at Illinois college?\nA: SELECT Player name FROM 1-10361625-1 WHERE Position = 'Linebacker' AND College = 'Illinois'"}
-{"text": "table: 1-10361625-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: What is the greatest round of overall 83?\nA: SELECT MAX Round FROM 1-10361625-1 WHERE Overall = 83"}
-{"text": "table: 1-10361625-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: Which round did Tommy Kramer play in>\nA: SELECT Round FROM 1-10361625-1 WHERE Player name = 'Tommy Kramer'"}
-{"text": "table: 1-10361625-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: What is Rice's collage score?\nA: SELECT Overall FROM 1-10361625-1 WHERE College = 'Rice'"}
-{"text": "table: 1-10361230-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: Where does the defensive back position appear first?\nA: SELECT MIN Round FROM 1-10361230-1 WHERE Position = 'Defensive Back'"}
-{"text": "table: 1-10361230-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: What is Bruce Cerone overall?\nA: SELECT MIN Overall FROM 1-10361230-1 WHERE Player name = 'Bruce Cerone'"}
-{"text": "table: 1-10361230-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: Which player went to Emporia State?\nA: SELECT Player name FROM 1-10361230-1 WHERE College = 'Emporia State'"}
-{"text": "table: 1-10361230-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: What is the highest choice?\nA: SELECT MAX Choice FROM 1-10361230-1"}
-{"text": "table: 1-10361230-1\ncolumns: Round, Choice, Overall, Player name, Position, College\nQ: What college did Bill Cappleman go to?\nA: SELECT College FROM 1-10361230-1 WHERE Player name = 'Bill Cappleman'"}
-{"text": "table: 1-1036189-1\ncolumns: Headstamp ID, Primer Annulus Color, Bullet Tip Color, Other Features, Functional Type\nQ: For the headstamp id of h2, what was the color of the bullet tip?\nA: SELECT Bullet Tip Color FROM 1-1036189-1 WHERE Headstamp ID = 'H2'"}
-{"text": "table: 1-1036189-1\ncolumns: Headstamp ID, Primer Annulus Color, Bullet Tip Color, Other Features, Functional Type\nQ: For the functional type of light ball, what were the other features?\nA: SELECT Other Features FROM 1-1036189-1 WHERE Functional Type = 'Light Ball'"}
-{"text": "table: 1-1036189-1\ncolumns: Headstamp ID, Primer Annulus Color, Bullet Tip Color, Other Features, Functional Type\nQ: How many primers annulus colors were there when the color of the bullet tip was white?\nA: SELECT COUNT Primer Annulus Color FROM 1-1036189-1 WHERE Bullet Tip Color = 'White'"}
-{"text": "table: 1-1036189-1\ncolumns: Headstamp ID, Primer Annulus Color, Bullet Tip Color, Other Features, Functional Type\nQ: How many bullet tips colors had other features of a blue band on case base?\nA: SELECT COUNT Bullet Tip Color FROM 1-1036189-1 WHERE Other Features = 'Blue band on case base'"}
-{"text": "table: 1-1037590-1\ncolumns: Year, Games, Games started, Completions, Attempts, Completion %, Yards, Yards/Attempt, Touchdowns, Interceptions, Rating\nQ: How many touchdowns were scored in the year with a completion percentage of 56.0?\nA: SELECT MIN Touchdowns FROM 1-1037590-1 WHERE Completion % = '56.0'"}
-{"text": "table: 1-1037590-1\ncolumns: Year, Games, Games started, Completions, Attempts, Completion %, Yards, Yards/Attempt, Touchdowns, Interceptions, Rating\nQ: What number of completions are recorded for the year with 12 games started?\nA: SELECT Completions FROM 1-1037590-1 WHERE Games started = 12"}
-{"text": "table: 1-1037590-1\ncolumns: Year, Games, Games started, Completions, Attempts, Completion %, Yards, Yards/Attempt, Touchdowns, Interceptions, Rating\nQ: How many years were there with 348 attempts?\nA: SELECT COUNT Yards FROM 1-1037590-1 WHERE Attempts = 348"}
-{"text": "table: 1-10402018-1\ncolumns: Character, Australia & New Zealand (Sydney - first run, Melbourne, Auckland), London, Toronto / Broadway, Brazil, UK Tour, US Tour, Italy (Milan, Rome, Trieste)\nQ: How many characters is by Babs Rubenstein?\nA: SELECT COUNT London FROM 1-10402018-1 WHERE US Tour = 'Babs Rubenstein'"}
-{"text": "table: 1-10402018-1\ncolumns: Character, Australia & New Zealand (Sydney - first run, Melbourne, Auckland), London, Toronto / Broadway, Brazil, UK Tour, US Tour, Italy (Milan, Rome, Trieste)\nQ: Which person is in the tronto/broadway and has a uk tour of n/a\nA: SELECT Toronto / Broadway FROM 1-10402018-1 WHERE UK Tour = 'n/a'"}
-{"text": "table: 1-10402018-1\ncolumns: Character, Australia & New Zealand (Sydney - first run, Melbourne, Auckland), London, Toronto / Broadway, Brazil, UK Tour, US Tour, Italy (Milan, Rome, Trieste)\nQ: How many people play Frank in London?\nA: SELECT COUNT London FROM 1-10402018-1 WHERE Character = 'Frank'"}
-{"text": "table: 1-10399701-2\ncolumns: School Year, Class A, Class AA, Class AAA, Class AAAA, Class AAAAA\nQ: Who was Class AAA during the school year of 2000-01?\nA: SELECT Class AAA FROM 1-10399701-2 WHERE School Year = '2000-01'"}
-{"text": "table: 1-10399701-2\ncolumns: School Year, Class A, Class AA, Class AAA, Class AAAA, Class AAAAA\nQ: Who was Class AAA during the same year that Class A was (tie) Apple Springs/Texline?\nA: SELECT Class AAA FROM 1-10399701-2 WHERE Class A = '(tie) Apple Springs/Texline'"}
-{"text": "table: 1-10399701-2\ncolumns: School Year, Class A, Class AA, Class AAA, Class AAAA, Class AAAAA\nQ: Who was Class AAAAA during the school year of 1995-96?\nA: SELECT Class AAAAA FROM 1-10399701-2 WHERE School Year = '1995-96'"}
-{"text": "table: 1-10399701-2\ncolumns: School Year, Class A, Class AA, Class AAA, Class AAAA, Class AAAAA\nQ: Who was Class AAA during the same year that Class AAAAA was Brownsville Pace?\nA: SELECT Class AAA FROM 1-10399701-2 WHERE Class AAAAA = 'Brownsville Pace'"}
-{"text": "table: 1-10399701-2\ncolumns: School Year, Class A, Class AA, Class AAA, Class AAAA, Class AAAAA\nQ: What was the total number of Class AAA during the same year that Class AAA was White Oak?\nA: SELECT COUNT Class AA FROM 1-10399701-2 WHERE Class AAA = 'White Oak'"}
-{"text": "table: 1-10392906-2\ncolumns: Week, Date, Kickoff, Opponent, Final score, Team record, Game site, Attendance\nQ: How many records are listed on Friday, May 25?\nA: SELECT COUNT Team record FROM 1-10392906-2 WHERE Date = 'Friday, May 25'"}
-{"text": "table: 1-10392906-2\ncolumns: Week, Date, Kickoff, Opponent, Final score, Team record, Game site, Attendance\nQ: How many opponents were played on Saturday, June 9?\nA: SELECT COUNT Opponent FROM 1-10392906-2 WHERE Date = 'Saturday, June 9'"}
-{"text": "table: 1-10392906-2\ncolumns: Week, Date, Kickoff, Opponent, Final score, Team record, Game site, Attendance\nQ: In what week was the first game played at the Commerzbank-Arena?\nA: SELECT MIN Week FROM 1-10392906-2 WHERE Game site = 'Commerzbank-Arena'"}
-{"text": "table: 1-10413597-5\ncolumns: No. in series, No. in season, Title, Setting, Directed by, Written by, U.S. viewers (million), Original air date\nQ: What was the original air date of an episode set in 1544?\nA: SELECT Original air date FROM 1-10413597-5 WHERE Setting = '1544'"}
-{"text": "table: 1-10413597-5\ncolumns: No. in series, No. in season, Title, Setting, Directed by, Written by, U.S. viewers (million), Original air date\nQ: How many settings where there for episode 29 of the season?\nA: SELECT COUNT Setting FROM 1-10413597-5 WHERE No. in series = 29"}
-{"text": "table: 1-10413597-5\ncolumns: No. in series, No. in season, Title, Setting, Directed by, Written by, U.S. viewers (million), Original air date\nQ: Who wrote the episode that was set in winter 1541/february 13, 1542?\nA: SELECT Written by FROM 1-10413597-5 WHERE Setting = 'Winter 1541/February 13, 1542'"}
-{"text": "table: 1-10413597-4\ncolumns: No. in series, No. in season, Title, Setting, Directed by, Written by, Original air date\nQ: What episode number of the season was \"The Northern Uprising\"?\nA: SELECT No. in season FROM 1-10413597-4 WHERE Title = '\"The Northern Uprising\"'"}
-{"text": "table: 1-10416547-1\ncolumns: Date, Album name, Track, Track title, Lyricist, Music genre/style, Major instrument(s), Lyrics theme/style, Duration\nQ: What is the name of the track that lasts 5:30?\nA: SELECT Track title FROM 1-10416547-1 WHERE Duration = '5:30'"}
-{"text": "table: 1-10416547-1\ncolumns: Date, Album name, Track, Track title, Lyricist, Music genre/style, Major instrument(s), Lyrics theme/style, Duration\nQ: What is the album namethat has the track title Sweetness \u751c\u751c\u7684 (ti\u00e1n ti\u00e1n de)?\nA: SELECT Album name FROM 1-10416547-1 WHERE Track title = 'Sweetness \u751c\u751c\u7684 (Ti\u00e1n ti\u00e1n de)'"}
-{"text": "table: 1-10416547-1\ncolumns: Date, Album name, Track, Track title, Lyricist, Music genre/style, Major instrument(s), Lyrics theme/style, Duration\nQ: What is the duration of the song where the major instrument is the piano and the date is 2004-02-03?\nA: SELECT Duration FROM 1-10416547-1 WHERE Major instrument(s) = 'Piano' AND Date = '2004-02-03'"}
-{"text": "table: 1-10416547-1\ncolumns: Date, Album name, Track, Track title, Lyricist, Music genre/style, Major instrument(s), Lyrics theme/style, Duration\nQ: What is the total number of lyricist where the lyrics theme is romance and the song lasts 3:50?\nA: SELECT COUNT Lyricist FROM 1-10416547-1 WHERE Lyrics theme/style = 'Romance' AND Duration = '3:50'"}
-{"text": "table: 1-10416547-1\ncolumns: Date, Album name, Track, Track title, Lyricist, Music genre/style, Major instrument(s), Lyrics theme/style, Duration\nQ: What is the major instrument of the song that lasts 4:32? \nA: SELECT Major instrument(s) FROM 1-10416547-1 WHERE Duration = '4:32'"}
-{"text": "table: 1-10416547-1\ncolumns: Date, Album name, Track, Track title, Lyricist, Music genre/style, Major instrument(s), Lyrics theme/style, Duration\nQ: What is the total number of music genre/style in which the lyrics are a detective story?\nA: SELECT COUNT Music genre/style FROM 1-10416547-1 WHERE Lyrics theme/style = 'Detective story'"}
-{"text": "table: 1-1046071-1\ncolumns: Year, Division, League, Regular Season, Playoffs, Open Cup\nQ: What is the playoffs for the usl pro select league?\nA: SELECT Playoffs FROM 1-1046071-1 WHERE League = 'USL Pro Select League'"}
-{"text": "table: 1-1046071-1\ncolumns: Year, Division, League, Regular Season, Playoffs, Open Cup\nQ: What is the number of the division for the 1st round?\nA: SELECT COUNT Division FROM 1-1046071-1 WHERE Open Cup = '1st Round'"}
-{"text": "table: 1-10420426-1\ncolumns: Season, Series, Team, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: What was the team where series is formula renault 2.0 nec?\nA: SELECT Team FROM 1-10420426-1 WHERE Series = 'Formula Renault 2.0 NEC'"}
-{"text": "table: 1-10420426-1\ncolumns: Season, Series, Team, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: What is the total number of poles for arden international?\nA: SELECT COUNT Poles FROM 1-10420426-1 WHERE Team = 'Arden International'"}
-{"text": "table: 1-10420426-1\ncolumns: Season, Series, Team, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: What is the number of wins for gp2 series for racing engineering?\nA: SELECT COUNT Wins FROM 1-10420426-1 WHERE Series = 'GP2 Series' AND Team = 'Racing Engineering'"}
-{"text": "table: 1-10420426-1\ncolumns: Season, Series, Team, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: What is the number of podiums for season 2010 for campionato italiano superstars.\nA: SELECT COUNT Podiums FROM 1-10420426-1 WHERE Season = '2010' AND Series = 'Campionato Italiano Superstars'"}
-{"text": "table: 1-10420426-1\ncolumns: Season, Series, Team, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: What is the podium for 144 points?\nA: SELECT Podiums FROM 1-10420426-1 WHERE Points = 144"}
-{"text": "table: 1-10470082-3\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: How many writers had an US air date of september 25, 1993?\nA: SELECT COUNT Writer FROM 1-10470082-3 WHERE US air date = 'September 25, 1993'"}
-{"text": "table: 1-10470082-3\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: How many villians were in No. 25?\nA: SELECT COUNT Villains FROM 1-10470082-3 WHERE No. = 25"}
-{"text": "table: 1-1046454-1\ncolumns: Year, Division, League, Regular Season, Playoffs, Open Cup\nQ: what being the maximum\u00a0year\u00a0where\u00a0regular season\u00a0is 4th, northwest\nA: SELECT MAX Year FROM 1-1046454-1 WHERE Regular Season = '4th, Northwest'"}
-{"text": "table: 1-1046454-1\ncolumns: Year, Division, League, Regular Season, Playoffs, Open Cup\nQ: what is the total number of\u00a0playoffs\u00a0where\u00a0regular season\u00a0is 6th, southwest\nA: SELECT COUNT Playoffs FROM 1-1046454-1 WHERE Regular Season = '6th, Southwest'"}
-{"text": "table: 1-1046454-1\ncolumns: Year, Division, League, Regular Season, Playoffs, Open Cup\nQ: what is the maximum\u00a0division\nA: SELECT MAX Division FROM 1-1046454-1"}
-{"text": "table: 1-1046454-1\ncolumns: Year, Division, League, Regular Season, Playoffs, Open Cup\nQ: what's the\u00a0league\u00a0where\u00a0regular season\u00a0is 2nd, northwest\nA: SELECT League FROM 1-1046454-1 WHERE Regular Season = '2nd, Northwest'"}
-{"text": "table: 1-1046454-1\ncolumns: Year, Division, League, Regular Season, Playoffs, Open Cup\nQ: what are all the regular season where year is 2011\nA: SELECT Regular Season FROM 1-1046454-1 WHERE Year = 2011"}
-{"text": "table: 1-10470082-5\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: How many titles have the number 11\nA: SELECT COUNT Title FROM 1-10470082-5 WHERE # = 11"}
-{"text": "table: 1-10470082-5\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: How many have Mrs. briar as a villain\nA: SELECT COUNT No. FROM 1-10470082-5 WHERE Villains = 'Mrs. Briar'"}
-{"text": "table: 1-10470082-5\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: how many have the number 8\nA: SELECT COUNT No. FROM 1-10470082-5 WHERE # = 8"}
-{"text": "table: 1-10470082-5\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: How many have the title \"the tale of the room for rent\"\nA: SELECT COUNT # FROM 1-10470082-5 WHERE Title = '\"The Tale of the Room for Rent\"'"}
-{"text": "table: 1-10470082-6\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: What is the name of the episode told by Kiki and directed by Will Dixon?\nA: SELECT Title FROM 1-10470082-6 WHERE Storyteller = 'Kiki' AND Director = 'Will Dixon'"}
-{"text": "table: 1-10470082-6\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: Who is the storyteller in the episode called \"The Tale of the Jagged Sign\"?\nA: SELECT Storyteller FROM 1-10470082-6 WHERE Title = '\"The Tale of the Jagged Sign\"'"}
-{"text": "table: 1-10470082-6\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: Who wrote Episode #3?\nA: SELECT Writer FROM 1-10470082-6 WHERE # = 3"}
-{"text": "table: 1-10470082-7\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: Who are the villains in the episode titled \"The Tale of the Forever Game\"?\nA: SELECT Villains FROM 1-10470082-7 WHERE Title = '\"The Tale of The Forever Game\"'"}
-{"text": "table: 1-10470082-7\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: How many villains appeared in the episode titled \"The Tale of the Virtual Pets\"?\nA: SELECT COUNT Villains FROM 1-10470082-7 WHERE Title = '\"The Tale of The Virtual Pets\"'"}
-{"text": "table: 1-10470082-7\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: Who are the villains in the episodes where Megan is the storyteller and Lorette LeBlanc is the director?\nA: SELECT Villains FROM 1-10470082-7 WHERE Storyteller = 'Megan' AND Director = 'Lorette LeBlanc'"}
-{"text": "table: 1-10470082-7\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: What is the largest # for an episode that was written by Allison Lea Bingeman?\nA: SELECT MAX # FROM 1-10470082-7 WHERE Writer = 'Allison Lea Bingeman'"}
-{"text": "table: 1-10477224-1\ncolumns: Sepal length, Sepal width, Petal length, Petal width, Species\nQ: Name the species when petal width is 2.0 and petal length is 4.9\nA: SELECT Species FROM 1-10477224-1 WHERE Petal width = '2.0' AND Petal length = '4.9'"}
-{"text": "table: 1-10477224-1\ncolumns: Sepal length, Sepal width, Petal length, Petal width, Species\nQ: Name the sepal width for i.virginica with petal length of 5.1\nA: SELECT Sepal width FROM 1-10477224-1 WHERE Species = 'I.virginica' AND Petal length = '5.1'"}
-{"text": "table: 1-10477224-1\ncolumns: Sepal length, Sepal width, Petal length, Petal width, Species\nQ: Name the number of species with sepal width of 3.4 and sepal length of 5.4\nA: SELECT COUNT Species FROM 1-10477224-1 WHERE Sepal width = '3.4' AND Sepal length = '5.4'"}
-{"text": "table: 1-10477224-1\ncolumns: Sepal length, Sepal width, Petal length, Petal width, Species\nQ: Name the sepal length for sepal width of 2.8 and petal length of 5.1\nA: SELECT Sepal length FROM 1-10477224-1 WHERE Sepal width = '2.8' AND Petal length = '5.1'"}
-{"text": "table: 1-10477224-1\ncolumns: Sepal length, Sepal width, Petal length, Petal width, Species\nQ: Name the sepal width when sepal length is 6.5 and petal width is 2.2\nA: SELECT Sepal width FROM 1-10477224-1 WHERE Sepal length = '6.5' AND Petal width = '2.2'"}
-{"text": "table: 1-10477224-1\ncolumns: Sepal length, Sepal width, Petal length, Petal width, Species\nQ: Name the sepal lengh when sepal width is 2.9 and petal width 1.3\nA: SELECT Sepal length FROM 1-10477224-1 WHERE Sepal width = '2.9' AND Petal width = '1.3'"}
-{"text": "table: 1-10470082-4\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: Who is the director and what number is the episode for episode #1 of Are You Afraid of the Dark season 3?\nA: SELECT COUNT Director FROM 1-10470082-4 WHERE # = 1"}
-{"text": "table: 1-10470082-4\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: Who is the director of the episode whom Scott Peters is the writer?\nA: SELECT Director FROM 1-10470082-4 WHERE Writer = 'Scott Peters'"}
-{"text": "table: 1-10470082-4\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: Who is the villain in episode #7?\nA: SELECT Villains FROM 1-10470082-4 WHERE # = 7"}
-{"text": "table: 1-10470082-8\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: Who wrote episode #1 in season 7?\nA: SELECT COUNT Writer FROM 1-10470082-8 WHERE # = 1"}
-{"text": "table: 1-10470082-8\ncolumns: No., #, Title, Director, Writer, US air date, Storyteller, Villains\nQ: When did the episode written by Jim Morris air?\nA: SELECT US air date FROM 1-10470082-8 WHERE Writer = 'Jim Morris'"}
-{"text": "table: 1-10527215-3\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: What was Datsun Twin 200's fastest lap?\nA: SELECT Fastest Lap FROM 1-10527215-3 WHERE Name = 'Datsun Twin 200'"}
-{"text": "table: 1-10527215-3\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: In the Datsun Twin 200 race, what was the fastest lap?\nA: SELECT Fastest Lap FROM 1-10527215-3 WHERE Name = 'Datsun Twin 200'"}
-{"text": "table: 1-10527215-3\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: What's the report for the True Value 500?\nA: SELECT Report FROM 1-10527215-3 WHERE Name = 'True Value 500'"}
-{"text": "table: 1-10527215-3\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: What was Johnny Rutherford's fastest lap while Al Unser was the pole position?\nA: SELECT Fastest Lap FROM 1-10527215-3 WHERE Winning driver = 'Johnny Rutherford' AND Pole Position = 'Al Unser'"}
-{"text": "table: 1-10527215-3\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: What's the report on Penske Racing winning while the pole position was Al Unser?\nA: SELECT COUNT Report FROM 1-10527215-3 WHERE Pole Position = 'Al Unser' AND Winning team = 'Penske Racing'"}
-{"text": "table: 1-104858-1\ncolumns: Country, Membership (from 2010), Name of member organization, Year current Scouting organization joined WOSM, Year member organization was founded, Admits boys/girls\nQ: Which countries have a scouting organization that was founded in 1926, and joined WOSM in 1930?\nA: SELECT Country FROM 1-104858-1 WHERE Year current Scouting organization joined WOSM = '1930' AND Year member organization was founded = '1926'"}
-{"text": "table: 1-104858-1\ncolumns: Country, Membership (from 2010), Name of member organization, Year current Scouting organization joined WOSM, Year member organization was founded, Admits boys/girls\nQ: Does Venezuela admit only boys, only girls, or both?\nA: SELECT Admits boys/girls FROM 1-104858-1 WHERE Country = 'Venezuela'"}
-{"text": "table: 1-104858-1\ncolumns: Country, Membership (from 2010), Name of member organization, Year current Scouting organization joined WOSM, Year member organization was founded, Admits boys/girls\nQ: Which organizations were founded in 1972, but became WOSM members until 1977?\nA: SELECT Name of member organization FROM 1-104858-1 WHERE Year member organization was founded = '1972' AND Year current Scouting organization joined WOSM = '1977'"}
-{"text": "table: 1-104858-1\ncolumns: Country, Membership (from 2010), Name of member organization, Year current Scouting organization joined WOSM, Year member organization was founded, Admits boys/girls\nQ: Does the Scout Association of Hong Kong admit boys, girls, or both?\nA: SELECT Admits boys/girls FROM 1-104858-1 WHERE Name of member organization = 'The Scout Association of Hong Kong'"}
-{"text": "table: 1-104858-1\ncolumns: Country, Membership (from 2010), Name of member organization, Year current Scouting organization joined WOSM, Year member organization was founded, Admits boys/girls\nQ: Does the Ghana Scout Association (founded in 1912) admit boys, girls, or both?\nA: SELECT Admits boys/girls FROM 1-104858-1 WHERE Year member organization was founded = '1912' AND Name of member organization = 'The Ghana Scout Association'"}
-{"text": "table: 1-10528691-4\ncolumns: Model, Introduction, Discontinued, CPU Speed, Print resolution (DPI) Resolution is given in dots per inch (DPI), Print speed (PPM), Standard memory, Maximum memory\nQ: What is the model number introduced May 1999?\nA: SELECT MAX Model FROM 1-10528691-4 WHERE Introduction = 'May 1999'"}
-{"text": "table: 1-10528691-4\ncolumns: Model, Introduction, Discontinued, CPU Speed, Print resolution (DPI) Resolution is given in dots per inch (DPI), Print speed (PPM), Standard memory, Maximum memory\nQ: What is the print resolution (FPI) for December 2002?\nA: SELECT Print resolution (DPI) Resolution is given in dots per inch (DPI) FROM 1-10528691-4 WHERE Introduction = 'December 2002'"}
-{"text": "table: 1-10528691-4\ncolumns: Model, Introduction, Discontinued, CPU Speed, Print resolution (DPI) Resolution is given in dots per inch (DPI), Print speed (PPM), Standard memory, Maximum memory\nQ: What is the maximum memory for the model discontinued in November 2001?\nA: SELECT Maximum memory FROM 1-10528691-4 WHERE Discontinued = 'November 2001'"}
-{"text": "table: 1-1053802-1\ncolumns: Region/country, Local title, Network, Winners, Main presenters\nQ: What is main presenters of La Granja?\nA: SELECT Main presenters FROM 1-1053802-1 WHERE Local title = 'La Granja'"}
-{"text": "table: 1-1053802-1\ncolumns: Region/country, Local title, Network, Winners, Main presenters\nQ: What is the main presenter of bulgaria?\nA: SELECT Main presenters FROM 1-1053802-1 WHERE Region/country = 'Bulgaria'"}
-{"text": "table: 1-1053802-1\ncolumns: Region/country, Local title, Network, Winners, Main presenters\nQ: How many winners are there of farma?\nA: SELECT COUNT Winners FROM 1-1053802-1 WHERE Local title = 'Farma'"}
-{"text": "table: 1-10556257-1\ncolumns: Season, Team, League Apps, League Goals, Cup Apps, Cup Goals\nQ: What is the most cup goals for seasson 1911-12?\nA: SELECT MAX Cup Goals FROM 1-10556257-1 WHERE Season = '1911-12'"}
-{"text": "table: 1-10556257-1\ncolumns: Season, Team, League Apps, League Goals, Cup Apps, Cup Goals\nQ: What is the league apps for season 1923-24?\nA: SELECT League Apps FROM 1-10556257-1 WHERE Season = '1923-24'"}
-{"text": "table: 1-10556257-1\ncolumns: Season, Team, League Apps, League Goals, Cup Apps, Cup Goals\nQ: What is the team for season 1911-12?\nA: SELECT Team FROM 1-10556257-1 WHERE Season = '1911-12'"}
-{"text": "table: 1-10566855-1\ncolumns: Season, Premier, Runner-up, Score, Margin, Venue, Attendance\nQ: what's the minimum\u00a0attendance\u00a0with\u00a0score\u00a0 10.16 (76) \u2013 9.22 (76)\nA: SELECT MIN Attendance FROM 1-10566855-1 WHERE Score = '10.16 (76) \u2013 9.22 (76)'"}
-{"text": "table: 1-10566855-1\ncolumns: Season, Premier, Runner-up, Score, Margin, Venue, Attendance\nQ: who's the\u00a0premier\u00a0with\u00a0in 1970\nA: SELECT Premier FROM 1-10566855-1 WHERE Season = 1970"}
-{"text": "table: 1-10566855-1\ncolumns: Season, Premier, Runner-up, Score, Margin, Venue, Attendance\nQ: who are all the runner-up for premier in richmond\nA: SELECT Runner-up FROM 1-10566855-1 WHERE Premier = 'Richmond'"}
-{"text": "table: 1-10566855-1\ncolumns: Season, Premier, Runner-up, Score, Margin, Venue, Attendance\nQ: what is the minimum attendance with score 8.16 (64) \u2013 8.12 (60)\nA: SELECT MIN Attendance FROM 1-10566855-1 WHERE Score = '8.16 (64) \u2013 8.12 (60)'"}
-{"text": "table: 1-10568553-1\ncolumns: County, Location, Street Names, Milepost, Roads Intersected, Notes\nQ: How many mileposts are there on Anne Street?\nA: SELECT COUNT Milepost FROM 1-10568553-1 WHERE Street Names = 'Anne Street'"}
-{"text": "table: 1-10568553-1\ncolumns: County, Location, Street Names, Milepost, Roads Intersected, Notes\nQ: Which street is 12.2 miles long?\nA: SELECT Street Names FROM 1-10568553-1 WHERE Milepost = '12.2'"}
-{"text": "table: 1-10568553-1\ncolumns: County, Location, Street Names, Milepost, Roads Intersected, Notes\nQ: Where does Route 24 intersect?\nA: SELECT Location FROM 1-10568553-1 WHERE Roads Intersected = 'Route 24'"}
-{"text": "table: 1-10568553-1\ncolumns: County, Location, Street Names, Milepost, Roads Intersected, Notes\nQ: Where is milepost 12.8?\nA: SELECT Location FROM 1-10568553-1 WHERE Milepost = '12.8'"}
-{"text": "table: 1-1057262-1\ncolumns: Commodity, 2001-02, 2002-03, 2003-04, 2004-05, 2005-06, 2006-07\nQ: What is the minimum amount for wool for 2001-02?\nA: SELECT MIN 2001-02 FROM 1-1057262-1 WHERE Commodity = 'Wool'"}
-{"text": "table: 1-1057316-1\ncolumns: Serial number, Wheel arrangement ( Whyte notation ), Build date, Operational owner(s), Disposition\nQ: Who were the operational owners during the construction date of April 1892?\nA: SELECT Operational owner(s) FROM 1-1057316-1 WHERE Build date = 'April 1892'"}
-{"text": "table: 1-1057316-1\ncolumns: Serial number, Wheel arrangement ( Whyte notation ), Build date, Operational owner(s), Disposition\nQ: Where can you find Colorado and Southern Railway #9?\nA: SELECT Disposition FROM 1-1057316-1 WHERE Operational owner(s) = 'Colorado and Southern Railway #9'"}
-{"text": "table: 1-1057316-1\ncolumns: Serial number, Wheel arrangement ( Whyte notation ), Build date, Operational owner(s), Disposition\nQ: What is the wheel arrangement for the train in Riverdale, Georgia?\nA: SELECT Wheel arrangement ( Whyte notation ) FROM 1-1057316-1 WHERE Disposition = 'Riverdale, Georgia'"}
-{"text": "table: 1-1057316-1\ncolumns: Serial number, Wheel arrangement ( Whyte notation ), Build date, Operational owner(s), Disposition\nQ: When was the train 2053 built?\nA: SELECT Build date FROM 1-1057316-1 WHERE Serial number = '2053'"}
-{"text": "table: 1-1057316-1\ncolumns: Serial number, Wheel arrangement ( Whyte notation ), Build date, Operational owner(s), Disposition\nQ: How many wheels does the train owned by Texas and New Orleans Railroad #319 have?\nA: SELECT COUNT Wheel arrangement ( Whyte notation ) FROM 1-1057316-1 WHERE Operational owner(s) = 'Texas and New Orleans Railroad #319'"}
-{"text": "table: 1-10577579-3\ncolumns: Institution, Location, Men\u2019s Nickname, Women\u2019s Nickname, Founded, Type, Enrollment, Joined, Left, Current Conference, Classification\nQ: Which college has the men's nickname of the blazers?\nA: SELECT Institution FROM 1-10577579-3 WHERE Men\u2019s Nickname = 'Blazers'"}
-{"text": "table: 1-10577579-3\ncolumns: Institution, Location, Men\u2019s Nickname, Women\u2019s Nickname, Founded, Type, Enrollment, Joined, Left, Current Conference, Classification\nQ: Name the joined for the wolfpack women's nickname\nA: SELECT Joined FROM 1-10577579-3 WHERE Women\u2019s Nickname = 'Wolfpack'"}
-{"text": "table: 1-10577579-3\ncolumns: Institution, Location, Men\u2019s Nickname, Women\u2019s Nickname, Founded, Type, Enrollment, Joined, Left, Current Conference, Classification\nQ: Name the left of the Lady Pilots.\nA: SELECT Left FROM 1-10577579-3 WHERE Women\u2019s Nickname = 'Lady Pilots'"}
-{"text": "table: 1-10577579-3\ncolumns: Institution, Location, Men\u2019s Nickname, Women\u2019s Nickname, Founded, Type, Enrollment, Joined, Left, Current Conference, Classification\nQ: Name the women's nickname when the enrollment is 1500 in mobile, Alabama.\nA: SELECT Women\u2019s Nickname FROM 1-10577579-3 WHERE Enrollment = 1500 AND Location = 'Mobile, Alabama'"}
-{"text": "table: 1-10577579-3\ncolumns: Institution, Location, Men\u2019s Nickname, Women\u2019s Nickname, Founded, Type, Enrollment, Joined, Left, Current Conference, Classification\nQ: Which conference is in Jackson, Mississippi?\nA: SELECT Current Conference FROM 1-10577579-3 WHERE Location = 'Jackson, Mississippi'"}
-{"text": "table: 1-10577579-3\ncolumns: Institution, Location, Men\u2019s Nickname, Women\u2019s Nickname, Founded, Type, Enrollment, Joined, Left, Current Conference, Classification\nQ: What is the men's nickname at the school that has the lady wildcats women's nickname?\nA: SELECT Men\u2019s Nickname FROM 1-10577579-3 WHERE Women\u2019s Nickname = 'Lady Wildcats'"}
-{"text": "table: 1-10577579-2\ncolumns: Institution, Location, Mens Nickname, Womens Nickname, Founded, Type, Enrollment, Joined\nQ: What is the Mens Nickname for the member location of Jacksonville, florida?\nA: SELECT Mens Nickname FROM 1-10577579-2 WHERE Location = 'Jacksonville, Florida'"}
-{"text": "table: 1-10577579-2\ncolumns: Institution, Location, Mens Nickname, Womens Nickname, Founded, Type, Enrollment, Joined\nQ: What is the enrollment for the institution that was founded in 1866 and is a private/(african methodist) type?\nA: SELECT MAX Enrollment FROM 1-10577579-2 WHERE Founded = 1866 AND Type = 'Private/(African Methodist)'"}
-{"text": "table: 1-10577579-2\ncolumns: Institution, Location, Mens Nickname, Womens Nickname, Founded, Type, Enrollment, Joined\nQ: That is the year founded for the institution location of Nashville, Tennessee?\nA: SELECT MIN Founded FROM 1-10577579-2 WHERE Location = 'Nashville, Tennessee'"}
-{"text": "table: 1-10577579-2\ncolumns: Institution, Location, Mens Nickname, Womens Nickname, Founded, Type, Enrollment, Joined\nQ: What is the year the institution Tougaloo College joined?\nA: SELECT Joined FROM 1-10577579-2 WHERE Institution = 'Tougaloo College'"}
-{"text": "table: 1-10592536-8\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment, Position in table\nQ: What is the date of vacancy when the date of appointment is 28 november 2007 and replaced by is alex mcleish?\nA: SELECT Date of vacancy FROM 1-10592536-8 WHERE Date of appointment = '28 November 2007' AND Replaced by = 'Alex McLeish'"}
-{"text": "table: 1-10592536-8\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment, Position in table\nQ: What is the date of appointment when the date of vacancy is 21 december 2007?\nA: SELECT Date of appointment FROM 1-10592536-8 WHERE Date of vacancy = '21 December 2007'"}
-{"text": "table: 1-10592536-8\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment, Position in table\nQ: Who replaced when team is wigan athletic?\nA: SELECT Replaced by FROM 1-10592536-8 WHERE Team = 'Wigan Athletic'"}
-{"text": "table: 1-10592536-8\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment, Position in table\nQ: What is the date of vacancy when the team is manchester city and replaced by is mark hughes?\nA: SELECT Date of vacancy FROM 1-10592536-8 WHERE Team = 'Manchester City' AND Replaced by = 'Mark Hughes'"}
-{"text": "table: 1-10592536-8\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment, Position in table\nQ: What is the date of appointment when replaced by is roy hodgson?\nA: SELECT Date of appointment FROM 1-10592536-8 WHERE Replaced by = 'Roy Hodgson'"}
-{"text": "table: 1-10592536-8\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment, Position in table\nQ: Who replaced when position in table is pre-season?\nA: SELECT Replaced by FROM 1-10592536-8 WHERE Position in table = 'Pre-season'"}
-{"text": "table: 1-1059743-1\ncolumns: Rank, Member Association, Points, Group stage, Play-off, AFC Cup\nQ: How many games had a score value of 813.5 in post-season play?\nA: SELECT COUNT Play-off FROM 1-1059743-1 WHERE Points = '813.5'"}
-{"text": "table: 1-1059743-1\ncolumns: Rank, Member Association, Points, Group stage, Play-off, AFC Cup\nQ: Did any team score games that totaled up to 860.5?\nA: SELECT Play-off FROM 1-1059743-1 WHERE Points = '860.5'"}
-{"text": "table: 1-10595672-1\ncolumns: Date, Opponent, Home / Away, Score, High points, High rebounds, High assists, Location/Attendance, Record\nQ: What was the score of the game when the team reached a record of 6-9?\nA: SELECT Score FROM 1-10595672-1 WHERE Record = '6-9'"}
-{"text": "table: 1-10581768-2\ncolumns: Institution, Nickname, Location, Founded, Type, Enrollment\nQ: What type institution is point park university\nA: SELECT Type FROM 1-10581768-2 WHERE Institution = 'Point Park University'"}
-{"text": "table: 1-10581768-2\ncolumns: Institution, Nickname, Location, Founded, Type, Enrollment\nQ: How many institutions are located in wilmore, kentucky and private\nA: SELECT MAX Founded FROM 1-10581768-2 WHERE Type = 'Private' AND Location = 'Wilmore, Kentucky'"}
-{"text": "table: 1-10581768-2\ncolumns: Institution, Nickname, Location, Founded, Type, Enrollment\nQ: point park university is what type of institution\nA: SELECT Type FROM 1-10581768-2 WHERE Institution = 'Point Park University'"}
-{"text": "table: 1-10581768-2\ncolumns: Institution, Nickname, Location, Founded, Type, Enrollment\nQ: how many founded dates are listed for carlow university 1\nA: SELECT COUNT Founded FROM 1-10581768-2 WHERE Institution = 'Carlow University 1'"}
-{"text": "table: 1-10610087-6\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date\nQ: Who wrote the episode titled \"black\"?\nA: SELECT Written by FROM 1-10610087-6 WHERE Title = '\"Black\"'"}
-{"text": "table: 1-10610087-6\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date\nQ: Who are the writers for the episode \"solo\"?\nA: SELECT Written by FROM 1-10610087-6 WHERE Title = '\"Solo\"'"}
-{"text": "table: 1-10610087-3\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date\nQ: what is the original air date of the episode no in season 9?\nA: SELECT Original air date FROM 1-10610087-3 WHERE No. in season = 9"}
-{"text": "table: 1-10610087-3\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date\nQ: What is the title of the episode written by denis leary, peter tolan and evan reilly?\nA: SELECT Title FROM 1-10610087-3 WHERE Written by = 'Denis Leary, Peter Tolan and Evan Reilly'"}
-{"text": "table: 1-10610087-3\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date\nQ: How many episodes were titles \"voicemail\"?\nA: SELECT COUNT Directed by FROM 1-10610087-3 WHERE Title = '\"Voicemail\"'"}
-{"text": "table: 1-10602294-1\ncolumns: Name, Dates active, Peak classification, Windspeeds, Pressure, Areas affected, Damage (USD), Deaths, Refs\nQ: When was Kamba active?\nA: SELECT Dates active FROM 1-10602294-1 WHERE Name = 'Kamba'"}
-{"text": "table: 1-10602294-1\ncolumns: Name, Dates active, Peak classification, Windspeeds, Pressure, Areas affected, Damage (USD), Deaths, Refs\nQ: What was the cyclone's pressure in the storm that death was equal to 95km/h (60mph)?\nA: SELECT Pressure FROM 1-10602294-1 WHERE Deaths = '95km/h (60mph)'"}
-{"text": "table: 1-10602294-1\ncolumns: Name, Dates active, Peak classification, Windspeeds, Pressure, Areas affected, Damage (USD), Deaths, Refs\nQ: What were the active dates for the storm that had 185km/h (115mph) deaths?\nA: SELECT Dates active FROM 1-10602294-1 WHERE Deaths = '185km/h (115mph)'"}
-{"text": "table: 1-10602294-1\ncolumns: Name, Dates active, Peak classification, Windspeeds, Pressure, Areas affected, Damage (USD), Deaths, Refs\nQ: What was the damage (usd) from the cyclones that measured 1003hPa (29.62inHg) pressure?\nA: SELECT Damage (USD) FROM 1-10602294-1 WHERE Pressure = '1003hPa (29.62inHg)'"}
-{"text": "table: 1-10621256-1\ncolumns: Player, Matches, Inns, N/O, Runs, High Score, Average, 100, 50, Catches, Stump\nQ: what's the\u00a0average\u00a0where\u00a0high score\u00a0is 120\nA: SELECT Average FROM 1-10621256-1 WHERE High Score = 120"}
-{"text": "table: 1-10621256-1\ncolumns: Player, Matches, Inns, N/O, Runs, High Score, Average, 100, 50, Catches, Stump\nQ: what's the\u00a0player\u00a0where\u00a050\u00a0is 2 and\u00a0n/o\u00a0is 0\nA: SELECT Player FROM 1-10621256-1 WHERE 50 = 2 AND N/O = 0"}
-{"text": "table: 1-10621256-1\ncolumns: Player, Matches, Inns, N/O, Runs, High Score, Average, 100, 50, Catches, Stump\nQ: what's the\u00a0player\u00a0where\u00a0inns\u00a0is 21\nA: SELECT Player FROM 1-10621256-1 WHERE Inns = 21"}
-{"text": "table: 1-106367-2\ncolumns: General election, # of candidates, # of seats won, % of popular vote, Result\nQ: Which general election had a pq majority and a 44.75% of the popular vote?\nA: SELECT General election FROM 1-106367-2 WHERE Result = 'PQ majority' AND % of popular vote = '44.75%'"}
-{"text": "table: 1-106367-2\ncolumns: General election, # of candidates, # of seats won, % of popular vote, Result\nQ: What is the least number of candidates running were there when 80 seats were won?\nA: SELECT MIN # of candidates FROM 1-106367-2 WHERE # of seats won = 80"}
-{"text": "table: 1-106367-2\ncolumns: General election, # of candidates, # of seats won, % of popular vote, Result\nQ: How many seats were won in the election with 125 candidates?\nA: SELECT COUNT # of seats won FROM 1-106367-2 WHERE # of candidates = 125"}
-{"text": "table: 1-10647639-1\ncolumns: Week, Date, Opponent, Result, Game site, Record, Attendance\nQ: How many weeks are there?\nA: SELECT MAX Week FROM 1-10647639-1"}
-{"text": "table: 1-10647639-1\ncolumns: Week, Date, Opponent, Result, Game site, Record, Attendance\nQ: How many people attended the game against the indianapolis colts?\nA: SELECT COUNT Attendance FROM 1-10647639-1 WHERE Opponent = 'Indianapolis Colts'"}
-{"text": "table: 1-10647639-1\ncolumns: Week, Date, Opponent, Result, Game site, Record, Attendance\nQ: On december 16, 1985, all the records were what?\nA: SELECT Record FROM 1-10647639-1 WHERE Date = 'December 16, 1985'"}
-{"text": "table: 1-10646790-2\ncolumns: Week, Date, Opponent, Result, Stadium, Record, Attendance\nQ: How many results are there for the 0-4 record?\nA: SELECT COUNT Result FROM 1-10646790-2 WHERE Record = '0-4'"}
-{"text": "table: 1-10646790-2\ncolumns: Week, Date, Opponent, Result, Stadium, Record, Attendance\nQ: How many weeks are there that include the date October 11, 1969.\nA: SELECT COUNT Week FROM 1-10646790-2 WHERE Date = 'October 11, 1969'"}
-{"text": "table: 1-10646790-2\ncolumns: Week, Date, Opponent, Result, Stadium, Record, Attendance\nQ: How many weeks are there that include the date November 9, 1969.\nA: SELECT COUNT Week FROM 1-10646790-2 WHERE Date = 'November 9, 1969'"}
-{"text": "table: 1-10646790-2\ncolumns: Week, Date, Opponent, Result, Stadium, Record, Attendance\nQ: How many records are there at the War Memorial Stadium?\nA: SELECT COUNT Record FROM 1-10646790-2 WHERE Stadium = 'War Memorial Stadium'"}
-{"text": "table: 1-10646790-2\ncolumns: Week, Date, Opponent, Result, Stadium, Record, Attendance\nQ: What was the minimum attendance on December 7, 1969?\nA: SELECT MIN Attendance FROM 1-10646790-2 WHERE Date = 'December 7, 1969'"}
-{"text": "table: 1-10647401-1\ncolumns: Week, Opponent, Result, Stadium, Record, Attendance\nQ: What week corresponds to the last one to be played at the memorial stadium?\nA: SELECT MAX Week FROM 1-10647401-1 WHERE Stadium = 'Memorial Stadium'"}
-{"text": "table: 1-10647401-1\ncolumns: Week, Opponent, Result, Stadium, Record, Attendance\nQ: In which stadium is the week 5 game played?\nA: SELECT Stadium FROM 1-10647401-1 WHERE Week = 5"}
-{"text": "table: 1-10664957-2\ncolumns: 1st players choice, 2nd players choice, Probability 1st player wins, Probability 2nd player wins, Probability of a draw\nQ: In Penney's game what is the probability where the 1st player wins if the probability of a draw is 8.28% and the 2nd player chooses B BR?\nA: SELECT Probability 1st player wins FROM 1-10664957-2 WHERE Probability of a draw = '8.28%' AND 2nd players choice = 'B BR'"}
-{"text": "table: 1-10664957-2\ncolumns: 1st players choice, 2nd players choice, Probability 1st player wins, Probability 2nd player wins, Probability of a draw\nQ: If the first player chooses RB B, what is the second player's choices?\nA: SELECT 2nd players choice FROM 1-10664957-2 WHERE 1st players choice = 'RB B'"}
-{"text": "table: 1-10664957-2\ncolumns: 1st players choice, 2nd players choice, Probability 1st player wins, Probability 2nd player wins, Probability of a draw\nQ: What is the probability where the second player wins where their choice is R RB and the first player has a 5.18% chance of winning?\nA: SELECT Probability 2nd player wins FROM 1-10664957-2 WHERE 2nd players choice = 'R RB' AND Probability 1st player wins = '5.18%'"}
-{"text": "table: 1-10664957-2\ncolumns: 1st players choice, 2nd players choice, Probability 1st player wins, Probability 2nd player wins, Probability of a draw\nQ: What are the chances the first player will win if the 2nd player has an 80.11% chance of winning with the choice of R RB?\nA: SELECT Probability 1st player wins FROM 1-10664957-2 WHERE Probability 2nd player wins = '80.11%' AND 2nd players choice = 'R RB'"}
-{"text": "table: 1-10664957-2\ncolumns: 1st players choice, 2nd players choice, Probability 1st player wins, Probability 2nd player wins, Probability of a draw\nQ: What are the chances that player 2 wins if player 1's choice is BB R?\nA: SELECT Probability 2nd player wins FROM 1-10664957-2 WHERE 1st players choice = 'BB R'"}
-{"text": "table: 1-10664957-2\ncolumns: 1st players choice, 2nd players choice, Probability 1st player wins, Probability 2nd player wins, Probability of a draw\nQ: How high is the chance that player 1 wins if player 2 has an 88.29% chance of winning with the choice of R RB?\nA: SELECT Probability 1st player wins FROM 1-10664957-2 WHERE Probability 2nd player wins = '88.29%' AND 2nd players choice = 'R RB'"}
-{"text": "table: 1-10650711-1\ncolumns: Pick #, NFL Team, Player, Position, College\nQ: what is the\u00a0nfl team\u00a0where\u00a0player\u00a0is thane gash\nA: SELECT NFL Team FROM 1-10650711-1 WHERE Player = 'Thane Gash'"}
-{"text": "table: 1-10650711-1\ncolumns: Pick #, NFL Team, Player, Position, College\nQ: what is the maximum\u00a0pick #\u00a0where\u00a0player\u00a0is anthony blaylock\nA: SELECT MAX Pick # FROM 1-10650711-1 WHERE Player = 'Anthony Blaylock'"}
-{"text": "table: 1-10650711-1\ncolumns: Pick #, NFL Team, Player, Position, College\nQ: what's the\u00a0nfl team\u00a0where\u00a0player\u00a0is clifford charlton\nA: SELECT NFL Team FROM 1-10650711-1 WHERE Player = 'Clifford Charlton'"}
-{"text": "table: 1-10650711-1\ncolumns: Pick #, NFL Team, Player, Position, College\nQ: what's the\u00a0position\u00a0where\u00a0player\u00a0is anthony blaylock\nA: SELECT Position FROM 1-10650711-1 WHERE Player = 'Anthony Blaylock'"}
-{"text": "table: 1-10650711-1\ncolumns: Pick #, NFL Team, Player, Position, College\nQ: what is the minimum\u00a0pick #\u00a0where\u00a0position\u00a0is defensive tackle\nA: SELECT MIN Pick # FROM 1-10650711-1 WHERE Position = 'Defensive Tackle'"}
-{"text": "table: 1-1067441-1\ncolumns: Province, Population (2004 estimate), Area (km\u00b2), Density, GDP (2003, PPS in mil. \u20ac ), GDP per cap. (2003, in \u20ac)\nQ: Which province has a density of 971.4?\nA: SELECT Province FROM 1-1067441-1 WHERE Density = '971.4'"}
-{"text": "table: 1-1067441-1\ncolumns: Province, Population (2004 estimate), Area (km\u00b2), Density, GDP (2003, PPS in mil. \u20ac ), GDP per cap. (2003, in \u20ac)\nQ: What is Friesland's gdp per capita?\nA: SELECT MIN GDP per cap. (2003, in \u20ac) FROM 1-1067441-1 WHERE Province = 'Friesland'"}
-{"text": "table: 1-1067441-1\ncolumns: Province, Population (2004 estimate), Area (km\u00b2), Density, GDP (2003, PPS in mil. \u20ac ), GDP per cap. (2003, in \u20ac)\nQ: What is the area of the place that has a population density of 331.4?\nA: SELECT MAX Area (km\u00b2) FROM 1-1067441-1 WHERE Density = '331.4'"}
-{"text": "table: 1-1067441-1\ncolumns: Province, Population (2004 estimate), Area (km\u00b2), Density, GDP (2003, PPS in mil. \u20ac ), GDP per cap. (2003, in \u20ac)\nQ: Which province has a gdp of 38355\u20ac million euros?\nA: SELECT Province FROM 1-1067441-1 WHERE GDP (2003, PPS in mil. \u20ac ) = 38355"}
-{"text": "table: 1-1067441-1\ncolumns: Province, Population (2004 estimate), Area (km\u00b2), Density, GDP (2003, PPS in mil. \u20ac ), GDP per cap. (2003, in \u20ac)\nQ: What is the population estimate for the place that gad a 18496\u20ac million euro gdp?\nA: SELECT Population (2004 estimate) FROM 1-1067441-1 WHERE GDP (2003, PPS in mil. \u20ac ) = 18496"}
-{"text": "table: 1-10701133-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Featured character(s), Original air date, U.S. viewers (million)\nQ: What is the title when original air date is may15,2008?\nA: SELECT Title FROM 1-10701133-1 WHERE Original air date = 'May15,2008'"}
-{"text": "table: 1-10701133-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Featured character(s), Original air date, U.S. viewers (million)\nQ: What is the highest no. in season?\nA: SELECT MAX No. in season FROM 1-10701133-1"}
-{"text": "table: 1-10701133-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Featured character(s), Original air date, U.S. viewers (million)\nQ: Who directed the episode where u.s. viewers (million) is 12.90?\nA: SELECT Directed by FROM 1-10701133-1 WHERE U.S. viewers (million) = '12.90'"}
-{"text": "table: 1-1067134-1\ncolumns: DVD Name, # of Ep, Region 1, Region 2, Region 4\nQ: How many episodes aired in Region 2 beginning May 26, 2008?\nA: SELECT MIN # of Ep FROM 1-1067134-1 WHERE Region 2 = 'May 26, 2008'"}
-{"text": "table: 1-1067134-1\ncolumns: DVD Name, # of Ep, Region 1, Region 2, Region 4\nQ: What date did the DVD for season six come out in region 2?\nA: SELECT Region 2 FROM 1-1067134-1 WHERE DVD Name = 'Season Six'"}
-{"text": "table: 1-1067134-1\ncolumns: DVD Name, # of Ep, Region 1, Region 2, Region 4\nQ: What is the least amount of season epidsodes?\nA: SELECT MIN # of Ep FROM 1-1067134-1"}
-{"text": "table: 1-1067134-1\ncolumns: DVD Name, # of Ep, Region 1, Region 2, Region 4\nQ: What DVD season/name for region 2 was released August 22, 2010?\nA: SELECT DVD Name FROM 1-1067134-1 WHERE Region 2 = 'August 22, 2010'"}
-{"text": "table: 1-10705060-1\ncolumns: Season, Series, Team Name, Races, Poles, Wins, Points, Position\nQ: How many points for 2005?\nA: SELECT COUNT Points FROM 1-10705060-1 WHERE Season = '2005'"}
-{"text": "table: 1-10705060-1\ncolumns: Season, Series, Team Name, Races, Poles, Wins, Points, Position\nQ: what is the score for the dams?\nA: SELECT Points FROM 1-10705060-1 WHERE Team Name = 'DAMS'"}
-{"text": "table: 1-10705060-1\ncolumns: Season, Series, Team Name, Races, Poles, Wins, Points, Position\nQ: how many positions in 2009?\nA: SELECT COUNT Position FROM 1-10705060-1 WHERE Season = '2009'"}
-{"text": "table: 1-10705060-1\ncolumns: Season, Series, Team Name, Races, Poles, Wins, Points, Position\nQ: what is the least number of poles?\nA: SELECT MIN Poles FROM 1-10705060-1"}
-{"text": "table: 1-10705060-1\ncolumns: Season, Series, Team Name, Races, Poles, Wins, Points, Position\nQ: Which series with 62 points?\nA: SELECT Series FROM 1-10705060-1 WHERE Points = 62"}
-{"text": "table: 1-10705060-1\ncolumns: Season, Series, Team Name, Races, Poles, Wins, Points, Position\nQ: What is the total for 10th position?\nA: SELECT COUNT Points FROM 1-10705060-1 WHERE Position = '10th'"}
-{"text": "table: 1-10707142-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Winning driver, Winning team, Report\nQ: how many reports of races took place on october 16?\nA: SELECT COUNT Report FROM 1-10707142-2 WHERE Date = 'October 16'"}
-{"text": "table: 1-10707142-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Winning driver, Winning team, Report\nQ: what is the name of the report that lists the race name as long beach grand prix?\nA: SELECT Report FROM 1-10707142-2 WHERE Race Name = 'Long Beach Grand Prix'"}
-{"text": "table: 1-10707142-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Winning driver, Winning team, Report\nQ: what is the report called where the circuit took place at the nazareth speedway?\nA: SELECT Report FROM 1-10707142-2 WHERE Circuit = 'Nazareth Speedway'"}
-{"text": "table: 1-10707142-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Winning driver, Winning team, Report\nQ: what is the name of the race where newman/haas racing is the winning team and rick mears is at the pole position?\nA: SELECT Race Name FROM 1-10707142-2 WHERE Winning team = 'Newman/Haas Racing' AND Pole position = 'Rick Mears'"}
-{"text": "table: 1-10707142-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Winning driver, Winning team, Report\nQ: meadowlands sports complex is the circuit at which city/location?\nA: SELECT City/Location FROM 1-10707142-2 WHERE Circuit = 'Meadowlands Sports Complex'"}
-{"text": "table: 1-10710364-2\ncolumns: Religious group, Population %, Growth (1991\u20132001), Sex ratio (total), Literacy (%), Work participation (%), Sex ratio (rural), Sex ratio (urban), Sex ratio (child)\nQ: What is the literacy rate for groups that grew 103.1% between 1991 and 2001?\nA: SELECT Literacy (%) FROM 1-10710364-2 WHERE Growth (1991\u20132001) = '103.1%'"}
-{"text": "table: 1-10710364-2\ncolumns: Religious group, Population %, Growth (1991\u20132001), Sex ratio (total), Literacy (%), Work participation (%), Sex ratio (rural), Sex ratio (urban), Sex ratio (child)\nQ: What is the lowest sex ratio in rural areas?\nA: SELECT MIN Sex ratio (rural) FROM 1-10710364-2"}
-{"text": "table: 1-10710364-2\ncolumns: Religious group, Population %, Growth (1991\u20132001), Sex ratio (total), Literacy (%), Work participation (%), Sex ratio (rural), Sex ratio (urban), Sex ratio (child)\nQ: What is the lowest child sex ratio in groups where employment is 31.3%?\nA: SELECT MIN Sex ratio (child) FROM 1-10710364-2 WHERE Work participation (%) = '31.3%'"}
-{"text": "table: 1-10710364-2\ncolumns: Religious group, Population %, Growth (1991\u20132001), Sex ratio (total), Literacy (%), Work participation (%), Sex ratio (rural), Sex ratio (urban), Sex ratio (child)\nQ: What is the population percentage of the group where the rural sex ratio is 953?\nA: SELECT Population % FROM 1-10710364-2 WHERE Sex ratio (rural) = 953"}
-{"text": "table: 1-10715317-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: What is the original air date for the title \"felonious monk\"?\nA: SELECT Original air date FROM 1-10715317-2 WHERE Title = '\"Felonious Monk\"'"}
-{"text": "table: 1-10715317-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: What is the title of the episode directed by Peter Markle and written by Jerry Stahl?\nA: SELECT Title FROM 1-10715317-2 WHERE Directed by = 'Peter Markle' AND Written by = 'Jerry Stahl'"}
-{"text": "table: 1-10715317-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many episodes were titled \"identity crisis\"?\nA: SELECT COUNT Directed by FROM 1-10715317-2 WHERE Title = '\"Identity Crisis\"'"}
-{"text": "table: 1-10716893-3\ncolumns: Year, Network, Host, Pre-race analyst, Lap-by-lap, Color commentator(s), Pit reporters\nQ: Who does the lap-by-lap in 2011?\nA: SELECT Lap-by-lap FROM 1-10716893-3 WHERE Year = 2011"}
-{"text": "table: 1-10716893-3\ncolumns: Year, Network, Host, Pre-race analyst, Lap-by-lap, Color commentator(s), Pit reporters\nQ: Which network has Marty Reid as host and lap-by-lap broadcaster?\nA: SELECT Network FROM 1-10716893-3 WHERE Lap-by-lap = 'Marty Reid' AND Host = 'Marty Reid'"}
-{"text": "table: 1-10716893-3\ncolumns: Year, Network, Host, Pre-race analyst, Lap-by-lap, Color commentator(s), Pit reporters\nQ: How many pre-race analysis occur when Allen Bestwick does the lap-by-lap?\nA: SELECT COUNT Pre-race analyst FROM 1-10716893-3 WHERE Lap-by-lap = 'Allen Bestwick'"}
-{"text": "table: 1-10718192-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: What's the highest season number of an episode in the series?\nA: SELECT MAX No. in season FROM 1-10718192-2"}
-{"text": "table: 1-10718192-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: When did the episode titled \"Lucky Strike\" air for the first time?\nA: SELECT Original air date FROM 1-10718192-2 WHERE Title = '\"Lucky Strike\"'"}
-{"text": "table: 1-10718192-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: Who was the writer of the episode titled \"One Hit Wonder\"?\nA: SELECT Written by FROM 1-10718192-2 WHERE Title = '\"One Hit Wonder\"'"}
-{"text": "table: 1-10718192-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: What's the date of the first airing of the episode with series number 63?\nA: SELECT Original air date FROM 1-10718192-2 WHERE No. in series = 63"}
-{"text": "table: 1-10718525-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many titles got a viewership of 26.53 million?\nA: SELECT COUNT Title FROM 1-10718525-2 WHERE U.S. viewers (millions) = '26.53'"}
-{"text": "table: 1-10718525-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many viewers tuned into the show directed by Matt Earl Beesley?\nA: SELECT U.S. viewers (millions) FROM 1-10718525-2 WHERE Directed by = 'Matt Earl Beesley'"}
-{"text": "table: 1-10718631-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: Who wrote episode 94?\nA: SELECT Written by FROM 1-10718631-2 WHERE No. in series = 94"}
-{"text": "table: 1-10718631-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: Which episode was the number in the season where the number in the season is 10?\nA: SELECT No. in series FROM 1-10718631-2 WHERE No. in season = 10"}
-{"text": "table: 1-10718631-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many episodes were in the season that had the epiosde of \"crow's feet\"?\nA: SELECT No. in season FROM 1-10718631-2 WHERE Title = '\"Crow's Feet\"'"}
-{"text": "table: 1-10718631-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: When did the 113 episode air?\nA: SELECT Original air date FROM 1-10718631-2 WHERE No. in series = 113"}
-{"text": "table: 1-10718631-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many titles were there for the 113 episode?\nA: SELECT COUNT Title FROM 1-10718631-2 WHERE No. in series = 113"}
-{"text": "table: 1-10718984-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: What is the number in the season that Marlene Meyer wrote and 20.49 million people watched?\nA: SELECT MAX No. in season FROM 1-10718984-2 WHERE Written by = 'Marlene Meyer' AND U.S. viewers (millions) = '20.49'"}
-{"text": "table: 1-10718984-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: When did the no. 23 show originally air?\nA: SELECT Original air date FROM 1-10718984-2 WHERE No. in season = 23"}
-{"text": "table: 1-10725629-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Fastest lap, Winning driver, Winning team, Report\nQ: Which circuits had a race on October 4?\nA: SELECT Circuit FROM 1-10725629-2 WHERE Date = 'October 4'"}
-{"text": "table: 1-10725629-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Fastest lap, Winning driver, Winning team, Report\nQ: In which reports does Michael Andretti have the pole position and Galles-Kraco Racing is the winning team?\nA: SELECT Report FROM 1-10725629-2 WHERE Pole position = 'Michael Andretti' AND Winning team = 'Galles-Kraco Racing'"}
-{"text": "table: 1-10725629-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Fastest lap, Winning driver, Winning team, Report\nQ: How many rounds were there of the Bosch Spark Plug Grand Prix?\nA: SELECT COUNT Rnd FROM 1-10725629-2 WHERE Race Name = 'Bosch Spark Plug Grand Prix'"}
-{"text": "table: 1-10725629-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Fastest lap, Winning driver, Winning team, Report\nQ: Which rounds were held on August 9?\nA: SELECT Rnd FROM 1-10725629-2 WHERE Date = 'August 9'"}
-{"text": "table: 1-10725629-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Fastest lap, Winning driver, Winning team, Report\nQ: On how many dates did the Michigan International Speedway hold a round?\nA: SELECT COUNT Date FROM 1-10725629-2 WHERE Circuit = 'Michigan International Speedway'"}
-{"text": "table: 1-10722506-6\ncolumns: Conference, # of Bids, Record, Win %, Round of 32, Sweet Sixteen, Elite Eight, Final Four, Championship Game\nQ: Name the total number of bids of the sun belt conference\nA: SELECT COUNT # of Bids FROM 1-10722506-6 WHERE Conference = 'Sun Belt'"}
-{"text": "table: 1-10722506-6\ncolumns: Conference, # of Bids, Record, Win %, Round of 32, Sweet Sixteen, Elite Eight, Final Four, Championship Game\nQ: Name the round of 32 in conference usa\nA: SELECT Round of 32 FROM 1-10722506-6 WHERE Conference = 'Conference USA'"}
-{"text": "table: 1-10722506-6\ncolumns: Conference, # of Bids, Record, Win %, Round of 32, Sweet Sixteen, Elite Eight, Final Four, Championship Game\nQ: What is the record when round of 32 is 0 and metro atlantic conference?\nA: SELECT Record FROM 1-10722506-6 WHERE Round of 32 = 0 AND Conference = 'Metro Atlantic'"}
-{"text": "table: 1-10722506-6\ncolumns: Conference, # of Bids, Record, Win %, Round of 32, Sweet Sixteen, Elite Eight, Final Four, Championship Game\nQ: What is the number of bids with elite eight larger than 1.0\nA: SELECT COUNT # of Bids FROM 1-10722506-6 WHERE Elite Eight > 1.0"}
-{"text": "table: 1-10749143-2\ncolumns: Series #, Season #, Title, Directed by, Written by, Original air date, Production code, U.S. viewers (millions)\nQ: Who directed the episode with production code 7aff03?\nA: SELECT Directed by FROM 1-10749143-2 WHERE Production code = '7AFF03'"}
-{"text": "table: 1-10749143-2\ncolumns: Series #, Season #, Title, Directed by, Written by, Original air date, Production code, U.S. viewers (millions)\nQ: What is the title of the episode wtih 10.34 million U.S viewers?\nA: SELECT Title FROM 1-10749143-2 WHERE U.S. viewers (millions) = '10.34'"}
-{"text": "table: 1-10748727-1\ncolumns: Season, Series, Team Name, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: What place is the team that completed 6 races?\nA: SELECT Position FROM 1-10748727-1 WHERE Races = 6"}
-{"text": "table: 1-10748727-1\ncolumns: Season, Series, Team Name, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: how much did the british formula three called \"fortec motorsport\" score?\nA: SELECT Points FROM 1-10748727-1 WHERE Series = 'British Formula Three' AND Team Name = 'Fortec Motorsport'"}
-{"text": "table: 1-10748727-1\ncolumns: Season, Series, Team Name, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: how many races were in 2009 with 0 wins?\nA: SELECT Races FROM 1-10748727-1 WHERE Season = '2009' AND Wins = 0"}
-{"text": "table: 1-10748727-1\ncolumns: Season, Series, Team Name, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: What years did art grand prix compete?\nA: SELECT Season FROM 1-10748727-1 WHERE Team Name = 'ART Grand Prix'"}
-{"text": "table: 1-10748727-1\ncolumns: Season, Series, Team Name, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: What year had a score of 9?\nA: SELECT Season FROM 1-10748727-1 WHERE Points = '9'"}
-{"text": "table: 1-10748727-1\ncolumns: Season, Series, Team Name, Races, Wins, Poles, F/Laps, Podiums, Points, Position\nQ: what is the greatest number of wins by japanese formula three?\nA: SELECT MAX Wins FROM 1-10748727-1 WHERE Series = 'Japanese Formula Three'"}
-{"text": "table: 1-10749367-3\ncolumns: #, Air Date, Challenge, Winner, Test-taker, Passed?\nQ: Who took test #4?\nA: SELECT Test-taker FROM 1-10749367-3 WHERE # = 4"}
-{"text": "table: 1-10749367-3\ncolumns: #, Air Date, Challenge, Winner, Test-taker, Passed?\nQ: What episode aired on 18 April 2007?\nA: SELECT MIN # FROM 1-10749367-3 WHERE Air Date = '18 April 2007'"}
-{"text": "table: 1-10749367-3\ncolumns: #, Air Date, Challenge, Winner, Test-taker, Passed?\nQ: Who had the challenge of night driving?\nA: SELECT Test-taker FROM 1-10749367-3 WHERE Challenge = 'Night Driving'"}
-{"text": "table: 1-10798928-1\ncolumns: Year (Ceremony), Film title used in nomination, Original title, Director, Result\nQ: How many directors were there for the film Course Completed?\nA: SELECT COUNT Director FROM 1-10798928-1 WHERE Film title used in nomination = 'Course Completed'"}
-{"text": "table: 1-10798928-1\ncolumns: Year (Ceremony), Film title used in nomination, Original title, Director, Result\nQ: Who directed El Nido?\nA: SELECT Director FROM 1-10798928-1 WHERE Original title = 'El nido'"}
-{"text": "table: 1-10798928-1\ncolumns: Year (Ceremony), Film title used in nomination, Original title, Director, Result\nQ: Who directed Dulcinea?\nA: SELECT Director FROM 1-10798928-1 WHERE Original title = 'Dulcinea'"}
-{"text": "table: 1-10797463-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: What are the slovenian names of the villages that had 65.9% of slovenes in 1951?\nA: SELECT Village (Slovenian) FROM 1-10797463-1 WHERE Percent of Slovenes 1951 = '65.9%'"}
-{"text": "table: 1-10797463-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: What are the slovenian names of the villages that had 16.7% of slovenes in 1991?\nA: SELECT Village (Slovenian) FROM 1-10797463-1 WHERE Percent of Slovenes 1991 = '16.7%'"}
-{"text": "table: 1-10797463-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: How many villages had 21.7% of slovenes in 1991?\nA: SELECT COUNT Village (German) FROM 1-10797463-1 WHERE Percent of Slovenes 1991 = '21.7%'"}
-{"text": "table: 1-10797463-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: what percent of slovenes did the village called \u010dahor\u010de in slovenian have in 1991?\nA: SELECT Percent of Slovenes 1991 FROM 1-10797463-1 WHERE Village (Slovenian) = '\u010cahor\u010de'"}
-{"text": "table: 1-10797463-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: What is the slovenian name for the village that in german is known as st.margarethen?\nA: SELECT Village (Slovenian) FROM 1-10797463-1 WHERE Village (German) = 'St.Margarethen'"}
-{"text": "table: 1-10812293-4\ncolumns: Game, Date, Team, Score, High points, High rebounds, High assists, Location Attendance, Record\nQ: For games on December 20, how many points did the scoring leaders get?\nA: SELECT High points FROM 1-10812293-4 WHERE Date = 'December 20'"}
-{"text": "table: 1-10812293-4\ncolumns: Game, Date, Team, Score, High points, High rebounds, High assists, Location Attendance, Record\nQ: Who was the scoring leader and how many points did he get in games on December 23?\nA: SELECT High points FROM 1-10812293-4 WHERE Date = 'December 23'"}
-{"text": "table: 1-10812938-3\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the pick # for the position de?\nA: SELECT Pick # FROM 1-10812938-3 WHERE Position = 'DE'"}
-{"text": "table: 1-10812938-3\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: Which player went to college at Saint Mary's?\nA: SELECT Player FROM 1-10812938-3 WHERE College = 'Saint Mary's'"}
-{"text": "table: 1-10812938-3\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the position for the player with cfl team Winnipeg blue bombers?\nA: SELECT Position FROM 1-10812938-3 WHERE CFL Team = 'Winnipeg Blue Bombers'"}
-{"text": "table: 1-10812938-3\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: Which player went to college at Laval?\nA: SELECT Player FROM 1-10812938-3 WHERE College = 'Laval'"}
-{"text": "table: 1-10812938-3\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What was the college for the player with the cfl team of Edmonton Eskimos (via calgary)?\nA: SELECT College FROM 1-10812938-3 WHERE CFL Team = 'Edmonton Eskimos (via Calgary)'"}
-{"text": "table: 1-10812938-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What's the team of the player from St. Francis Xavier College?\nA: SELECT CFL Team FROM 1-10812938-5 WHERE College = 'St. Francis Xavier'"}
-{"text": "table: 1-10812938-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What player is on the Montreal Alouettes CFl team?\nA: SELECT Player FROM 1-10812938-5 WHERE CFL Team = 'Montreal Alouettes'"}
-{"text": "table: 1-10812938-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What's the pick number of the player from Toronto Argonauts?\nA: SELECT MIN Pick # FROM 1-10812938-5 WHERE CFL Team = 'Toronto Argonauts'"}
-{"text": "table: 1-10812938-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What's the pick number of the player whose position is CB?\nA: SELECT Pick # FROM 1-10812938-5 WHERE Position = 'CB'"}
-{"text": "table: 1-10812938-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What's the pick number of the player from New Mexico?\nA: SELECT MAX Pick # FROM 1-10812938-5 WHERE College = 'New Mexico'"}
-{"text": "table: 1-10812938-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What player went to Ohio State College?\nA: SELECT Player FROM 1-10812938-5 WHERE College = 'Ohio State'"}
-{"text": "table: 1-1081459-1\ncolumns: Number Range, Builder, Introduced, No. Built, Region, Withdrawn\nQ: What is the minimum introduced value for the Departmental region?\nA: SELECT MIN Introduced FROM 1-1081459-1 WHERE Region = 'Departmental'"}
-{"text": "table: 1-1081459-1\ncolumns: Number Range, Builder, Introduced, No. Built, Region, Withdrawn\nQ: What is the smallest introduced value?\nA: SELECT MIN Introduced FROM 1-1081459-1"}
-{"text": "table: 1-10812938-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: Which CFL Teams drafted an OL in 2006?\nA: SELECT CFL Team FROM 1-10812938-4 WHERE Position = 'OL'"}
-{"text": "table: 1-10812938-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: Which college is aligned to the Saskatchewan Roughriders?\nA: SELECT College FROM 1-10812938-4 WHERE CFL Team = 'Saskatchewan Roughriders'"}
-{"text": "table: 1-10812938-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What Position did the Hamilton Tiger-Cats (via Ottawa) pick in the 2006 Draft.\nA: SELECT Position FROM 1-10812938-4 WHERE CFL Team = 'Hamilton Tiger-Cats (via Ottawa)'"}
-{"text": "table: 1-10812938-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the earliest pick listed in the table.\nA: SELECT MIN Pick # FROM 1-10812938-4"}
-{"text": "table: 1-10842344-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, Production code, U.S. viewers (millions)\nQ: What episode number had production code e4423?\nA: SELECT MAX No. in season FROM 1-10842344-1 WHERE Production code = 'E4423'"}
-{"text": "table: 1-10842344-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, Production code, U.S. viewers (millions)\nQ: What's the latest episode in a season where the U.S. viewers totaled 14.37 million?\nA: SELECT MAX No. in season FROM 1-10842344-1 WHERE U.S. viewers (millions) = '14.37'"}
-{"text": "table: 1-10842344-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, Production code, U.S. viewers (millions)\nQ: Who directed the episode \"Escape\"?\nA: SELECT Directed by FROM 1-10842344-1 WHERE Title = '\"Escape\"'"}
-{"text": "table: 1-10819266-8\ncolumns: Season, Episodes, Time slot (EST), Season premiere, Season finale, TV season, Rank, Viewers (in millions)\nQ: How many seasons is the season finale on May 26, 2010?\nA: SELECT COUNT Season FROM 1-10819266-8 WHERE Season finale = 'May 26, 2010'"}
-{"text": "table: 1-10819266-8\ncolumns: Season, Episodes, Time slot (EST), Season premiere, Season finale, TV season, Rank, Viewers (in millions)\nQ: What episodes are there where the season premier is September 23, 2009?\nA: SELECT Episodes FROM 1-10819266-8 WHERE Season premiere = 'September 23, 2009'"}
-{"text": "table: 1-10819266-8\ncolumns: Season, Episodes, Time slot (EST), Season premiere, Season finale, TV season, Rank, Viewers (in millions)\nQ: What is the season finale for season 4?\nA: SELECT Season finale FROM 1-10819266-8 WHERE Season = 4"}
-{"text": "table: 1-10819266-8\ncolumns: Season, Episodes, Time slot (EST), Season premiere, Season finale, TV season, Rank, Viewers (in millions)\nQ: How many season premiers have a rank of #21?\nA: SELECT Season premiere FROM 1-10819266-8 WHERE Rank = '#21'"}
-{"text": "table: 1-10819266-8\ncolumns: Season, Episodes, Time slot (EST), Season premiere, Season finale, TV season, Rank, Viewers (in millions)\nQ: What are the seasons where September 26, 2007 is the season premier?\nA: SELECT TV season FROM 1-10819266-8 WHERE Season premiere = 'September 26, 2007'"}
-{"text": "table: 1-10818465-1\ncolumns: Model, RU, Max processors, Processor frequency, Max memory, Max disk capacity, GA Date\nQ: What max processor has a maximum memory of 256 gb?\nA: SELECT Max processors FROM 1-10818465-1 WHERE Max memory = '256 GB'"}
-{"text": "table: 1-10818465-1\ncolumns: Model, RU, Max processors, Processor frequency, Max memory, Max disk capacity, GA Date\nQ: What is the max memory of the t5120 model?\nA: SELECT Max memory FROM 1-10818465-1 WHERE Model = 'T5120'"}
-{"text": "table: 1-10818465-1\ncolumns: Model, RU, Max processors, Processor frequency, Max memory, Max disk capacity, GA Date\nQ: What is the lowest ru?\nA: SELECT MIN RU FROM 1-10818465-1"}
-{"text": "table: 1-10818465-1\ncolumns: Model, RU, Max processors, Processor frequency, Max memory, Max disk capacity, GA Date\nQ: What ga date do the models with 1.0, 1.2, 1.4ghz processor frequencies have?\nA: SELECT GA Date FROM 1-10818465-1 WHERE Processor frequency = '1.0, 1.2, 1.4GHz'"}
-{"text": "table: 1-10818465-1\ncolumns: Model, RU, Max processors, Processor frequency, Max memory, Max disk capacity, GA Date\nQ: What is the ga date of the t5120 model?\nA: SELECT GA Date FROM 1-10818465-1 WHERE Model = 'T5120'"}
-{"text": "table: 1-10815352-1\ncolumns: League, Sport, Country, Season, Games, Average attendance, Total attendance\nQ: What is the sport of the La Liga league?\nA: SELECT Sport FROM 1-10815352-1 WHERE League = 'La Liga'"}
-{"text": "table: 1-10815352-1\ncolumns: League, Sport, Country, Season, Games, Average attendance, Total attendance\nQ: What's the minimum total attendance of the Premier League association football?\nA: SELECT MIN Total attendance FROM 1-10815352-1 WHERE Sport = 'Association football' AND League = 'Premier League'"}
-{"text": "table: 1-10815352-1\ncolumns: League, Sport, Country, Season, Games, Average attendance, Total attendance\nQ: What's the average attendance of the leagues in the season of 2013?\nA: SELECT MIN Average attendance FROM 1-10815352-1 WHERE Season = '2013'"}
-{"text": "table: 1-10815352-1\ncolumns: League, Sport, Country, Season, Games, Average attendance, Total attendance\nQ: What's the total attendance of the leagues in season of 2010?\nA: SELECT COUNT Total attendance FROM 1-10815352-1 WHERE Season = '2010'"}
-{"text": "table: 1-10874596-1\ncolumns: Year [e ] (Ceremony), Film title used in nomination, Original title, Director, Result\nQ: Who were the directors of the film submitted with the title Young T\u00f6rless?\nA: SELECT Director FROM 1-10874596-1 WHERE Film title used in nomination = 'Young T\u00f6rless'"}
-{"text": "table: 1-10874596-1\ncolumns: Year [e ] (Ceremony), Film title used in nomination, Original title, Director, Result\nQ: What was the original title of the film submitted with the title A Woman in Flames?\nA: SELECT Original title FROM 1-10874596-1 WHERE Film title used in nomination = 'A Woman in Flames'"}
-{"text": "table: 1-10874596-1\ncolumns: Year [e ] (Ceremony), Film title used in nomination, Original title, Director, Result\nQ: In what years was a film submitted with the title The Enigma of Kaspar Hauser?\nA: SELECT Year [e ] (Ceremony) FROM 1-10874596-1 WHERE Film title used in nomination = 'The Enigma of Kaspar Hauser'"}
-{"text": "table: 1-10874596-1\ncolumns: Year [e ] (Ceremony), Film title used in nomination, Original title, Director, Result\nQ: Who were the directors of the film with the original title o.k.?\nA: SELECT Director FROM 1-10874596-1 WHERE Original title = 'o.k.'"}
-{"text": "table: 1-1087659-2\ncolumns: Year, Division, League, Reg. Season, Playoffs, Avg. Attendance\nQ: What is the division for the division semifinals playoffs?\nA: SELECT Division FROM 1-1087659-2 WHERE Playoffs = 'Division Semifinals'"}
-{"text": "table: 1-10908676-7\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date\nQ: What is the number in series of \"say uncle\"?\nA: SELECT No. in series FROM 1-10908676-7 WHERE Title = '\"Say Uncle\"'"}
-{"text": "table: 1-10908676-7\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date\nQ: What is the title written by David Mamet?\nA: SELECT Title FROM 1-10908676-7 WHERE Written by = 'David Mamet'"}
-{"text": "table: 1-10942714-1\ncolumns: Rank, English title, Chinese title, Average, Peak, Premiere, Finale, HK viewers\nQ: What was the finale for \u6f6e\u7206\u5927\u72c0\nA: SELECT Finale FROM 1-10942714-1 WHERE Chinese title = '\u6f6e\u7206\u5927\u72c0'"}
-{"text": "table: 1-10942714-1\ncolumns: Rank, English title, Chinese title, Average, Peak, Premiere, Finale, HK viewers\nQ: What was the finale for \u6f6e\u7206\u5927\u72c0\nA: SELECT Finale FROM 1-10942714-1 WHERE Chinese title = '\u6f6e\u7206\u5927\u72c0'"}
-{"text": "table: 1-10942714-1\ncolumns: Rank, English title, Chinese title, Average, Peak, Premiere, Finale, HK viewers\nQ: How many viewers were there for the premier with 34\nA: SELECT HK viewers FROM 1-10942714-1 WHERE Premiere = 34"}
-{"text": "table: 1-10942714-1\ncolumns: Rank, English title, Chinese title, Average, Peak, Premiere, Finale, HK viewers\nQ: How many are listed under \u6f6e\u7206\u5927\u72c0\nA: SELECT COUNT Peak FROM 1-10942714-1 WHERE Chinese title = '\u6f6e\u7206\u5927\u72c0'"}
-{"text": "table: 1-10953197-2\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: Who was the director of the episode with a production code of 2393059?\nA: SELECT Director FROM 1-10953197-2 WHERE Production code = '2393059'"}
-{"text": "table: 1-10953197-2\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: How many people wrote \"Michael's Game\"?\nA: SELECT COUNT Writer(s) FROM 1-10953197-2 WHERE Title = '\"Michael's Game\"'"}
-{"text": "table: 1-10953197-2\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: When did the episode title \"Duet For One\" air?\nA: SELECT Original air date FROM 1-10953197-2 WHERE Title = '\"Duet for One\"'"}
-{"text": "table: 1-10935548-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, Production code, U.S. viewers (millions)\nQ: Which episode had 16.38 million U.S. viewers?\nA: SELECT Title FROM 1-10935548-1 WHERE U.S. viewers (millions) = '16.38'"}
-{"text": "table: 1-10935548-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, Production code, U.S. viewers (millions)\nQ: What is the production code of the episode written by Jos\u00e9 Molina that aired on October 12, 2004?\nA: SELECT Production code FROM 1-10935548-1 WHERE Written by = 'Jos\u00e9 Molina' AND Original air date = 'October 12, 2004'"}
-{"text": "table: 1-10935548-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, Production code, U.S. viewers (millions)\nQ: What was the original air date of the episode \"Quarry\"?\nA: SELECT Original air date FROM 1-10935548-1 WHERE Title = '\"Quarry\"'"}
-{"text": "table: 1-10935548-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, Production code, U.S. viewers (millions)\nQ: Which episode was directed by Jean de Segonzac?\nA: SELECT Title FROM 1-10935548-1 WHERE Directed by = 'Jean de Segonzac'"}
-{"text": "table: 1-10953197-3\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: what are the original air dates with a production code of 2394087\nA: SELECT Original air date FROM 1-10953197-3 WHERE Production code = '2394087'"}
-{"text": "table: 1-10953197-3\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: Who are the writers for the title \"boxing sydney\"\nA: SELECT Writer(s) FROM 1-10953197-3 WHERE Title = '\"Boxing Sydney\"'"}
-{"text": "table: 1-10953197-3\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: What are the production codes for the title \"all about brooke\"\nA: SELECT Production code FROM 1-10953197-3 WHERE Title = '\"All About Brooke\"'"}
-{"text": "table: 1-10953197-3\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: Who are the writer(s) for the production code 2394084\nA: SELECT Writer(s) FROM 1-10953197-3 WHERE Production code = '2394084'"}
-{"text": "table: 1-10953197-4\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: What's the total number of episodes with the production code 2395113A?\nA: SELECT COUNT Title FROM 1-10953197-4 WHERE Production code = '2395113A'"}
-{"text": "table: 1-10953197-4\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: What's the number of the episode called \"Melrose Unglued\"?\nA: SELECT MAX No. in series FROM 1-10953197-4 WHERE Title = '\"Melrose Unglued\"'"}
-{"text": "table: 1-10953197-4\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: Who's the writer for the episode with a production code 2395114?\nA: SELECT Writer(s) FROM 1-10953197-4 WHERE Production code = '2395114'"}
-{"text": "table: 1-10953197-4\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: Who directed the episode titled \"Full Metal Betsy\"?\nA: SELECT Director FROM 1-10953197-4 WHERE Title = '\"Full Metal Betsy\"'"}
-{"text": "table: 1-10953197-4\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: What's the number of the episode with production code 2395118?\nA: SELECT No. in season FROM 1-10953197-4 WHERE Production code = '2395118'"}
-{"text": "table: 1-10953197-4\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: Who was the writer for the episode with production code 2395096?\nA: SELECT Writer(s) FROM 1-10953197-4 WHERE Production code = '2395096'"}
-{"text": "table: 1-10932739-2\ncolumns: Planet, Planet Type, Semimajor Axis ( AU ), Orbital Period, Radial velocity (m/s), Detectable by:\nQ: What generation of spectrograph is most likely to detect a planet with a radial velocity of 0.089 m/s?\nA: SELECT Detectable by: FROM 1-10932739-2 WHERE Radial velocity (m/s) = '0.089'"}
-{"text": "table: 1-10932739-2\ncolumns: Planet, Planet Type, Semimajor Axis ( AU ), Orbital Period, Radial velocity (m/s), Detectable by:\nQ: How long is the orbital period for the planet that has a semimajor axis of 5.20 au?\nA: SELECT Orbital Period FROM 1-10932739-2 WHERE Semimajor Axis ( AU ) = '5.20'"}
-{"text": "table: 1-10932739-2\ncolumns: Planet, Planet Type, Semimajor Axis ( AU ), Orbital Period, Radial velocity (m/s), Detectable by:\nQ: What generation of spectrograph is Jupiter detected by?\nA: SELECT Detectable by: FROM 1-10932739-2 WHERE Planet = 'Jupiter'"}
-{"text": "table: 1-10932739-2\ncolumns: Planet, Planet Type, Semimajor Axis ( AU ), Orbital Period, Radial velocity (m/s), Detectable by:\nQ: Which planet has an orbital period of 11.86 years?\nA: SELECT Planet FROM 1-10932739-2 WHERE Orbital Period = '11.86 years'"}
-{"text": "table: 1-10953197-7\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: who directed the production code 2398204\nA: SELECT Director FROM 1-10953197-7 WHERE Production code = '2398204'"}
-{"text": "table: 1-10953197-7\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: when did \"unpleasantville\" air?\nA: SELECT Original air date FROM 1-10953197-7 WHERE Title = '\"Unpleasantville\"'"}
-{"text": "table: 1-10960039-1\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is player Alexis Bwenge's pick number?\nA: SELECT Pick # FROM 1-10960039-1 WHERE Player = 'Alexis Bwenge'"}
-{"text": "table: 1-10960039-1\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What player is pick #2?\nA: SELECT Player FROM 1-10960039-1 WHERE Pick # = 2"}
-{"text": "table: 1-10960039-1\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: Which player's college is Saskatchewan?\nA: SELECT Player FROM 1-10960039-1 WHERE College = 'Saskatchewan'"}
-{"text": "table: 1-10960039-1\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is McMaster College's pick number?\nA: SELECT MIN Pick # FROM 1-10960039-1 WHERE College = 'McMaster'"}
-{"text": "table: 1-10953197-6\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: give the least number of times an episode was shown from 1997-1998\nA: SELECT MIN No. in season FROM 1-10953197-6"}
-{"text": "table: 1-10953197-6\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: when was the episode named \"the doctor is in... deep\" first broadcast \nA: SELECT Original air date FROM 1-10953197-6 WHERE Title = '\"The Doctor Is In... Deep\"'"}
-{"text": "table: 1-10953197-6\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: how many times does the episode called \"coop de grace\" appear \nA: SELECT COUNT No. in series FROM 1-10953197-6 WHERE Title = '\"Coop de Grace\"'"}
-{"text": "table: 1-10953197-6\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: what is season 6 sum of both the number of times processing ID 2397162 was assigned and the number of times chip chalmers managed an episode \nA: SELECT MAX No. in season FROM 1-10953197-6 WHERE Director = 'Chip Chalmers' AND Production code = '2397162'"}
-{"text": "table: 1-10966926-2\ncolumns: Round, Choice, Player name, Position, Height, Weight, College\nQ: Which player went to Michigan State?\nA: SELECT Player name FROM 1-10966926-2 WHERE College = 'Michigan State'"}
-{"text": "table: 1-10966926-2\ncolumns: Round, Choice, Player name, Position, Height, Weight, College\nQ: Which player went to college in Oklahoma?\nA: SELECT Player name FROM 1-10966926-2 WHERE College = 'Oklahoma'"}
-{"text": "table: 1-10966926-2\ncolumns: Round, Choice, Player name, Position, Height, Weight, College\nQ: Which position does Colt Brennan play?\nA: SELECT Position FROM 1-10966926-2 WHERE Player name = 'Colt Brennan'"}
-{"text": "table: 1-10966926-2\ncolumns: Round, Choice, Player name, Position, Height, Weight, College\nQ: What is the height of the person that weighs 320 pounds?\nA: SELECT Height FROM 1-10966926-2 WHERE Weight = 320"}
-{"text": "table: 1-10975034-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: How many colleges have a DB position?\nA: SELECT COUNT College FROM 1-10975034-4 WHERE Position = 'DB'"}
-{"text": "table: 1-10975034-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the maximum number of picks for the CFL team Calgary Stampeders?\nA: SELECT MAX Pick # FROM 1-10975034-4 WHERE CFL Team = 'Calgary Stampeders'"}
-{"text": "table: 1-10975034-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: How many CFL teams are from York college?\nA: SELECT COUNT CFL Team FROM 1-10975034-4 WHERE College = 'York'"}
-{"text": "table: 1-10975034-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What CFL teams are part of Simon Fraser college?\nA: SELECT CFL Team FROM 1-10975034-4 WHERE College = 'Simon Fraser'"}
-{"text": "table: 1-10975034-4\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: Which players have a pick number of 27?\nA: SELECT Player FROM 1-10975034-4 WHERE Pick # = 27"}
-{"text": "table: 1-10960039-6\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: How many times were players named brett ralph were selected?\nA: SELECT COUNT Pick # FROM 1-10960039-6 WHERE Player = 'Brett Ralph'"}
-{"text": "table: 1-10960039-6\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What schools did lenard semajuste play for?\nA: SELECT College FROM 1-10960039-6 WHERE Player = 'Lenard Semajuste'"}
-{"text": "table: 1-10960039-6\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the highest selection number for the saskatchewan roughriders team?\nA: SELECT MAX Pick # FROM 1-10960039-6 WHERE CFL Team = 'Saskatchewan Roughriders'"}
-{"text": "table: 1-10960039-6\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: How many fb players were drafted?\nA: SELECT COUNT Pick # FROM 1-10960039-6 WHERE Position = 'FB'"}
-{"text": "table: 1-10960039-6\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: How many players played for adams state school?\nA: SELECT COUNT Player FROM 1-10960039-6 WHERE College = 'Adams State'"}
-{"text": "table: 1-10960039-6\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What teams drafted players that played for northwood school?\nA: SELECT CFL Team FROM 1-10960039-6 WHERE College = 'Northwood'"}
-{"text": "table: 1-10975034-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What college did Craig Zimmer go to?\nA: SELECT College FROM 1-10975034-5 WHERE Player = 'Craig Zimmer'"}
-{"text": "table: 1-10975034-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the pick number of regina?\nA: SELECT Pick # FROM 1-10975034-5 WHERE College = 'Regina'"}
-{"text": "table: 1-10975034-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the player who is lb and cfl team is saskatchewan roughriders?\nA: SELECT Player FROM 1-10975034-5 WHERE Position = 'LB' AND CFL Team = 'Saskatchewan Roughriders'"}
-{"text": "table: 1-10975034-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the cfl team that has a position of ol?\nA: SELECT CFL Team FROM 1-10975034-5 WHERE Position = 'OL'"}
-{"text": "table: 1-10975034-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the number of position where the pick number is 43?\nA: SELECT COUNT Position FROM 1-10975034-5 WHERE Pick # = 43"}
-{"text": "table: 1-10975034-5\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the cfl team with ryan folk?\nA: SELECT CFL Team FROM 1-10975034-5 WHERE Player = 'Ryan Folk'"}
-{"text": "table: 1-10979230-5\ncolumns: Romaji title, Japanese title, Release date, Reference, Oricon\nQ: What release date is when kids-270 is a reference? \nA: SELECT Release date FROM 1-10979230-5 WHERE Reference = 'KIDS-270'"}
-{"text": "table: 1-10979230-5\ncolumns: Romaji title, Japanese title, Release date, Reference, Oricon\nQ: what is the title where romaji is titles da.i.su.ki\nA: SELECT Japanese title FROM 1-10979230-5 WHERE Romaji title = 'Da.i.su.ki'"}
-{"text": "table: 1-10979230-5\ncolumns: Romaji title, Japanese title, Release date, Reference, Oricon\nQ: what are the title in japanese where the reference is kids-430?\nA: SELECT Japanese title FROM 1-10979230-5 WHERE Reference = 'KIDS-430'"}
-{"text": "table: 1-10979230-5\ncolumns: Romaji title, Japanese title, Release date, Reference, Oricon\nQ: who is the reference when romaji title is heartbreak sniper?\nA: SELECT Reference FROM 1-10979230-5 WHERE Romaji title = 'Heartbreak Sniper'"}
-{"text": "table: 1-10979230-4\ncolumns: Romaji title, Japanese title, Release date, Reference, Oricon\nQ: What rank is \u611b\u306e\u30d0\u30ab on the Japanese singles chart?\nA: SELECT COUNT Oricon FROM 1-10979230-4 WHERE Japanese title = '\u611b\u306e\u30d0\u30ab'"}
-{"text": "table: 1-10979230-4\ncolumns: Romaji title, Japanese title, Release date, Reference, Oricon\nQ: How many songs have mi-chemin as their Japanese name and romanji name?\nA: SELECT COUNT Romaji title FROM 1-10979230-4 WHERE Japanese title = 'Mi-Chemin'"}
-{"text": "table: 1-1099080-1\ncolumns: Condition, Prothrombin time, Partial thromboplastin time, Bleeding time, Platelet count\nQ: What was the partial thromboplastin time for factor x deficiency as seen in amyloid purpura\nA: SELECT Partial thromboplastin time FROM 1-1099080-1 WHERE Condition = 'Factor X deficiency as seen in amyloid purpura'"}
-{"text": "table: 1-1099080-1\ncolumns: Condition, Prothrombin time, Partial thromboplastin time, Bleeding time, Platelet count\nQ: How many conditions have an unaffected prothrombin time and a prolonged bleeding time\nA: SELECT COUNT Condition FROM 1-1099080-1 WHERE Prothrombin time = 'Unaffected' AND Bleeding time = 'Prolonged'"}
-{"text": "table: 1-1099080-1\ncolumns: Condition, Prothrombin time, Partial thromboplastin time, Bleeding time, Platelet count\nQ: What was the bleeding time for the factor x deficiency as seen in amyloid purpura\nA: SELECT Bleeding time FROM 1-1099080-1 WHERE Condition = 'Factor X deficiency as seen in amyloid purpura'"}
-{"text": "table: 1-1099080-1\ncolumns: Condition, Prothrombin time, Partial thromboplastin time, Bleeding time, Platelet count\nQ: What conditions had both prolonged bleeding times and prolonged partial thromboplastin times\nA: SELECT Condition FROM 1-1099080-1 WHERE Partial thromboplastin time = 'Prolonged' AND Bleeding time = 'Prolonged'"}
-{"text": "table: 1-1099080-1\ncolumns: Condition, Prothrombin time, Partial thromboplastin time, Bleeding time, Platelet count\nQ: What was the bleeding time for factor xii deficiency\nA: SELECT Bleeding time FROM 1-1099080-1 WHERE Condition = 'Factor XII deficiency'"}
-{"text": "table: 1-1099080-1\ncolumns: Condition, Prothrombin time, Partial thromboplastin time, Bleeding time, Platelet count\nQ: What were the bleeding times when both the platelet count was unaffected and the partial thromboplastin time was unaffected\nA: SELECT Bleeding time FROM 1-1099080-1 WHERE Partial thromboplastin time = 'Unaffected' AND Platelet count = 'Unaffected'"}
-{"text": "table: 1-11019212-1\ncolumns: Location, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday\nQ: what's the\u00a0tuesday\u00a0time with\u00a0location\u00a0being millhopper\nA: SELECT Tuesday FROM 1-11019212-1 WHERE Location = 'Millhopper'"}
-{"text": "table: 1-11019212-1\ncolumns: Location, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday\nQ: what's the\u00a0wednesday time\u00a0with\u00a0monday\u00a0being 10:00-8:00\nA: SELECT Wednesday FROM 1-11019212-1 WHERE Monday = '10:00-8:00'"}
-{"text": "table: 1-11019212-1\ncolumns: Location, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday\nQ: what's the\u00a0thursday\u00a0time with\u00a0location\u00a0being hawthorne\nA: SELECT Thursday FROM 1-11019212-1 WHERE Location = 'Hawthorne'"}
-{"text": "table: 1-11019212-1\ncolumns: Location, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday\nQ: what's the\u00a0saturday\u00a0time with\u00a0wednesday\u00a0being 10:00-5:00\nA: SELECT Saturday FROM 1-11019212-1 WHERE Wednesday = '10:00-5:00'"}
-{"text": "table: 1-11019212-1\ncolumns: Location, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday\nQ: what's the\u00a0thursday\u00a0time with\u00a0sunday\u00a0being 1:00-5:00 and\u00a0tuesday\u00a0being 1:00-7:00\nA: SELECT Thursday FROM 1-11019212-1 WHERE Sunday = '1:00-5:00' AND Tuesday = '1:00-7:00'"}
-{"text": "table: 1-11019212-1\ncolumns: Location, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday\nQ: what's the\u00a0monday\u00a0time with\u00a0tuesday\u00a0being 9:00-6:00\nA: SELECT Monday FROM 1-11019212-1 WHERE Tuesday = '9:00-6:00'"}
-{"text": "table: 1-11056278-3\ncolumns: Rnd, Race Name, Pole position, Fastest lap, Winning driver, Winning team, Report\nQ: What are all the reports where Paul Tracy had the fastest lap?\nA: SELECT Report FROM 1-11056278-3 WHERE Fastest lap = 'Paul Tracy'"}
-{"text": "table: 1-11056278-3\ncolumns: Rnd, Race Name, Pole position, Fastest lap, Winning driver, Winning team, Report\nQ: Who drove the fastest lap at the Tenneco Automotive Grand Prix of Detroit?\nA: SELECT Fastest lap FROM 1-11056278-3 WHERE Race Name = 'Tenneco Automotive Grand Prix of Detroit'"}
-{"text": "table: 1-11056278-3\ncolumns: Rnd, Race Name, Pole position, Fastest lap, Winning driver, Winning team, Report\nQ: Who had the fastest lap in the races won by Max Papis?\nA: SELECT Fastest lap FROM 1-11056278-3 WHERE Winning driver = 'Max Papis'"}
-{"text": "table: 1-11056278-3\ncolumns: Rnd, Race Name, Pole position, Fastest lap, Winning driver, Winning team, Report\nQ: In Round 6, how many winning drivers were there?\nA: SELECT COUNT Winning driver FROM 1-11056278-3 WHERE Rnd = 6"}
-{"text": "table: 1-1104312-5\ncolumns: English name, Original name, Area in km\u00b2, Population at 2010 Census, Number of settlements and villages\nQ: What are the original names of the districts where the population in the 2010 census was 210450?\nA: SELECT Original name FROM 1-1104312-5 WHERE Population at 2010 Census = 210450"}
-{"text": "table: 1-1104312-5\ncolumns: English name, Original name, Area in km\u00b2, Population at 2010 Census, Number of settlements and villages\nQ: What is the original name of the district with the current English name of South Bogor?\nA: SELECT Original name FROM 1-1104312-5 WHERE English name = 'South Bogor'"}
-{"text": "table: 1-1104312-5\ncolumns: English name, Original name, Area in km\u00b2, Population at 2010 Census, Number of settlements and villages\nQ: What is the listed population from the 2010 census of West Bogor?\nA: SELECT MIN Population at 2010 Census FROM 1-1104312-5 WHERE English name = 'West Bogor'"}
-{"text": "table: 1-1104312-5\ncolumns: English name, Original name, Area in km\u00b2, Population at 2010 Census, Number of settlements and villages\nQ: How many districts have an area of 17.72 KM2?\nA: SELECT COUNT English name FROM 1-1104312-5 WHERE Area in km\u00b2 = '17.72'"}
-{"text": "table: 1-1104312-5\ncolumns: English name, Original name, Area in km\u00b2, Population at 2010 Census, Number of settlements and villages\nQ: What is the area in km2 for the district whose original name was Kecamatan Bogor Timur?\nA: SELECT Area in km\u00b2 FROM 1-1104312-5 WHERE Original name = 'Kecamatan Bogor Timur'"}
-{"text": "table: 1-11066073-1\ncolumns: Pilot car No., Colour, Serial No., Engine No., Registration No.\nQ: What is the number of colour with the regisration number of mg-509?\nA: SELECT COUNT Colour FROM 1-11066073-1 WHERE Registration No. = 'MG-509'"}
-{"text": "table: 1-11058032-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: What is the title of the episode directed by Mark Tinker?\nA: SELECT Title FROM 1-11058032-1 WHERE Directed by = 'Mark Tinker'"}
-{"text": "table: 1-11058032-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: What episode in the season was directed by Jeff Melman?\nA: SELECT MIN No. in season FROM 1-11058032-1 WHERE Directed by = 'Jeff Melman'"}
-{"text": "table: 1-11058032-1\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many episodes had 16.03 million viewers?\nA: SELECT COUNT No. in series FROM 1-11058032-1 WHERE U.S. viewers (millions) = '16.03'"}
-{"text": "table: 1-11071897-1\ncolumns: Interregnum began, Interregnum ended, Duration, Count Palatine of Saxony, Count Palatine of the Rhine\nQ: What is the number of interregnum for duration 3 months, 6 days?\nA: SELECT COUNT Interregnum ended FROM 1-11071897-1 WHERE Duration = '3 months, 6 days'"}
-{"text": "table: 1-11075747-4\ncolumns: Series #, Episode #, Title, Directed by, Written by, Original air date\nQ: Who directed Episode 8?\nA: SELECT Directed by FROM 1-11075747-4 WHERE Episode # = 8"}
-{"text": "table: 1-11075747-4\ncolumns: Series #, Episode #, Title, Directed by, Written by, Original air date\nQ: Who directed the episode called \"Tell-tale Heart\"?\nA: SELECT Directed by FROM 1-11075747-4 WHERE Title = '\"Tell-Tale Heart\"'"}
-{"text": "table: 1-11075747-4\ncolumns: Series #, Episode #, Title, Directed by, Written by, Original air date\nQ: What was the original air date for Series 36?\nA: SELECT Original air date FROM 1-11075747-4 WHERE Series # = 36"}
-{"text": "table: 1-11075747-4\ncolumns: Series #, Episode #, Title, Directed by, Written by, Original air date\nQ: Who wrote Series 38?\nA: SELECT Written by FROM 1-11075747-4 WHERE Series # = 38"}
-{"text": "table: 1-1108394-24\ncolumns: 1973 Democratic initial primary, Manhattan, The Bronx, Brooklyn, Queens, Richmond [Staten Is.], Total, %\nQ: What is the percentage for manhattan 45,901?\nA: SELECT COUNT % FROM 1-1108394-24 WHERE Manhattan = '45,901'"}
-{"text": "table: 1-1108394-24\ncolumns: 1973 Democratic initial primary, Manhattan, The Bronx, Brooklyn, Queens, Richmond [Staten Is.], Total, %\nQ: Who won the 1973 democratic initial primary for queens of 19%?\nA: SELECT 1973 Democratic initial primary FROM 1-1108394-24 WHERE Queens = '19%'"}
-{"text": "table: 1-1108394-24\ncolumns: 1973 Democratic initial primary, Manhattan, The Bronx, Brooklyn, Queens, Richmond [Staten Is.], Total, %\nQ: What is the manhattan for richmond 35%?\nA: SELECT Manhattan FROM 1-1108394-24 WHERE Richmond [Staten Is.] = '35%'"}
-{"text": "table: 1-1108394-24\ncolumns: 1973 Democratic initial primary, Manhattan, The Bronx, Brooklyn, Queens, Richmond [Staten Is.], Total, %\nQ: What is the queens where richmond staten is 42%?\nA: SELECT Queens FROM 1-1108394-24 WHERE Richmond [Staten Is.] = '42%'"}
-{"text": "table: 1-1108394-43\ncolumns: 1932 (before recount), party, Manhattan, The Bronx, Brooklyn, Queens, Richmond [Staten Is.], Total, %\nQ: what's the\u00a0party\u00a0with\u00a0brooklyn\u00a0value of 51.0%\nA: SELECT party FROM 1-1108394-43 WHERE Brooklyn = '51.0%'"}
-{"text": "table: 1-1108394-43\ncolumns: 1932 (before recount), party, Manhattan, The Bronx, Brooklyn, Queens, Richmond [Staten Is.], Total, %\nQ: what's the\u00a0brooklyn\u00a0with\u00a0queens\u00a0value of 16.8%\nA: SELECT Brooklyn FROM 1-1108394-43 WHERE Queens = '16.8%'"}
-{"text": "table: 1-1108394-43\ncolumns: 1932 (before recount), party, Manhattan, The Bronx, Brooklyn, Queens, Richmond [Staten Is.], Total, %\nQ: what is the minimum total\nA: SELECT MIN Total FROM 1-1108394-43"}
-{"text": "table: 1-1108394-43\ncolumns: 1932 (before recount), party, Manhattan, The Bronx, Brooklyn, Queens, Richmond [Staten Is.], Total, %\nQ: what's the\u00a0%\u00a0with\u00a0total\u00a0value of 249887 and\u00a0queens\u00a0value of 6.8%\nA: SELECT % FROM 1-1108394-43 WHERE Total = 249887 AND Queens = '6.8%'"}
-{"text": "table: 1-11094950-1\ncolumns: Team, Location, Joined, Conference, Division, Previous Conference\nQ: Which teams were in the central division and located in livonia?\nA: SELECT Team FROM 1-11094950-1 WHERE Division = 'Central' AND Location = 'Livonia'"}
-{"text": "table: 1-11094950-1\ncolumns: Team, Location, Joined, Conference, Division, Previous Conference\nQ: Which teams are located in highland township?\nA: SELECT Team FROM 1-11094950-1 WHERE Location = 'Highland Township'"}
-{"text": "table: 1-11094950-1\ncolumns: Team, Location, Joined, Conference, Division, Previous Conference\nQ: What conference was the churchill chargers team in?\nA: SELECT Conference FROM 1-11094950-1 WHERE Team = 'Churchill Chargers'"}
-{"text": "table: 1-11111116-7\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: What was the titles of the episodes written by ken lazebnik?\nA: SELECT Title FROM 1-11111116-7 WHERE Written by = 'Ken LaZebnik'"}
-{"text": "table: 1-11111116-7\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: Who directed an episode that had 2.81 million U.S. viewers?\nA: SELECT Directed by FROM 1-11111116-7 WHERE U.S. viewers (million) = '2.81'"}
-{"text": "table: 1-11111116-7\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: What were the names of the episodes that had 3.02 million U.S. viewers?\nA: SELECT Title FROM 1-11111116-7 WHERE U.S. viewers (million) = '3.02'"}
-{"text": "table: 1-11111116-7\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: What were the original air dates of the episode named \"winds of war\"?\nA: SELECT Original air date FROM 1-11111116-7 WHERE Title = '\"Winds of War\"'"}
-{"text": "table: 1-11111116-7\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: Who directed episodes that had 2.61 million U.S. viewers?\nA: SELECT Directed by FROM 1-11111116-7 WHERE U.S. viewers (million) = '2.61'"}
-{"text": "table: 1-11111116-8\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: How many millions of U.S. viewers watched \"Brace for Impact\"?\nA: SELECT U.S. viewers (million) FROM 1-11111116-8 WHERE Title = '\"Brace for Impact\"'"}
-{"text": "table: 1-11111116-8\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: How many millions of U.S. viewers watched the episode that first aired on March 31, 2013?\nA: SELECT U.S. viewers (million) FROM 1-11111116-8 WHERE Original air date = 'March 31, 2013'"}
-{"text": "table: 1-11111116-8\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: Who wrote the episodes that were viewed by 2.12 million viewers?\nA: SELECT Written by FROM 1-11111116-8 WHERE U.S. viewers (million) = '2.12'"}
-{"text": "table: 1-11111116-6\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: The episode written by Rebecca Dameron aired on what date? \nA: SELECT Original air date FROM 1-11111116-6 WHERE Written by = 'Rebecca Dameron'"}
-{"text": "table: 1-11111116-6\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: Which episode in the series drew 3.6 million U.S. viewers? \nA: SELECT MIN No. in series FROM 1-11111116-6 WHERE U.S. viewers (million) = '3.6'"}
-{"text": "table: 1-11111116-6\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: Who wrote the episode that aired on April 17, 2011? \nA: SELECT Written by FROM 1-11111116-6 WHERE Original air date = 'April 17, 2011'"}
-{"text": "table: 1-11111116-6\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: How many times did episode 79 originally air? \nA: SELECT COUNT Original air date FROM 1-11111116-6 WHERE No. in series = 79"}
-{"text": "table: 1-11111116-6\ncolumns: No. in season, No. in series, Title, Directed by, Written by, Original air date, U.S. viewers (million)\nQ: How many millions of views in the country watched \"Line of Departure\"?\nA: SELECT U.S. viewers (million) FROM 1-11111116-6 WHERE Title = '\"Line of Departure\"'"}
-{"text": "table: 1-11148572-1\ncolumns: Season, MLS Cup Winner, MLS Cup Runner-Up, MLS Supporters Shield Winner, MLS Supporters Shield Runner-Up\nQ: What is the name of the shield winner in which the mls cup winner and mls cup runner up is colorado rapids?\nA: SELECT MLS Cup Winner FROM 1-11148572-1 WHERE MLS Cup Runner-Up = 'Colorado Rapids'"}
-{"text": "table: 1-11148572-1\ncolumns: Season, MLS Cup Winner, MLS Cup Runner-Up, MLS Supporters Shield Winner, MLS Supporters Shield Runner-Up\nQ: What is the name of the shield winner in which the mls cup winner and mls supporters shield runner up is Chivas usa?\nA: SELECT MLS Cup Winner FROM 1-11148572-1 WHERE MLS Supporters Shield Runner-Up = 'Chivas USA'"}
-{"text": "table: 1-11148572-1\ncolumns: Season, MLS Cup Winner, MLS Cup Runner-Up, MLS Supporters Shield Winner, MLS Supporters Shield Runner-Up\nQ: who is the of the shield winnerin which the mls cup runner-up and mls cup winner is real salt lake?\nA: SELECT MLS Cup Runner-Up FROM 1-11148572-1 WHERE MLS Cup Winner = 'Real Salt Lake'"}
-{"text": "table: 1-11148572-1\ncolumns: Season, MLS Cup Winner, MLS Cup Runner-Up, MLS Supporters Shield Winner, MLS Supporters Shield Runner-Up\nQ: Which shield winner has the mls cup runner up and the season is 2000?\nA: SELECT MLS Cup Runner-Up FROM 1-11148572-1 WHERE Season = 2000"}
-{"text": "table: 1-1112176-1\ncolumns: Season, Division, League Apps (Sub), League Goals, FA Cup Apps (Sub), FA Cup Goals, FL Cup Apps (Sub), FL Cup Goals, Other Apps, Other Goals, Total Apps (Sub), Total Goals\nQ: League apps (sub) maximum?\nA: SELECT MAX League Apps (Sub) FROM 1-1112176-1"}
-{"text": "table: 1-1112176-1\ncolumns: Season, Division, League Apps (Sub), League Goals, FA Cup Apps (Sub), FA Cup Goals, FL Cup Apps (Sub), FL Cup Goals, Other Apps, Other Goals, Total Apps (Sub), Total Goals\nQ: When total goals is 11 what was the league apps (sub)?\nA: SELECT MAX League Apps (Sub) FROM 1-1112176-1 WHERE Total Goals = 11"}
-{"text": "table: 1-11129123-1\ncolumns: Episode Air Date, Audition City, Date, First Audition Venue, Callback Date, Callback Venue, Golden Tickets\nQ: Which city had the charleston area convention center as its callback location\nA: SELECT Audition City FROM 1-11129123-1 WHERE Callback Venue = 'Charleston Area Convention Center'"}
-{"text": "table: 1-11129123-1\ncolumns: Episode Air Date, Audition City, Date, First Audition Venue, Callback Date, Callback Venue, Golden Tickets\nQ: When did the callbacks from rancho bernardo inn air\nA: SELECT Episode Air Date FROM 1-11129123-1 WHERE Callback Venue = 'Rancho Bernardo Inn'"}
-{"text": "table: 1-11147852-1\ncolumns: City of license/Market, Station, Channel TV ( DT ), Year of affiliation, Owned since\nQ: The station located in Albuquerque has been owned since what year?\nA: SELECT Owned since FROM 1-11147852-1 WHERE City of license/Market = 'Albuquerque'"}
-{"text": "table: 1-11147852-1\ncolumns: City of license/Market, Station, Channel TV ( DT ), Year of affiliation, Owned since\nQ: What channels have stations that were affiliated in 2002?\nA: SELECT Channel TV ( DT ) FROM 1-11147852-1 WHERE Year of affiliation = '2002'"}
-{"text": "table: 1-11147852-1\ncolumns: City of license/Market, Station, Channel TV ( DT ), Year of affiliation, Owned since\nQ: What market is KTFK-DT in?\nA: SELECT City of license/Market FROM 1-11147852-1 WHERE Station = 'KTFK-DT'"}
-{"text": "table: 1-11167610-1\ncolumns: Trim, Engine, Turbo, Fuel Delivery, Power, Torque, Transmission, Performance\nQ: what's the\u00a0engine\u00a0where\u00a0performance\u00a0is 0\u2013100km/h: 10.5s, vmax km/h (mph)\nA: SELECT Engine FROM 1-11167610-1 WHERE Performance = '0\u2013100km/h: 10.5s, VMax km/h (mph)'"}
-{"text": "table: 1-11167610-1\ncolumns: Trim, Engine, Turbo, Fuel Delivery, Power, Torque, Transmission, Performance\nQ: what's the\u00a0turbo\u00a0where\u00a0trim\u00a0is 2.0 20v\nA: SELECT Turbo FROM 1-11167610-1 WHERE Trim = '2.0 20v'"}
-{"text": "table: 1-11167610-1\ncolumns: Trim, Engine, Turbo, Fuel Delivery, Power, Torque, Transmission, Performance\nQ: what's the\u00a0torque\u00a0where\u00a0performance\u00a0is 0\u2013100km/h: 7.5s auto, vmax: km/h (mph)\nA: SELECT Torque FROM 1-11167610-1 WHERE Performance = '0\u2013100km/h: 7.5s auto, VMax: km/h (mph)'"}
-{"text": "table: 1-11167610-1\ncolumns: Trim, Engine, Turbo, Fuel Delivery, Power, Torque, Transmission, Performance\nQ: what's the\u00a0transmission\u00a0where\u00a0turbo\u00a0is yes (mitsubishi td04-16t )\nA: SELECT Transmission FROM 1-11167610-1 WHERE Turbo = 'Yes (Mitsubishi TD04-16t )'"}
-{"text": "table: 1-11167610-1\ncolumns: Trim, Engine, Turbo, Fuel Delivery, Power, Torque, Transmission, Performance\nQ: what's the\u00a0fuel delivery\u00a0where\u00a0power\u00a0is hp (kw) @6500 rpm\nA: SELECT Fuel Delivery FROM 1-11167610-1 WHERE Power = 'hp (kW) @6500 rpm'"}
-{"text": "table: 1-11167610-1\ncolumns: Trim, Engine, Turbo, Fuel Delivery, Power, Torque, Transmission, Performance\nQ: \" what's the engine with turbo being yes (mitsubishi td04-15g ) \"\nA: SELECT Engine FROM 1-11167610-1 WHERE Turbo = 'Yes (Mitsubishi TD04-15g )'"}
-{"text": "table: 1-11173827-1\ncolumns: Rank, English title, Chinese title, Average, Peak, Premiere, Finale, HK viewers\nQ: What is the english title that has finale as 33 and peak as 42?\nA: SELECT English title FROM 1-11173827-1 WHERE Finale = 33 AND Peak = 42"}
-{"text": "table: 1-11173827-1\ncolumns: Rank, English title, Chinese title, Average, Peak, Premiere, Finale, HK viewers\nQ: What is the english title where the premiere is less than 30.0 and the finale is bigger than 36.0?\nA: SELECT English title FROM 1-11173827-1 WHERE Premiere < 30.0 AND Finale > 36.0"}
-{"text": "table: 1-11173827-1\ncolumns: Rank, English title, Chinese title, Average, Peak, Premiere, Finale, HK viewers\nQ: What is the rank of the chinese title \u7de3\u4f86\u81ea\u6709\u6a5f?\nA: SELECT Rank FROM 1-11173827-1 WHERE Chinese title = '\u7de3\u4f86\u81ea\u6709\u6a5f'"}
-{"text": "table: 1-11173827-1\ncolumns: Rank, English title, Chinese title, Average, Peak, Premiere, Finale, HK viewers\nQ: What amount is the number of hk viewers where chinese title is \u5341\u5144\u5f1f?\nA: SELECT HK viewers FROM 1-11173827-1 WHERE Chinese title = '\u5341\u5144\u5f1f'"}
-{"text": "table: 1-11178271-1\ncolumns: #, Episode, Air Date, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Weekly Rank\nQ: What is the weekly rank with an air date is november 12, 2007?\nA: SELECT Weekly Rank FROM 1-11178271-1 WHERE Air Date = 'November 12, 2007'"}
-{"text": "table: 1-11178271-1\ncolumns: #, Episode, Air Date, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Weekly Rank\nQ: What is the air date of the episode \"blowback\"?\nA: SELECT Air Date FROM 1-11178271-1 WHERE Episode = '\"Blowback\"'"}
-{"text": "table: 1-11178271-1\ncolumns: #, Episode, Air Date, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Weekly Rank\nQ: What is the lowest weekly rank with an air date of november 26, 2007?\nA: SELECT MIN Weekly Rank FROM 1-11178271-1 WHERE Air Date = 'November 26, 2007'"}
-{"text": "table: 1-11178271-1\ncolumns: #, Episode, Air Date, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Weekly Rank\nQ: What is the episode where 18-49 has a rating/share of 3.5/9\nA: SELECT Episode FROM 1-11178271-1 WHERE 18\u201349 (Rating/Share) = '3.5/9'"}
-{"text": "table: 1-11178271-1\ncolumns: #, Episode, Air Date, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Weekly Rank\nQ: What is the viewers where the rating is 5.3?\nA: SELECT Viewers (m) FROM 1-11178271-1 WHERE Rating = '5.3'"}
-{"text": "table: 1-11178271-1\ncolumns: #, Episode, Air Date, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Weekly Rank\nQ: What is the 18-49 rating/share where the viewers is 5.61?\nA: SELECT 18\u201349 (Rating/Share) FROM 1-11178271-1 WHERE Viewers (m) = '5.61'"}
-{"text": "table: 1-11206787-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What is the highest of balmoor/\nA: SELECT Highest FROM 1-11206787-5 WHERE Stadium = 'Balmoor'"}
-{"text": "table: 1-11206787-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What is the number of capacity at somerset park?\nA: SELECT COUNT Capacity FROM 1-11206787-5 WHERE Stadium = 'Somerset Park'"}
-{"text": "table: 1-11206787-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What is the minimum capacity where airdrie united is?\nA: SELECT MIN Capacity FROM 1-11206787-5 WHERE Team = 'Airdrie United'"}
-{"text": "table: 1-11206787-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What is the stadium for alloa athletic?\nA: SELECT Stadium FROM 1-11206787-5 WHERE Team = 'Alloa Athletic'"}
-{"text": "table: 1-11206787-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What is the highest of ayr united?\nA: SELECT MIN Highest FROM 1-11206787-5 WHERE Team = 'Ayr United'"}
-{"text": "table: 1-11206787-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What is the average?\nA: SELECT MIN Average FROM 1-11206787-5"}
-{"text": "table: 1-11190568-7\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment, Position\nQ: When are team Galway's dates of appointment?\nA: SELECT Date of appointment FROM 1-11190568-7 WHERE Team = 'Galway'"}
-{"text": "table: 1-11190568-7\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment, Position\nQ: When are the vacancy dates for outgoing manager Damien Fox?\nA: SELECT Date of vacancy FROM 1-11190568-7 WHERE Outgoing manager = 'Damien Fox'"}
-{"text": "table: 1-11190568-7\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment, Position\nQ: When is the date of vacancy of Davy Fitzgerald being a replacement?\nA: SELECT Date of vacancy FROM 1-11190568-7 WHERE Replaced by = 'Davy FitzGerald'"}
-{"text": "table: 1-11190568-7\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment, Position\nQ: Which team has the outgoing manager John Meyler?\nA: SELECT Team FROM 1-11190568-7 WHERE Outgoing manager = 'John Meyler'"}
-{"text": "table: 1-11200856-1\ncolumns: Hand, 1 credit, 2 credits, 3 credits, 4 credits, 5 credits\nQ: How many times is 3 credits 180?\nA: SELECT COUNT 1 credit FROM 1-11200856-1 WHERE 3 credits = 180"}
-{"text": "table: 1-11200856-1\ncolumns: Hand, 1 credit, 2 credits, 3 credits, 4 credits, 5 credits\nQ: What is the hand for 4 credits is 1600?\nA: SELECT Hand FROM 1-11200856-1 WHERE 4 credits = 1600"}
-{"text": "table: 1-11200856-1\ncolumns: Hand, 1 credit, 2 credits, 3 credits, 4 credits, 5 credits\nQ: How many 3 credits are there with 5 credits of 5?\nA: SELECT COUNT 3 credits FROM 1-11200856-1 WHERE 5 credits = '5'"}
-{"text": "table: 1-11200856-1\ncolumns: Hand, 1 credit, 2 credits, 3 credits, 4 credits, 5 credits\nQ: How many 4 credits is the hand two pair?\nA: SELECT COUNT 4 credits FROM 1-11200856-1 WHERE Hand = 'Two pair'"}
-{"text": "table: 1-11210576-3\ncolumns: Character, Position, Actor, First Episode, Final Episode, Duration, Final Episode Count\nQ: What duration is listed for Christian de la Fuente?\nA: SELECT Duration FROM 1-11210576-3 WHERE Actor = 'Christian de la Fuente'"}
-{"text": "table: 1-11210576-3\ncolumns: Character, Position, Actor, First Episode, Final Episode, Duration, Final Episode Count\nQ: What was the final episode for Dea Agent?\nA: SELECT Final Episode FROM 1-11210576-3 WHERE Position = 'DEA Agent'"}
-{"text": "table: 1-11207040-6\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment\nQ: What days is greenock morton vacant?\nA: SELECT Date of vacancy FROM 1-11207040-6 WHERE Team = 'Greenock Morton'"}
-{"text": "table: 1-11207040-6\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment\nQ: What are the dates of the outgoing manager colin hendry does appointments? \nA: SELECT Date of appointment FROM 1-11207040-6 WHERE Outgoing manager = 'Colin Hendry'"}
-{"text": "table: 1-11207040-6\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment\nQ: What teams does jim mcinally manage?\nA: SELECT Team FROM 1-11207040-6 WHERE Outgoing manager = 'Jim McInally'"}
-{"text": "table: 1-11207040-6\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment\nQ: What days are vacant that were replaced by john brown?\nA: SELECT Date of vacancy FROM 1-11207040-6 WHERE Replaced by = 'John Brown'"}
-{"text": "table: 1-11206916-2\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment\nQ: What manner of departure is listed with an appointment date of 13 march 2008\nA: SELECT Manner of departure FROM 1-11206916-2 WHERE Date of appointment = '13 March 2008'"}
-{"text": "table: 1-11206916-2\ncolumns: Team, Outgoing manager, Manner of departure, Date of vacancy, Replaced by, Date of appointment\nQ: What is the date of appointment for outgoing manager Campbell Money\nA: SELECT Date of appointment FROM 1-11206916-2 WHERE Outgoing manager = 'Campbell Money'"}
-{"text": "table: 1-11207040-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What is the lowest attendance that East End Park has ever had?\nA: SELECT MIN Lowest FROM 1-11207040-5 WHERE Stadium = 'East End Park'"}
-{"text": "table: 1-11207040-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What team plays at Palmerston Park?\nA: SELECT Team FROM 1-11207040-5 WHERE Stadium = 'Palmerston Park'"}
-{"text": "table: 1-11207040-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What is the lowest attandance recorded at Cappielow?\nA: SELECT MIN Lowest FROM 1-11207040-5 WHERE Stadium = 'Cappielow'"}
-{"text": "table: 1-11207040-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What is the highest attendance at a game played by St. Johnstone?\nA: SELECT MAX Highest FROM 1-11207040-5 WHERE Team = 'St. Johnstone'"}
-{"text": "table: 1-11207040-5\ncolumns: Team, Stadium, Capacity, Highest, Lowest, Average\nQ: What is the highest attandence at a Hamilton Academical game?\nA: SELECT MIN Highest FROM 1-11207040-5 WHERE Team = 'Hamilton Academical'"}
-{"text": "table: 1-11214772-1\ncolumns: Year, Champion, Score, Runner-Up, Location, Semi-Finalist #1, Semi-Finalist #2\nQ: who is the\u00a0champion\u00a0where\u00a0semi-finalist #2\u00a0is na and\u00a0location\u00a0is morrisville, nc\nA: SELECT Champion FROM 1-11214772-1 WHERE Semi-Finalist #2 = 'NA' AND Location = 'Morrisville, NC'"}
-{"text": "table: 1-11214772-1\ncolumns: Year, Champion, Score, Runner-Up, Location, Semi-Finalist #1, Semi-Finalist #2\nQ: what's the\u00a0score\u00a0where\u00a0year\u00a0is 2007\nA: SELECT Score FROM 1-11214772-1 WHERE Year = '2007'"}
-{"text": "table: 1-11214772-1\ncolumns: Year, Champion, Score, Runner-Up, Location, Semi-Finalist #1, Semi-Finalist #2\nQ: what is the total number of\u00a0semi-finalist #2\u00a0where\u00a0runner-up\u00a0is east carolina\nA: SELECT COUNT Semi-Finalist #2 FROM 1-11214772-1 WHERE Runner-Up = 'East Carolina'"}
-{"text": "table: 1-11214772-1\ncolumns: Year, Champion, Score, Runner-Up, Location, Semi-Finalist #1, Semi-Finalist #2\nQ: who is the\u00a0semi-finalist #1\u00a0where\u00a0runner-up\u00a0is elon university\nA: SELECT Semi-Finalist #1 FROM 1-11214772-1 WHERE Runner-Up = 'Elon University'"}
-{"text": "table: 1-11214772-1\ncolumns: Year, Champion, Score, Runner-Up, Location, Semi-Finalist #1, Semi-Finalist #2\nQ: who is the\u00a0runner-up\u00a0where\u00a0year\u00a0is 2004 and\u00a0champion\u00a0is north carolina state\nA: SELECT Runner-Up FROM 1-11214772-1 WHERE Year = '2004' AND Champion = 'North Carolina State'"}
-{"text": "table: 1-11214772-1\ncolumns: Year, Champion, Score, Runner-Up, Location, Semi-Finalist #1, Semi-Finalist #2\nQ: who is the\u00a0runner-up\u00a0where\u00a0location\u00a0is ellenton, fl and\u00a0year\u00a0is 2004\nA: SELECT Runner-Up FROM 1-11214772-1 WHERE Location = 'Ellenton, FL' AND Year = '2004'"}
-{"text": "table: 1-11214212-1\ncolumns: Year, Numer of Jamaicans granted British citizenship, Naturalisation by residence, Naturalisation by marriage, Registration of a minor child, Registration by other means\nQ: what's the\u00a0naturalisation by marriage\u00a0with\u00a0numer of jamaicans granted british citizenship\u00a0being 3165\nA: SELECT Naturalisation by marriage FROM 1-11214212-1 WHERE Numer of Jamaicans granted British citizenship = 3165"}
-{"text": "table: 1-11214212-1\ncolumns: Year, Numer of Jamaicans granted British citizenship, Naturalisation by residence, Naturalisation by marriage, Registration of a minor child, Registration by other means\nQ: how many\u00a0numer of jamaicans granted british citizenship\u00a0with\u00a0naturalisation by marriage\u00a0being 1060\nA: SELECT COUNT Numer of Jamaicans granted British citizenship FROM 1-11214212-1 WHERE Naturalisation by marriage = 1060"}
-{"text": "table: 1-11214212-1\ncolumns: Year, Numer of Jamaicans granted British citizenship, Naturalisation by residence, Naturalisation by marriage, Registration of a minor child, Registration by other means\nQ: what's the\u00a0naturalisation by marriage\u00a0with\u00a0regbeingtration of a minor child\u00a0being 114\nA: SELECT Naturalisation by marriage FROM 1-11214212-1 WHERE Registration of a minor child = 114"}
-{"text": "table: 1-11214212-1\ncolumns: Year, Numer of Jamaicans granted British citizenship, Naturalisation by residence, Naturalisation by marriage, Registration of a minor child, Registration by other means\nQ: what's the\u00a0numer of jamaicans granted british citizenship\u00a0with\u00a0naturalisation by residence\u00a0being 927\nA: SELECT Numer of Jamaicans granted British citizenship FROM 1-11214212-1 WHERE Naturalisation by residence = 927"}
-{"text": "table: 1-11214212-1\ncolumns: Year, Numer of Jamaicans granted British citizenship, Naturalisation by residence, Naturalisation by marriage, Registration of a minor child, Registration by other means\nQ: what is the maximum\u00a0year\u00a0with\u00a0registration of a minor child\u00a0being 281\nA: SELECT MAX Year FROM 1-11214212-1 WHERE Registration of a minor child = 281"}
-{"text": "table: 1-11220799-2\ncolumns: Episode Titles, First air date, Reward, Immunity, Exiled, Eliminated, Vote, Finish\nQ: How many episodes had their first air date on March 6, 2008?\nA: SELECT COUNT Episode Titles FROM 1-11220799-2 WHERE First air date = 'March 6, 2008'"}
-{"text": "table: 1-11220799-2\ncolumns: Episode Titles, First air date, Reward, Immunity, Exiled, Eliminated, Vote, Finish\nQ: What were the results of episodes with the first air date of March 6, 2008?\nA: SELECT Finish FROM 1-11220799-2 WHERE First air date = 'March 6, 2008'"}
-{"text": "table: 1-11230937-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many millions of viewers watched episode 15?\nA: SELECT U.S. viewers (millions) FROM 1-11230937-2 WHERE No. in season = 15"}
-{"text": "table: 1-11230937-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many millions of viewers watched the \"Throwing Heat\" episode?\nA: SELECT U.S. viewers (millions) FROM 1-11230937-2 WHERE Title = '\"Throwing Heat\"'"}
-{"text": "table: 1-11230937-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many millions of viewers watched the episode directed by Anthony Hemingway?\nA: SELECT U.S. viewers (millions) FROM 1-11230937-2 WHERE Directed by = 'Anthony Hemingway'"}
-{"text": "table: 1-11222744-2\ncolumns: Year, Title, Format, Studio, Release Date, Copyright Information, Catalog Number\nQ: The Catalog number is 80809 what is the title?\nA: SELECT Title FROM 1-11222744-2 WHERE Catalog Number = '80809'"}
-{"text": "table: 1-11222744-2\ncolumns: Year, Title, Format, Studio, Release Date, Copyright Information, Catalog Number\nQ: where title is beginning callanetics , what is the total of format ?\nA: SELECT COUNT Format FROM 1-11222744-2 WHERE Title = 'Beginning Callanetics'"}
-{"text": "table: 1-11222744-2\ncolumns: Year, Title, Format, Studio, Release Date, Copyright Information, Catalog Number\nQ: where catalog number is 81258 , what are all the studio ?\nA: SELECT Studio FROM 1-11222744-2 WHERE Catalog Number = '81258'"}
-{"text": "table: 1-11222744-2\ncolumns: Year, Title, Format, Studio, Release Date, Copyright Information, Catalog Number\nQ: where title is am/pm callanetics , what are all the copyright information?\nA: SELECT Copyright Information FROM 1-11222744-2 WHERE Title = 'AM/PM Callanetics'"}
-{"text": "table: 1-11236195-2\ncolumns: Season, Grand FinalDate, WinningTeam, Score, LosingTeam, Location, GF Attendance, Clive Churchill Medal\nQ: What was the GF attendance at the location of Sydney Football Stadium, Sydney (6)?\nA: SELECT COUNT GF Attendance FROM 1-11236195-2 WHERE Location = 'Sydney Football Stadium, Sydney (6)'"}
-{"text": "table: 1-11236195-2\ncolumns: Season, Grand FinalDate, WinningTeam, Score, LosingTeam, Location, GF Attendance, Clive Churchill Medal\nQ: Which losing team had a score of 24-12?\nA: SELECT LosingTeam FROM 1-11236195-2 WHERE Score = '24-12'"}
-{"text": "table: 1-11236195-2\ncolumns: Season, Grand FinalDate, WinningTeam, Score, LosingTeam, Location, GF Attendance, Clive Churchill Medal\nQ: What was the losing team in the 1993 season?\nA: SELECT LosingTeam FROM 1-11236195-2 WHERE Season = 1993"}
-{"text": "table: 1-1123802-1\ncolumns: Engine, Power, continuous, Critical altitude This is the highest altitude at which the engine can achieve its full continuous power rating. Above this altitude, power falls off with height as with a naturally aspirated engine . See Supercharger#Altitude effects for details., Power, takeoff, Compression ratio, Supercharger gear ratio, Octane rating, Dry weight\nQ: What was the compression ration when the engine was Wasp Jr. T1B2?\nA: SELECT Compression ratio FROM 1-1123802-1 WHERE Engine = 'Wasp Jr. T1B2'"}
-{"text": "table: 1-1123802-1\ncolumns: Engine, Power, continuous, Critical altitude This is the highest altitude at which the engine can achieve its full continuous power rating. Above this altitude, power falls off with height as with a naturally aspirated engine . See Supercharger#Altitude effects for details., Power, takeoff, Compression ratio, Supercharger gear ratio, Octane rating, Dry weight\nQ: What is the compression ration when the continuous power is hp (kw) at 2,200 RPM and the octane rating is 80/87?\nA: SELECT Compression ratio FROM 1-1123802-1 WHERE Power, continuous = 'hp (kW) at 2,200 RPM' AND Octane rating = '80/87'"}
-{"text": "table: 1-1123802-1\ncolumns: Engine, Power, continuous, Critical altitude This is the highest altitude at which the engine can achieve its full continuous power rating. Above this altitude, power falls off with height as with a naturally aspirated engine . See Supercharger#Altitude effects for details., Power, takeoff, Compression ratio, Supercharger gear ratio, Octane rating, Dry weight\nQ: What is the compression ratio when the continuous power is hp (KW) at 2,200 RPM and the critical altitude is at sea level?\nA: SELECT COUNT Compression ratio FROM 1-1123802-1 WHERE Power, continuous = 'hp (kW) at 2,200 RPM' AND Critical altitude This is the highest altitude at which the engine can achieve its full continuous power rating. Above this altitude, power falls off with height as with a naturally aspirated engine . See Supercharger#Altitude effects for details. = 'sea level'"}
-{"text": "table: 1-1123802-1\ncolumns: Engine, Power, continuous, Critical altitude This is the highest altitude at which the engine can achieve its full continuous power rating. Above this altitude, power falls off with height as with a naturally aspirated engine . See Supercharger#Altitude effects for details., Power, takeoff, Compression ratio, Supercharger gear ratio, Octane rating, Dry weight\nQ: When the engine is Wasp Jr. T1B2, what is the number needed for takeoff power?\nA: SELECT COUNT Power, takeoff FROM 1-1123802-1 WHERE Engine = 'Wasp Jr. T1B2'"}
-{"text": "table: 1-1123802-1\ncolumns: Engine, Power, continuous, Critical altitude This is the highest altitude at which the engine can achieve its full continuous power rating. Above this altitude, power falls off with height as with a naturally aspirated engine . See Supercharger#Altitude effects for details., Power, takeoff, Compression ratio, Supercharger gear ratio, Octane rating, Dry weight\nQ: When critical altitude is sea level, what is the compression ration for a supercharger gear ratio of 7:1?\nA: SELECT Compression ratio FROM 1-1123802-1 WHERE Critical altitude This is the highest altitude at which the engine can achieve its full continuous power rating. Above this altitude, power falls off with height as with a naturally aspirated engine . See Supercharger#Altitude effects for details. = 'sea level' AND Supercharger gear ratio = '7:1'"}
-{"text": "table: 1-11235334-2\ncolumns: #, Episode, Air Date, Timeslot, Viewers, Weekly Rank for Living\nQ: How many episodes aired on october 27, 2008\nA: SELECT COUNT Episode FROM 1-11235334-2 WHERE Air Date = 'October 27, 2008'"}
-{"text": "table: 1-11235334-2\ncolumns: #, Episode, Air Date, Timeslot, Viewers, Weekly Rank for Living\nQ: The episode \"chapter five: dressed to kill\" had a weekly ranking of what?\nA: SELECT Weekly Rank for Living FROM 1-11235334-2 WHERE Episode = '\"Chapter Five: Dressed to Kill\"'"}
-{"text": "table: 1-11235334-2\ncolumns: #, Episode, Air Date, Timeslot, Viewers, Weekly Rank for Living\nQ: what is the most # that aired on september 29, 2008?\nA: SELECT MAX # FROM 1-11235334-2 WHERE Air Date = 'September 29, 2008'"}
-{"text": "table: 1-11236195-5\ncolumns: Season, Grand FinalDate, WinningTeam, Score, LosingTeam, Location, GF Attendance, Clive Churchill Medal\nQ: How many seasons did the canterbury bulldogs (8) win?\nA: SELECT COUNT Season FROM 1-11236195-5 WHERE WinningTeam = 'Canterbury Bulldogs (8)'"}
-{"text": "table: 1-11236195-5\ncolumns: Season, Grand FinalDate, WinningTeam, Score, LosingTeam, Location, GF Attendance, Clive Churchill Medal\nQ: How many teams lost at the sydney football stadium, sydney (11)?\nA: SELECT COUNT LosingTeam FROM 1-11236195-5 WHERE Location = 'Sydney Football Stadium, Sydney (11)'"}
-{"text": "table: 1-11236195-5\ncolumns: Season, Grand FinalDate, WinningTeam, Score, LosingTeam, Location, GF Attendance, Clive Churchill Medal\nQ: What was the date that the st. george-illawarra dragons lost?\nA: SELECT Grand FinalDate FROM 1-11236195-5 WHERE LosingTeam = 'St. George-Illawarra Dragons'"}
-{"text": "table: 1-11236195-5\ncolumns: Season, Grand FinalDate, WinningTeam, Score, LosingTeam, Location, GF Attendance, Clive Churchill Medal\nQ: Brett kimmorley, who was chosen for the clive churchill medal belonged to what team?\nA: SELECT WinningTeam FROM 1-11236195-5 WHERE Clive Churchill Medal = 'Brett Kimmorley'"}
-{"text": "table: 1-11244302-1\ncolumns: #, Episode, Air Date, Time slot (EST), Rating, Share, 18-49 (Rating/Share), Viewers (m), Rank (Overall)\nQ: What time slots have a 6.3 rating\nA: SELECT Time slot (EST) FROM 1-11244302-1 WHERE Rating = '6.3'"}
-{"text": "table: 1-11244302-1\ncolumns: #, Episode, Air Date, Time slot (EST), Rating, Share, 18-49 (Rating/Share), Viewers (m), Rank (Overall)\nQ: What time slot is the episode \"the way we weren't\" in\nA: SELECT Time slot (EST) FROM 1-11244302-1 WHERE Episode = '\"The Way We Weren't\"'"}
-{"text": "table: 1-11244302-1\ncolumns: #, Episode, Air Date, Time slot (EST), Rating, Share, 18-49 (Rating/Share), Viewers (m), Rank (Overall)\nQ: What time slot is the episode \"who's your daddy\" in\nA: SELECT Time slot (EST) FROM 1-11244302-1 WHERE Episode = '\"Who's Your Daddy\"'"}
-{"text": "table: 1-11244302-1\ncolumns: #, Episode, Air Date, Time slot (EST), Rating, Share, 18-49 (Rating/Share), Viewers (m), Rank (Overall)\nQ: Which air date had an 11 share\nA: SELECT Air Date FROM 1-11244302-1 WHERE Share = 11"}
-{"text": "table: 1-11244302-1\ncolumns: #, Episode, Air Date, Time slot (EST), Rating, Share, 18-49 (Rating/Share), Viewers (m), Rank (Overall)\nQ: Which air date had the 18-49 rating/share of 3.3/9\nA: SELECT Air Date FROM 1-11244302-1 WHERE 18-49 (Rating/Share) = '3.3/9'"}
-{"text": "table: 1-11240028-3\ncolumns: Character, Portrayed by, Relationship, First appearance, Last appearance\nQ: Which characters had their first experience in the episode \"consequences\"?\nA: SELECT Character FROM 1-11240028-3 WHERE First appearance = '\"Consequences\"'"}
-{"text": "table: 1-11240028-3\ncolumns: Character, Portrayed by, Relationship, First appearance, Last appearance\nQ: What episode had the last appearances of the late wife of mac taylor?\nA: SELECT Last appearance FROM 1-11240028-3 WHERE Relationship = 'Late wife of Mac Taylor'"}
-{"text": "table: 1-11240028-3\ncolumns: Character, Portrayed by, Relationship, First appearance, Last appearance\nQ: Which characters were portrayed by reed garrett?\nA: SELECT Portrayed by FROM 1-11240028-3 WHERE Character = 'Reed Garrett'"}
-{"text": "table: 1-11240028-3\ncolumns: Character, Portrayed by, Relationship, First appearance, Last appearance\nQ: How many characters were portrayed by the informant of don flack?\nA: SELECT COUNT Portrayed by FROM 1-11240028-3 WHERE Relationship = 'Informant of Don Flack'"}
-{"text": "table: 1-11240028-3\ncolumns: Character, Portrayed by, Relationship, First appearance, Last appearance\nQ: What episode was the last appearance of the character, rikki sandoval?\nA: SELECT Last appearance FROM 1-11240028-3 WHERE Character = 'Rikki Sandoval'"}
-{"text": "table: 1-11240028-1\ncolumns: Character, Portrayed by, First appearance, Last appearance, Duration, Episodes\nQ: On which episode did actress Sela Ward make her last appearance?\nA: SELECT Last appearance FROM 1-11240028-1 WHERE Portrayed by = 'Sela Ward'"}
-{"text": "table: 1-11240028-1\ncolumns: Character, Portrayed by, First appearance, Last appearance, Duration, Episodes\nQ: Which actors first appeared in \"Zoo York\"?\nA: SELECT Portrayed by FROM 1-11240028-1 WHERE First appearance = '\"Zoo York\"'"}
-{"text": "table: 1-11240028-1\ncolumns: Character, Portrayed by, First appearance, Last appearance, Duration, Episodes\nQ: How many episodes did actress Vanessa Ferlito appear in?\nA: SELECT Episodes FROM 1-11240028-1 WHERE Portrayed by = 'Vanessa Ferlito'"}
-{"text": "table: 1-11240028-1\ncolumns: Character, Portrayed by, First appearance, Last appearance, Duration, Episodes\nQ: Which actors first appeared in episode \"Blink\" 1, 2, 3?\nA: SELECT Portrayed by FROM 1-11240028-1 WHERE First appearance = '\"Blink\" 1, 2, 3'"}
-{"text": "table: 1-11240028-1\ncolumns: Character, Portrayed by, First appearance, Last appearance, Duration, Episodes\nQ: What was the duration of Robert Joy's portrayal?\nA: SELECT COUNT Duration FROM 1-11240028-1 WHERE Portrayed by = 'Robert Joy'"}
-{"text": "table: 1-11240028-1\ncolumns: Character, Portrayed by, First appearance, Last appearance, Duration, Episodes\nQ: Which episode did actor A. J. Buckley last appear in?\nA: SELECT Last appearance FROM 1-11240028-1 WHERE Portrayed by = 'A. J. Buckley'"}
-{"text": "table: 1-11250-4\ncolumns: Club, Position in 2012\u201313, First season in top division, Number of seasons in top division, Number of seasons in the Premier League, First season of current spell in top division, Top division titles, Last top division title\nQ: What is the least top division titles?\nA: SELECT MIN Top division titles FROM 1-11250-4"}
-{"text": "table: 1-11250-4\ncolumns: Club, Position in 2012\u201313, First season in top division, Number of seasons in top division, Number of seasons in the Premier League, First season of current spell in top division, Top division titles, Last top division title\nQ: What is the least number of seasons in top division?\nA: SELECT MIN Number of seasons in top division FROM 1-11250-4"}
-{"text": "table: 1-11253290-2\ncolumns: #, Episode, Rating, Share, Rating/Share (18-49), Viewers (millions), Rank (timeslot), Rank (night), Rank (week)\nQ: How many viewers (millions) were there for rank (week) 20?\nA: SELECT COUNT Viewers (millions) FROM 1-11253290-2 WHERE Rank (week) = '20'"}
-{"text": "table: 1-11253290-2\ncolumns: #, Episode, Rating, Share, Rating/Share (18-49), Viewers (millions), Rank (timeslot), Rank (night), Rank (week)\nQ: What is the rank (timeslot) with the episode name \"dangerous liaisons\"?\nA: SELECT Rank (timeslot) FROM 1-11253290-2 WHERE Episode = '\"Dangerous Liaisons\"'"}
-{"text": "table: 1-11253290-2\ncolumns: #, Episode, Rating, Share, Rating/Share (18-49), Viewers (millions), Rank (timeslot), Rank (night), Rank (week)\nQ: What is the lowest rank (night) for having viewers (millions) 5.25?\nA: SELECT MIN Rank (night) FROM 1-11253290-2 WHERE Viewers (millions) = '5.25'"}
-{"text": "table: 1-11253290-2\ncolumns: #, Episode, Rating, Share, Rating/Share (18-49), Viewers (millions), Rank (timeslot), Rank (night), Rank (week)\nQ: How many times was the episode named \"conference call\"?\nA: SELECT COUNT # FROM 1-11253290-2 WHERE Episode = '\"Conference Call\"'"}
-{"text": "table: 1-11253290-2\ncolumns: #, Episode, Rating, Share, Rating/Share (18-49), Viewers (millions), Rank (timeslot), Rank (night), Rank (week)\nQ: How many times was the rank (night) 11?\nA: SELECT COUNT Viewers (millions) FROM 1-11253290-2 WHERE Rank (night) = 11"}
-{"text": "table: 1-11251601-2\ncolumns: Country, Carbon dioxide emissions per year (10 6 Tons) (2006), Percentage of global total, Avg. emission per km 2 of its land (tons), Carbon dioxide emissions per year (Tons per person) (2007)\nQ: WHAT WAS THE AMOUNT OF CARBON DIOXIDE EMISSIONS IN 2006 IN THE COUNTRY WHOSE CO2 EMISSIONS (TONS PER PERSON) REACHED 1.4 IN 2OO7?\nA: SELECT Carbon dioxide emissions per year (10 6 Tons) (2006) FROM 1-11251601-2 WHERE Carbon dioxide emissions per year (Tons per person) (2007) = '1.4'"}
-{"text": "table: 1-11251601-2\ncolumns: Country, Carbon dioxide emissions per year (10 6 Tons) (2006), Percentage of global total, Avg. emission per km 2 of its land (tons), Carbon dioxide emissions per year (Tons per person) (2007)\nQ: HOW MANY TONS OF CO2 EMISSIONS DID RUSSIA PRODUCE IN 2006?\nA: SELECT MAX Carbon dioxide emissions per year (10 6 Tons) (2006) FROM 1-11251601-2 WHERE Country = 'Russia'"}
-{"text": "table: 1-11251601-2\ncolumns: Country, Carbon dioxide emissions per year (10 6 Tons) (2006), Percentage of global total, Avg. emission per km 2 of its land (tons), Carbon dioxide emissions per year (Tons per person) (2007)\nQ: WHAT PERCENTAGE OF GLOBAL TOTAL EMISSIONS DID INDIA PRODUCE?\nA: SELECT Percentage of global total FROM 1-11251601-2 WHERE Country = 'India'"}
-{"text": "table: 1-11251601-2\ncolumns: Country, Carbon dioxide emissions per year (10 6 Tons) (2006), Percentage of global total, Avg. emission per km 2 of its land (tons), Carbon dioxide emissions per year (Tons per person) (2007)\nQ: HOW MUCH IS THE PERCENTAGE OF GLOBAL TOTAL EMISSIONS IN THE COUNTRY THAT PRODUCED 4.9 TONS PER PERSON IN 2007?\nA: SELECT Percentage of global total FROM 1-11251601-2 WHERE Carbon dioxide emissions per year (Tons per person) (2007) = '4.9'"}
-{"text": "table: 1-11251601-2\ncolumns: Country, Carbon dioxide emissions per year (10 6 Tons) (2006), Percentage of global total, Avg. emission per km 2 of its land (tons), Carbon dioxide emissions per year (Tons per person) (2007)\nQ: WHAT WAS THE AVERAGE EMISSION PER KM 2 IN INDIA?\nA: SELECT MAX Avg. emission per km 2 of its land (tons) FROM 1-11251601-2 WHERE Country = 'India'"}
-{"text": "table: 1-11251109-3\ncolumns: #, Episode, Air Date, Timeslot (EST), Season, Rating, Share, 18\u201349, Viewers (m), Rank (#)\nQ: What is the rank number that aired october 26, 2007?\nA: SELECT Rank (#) FROM 1-11251109-3 WHERE Air Date = 'October 26, 2007'"}
-{"text": "table: 1-11251109-3\ncolumns: #, Episode, Air Date, Timeslot (EST), Season, Rating, Share, 18\u201349, Viewers (m), Rank (#)\nQ: What is the number of rank with the viewership of 5.96 million?\nA: SELECT COUNT Rank (#) FROM 1-11251109-3 WHERE Viewers (m) = '5.96'"}
-{"text": "table: 1-11251109-3\ncolumns: #, Episode, Air Date, Timeslot (EST), Season, Rating, Share, 18\u201349, Viewers (m), Rank (#)\nQ: What is the viewership on november 9, 2007?\nA: SELECT Viewers (m) FROM 1-11251109-3 WHERE Air Date = 'November 9, 2007'"}
-{"text": "table: 1-11254821-2\ncolumns: Finishing position, Points awarded (Platinum), Points awarded (Gold), Points awarded (Silver), Points awarded (Satellite)\nQ: How many platinum points were awarded when 6 gold points were awarded?\nA: SELECT MAX Points awarded (Platinum) FROM 1-11254821-2 WHERE Points awarded (Gold) = 6"}
-{"text": "table: 1-11254821-2\ncolumns: Finishing position, Points awarded (Platinum), Points awarded (Gold), Points awarded (Silver), Points awarded (Satellite)\nQ: What was the range of finishing position for 15 awarded platinum points?\nA: SELECT Finishing position FROM 1-11254821-2 WHERE Points awarded (Platinum) = 15"}
-{"text": "table: 1-11254821-2\ncolumns: Finishing position, Points awarded (Platinum), Points awarded (Gold), Points awarded (Silver), Points awarded (Satellite)\nQ: How many platinum points were awarded for 5th place?\nA: SELECT MAX Points awarded (Platinum) FROM 1-11254821-2 WHERE Finishing position = '5th'"}
-{"text": "table: 1-11254821-2\ncolumns: Finishing position, Points awarded (Platinum), Points awarded (Gold), Points awarded (Silver), Points awarded (Satellite)\nQ: How many platinum points were awarded when 70 silver points were awarded?\nA: SELECT Points awarded (Platinum) FROM 1-11254821-2 WHERE Points awarded (Silver) = 70"}
-{"text": "table: 1-11254821-2\ncolumns: Finishing position, Points awarded (Platinum), Points awarded (Gold), Points awarded (Silver), Points awarded (Satellite)\nQ: How many platinum points were awarded when 9 gold points were awarded?\nA: SELECT Points awarded (Platinum) FROM 1-11254821-2 WHERE Points awarded (Gold) = 9"}
-{"text": "table: 1-11274401-2\ncolumns: No., Episode, Air Date, Timeslot, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Rank (#)\nQ: How did the episode rank that had 2.65 million viewers?\nA: SELECT Rank (#) FROM 1-11274401-2 WHERE Viewers (m) = '2.65'"}
-{"text": "table: 1-11274401-2\ncolumns: No., Episode, Air Date, Timeslot, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Rank (#)\nQ: What was the share for the first episode that ranked 85?\nA: SELECT MIN Share FROM 1-11274401-2 WHERE Rank (#) = '85'"}
-{"text": "table: 1-11274401-2\ncolumns: No., Episode, Air Date, Timeslot, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Rank (#)\nQ: Which timeslot did episode no. 15 hold?\nA: SELECT Timeslot FROM 1-11274401-2 WHERE No. = 15"}
-{"text": "table: 1-11274401-3\ncolumns: No., Episode, Air Date, Timeslot, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Rank (#)\nQ: What was the timeslot for the episode that aired on May 12, 2009?\nA: SELECT Timeslot FROM 1-11274401-3 WHERE Air Date = 'May 12, 2009'"}
-{"text": "table: 1-11274401-3\ncolumns: No., Episode, Air Date, Timeslot, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Rank (#)\nQ: What's the 18-49 (rating/share) of the episode that originally aired on May 5, 2009?\nA: SELECT 18\u201349 (Rating/Share) FROM 1-11274401-3 WHERE Air Date = 'May 5, 2009'"}
-{"text": "table: 1-11274401-3\ncolumns: No., Episode, Air Date, Timeslot, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Rank (#)\nQ: What's the total number of episodes whose original airings were viewed by 1.82 million viewers?\nA: SELECT COUNT Air Date FROM 1-11274401-3 WHERE Viewers (m) = '1.82'"}
-{"text": "table: 1-11274401-3\ncolumns: No., Episode, Air Date, Timeslot, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Rank (#)\nQ: What's the rating of the episode originally aired on May 5, 2009?\nA: SELECT Rating FROM 1-11274401-3 WHERE Air Date = 'May 5, 2009'"}
-{"text": "table: 1-11274401-3\ncolumns: No., Episode, Air Date, Timeslot, Rating, Share, 18\u201349 (Rating/Share), Viewers (m), Rank (#)\nQ: What episode was seen by 2.05 million viewers?\nA: SELECT Episode FROM 1-11274401-3 WHERE Viewers (m) = '2.05'"}
-{"text": "table: 1-11256021-1\ncolumns: Date, Founder, Extroversion Scales, People-task orientation scale, Introverted, Task-Oriented, Extroverted, Task-Oriented, Extroverted, Relationship-Oriented, Introverted, Relationship Oriented, Moderate\nQ: what's the\u00a0extroverted, relationship-oriented\u00a0where\u00a0extroverted, task-oriented\u00a0is director\nA: SELECT Extroverted, Relationship-Oriented FROM 1-11256021-1 WHERE Extroverted, Task-Oriented = 'Director'"}
-{"text": "table: 1-11256021-1\ncolumns: Date, Founder, Extroversion Scales, People-task orientation scale, Introverted, Task-Oriented, Extroverted, Task-Oriented, Extroverted, Relationship-Oriented, Introverted, Relationship Oriented, Moderate\nQ: what's the\u00a0extroverted, relationship-oriented\u00a0where\u00a0moderate\u00a0is introverted sanguine\nA: SELECT Extroverted, Relationship-Oriented FROM 1-11256021-1 WHERE Moderate = 'Introverted Sanguine'"}
-{"text": "table: 1-11256021-1\ncolumns: Date, Founder, Extroversion Scales, People-task orientation scale, Introverted, Task-Oriented, Extroverted, Task-Oriented, Extroverted, Relationship-Oriented, Introverted, Relationship Oriented, Moderate\nQ: what's the\u00a0founder\u00a0where\u00a0moderate\u00a0is ether\nA: SELECT Founder FROM 1-11256021-1 WHERE Moderate = 'ether'"}
-{"text": "table: 1-11256021-1\ncolumns: Date, Founder, Extroversion Scales, People-task orientation scale, Introverted, Task-Oriented, Extroverted, Task-Oriented, Extroverted, Relationship-Oriented, Introverted, Relationship Oriented, Moderate\nQ: what's the\u00a0extroverted, relationship-oriented\u00a0where\u00a0date\u00a0is c. 1928\nA: SELECT Extroverted, Relationship-Oriented FROM 1-11256021-1 WHERE Date = 'c. 1928'"}
-{"text": "table: 1-11256021-1\ncolumns: Date, Founder, Extroversion Scales, People-task orientation scale, Introverted, Task-Oriented, Extroverted, Task-Oriented, Extroverted, Relationship-Oriented, Introverted, Relationship Oriented, Moderate\nQ: who is the\u00a0founder\u00a0where\u00a0date\u00a0is c. 1900\nA: SELECT Founder FROM 1-11256021-1 WHERE Date = 'c. 1900'"}
-{"text": "table: 1-11256021-1\ncolumns: Date, Founder, Extroversion Scales, People-task orientation scale, Introverted, Task-Oriented, Extroverted, Task-Oriented, Extroverted, Relationship-Oriented, Introverted, Relationship Oriented, Moderate\nQ: what's the\u00a0people-task orientation scale\u00a0where\u00a0extroverted, relationship-oriented\u00a0is team type\nA: SELECT People-task orientation scale FROM 1-11256021-1 WHERE Extroverted, Relationship-Oriented = 'Team Type'"}
-{"text": "table: 1-11303072-5\ncolumns: Wicket, Runs, Batting partners, Batting team, Fielding team, Venue, Season\nQ: What is the batting team where the runs are 276?\nA: SELECT Batting team FROM 1-11303072-5 WHERE Runs = '276'"}
-{"text": "table: 1-11303072-5\ncolumns: Wicket, Runs, Batting partners, Batting team, Fielding team, Venue, Season\nQ: Name the batting team at Durham\nA: SELECT Batting team FROM 1-11303072-5 WHERE Fielding team = 'Durham'"}
-{"text": "table: 1-11303072-5\ncolumns: Wicket, Runs, Batting partners, Batting team, Fielding team, Venue, Season\nQ: What is the batting team with the batting partnets of thilina kandamby and rangana herath?\nA: SELECT Batting team FROM 1-11303072-5 WHERE Batting partners = 'Thilina Kandamby and Rangana Herath'"}
-{"text": "table: 1-11303072-5\ncolumns: Wicket, Runs, Batting partners, Batting team, Fielding team, Venue, Season\nQ: What is the fielding team with 155 runs?\nA: SELECT Fielding team FROM 1-11303072-5 WHERE Runs = '155'"}
-{"text": "table: 1-11303072-5\ncolumns: Wicket, Runs, Batting partners, Batting team, Fielding team, Venue, Season\nQ: What is the batting partners with runs of 226?\nA: SELECT Batting partners FROM 1-11303072-5 WHERE Runs = '226'"}
-{"text": "table: 1-11303072-9\ncolumns: Rank, Dismissals, Player, Nationality, Catches, Stumpings, Career Span\nQ: What is the nationality of David Bairstow?\nA: SELECT Nationality FROM 1-11303072-9 WHERE Player = 'David Bairstow'"}
-{"text": "table: 1-11303072-9\ncolumns: Rank, Dismissals, Player, Nationality, Catches, Stumpings, Career Span\nQ: What are the players whose rank is 2?\nA: SELECT Player FROM 1-11303072-9 WHERE Rank = 2"}
-{"text": "table: 1-11303072-9\ncolumns: Rank, Dismissals, Player, Nationality, Catches, Stumpings, Career Span\nQ: How many stumpings has Paul Nixon in his career?\nA: SELECT Stumpings FROM 1-11303072-9 WHERE Player = 'Paul Nixon'"}
-{"text": "table: 1-11303072-9\ncolumns: Rank, Dismissals, Player, Nationality, Catches, Stumpings, Career Span\nQ: Where is Adam Gilchrist from?\nA: SELECT Nationality FROM 1-11303072-9 WHERE Player = 'Adam Gilchrist'"}
-{"text": "table: 1-1130632-1\ncolumns: No. in series, Title, Directed by, Written by, Featured character(s), Original air date, U.S. viewers (million)\nQ: What are the title that have 19.48 million u.s. viewers?\nA: SELECT Title FROM 1-1130632-1 WHERE U.S. viewers (million) = '19.48'"}
-{"text": "table: 1-1130632-1\ncolumns: No. in series, Title, Directed by, Written by, Featured character(s), Original air date, U.S. viewers (million)\nQ: Which titles have 18.73 u.s. viewers.\nA: SELECT Title FROM 1-1130632-1 WHERE U.S. viewers (million) = '18.73'"}
-{"text": "table: 1-1130632-1\ncolumns: No. in series, Title, Directed by, Written by, Featured character(s), Original air date, U.S. viewers (million)\nQ: Who wrote all the shows with 18.73 u.s. viewers?\nA: SELECT Written by FROM 1-1130632-1 WHERE U.S. viewers (million) = '18.73'"}
-{"text": "table: 1-1131183-2\ncolumns: Rank ( WJC ), Rank (ARDA), Metro area, Number of Jews (WJC), Number of Jews (ASARB)\nQ: What is the rank where the area is Los Angeles?\nA: SELECT Rank ( WJC ) FROM 1-1131183-2 WHERE Metro area = 'Los Angeles'"}
-{"text": "table: 1-1131183-2\ncolumns: Rank ( WJC ), Rank (ARDA), Metro area, Number of Jews (WJC), Number of Jews (ASARB)\nQ: What is the number of jews where the rank is 1?\nA: SELECT COUNT Number of Jews (WJC) FROM 1-1131183-2 WHERE Rank (ARDA) = 1"}
-{"text": "table: 1-1131183-2\ncolumns: Rank ( WJC ), Rank (ARDA), Metro area, Number of Jews (WJC), Number of Jews (ASARB)\nQ: What is the number of jews asarb where the metro area is philadelphia?\nA: SELECT Number of Jews (ASARB) FROM 1-1131183-2 WHERE Metro area = 'Philadelphia'"}
-{"text": "table: 1-11318462-5\ncolumns: Crew, Open 1st VIII, Open 2nd VIII, Open 3rd VIII, U16 1st VIII, U16 2nd VIII, U16 3rd VIII, U15 1st IV, U15 2nd IV, U15 3rd IV, U15 4th IV, U15 5th IV, U15 6th IV\nQ: what are all the open 1st viii with u15 6th iv being bgs\nA: SELECT Open 1st VIII FROM 1-11318462-5 WHERE U15 6th IV = 'BGS'"}
-{"text": "table: 1-11318462-5\ncolumns: Crew, Open 1st VIII, Open 2nd VIII, Open 3rd VIII, U16 1st VIII, U16 2nd VIII, U16 3rd VIII, U15 1st IV, U15 2nd IV, U15 3rd IV, U15 4th IV, U15 5th IV, U15 6th IV\nQ: what are all the u16 2nd viii with u15 3rd iv being bbc\nA: SELECT U16 2nd VIII FROM 1-11318462-5 WHERE U15 3rd IV = 'BBC'"}
-{"text": "table: 1-11318462-5\ncolumns: Crew, Open 1st VIII, Open 2nd VIII, Open 3rd VIII, U16 1st VIII, U16 2nd VIII, U16 3rd VIII, U15 1st IV, U15 2nd IV, U15 3rd IV, U15 4th IV, U15 5th IV, U15 6th IV\nQ: what are all the open 1st viii with u15 4th iv being gt\nA: SELECT Open 1st VIII FROM 1-11318462-5 WHERE U15 4th IV = 'GT'"}
-{"text": "table: 1-11318462-5\ncolumns: Crew, Open 1st VIII, Open 2nd VIII, Open 3rd VIII, U16 1st VIII, U16 2nd VIII, U16 3rd VIII, U15 1st IV, U15 2nd IV, U15 3rd IV, U15 4th IV, U15 5th IV, U15 6th IV\nQ: how many crew had u15 3rd iv being bgs and u15 1st iv being acgs and open 1st viii being acgs\nA: SELECT COUNT Crew FROM 1-11318462-5 WHERE U15 3rd IV = 'BGS' AND U15 1st IV = 'ACGS' AND Open 1st VIII = 'ACGS'"}
-{"text": "table: 1-11318462-5\ncolumns: Crew, Open 1st VIII, Open 2nd VIII, Open 3rd VIII, U16 1st VIII, U16 2nd VIII, U16 3rd VIII, U15 1st IV, U15 2nd IV, U15 3rd IV, U15 4th IV, U15 5th IV, U15 6th IV\nQ: what are all the u15 3rd iv with u15 4th iv being bbc\nA: SELECT U15 3rd IV FROM 1-11318462-5 WHERE U15 4th IV = 'BBC'"}
-{"text": "table: 1-11318462-5\ncolumns: Crew, Open 1st VIII, Open 2nd VIII, Open 3rd VIII, U16 1st VIII, U16 2nd VIII, U16 3rd VIII, U15 1st IV, U15 2nd IV, U15 3rd IV, U15 4th IV, U15 5th IV, U15 6th IV\nQ: how many open 2nd viii had u15 3rd iv being gt\nA: SELECT COUNT Open 2nd VIII FROM 1-11318462-5 WHERE U15 3rd IV = 'GT'"}
-{"text": "table: 1-11318462-29\ncolumns: School, Location, Enrolment, Founded, Denomination, Day/Boarding, School Colours, Abbreviation, In competition since\nQ: How many schools have an enrollment of 850?\nA: SELECT COUNT Founded FROM 1-11318462-29 WHERE Enrolment = 850"}
-{"text": "table: 1-11318462-29\ncolumns: School, Location, Enrolment, Founded, Denomination, Day/Boarding, School Colours, Abbreviation, In competition since\nQ: What is the location of the school named Brisbane Girls' Grammar School?\nA: SELECT Location FROM 1-11318462-29 WHERE School = 'Brisbane Girls' Grammar School'"}
-{"text": "table: 1-11318462-29\ncolumns: School, Location, Enrolment, Founded, Denomination, Day/Boarding, School Colours, Abbreviation, In competition since\nQ: How many schools are located in South Brisbane?\nA: SELECT COUNT School FROM 1-11318462-29 WHERE Location = 'South Brisbane'"}
-{"text": "table: 1-11318462-29\ncolumns: School, Location, Enrolment, Founded, Denomination, Day/Boarding, School Colours, Abbreviation, In competition since\nQ: When was SPLC founded?\nA: SELECT MIN Founded FROM 1-11318462-29 WHERE Abbreviation = 'SPLC'"}
-{"text": "table: 1-11318462-29\ncolumns: School, Location, Enrolment, Founded, Denomination, Day/Boarding, School Colours, Abbreviation, In competition since\nQ: What is the enrollment of STM which has been in competition since 1990?\nA: SELECT COUNT Enrolment FROM 1-11318462-29 WHERE In competition since = 1990 AND Abbreviation = 'STM'"}
-{"text": "table: 1-1132568-3\ncolumns: Rd., Grand Prix, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What number is the Monaco Grand Prix?\nA: SELECT Rd. FROM 1-1132568-3 WHERE Grand Prix = 'Monaco Grand Prix'"}
-{"text": "table: 1-1132568-3\ncolumns: Rd., Grand Prix, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: Who is in the pole position for the French Grand Prix?\nA: SELECT Pole Position FROM 1-1132568-3 WHERE Grand Prix = 'French Grand Prix'"}
-{"text": "table: 1-1132568-3\ncolumns: Rd., Grand Prix, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What are the numbers for the raceways that are constructed by Ferrari, with Michael Schumacher holding the fastest lap and pole position?\nA: SELECT Rd. FROM 1-1132568-3 WHERE Fastest Lap = 'Michael Schumacher' AND Constructor = 'Ferrari' AND Pole Position = 'Michael Schumacher'"}
-{"text": "table: 1-1132568-3\ncolumns: Rd., Grand Prix, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: How many on the list are called the Austrian Grand Prix?\nA: SELECT COUNT Rd. FROM 1-1132568-3 WHERE Grand Prix = 'Austrian Grand Prix'"}
-{"text": "table: 1-1132568-3\ncolumns: Rd., Grand Prix, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What number is the Canadian Grand Prix on the list?\nA: SELECT Rd. FROM 1-1132568-3 WHERE Grand Prix = 'Canadian Grand Prix'"}
-{"text": "table: 1-1132588-3\ncolumns: Rd., Grand Prix, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What is the rd for the canadian grand prix?\nA: SELECT Rd. FROM 1-1132588-3 WHERE Grand Prix = 'Canadian Grand Prix'"}
-{"text": "table: 1-1132588-3\ncolumns: Rd., Grand Prix, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What is the fastest lap for the european grand prix?\nA: SELECT Fastest Lap FROM 1-1132588-3 WHERE Grand Prix = 'European Grand Prix'"}
-{"text": "table: 1-1132588-3\ncolumns: Rd., Grand Prix, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What is the pole position for the ferrari at the austrian grand prix?\nA: SELECT Pole Position FROM 1-1132588-3 WHERE Constructor = 'Ferrari' AND Grand Prix = 'Austrian Grand Prix'"}
-{"text": "table: 1-11326124-3\ncolumns: Edition, Zone, Round, Date, Against, Surface, Opponent, Outcome, Result\nQ: What was the result of round 2r?\nA: SELECT Outcome FROM 1-11326124-3 WHERE Round = '2R'"}
-{"text": "table: 1-11326124-3\ncolumns: Edition, Zone, Round, Date, Against, Surface, Opponent, Outcome, Result\nQ: Who did Tina Pisnik verse?\nA: SELECT Against FROM 1-11326124-3 WHERE Opponent = 'Tina Pisnik'"}
-{"text": "table: 1-11326124-3\ncolumns: Edition, Zone, Round, Date, Against, Surface, Opponent, Outcome, Result\nQ: How many rounds were 2r?\nA: SELECT COUNT Result FROM 1-11326124-3 WHERE Round = '2R'"}
-{"text": "table: 1-11326124-3\ncolumns: Edition, Zone, Round, Date, Against, Surface, Opponent, Outcome, Result\nQ: Name the outcome for round 2r\nA: SELECT Outcome FROM 1-11326124-3 WHERE Round = '2R'"}
-{"text": "table: 1-11354111-3\ncolumns: #, Episode, Air Date, Rating, Share, Rating/Share 18\u201349, Viewers (m), Timeslot Rank, Night Rank, Overall Rank\nQ: what's the night rank with viewers (m) of 6.63\nA: SELECT Night Rank FROM 1-11354111-3 WHERE Viewers (m) = '6.63'"}
-{"text": "table: 1-11354111-3\ncolumns: #, Episode, Air Date, Rating, Share, Rating/Share 18\u201349, Viewers (m), Timeslot Rank, Night Rank, Overall Rank\nQ: what's the overall rank with viewers (m) of 7.44\nA: SELECT Overall Rank FROM 1-11354111-3 WHERE Viewers (m) = '7.44'"}
-{"text": "table: 1-11354111-3\ncolumns: #, Episode, Air Date, Rating, Share, Rating/Share 18\u201349, Viewers (m), Timeslot Rank, Night Rank, Overall Rank\nQ: what's the overall rank with rating/share 18\u201349 of 2.1/5\nA: SELECT COUNT Overall Rank FROM 1-11354111-3 WHERE Rating/Share 18\u201349 = '2.1/5'"}
-{"text": "table: 1-11354111-3\ncolumns: #, Episode, Air Date, Rating, Share, Rating/Share 18\u201349, Viewers (m), Timeslot Rank, Night Rank, Overall Rank\nQ: what's the night rank with rating of 6.2\nA: SELECT Night Rank FROM 1-11354111-3 WHERE Rating = '6.2'"}
-{"text": "table: 1-11354111-3\ncolumns: #, Episode, Air Date, Rating, Share, Rating/Share 18\u201349, Viewers (m), Timeslot Rank, Night Rank, Overall Rank\nQ: what's the viewers (m) with episode of \"legacy\"\nA: SELECT Viewers (m) FROM 1-11354111-3 WHERE Episode = '\"Legacy\"'"}
-{"text": "table: 1-1137142-1\ncolumns: Season, Group A Winner, Group B Winner, Group C Winner, Group D Winner\nQ: What is the number of group b winner for francavilla?\nA: SELECT COUNT Group B Winner FROM 1-1137142-1 WHERE Group C Winner = 'Francavilla'"}
-{"text": "table: 1-1137142-1\ncolumns: Season, Group A Winner, Group B Winner, Group C Winner, Group D Winner\nQ: What is the group a winner for modena?\nA: SELECT Group A Winner FROM 1-1137142-1 WHERE Group B Winner = 'Modena'"}
-{"text": "table: 1-1137142-1\ncolumns: Season, Group A Winner, Group B Winner, Group C Winner, Group D Winner\nQ: What is the group a winner for vis pesaro?\nA: SELECT Group A Winner FROM 1-1137142-1 WHERE Group C Winner = 'Vis Pesaro'"}
-{"text": "table: 1-1137142-1\ncolumns: Season, Group A Winner, Group B Winner, Group C Winner, Group D Winner\nQ: What group a winner was for nocerina?\nA: SELECT Group A Winner FROM 1-1137142-1 WHERE Group D Winner = 'Nocerina'"}
-{"text": "table: 1-1137142-1\ncolumns: Season, Group A Winner, Group B Winner, Group C Winner, Group D Winner\nQ: What was the group d winner for modena?\nA: SELECT Group D Winner FROM 1-1137142-1 WHERE Group B Winner = 'Modena'"}
-{"text": "table: 1-1137695-3\ncolumns: Round, Grand Prix, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: Who had the fastest lap at the brazilian grand prix?\nA: SELECT Fastest Lap FROM 1-1137695-3 WHERE Grand Prix = 'Brazilian Grand Prix'"}
-{"text": "table: 1-1137695-3\ncolumns: Round, Grand Prix, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: Who was on the pole position at the monaco grand prix?\nA: SELECT Pole Position FROM 1-1137695-3 WHERE Grand Prix = 'Monaco Grand Prix'"}
-{"text": "table: 1-1137695-3\ncolumns: Round, Grand Prix, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: Who was the winning driver when Michael Schumacher had the pole and the fastest lap?\nA: SELECT Winning Driver FROM 1-1137695-3 WHERE Fastest Lap = 'Michael Schumacher' AND Pole Position = 'Michael Schumacher'"}
-{"text": "table: 1-1137704-2\ncolumns: Round, Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: what are all the\u00a0location\u00a0where\u00a0date\u00a0is 5 april\nA: SELECT Location FROM 1-1137704-2 WHERE Date = '5 April'"}
-{"text": "table: 1-1137704-2\ncolumns: Round, Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: what are all the\u00a0pole position\u00a0where\u00a0date\u00a0is 26 july\nA: SELECT Pole Position FROM 1-1137704-2 WHERE Date = '26 July'"}
-{"text": "table: 1-1137704-2\ncolumns: Round, Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: who are all the\u00a0winning constructors\u00a0where\u00a0fastest lap\u00a0is riccardo patrese and\u00a0location\u00a0is interlagos\nA: SELECT Winning Constructor FROM 1-1137704-2 WHERE Fastest Lap = 'Riccardo Patrese' AND Location = 'Interlagos'"}
-{"text": "table: 1-1137704-2\ncolumns: Round, Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: what are all the\u00a0report\u00a0where\u00a0winning constructor\u00a0is williams - renault and\u00a0grand prix\u00a0is south african grand prix\nA: SELECT Report FROM 1-1137704-2 WHERE Winning Constructor = 'Williams - Renault' AND Grand Prix = 'South African Grand Prix'"}
-{"text": "table: 1-1137704-2\ncolumns: Round, Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: whatthe minimum\u00a0round\u00a0where\u00a0grand prix\u00a0is german grand prix\nA: SELECT MIN Round FROM 1-1137704-2 WHERE Grand Prix = 'German Grand Prix'"}
-{"text": "table: 1-1137704-2\ncolumns: Round, Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: what of the total number of\u00a0date\u00a0where\u00a0grand prix\u00a0is portuguese grand prix\nA: SELECT COUNT Date FROM 1-1137704-2 WHERE Grand Prix = 'Portuguese Grand Prix'"}
-{"text": "table: 1-1137707-2\ncolumns: Round, Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: What is the number of pole position with a round of 15?\nA: SELECT COUNT Pole Position FROM 1-1137707-2 WHERE Round = 15"}
-{"text": "table: 1-1137707-2\ncolumns: Round, Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: What is the date of the circuit gilles villeneuve?\nA: SELECT Date FROM 1-1137707-2 WHERE Location = 'Circuit Gilles Villeneuve'"}
-{"text": "table: 1-1137707-2\ncolumns: Round, Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Winning Constructor, Report\nQ: What is the location of thierry boutsen?\nA: SELECT Location FROM 1-1137707-2 WHERE Fastest Lap = 'Thierry Boutsen'"}
-{"text": "table: 1-1137718-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: Who had the pole position at the German Grand Prix?\nA: SELECT Pole Position FROM 1-1137718-2 WHERE Grand Prix = 'German Grand Prix'"}
-{"text": "table: 1-1137718-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: Which rd. occurred on 22 October?\nA: SELECT MIN Rd. FROM 1-1137718-2 WHERE Date = '22 October'"}
-{"text": "table: 1-1137718-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: Who was the winning driver on 13 August?\nA: SELECT Winning Driver FROM 1-1137718-2 WHERE Date = '13 August'"}
-{"text": "table: 1-1137718-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What was the fastest lap at the Mexican Grand Prix?\nA: SELECT Fastest Lap FROM 1-1137718-2 WHERE Grand Prix = 'Mexican Grand Prix'"}
-{"text": "table: 1-1137718-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: Which rd. took place at Hockenheimring?\nA: SELECT MIN Rd. FROM 1-1137718-2 WHERE Location = 'Hockenheimring'"}
-{"text": "table: 1-1137718-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: How many drivers had the fastest lap at Silverstone?\nA: SELECT COUNT Fastest Lap FROM 1-1137718-2 WHERE Location = 'Silverstone'"}
-{"text": "table: 1-11381701-3\ncolumns: Source, Date, Method, iOS, Android, BlackBerry, Symbian / Series 40, Bada, Windows, Other\nQ: What is the percentage of Android use when Windows is 1.15%?\nA: SELECT Android FROM 1-11381701-3 WHERE Windows = '1.15%'"}
-{"text": "table: 1-11381701-3\ncolumns: Source, Date, Method, iOS, Android, BlackBerry, Symbian / Series 40, Bada, Windows, Other\nQ: On which dates was the value of Bada 0.05%?\nA: SELECT Date FROM 1-11381701-3 WHERE Bada = '0.05%'"}
-{"text": "table: 1-11381701-3\ncolumns: Source, Date, Method, iOS, Android, BlackBerry, Symbian / Series 40, Bada, Windows, Other\nQ: When the value of \"other\" is 0.7%, what is the percentage for Windows?\nA: SELECT Windows FROM 1-11381701-3 WHERE Other = '0.7%'"}
-{"text": "table: 1-11381701-3\ncolumns: Source, Date, Method, iOS, Android, BlackBerry, Symbian / Series 40, Bada, Windows, Other\nQ: When Symbian/Series 40 is 0.40%, what is the percentage of \"other\"?\nA: SELECT Other FROM 1-11381701-3 WHERE Symbian / Series 40 = '0.40%'"}
-{"text": "table: 1-11381701-3\ncolumns: Source, Date, Method, iOS, Android, BlackBerry, Symbian / Series 40, Bada, Windows, Other\nQ: Which source shows Blackberry at 2.9%?\nA: SELECT Source FROM 1-11381701-3 WHERE BlackBerry = '2.9%'"}
-{"text": "table: 1-11390711-4\ncolumns: English Name, Japanese orthography, Pronouciation, abbreviation, Provider(IAI), Foundation\nQ: Which colleges have the english abbreviation MTC?\nA: SELECT English Name FROM 1-11390711-4 WHERE abbreviation = 'MTC'"}
-{"text": "table: 1-11390711-4\ncolumns: English Name, Japanese orthography, Pronouciation, abbreviation, Provider(IAI), Foundation\nQ: What is the Japanese orthography for the English name National Farmers Academy?\nA: SELECT Japanese orthography FROM 1-11390711-4 WHERE English Name = 'National Farmers Academy'"}
-{"text": "table: 1-11390711-4\ncolumns: English Name, Japanese orthography, Pronouciation, abbreviation, Provider(IAI), Foundation\nQ: What is the abbreviation for the college pronounced \"k\u014dk\u016b daigakk\u014d\"?\nA: SELECT abbreviation FROM 1-11390711-4 WHERE Pronouciation = 'K\u014dk\u016b Daigakk\u014d'"}
-{"text": "table: 1-11390711-4\ncolumns: English Name, Japanese orthography, Pronouciation, abbreviation, Provider(IAI), Foundation\nQ: How many providers were founded in 1964?\nA: SELECT COUNT Provider(IAI) FROM 1-11390711-4 WHERE Foundation = 1964"}
-{"text": "table: 1-11390711-4\ncolumns: English Name, Japanese orthography, Pronouciation, abbreviation, Provider(IAI), Foundation\nQ: What is the Japanese orthography for National Fisheries University?\nA: SELECT Japanese orthography FROM 1-11390711-4 WHERE English Name = 'National Fisheries University'"}
-{"text": "table: 1-11391954-3\ncolumns: Country, Total, Marathon (mens), Marathon (womens), Half Marathon (mens), Half Marathon (womens)\nQ: What is the minimum number for the half marathon (womens)?\nA: SELECT MIN Half Marathon (womens) FROM 1-11391954-3"}
-{"text": "table: 1-11391954-3\ncolumns: Country, Total, Marathon (mens), Marathon (womens), Half Marathon (mens), Half Marathon (womens)\nQ: Whatis the total number of half marathon (mens) that represented kazakhstan?\nA: SELECT COUNT Half Marathon (mens) FROM 1-11391954-3 WHERE Country = 'Kazakhstan'"}
-{"text": "table: 1-11391954-3\ncolumns: Country, Total, Marathon (mens), Marathon (womens), Half Marathon (mens), Half Marathon (womens)\nQ: What is amount of countries where half marathon (women) is larger than 1.0?\nA: SELECT COUNT Country FROM 1-11391954-3 WHERE Half Marathon (womens) > 1.0"}
-{"text": "table: 1-11391954-3\ncolumns: Country, Total, Marathon (mens), Marathon (womens), Half Marathon (mens), Half Marathon (womens)\nQ: How many times is Moldova the winner of half marathon (womens)?\nA: SELECT COUNT Half Marathon (womens) FROM 1-11391954-3 WHERE Country = 'Moldova'"}
-{"text": "table: 1-11391954-3\ncolumns: Country, Total, Marathon (mens), Marathon (womens), Half Marathon (mens), Half Marathon (womens)\nQ: Which country has half marathon (womens) that is larger than 1.0?\nA: SELECT Country FROM 1-11391954-3 WHERE Half Marathon (womens) > 1.0"}
-{"text": "table: 1-1139087-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What is the make of the car that won the brazilian grand prix?\nA: SELECT Constructor FROM 1-1139087-2 WHERE Grand Prix = 'Brazilian Grand Prix'"}
-{"text": "table: 1-1139087-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: Who drove the fastest lap for round 8?\nA: SELECT Fastest Lap FROM 1-1139087-2 WHERE Rd. = 8"}
-{"text": "table: 1-1139087-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What day was the grand prix in jerez?\nA: SELECT Date FROM 1-1139087-2 WHERE Location = 'Jerez'"}
-{"text": "table: 1-1139087-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What event was in detroit?\nA: SELECT Grand Prix FROM 1-1139087-2 WHERE Location = 'Detroit'"}
-{"text": "table: 1-1139087-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: How many events did nigel mansell drive the fastest and a mclaren - honda win?\nA: SELECT COUNT Grand Prix FROM 1-1139087-2 WHERE Constructor = 'McLaren - Honda' AND Fastest Lap = 'Nigel Mansell'"}
-{"text": "table: 1-1139087-2\ncolumns: Rd., Grand Prix, Date, Location, Pole Position, Fastest Lap, Winning Driver, Constructor, Report\nQ: What day is the french grand prix\nA: SELECT Date FROM 1-1139087-2 WHERE Grand Prix = 'French Grand Prix'"}
-{"text": "table: 1-1139835-3\ncolumns: Year, Winners, Grand Finalist, Scores, Venue, Crowd, Margin, Season Result\nQ: who is the\u00a0winners\u00a0where\u00a0season result\u00a0is 7th\nA: SELECT Winners FROM 1-1139835-3 WHERE Season Result = '7th'"}
-{"text": "table: 1-1139835-3\ncolumns: Year, Winners, Grand Finalist, Scores, Venue, Crowd, Margin, Season Result\nQ: who is the\u00a0winners\u00a0where\u00a0season result\u00a0is 9th\nA: SELECT Winners FROM 1-1139835-3 WHERE Season Result = '9th'"}
-{"text": "table: 1-1139835-3\ncolumns: Year, Winners, Grand Finalist, Scores, Venue, Crowd, Margin, Season Result\nQ: what's the\u00a0grand finalist\u00a0where\u00a0winners\u00a0is collingwood\nA: SELECT Grand Finalist FROM 1-1139835-3 WHERE Winners = 'Collingwood'"}
-{"text": "table: 1-1139835-3\ncolumns: Year, Winners, Grand Finalist, Scores, Venue, Crowd, Margin, Season Result\nQ: who is the\u00a0season result\u00a0where\u00a0margin\u00a0is 51\nA: SELECT Season Result FROM 1-1139835-3 WHERE Margin = 51"}
-{"text": "table: 1-1139835-3\ncolumns: Year, Winners, Grand Finalist, Scores, Venue, Crowd, Margin, Season Result\nQ: who is the\u00a0grand finalist\u00a0where\u00a0scores\u00a0is 11.11 (77) \u2013 10.8 (68)\nA: SELECT Grand Finalist FROM 1-1139835-3 WHERE Scores = '11.11 (77) \u2013 10.8 (68)'"}
-{"text": "table: 1-1139835-3\ncolumns: Year, Winners, Grand Finalist, Scores, Venue, Crowd, Margin, Season Result\nQ: who is the\u00a0grand finalist\u00a0where\u00a0scores\u00a0is 8.9 (57) \u2013 7.12 (54)\nA: SELECT Grand Finalist FROM 1-1139835-3 WHERE Scores = '8.9 (57) \u2013 7.12 (54)'"}
-{"text": "table: 1-1139835-1\ncolumns: Year, Winners, Grand Finalist, Scores, Venue, Crowd, Margin, Season Result\nQ: what was the crowd when the scores are 10.12 (72) \u2013 8.11 (59)?\nA: SELECT MAX Crowd FROM 1-1139835-1 WHERE Scores = '10.12 (72) \u2013 8.11 (59)'"}
-{"text": "table: 1-1139835-1\ncolumns: Year, Winners, Grand Finalist, Scores, Venue, Crowd, Margin, Season Result\nQ: what is the venue where the scores are 15.13 (103) \u2013 8.4 (52)?\nA: SELECT Venue FROM 1-1139835-1 WHERE Scores = '15.13 (103) \u2013 8.4 (52)'"}
-{"text": "table: 1-1139835-1\ncolumns: Year, Winners, Grand Finalist, Scores, Venue, Crowd, Margin, Season Result\nQ: what is the venue where the margin is 4?\nA: SELECT Venue FROM 1-1139835-1 WHERE Margin = 4"}
-{"text": "table: 1-1139835-1\ncolumns: Year, Winners, Grand Finalist, Scores, Venue, Crowd, Margin, Season Result\nQ: what is the crowd when the grand finalist was south melbourne?\nA: SELECT Crowd FROM 1-1139835-1 WHERE Grand Finalist = 'South Melbourne'"}
-{"text": "table: 1-1140067-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: What was the date for monaco grand prix?\nA: SELECT Date FROM 1-1140067-2 WHERE Race = 'Monaco Grand Prix'"}
-{"text": "table: 1-1140067-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: What was the date for the pole position of alain prost?\nA: SELECT Date FROM 1-1140067-2 WHERE Pole Position = 'Alain Prost'"}
-{"text": "table: 1-1140067-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: What is the race winer of the portuguese grand prix?\nA: SELECT Race Winner FROM 1-1140067-2 WHERE Race = 'Portuguese Grand Prix'"}
-{"text": "table: 1-1140074-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the\u00a0race winner\u00a0with\u00a0date\u00a0being 12 june\nA: SELECT Race Winner FROM 1-1140074-2 WHERE Date = '12 June'"}
-{"text": "table: 1-1140074-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the\u00a0constructor\u00a0with\u00a0location\u00a0being hockenheimring\nA: SELECT Constructor FROM 1-1140074-2 WHERE Location = 'Hockenheimring'"}
-{"text": "table: 1-1140074-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the\u00a0race winner\u00a0with\u00a0location\u00a0being jacarepagu\u00e1\nA: SELECT Race Winner FROM 1-1140074-2 WHERE Location = 'Jacarepagu\u00e1'"}
-{"text": "table: 1-1140074-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the total number of\u00a0race winner\u00a0with\u00a0rnd\u00a0being 10\nA: SELECT COUNT Race Winner FROM 1-1140074-2 WHERE Rnd = 10"}
-{"text": "table: 1-1140074-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the\u00a0pole position\u00a0with\u00a0location\u00a0being hockenheimring\nA: SELECT Pole Position FROM 1-1140074-2 WHERE Location = 'Hockenheimring'"}
-{"text": "table: 1-1140074-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the\u00a0report\u00a0with\u00a0rnd\u00a0being 4\nA: SELECT Report FROM 1-1140074-2 WHERE Rnd = 4"}
-{"text": "table: 1-1139835-9\ncolumns: Season, Premier, Runner Up, Score, Venue, Attendance, Premiership\nQ: What venue has an attendance of 30824 at Essendon in 1984?\nA: SELECT Venue FROM 1-1139835-9 WHERE Premier = 'Essendon' AND Attendance = 30824"}
-{"text": "table: 1-1139835-9\ncolumns: Season, Premier, Runner Up, Score, Venue, Attendance, Premiership\nQ: What other venue was a runner up to Hawthorn?\nA: SELECT Venue FROM 1-1139835-9 WHERE Runner Up = 'Hawthorn'"}
-{"text": "table: 1-1139835-9\ncolumns: Season, Premier, Runner Up, Score, Venue, Attendance, Premiership\nQ: What is the other premiership when the runner up wis Geelong?\nA: SELECT Premiership FROM 1-1139835-9 WHERE Runner Up = 'Geelong'"}
-{"text": "table: 1-1139835-9\ncolumns: Season, Premier, Runner Up, Score, Venue, Attendance, Premiership\nQ: Who are all the runner ups when the score is 9.12 (66) \u2013 5.6 (36)?\nA: SELECT Runner Up FROM 1-1139835-9 WHERE Score = '9.12 (66) \u2013 5.6 (36)'"}
-{"text": "table: 1-1140073-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: Who had the fastest lap in the race where Patrick Tambay was on the pole?\nA: SELECT Fastest Lap FROM 1-1140073-2 WHERE Pole Position = 'Patrick Tambay'"}
-{"text": "table: 1-1140073-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: What race had Nelson Piquet on the pole and was in N\u00fcrburgring?\nA: SELECT Race FROM 1-1140073-2 WHERE Pole Position = 'Nelson Piquet' AND Location = 'N\u00fcrburgring'"}
-{"text": "table: 1-1140073-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: How many rounds did Patrick Tambay record the fastest lap?\nA: SELECT COUNT Rnd FROM 1-1140073-2 WHERE Fastest Lap = 'Patrick Tambay'"}
-{"text": "table: 1-1140073-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: Which race is located in kyalami?\nA: SELECT Race FROM 1-1140073-2 WHERE Location = 'Kyalami'"}
-{"text": "table: 1-1140077-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: What is the fastest lap with pole position of gilles villeneuve?\nA: SELECT Fastest Lap FROM 1-1140077-2 WHERE Pole Position = 'Gilles Villeneuve'"}
-{"text": "table: 1-1140077-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: Who did the fastest lap in the dutch grand prix?\nA: SELECT Fastest Lap FROM 1-1140077-2 WHERE Race = 'Dutch Grand Prix'"}
-{"text": "table: 1-1140077-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: Who did the fastest lap with the race winner john watson?\nA: SELECT Fastest Lap FROM 1-1140077-2 WHERE Race Winner = 'John Watson'"}
-{"text": "table: 1-1140076-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: What is the constructor for 9 May?\nA: SELECT Constructor FROM 1-1140076-2 WHERE Date = '9 May'"}
-{"text": "table: 1-1140076-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: What is the pole position for the race with the fastest lap by Nelson Piquet and the constructor is Ferrari?\nA: SELECT Pole Position FROM 1-1140076-2 WHERE Fastest Lap = 'Nelson Piquet' AND Constructor = 'Ferrari'"}
-{"text": "table: 1-1140076-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: What is the report listed for the race in San Marino Grand Prix?\nA: SELECT Report FROM 1-1140076-2 WHERE Race = 'San Marino Grand Prix'"}
-{"text": "table: 1-1140076-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: Who was the constructor in the location Monza?\nA: SELECT Constructor FROM 1-1140076-2 WHERE Location = 'Monza'"}
-{"text": "table: 1-1140076-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: How many races had the pole position Alain Prost and the race winner Keke Rosberg?\nA: SELECT COUNT Race FROM 1-1140076-2 WHERE Pole Position = 'Alain Prost' AND Race Winner = 'Keke Rosberg'"}
-{"text": "table: 1-1140080-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the\u00a0report\u00a0with\u00a0location\u00a0 \u00f6sterreichring\nA: SELECT Report FROM 1-1140080-2 WHERE Location = '\u00d6sterreichring'"}
-{"text": "table: 1-1140080-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the\u00a0report\u00a0with\u00a0race\u00a0argentine grand prix\nA: SELECT Report FROM 1-1140080-2 WHERE Race = 'Argentine Grand Prix'"}
-{"text": "table: 1-1140080-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the minimum\u00a0rnd\u00a0with\u00a0race\u00a0 italian grand prix\nA: SELECT MIN Rnd FROM 1-1140080-2 WHERE Race = 'Italian Grand Prix'"}
-{"text": "table: 1-1140080-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the total number of\u00a0report\u00a0with\u00a0date\u00a0 29 april\nA: SELECT COUNT Report FROM 1-1140080-2 WHERE Date = '29 April'"}
-{"text": "table: 1-1140080-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the\u00a0race winner\u00a0with\u00a0constructor\u00a0 renault\nA: SELECT Race Winner FROM 1-1140080-2 WHERE Constructor = 'Renault'"}
-{"text": "table: 1-1140080-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what's the\u00a0date\u00a0with\u00a0rnd\u00a0 1\nA: SELECT Date FROM 1-1140080-2 WHERE Rnd = 1"}
-{"text": "table: 1-1140083-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: How many days is the Monaco Grand Prix?\nA: SELECT COUNT Date FROM 1-1140083-2 WHERE Race = 'Monaco Grand Prix'"}
-{"text": "table: 1-1140083-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: How many rounds were won with James Hunt as pole position and John Watson as fastest lap?\nA: SELECT COUNT Rnd FROM 1-1140083-2 WHERE Pole Position = 'James Hunt' AND Fastest Lap = 'John Watson'"}
-{"text": "table: 1-1140083-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: The Dijon-prenois had how many fastest laps?\nA: SELECT COUNT Fastest Lap FROM 1-1140083-2 WHERE Location = 'Dijon-Prenois'"}
-{"text": "table: 1-1140083-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: What was the constructor for round 15?\nA: SELECT Constructor FROM 1-1140083-2 WHERE Rnd = 15"}
-{"text": "table: 1-1140088-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: Who won the Brands Hatch circuit?\nA: SELECT Winning driver FROM 1-1140088-6 WHERE Circuit = 'Brands Hatch'"}
-{"text": "table: 1-1140088-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: Who constructed the I Italian Republic Grand Prix?\nA: SELECT Constructor FROM 1-1140088-6 WHERE Race Name = 'I Italian Republic Grand Prix'"}
-{"text": "table: 1-1140088-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What race was held at Oulton Park?\nA: SELECT Race Name FROM 1-1140088-6 WHERE Circuit = 'Oulton Park'"}
-{"text": "table: 1-1140088-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: Did the I Brazilian Grand Prix have a report?\nA: SELECT Report FROM 1-1140088-6 WHERE Race Name = 'I Brazilian Grand Prix'"}
-{"text": "table: 1-1140085-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what is the race where the pole position is niki lauda and the date is 27 april?\nA: SELECT Race FROM 1-1140085-2 WHERE Pole Position = 'Niki Lauda' AND Date = '27 April'"}
-{"text": "table: 1-1140085-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what is the date where the constructor is ferrari and the location is anderstorp?\nA: SELECT Date FROM 1-1140085-2 WHERE Constructor = 'Ferrari' AND Location = 'Anderstorp'"}
-{"text": "table: 1-1140085-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: how many times is the pole position niki lauda and the race is monaco grand prix?\nA: SELECT COUNT Rnd FROM 1-1140085-2 WHERE Pole Position = 'Niki Lauda' AND Race = 'Monaco Grand Prix'"}
-{"text": "table: 1-1140085-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what is the report where the location is kyalami?\nA: SELECT Report FROM 1-1140085-2 WHERE Location = 'Kyalami'"}
-{"text": "table: 1-1140085-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: who is the pole position for the rnd 3\nA: SELECT Pole Position FROM 1-1140085-2 WHERE Rnd = 3"}
-{"text": "table: 1-1140085-2\ncolumns: Rnd, Race, Date, Location, Pole Position, Fastest Lap, Race Winner, Constructor, Report\nQ: what is the race where the fastest lap is by jean-pierre jarier?\nA: SELECT Race FROM 1-1140085-2 WHERE Fastest Lap = 'Jean-Pierre Jarier'"}
-{"text": "table: 1-1140090-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What circuit did Clay Regazzoni win?\nA: SELECT Circuit FROM 1-1140090-6 WHERE Winning driver = 'Clay Regazzoni'"}
-{"text": "table: 1-1140090-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What was the date when Chris Amon won?\nA: SELECT Date FROM 1-1140090-6 WHERE Winning driver = 'Chris Amon'"}
-{"text": "table: 1-1140090-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What circuit is the Vi Rhein-Pokalrennen race in?\nA: SELECT Circuit FROM 1-1140090-6 WHERE Race Name = 'VI Rhein-Pokalrennen'"}
-{"text": "table: 1-1140103-6\ncolumns: #, Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What date is listed at place 13\nA: SELECT Date FROM 1-1140103-6 WHERE # = 13"}
-{"text": "table: 1-1140103-6\ncolumns: #, Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What date has a solitudering circuit\nA: SELECT Date FROM 1-1140103-6 WHERE Circuit = 'Solitudering'"}
-{"text": "table: 1-1140103-6\ncolumns: #, Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: How many dates have silverstone circuit\nA: SELECT COUNT Date FROM 1-1140103-6 WHERE Circuit = 'Silverstone'"}
-{"text": "table: 1-1140103-6\ncolumns: #, Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: How many constructors are listed for the XVI BRDC international trophy race\nA: SELECT COUNT Constructor FROM 1-1140103-6 WHERE Race Name = 'XVI BRDC International Trophy'"}
-{"text": "table: 1-1140105-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What is the name of the circuit in which the race name is ii danish grand prix?\nA: SELECT Circuit FROM 1-1140105-6 WHERE Race Name = 'II Danish Grand Prix'"}
-{"text": "table: 1-1140105-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What is te name of the constructors dated 26 march?\nA: SELECT Constructor FROM 1-1140105-6 WHERE Date = '26 March'"}
-{"text": "table: 1-1140105-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What is the total amount of circuts dated 22 april?\nA: SELECT COUNT Circuit FROM 1-1140105-6 WHERE Date = '22 April'"}
-{"text": "table: 1-1140105-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: what is the name of the constructor that has the circuit zeltweg airfield?\nA: SELECT Constructor FROM 1-1140105-6 WHERE Circuit = 'Zeltweg Airfield'"}
-{"text": "table: 1-1140105-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What is the name of the winning driver where the circuit name is posillipo?\nA: SELECT Winning driver FROM 1-1140105-6 WHERE Circuit = 'Posillipo'"}
-{"text": "table: 1-1140105-6\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What is the name of the circuit where the race xi Syracuse grand prix was held?\nA: SELECT Circuit FROM 1-1140105-6 WHERE Race Name = 'XI Syracuse Grand Prix'"}
-{"text": "table: 1-1140111-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What kind of report is for the Pau circuit?\nA: SELECT Report FROM 1-1140111-5 WHERE Circuit = 'Pau'"}
-{"text": "table: 1-1140111-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: How many different kinds of reports are there for races that Juan Manuel Fangio won?\nA: SELECT COUNT Report FROM 1-1140111-5 WHERE Winning driver = 'Juan Manuel Fangio'"}
-{"text": "table: 1-1140111-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: Who constructed the Syracuse circuit?\nA: SELECT Constructor FROM 1-1140111-5 WHERE Circuit = 'Syracuse'"}
-{"text": "table: 1-1140116-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What is the name of the race in the Modena circuit?\nA: SELECT Race Name FROM 1-1140116-5 WHERE Circuit = 'Modena'"}
-{"text": "table: 1-1140116-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What is the race name in the Monza circuit?\nA: SELECT Race Name FROM 1-1140116-5 WHERE Circuit = 'Monza'"}
-{"text": "table: 1-1140116-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: When does V Madgwick Cup take place?\nA: SELECT Date FROM 1-1140116-5 WHERE Race Name = 'V Madgwick Cup'"}
-{"text": "table: 1-1140116-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: Which driver won the race xiv el\u00e4intarhanajot?\nA: SELECT Winning driver FROM 1-1140116-5 WHERE Race Name = 'XIV El\u00e4intarhanajot'"}
-{"text": "table: 1-1140116-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: Who won the Modena circuit?\nA: SELECT Winning driver FROM 1-1140116-5 WHERE Circuit = 'Modena'"}
-{"text": "table: 1-1140113-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: How many constructors won the III Redex Trophy?\nA: SELECT COUNT Constructor FROM 1-1140113-5 WHERE Race Name = 'III RedeX Trophy'"}
-{"text": "table: 1-1140113-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What was the report of Mike Hawthorn's winning race?\nA: SELECT Report FROM 1-1140113-5 WHERE Winning driver = 'Mike Hawthorn'"}
-{"text": "table: 1-1140113-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What was the name of the race in Bordeaux?\nA: SELECT Race Name FROM 1-1140113-5 WHERE Circuit = 'Bordeaux'"}
-{"text": "table: 1-1140117-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What is the report for the race name V Ulster Trophy?\nA: SELECT Report FROM 1-1140117-5 WHERE Race Name = 'V Ulster Trophy'"}
-{"text": "table: 1-1140117-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What is the constructor for the Silverstone circuit?\nA: SELECT Constructor FROM 1-1140117-5 WHERE Circuit = 'Silverstone'"}
-{"text": "table: 1-1140117-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What's the report for the Silverstone circuit?\nA: SELECT Report FROM 1-1140117-5 WHERE Circuit = 'Silverstone'"}
-{"text": "table: 1-1140117-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: What's the report for the race name, XIII Grand Prix de l'Albigeois?\nA: SELECT Report FROM 1-1140117-5 WHERE Race Name = 'XIII Grand Prix de l'Albigeois'"}
-{"text": "table: 1-1140117-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: Which date did the race name XII Pau Grand Prix take place on?\nA: SELECT Date FROM 1-1140117-5 WHERE Race Name = 'XII Pau Grand Prix'"}
-{"text": "table: 1-1140117-5\ncolumns: Race Name, Circuit, Date, Winning driver, Constructor, Report\nQ: Who was the winning driver for the goodwood circuit?\nA: SELECT Winning driver FROM 1-1140117-5 WHERE Circuit = 'Goodwood'"}
-{"text": "table: 1-11411026-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many millions of U.S. viewers whatched episodes written by Krystal Houghton?\nA: SELECT U.S. viewers (millions) FROM 1-11411026-2 WHERE Written by = 'Krystal Houghton'"}
-{"text": "table: 1-11411026-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: How many titles were directed in series 79?\nA: SELECT COUNT Directed by FROM 1-11411026-2 WHERE No. in series = 79"}
-{"text": "table: 1-11411026-2\ncolumns: No. in series, No. in season, Title, Directed by, Written by, Original air date, U.S. viewers (millions)\nQ: Who wrote an episode watched by 19.01 million US viewers?\nA: SELECT Written by FROM 1-11411026-2 WHERE U.S. viewers (millions) = '19.01'"}
-{"text": "table: 1-11404452-1\ncolumns: Series #, Episode title, Writer(s), Director, U.S. viewers (millions), Original air date\nQ: What are the titles of the episodes where Rodman Flender is the director?\nA: SELECT Episode title FROM 1-11404452-1 WHERE Director = 'Rodman Flender'"}
-{"text": "table: 1-11404452-1\ncolumns: Series #, Episode title, Writer(s), Director, U.S. viewers (millions), Original air date\nQ: What is the original air date of the Jamie Babbit directed episode?\nA: SELECT Original air date FROM 1-11404452-1 WHERE Director = 'Jamie Babbit'"}
-{"text": "table: 1-11404452-1\ncolumns: Series #, Episode title, Writer(s), Director, U.S. viewers (millions), Original air date\nQ: What is the original air date when there were 12.81 million u.s viewers?\nA: SELECT Original air date FROM 1-11404452-1 WHERE U.S. viewers (millions) = '12.81'"}
-{"text": "table: 1-11404452-1\ncolumns: Series #, Episode title, Writer(s), Director, U.S. viewers (millions), Original air date\nQ: When did Shelia Lawrence join the series?\nA: SELECT MIN Series # FROM 1-11404452-1 WHERE Writer(s) = 'Shelia Lawrence'"}
-{"text": "table: 1-11404452-1\ncolumns: Series #, Episode title, Writer(s), Director, U.S. viewers (millions), Original air date\nQ: Who was the director when there were 13.66 million u.s viewers?\nA: SELECT Director FROM 1-11404452-1 WHERE U.S. viewers (millions) = '13.66'"}
-{"text": "table: 1-1143966-1\ncolumns: Season, Games, Won, Lost, Tied, Points, Pct %, Goals for, Goals against, Standing\nQ: Name the percentage where the amount won was 25\nA: SELECT Pct % FROM 1-1143966-1 WHERE Won = 25"}
-{"text": "table: 1-1143966-1\ncolumns: Season, Games, Won, Lost, Tied, Points, Pct %, Goals for, Goals against, Standing\nQ: How many games were won with 2nd oha was standing and there were 62 games?\nA: SELECT Won FROM 1-1143966-1 WHERE Standing = '2nd OHA' AND Games = 62"}
-{"text": "table: 1-11447995-2\ncolumns: Ward, Bello, Ben-Tahir, Doucet, Furtenbacher, Gauthier, Haydon, Larter, Lawrance, Libweshya, Liscumb\nQ: What is the Liscumb when Gauthier is 34?\nA: SELECT Liscumb FROM 1-11447995-2 WHERE Gauthier = '34'"}
-{"text": "table: 1-11447995-2\ncolumns: Ward, Bello, Ben-Tahir, Doucet, Furtenbacher, Gauthier, Haydon, Larter, Lawrance, Libweshya, Liscumb\nQ: What is the Bello when Ben-Tahir is 296?\nA: SELECT Bello FROM 1-11447995-2 WHERE Ben-Tahir = '296'"}
-{"text": "table: 1-11447995-2\ncolumns: Ward, Bello, Ben-Tahir, Doucet, Furtenbacher, Gauthier, Haydon, Larter, Lawrance, Libweshya, Liscumb\nQ: What is Ben-Tahir when Bello is 51?\nA: SELECT Ben-Tahir FROM 1-11447995-2 WHERE Bello = '51'"}
-{"text": "table: 1-11447995-2\ncolumns: Ward, Bello, Ben-Tahir, Doucet, Furtenbacher, Gauthier, Haydon, Larter, Lawrance, Libweshya, Liscumb\nQ: What is Haydon when Larter is 11 and Libweshya is 4?\nA: SELECT Haydon FROM 1-11447995-2 WHERE Larter = '11' AND Libweshya = '4'"}
-{"text": "table: 1-11447995-2\ncolumns: Ward, Bello, Ben-Tahir, Doucet, Furtenbacher, Gauthier, Haydon, Larter, Lawrance, Libweshya, Liscumb\nQ: What is Liscumb when Haydon is 1632?\nA: SELECT Liscumb FROM 1-11447995-2 WHERE Haydon = '1632'"}
-{"text": "table: 1-11447995-2\ncolumns: Ward, Bello, Ben-Tahir, Doucet, Furtenbacher, Gauthier, Haydon, Larter, Lawrance, Libweshya, Liscumb\nQ: What is Doucet when Lawrance is 36?\nA: SELECT Doucet FROM 1-11447995-2 WHERE Lawrance = '36'"}
-{"text": "table: 1-11449590-2\ncolumns: Week, Date, Opponent, Result, Kickoff [a ], Game site, TV, Attendance, Record\nQ: Which tv had the date december 7, 1986?\nA: SELECT TV FROM 1-11449590-2 WHERE Date = 'December 7, 1986'"}
-{"text": "table: 1-11449590-2\ncolumns: Week, Date, Opponent, Result, Kickoff [a ], Game site, TV, Attendance, Record\nQ: Which kickoff has the opponent at new orleans saints?\nA: SELECT Kickoff [a ] FROM 1-11449590-2 WHERE Opponent = 'at New Orleans Saints'"}
-{"text": "table: 1-11464746-1\ncolumns: House Name, Composition, Named after, Founded, Colours\nQ: How many houses are green?\nA: SELECT COUNT House Name FROM 1-11464746-1 WHERE Colours = 'Green'"}
-{"text": "table: 1-11464746-1\ncolumns: House Name, Composition, Named after, Founded, Colours\nQ: What year was the house named gongola made?\nA: SELECT Founded FROM 1-11464746-1 WHERE House Name = 'Gongola'"}
-{"text": "table: 1-11464746-1\ncolumns: House Name, Composition, Named after, Founded, Colours\nQ: What is the name of the green house?\nA: SELECT House Name FROM 1-11464746-1 WHERE Colours = 'Green'"}
-{"text": "table: 1-11464746-1\ncolumns: House Name, Composition, Named after, Founded, Colours\nQ: What is the green house made of?\nA: SELECT Composition FROM 1-11464746-1 WHERE Colours = 'Green'"}
-{"text": "table: 1-11464746-1\ncolumns: House Name, Composition, Named after, Founded, Colours\nQ: What is the benue house made of?\nA: SELECT Composition FROM 1-11464746-1 WHERE House Name = 'Benue'"}
-{"text": "table: 1-11465521-2\ncolumns: Week, Date, Opponent, Result, Kickoff [a ], Game site, TV, Attendance, Record\nQ: In which week was the game against a team with a record of 3-6 played?\nA: SELECT COUNT Week FROM 1-11465521-2 WHERE Record = '3-6'"}
-{"text": "table: 1-11465521-2\ncolumns: Week, Date, Opponent, Result, Kickoff [a ], Game site, TV, Attendance, Record\nQ: Which channel had the game against the Minnesota Vikings?\nA: SELECT TV FROM 1-11465521-2 WHERE Opponent = 'Minnesota Vikings'"}
-{"text": "table: 1-11465521-2\ncolumns: Week, Date, Opponent, Result, Kickoff [a ], Game site, TV, Attendance, Record\nQ: How many opponents were there at the game with 64,087 people in attendance?\nA: SELECT COUNT Opponent FROM 1-11465521-2 WHERE Attendance = '64,087'"}
-{"text": "table: 1-11452830-2\ncolumns: Week, Date, Opponent, Result, Kickoff [a ], Game site, TV, Attendance, Record\nQ: Where was the game played when the team's record was 1-3? \nA: SELECT Game site FROM 1-11452830-2 WHERE Record = '1-3'"}
-{"text": "table: 1-11452830-2\ncolumns: Week, Date, Opponent, Result, Kickoff [a ], Game site, TV, Attendance, Record\nQ: How many times was there a kickoff in the September 4, 1988 game? \nA: SELECT COUNT Kickoff [a ] FROM 1-11452830-2 WHERE Date = 'September 4, 1988'"}
-{"text": "table: 1-11452830-2\ncolumns: Week, Date, Opponent, Result, Kickoff [a ], Game site, TV, Attendance, Record\nQ: How many crowds watched the game where the record was 1-3? \nA: SELECT COUNT Attendance FROM 1-11452830-2 WHERE Record = '1-3'"}
-{"text": "table: 1-1149495-1\ncolumns: Series, Year, Winner, Runner-up, Third place, Fourth place, Fifth place, Sixth place, Host\nQ: What year finished with Daniel Zueras as the runner-up?\nA: SELECT COUNT Year FROM 1-1149495-1 WHERE Runner-up = 'Daniel Zueras'"}
-{"text": "table: 1-1149495-1\ncolumns: Series, Year, Winner, Runner-up, Third place, Fourth place, Fifth place, Sixth place, Host\nQ: How many people hosted the show in the year when Chenoa ended up in fourth place?\nA: SELECT COUNT Host FROM 1-1149495-1 WHERE Fourth place = 'Chenoa'"}
-{"text": "table: 1-1149495-1\ncolumns: Series, Year, Winner, Runner-up, Third place, Fourth place, Fifth place, Sixth place, Host\nQ: How many fourth places were there in 2003?\nA: SELECT COUNT Fourth place FROM 1-1149495-1 WHERE Year = '2003'"}
-{"text": "table: 1-1147705-1\ncolumns: model, max. motive power, max. torque at rpm, engine displacement, engine type, engine configuration & notes 0-100km/h\nQ: What is the engine type when the max torque at rpm is n\u00b7m ( lbf\u00b7ft ) @ 4,800 Answers:?\nA: SELECT engine type FROM 1-1147705-1 WHERE max. torque at rpm = 'N\u00b7m ( lbf\u00b7ft ) @ 4,800'"}
-{"text": "table: 1-1147705-1\ncolumns: model, max. motive power, max. torque at rpm, engine displacement, engine type, engine configuration & notes 0-100km/h\nQ: What is the engine configuration $notes 0-100km/h for the engine type b5244 t2?\nA: SELECT engine configuration & notes 0-100km/h FROM 1-1147705-1 WHERE engine type = 'B5244 T2'"}
-{"text": "table: 1-1147705-1\ncolumns: model, max. motive power, max. torque at rpm, engine displacement, engine type, engine configuration & notes 0-100km/h\nQ: What is the engine displacement for the engine type b5254 t?\nA: SELECT engine displacement FROM 1-1147705-1 WHERE engine type = 'B5254 T'"}
-{"text": "table: 1-1147705-1\ncolumns: model, max. motive power, max. torque at rpm, engine displacement, engine type, engine configuration & notes 0-100km/h\nQ: How many have are model 2.4 awd?\nA: SELECT COUNT engine type FROM 1-1147705-1 WHERE model = '2.4 AWD'"}
-{"text": "table: 1-1147705-1\ncolumns: model, max. motive power, max. torque at rpm, engine displacement, engine type, engine configuration & notes 0-100km/h\nQ: How many engine b5204 t3?\nA: SELECT COUNT engine displacement FROM 1-1147705-1 WHERE engine type = 'B5204 T3'"}
-{"text": "table: 1-1147705-1\ncolumns: model, max. motive power, max. torque at rpm, engine displacement, engine type, engine configuration & notes 0-100km/h\nQ: How many engine b5234 t3?\nA: SELECT COUNT model FROM 1-1147705-1 WHERE engine type = 'B5234 T3'"}
-{"text": "table: 1-1147701-4\ncolumns: Model name, Power (ps), Torque (Nm@rpm), Displacement (cm\u00b3), Engine code, Comment\nQ: how many\u00a0power (ps)\u00a0with\u00a0torque (nm@rpm)\u00a0being 240@2200-5000\nA: SELECT COUNT Power (ps) FROM 1-1147701-4 WHERE Torque (Nm@rpm) = '240@2200-5000'"}
-{"text": "table: 1-1147701-4\ncolumns: Model name, Power (ps), Torque (Nm@rpm), Displacement (cm\u00b3), Engine code, Comment\nQ: what's the\u00a0comment\u00a0with\u00a0model name\u00a0being 2.4 (2001-2007)\nA: SELECT Comment FROM 1-1147701-4 WHERE Model name = '2.4 (2001-2007)'"}
-{"text": "table: 1-1147701-4\ncolumns: Model name, Power (ps), Torque (Nm@rpm), Displacement (cm\u00b3), Engine code, Comment\nQ: what's the\u00a0model name\u00a0with\u00a0engine code\u00a0being b5204 t5\nA: SELECT Model name FROM 1-1147701-4 WHERE Engine code = 'B5204 T5'"}
-{"text": "table: 1-1147701-4\ncolumns: Model name, Power (ps), Torque (Nm@rpm), Displacement (cm\u00b3), Engine code, Comment\nQ: what's the\u00a0dbeingplacement (cm\u00b3)\u00a0with\u00a0torque (nm@rpm)\u00a0being 350@1800-6000\nA: SELECT Displacement (cm\u00b3) FROM 1-1147701-4 WHERE Torque (Nm@rpm) = '350@1800-6000'"}
-{"text": "table: 1-1147701-4\ncolumns: Model name, Power (ps), Torque (Nm@rpm), Displacement (cm\u00b3), Engine code, Comment\nQ: what's the\u00a0model name\u00a0with\u00a0torque (nm@rpm)\u00a0being 230@4500\nA: SELECT Model name FROM 1-1147701-4 WHERE Torque (Nm@rpm) = '230@4500'"}
-{"text": "table: 1-1147701-4\ncolumns: Model name, Power (ps), Torque (Nm@rpm), Displacement (cm\u00b3), Engine code, Comment\nQ: what's the\u00a0model name\u00a0with\u00a0engine code\u00a0being b5254 t4\nA: SELECT Model name FROM 1-1147701-4 WHERE Engine code = 'B5254 T4'"}
-{"text": "table: 1-1147701-5\ncolumns: Model name, Power (ps), Torque (Nm@rpm), Displacement (cm\u00b3), Engine code, Comment\nQ: Name the torque of the engine is d5244 t5\nA: SELECT Torque (Nm@rpm) FROM 1-1147701-5 WHERE Engine code = 'D5244 T5'"}
-{"text": "table: 1-1147701-5\ncolumns: Model name, Power (ps), Torque (Nm@rpm), Displacement (cm\u00b3), Engine code, Comment\nQ: What is the model of the engine d5244 t?\nA: SELECT Model name FROM 1-1147701-5 WHERE Engine code = 'D5244 T'"}
-{"text": "table: 1-1147701-5\ncolumns: Model name, Power (ps), Torque (Nm@rpm), Displacement (cm\u00b3), Engine code, Comment\nQ: What is the model of the enginge d5252 t?\nA: SELECT Model name FROM 1-1147701-5 WHERE Engine code = 'D5252 T'"}
-{"text": "table: 1-1147701-5\ncolumns: Model name, Power (ps), Torque (Nm@rpm), Displacement (cm\u00b3), Engine code, Comment\nQ: What is the model of the engine d5244 t7?\nA: SELECT Model name FROM 1-1147701-5 WHERE Engine code = 'D5244 T7'"}
-{"text": "table: 1-11545282-11\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: What is the position of number 47?\nA: SELECT Position FROM 1-11545282-11 WHERE No. = '47'"}
-{"text": "table: 1-11545282-11\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: Name the position of Turkey\nA: SELECT Position FROM 1-11545282-11 WHERE Nationality = 'Turkey'"}
-{"text": "table: 1-11545282-11\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: Who is player number 51?\nA: SELECT Player FROM 1-11545282-11 WHERE No. = '51'"}
-{"text": "table: 1-11545282-11\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: What is the position for the years 1998-99\nA: SELECT Position FROM 1-11545282-11 WHERE Years for Jazz = '1998-99'"}
-{"text": "table: 1-11545282-11\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: How many positions are for creighton?\nA: SELECT COUNT Position FROM 1-11545282-11 WHERE School/Club Team = 'Creighton'"}
-{"text": "table: 1-11545282-12\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: Which player is from Marshall and played 1974-75?\nA: SELECT Player FROM 1-11545282-12 WHERE Years for Jazz = '1974-75' AND School/Club Team = 'Marshall'"}
-{"text": "table: 1-11545282-12\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: Which country is the player that went to Oregon?\nA: SELECT Nationality FROM 1-11545282-12 WHERE School/Club Team = 'Oregon'"}
-{"text": "table: 1-11545282-12\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: Which country is Jim Les from?\nA: SELECT Nationality FROM 1-11545282-12 WHERE Player = 'Jim Les'"}
-{"text": "table: 1-11545282-12\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: Which number is the player from Minnesota?\nA: SELECT MAX No. FROM 1-11545282-12 WHERE School/Club Team = 'Minnesota'"}
-{"text": "table: 1-11545282-18\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: Which player played for years 2000-02\nA: SELECT Player FROM 1-11545282-18 WHERE Years for Jazz = '2000-02'"}
-{"text": "table: 1-11545282-18\ncolumns: Player, No., Nationality, Position, Years for Jazz, School/Club Team\nQ: Which school is Kirk Snyder from?\nA: SELECT School/Club Team FROM 1-11545282-18 WHERE Player = 'Kirk Snyder'"}
diff --git a/exo/train/data/lora/valid.jsonl b/exo/train/data/lora/valid.jsonl
deleted file mode 100644
index 6c8d6afe..00000000
--- a/exo/train/data/lora/valid.jsonl
+++ /dev/null
@@ -1,100 +0,0 @@
-{"text": "table: 1-10015132-11\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What position does the player who played for butler cc (ks) play?\nA: SELECT Position FROM 1-10015132-11 WHERE School/Club Team = 'Butler CC (KS)'"}
-{"text": "table: 1-10015132-11\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: How many schools did player number 3 play at?\nA: SELECT COUNT School/Club Team FROM 1-10015132-11 WHERE No. = '3'"}
-{"text": "table: 1-10015132-11\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What school did player number 21 play for?\nA: SELECT School/Club Team FROM 1-10015132-11 WHERE No. = '21'"}
-{"text": "table: 1-10015132-11\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: Who is the player that wears number 42?\nA: SELECT Player FROM 1-10015132-11 WHERE No. = '42'"}
-{"text": "table: 1-10015132-11\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What player played guard for toronto in 1996-97?\nA: SELECT Player FROM 1-10015132-11 WHERE Position = 'Guard' AND Years in Toronto = '1996-97'"}
-{"text": "table: 1-10015132-9\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: Who are all of the players on the Westchester High School club team?\nA: SELECT Player FROM 1-10015132-9 WHERE School/Club Team = 'Westchester High School'"}
-{"text": "table: 1-10015132-9\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What school/club team is Amir Johnson on?\nA: SELECT School/Club Team FROM 1-10015132-9 WHERE Player = 'Amir Johnson'"}
-{"text": "table: 1-10015132-9\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What are the total amount of numbers on the Toronto team in 2005-06?\nA: SELECT COUNT No. FROM 1-10015132-9 WHERE Years in Toronto = '2005-06'"}
-{"text": "table: 1-10015132-9\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What are the total number of positions on the Toronto team in 2006-07?\nA: SELECT COUNT Position FROM 1-10015132-9 WHERE Years in Toronto = '2006-07'"}
-{"text": "table: 1-10015132-9\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What are the nationality of the players on the Fresno State school/club team?\nA: SELECT Nationality FROM 1-10015132-9 WHERE School/Club Team = 'Fresno State'"}
-{"text": "table: 1-10015132-9\ncolumns: Player, No., Nationality, Position, Years in Toronto, School/Club Team\nQ: What school/club team is Trey Johnson on?\nA: SELECT School/Club Team FROM 1-10015132-9 WHERE Player = 'Trey Johnson'"}
-{"text": "table: 1-10026563-1\ncolumns: Entered office as Head of State or Government, Began time as senior G8 leader, Ended time as senior G8 leader, Person, Office\nQ: When did Jacques Chirac stop being a G8 leader?\nA: SELECT Ended time as senior G8 leader FROM 1-10026563-1 WHERE Person = 'Jacques Chirac'"}
-{"text": "table: 1-10026563-1\ncolumns: Entered office as Head of State or Government, Began time as senior G8 leader, Ended time as senior G8 leader, Person, Office\nQ: When did the Prime Minister of Italy take office?\nA: SELECT Entered office as Head of State or Government FROM 1-10026563-1 WHERE Office = 'Prime Minister of Italy'"}
-{"text": "table: 1-1008653-1\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: What is the English name of the country whose official native language is Dutch Papiamento?\nA: SELECT Country ( exonym ) FROM 1-1008653-1 WHERE Official or native language(s) (alphabet/script) = 'Dutch Papiamento'"}
-{"text": "table: 1-1008653-1\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: What official or native languages are spoken in the country whose capital city is Canberra?\nA: SELECT Official or native language(s) (alphabet/script) FROM 1-1008653-1 WHERE Capital ( exonym ) = 'Canberra'"}
-{"text": "table: 1-1008653-1\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: What is the local name given to the city of Canberra?\nA: SELECT Capital ( endonym ) FROM 1-1008653-1 WHERE Capital ( exonym ) = 'Canberra'"}
-{"text": "table: 1-1008653-1\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: What is the local name given to the capital of Anguilla?\nA: SELECT Capital ( endonym ) FROM 1-1008653-1 WHERE Country ( endonym ) = 'Anguilla'"}
-{"text": "table: 1-1008653-1\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: What is the English name given to the city of St. John's?\nA: SELECT Capital ( exonym ) FROM 1-1008653-1 WHERE Capital ( endonym ) = 'St. John's'"}
-{"text": "table: 1-1008653-1\ncolumns: Country ( exonym ), Capital ( exonym ), Country ( endonym ), Capital ( endonym ), Official or native language(s) (alphabet/script)\nQ: How many capital cities does Australia have?\nA: SELECT COUNT Capital ( endonym ) FROM 1-1008653-1 WHERE Country ( endonym ) = 'Australia'"}
-{"text": "table: 1-10088101-1\ncolumns: No. in set, No. in series, Title, Directed by, Written by, Original air date, Production code\nQ: The episode with production code 9abx02 was originally aired on what date?\nA: SELECT Original air date FROM 1-10088101-1 WHERE Production code = '9ABX02'"}
-{"text": "table: 1-10088101-1\ncolumns: No. in set, No. in series, Title, Directed by, Written by, Original air date, Production code\nQ: What is the episode number that has production code 8abx15?\nA: SELECT MIN No. in series FROM 1-10088101-1 WHERE Production code = '8ABX15'"}
-{"text": "table: 1-10295819-2\ncolumns: Player, Highest singles ranking, Highest doubles ranking, First year played, Years played, Ties played, Total W\u2013L, Singles W\u2013L, Doubles W\u2013L\nQ: Name the minimum tiesplayed for 6 years\nA: SELECT MIN Ties played FROM 1-10295819-2 WHERE Years played = 6"}
-{"text": "table: 1-10342194-3\ncolumns: District, Total amount of trees, Prevailing types, %, Amount of old trees, Amount of trees, that require replacement\nQ: What is the amount of trees, that require replacement when prevailing types, % is pine \u2014 29.37 poplar \u2014 26.12 acer negundo \u2014 13.2?\nA: SELECT Amount of trees, that require replacement FROM 1-10342194-3 WHERE Prevailing types, % = 'Pine \u2014 29.37 Poplar \u2014 26.12 Acer negundo \u2014 13.2'"}
-{"text": "table: 1-10342194-3\ncolumns: District, Total amount of trees, Prevailing types, %, Amount of old trees, Amount of trees, that require replacement\nQ: What is the amount of trees, that require replacement when district is leninsky?\nA: SELECT Amount of trees, that require replacement FROM 1-10342194-3 WHERE District = 'Leninsky'"}
-{"text": "table: 1-10342194-3\ncolumns: District, Total amount of trees, Prevailing types, %, Amount of old trees, Amount of trees, that require replacement\nQ: What is the district when the total amount of trees is smaller than 150817.6878461314 and amount of old trees is 1,928 (1.89%)?\nA: SELECT District FROM 1-10342194-3 WHERE Total amount of trees < 150817.6878461314 AND Amount of old trees = '1,928 (1.89%)'"}
-{"text": "table: 1-10342194-3\ncolumns: District, Total amount of trees, Prevailing types, %, Amount of old trees, Amount of trees, that require replacement\nQ: What is the amount of trees, that require replacement when the district is motovilikhinsky?\nA: SELECT Amount of trees, that require replacement FROM 1-10342194-3 WHERE District = 'Motovilikhinsky'"}
-{"text": "table: 1-10342194-3\ncolumns: District, Total amount of trees, Prevailing types, %, Amount of old trees, Amount of trees, that require replacement\nQ: What is the total amount of trees when district is leninsky?\nA: SELECT MAX Total amount of trees FROM 1-10342194-3 WHERE District = 'Leninsky'"}
-{"text": "table: 1-10342194-3\ncolumns: District, Total amount of trees, Prevailing types, %, Amount of old trees, Amount of trees, that require replacement\nQ: What is the district when prevailing types, % is acer negundo \u2014 30.22 tilia \u2014 18.6 poplar \u2014 15.23?\nA: SELECT District FROM 1-10342194-3 WHERE Prevailing types, % = 'Acer negundo \u2014 30.22 Tilia \u2014 18.6 Poplar \u2014 15.23'"}
-{"text": "table: 1-10429820-13\ncolumns: Iowa State vs., Overall Record, in Ames, at Opponents Venue, at Neutral Site, Last 5 Meetings, Last 10 Meetings, Current Streak, Since Beginning of Big 12\nQ: When the value of \"since beginning of big 12\" is synonymous with its' category, what are the in Ames values?\nA: SELECT in Ames FROM 1-10429820-13 WHERE Since Beginning of Big 12 = 'Since Beginning of Big 12'"}
-{"text": "table: 1-1046170-5\ncolumns: Year, Division, League, Regular Season, Playoffs, U.S. Open Cup\nQ: what's the\u00a0u.s. open cup status\u00a0for regular season\u00a0of 4th, atlantic division \nA: SELECT U.S. Open Cup FROM 1-1046170-5 WHERE Regular Season = '4th, Atlantic Division'"}
-{"text": "table: 1-1046170-5\ncolumns: Year, Division, League, Regular Season, Playoffs, U.S. Open Cup\nQ: how many division did not qualify for u.s. open cup in 2003\nA: SELECT Division FROM 1-1046170-5 WHERE U.S. Open Cup = 'Did Not Qualify' AND Year = 2003"}
-{"text": "table: 1-1046170-5\ncolumns: Year, Division, League, Regular Season, Playoffs, U.S. Open Cup\nQ: which round is u.s. open cup division semifinals\nA: SELECT U.S. Open Cup FROM 1-1046170-5 WHERE Playoffs = 'Division Semifinals'"}
-{"text": "table: 1-1046170-5\ncolumns: Year, Division, League, Regular Season, Playoffs, U.S. Open Cup\nQ: what are all the playoffs for regular season is 1st, atlantic division\nA: SELECT Playoffs FROM 1-1046170-5 WHERE Regular Season = '1st, Atlantic Division'"}
-{"text": "table: 1-1046170-5\ncolumns: Year, Division, League, Regular Season, Playoffs, U.S. Open Cup\nQ: what are all the playoffs for u.s. open cup in 1st round\nA: SELECT Playoffs FROM 1-1046170-5 WHERE U.S. Open Cup = '1st Round'"}
-{"text": "table: 1-1061075-1\ncolumns: Season, Competition, Round, Opponents, 1st leg, 2nd leg, Aggregate\nQ: what is the total number of\u00a02nd leg\u00a0where\u00a0aggregate\u00a0is 7-2\nA: SELECT COUNT 2nd leg FROM 1-1061075-1 WHERE Aggregate = '7-2'"}
-{"text": "table: 1-1061075-1\ncolumns: Season, Competition, Round, Opponents, 1st leg, 2nd leg, Aggregate\nQ: what's the\u00a0aggregate\u00a0where\u00a01st leg\u00a0is 3\u20132\nA: SELECT Aggregate FROM 1-1061075-1 WHERE 1st leg = '3\u20132'"}
-{"text": "table: 1-1061075-1\ncolumns: Season, Competition, Round, Opponents, 1st leg, 2nd leg, Aggregate\nQ: what's the\u00a0competition\u00a0where\u00a0aggregate\u00a0is 4\u20137\nA: SELECT Competition FROM 1-1061075-1 WHERE Aggregate = '4\u20137'"}
-{"text": "table: 1-1061075-1\ncolumns: Season, Competition, Round, Opponents, 1st leg, 2nd leg, Aggregate\nQ: what's the\u00a0competition\u00a0where\u00a01st leg\u00a0is 4-1 (h)\nA: SELECT Competition FROM 1-1061075-1 WHERE 1st leg = '4-1 (h)'"}
-{"text": "table: 1-1061075-1\ncolumns: Season, Competition, Round, Opponents, 1st leg, 2nd leg, Aggregate\nQ: what is the total number of\u00a0round\u00a0where\u00a0opponents\u00a0is haugar\nA: SELECT COUNT Round FROM 1-1061075-1 WHERE Opponents = 'Haugar'"}
-{"text": "table: 1-1061075-1\ncolumns: Season, Competition, Round, Opponents, 1st leg, 2nd leg, Aggregate\nQ: what's the\u00a01st leg\u00a0where\u00a0opponents\u00a0is galatasaray\nA: SELECT 1st leg FROM 1-1061075-1 WHERE Opponents = 'Galatasaray'"}
-{"text": "table: 1-10706961-2\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: What is the highest Rd that Tom Sneva had the pole position in?\nA: SELECT MAX Rd FROM 1-10706961-2 WHERE Pole Position = 'Tom Sneva'"}
-{"text": "table: 1-10706961-2\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: How many winning drivers were there in the race that had a fastest lap time of 56.920?\nA: SELECT COUNT Winning driver FROM 1-10706961-2 WHERE Fastest Lap = '56.920'"}
-{"text": "table: 1-10706961-2\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: How many reports are there in the race that Forsythe Racing won and Teo Fabi had the pole position in?\nA: SELECT COUNT Report FROM 1-10706961-2 WHERE Winning team = 'Forsythe Racing' AND Pole Position = 'Teo Fabi'"}
-{"text": "table: 1-10706961-2\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: Which Rd took place at the Indianapolis 500?\nA: SELECT Rd FROM 1-10706961-2 WHERE Name = 'Indianapolis 500'"}
-{"text": "table: 1-10706961-2\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: Which teams won when Bobby Rahal was their winning driver?\nA: SELECT Winning team FROM 1-10706961-2 WHERE Winning driver = 'Bobby Rahal'"}
-{"text": "table: 1-10706961-2\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: What was the fastest lap time in the Escort Radar Warning 200?\nA: SELECT Fastest Lap FROM 1-10706961-2 WHERE Name = 'Escort Radar Warning 200'"}
-{"text": "table: 1-10707176-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Winning driver, Winning team, Report\nQ: What report was there for the porsche north america?\nA: SELECT Report FROM 1-10707176-2 WHERE Winning team = 'Porsche North America'"}
-{"text": "table: 1-10707176-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Winning driver, Winning team, Report\nQ: What rnds were there for the phoenix international raceway?\nA: SELECT Rnd FROM 1-10707176-2 WHERE Circuit = 'Phoenix International Raceway'"}
-{"text": "table: 1-10707176-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Winning driver, Winning team, Report\nQ: Who was the pole position for the rnd equalling 12?\nA: SELECT Pole position FROM 1-10707176-2 WHERE Rnd = '12'"}
-{"text": "table: 1-10707176-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Winning driver, Winning team, Report\nQ: How many reports were the for the cleveland burke lakefront airport circut?\nA: SELECT COUNT Report FROM 1-10707176-2 WHERE Circuit = 'Cleveland Burke Lakefront Airport'"}
-{"text": "table: 1-10707176-2\ncolumns: Rnd, Race Name, Circuit, City/Location, Date, Pole position, Winning driver, Winning team, Report\nQ: How many winning drivers were the for the rnd equalling 5?\nA: SELECT COUNT Winning driver FROM 1-10707176-2 WHERE Rnd = '5'"}
-{"text": "table: 1-10706879-3\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: The race tony bettenhausen 200 has what smallest rd?\nA: SELECT MIN Rd FROM 1-10706879-3 WHERE Name = 'Tony Bettenhausen 200'"}
-{"text": "table: 1-10706879-3\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: The winning team of the race, los angeles times 500 is who?\nA: SELECT Winning team FROM 1-10706879-3 WHERE Name = 'Los Angeles Times 500'"}
-{"text": "table: 1-10706879-3\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: How many winning drivers in the kraco twin 125 (r2) race were there?\nA: SELECT COUNT Winning driver FROM 1-10706879-3 WHERE Name = 'Kraco Twin 125 (R2)'"}
-{"text": "table: 1-10706879-3\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: What are the races that johnny rutherford has won?\nA: SELECT Name FROM 1-10706879-3 WHERE Winning driver = 'Johnny Rutherford'"}
-{"text": "table: 1-10706879-3\ncolumns: Rd, Name, Pole Position, Fastest Lap, Winning driver, Winning team, Report\nQ: How many fastest laps were there for a rd that equals 10?\nA: SELECT COUNT Fastest Lap FROM 1-10706879-3 WHERE Rd = 10"}
-{"text": "table: 1-10712301-5\ncolumns: Region, Operator, Licence award date, On air date, Closure date\nQ: What is the license award date for North East England?\nA: SELECT Licence award date FROM 1-10712301-5 WHERE Region = 'North East England'"}
-{"text": "table: 1-10733530-3\ncolumns: Nation, Population (thousands), Internet subscriptions (2000) (thousands of users), Internet subscriptions (2008) (thousands of users), % growth (2000\u20132008), % Internet users\nQ: What is the percentage of growth in 2000-2008 in ethiopia?\nA: SELECT % growth (2000\u20132008) FROM 1-10733530-3 WHERE Nation = 'Ethiopia'"}
-{"text": "table: 1-10733530-3\ncolumns: Nation, Population (thousands), Internet subscriptions (2000) (thousands of users), Internet subscriptions (2008) (thousands of users), % growth (2000\u20132008), % Internet users\nQ: Name the total number of percentage growth 2000-2008 of uganda?\nA: SELECT COUNT % growth (2000\u20132008) FROM 1-10733530-3 WHERE Nation = 'Uganda'"}
-{"text": "table: 1-10733530-3\ncolumns: Nation, Population (thousands), Internet subscriptions (2000) (thousands of users), Internet subscriptions (2008) (thousands of users), % growth (2000\u20132008), % Internet users\nQ: What is the maximum percentage grown 2000-2008 in burundi\nA: SELECT MAX % growth (2000\u20132008) FROM 1-10733530-3 WHERE Nation = 'Burundi'"}
-{"text": "table: 1-10798421-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: Provide me with the names of all the villages (German) that has 76.3% of Slovenes in 1951.\nA: SELECT Village (German) FROM 1-10798421-1 WHERE Percent of Slovenes 1951 = '76.3%'"}
-{"text": "table: 1-10798421-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: Give me the minimum number of people in 1991 with 92.5% of Slovenes in 1991.\nA: SELECT MIN Number of people 1991 FROM 1-10798421-1 WHERE Percent of Slovenes 1991 = '92.5%'"}
-{"text": "table: 1-10798421-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: Provide me with the name of all the village (German) that are part of the village (Slovenian) with sele srednji kot. \nA: SELECT Village (German) FROM 1-10798421-1 WHERE Village (Slovenian) = 'Sele Srednji Kot'"}
-{"text": "table: 1-10798421-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: Provide me with the name of all the village (German) that are part of the village (Slovenian) with sele borovnica.\nA: SELECT Village (German) FROM 1-10798421-1 WHERE Village (Slovenian) = 'Sele Borovnica'"}
-{"text": "table: 1-10798421-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: Provide me with the name of the village (German) where there is 96.9% Slovenes in 1951. \nA: SELECT Village (German) FROM 1-10798421-1 WHERE Percent of Slovenes 1951 = '96.9%'"}
-{"text": "table: 1-10798421-1\ncolumns: Village (German), Village (Slovenian), Number of people 1991, Percent of Slovenes 1991, Percent of Slovenes 1951\nQ: Provide with the names of the village (German) that is part of village (Slovenian) with sele srednji kot.\nA: SELECT Village (German) FROM 1-10798421-1 WHERE Village (Slovenian) = 'Sele Srednji Kot'"}
-{"text": "table: 1-10812293-3\ncolumns: Game, Date, Team, Score, High points, High rebounds, High assists, Location Attendance, Record\nQ: What was the score of the game on November 12?\nA: SELECT Score FROM 1-10812293-3 WHERE Date = 'November 12'"}
-{"text": "table: 1-10812293-3\ncolumns: Game, Date, Team, Score, High points, High rebounds, High assists, Location Attendance, Record\nQ: Who had high assists when they played against San Antonio?\nA: SELECT High assists FROM 1-10812293-3 WHERE Team = 'San Antonio'"}
-{"text": "table: 1-10812293-3\ncolumns: Game, Date, Team, Score, High points, High rebounds, High assists, Location Attendance, Record\nQ: Who scored the most points in game 4?\nA: SELECT High points FROM 1-10812293-3 WHERE Game = 4"}
-{"text": "table: 1-10812293-3\ncolumns: Game, Date, Team, Score, High points, High rebounds, High assists, Location Attendance, Record\nQ: Where was the game on November 20?\nA: SELECT Location Attendance FROM 1-10812293-3 WHERE Date = 'November 20'"}
-{"text": "table: 1-10935205-1\ncolumns: No. in season, No. in series, Title, Canadian airdate, US airdate, Production code\nQ: The canadian airdate of 11 february 2008 applied to what series number?\nA: SELECT COUNT No. in series FROM 1-10935205-1 WHERE Canadian airdate = '11 February 2008'"}
-{"text": "table: 1-10935205-1\ncolumns: No. in season, No. in series, Title, Canadian airdate, US airdate, Production code\nQ: The U.S. airdate of 4 april 2008 had a production code of what?\nA: SELECT MAX Production code FROM 1-10935205-1 WHERE US airdate = '4 April 2008'"}
-{"text": "table: 1-10935205-1\ncolumns: No. in season, No. in series, Title, Canadian airdate, US airdate, Production code\nQ: The episode titled \"don't stop believin'\" was what highest number of the season?\nA: SELECT MAX No. in season FROM 1-10935205-1 WHERE Title = '\"Don't Stop Believin'\"'"}
-{"text": "table: 1-10935205-1\ncolumns: No. in season, No. in series, Title, Canadian airdate, US airdate, Production code\nQ: The U.S. airdate of 8 august 2008 also had canadian airdates of what?\nA: SELECT Canadian airdate FROM 1-10935205-1 WHERE US airdate = '8 August 2008'"}
-{"text": "table: 1-10935205-1\ncolumns: No. in season, No. in series, Title, Canadian airdate, US airdate, Production code\nQ: The canadian airdate of 17 march 2008 had how many numbers in the season?\nA: SELECT COUNT No. in season FROM 1-10935205-1 WHERE Canadian airdate = '17 March 2008'"}
-{"text": "table: 1-10935205-1\ncolumns: No. in season, No. in series, Title, Canadian airdate, US airdate, Production code\nQ: For the episode(s) aired in the U.S. on 4 april 2008, what were the names?\nA: SELECT Title FROM 1-10935205-1 WHERE US airdate = '4 April 2008'"}
-{"text": "table: 1-10953197-5\ncolumns: No. in series, No. in season, Title, Director, Writer(s), Original air date, Production code\nQ: Who directed the episode \"Great Sexpectations (2)\"?\nA: SELECT Director FROM 1-10953197-5 WHERE Title = '\"Great Sexpectations (2)\"'"}
-{"text": "table: 1-10975034-2\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: Which player from the 2004 CFL draft attended Wilfrid Laurier?\nA: SELECT Player FROM 1-10975034-2 WHERE College = 'Wilfrid Laurier'"}
-{"text": "table: 1-10975034-2\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What position does Christian Leibl-Cote play?\nA: SELECT Position FROM 1-10975034-2 WHERE Player = 'Christian Leibl-Cote'"}
-{"text": "table: 1-10975034-2\ncolumns: Pick #, CFL Team, Player, Position, College\nQ: What is the pick number for Northwestern college?\nA: SELECT MAX Pick # FROM 1-10975034-2 WHERE College = 'Northwestern'"}
-{"text": "table: 1-10992-3\ncolumns: No, City district (Stadtteil), Area in km\u00b2, Population, Foreign nationals, Foreign nationals in %, Area district (Ortsbezirk)\nQ: How many foreigners in percentage terms had a population of 4.911?\nA: SELECT COUNT Foreign nationals in % FROM 1-10992-3 WHERE Population = '4.911'"}
-{"text": "table: 1-10992-3\ncolumns: No, City district (Stadtteil), Area in km\u00b2, Population, Foreign nationals, Foreign nationals in %, Area district (Ortsbezirk)\nQ: What is the number of the city district of stadtteil where foreigners are 5.162?\nA: SELECT COUNT City district (Stadtteil) FROM 1-10992-3 WHERE Foreign nationals = '5.162'"}
-{"text": "table: 1-10992-3\ncolumns: No, City district (Stadtteil), Area in km\u00b2, Population, Foreign nationals, Foreign nationals in %, Area district (Ortsbezirk)\nQ: What is the city where the number is 47?\nA: SELECT City district (Stadtteil) FROM 1-10992-3 WHERE No = '47'"}
-{"text": "table: 1-11044765-1\ncolumns: School, Mascot, Location, League, Enrollment\nQ: Which leagues have Raiders as their mascot?\nA: SELECT League FROM 1-11044765-1 WHERE Mascot = 'Raiders'"}
-{"text": "table: 1-11044765-1\ncolumns: School, Mascot, Location, League, Enrollment\nQ: Which leagues is the Galena school in?\nA: SELECT League FROM 1-11044765-1 WHERE School = 'Galena'"}
-{"text": "table: 1-11044765-1\ncolumns: School, Mascot, Location, League, Enrollment\nQ: What city and state is the Lancers mascot located?\nA: SELECT Location FROM 1-11044765-1 WHERE Mascot = 'Lancers'"}
-{"text": "table: 1-11044765-1\ncolumns: School, Mascot, Location, League, Enrollment\nQ: What city and state are the miners located in?\nA: SELECT Location FROM 1-11044765-1 WHERE Mascot = 'Miners'"}
-{"text": "table: 1-11044765-1\ncolumns: School, Mascot, Location, League, Enrollment\nQ: Which school has the Raiders as their mascot?\nA: SELECT School FROM 1-11044765-1 WHERE Mascot = 'Raiders'"}
-{"text": "table: 1-1121352-2\ncolumns: No., Date, Tournament, Winning score, To par, Margin of victory, Runner(s)-up\nQ: Where was the tournament dated nov 3, 2002?\nA: SELECT Tournament FROM 1-1121352-2 WHERE Date = 'Nov 3, 2002'"}
-{"text": "table: 1-1121352-2\ncolumns: No., Date, Tournament, Winning score, To par, Margin of victory, Runner(s)-up\nQ: Where is the margin of victory dated mar 28, 2004?\nA: SELECT Margin of victory FROM 1-1121352-2 WHERE Date = 'Mar 28, 2004'"}
-{"text": "table: 1-1121352-2\ncolumns: No., Date, Tournament, Winning score, To par, Margin of victory, Runner(s)-up\nQ: What is the to par dated may 4, 2003?\nA: SELECT To par FROM 1-1121352-2 WHERE Date = 'May 4, 2003'"}
-{"text": "table: 1-1121352-2\ncolumns: No., Date, Tournament, Winning score, To par, Margin of victory, Runner(s)-up\nQ: What date were the runner ups pat hurst juli inkster?\nA: SELECT Date FROM 1-1121352-2 WHERE Runner(s)-up = 'Pat Hurst Juli Inkster'"}
-{"text": "table: 1-11210576-4\ncolumns: Character, Fate, Actor, First Episode, Final Episode, Duration, Final Episode Count\nQ: what's the total number of\u00a0final epbeingode count\u00a0with\u00a0character\u00a0being rick stetler\nA: SELECT COUNT Final Episode Count FROM 1-11210576-4 WHERE Character = 'Rick Stetler'"}
-{"text": "table: 1-11210576-4\ncolumns: Character, Fate, Actor, First Episode, Final Episode, Duration, Final Episode Count\nQ: what are all the actor where first episode is \"ambush\"\nA: SELECT Actor FROM 1-11210576-4 WHERE First Episode = '\"Ambush\"'"}
-{"text": "table: 1-11210576-4\ncolumns: Character, Fate, Actor, First Episode, Final Episode, Duration, Final Episode Count\nQ: what's the\u00a0character\u00a0with\u00a0fate\u00a0being deceased: knife wound\nA: SELECT Character FROM 1-11210576-4 WHERE Fate = 'Deceased: Knife Wound'"}
-{"text": "table: 1-11210576-4\ncolumns: Character, Fate, Actor, First Episode, Final Episode, Duration, Final Episode Count\nQ: what's the total number of\u00a0final epbeingode count\u00a0with\u00a0first epbeingode\u00a0being \"l.a.\"\nA: SELECT COUNT Final Episode Count FROM 1-11210576-4 WHERE First Episode = '\"L.A.\"'"}
-{"text": "table: 1-11210576-4\ncolumns: Character, Fate, Actor, First Episode, Final Episode, Duration, Final Episode Count\nQ: what's the\u00a0actor\u00a0with\u00a0character\u00a0being judge joseph ratner\nA: SELECT Actor FROM 1-11210576-4 WHERE Character = 'Judge Joseph Ratner'"}
-{"text": "table: 1-11210576-4\ncolumns: Character, Fate, Actor, First Episode, Final Episode, Duration, Final Episode Count\nQ: what's the\u00a0first epbeingode\u00a0with\u00a0final epbeingode\u00a0being \"rio\"\nA: SELECT First Episode FROM 1-11210576-4 WHERE Final Episode = '\"Rio\"'"}
-{"text": "table: 1-11214772-2\ncolumns: Year, Champion, Score, Runner-Up, Location, Semi-Finalist #1, Semi-Finalist #2\nQ: Which team was the second semi finalist in 2007?\nA: SELECT Semi-Finalist #2 FROM 1-11214772-2 WHERE Year = 2007"}
-{"text": "table: 1-11214772-2\ncolumns: Year, Champion, Score, Runner-Up, Location, Semi-Finalist #1, Semi-Finalist #2\nQ: How many teams were listed as runner up in 2005 and there the first semi finalist was Western Carolina?\nA: SELECT COUNT Runner-Up FROM 1-11214772-2 WHERE Semi-Finalist #1 = 'Western Carolina' AND Year = 2005"}
diff --git a/exo/train/dataset.py b/exo/train/dataset.py
deleted file mode 100644
index 8d583ecc..00000000
--- a/exo/train/dataset.py
+++ /dev/null
@@ -1,80 +0,0 @@
-#from https://github.com/ml-explore/mlx-examples
-from pathlib import Path
-import numpy as np
-import json
-from functools import partial, reduce
-def compose(*funcs):
- return reduce(lambda f, g: lambda x: f(g(x)), funcs, lambda x : x)
-
-def batch_with_lengths(tokens, maxlen = None):
- lengths = [len(x) for x in tokens]
- batch_size = len(lengths)
- if maxlen is None:
- maxlen = max(lengths)
- else:
- lengths = [min(maxlen, l) for l in lengths]
-
- # Pad to the max length
- batch_arr = np.zeros((batch_size, maxlen), np.int32)
-
- for j in range(batch_size):
- batch_arr[j, : lengths[j]] = tokens[j]
- batch = np.array(batch_arr)
- return batch[:, :-1], batch[:, 1:], np.array(lengths)
-
-def batch_chunk(batch_size):
- return lambda d, i: d[i:i + batch_size]
-
-
-def iterate_batches(dset, batch_size, train=False, uniform_length=True):
-# Shuffle indices
- make_batch = lambda b: batch_with_lengths(b, maxlen=dset._maxlen if uniform_length else None)
- chunk = batch_chunk(batch_size)
- while True:
- indices = np.arange(len(dset))
- if train:
- indices = np.random.permutation(indices)
- batch = compose(make_batch, lambda i: [dset[k] for k in i], partial(chunk, indices))
-
- # Collect batches from dataset
- for i in range(0, len(indices) - batch_size + 1, batch_size):
- yield batch(i)
-
- if not train:
- break
-
-class Dataset:
- def __init__(self, path: Path, preprocess=lambda item: item, loadline=json.loads, metrics={}):
- if not path.exists():
- self._data = None
- else:
- self.preprocess = preprocess
- with open(path, "r") as fid:
- self._data = [loadline(l) for l in fid]
- self._maxlen = max([len(preprocess(x)) for x in self._data])
- # Check if any sequence is longer than 2048 tokens
- if self._maxlen > 2048:
- print("You've got sequences with over 2048 tokens in here! Split your data fool!")
-
-
- def __getitem__(self, idx: int):
- return self.preprocess(self._data[idx])
-
- def __len__(self):
- return len(self._data)
-
-
-def load_dataset(data_path: str, preprocess=lambda i: i, loadline=json.loads):
- def load_and_check(name):
- dataset_path = Path(data_path) / f"{name}.jsonl"
- try:
- return Dataset(dataset_path, preprocess=preprocess, loadline=loadline)
- except Exception as e:
- print(f"Unable to build dataset {dataset_path} ({e})")
- raise
-
- names = ("train", "valid", "test")
- train, valid, test = (load_and_check(n) for n in names)
-
- return train, valid, test
-
diff --git a/exo/viz/__init__.py b/exo/viz/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/exo/viz/test_topology_viz.py b/exo/viz/test_topology_viz.py
deleted file mode 100644
index a10ac43b..00000000
--- a/exo/viz/test_topology_viz.py
+++ /dev/null
@@ -1,129 +0,0 @@
-import asyncio
-import unittest
-from datetime import timedelta
-from exo.viz.topology_viz import TopologyViz
-from exo.topology.topology import Topology
-from exo.topology.device_capabilities import DeviceCapabilities, DeviceFlops
-from exo.topology.partitioning_strategy import Partition
-from exo.download.download_progress import RepoProgressEvent
-
-
-def create_hf_repo_progress_event(
- completed_files: int = 5,
- total_files: int = 10,
- downloaded_bytes: int = 500000000,
- downloaded_bytes_this_session: int = 250000000,
- total_bytes: int = 1000000000,
- overall_speed: int = 5000000,
- overall_eta: timedelta = timedelta(seconds=100),
- file_progress: dict = None,
- status: str = "in_progress"
-) -> RepoProgressEvent:
- if file_progress is None:
- file_progress = {
- "file1.bin":
- RepoFileProgressEvent(
- repo_id="repo_id",
- repo_revision="repo_revision",
- file_path="file1.bin",
- downloaded=100000000,
- downloaded_this_session=50000000,
- total=200000000,
- speed=1000000,
- eta=timedelta(seconds=100),
- status="in_progress"
- ), "file2.bin":
- RepoFileProgressEvent(
- repo_id="repo_id",
- repo_revision="repo_revision",
- file_path="file2.bin",
- downloaded=200000000,
- downloaded_this_session=100000000,
- total=200000000,
- speed=2000000,
- eta=timedelta(seconds=0),
- status="complete"
- )
- }
-
- return RepoProgressEvent(
- repo_id="repo_id",
- repo_revision="repo_revision",
- completed_files=completed_files,
- total_files=total_files,
- downloaded_bytes=downloaded_bytes,
- downloaded_bytes_this_session=downloaded_bytes_this_session,
- total_bytes=total_bytes,
- overall_speed=overall_speed,
- overall_eta=overall_eta,
- file_progress=file_progress,
- status=status
- )
-
-
-class TestNodeViz(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- self.topology = Topology()
- self.topology.update_node(
- "node1",
- DeviceCapabilities(model="ModelA", chip="ChipA", memory=8*1024, flops=DeviceFlops(fp32=1.0, fp16=2.0, int8=4.0)),
- )
- self.topology.update_node(
- "node2",
- DeviceCapabilities(model="ModelB", chip="ChipB", memory=16*1024, flops=DeviceFlops(fp32=2.0, fp16=4.0, int8=8.0)),
- )
- self.topology.update_node(
- "node3",
- DeviceCapabilities(model="ModelC", chip="ChipC", memory=32*1024, flops=DeviceFlops(fp32=4.0, fp16=8.0, int8=16.0)),
- )
- self.topology.update_node(
- "node4",
- DeviceCapabilities(model="ModelD", chip="ChipD", memory=64*1024, flops=DeviceFlops(fp32=8.0, fp16=16.0, int8=32.0)),
- )
-
- self.top_viz = TopologyViz()
- await asyncio.sleep(2) # Simulate running for a short time
-
- async def test_layout_generation(self):
- # self.top_viz._generate_layout()
- self.top_viz.refresh()
- import time
-
- time.sleep(2)
- self.top_viz.update_visualization(
- self.topology,
- [
- Partition("node1", 0, 0.2),
- Partition("node4", 0.2, 0.4),
- Partition("node2", 0.4, 0.8),
- Partition("node3", 0.8, 0.9),
- ],
- "node1",
- {
- "node1": create_hf_repo_progress_event(),
- "node2": create_hf_repo_progress_event(),
- "node3": create_hf_repo_progress_event(),
- "node4": create_hf_repo_progress_event(),
- },
- )
- time.sleep(2)
- self.topology.active_node_id = "node3"
- self.top_viz.update_visualization(
- self.topology,
- [
- Partition("node1", 0, 0.3),
- Partition("node5", 0.3, 0.5),
- Partition("node2", 0.5, 0.7),
- Partition("node4", 0.7, 0.9),
- ],
- "node5",
- {
- "node1": create_hf_repo_progress_event(),
- "node5": create_hf_repo_progress_event(),
- },
- )
- time.sleep(2)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/exo/viz/topology_viz.py b/exo/viz/topology_viz.py
deleted file mode 100644
index 82064b42..00000000
--- a/exo/viz/topology_viz.py
+++ /dev/null
@@ -1,377 +0,0 @@
-import math
-from collections import OrderedDict
-from typing import List, Optional, Tuple, Dict
-from exo.helpers import exo_text, pretty_print_bytes, pretty_print_bytes_per_second
-from exo.topology.topology import Topology
-from exo.topology.partitioning_strategy import Partition
-from exo.download.download_progress import RepoProgressEvent
-from exo.topology.device_capabilities import UNKNOWN_DEVICE_CAPABILITIES
-from rich.console import Console, Group
-from rich.text import Text
-from rich.live import Live
-from rich.style import Style
-from rich.table import Table
-from rich.layout import Layout
-from rich.syntax import Syntax
-from rich.panel import Panel
-from rich.markdown import Markdown
-
-
-class TopologyViz:
- def __init__(self, chatgpt_api_endpoints: List[str] = [], web_chat_urls: List[str] = []):
- self.chatgpt_api_endpoints = chatgpt_api_endpoints
- self.web_chat_urls = web_chat_urls
- self.topology = Topology()
- self.partitions: List[Partition] = []
- self.node_id = None
- self.node_download_progress: Dict[str, RepoProgressEvent] = {}
- self.requests: OrderedDict[str, Tuple[str, str]] = {}
-
- self.console = Console()
- self.layout = Layout()
- self.layout.split(Layout(name="main"), Layout(name="prompt_output", size=15), Layout(name="download", size=25))
- self.main_panel = Panel(self._generate_main_layout(), title="Exo Cluster (0 nodes)", border_style="bright_yellow")
- self.prompt_output_panel = Panel("", title="Prompt and Output", border_style="green")
- self.download_panel = Panel("", title="Download Progress", border_style="cyan")
- self.layout["main"].update(self.main_panel)
- self.layout["prompt_output"].update(self.prompt_output_panel)
- self.layout["download"].update(self.download_panel)
-
- # Initially hide the prompt_output panel
- self.layout["prompt_output"].visible = False
- self.live_panel = Live(self.layout, auto_refresh=False, console=self.console)
- self.live_panel.start()
-
- def update_visualization(self, topology: Topology, partitions: List[Partition], node_id: Optional[str] = None, node_download_progress: Dict[str, RepoProgressEvent] = {}):
- self.topology = topology
- self.partitions = partitions
- self.node_id = node_id
- if node_download_progress:
- self.node_download_progress = node_download_progress
- self.refresh()
-
- def update_prompt(self, request_id: str, prompt: Optional[str] = None):
- self.requests[request_id] = [prompt, self.requests.get(request_id, ["", ""])[1]]
- self.refresh()
-
- def update_prompt_output(self, request_id: str, output: Optional[str] = None):
- self.requests[request_id] = [self.requests.get(request_id, ["", ""])[0], output]
- self.refresh()
-
- def refresh(self):
- self.main_panel.renderable = self._generate_main_layout()
- # Update the panel title with the number of nodes and partitions
- node_count = len(self.topology.nodes)
- self.main_panel.title = f"Exo Cluster ({node_count} node{'s' if node_count != 1 else ''})"
-
- # Update and show/hide prompt and output panel
- if any(r[0] or r[1] for r in self.requests.values()):
- self.prompt_output_panel = self._generate_prompt_output_layout()
- self.layout["prompt_output"].update(self.prompt_output_panel)
- self.layout["prompt_output"].visible = True
- else:
- self.layout["prompt_output"].visible = False
-
- # Only show download_panel if there are in-progress downloads
- if any(progress.status == "in_progress" for progress in self.node_download_progress.values()):
- self.download_panel.renderable = self._generate_download_layout()
- self.layout["download"].visible = True
- else:
- self.layout["download"].visible = False
-
- self.live_panel.update(self.layout, refresh=True)
-
- def _generate_prompt_output_layout(self) -> Panel:
- content = []
- requests = list(self.requests.values())[-3:] # Get the 3 most recent requests
- max_width = self.console.width - 6 # Full width minus padding and icon
-
- # Calculate available height for content
- panel_height = 15 # Fixed panel height
- available_lines = panel_height - 2 # Subtract 2 for panel borders
- lines_per_request = available_lines // len(requests) if requests else 0
-
- for (prompt, output) in reversed(requests):
- prompt_icon, output_icon = "💬️", "🤖"
-
- # Equal space allocation for prompt and output
- max_prompt_lines = lines_per_request // 2
- max_output_lines = lines_per_request - max_prompt_lines - 1 # -1 for spacing
-
- # Process prompt
- prompt_lines = []
- for line in prompt.split('\n'):
- words = line.split()
- current_line = []
- current_length = 0
-
- for word in words:
- if current_length + len(word) + 1 <= max_width:
- current_line.append(word)
- current_length += len(word) + 1
- else:
- if current_line:
- prompt_lines.append(' '.join(current_line))
- current_line = [word]
- current_length = len(word)
-
- if current_line:
- prompt_lines.append(' '.join(current_line))
-
- # Truncate prompt if needed
- if len(prompt_lines) > max_prompt_lines:
- prompt_lines = prompt_lines[:max_prompt_lines]
- if prompt_lines:
- last_line = prompt_lines[-1]
- if len(last_line) + 4 <= max_width:
- prompt_lines[-1] = last_line + " ..."
- else:
- prompt_lines[-1] = last_line[:max_width-4] + " ..."
-
- prompt_text = Text(f"{prompt_icon} ", style="bold bright_blue")
- prompt_text.append('\n'.join(prompt_lines), style="white")
- content.append(prompt_text)
-
- # Process output with similar word wrapping
- if output: # Only process output if it exists
- output_lines = []
- for line in output.split('\n'):
- words = line.split()
- current_line = []
- current_length = 0
-
- for word in words:
- if current_length + len(word) + 1 <= max_width:
- current_line.append(word)
- current_length += len(word) + 1
- else:
- if current_line:
- output_lines.append(' '.join(current_line))
- current_line = [word]
- current_length = len(word)
-
- if current_line:
- output_lines.append(' '.join(current_line))
-
- # Truncate output if needed
- if len(output_lines) > max_output_lines:
- output_lines = output_lines[:max_output_lines]
- if output_lines:
- last_line = output_lines[-1]
- if len(last_line) + 4 <= max_width:
- output_lines[-1] = last_line + " ..."
- else:
- output_lines[-1] = last_line[:max_width-4] + " ..."
-
- output_text = Text(f"{output_icon} ", style="bold bright_magenta")
- output_text.append('\n'.join(output_lines), style="white")
- content.append(output_text)
-
- content.append(Text()) # Empty line between entries
-
- return Panel(
- Group(*content),
- title="",
- border_style="cyan",
- height=panel_height,
- expand=True
- )
-
- def _generate_main_layout(self) -> str:
- # Calculate visualization parameters
- num_partitions = len(self.partitions)
- radius_x = 30
- radius_y = 12
- center_x, center_y = 50, 24 # Increased center_y to add more space
-
- # Generate visualization
- visualization = [[" " for _ in range(100)] for _ in range(48)] # Increased height to 48
-
- # Add exo_text at the top in bright yellow
- exo_lines = exo_text.split("\n")
- yellow_style = Style(color="bright_yellow")
- max_line_length = max(len(line) for line in exo_lines)
- for i, line in enumerate(exo_lines):
- centered_line = line.center(max_line_length)
- start_x = (100-max_line_length) // 2 + 15
- colored_line = Text(centered_line, style=yellow_style)
- for j, char in enumerate(str(colored_line)):
- if 0 <= start_x + j < 100 and i < len(visualization):
- visualization[i][start_x + j] = char
-
- # Display chatgpt_api_endpoints and web_chat_urls
- info_lines = []
- if len(self.web_chat_urls) > 0:
- info_lines.append(f"Web Chat URL (tinychat): {' '.join(self.web_chat_urls[:1])}")
- if len(self.chatgpt_api_endpoints) > 0:
- info_lines.append(f"ChatGPT API endpoint: {' '.join(self.chatgpt_api_endpoints[:1])}")
-
- info_start_y = len(exo_lines) + 1
- for i, line in enumerate(info_lines):
- start_x = (100 - len(line)) // 2 + 15
- for j, char in enumerate(line):
- if 0 <= start_x + j < 100 and info_start_y + i < 48:
- visualization[info_start_y + i][start_x + j] = char
-
- # Calculate total FLOPS and position on the bar
- total_flops = sum(self.topology.nodes.get(partition.node_id, UNKNOWN_DEVICE_CAPABILITIES).flops.fp16 for partition in self.partitions)
- bar_pos = (math.tanh(total_flops**(1/3)/2.5 - 2) + 1)
-
- # Add GPU poor/rich bar
- bar_width = 30
- bar_start_x = (100-bar_width) // 2
- bar_y = info_start_y + len(info_lines) + 1
-
- # Create a gradient bar using emojis
- gradient_bar = Text()
- emojis = ["🟥", "🟧", "🟨", "🟩"]
- for i in range(bar_width):
- emoji_index = min(int(i/(bar_width/len(emojis))), len(emojis) - 1)
- gradient_bar.append(emojis[emoji_index])
-
- # Add the gradient bar to the visualization
- visualization[bar_y][bar_start_x - 1] = "["
- visualization[bar_y][bar_start_x + bar_width] = "]"
- for i, segment in enumerate(str(gradient_bar)):
- visualization[bar_y][bar_start_x + i] = segment
-
- # Add labels
- visualization[bar_y - 1][bar_start_x - 10:bar_start_x - 3] = "GPU poor"
- visualization[bar_y - 1][bar_start_x + bar_width*2 + 2:bar_start_x + bar_width*2 + 11] = "GPU rich"
-
- # Add position indicator and FLOPS value
- pos_x = bar_start_x + int(bar_pos*bar_width)
- flops_str = f"{total_flops:.2f} TFLOPS"
- visualization[bar_y - 1][pos_x] = "▼"
- visualization[bar_y + 1][pos_x - len(flops_str) // 2:pos_x + len(flops_str) // 2 + len(flops_str) % 2] = flops_str
- visualization[bar_y + 2][pos_x] = "▲"
-
- # Add an extra empty line for spacing
- bar_y += 4
-
- for i, partition in enumerate(self.partitions):
- device_capabilities = self.topology.nodes.get(partition.node_id, UNKNOWN_DEVICE_CAPABILITIES)
-
- angle = 2*math.pi*i/num_partitions
- x = int(center_x + radius_x*math.cos(angle))
- y = int(center_y + radius_y*math.sin(angle))
-
- # Place node with different color for active node and this node
- if partition.node_id == self.topology.active_node_id:
- visualization[y][x] = "🔴"
- elif partition.node_id == self.node_id:
- visualization[y][x] = "🟢"
- else:
- visualization[y][x] = "🔵"
-
- # Place node info (model, memory, TFLOPS, partition) on three lines
- node_info = [
- f"{device_capabilities.model} {device_capabilities.memory // 1024}GB",
- f"{device_capabilities.flops.fp16}TFLOPS",
- f"[{partition.start:.2f}-{partition.end:.2f}]",
- ]
-
- # Calculate info position based on angle
- info_distance_x = radius_x + 6
- info_distance_y = radius_y + 3
- info_x = int(center_x + info_distance_x*math.cos(angle))
- info_y = int(center_y + info_distance_y*math.sin(angle))
-
- # Adjust text position to avoid overwriting the node icon and prevent cutoff
- if info_x < x:
- info_x = max(0, x - len(max(node_info, key=len)) - 1)
- elif info_x > x:
- info_x = min(99 - len(max(node_info, key=len)), info_x)
-
- # Adjust for top and bottom nodes
- if 5*math.pi/4 < angle < 7*math.pi/4:
- info_x += 4
- elif math.pi/4 < angle < 3*math.pi/4:
- info_x += 3
- info_y -= 2
-
- for j, line in enumerate(node_info):
- for k, char in enumerate(line):
- if 0 <= info_y + j < 48 and 0 <= info_x + k < 100:
- if info_y + j != y or info_x + k != x:
- visualization[info_y + j][info_x + k] = char
-
- # Draw line to next node and add connection description
- next_i = (i+1) % num_partitions
- next_angle = 2*math.pi*next_i/num_partitions
- next_x = int(center_x + radius_x*math.cos(next_angle))
- next_y = int(center_y + radius_y*math.sin(next_angle))
-
- # Get connection descriptions
- conn1 = self.topology.peer_graph.get(partition.node_id, set())
- conn2 = self.topology.peer_graph.get(self.partitions[next_i].node_id, set())
- description1 = next((c.description for c in conn1 if c.to_id == self.partitions[next_i].node_id), "")
- description2 = next((c.description for c in conn2 if c.to_id == partition.node_id), "")
- connection_description = f"{description1}/{description2}"
-
- # Simple line drawing
- steps = max(abs(next_x - x), abs(next_y - y))
- for step in range(1, steps):
- line_x = int(x + (next_x-x)*step/steps)
- line_y = int(y + (next_y-y)*step/steps)
- if 0 <= line_y < 48 and 0 <= line_x < 100:
- visualization[line_y][line_x] = "-"
-
- # Add connection description near the midpoint of the line
- mid_x = (x + next_x) // 2
- mid_y = (y + next_y) // 2
- # Center the description text around the midpoint
- desc_start_x = mid_x - len(connection_description) // 2
- for j, char in enumerate(connection_description):
- if 0 <= mid_y < 48 and 0 <= desc_start_x + j < 100:
- visualization[mid_y][desc_start_x + j] = char
-
- # Convert to string
- return "\n".join("".join(str(char) for char in row) for row in visualization)
-
- def _generate_download_layout(self) -> Table:
- summary = Table(show_header=False, box=None, padding=(0, 1), expand=True)
- summary.add_column("Info", style="cyan", no_wrap=True, ratio=50)
- summary.add_column("Progress", style="cyan", no_wrap=True, ratio=40)
- summary.add_column("Percentage", style="cyan", no_wrap=True, ratio=10)
-
- # Current node download progress
- if self.node_id in self.node_download_progress:
- download_progress = self.node_download_progress[self.node_id]
- title = f"Downloading model {download_progress.repo_id}@{download_progress.repo_revision} ({download_progress.completed_files}/{download_progress.total_files}):"
- summary.add_row(Text(title, style="bold"))
- progress_info = f"{pretty_print_bytes(download_progress.downloaded_bytes)} / {pretty_print_bytes(download_progress.total_bytes)} ({pretty_print_bytes_per_second(download_progress.overall_speed)})"
- summary.add_row(progress_info)
-
- eta_info = f"{download_progress.overall_eta}"
- summary.add_row(eta_info)
-
- summary.add_row("") # Empty row for spacing
-
- for file_path, file_progress in download_progress.file_progress.items():
- if file_progress.status != "complete":
- progress = int(file_progress.downloaded/file_progress.total*30)
- bar = f"[{'=' * progress}{' ' * (30 - progress)}]"
- percentage = f"{file_progress.downloaded / file_progress.total * 100:.0f}%"
- summary.add_row(Text(file_path[:30], style="cyan"), bar, percentage)
-
- summary.add_row("") # Empty row for spacing
-
- # Other nodes download progress summary
- summary.add_row(Text("Other Nodes Download Progress:", style="bold"))
- for node_id, progress in self.node_download_progress.items():
- if node_id != self.node_id:
- device = self.topology.nodes.get(node_id)
- partition = next((p for p in self.partitions if p.node_id == node_id), None)
- partition_info = f"[{partition.start:.2f}-{partition.end:.2f}]" if partition else ""
- percentage = progress.downloaded_bytes/progress.total_bytes*100 if progress.total_bytes > 0 else 0
- speed = pretty_print_bytes_per_second(progress.overall_speed)
- device_info = f"{device.model if device else 'Unknown Device'} {device.memory // 1024 if device else '?'}GB {partition_info}"
- progress_info = f"{progress.repo_id}@{progress.repo_revision} ({speed})"
- progress_bar = f"[{'=' * int(percentage // 3.33)}{' ' * (30 - int(percentage // 3.33))}]"
- percentage_str = f"{percentage:.1f}%"
- eta_str = f"{progress.overall_eta}"
- summary.add_row(device_info, progress_info, percentage_str)
- summary.add_row("", progress_bar, eta_str)
-
- return summary
diff --git a/extra/dashboard/dashboard.py b/extra/dashboard/dashboard.py
deleted file mode 100644
index 0ef6d3ed..00000000
--- a/extra/dashboard/dashboard.py
+++ /dev/null
@@ -1,1147 +0,0 @@
-import os
-import json
-import logging
-import asyncio
-import aiohttp
-import pandas as pd
-import plotly.express as px
-from typing import List, Dict, Optional
-from pathlib import Path
-from plotly.subplots import make_subplots
-import plotly.graph_objects as go
-import time
-import pygame.mixer
-from datetime import datetime
-
-class AsyncCircleCIClient:
- def __init__(self, token: str, project_slug: str):
- self.token = token
- self.project_slug = project_slug
- self.base_url = "https://circleci.com/api/v2"
- self.headers = {
- "Circle-Token": token,
- "Accept": "application/json"
- }
- self.logger = logging.getLogger("CircleCI")
-
- async def get_json(self, session: aiohttp.ClientSession, url: str, params: Dict = None) -> Dict:
- async with session.get(url, params=params) as response:
- response.raise_for_status()
- return await response.json()
-
- async def get_recent_pipelines(
- self,
- session: aiohttp.ClientSession,
- org_slug: str = None,
- page_token: str = None,
- limit: int = None,
- branch: str = None
- ):
- """
- Get recent pipelines for a project with pagination support
- """
- params = {
- "branch": branch,
- "page-token": page_token
- }
-
- # Remove None values
- params = {k: v for k, v in params.items() if v is not None}
-
- url = f"{self.base_url}/project/{self.project_slug}/pipeline"
- data = await self.get_json(session, url, params)
- pipelines = data["items"]
-
- next_page_token = data.get("next_page_token")
-
- # If we have a limit, check if we need more pages
- if limit and len(pipelines) >= limit:
- return pipelines
-
- # If there are more pages and we haven't hit the limit, recursively get them
- if next_page_token:
- next_pipelines = await self.get_recent_pipelines(
- session,
- org_slug,
- page_token=next_page_token,
- limit=limit - len(pipelines) if limit else None, # Adjust limit for next page
- branch=branch
- )
- pipelines.extend(next_pipelines)
-
- return pipelines
-
- async def get_workflow_jobs(self, session: aiohttp.ClientSession, pipeline_id: str) -> List[Dict]:
- self.logger.debug(f"Fetching workflows for pipeline {pipeline_id}")
- url = f"{self.base_url}/pipeline/{pipeline_id}/workflow"
- workflows_data = await self.get_json(session, url)
- workflows = workflows_data["items"]
-
- # Fetch all jobs for all workflows in parallel
- jobs_tasks = []
- for workflow in workflows:
- url = f"{self.base_url}/workflow/{workflow['id']}/job"
- jobs_tasks.append(self.get_json(session, url))
-
- jobs_responses = await asyncio.gather(*jobs_tasks, return_exceptions=True)
-
- all_jobs = []
- for jobs_data in jobs_responses:
- if isinstance(jobs_data, Exception):
- continue
- all_jobs.extend(jobs_data["items"])
-
- return all_jobs
-
- async def get_artifacts(self, session: aiohttp.ClientSession, job_number: str) -> List[Dict]:
- url = f"{self.base_url}/project/{self.project_slug}/{job_number}/artifacts"
- data = await self.get_json(session, url)
- return data["items"]
-
-class PackageSizeTracker:
- def __init__(self, token: str, project_slug: str, debug: bool = False):
- self.setup_logging(debug)
- self.client = AsyncCircleCIClient(token, project_slug)
- self.logger = logging.getLogger("PackageSizeTracker")
- self.last_data_hash = None
- self.debug = debug
-
- # Initialize pygame mixer
- pygame.mixer.init()
-
- # Sound file paths - can use MP3 files with pygame
- sounds_dir = Path(__file__).parent / "sounds"
- self.sounds = {
- 'lines_up': sounds_dir / "gta5_wasted.mp3",
- 'lines_down': sounds_dir / "pokemon_evolve.mp3",
- 'tokens_up': sounds_dir / "pokemon_evolve.mp3",
- 'tokens_down': sounds_dir / "gta5_wasted.mp3",
- 'size_up': sounds_dir / "gta5_wasted.mp3",
- 'size_down': sounds_dir / "pokemon_evolve.mp3"
- }
-
- def test_sound_effects(self):
- """Test all sound effects with a small delay between each"""
- self.logger.info("Testing sound effects...")
- for sound_key in self.sounds:
- self.logger.info(f"Playing {sound_key}")
- self._play_sound(sound_key)
- time.sleep(1) # Wait 1 second between sounds
-
- def setup_logging(self, debug: bool):
- level = logging.DEBUG if debug else logging.INFO
- logging.basicConfig(
- level=level,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- datefmt='%H:%M:%S'
- )
-
- def extract_commit_info(self, pipeline: Dict) -> Optional[Dict]:
- try:
- # Extract from github_app first (preferred)
- if 'trigger_parameters' in pipeline and 'github_app' in pipeline['trigger_parameters']:
- github_app = pipeline['trigger_parameters']['github_app']
- return {
- 'commit_hash': github_app.get('checkout_sha'),
- 'web_url': f"{github_app.get('repo_url')}/commit/{github_app.get('checkout_sha')}",
- 'branch': github_app.get('branch', 'unknown'),
- 'author': {
- 'name': github_app.get('commit_author_name'),
- 'email': github_app.get('commit_author_email'),
- 'username': github_app.get('user_username')
- },
- 'message': github_app.get('commit_message')
- }
-
- # Fallback to git parameters
- if 'trigger_parameters' in pipeline and 'git' in pipeline['trigger_parameters']:
- git = pipeline['trigger_parameters']['git']
- return {
- 'commit_hash': git.get('checkout_sha'),
- 'web_url': f"{git.get('repo_url')}/commit/{git.get('checkout_sha')}",
- 'branch': git.get('branch', 'unknown'),
- 'author': {
- 'name': git.get('commit_author_name'),
- 'email': git.get('commit_author_email'),
- 'username': git.get('author_login')
- },
- 'message': git.get('commit_message')
- }
-
- self.logger.warning(f"Could not find commit info in pipeline {pipeline['id']}")
- return None
-
- except Exception as e:
- self.logger.error(f"Error extracting commit info: {str(e)}")
- return None
-
- async def process_pipeline(self, session: aiohttp.ClientSession, pipeline: Dict) -> Optional[Dict]:
- try:
- commit_info = self.extract_commit_info(pipeline)
- if not commit_info:
- return None
-
- data_point = {
- "commit_hash": commit_info['commit_hash'],
- "commit_url": commit_info['web_url'],
- "timestamp": pipeline.get("created_at", pipeline.get("updated_at")),
- "pipeline_status": pipeline.get("state", "unknown"),
- "branch": commit_info['branch'],
- "author": commit_info['author'],
- "commit_message": commit_info['message']
- }
-
- jobs = await self.client.get_workflow_jobs(session, pipeline["id"])
-
- # Get package size data
- size_job = next(
- (j for j in jobs if j["name"] == "measure_pip_sizes" and j["status"] == "success"),
- None
- )
-
- # Get line count data
- linecount_job = next(
- (j for j in jobs if j["name"] == "check_line_count" and j["status"] == "success"),
- None
- )
-
- # Get benchmark data from runner job
- benchmark_job = next(
- (j for j in jobs if j["name"] == "runner" and j["status"] == "success"),
- None
- )
-
- # Return None if no relevant jobs found
- if not size_job and not linecount_job and not benchmark_job:
- self.logger.debug(f"No relevant jobs found for pipeline {pipeline['id']}")
- return None
-
- # Process benchmark data if available
- if benchmark_job:
- benchmark_artifacts = await self.client.get_artifacts(session, benchmark_job["job_number"])
- benchmark_report = next(
- (a for a in benchmark_artifacts if a["path"].endswith("benchmark.json")),
- None
- )
- if benchmark_report:
- benchmark_data = await self.client.get_json(session, benchmark_report["url"])
- data_point.update({
- "tokens_per_second": benchmark_data["tokens_per_second"],
- "time_to_first_token": benchmark_data.get("time_to_first_token", 0)
- })
- self.logger.info(
- f"Processed benchmark data for pipeline {pipeline['id']}: "
- f"commit {commit_info['commit_hash'][:7]}, "
- f"tokens/s {benchmark_data['tokens_per_second']:.2f}"
- )
-
- # Process size data if available
- if size_job:
- size_artifacts = await self.client.get_artifacts(session, size_job["job_number"])
- size_report = next(
- (a for a in size_artifacts if a["path"].endswith("pip-sizes.json")),
- None
- )
- if size_report:
- size_data = await self.client.get_json(session, size_report["url"])
- data_point.update({
- "total_size_mb": size_data["total_size_mb"],
- "packages": size_data["packages"]
- })
- self.logger.info(
- f"Processed size data for pipeline {pipeline['id']}: "
- f"commit {commit_info['commit_hash'][:7]}, "
- f"size {size_data['total_size_mb']:.2f}MB"
- )
-
- # Process linecount data if available
- if linecount_job:
- linecount_artifacts = await self.client.get_artifacts(session, linecount_job["job_number"])
- linecount_report = next(
- (a for a in linecount_artifacts if a["path"].endswith("line-count-snapshot.json")),
- None
- )
- if linecount_report:
- linecount_data = await self.client.get_json(session, linecount_report["url"])
- data_point.update({
- "total_lines": linecount_data["total_lines"],
- "total_files": linecount_data["total_files"],
- "files": linecount_data["files"]
- })
- self.logger.info(
- f"Processed line count data for pipeline {pipeline['id']}: "
- f"commit {commit_info['commit_hash'][:7]}, "
- f"lines {linecount_data['total_lines']:,}"
- )
-
- return data_point
-
- except Exception as e:
- self.logger.error(f"Error processing pipeline {pipeline['id']}: {str(e)}")
- return None
-
- async def process_pipeline_batch(
- self,
- session: aiohttp.ClientSession,
- pipelines: List[Dict],
- batch_size: int = 5
- ) -> List[Dict]:
- """
- Process a batch of pipelines with rate limiting.
-
- Args:
- session: aiohttp client session
- pipelines: List of pipelines to process
- batch_size: Number of pipelines to process in parallel
-
- Returns:
- List of processed pipeline data points
- """
- data_points = []
-
- for i in range(0, len(pipelines), batch_size):
- batch = pipelines[i:i + batch_size]
-
- # Process batch in parallel
- tasks = [self.process_pipeline(session, pipeline) for pipeline in batch]
- batch_results = await asyncio.gather(*tasks)
-
- # Filter out None results
- batch_data = [r for r in batch_results if r is not None]
- data_points.extend(batch_data)
-
- # Add delay between batches if there are more to process
- if i + batch_size < len(pipelines):
- await asyncio.sleep(1) # 1 second delay between batches
-
- return data_points
-
- async def collect_data(self) -> List[Dict]:
- self.logger.info("Starting data collection...")
- async with aiohttp.ClientSession(headers=self.client.headers) as session:
- # Get pipelines from main branch
- main_pipelines = await self.client.get_recent_pipelines(
- session,
- org_slug=self.client.project_slug,
- limit=20,
- branch="main"
- )
-
- # Add delay between branch requests
- await asyncio.sleep(2)
-
- # Get pipelines from circleci branch
- circleci_pipelines = await self.client.get_recent_pipelines(
- session,
- org_slug=self.client.project_slug,
- limit=20,
- branch="circleci"
- )
-
- # Combine pipelines and sort by created_at date
- pipelines = main_pipelines + circleci_pipelines
- pipelines.sort(
- key=lambda x: datetime.fromisoformat(
- x.get("created_at", x.get("updated_at")).replace('Z', '+00:00')
- ),
- reverse=True # Most recent first
- )
-
- self.logger.info(f"Found {len(pipelines)} recent pipelines")
-
- # Process pipelines in batches
- data_points = await self.process_pipeline_batch(session, pipelines)
-
- # Sort by timestamp
- data_points.sort(
- key=lambda x: datetime.fromisoformat(
- x.get("timestamp").replace('Z', '+00:00')
- ),
- reverse=True # Most recent first
- )
-
- return data_points
-
- def generate_report(self, data: List[Dict], output_dir: str = "reports") -> Optional[str]:
- self.logger.info("Generating report...")
- if not data:
- self.logger.error("No data to generate report from!")
- return None
-
- # Get latest pipeline status based on errors
- latest_main_pipeline = next((d for d in data if d.get('branch') == 'main'), None)
- latest_pipeline_status = 'success' if latest_main_pipeline and not latest_main_pipeline.get('errors') else 'failure'
-
- # Log the pipeline status
- if latest_main_pipeline:
- self.logger.info(
- f"Latest main branch pipeline status: {latest_pipeline_status} "
- f"(commit: {latest_main_pipeline['commit_hash'][:7]})"
- )
- else:
- self.logger.warning("No pipeline data found for main branch")
-
- # Convert output_dir to Path object
- output_dir = Path(output_dir)
-
- # Create output directory if it doesn't exist
- output_dir.mkdir(parents=True, exist_ok=True)
-
- # Create separate dataframes for each metric
- df_size = pd.DataFrame([d for d in data if 'total_size_mb' in d])
- df_lines = pd.DataFrame([d for d in data if 'total_lines' in d])
- df_benchmark = pd.DataFrame([d for d in data if 'tokens_per_second' in d])
-
- # Create a single figure with subplots
- fig = make_subplots(
- rows=3, cols=2,
- subplot_titles=('', 'Package Size', '', 'Line Count', '', 'Tokens per Second'),
- vertical_spacing=0.2,
- column_widths=[0.2, 0.8],
- specs=[[{"type": "indicator"}, {"type": "scatter"}],
- [None, {"type": "scatter"}],
- [None, {"type": "scatter"}]]
- )
-
- # Add package size trace if we have data
- if not df_size.empty:
- df_size['timestamp'] = pd.to_datetime(df_size['timestamp'])
- df_size = df_size.sort_values('timestamp')
-
- fig.add_trace(
- go.Scatter(
- x=df_size['timestamp'],
- y=df_size['total_size_mb'],
- mode='lines+markers',
- name='Package Size',
- customdata=df_size[['commit_hash', 'commit_url']].values,
- hovertemplate="<br>".join([
- "Size: %{y:.2f}MB",
- "Date: %{x}",
- "Commit: %{customdata[0]}",
- "<extra></extra>"
- ])
- ),
- row=1, col=2
- )
- fig.update_yaxes(title_text="Size (MB)", row=1, col=2)
-
- # Add line count trace if we have data
- if not df_lines.empty:
- df_lines['timestamp'] = pd.to_datetime(df_lines['timestamp'])
- df_lines = df_lines.sort_values('timestamp')
-
- fig.add_trace(
- go.Scatter(
- x=df_lines['timestamp'],
- y=df_lines['total_lines'],
- mode='lines+markers',
- name='Line Count',
- customdata=df_lines[['commit_hash', 'commit_url']].values,
- hovertemplate="<br>".join([
- "Lines: %{y:,.0f}",
- "Date: %{x}",
- "Commit: %{customdata[0]}",
- "<extra></extra>"
- ])
- ),
- row=2, col=2
- )
- fig.update_yaxes(title_text="Total Lines", row=2, col=2)
-
- # Add tokens per second trace if we have data
- if not df_benchmark.empty:
- df_benchmark['timestamp'] = pd.to_datetime(df_benchmark['timestamp'])
- df_benchmark = df_benchmark.sort_values('timestamp')
-
- fig.add_trace(
- go.Scatter(
- x=df_benchmark['timestamp'],
- y=df_benchmark['tokens_per_second'],
- mode='lines+markers',
- name='Tokens/Second',
- customdata=df_benchmark[['commit_hash', 'commit_url']].values,
- hovertemplate="<br>".join([
- "Tokens/s: %{y:.2f}",
- "Date: %{x}",
- "Commit: %{customdata[0]}",
- "<extra></extra>"
- ])
- ),
- row=3, col=2
- )
- fig.update_yaxes(title_text="Tokens per Second", row=3, col=2)
-
- # Update layout
- fig.update_layout(
- height=800,
- showlegend=False,
- title_text="Package Metrics Dashboard",
- title_x=0.5,
- plot_bgcolor='white',
- paper_bgcolor='white',
- font=dict(size=12),
- hovermode='x unified'
- )
-
- # Update the dashboard HTML with date range picker
- dashboard_html = f"""
- <html>
- <head>
- <title>Package Metrics Dashboard</title>
- <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
- <style>
- body {{
- background-color: #f5f6fa;
- margin: 0;
- padding: 20px;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- }}
-
- .date-picker-container {{
- background: white;
- padding: 15px;
- border-radius: 12px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- margin: 20px auto;
- width: fit-content;
- }}
-
- #daterange {{
- padding: 8px 12px;
- border: 1px solid #ddd;
- border-radius: 8px;
- font-size: 14px;
- width: 300px;
- cursor: pointer;
- }}
-
- .quick-ranges {{
- margin-top: 10px;
- display: flex;
- gap: 8px;
- justify-content: center;
- }}
-
- .quick-ranges button {{
- padding: 8px 16px;
- border: 1px solid #e1e4e8;
- border-radius: 8px;
- background: white;
- cursor: pointer;
- font-size: 13px;
- transition: all 0.2s ease;
- }}
-
- .quick-ranges button:hover {{
- background: #f0f0f0;
- transform: translateY(-1px);
- }}
-
- .dashboard-grid {{
- display: grid;
- grid-template-columns: 300px 1fr;
- gap: 20px;
- margin-top: 20px;
- }}
-
- .chart-container {{
- background: white;
- border-radius: 12px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- padding: 20px;
- height: 350px;
- }}
-
- .chart-row {{
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 20px;
- }}
-
- .chart-row-full {{
- grid-column: 2 / -1;
- }}
-
- .chart-box {{
- background: white;
- border-radius: 12px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- padding: 20px;
- display: flex;
- flex-direction: column;
- }}
-
- .chart-title {{
- font-size: 16px;
- font-weight: 600;
- color: #2c3e50;
- margin-bottom: 15px;
- padding-bottom: 10px;
- border-bottom: 1px solid #eee;
- }}
-
- .status-container {{
- background: white;
- border-radius: 12px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- padding: 20px;
- height: 350px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- }}
-
- .traffic-light {{
- width: 150px;
- height: 150px;
- border-radius: 50%;
- margin: 20px;
- box-shadow: 0 0 20px rgba(0,0,0,0.2);
- position: relative;
- }}
-
- .traffic-light.success {{
- background: #2ecc71; /* Bright green */
- border: 8px solid #27ae60; /* Darker green border */
- }}
-
- .traffic-light.failure {{
- background: #e74c3c; /* Bright red */
- border: 8px solid #c0392b; /* Darker red border */
- }}
-
- .status-text {{
- font-size: 24px;
- font-weight: bold;
- margin-top: 20px;
- color: #2c3e50;
- }}
-
- /* Override Plotly's default margins */
- .js-plotly-plot .plotly {{
- margin: 0 !important;
- }}
- </style>
- </head>
- <body>
- <div class="date-picker-container">
- <input type="text" id="daterange" />
- <div class="quick-ranges">
- <button onclick="setQuickRange('1h')">Last Hour</button>
- <button onclick="setQuickRange('6h')">Last 6 Hours</button>
- <button onclick="setQuickRange('1d')">Last 24 Hours</button>
- <button onclick="setQuickRange('7d')">Last 7 Days</button>
- <button onclick="setQuickRange('30d')">Last 30 Days</button>
- <button onclick="setQuickRange('all')">All Time</button>
- </div>
- </div>
-
- <div class="dashboard-grid">
- <div class="status-container">
- <div class="chart-title">Pipeline Status</div>
- <div class="traffic-light {'success' if latest_pipeline_status == 'success' else 'failure'}"></div>
- <div class="status-text">
- {'✓ Pipeline Passing' if latest_pipeline_status == 'success' else '✗ Pipeline Failing'}
- </div>
- </div>
- <div class="chart-row">
- <div class="chart-box">
- <div class="chart-title">Package Size</div>
- <div id="size-chart"></div>
- </div>
- <div class="chart-box">
- <div class="chart-title">Line Count</div>
- <div id="lines-chart"></div>
- </div>
- </div>
- <div class="chart-row chart-row-full">
- <div class="chart-box">
- <div class="chart-title">Tokens per Second</div>
- <div id="tokens-chart"></div>
- </div>
- </div>
- </div>
-
- <script type="text/javascript" src="https://cdn.jsdelivr.net/jquery/latest/jquery.min.js"></script>
- <script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
- <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
- <script>
- let globalMinDate = null;
- let globalMaxDate = null;
-
- // Split the original figure into separate charts
- const originalData = {fig.to_json()};
-
- function initializeCharts() {{
- // Create the size trend chart
- const sizeTrace = originalData.data.find(trace => trace.name === 'Package Size');
- if (sizeTrace) {{
- Plotly.newPlot('size-chart',
- [sizeTrace],
- {{
- showlegend: false,
- height: 280,
- margin: {{ t: 10, b: 40, l: 50, r: 20 }},
- yaxis: {{ title: 'Size (MB)' }},
- xaxis: {{
- type: 'date',
- title: null,
- range: [sizeTrace.x[0], sizeTrace.x[sizeTrace.x.length - 1]]
- }}
- }}
- );
- }}
-
- // Create the line count chart
- const lineTrace = originalData.data.find(trace => trace.name === 'Line Count');
- if (lineTrace) {{
- Plotly.newPlot('lines-chart',
- [lineTrace],
- {{
- showlegend: false,
- height: 280,
- margin: {{ t: 10, b: 40, l: 50, r: 20 }},
- yaxis: {{ title: 'Total Lines' }},
- xaxis: {{
- type: 'date',
- title: null,
- range: [lineTrace.x[0], lineTrace.x[lineTrace.x.length - 1]]
- }}
- }}
- );
- }}
-
- // Create the tokens per second chart
- const tokensTrace = originalData.data.find(trace => trace.name === 'Tokens/Second');
- if (tokensTrace) {{
- Plotly.newPlot('tokens-chart',
- [tokensTrace],
- {{
- showlegend: false,
- height: 280,
- margin: {{ t: 10, b: 40, l: 50, r: 20 }},
- yaxis: {{ title: 'Tokens/Second' }},
- xaxis: {{
- type: 'date',
- title: null,
- range: [tokensTrace.x[0], tokensTrace.x[tokensTrace.x.length - 1]]
- }}
- }}
- );
- }}
-
- // Add debug logs to check axis names
- console.log('Size Chart Layout:', document.getElementById('size-chart').layout);
- console.log('Lines Chart Layout:', document.getElementById('lines-chart').layout);
- console.log('Tokens Chart Layout:', document.getElementById('tokens-chart').layout);
- }}
-
- function setQuickRange(range) {{
- let start, end = moment();
-
- switch(range) {{
- case '1h':
- start = moment().subtract(1, 'hours');
- break;
- case '6h':
- start = moment().subtract(6, 'hours');
- break;
- case '1d':
- start = moment().subtract(1, 'days');
- break;
- case '7d':
- start = moment().subtract(7, 'days');
- break;
- case '30d':
- start = moment().subtract(30, 'days');
- break;
- case 'all':
- start = moment(globalMinDate);
- end = moment(globalMaxDate);
- break;
- }}
-
- $('#daterange').data('daterangepicker').setStartDate(start);
- $('#daterange').data('daterangepicker').setEndDate(end);
- updatePlotRange(start.toISOString(), end.toISOString());
- }}
-
- function updatePlotRange(startDate, endDate) {{
- console.log('Updating range:', startDate, endDate);
-
- // Get the actual x-axis names from the chart layouts
- const sizeChartLayout = document.getElementById('size-chart').layout;
- const sizeXAxisName = Object.keys(sizeChartLayout).find(key => key.startsWith('xaxis'));
-
- const linesChartLayout = document.getElementById('lines-chart').layout;
- const linesXAxisName = Object.keys(linesChartLayout).find(key => key.startsWith('xaxis'));
-
- const tokensChartLayout = document.getElementById('tokens-chart').layout;
- const tokensXAxisName = Object.keys(tokensChartLayout).find(key => key.startsWith('xaxis'));
-
- // Update the ranges
- const sizeUpdateLayout = {{}};
- sizeUpdateLayout[`{{sizeXAxisName}}.range`] = [startDate, endDate];
-
- const linesUpdateLayout = {{}};
- linesUpdateLayout[`{{linesXAxisName}}.range`] = [startDate, endDate];
-
- const tokensUpdateLayout = {{}};
- tokensUpdateLayout[`{{tokensXAxisName}}.range`] = [startDate, endDate];
-
- // Update both charts
- Plotly.relayout('size-chart', sizeUpdateLayout)
- .catch(err => console.error('Error updating size chart:', err));
-
- Plotly.relayout('lines-chart', linesUpdateLayout)
- .catch(err => console.error('Error updating lines chart:', err));
-
- Plotly.relayout('tokens-chart', tokensUpdateLayout)
- .catch(err => console.error('Error updating tokens chart:', err));
- }}
-
- function findDateRange(data) {{
- let minDate = null;
- let maxDate = null;
-
- data.forEach(trace => {{
- if (trace.x && trace.x.length > 0) {{
- const dates = trace.x.map(d => new Date(d));
- const traceMin = new Date(Math.min(...dates));
- const traceMax = new Date(Math.max(...dates));
-
- if (!minDate || traceMin < minDate) minDate = traceMin;
- if (!maxDate || traceMax > maxDate) maxDate = traceMax;
- }}
- }});
-
- return {{ minDate, maxDate }};
- }}
-
- // Initialize everything when document is ready
- $(document).ready(function() {{
- // Initialize charts
- initializeCharts();
-
- // Find date range from data
- const {{ minDate, maxDate }} = findDateRange(originalData.data);
- globalMinDate = minDate;
- globalMaxDate = maxDate;
-
- // Initialize daterangepicker
- $('#daterange').daterangepicker({{
- startDate: minDate,
- endDate: maxDate,
- minDate: minDate,
- maxDate: maxDate,
- timePicker: true,
- timePicker24Hour: true,
- timePickerIncrement: 1,
- opens: 'center',
- locale: {{
- format: 'YYYY-MM-DD HH:mm',
- applyLabel: "Apply",
- cancelLabel: "Cancel",
- customRangeLabel: "Custom Range"
- }},
- ranges: {{
- 'Last Hour': [moment().subtract(1, 'hours'), moment()],
- 'Last 6 Hours': [moment().subtract(6, 'hours'), moment()],
- 'Last 24 Hours': [moment().subtract(1, 'days'), moment()],
- 'Last 7 Days': [moment().subtract(7, 'days'), moment()],
- 'Last 30 Days': [moment().subtract(30, 'days'), moment()],
- 'All Time': [moment(minDate), moment(maxDate)]
- }}
- }});
-
- // Update plots when date range changes
- $('#daterange').on('apply.daterangepicker', function(ev, picker) {{
- console.log('Date range changed:', picker.startDate.toISOString(), picker.endDate.toISOString());
- updatePlotRange(picker.startDate.toISOString(), picker.endDate.toISOString());
- }});
-
- // Add click handlers for charts
- ['size-chart', 'lines-chart', 'tokens-chart'].forEach(chartId => {{
- const chart = document.getElementById(chartId);
- if (chart) {{
- chart.on('plotly_click', function(data) {{
- const point = data.points[0];
- if (point.customdata && point.customdata[1]) {{
- window.open(point.customdata[1], '_blank');
- }}
- }});
- }}
- }});
-
- // Add debug logging for chart initialization
- console.log('Size Chart:', document.getElementById('size-chart'));
- console.log('Lines Chart:', document.getElementById('lines-chart'));
- console.log('Tokens Chart:', document.getElementById('tokens-chart'));
- }});
- </script>
- </body>
- </html>
- """
-
- # Write the dashboard
- dashboard_path = output_dir / "dashboard.html"
- with open(dashboard_path, "w") as f:
- f.write(dashboard_html)
-
- # Generate summary with available metrics
- latest_data = {}
-
- if not df_size.empty:
- latest = df_size.iloc[-1]
- previous = df_size.iloc[-2] if len(df_size) > 1 else latest
- size_change = float(latest['total_size_mb'] - previous['total_size_mb'])
- latest_data.update({
- 'timestamp': latest['timestamp'].isoformat(),
- 'commit_hash': latest['commit_hash'],
- 'commit_url': latest['commit_url'],
- 'total_size_mb': float(latest['total_size_mb']),
- 'size_change_mb': size_change,
- 'packages': latest.get('packages', [])
- })
-
- if not df_lines.empty:
- latest = df_lines.iloc[-1]
- previous = df_lines.iloc[-2] if len(df_lines) > 1 else latest
- linecount_change = int(latest['total_lines'] - previous['total_lines'])
- if not latest_data: # Only add timestamp and commit info if not already added
- latest_data.update({
- 'timestamp': latest['timestamp'].isoformat(),
- 'commit_hash': latest['commit_hash'],
- 'commit_url': latest['commit_url'],
- })
- latest_data.update({
- 'total_lines': int(latest['total_lines']),
- 'linecount_change': linecount_change
- })
-
- if not df_benchmark.empty:
- latest = df_benchmark.iloc[-1]
- previous = df_benchmark.iloc[-2] if len(df_benchmark) > 1 else latest
- tokens_change = float(latest['tokens_per_second'] - previous['tokens_per_second'])
- if not latest_data: # Only add timestamp and commit info if not already added
- latest_data.update({
- 'timestamp': latest['timestamp'].isoformat(),
- 'commit_hash': latest['commit_hash'],
- 'commit_url': latest['commit_url'],
- })
- latest_data.update({
- 'tokens_per_second': float(latest['tokens_per_second']),
- 'tokens_change': tokens_change
- })
-
- if latest_data:
- with open(output_dir / 'latest_data.json', 'w') as f:
- json.dump(latest_data, f, indent=2)
-
- self._print_summary(latest_data)
- self.logger.info(f"Report generated in {output_dir}")
- return str(output_dir)
-
- return None
-
- def _print_summary(self, latest_data: Dict):
- print("\n=== Package Size Summary ===")
- print(f"Timestamp: {latest_data['timestamp']}")
- print(f"Commit: {latest_data['commit_hash'][:7]}")
-
- if 'total_size_mb' in latest_data:
- print(f"Total Size: {latest_data['total_size_mb']:.2f}MB")
- change = latest_data['size_change_mb']
- change_symbol = "↓" if change <= 0 else "↑"
- print(f"Change: {change_symbol} {abs(change):.2f}MB")
-
- if latest_data.get('packages'):
- print("\nTop 5 Largest Packages:")
- sorted_packages = sorted(latest_data['packages'], key=lambda x: x['size_mb'], reverse=True)
- for pkg in sorted_packages[:5]:
- print(f"- {pkg['name']}: {pkg['size_mb']:.2f}MB")
-
- if 'total_lines' in latest_data:
- print("\nLine Count Stats:")
- print(f"Total Lines: {latest_data['total_lines']:,}")
- change = latest_data['linecount_change']
- change_symbol = "↓" if change <= 0 else "↑"
- print(f"Change: {change_symbol} {abs(change):,}")
-
- if 'tokens_per_second' in latest_data:
- print("\nBenchmark Stats:")
- print(f"Tokens per Second: {latest_data['tokens_per_second']:.2f}")
- if 'time_to_first_token' in latest_data:
- print(f"Time to First Token: {latest_data['time_to_first_token']:.3f}s")
-
- print("\n")
-
- def _calculate_data_hash(self, data: List[Dict]) -> str:
- """Calculate a hash of the data to detect changes"""
- return hash(str(sorted([
- (d.get('commit_hash'), d.get('timestamp'))
- for d in data
- ])))
-
- def _play_sound(self, sound_key: str):
- """Play a specific notification sound using pygame"""
- try:
- sound_path = self.sounds.get(sound_key)
- if sound_path and sound_path.exists():
- sound = pygame.mixer.Sound(str(sound_path))
- sound.play()
- # Wait for the sound to finish playing
- pygame.time.wait(int(sound.get_length() * 1000))
- else:
- self.logger.warning(f"Sound file not found: {sound_key} at {sound_path}")
- except Exception as e:
- self.logger.error(f"Failed to play sound {sound_key}: {e}")
-
- def _check_metrics_changes(self, current_data: List[Dict], previous_data: List[Dict]):
- # Sort data by timestamp in descending order (most recent first)
- def sort_by_timestamp(data):
- return sorted(
- data,
- key=lambda x: x.get('timestamp', ''),
- reverse=True # Most recent first
- )
-
- current_data = sort_by_timestamp(current_data)
- previous_data = sort_by_timestamp(previous_data)
-
- # Helper to find latest entry with a specific metric
- def find_latest_with_metric(data: List[Dict], metric: str) -> Optional[Dict]:
- return next((d for d in data if metric in d), None)
-
- # Check line count changes
- current_lines = find_latest_with_metric(current_data, 'total_lines')
- previous_lines = find_latest_with_metric(previous_data, 'total_lines')
-
- if current_lines and previous_lines:
- diff = current_lines['total_lines'] - previous_lines['total_lines']
- self.logger.debug(f"Lines of code diff: {diff}")
- if diff > 0:
- self.logger.info(f"Lines of code increased by {diff:,}")
- self._play_sound('lines_up')
- elif diff < 0:
- self.logger.info(f"Lines of code decreased by {abs(diff):,}")
- self._play_sound('lines_down')
- else:
- self.logger.debug("No lines of code data found")
-
- # Check tokens per second changes
- current_tokens = find_latest_with_metric(current_data, 'tokens_per_second')
- previous_tokens = find_latest_with_metric(previous_data, 'tokens_per_second')
-
- if current_tokens and previous_tokens:
- diff = current_tokens['tokens_per_second'] - previous_tokens['tokens_per_second']
- self.logger.debug(f"Tokens per second diff: {diff}")
- if diff > 0:
- self.logger.info(f"Tokens per second increased by {diff:.2f}")
- self._play_sound('tokens_up')
- elif diff < 0:
- self.logger.info(f"Tokens per second decreased by {abs(diff):.2f}")
- self._play_sound('tokens_down')
- else:
- self.logger.debug("No tokens per second data found")
-
- # Check package size changes
- current_size = find_latest_with_metric(current_data, 'total_size_mb')
- previous_size = find_latest_with_metric(previous_data, 'total_size_mb')
-
- if current_size and previous_size:
- diff = current_size['total_size_mb'] - previous_size['total_size_mb']
- self.logger.debug(f"Package size diff: {diff:.2f}MB")
- if diff > 0:
- self.logger.info(f"Package size increased by {diff:.2f}MB")
- self._play_sound('size_up')
- elif diff < 0:
- self.logger.info(f"Package size decreased by {abs(diff):.2f}MB")
- self._play_sound('size_down')
- else:
- self.logger.debug("No package size data found")
-
- async def run_dashboard(self, update_interval: int = 10):
- """Run the dashboard with periodic updates"""
- try:
- update_interval = float(update_interval)
- self.logger.debug(f"Update interval type: {type(update_interval)}, value: {update_interval}")
- except ValueError as e:
- self.logger.error(f"Failed to convert update_interval to float: {update_interval}")
- raise
-
- self.logger.info(f"Starting real-time dashboard with {update_interval}s updates")
- previous_data = None
-
- while True:
- try:
- start_time = time.time()
-
- # Collect new data
- current_data = await self.collect_data()
- if not current_data:
- self.logger.warning("No data collected")
- await asyncio.sleep(update_interval)
- continue
-
- # Generate report
- report_path = self.generate_report(current_data)
- if report_path:
- self.logger.info(
- f"Dashboard updated at {datetime.now().strftime('%H:%M:%S')}"
- )
-
- print("Curr:", len(current_data))
- print("Prev:", len(previous_data) if previous_data else "None")
- if previous_data:
- # Check for metric changes and play appropriate sounds
- self.logger.debug(f"Checking metrics changes between {len(current_data)} current and {len(previous_data)} previous data points")
- self._check_metrics_changes(current_data, previous_data)
-
- # Update previous data
- previous_data = current_data.copy() # Make a copy to prevent reference issues
-
- # Calculate sleep time
- elapsed = float(time.time() - start_time)
- sleep_time = max(0.0, update_interval - elapsed)
- await asyncio.sleep(sleep_time)
-
- except Exception as e:
- self.logger.error(f"Error in dashboard update loop: {e}", exc_info=True)
- if self.debug:
- raise
- await asyncio.sleep(update_interval)
-
-async def main():
- token = os.getenv("CIRCLECI_TOKEN")
- project_slug = os.getenv("CIRCLECI_PROJECT_SLUG")
- debug = os.getenv("DEBUG", "").lower() in ("true", "1", "yes")
-
- try:
- # Get update interval from environment or use default
- update_interval = float(os.getenv("UPDATE_INTERVAL", "10"))
- print(f"Update interval type: {type(update_interval)}, value: {update_interval}") # Debug print
- except ValueError as e:
- print(f"Error converting UPDATE_INTERVAL to float: {os.getenv('UPDATE_INTERVAL')}")
- update_interval = 10.0
-
- if not token or not project_slug:
- print("Error: Please set CIRCLECI_TOKEN and CIRCLECI_PROJECT_SLUG environment variables")
- return
-
- tracker = PackageSizeTracker(token, project_slug, debug)
-
- try:
- await tracker.run_dashboard(update_interval)
- except KeyboardInterrupt:
- print("\nDashboard stopped by user")
- except Exception as e:
- logging.error(f"Error: {str(e)}", exc_info=True)
- if debug:
- raise
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/extra/dashboard/requirements.txt b/extra/dashboard/requirements.txt
deleted file mode 100644
index 7b978bc0..00000000
--- a/extra/dashboard/requirements.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-plotly
-pandas
-requests
-aiohttp
-pygame
\ No newline at end of file
diff --git a/extra/dashboard/sounds/gta5_wasted.mp3 b/extra/dashboard/sounds/gta5_wasted.mp3
deleted file mode 100644
index 2723ff8a..00000000
--- a/extra/dashboard/sounds/gta5_wasted.mp3
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:fb3fb66dd02827fbff86ef1ce3bc6438371c823aed7d4c3803ed522f008e4947
-size 206399
diff --git a/extra/dashboard/sounds/pokemon_evolve.mp3 b/extra/dashboard/sounds/pokemon_evolve.mp3
deleted file mode 100644
index 35b01432..00000000
--- a/extra/dashboard/sounds/pokemon_evolve.mp3
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:d99cc9bdab4a4639d50f439b424547000e7c79f195b5b121734ad4ead435911c
-size 633345
diff --git a/extra/line_counter.py b/extra/line_counter.py
deleted file mode 100644
index 01e7ee20..00000000
--- a/extra/line_counter.py
+++ /dev/null
@@ -1,210 +0,0 @@
-#!/usr/bin/env python3
-import os
-import sys
-import json
-import token
-import tokenize
-from datetime import datetime, timezone
-
-TOKEN_WHITELIST = [token.OP, token.NAME, token.NUMBER, token.STRING]
-
-def is_docstring(t):
- return t.type == token.STRING and t.string.startswith('"""') and t.line.strip().startswith('"""')
-
-def gen_stats(base_path="."):
- table = []
- exo_path = os.path.join(base_path, "exo")
- if not os.path.exists(exo_path):
- print(f"Warning: {exo_path} directory not found")
- return table
-
- for path, _, files in os.walk(exo_path):
- for name in files:
- if not name.endswith(".py"):
- continue
-
- filepath = os.path.join(path, name)
- relfilepath = os.path.relpath(filepath, base_path).replace('\\', '/')
-
- try:
- with tokenize.open(filepath) as file_:
- tokens = [t for t in tokenize.generate_tokens(file_.readline)
- if t.type in TOKEN_WHITELIST and not is_docstring(t)]
- token_count = len(tokens)
- line_count = len(set([x for t in tokens
- for x in range(t.start[0], t.end[0]+1)]))
- if line_count > 0:
- table.append([relfilepath, line_count, token_count/line_count])
- except Exception as e:
- print(f"Error processing {filepath}: {e}")
- continue
-
- return table
-
-def gen_diff(table_old, table_new):
- table = []
- files_new = set([x[0] for x in table_new])
- files_old = set([x[0] for x in table_old])
-
- added = files_new - files_old
- deleted = files_old - files_new
- unchanged = files_new & files_old
-
- for file in added:
- file_stat = [stats for stats in table_new if file in stats][0]
- table.append([file_stat[0], file_stat[1], file_stat[1], file_stat[2], file_stat[2]])
-
- for file in deleted:
- file_stat = [stats for stats in table_old if file in stats][0]
- table.append([file_stat[0], 0, -file_stat[1], 0, -file_stat[2]])
-
- for file in unchanged:
- file_stat_old = [stats for stats in table_old if file in stats][0]
- file_stat_new = [stats for stats in table_new if file in stats][0]
- if file_stat_new[1] != file_stat_old[1] or file_stat_new[2] != file_stat_old[2]:
- table.append([
- file_stat_new[0],
- file_stat_new[1],
- file_stat_new[1] - file_stat_old[1],
- file_stat_new[2],
- file_stat_new[2] - file_stat_old[2]
- ])
-
- return table
-
-def create_json_report(table, is_diff=False):
- timestamp = datetime.now(timezone.utc).isoformat()
- commit_sha = os.environ.get('GITHUB_SHA', 'unknown')
- branch = os.environ.get('GITHUB_REF_NAME', 'unknown')
- pr_number = os.environ.get('GITHUB_EVENT_NUMBER', '')
-
- if is_diff:
- files = [{
- 'name': row[0],
- 'current_lines': row[1],
- 'line_diff': row[2],
- 'current_tokens_per_line': row[3],
- 'tokens_per_line_diff': row[4]
- } for row in table]
-
- report = {
- 'type': 'diff',
- 'timestamp': timestamp,
- 'commit_sha': commit_sha,
- 'branch': branch,
- 'pr_number': pr_number,
- 'files': files,
- 'total_line_changes': sum(row[2] for row in table),
- 'total_files_changed': len(files)
- }
- else:
- files = [{
- 'name': row[0],
- 'lines': row[1],
- 'tokens_per_line': row[2]
- } for row in table]
-
- report = {
- 'type': 'snapshot',
- 'timestamp': timestamp,
- 'commit_sha': commit_sha,
- 'branch': branch,
- 'files': files,
- 'total_lines': sum(row[1] for row in table),
- 'total_files': len(files)
- }
-
- return report
-
-def display_diff(diff):
- return "+" + str(diff) if diff > 0 else str(diff)
-
-def format_table(rows, headers, floatfmt):
- if not rows:
- return ""
-
- # Add headers as first row
- all_rows = [headers] + rows
-
- # Calculate column widths
- col_widths = []
- for col in range(len(headers)):
- col_width = max(len(str(row[col])) for row in all_rows)
- col_widths.append(col_width)
-
- # Format rows
- output = []
- for row_idx, row in enumerate(all_rows):
- formatted_cols = []
- for col_idx, (value, width) in enumerate(zip(row, col_widths)):
- if isinstance(value, float):
- # Handle float formatting based on floatfmt
- fmt = floatfmt[col_idx]
- if fmt.startswith('+'):
- value = f"{value:+.1f}"
- else:
- value = f"{value:.1f}"
- elif isinstance(value, int) and col_idx > 0: # Skip filename column
- # Handle integer formatting based on floatfmt
- fmt = floatfmt[col_idx]
- if fmt.startswith('+'):
- value = f"{value:+d}"
- else:
- value = f"{value:d}"
- formatted_cols.append(str(value).ljust(width))
- output.append(" ".join(formatted_cols))
-
- # Add separator line after headers
- if row_idx == 0:
- separator = []
- for width in col_widths:
- separator.append("-" * width)
- output.append(" ".join(separator))
-
- return "\n".join(output)
-
-if __name__ == "__main__":
- if len(sys.argv) == 3:
- # Comparing two directories
- headers = ["File", "Lines", "Diff", "Tokens/Line", "Diff"]
- table = gen_diff(gen_stats(sys.argv[1]), gen_stats(sys.argv[2]))
-
- if table:
- # Print table output
- print("### Code Changes in 'exo' Directory")
- print("```")
- print(format_table(
- sorted(table, key=lambda x: abs(x[2]) if len(x) > 2 else 0, reverse=True),
- headers,
- (".1f", "d", "+d", ".1f", "+.1f")
- ))
- total_changes = sum(row[2] for row in table)
- print(f"\nTotal line changes: {display_diff(total_changes)}")
- print("```")
-
- # Generate JSON report
- report = create_json_report(table, is_diff=True)
- with open('line-count-diff.json', 'w') as f:
- json.dump(report, f, indent=2)
- else:
- # Single directory analysis
- headers = ["File", "Lines", "Tokens/Line"]
- table = gen_stats(sys.argv[1] if len(sys.argv) > 1 else ".")
-
- if table:
- # Print table output
- print("### Code Statistics for 'exo' Directory")
- print("```")
- print(format_table(
- sorted(table, key=lambda x: x[1], reverse=True),
- headers,
- (".1f", "d", ".1f")
- ))
- total_lines = sum(row[1] for row in table)
- print(f"\nTotal lines: {total_lines}")
- print("```")
-
- # Generate JSON report
- report = create_json_report(table, is_diff=False)
- with open('line-count-snapshot.json', 'w') as f:
- json.dump(report, f, indent=2)
diff --git a/extra/pipsize.py b/extra/pipsize.py
deleted file mode 100644
index 406d177c..00000000
--- a/extra/pipsize.py
+++ /dev/null
@@ -1,113 +0,0 @@
-import os
-import importlib.metadata
-import importlib.util
-import json
-import sys
-
-
-def calc_container(path):
- """Calculate total size of a directory or file."""
- if os.path.isfile(path):
- try:
- return os.path.getsize(path)
- except (OSError, FileNotFoundError):
- return 0
-
- total_size = 0
- for dirpath, dirnames, filenames in os.walk(path):
- for f in filenames:
- fp = os.path.join(dirpath, f)
- try:
- total_size += os.path.getsize(fp)
- except (OSError, FileNotFoundError):
- continue
- return total_size
-
-
-def get_package_location(package_name):
- """Get the actual location of a package's files."""
- try:
- spec = importlib.util.find_spec(package_name)
- if spec is None:
- return None
-
- if spec.submodule_search_locations:
- # Return the first location for namespace packages
- return spec.submodule_search_locations[0]
- elif spec.origin:
- # For single-file modules, return the file path itself
- return spec.origin
- except ImportError:
- return None
-
-
-def get_package_sizes(min_size_mb=0.1):
- """Get sizes of installed packages above minimum size threshold."""
- package_sizes = []
-
- # Get all installed distributions
- for dist in importlib.metadata.distributions():
- try:
- package_name = dist.metadata["Name"]
- location = get_package_location(package_name.replace("-", "_"))
-
- if location and os.path.exists(location):
- size = calc_container(location)
- size_mb = size / (1024 * 1024)
-
- if size_mb > min_size_mb:
- package_sizes.append((package_name, size))
- except Exception as e:
- print(
- f"Error processing {dist.metadata.get('Name', 'Unknown package')}: {e}"
- )
-
- return package_sizes
-
-
-def main():
- # Get and sort package sizes
- package_sizes = get_package_sizes()
- package_sizes.sort(key=lambda x: x[1], reverse=True)
-
- # Convert sizes to MB and prepare data
- table_data = [(name, size/(1024*1024)) for name, size in package_sizes]
- total_size = sum(size for _, size in package_sizes)/(1024*1024)
-
- # Check if --json flag is present
- if "--json" in sys.argv:
- try:
- output_file = sys.argv[sys.argv.index("--json") + 1]
- json_data = {
- "packages": [{
- "name": name,
- "size_mb": round(size, 2)
- } for name, size in table_data],
- "total_size_mb": round(total_size, 2)
- }
-
- with open(output_file, 'w') as f:
- json.dump(json_data, f, indent=2)
- print(f"JSON data written to {output_file}")
- return
- except IndexError:
- print("Error: Please provide a filename after --json")
- sys.exit(1)
- except Exception as e:
- print(f"Error writing JSON file: {e}")
- sys.exit(1)
-
- # Original table output code
- max_name_width = max(len(name) for name, _ in table_data)
- max_name_width = max(max_name_width, len("Package"))
-
- print(f"\n{'Package':<{max_name_width}} | Size (MB)")
- print("-" * max_name_width + "-+-" + "-" * 10)
-
- for name, size in table_data:
- print(f"{name:<{max_name_width}} | {size:>8.2f}")
-
- print(f"\nTotal size: {total_size:.2f} MB\n")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/extra/start_openwebui.sh b/extra/start_openwebui.sh
deleted file mode 100755
index 2cf3d66a..00000000
--- a/extra/start_openwebui.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-API_ENDPOINT="http://${API_ENDPOINT:-$(ifconfig | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | head -n 1):52415}"
-echo "Using API_ENDPOINT=${API_ENDPOINT}"
-docker run -d -p 3000:8080 -e OPENAI_API_BASE_URL="${API_ENDPOINT}" -e OPENAI_API_KEY=your_secret_key -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
diff --git a/format.py b/format.py
deleted file mode 100644
index 68842493..00000000
--- a/format.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env python
-import subprocess
-import sys
-import os
-
-
-def run_yapf(target):
- if os.path.isfile(target):
- files = [target]
- else:
- files = [os.path.join(root, file) for root, _, files in os.walk(target) for file in files if file.endswith('.py')]
-
- for file in files:
- try:
- command = ["yapf", "-i", file]
- subprocess.run(command, check=True, capture_output=True, text=True)
- print(f"Formatted: {file}")
- except subprocess.CalledProcessError as e:
- print(f"Error formatting {file}: {e.stderr}")
-
-
-def main():
- if len(sys.argv) < 2:
- print("Usage: python3 format.py <directory_or_file> e.g. python3 format.py ./exo")
- sys.exit(1)
-
- target = sys.argv[1]
- run_yapf(target)
- print("Formatting completed.")
-
-
-if __name__ == "__main__":
- main()
diff --git a/install.sh b/install.sh
deleted file mode 100755
index a5fffec6..00000000
--- a/install.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env bash
-
-if command -v python3.12 &>/dev/null; then
- echo "Python 3.12 is installed, proceeding with python3.12..."
- python3.12 -m venv .venv
-else
- echo "The recommended version of Python to run exo with is Python 3.12, but $(python3 --version) is installed. Proceeding with $(python3 --version)"
- python3 -m venv .venv
-fi
-source .venv/bin/activate
-pip install -e .
diff --git a/scripts/build_exo.py b/scripts/build_exo.py
deleted file mode 100644
index be3704e1..00000000
--- a/scripts/build_exo.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import site
-import subprocess
-import sys
-import os
-import pkgutil
-
-def run():
- site_packages = site.getsitepackages()[0]
- base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- baseimages_dir = os.path.join(base_dir, "exo", "apputil", "baseimages")
-
- command = [
- f"{sys.executable}", "-m", "nuitka", "exo/main.py",
- "--company-name=exolabs",
- "--product-name=exo",
- "--output-dir=dist",
- "--follow-imports",
- "--standalone",
- "--output-filename=exo",
- "--python-flag=no_site",
- "--onefile",
- f"--include-data-dir={baseimages_dir}=exo/apputil/baseimages"
- ]
-
- if sys.platform == "darwin":
- command.extend([
- "--macos-app-name=exo",
- "--macos-app-mode=gui",
- "--macos-app-version=0.0.1",
- "--macos-signed-app-name=net.exolabs.exo",
- "--include-distribution-meta=mlx",
- "--include-module=mlx._reprlib_fix",
- "--include-module=mlx._os_warning",
- "--include-distribution-meta=huggingface_hub",
- "--include-module=huggingface_hub.repocard",
- f"--include-data-files={site_packages}/mlx/lib/mlx.metallib=mlx/lib/mlx.metallib",
- f"--include-data-files={site_packages}/mlx/lib/mlx.metallib=./mlx.metallib",
- "--include-distribution-meta=pygments",
- "--nofollow-import-to=tinygrad"
- ])
- inference_modules = [
- name for _, name, _ in pkgutil.iter_modules(['exo/inference/mlx/models'])
- ]
- for module in inference_modules:
- command.append(f"--include-module=exo.inference.mlx.models.{module}")
- elif sys.platform == "win32":
- command.extend([
- "--windows-icon-from-ico=docs/exo-logo-win.ico",
- "--file-version=0.0.1",
- "--product-version=0.0.1"
- ])
- elif sys.platform.startswith("linux"):
- command.extend([
- "--include-distribution-metadata=pygments",
- "--linux-icon=docs/exo-rounded.png"
- ])
- try:
- subprocess.run(command, check=True)
- print("Build completed!")
- except subprocess.CalledProcessError as e:
- print(f"An error occurred: {e}")
-
-if __name__ == "__main__":
- run()
diff --git a/scripts/compile_grpc.sh b/scripts/compile_grpc.sh
deleted file mode 100755
index b0333bb5..00000000
--- a/scripts/compile_grpc.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-source ./install.sh
-pushd exo/networking/grpc
-python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. node_service.proto
-sed -i '' "s/import\ node_service_pb2/from . &/" node_service_pb2_grpc.py
-popd
-
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 6f922c43..00000000
--- a/setup.py
+++ /dev/null
@@ -1,89 +0,0 @@
-import sys
-import platform
-import subprocess
-
-from setuptools import find_packages, setup
-
-# Base requirements for all platforms
-install_requires = [
- "aiohttp==3.10.11",
- "aiohttp_cors==0.7.0",
- "aiofiles==24.1.0",
- "grpcio==1.70.0",
- "grpcio-tools==1.70.0",
- "Jinja2==3.1.4",
- "numpy==2.0.0",
- "nuitka==2.5.1",
- "nvidia-ml-py==12.560.30",
- "opencv-python==4.10.0.84",
- "pillow==10.4.0",
- "prometheus-client==0.20.0",
- "protobuf==5.28.1",
- "psutil==6.0.0",
- "pyamdgpuinfo==2.1.6;platform_system=='Linux'",
- "pydantic==2.9.2",
- "requests==2.32.3",
- "rich==13.7.1",
- "scapy==2.6.1",
- "tqdm==4.66.4",
- "transformers==4.46.3",
- "uuid==1.30",
- "uvloop==0.21.0",
- "tinygrad @ git+https://github.com/tinygrad/tinygrad.git@ec120ce6b9ce8e4ff4b5692566a683ef240e8bc8",
-]
-
-extras_require = {
- "formatting": ["yapf==0.40.2",],
- "apple_silicon": [
- "mlx==0.22.0",
- "mlx-lm==0.21.1",
- ],
- "windows": ["pywin32==308",],
- "nvidia-gpu": ["nvidia-ml-py==12.560.30",],
- "amd-gpu": ["pyrsmi==0.2.0"],
-}
-
-# Check if running on macOS with Apple Silicon
-if sys.platform.startswith("darwin") and platform.machine() == "arm64":
- install_requires.extend(extras_require["apple_silicon"])
-
-# Check if running Windows
-if sys.platform.startswith("win32"):
- install_requires.extend(extras_require["windows"])
-
-
-def _add_gpu_requires():
- global install_requires
- # Add Nvidia-GPU
- try:
- out = subprocess.run(['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'], shell=True, text=True, capture_output=True, check=False)
- if out.returncode == 0:
- install_requires.extend(extras_require["nvidia-gpu"])
- except subprocess.CalledProcessError:
- pass
-
- # Add AMD-GPU
- # This will mostly work only on Linux, amd/rocm-smi is not yet supported on Windows
- try:
- out = subprocess.run(['amd-smi', 'list', '--csv'], shell=True, text=True, capture_output=True, check=False)
- if out.returncode == 0:
- install_requires.extend(extras_require["amd-gpu"])
- except:
- out = subprocess.run(['rocm-smi', 'list', '--csv'], shell=True, text=True, capture_output=True, check=False)
- if out.returncode == 0:
- install_requires.extend(extras_require["amd-gpu"])
- finally:
- pass
-
-
-_add_gpu_requires()
-
-setup(
- name="exo",
- version="0.0.1",
- packages=find_packages(),
- install_requires=install_requires,
- extras_require=extras_require,
- package_data={"exo": ["tinychat/**/*"]},
- entry_points={"console_scripts": ["exo = exo.main:run"]},
-)
diff --git a/test/reconnect.sh b/test/reconnect.sh
deleted file mode 100755
index 1e9a2add..00000000
--- a/test/reconnect.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env bash
-
-echo "Starting node 1"
-DEBUG_DISCOVERY=7 DEBUG=7 python3 main.py --node-id "node1" --listen-port 5678 --broadcast-port 5679 --chatgpt-api-port 52415 --chatgpt-api-response-timeout 900 > output1.log 2>&1 &
-PID1=$!
-echo "Started node 1 PID: $PID1"
-echo "Starting node 2"
-DEBUG_DISCOVERY=7 DEBUG=7 python3 main.py --node-id "node2" --listen-port 5679 --broadcast-port 5678 --chatgpt-api-port 8001 --chatgpt-api-response-timeout 900 > output2.log 2>&1 &
-PID2=$!
-echo "Started node 2 PID: $PID2"
-sleep 5
-kill $PID2
-sleep 5
-echo "Starting node 2 again..."
-DEBUG_DISCOVERY=7 DEBUG=7 python3 main.py --node-id "node2" --listen-port 5679 --broadcast-port 5678 --chatgpt-api-port 8001 --chatgpt-api-response-timeout 900 > output3.log 2>&1 &
-PID2=$!
-sleep 5
-echo "Killing nodes and ending test..."
-kill $PID1
-kill $PID2
-echo "Test complete."
\ No newline at end of file
diff --git a/test/test_model_helpers.py b/test/test_model_helpers.py
deleted file mode 100644
index a8215017..00000000
--- a/test/test_model_helpers.py
+++ /dev/null
@@ -1,121 +0,0 @@
-import unittest
-from exo.models import get_supported_models, model_cards
-from exo.inference.inference_engine import inference_engine_classes
-from typing import NamedTuple
-
-class TestCase(NamedTuple):
- name: str
- engine_lists: list # Will contain short names, will be mapped to class names
- expected_models_contains: list
- min_count: int | None
- exact_count: int | None
- max_count: int | None
-
-# Helper function to map short names to class names
-def expand_engine_lists(engine_lists):
- def map_engine(engine):
- return inference_engine_classes.get(engine, engine) # Return original name if not found
-
- return [[map_engine(engine) for engine in sublist]
- for sublist in engine_lists]
-
-test_cases = [
- TestCase(
- name="single_mlx_engine",
- engine_lists=[["mlx"]],
- expected_models_contains=["llama-3.2-1b", "llama-3.1-70b", "mistral-nemo"],
- min_count=10,
- exact_count=None,
- max_count=None
- ),
- TestCase(
- name="single_tinygrad_engine",
- engine_lists=[["tinygrad"]],
- expected_models_contains=["llama-3.2-1b", "llama-3.2-3b"],
- min_count=5,
- exact_count=None,
- max_count=15
- ),
- TestCase(
- name="multiple_engines_or",
- engine_lists=[["mlx", "tinygrad"], ["mlx"]],
- expected_models_contains=["llama-3.2-1b", "llama-3.2-3b", "mistral-nemo"],
- min_count=10,
- exact_count=None,
- max_count=None
- ),
- TestCase(
- name="multiple_engines_all",
- engine_lists=[["mlx", "tinygrad"], ["mlx", "tinygrad"]],
- expected_models_contains=["llama-3.2-1b", "llama-3.2-3b", "mistral-nemo"],
- min_count=10,
- exact_count=None,
- max_count=None
- ),
- TestCase(
- name="distinct_engine_lists",
- engine_lists=[["mlx"], ["tinygrad"]],
- expected_models_contains=["llama-3.2-1b"],
- min_count=5,
- exact_count=None,
- max_count=15
- ),
- TestCase(
- name="no_engines",
- engine_lists=[],
- expected_models_contains=None,
- min_count=None,
- exact_count=len(model_cards),
- max_count=None
- ),
- TestCase(
- name="nonexistent_engine",
- engine_lists=[["NonexistentEngine"]],
- expected_models_contains=[],
- min_count=None,
- exact_count=0,
- max_count=None
- ),
- TestCase(
- name="dummy_engine",
- engine_lists=[["dummy"]],
- expected_models_contains=["dummy"],
- min_count=None,
- exact_count=1,
- max_count=None
- ),
-]
-
-class TestModelHelpers(unittest.TestCase):
- def test_get_supported_models(self):
- for case in test_cases:
- with self.subTest(f"{case.name}_short_names"):
- result = get_supported_models(case.engine_lists)
- self._verify_results(case, result)
-
- with self.subTest(f"{case.name}_class_names"):
- class_name_lists = expand_engine_lists(case.engine_lists)
- result = get_supported_models(class_name_lists)
- self._verify_results(case, result)
-
- def _verify_results(self, case, result):
- if case.expected_models_contains:
- for model in case.expected_models_contains:
- self.assertIn(model, result)
-
- if case.min_count:
- self.assertGreater(len(result), case.min_count)
-
- if case.exact_count is not None:
- self.assertEqual(len(result), case.exact_count)
-
- # Special case for distinct lists test
- if case.name == "distinct_engine_lists":
- self.assertLess(len(result), 15)
- self.assertNotIn("mistral-nemo", result)
-
- if case.max_count:
- self.assertLess(len(result), case.max_count)
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/test/test_tokenizers.py b/test/test_tokenizers.py
deleted file mode 100644
index 6e1b383e..00000000
--- a/test/test_tokenizers.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import os
-import re
-from transformers import AutoTokenizer, AutoProcessor
-from exo.models import model_cards
-
-
-def test_tokenizer(name, tokenizer, verbose=False):
- print(f"--- {name} ({tokenizer.__class__.__name__}) ---")
- text = "Hello! How can I assist you today? Let me know if you need help with something or just want to chat."
- encoded = tokenizer.encode(text)
- decoded = tokenizer.decode(encoded)
-
- print(f"{encoded=}")
- print(f"{decoded=}")
-
- reconstructed = ""
- for token in encoded:
- if verbose:
- print(f"{token=}")
- print(f"{tokenizer.decode([token])=}")
- reconstructed += tokenizer.decode([token])
- print(f"{reconstructed=}")
-
- strip_tokens = lambda s: s.lstrip(tokenizer.decode([tokenizer.bos_token_id])).rstrip(tokenizer.decode([tokenizer.eos_token_id]))
- assert text == strip_tokens(decoded) == strip_tokens(reconstructed)
-
-ignore = ["TriAiExperiments/SFR-Iterative-DPO-LLaMA-3-70B-R", "mlx-community/DeepSeek-Coder-V2-Lite-Instruct-4bit-mlx", "mlx-community/DeepSeek-V2.5-MLX-AQ4_1_64", "llava-hf/llava-1.5-7b-hf", "mlx-community/Qwen*", "dummy", "mlx-community/Meta-Llama-3.1-405B-Instruct-8bit", "mlx-community/Phi-3.5-mini-instruct-4bit", "mlx-community/phi-4-4bit", "stabilityai/stable-diffusion-2-1-base"]
-ignore_pattern = re.compile(r"^(" + "|".join(model.replace("*", ".*") for model in ignore) + r")")
-models = []
-for model_id in model_cards:
- for engine_type, repo_id in model_cards[model_id].get("repo", {}).items():
- if not ignore_pattern.match(repo_id):
- models.append(repo_id)
-models = list(set(models))
-
-verbose = os.environ.get("VERBOSE", "0").lower() == "1"
-for m in models:
- # TODO: figure out why use_fast=False is giving inconsistent behaviour (no spaces decoding invididual tokens) for Mistral-Large-Instruct-2407-4bit
- # test_tokenizer(m, AutoProcessor.from_pretrained(m, use_fast=False), verbose)
- if m not in ["mlx-community/DeepSeek-R1-4bit", "mlx-community/DeepSeek-R1-3bit", "mlx-community/DeepSeek-V3-4bit", "mlx-community/DeepSeek-V3-3bit"]:
- test_tokenizer(m, AutoProcessor.from_pretrained(m, use_fast=True, trust_remote_code=True), verbose)
- test_tokenizer(m, AutoTokenizer.from_pretrained(m, trust_remote_code=True), verbose)
← 09593c5e backport the dashboard to staging
·
back to Exo
·
github docs 2c16e00b →