Skip to content

Commit

Permalink
Merge pull request #5 from danielgafni/add-stop
Browse files Browse the repository at this point in the history
✨ add Freak.stop method 👷 add CI
  • Loading branch information
danielgafni authored Nov 7, 2023
2 parents 9eee3eb + 01eb90c commit 05a84ae
Show file tree
Hide file tree
Showing 12 changed files with 220 additions and 146 deletions.
117 changes: 117 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: CI

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

on:
workflow_dispatch:
push:
release:
types:
- created
# Sequence of patterns matched against refs/tags
tags:
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10

jobs:
test:
name: test py=${{ matrix.py }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: false
matrix:
os:
- Ubuntu
# MacOs is commented out because: 1. For some reason it's very slow. 2. It never had OS-specific issues
# - MacOs
py:
- "3.11"
- "3.10"
- "3.9"
- "3.8"
steps:
- name: Setup python for test ${{ matrix.py }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.py }}
- uses: actions/checkout@v2
- name: Install and configure Poetry
uses: snok/install-poetry@v1
with:
version: 1.4.2
virtualenvs-create: false
virtualenvs-in-project: false
installer-parallel: true
- name: Install dependencies
run: poetry install --all-extras --sync
- name: Run tests
run: pytest -v .

lint:
name: lint py=${{ matrix.py }} ${{ matrix.os }}
runs-on: ${{ matrix.os }}-latest
strategy:
fail-fast: false
matrix:
os:
- Ubuntu
py:
- "3.11"
- "3.10"
- "3.9"
- "3.8"
steps:
- name: Setup python for test ${{ matrix.py }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.py }}
- uses: actions/checkout@v2
- name: Install and configure Poetry
uses: snok/install-poetry@v1
with:
version: 1.4.2
virtualenvs-create: false
virtualenvs-in-project: false
installer-parallel: true
- name: Install dependencies
run: poetry install --all-extras --sync
- name: Run pre-commit hooks
run: pre-commit run --all-files

qa_success:
name: QA Success
needs: [test, lint]
runs-on: ubuntu-latest
steps:
- name: Success
run: echo "QA Succeeded!"

publish:
needs: qa_success
if: contains(github.ref, 'refs/tags/')
name: Publish to PyPI
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- uses: actions/checkout@v2
- name: Install and configure Poetry
uses: snok/install-poetry@v1
with:
version: 1.4.2
virtualenvs-create: false
virtualenvs-in-project: false
installer-parallel: true
- name: Install poetry-dynamic-versioning
run: poetry self add poetry-dynamic-versioning@0.21.4
- name: Publish to PyPI
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
poetry config pypi-token.pypi $PYPI_TOKEN
poetry publish --build
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ repos:
language: system
pass_filenames: false
# formatter
- id: black
name: black
entry: black .
- id: ruff-format
name: ruff format
entry: ruff format .
language: system
pass_filenames: false
# typecheck
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ curl -X 'PATCH' \

Because Freak is using `FastAPI`, it's possible to use auto-generated documentation to interact with the Freak server. The interactive documentation can be accessed at Freak's main endpoint, which by default is `localhost:4444`.

