├── .gitignore ├── Dockerfile ├── README.md ├── app.py ├── config.example.json ├── docker-compose.yaml ├── init.config ├── requirements.txt └── scripts └── init.sh /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .allorad 3 | .cache 4 | config.json 5 | env_file 6 | worker-data -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amd64/python:3.9-buster 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | # Install any needed packages specified in requirements.txt 8 | RUN pip install --upgrade pip \ 9 | && pip install -r requirements.txt 10 | 11 | EXPOSE 8000 12 | 13 | ENV NAME sample 14 | 15 | # Run gunicorn when the container launches and bind port 8000 from app.py 16 | CMD ["gunicorn", "-b", ":8000", "app:app"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Walkthrough: Deploying a Hugging Face Model as a Worker Node on the Allora Network 2 | 3 | This guide provides a step-by-step process to deploy a Hugging Face model as a Worker Node within the [Allora Network](https://docs.allora.network/). By following these instructions, you will be able to integrate and run models from Hugging Face, contributing to the Allora decentralized machine intelligence ecosystem. 4 | 5 | See [complete walkthrough and instructions here](https://docs.allora.network/devs/workers/walkthroughs/walkthrough-hugging-face-worker). 6 | 7 | --- 8 | ## Components 9 | 10 | - **Worker**: The node that publishes inferences to the Allora chain. 11 | - **Inference**: A container that conducts inferences, maintains the model state, and responds to internal inference requests via a Flask application. This node operates with a basic linear regression model for price predictions. 12 | 13 | Check the `docker-compose.yml` file for the detailed setup of each component. 14 | 15 | ## Docker-Compose Setup 16 | 17 | A complete working example is provided in the `docker-compose.yml` file. 18 | 19 | ### Steps to Setup 20 | 21 | 1. **Clone the Repository** 22 | 2. **Copy and Populate Configuration** 23 | 24 | Copy the example configuration file and populate it with your variables: 25 | ```sh 26 | cp config.example.json config.json 27 | ``` 28 | 29 | 3. **Initialize Worker** 30 | 31 | Run the following commands from the project's root directory to initialize the worker: 32 | ```sh 33 | chmod +x init.config 34 | ./init.config 35 | ``` 36 | These commands will: 37 | - Automatically create Allora keys for your worker. 38 | - Export the needed variables from the created account to be used by the worker node, bundle them with your provided `config.json`, and pass them to the node as environment variables. 39 | 40 | 4. **Faucet Your Worker Node** 41 | 42 | You can find the offchain worker node's address in `./worker-data/env_file` under `ALLORA_OFFCHAIN_ACCOUNT_ADDRESS`. [Add faucet funds](https://docs.allora.network/devs/get-started/setup-wallet#add-faucet-funds) to your worker's wallet before starting it. 43 | 44 | 5. **Start the Services** 45 | 46 | Run the following command to start the worker node, inference, and updater nodes: 47 | ```sh 48 | docker compose up --build 49 | ``` 50 | To confirm that the worker successfully sends the inferences to the chain, look for the following log: 51 | ``` 52 | {"level":"debug","msg":"Send Worker Data to chain","txHash":,"time":,"message":"Success"} 53 | ``` 54 | 55 | ## Testing Inference Only 56 | 57 | This setup allows you to develop your model without the need to bring up the offchain worker. To test the inference model only: 58 | 59 | 1. Run the following command to start the inference node: 60 | ```sh 61 | docker compose up --build inference 62 | ``` 63 | 64 | 2. Send requests to the inference model. For example, request ETH price inferences: 65 | 66 | ```sh 67 | curl http://127.0.0.1:8000/inference/ETH 68 | ``` 69 | Expected response: 70 | ```json 71 | {"value":"2564.021586281073"} 72 | ``` 73 | 74 | 3. Update the node's internal state (download pricing data, train, and update the model): 75 | 76 | ```sh 77 | curl http://127.0.0.1:8000/update 78 | ``` 79 | Expected response: 80 | ```sh 81 | 0 82 | ``` 83 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Response 2 | import requests 3 | import json 4 | import pandas as pd 5 | import torch 6 | from chronos import ChronosPipeline 7 | 8 | # create our Flask app 9 | app = Flask(__name__) 10 | 11 | # define the Hugging Face model we will use 12 | model_name = "amazon/chronos-t5-tiny" 13 | 14 | def get_coingecko_url(token): 15 | base_url = "https://api.coingecko.com/api/v3/coins/" 16 | token_map = { 17 | 'ETH': 'ethereum', 18 | 'SOL': 'solana', 19 | 'BTC': 'bitcoin', 20 | 'BNB': 'binancecoin', 21 | 'ARB': 'arbitrum' 22 | } 23 | 24 | token = token.upper() 25 | if token in token_map: 26 | url = f"{base_url}{token_map[token]}/market_chart?vs_currency=usd&days=30&interval=daily" 27 | return url 28 | else: 29 | raise ValueError("Unsupported token") 30 | 31 | # define our endpoint 32 | @app.route("/inference/") 33 | def get_inference(token): 34 | """Generate inference for given token.""" 35 | try: 36 | # use a pipeline as a high-level helper 37 | pipeline = ChronosPipeline.from_pretrained( 38 | model_name, 39 | device_map="auto", 40 | torch_dtype=torch.bfloat16, 41 | ) 42 | except Exception as e: 43 | return Response(json.dumps({"pipeline error": str(e)}), status=500, mimetype='application/json') 44 | 45 | try: 46 | # get the data from Coingecko 47 | url = get_coingecko_url(token) 48 | except ValueError as e: 49 | return Response(json.dumps({"error": str(e)}), status=400, mimetype='application/json') 50 | 51 | headers = { 52 | "accept": "application/json", 53 | "x-cg-demo-api-key": "" # replace with your API key 54 | } 55 | 56 | response = requests.get(url, headers=headers) 57 | if response.status_code == 200: 58 | data = response.json() 59 | df = pd.DataFrame(data["prices"]) 60 | df.columns = ["date", "price"] 61 | df["date"] = pd.to_datetime(df["date"], unit='ms') 62 | df = df[:-1] # removing today's price 63 | print(df.tail(5)) 64 | else: 65 | return Response(json.dumps({"Failed to retrieve data from the API": str(response.text)}), 66 | status=response.status_code, 67 | mimetype='application/json') 68 | 69 | # define the context and the prediction length 70 | context = torch.tensor(df["price"]) 71 | prediction_length = 1 72 | 73 | try: 74 | forecast = pipeline.predict(context, prediction_length) # shape [num_series, num_samples, prediction_length] 75 | print(forecast[0].mean().item()) # taking the mean of the forecasted prediction 76 | return Response(str(forecast[0].mean().item()), status=200) 77 | except Exception as e: 78 | return Response(json.dumps({"error": str(e)}), status=500, mimetype='application/json') 79 | 80 | # run our Flask app 81 | if __name__ == '__main__': 82 | app.run(host="0.0.0.0", port=8000, debug=True) 83 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "wallet": { 3 | "addressKeyName": "test", 4 | "addressRestoreMnemonic": "", 5 | "alloraHomeDir": "", 6 | "gas": "auto", 7 | "gasAdjustment": 1.5, 8 | "nodeRpc": "https://allora-rpc.testnet.allora.network", 9 | "maxRetries": 5, 10 | "retryDelay": 3, 11 | "accountSequenceRetryDelay": 5, 12 | "submitTx": true 13 | }, 14 | "worker": [ 15 | { 16 | "topicId": 4, 17 | "inferenceEntrypointName": "api-worker-reputer", 18 | "loopSeconds": 5, 19 | "parameters": { 20 | "InferenceEndpoint": "http://inference:8000/inference/{Token}", 21 | "Token": "BTC" 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | inference: 3 | container_name: inference-hf 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | command: python -u /app/app.py 8 | ports: 9 | - "8000:8000" 10 | 11 | worker: 12 | container_name: worker 13 | image: alloranetwork/allora-offchain-node:v0.5.0 14 | volumes: 15 | - ./worker-data:/data 16 | depends_on: 17 | - inference 18 | env_file: 19 | - ./worker-data/env_file 20 | 21 | volumes: 22 | inference-data: 23 | worker-data: -------------------------------------------------------------------------------- /init.config: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ ! -f config.json ]; then 6 | echo "Error: config.json file not found, please provide one" 7 | exit 1 8 | fi 9 | 10 | nodeName=$(jq -r '.wallet.addressKeyName' config.json) 11 | if [ -z "$nodeName" ]; then 12 | echo "No wallet name provided for the node, please provide your preferred wallet name. config.json >> wallet.addressKeyName" 13 | exit 1 14 | fi 15 | 16 | # Ensure the worker-data directory exists 17 | mkdir -p ./worker-data 18 | 19 | json_content=$(cat ./config.json) 20 | stringified_json=$(echo "$json_content" | jq -c .) 21 | 22 | mnemonic=$(jq -r '.wallet.addressRestoreMnemonic' config.json) 23 | if [ -n "$mnemonic" ]; then 24 | echo "ALLORA_OFFCHAIN_NODE_CONFIG_JSON='$stringified_json'" > ./worker-data/env_file 25 | echo "NAME=$nodeName" >> ./worker-data/env_file 26 | echo "ENV_LOADED=true" >> ./worker-data/env_file 27 | 28 | echo "wallet mnemonic already provided by you, loading config.json . Please proceed to run docker compose" 29 | exit 1 30 | fi 31 | 32 | if [ ! -f ./worker-data/env_file ]; then 33 | echo "ENV_LOADED=false" > ./worker-data/env_file 34 | fi 35 | 36 | ENV_LOADED=$(grep '^ENV_LOADED=' ./worker-data/env_file | cut -d '=' -f 2) 37 | if [ "$ENV_LOADED" = "false" ]; then 38 | json_content=$(cat ./config.json) 39 | stringified_json=$(echo "$json_content" | jq -c .) 40 | 41 | docker run -it --entrypoint=bash -v $(pwd)/worker-data:/data -v $(pwd)/scripts:/scripts -e NAME="${nodeName}" -e ALLORA_OFFCHAIN_NODE_CONFIG_JSON="${stringified_json}" alloranetwork/allora-chain:latest -c "bash /scripts/init.sh" 42 | echo "config.json saved to ./worker-data/env_file" 43 | else 44 | echo "config.json is already loaded, skipping the operation. You can set ENV_LOADED variable to false in ./worker-data/env_file to reload the config.json" 45 | fi -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask[async] 2 | gunicorn[gthread] 3 | transformers[torch] 4 | pandas 5 | python-dotenv 6 | git+https://github.com/amazon-science/chronos-forecasting.git -------------------------------------------------------------------------------- /scripts/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if allorad keys --home=/data/.allorad --keyring-backend test show $NAME > /dev/null 2>&1 ; then 6 | echo "allora account: $NAME already imported" 7 | else 8 | echo "creating allora account: $NAME" 9 | output=$(allorad keys add $NAME --home=/data/.allorad --keyring-backend test 2>&1) 10 | address=$(echo "$output" | grep 'address:' | sed 's/.*address: //') 11 | mnemonic=$(echo "$output" | tail -n 1) 12 | 13 | # Parse and update the JSON string 14 | updated_json=$(echo "$ALLORA_OFFCHAIN_NODE_CONFIG_JSON" | jq --arg name "$NAME" --arg mnemonic "$mnemonic" ' 15 | .wallet.addressKeyName = $name | 16 | .wallet.addressRestoreMnemonic = $mnemonic 17 | ') 18 | 19 | stringified_json=$(echo "$updated_json" | jq -c .) 20 | 21 | echo "ALLORA_OFFCHAIN_NODE_CONFIG_JSON='$stringified_json'" > /data/env_file 22 | echo ALLORA_OFFCHAIN_ACCOUNT_ADDRESS=$address >> /data/env_file 23 | echo "NAME=$NAME" >> /data/env_file 24 | 25 | echo "Updated ALLORA_OFFCHAIN_NODE_CONFIG_JSON saved to /data/env_file" 26 | fi 27 | 28 | 29 | if grep -q "ENV_LOADED=false" /data/env_file; then 30 | sed -i 's/ENV_LOADED=false/ENV_LOADED=true/' /data/env_file 31 | else 32 | echo "ENV_LOADED=true" >> /data/env_file 33 | fi 34 | --------------------------------------------------------------------------------