The following screenshot shows the generated endpoints for the ML [example](https://github.com/danielgafni/freak/blob/master/examples/dl_example.py). Warning: making ML pipelines less reproducible isn't the brightest idea!
The following screenshot shows the generated endpoints for the ML [example](https://github.com/danielgafni/freak/blob/master/examples/dl_example.py). Warning: making ML pipelines less reproducible isn't the brightest idea! But it's convenient to use Freak for stopping a training.

![Sample Generated Docs](https://raw.githubusercontent.com/danielgafni/freak/master/resources/swagger.png)

Expand Down
10 changes: 7 additions & 3 deletions examples/dl_example.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from logging import basicConfig
from time import sleep
from typing import List

from pydantic import BaseModel

from freak import control
from logging import basicConfig


class Head(BaseModel):
Expand All @@ -22,6 +22,7 @@ class Checkpoints(BaseModel):


class State(BaseModel):
training_stopped: bool = False
lr: float = 1e-3
checkpoints: Checkpoints = Checkpoints()
model: Model = Model()
Expand All @@ -39,11 +40,14 @@ def epoch_loop(config: State, current_epoch: int):
basicConfig(level="INFO")

state = State()
control(state)
server = control(state)

current_epoch = 0

while True:
while not state.training_stopped:
print(f"state: {state}")
epoch_loop(state, current_epoch)
current_epoch += 1
else:
print("Training stopped!")
server.stop()
4 changes: 2 additions & 2 deletions freak/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from freak.freak import control, Freak
from freak.__version__ import __version__
from freak._version import __version__
from freak.freak import Freak, control

__all__ = ["control", "Freak", "__version__"]
1 change: 0 additions & 1 deletion freak/__version__.py

This file was deleted.

1 change: 1 addition & 0 deletions freak/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.0.0"
30 changes: 19 additions & 11 deletions freak/freak.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
from typing import TypeVar, Any, Optional, List
import operator
from logging import getLogger
from typing import Any, List, Optional, TypeVar

from fastapi import FastAPI, APIRouter
from fastapi import APIRouter, FastAPI, Query
from pydantic import BaseModel

from starlette.requests import Request
from starlette.responses import JSONResponse
from uvicorn import Config
from logging import getLogger
from starlette.requests import Request
from fastapi import Query
import operator

from freak.uvicorn_threaded import UvicornServer
from freak.uvicorn_threaded import ThreadedUvicorn

logger = getLogger(__name__)

Expand Down Expand Up @@ -83,6 +81,8 @@ def __init__(
self.port = port
self.uvicorn_log_level = uvicorn_log_level

self.should_stop = False

def control(self, state: T, serve: bool = True):
if not state.Config.allow_mutation:
state.Config.allow_mutation = True
Expand All @@ -96,11 +96,19 @@ def control(self, state: T, serve: bool = True):
self.serve()

def serve(self):
server = UvicornServer(
self.server = ThreadedUvicorn(
config=Config(app=self.app, host=self.host, port=self.port, log_level=self.uvicorn_log_level)
)
server.run_in_thread()
# logger.info(f"Running Freak on http://{self.host}:{self.port}")
self.server.start()
logger.info(f"Running Freak at {self.host}:{self.port}")

def stop(self):
logger.info("Stopping Freak Server")
self.server.stop()

@property
def running(self) -> bool:
return self.server.thread.is_alive()

def add_routes(self, app: FastAPI, state: T) -> FastAPI:
init_state = state.copy(deep=True)
Expand Down
77 changes: 18 additions & 59 deletions freak/uvicorn_threaded.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,26 @@
# taken from https://github.com/encode/uvicorn/discussions/1103#discussioncomment-6187606

import asyncio
import threading
import time

import uvicorn


# this code is taken from freqtrade


def asyncio_setup() -> None: # pragma: no cover
# Set eventloop for win32 setups
# Reverts a change done in uvicorn 0.15.0 - which now sets the eventloop
# via policy.
import sys

if sys.version_info >= (3, 8) and sys.platform == "win32":
import asyncio
import selectors

selector = selectors.SelectSelector()
loop = asyncio.SelectorEventLoop(selector)
asyncio.set_event_loop(loop)


class UvicornServer(uvicorn.Server):
"""
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
class ThreadedUvicorn:
def __init__(self, config: uvicorn.Config):
self.server = uvicorn.Server(config)
self.thread = threading.Thread(daemon=True, target=self.server.run)

Removed install_signal_handlers() override based on changes from this commit:
https://github.com/encode/uvicorn/commit/ce2ef45a9109df8eae038c0ec323eb63d644cbc6
Cannot rely on asyncio.get_event_loop() to create new event loop because of this check:
https://github.com/python/cpython/blob/4d7f11e05731f67fd2c07ec2972c6cb9861d52be/Lib/asyncio/events.py#L638
Fix by overriding run() and forcing creation of new event loop if uvloop is available
"""

def run(self, sockets=None):
import asyncio

"""
Parent implementation calls self.config.setup_event_loop(),
but we need to create uvloop event loop manually
"""
try:
import uvloop # pyright: ignore[reportMissingImports]
except ImportError: # pragma: no cover
asyncio_setup()
else:
asyncio.set_event_loop(uvloop.new_event_loop())
try:
loop = asyncio.get_running_loop()
except RuntimeError:
# When running in a thread, we'll not have an eventloop yet.
loop = asyncio.new_event_loop()

loop.run_until_complete(self.serve(sockets=sockets))

def run_in_thread(self):
self.thread = threading.Thread(target=self.run)
def start(self):
self.thread.start()
while not self.started:
time.sleep(1e-3)
asyncio.run(self.wait_for_started())

async def wait_for_started(self):
while not self.server.started:
await asyncio.sleep(0.1)

def cleanup(self):
self.should_exit = True
self.thread.join()
def stop(self):
if self.thread.is_alive():
self.server.should_exit = True
while self.thread.is_alive():
continue
Loading

0 comments on commit 05a84ae

Please # to comment.